diff --git a/AGENTS.md b/AGENTS.md index 2b9917dc..ef87f5e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -385,6 +385,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건). diff --git a/CHANGELOG.md b/CHANGELOG.md index 12601015..345455ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ ## [7.0.7] - 2026-08-11 +### 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) + ### Added - 레이아웃 편집기 첨부 파일 업로드에 업로드 전/후 액션 훅과 파일 가공 필터 훅 제공 — 확장에서 다른 업로드 경로와 동일하게 개입할 수 있습니다. diff --git a/app/Contracts/Repositories/MenuRepositoryInterface.php b/app/Contracts/Repositories/MenuRepositoryInterface.php index 14b26a0d..4918dfe4 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; + /** * 슬러그로 메뉴를 찾습니다. * 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 @@ + 신뢰 호스트명 목록 (예: ['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 !== '' + )); + } + /** * 레이아웃 확장 파일 경로 반환 * diff --git a/app/Extension/AbstractPlugin.php b/app/Extension/AbstractPlugin.php index 00b1749f..0a7498bb 100644 --- a/app/Extension/AbstractPlugin.php +++ b/app/Extension/AbstractPlugin.php @@ -771,6 +771,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 !== '' + )); + } + /** * 레이아웃 확장 파일 경로 반환 * 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/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/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/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/UserController.php b/app/Http/Controllers/Api/Admin/UserController.php index 65baa1b1..de1f4c02 100644 --- a/app/Http/Controllers/Api/Admin/UserController.php +++ b/app/Http/Controllers/Api/Admin/UserController.php @@ -3,6 +3,8 @@ namespace App\Http\Controllers\Api\Admin; use App\Exceptions\CannotDeleteSuperAdminException; +use App\Exceptions\CannotModifySuperAdminException; +use App\Exceptions\PermissionEscalationException; use App\Http\Controllers\Api\Base\AdminBaseController; use App\Http\Requests\User\BulkUpdateUserStatusRequest; use App\Http\Requests\User\CheckEmailRequest; @@ -74,6 +76,8 @@ class UserController extends AdminBaseController new UserResource($user), 201 ); + } catch (PermissionEscalationException $e) { + return $this->error('exceptions.cannot_grant_unheld_permission', 403); } catch (ValidationException $e) { return $this->error('user.create_failed', 422, $e->errors()); } catch (Exception $e) { @@ -122,6 +126,10 @@ 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) { @@ -148,6 +156,8 @@ 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()]); } 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/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/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/Repositories/MenuRepository.php b/app/Repositories/MenuRepository.php index b9d5b17b..d4179169 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 Menu::query()->whereRaw('1 = 0')->get(); + } + + return Menu::whereIn('id', $ids)->get(); + } + /** * 슬러그로 메뉴를 찾습니다. * 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/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/Services/AttachmentService.php b/app/Services/AttachmentService.php index 2821cd4a..e9d5864a 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; @@ -231,6 +232,8 @@ class AttachmentService */ public function reorder(array $orderData): void { + $this->assertReorderWithinScope($orderData); + // Before 훅 HookManager::doAction('core.attachment.before_reorder', $orderData); @@ -240,6 +243,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 $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')); + } + } + /** * 다운로드 응답 생성 * diff --git a/app/Services/MenuService.php b/app/Services/MenuService.php index ce1e4839..c1c457c1 100644 --- a/app/Services/MenuService.php +++ b/app/Services/MenuService.php @@ -7,8 +7,10 @@ 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; @@ -203,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); @@ -227,6 +237,8 @@ class MenuService */ public function updateMenuOrderWithHierarchy(array $orderData): bool { + $this->assertOrderTargetsWithinScope($orderData); + $result = $this->menuRepository->updateOrderWithHierarchy($orderData); // 훅: 메뉴 계층 순서 변경 후 @@ -235,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 $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/RoleService.php b/app/Services/RoleService.php index 4e3fef88..fc3eb827 100644 --- a/app/Services/RoleService.php +++ b/app/Services/RoleService.php @@ -3,11 +3,13 @@ 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; @@ -17,7 +19,8 @@ use Illuminate\Support\Str; class RoleService { public function __construct( - private RoleRepositoryInterface $roleRepository + private RoleRepositoryInterface $roleRepository, + private PermissionEscalationGuard $escalationGuard ) {} /** @@ -105,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); @@ -134,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); @@ -213,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(); @@ -244,6 +272,9 @@ class RoleService */ public function toggleRoleStatus(Role $role): bool { + // 보호된 역할(코어/확장 소유) 상태변경 상한: 삭제 경로와 대칭. + $this->assertActorMayModifyRole($role); + $newStatus = ! $role->is_active; // 훅: 상태 변경 전 @@ -259,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/UserService.php b/app/Services/UserService.php index bcaba6dd..74e69c9a 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -6,12 +6,15 @@ 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; @@ -26,7 +29,8 @@ class UserService public function __construct( private UserRepositoryInterface $userRepository, private RoleRepositoryInterface $roleRepository, - private AttachmentService $attachmentService + private AttachmentService $attachmentService, + private PermissionEscalationGuard $escalationGuard ) {} /** @@ -79,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); @@ -102,6 +115,11 @@ class UserService throw $e; } + // 권한 상승 상한 위반은 그대로 전파해 컨트롤러가 403 으로 매핑하도록 한다. + if ($e instanceof PermissionEscalationException) { + throw $e; + } + throw ValidationException::withMessages([ 'general' => [__('user.create_failed', ['error' => $e->getMessage()])], ]); @@ -119,6 +137,10 @@ class UserService */ public function updateUser(User $user, array $data): User { + // 등급 상한 가드: 비-슈퍼관리자 액터는 슈퍼 관리자 계정을 수정할 수 없다. + // (비밀번호·status(withdrawn/blocked)·email 등 모든 수정 경로를 한 지점에서 차단) + UserGradeGuard::assertActorMayModify($user); + try { // 원본 데이터 보관 (after_update 훅에서 사용) $originalData = $data; @@ -148,17 +170,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 역할을 가진 다른 사용자가 있는지 확인 @@ -261,6 +301,12 @@ class UserService throw $e; } + // 권한 상승 상한 위반은 그대로 전파해 컨트롤러가 403 으로 매핑하도록 한다 + // (일반 실패로 감싸면 422 로 잘못 내려간다). + if ($e instanceof PermissionEscalationException) { + throw $e; + } + throw ValidationException::withMessages([ 'general' => [__('user.update_failed', ['error' => $e->getMessage()])], ]); @@ -589,22 +635,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; - } - /** * 사용자의 활동 로그를 조회합니다. * @@ -640,6 +670,9 @@ class UserService */ public function unlockAccount(User $user): User { + // 등급 상한 가드: 비-슈퍼관리자 액터는 슈퍼 관리자 계정을 조작할 수 없다(정합성). + UserGradeGuard::assertActorMayModify($user); + HookManager::doAction('core.user.before_unlock', $user); $this->userRepository->resetLoginAttempts($user); @@ -653,19 +686,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(); - } - /** * 사용자의 언어 설정을 업데이트합니다. * @@ -698,14 +718,38 @@ class UserService $statusEnum = UserStatus::from($status); + // 정적 라우트(`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) { - return $this->bulkWithdraw($uuids, $status); + $modifiableUuids = array_map(static fn (User $u): string => $u->uuid, $modifiable); + + return $this->bulkWithdraw($modifiableUuids, $status); } - // UUID → 정수 ID 변환 (내부 쿼리용) - $userIds = $this->userRepository->getIdsByUuids($uuids); + $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]; + } // DB 트랜잭션으로 일괄 업데이트 $updatedCount = DB::transaction(function () use ($userIds, $statusEnum) { 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/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 @@ + $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 $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/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 @@ + 중복 제거된 호스트명 목록 (소문자) + */ + 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|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 @@ +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 $targets 대상 사용자 목록 + * @param User|null $actor 행위자(미지정 시 현재 인증 사용자) + * @return array 수정 가능한 대상 목록 + */ + 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 @@ 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/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 레퍼런스 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` 에 필드별 메시지) | @@ -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` 내부) - +_단건 응답: `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 +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` 초과 (로그인과 같은 제한을 공유하므로 코드 대입 시도도 함께 억제된다) | 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` 에 필드별 메시지) | 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`)이 없는 경우 | @@ -323,7 +323,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 | @@ -395,7 +395,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | @@ -481,7 +481,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | diff --git a/docs/backend/api/identity.md b/docs/backend/api/identity.md index a18027c8..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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | 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` 내부) - +이 엔드포인트는 `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` 키는 병합 후 결과에는 남지 않는다 (병합 입력으로만 쓰인다). 실제 포함되는 키 집합은 저장된 레이아웃 내용에 따라 달라진다. **응답 예시** - +```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 다 | -**설명** +**설명** + +레이아웃 편집기에서 **저장하지 않은 편집 중 내용**을 실제 화면으로 확인하기 위한 미리보기 서빙 엔드포인트다. 인증이 아니라 **토큰 자체가 보안 메커니즘**이며(`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` 내부) - +확장자 없는 형태(`GET /api/layouts/preview/{token}`)와 **같은 컨트롤러 메서드**이므로 응답이 동일하다. `ResponseHelper` 봉투 없이 병합·확장 적용이 끝난 **레이아웃 JSON 자체**를 최상위로 반환한다. 필드 표는 위 항목을 참조한다. **응답 예시** - +```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 가 되므로, 이 경우 확장자 없는 형태로 재요청한다 | -**설명** +**설명** + +확장자 붙은 형태의 미리보기 서빙이다. 동작·토큰 수명(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 29d92e8f..5f5323e2 100644 --- a/docs/backend/api/me.md +++ b/docs/backend/api/me.md @@ -369,7 +369,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` 에 필드별 메시지) | @@ -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` 가 자기 자신·자손을 가리키는 순환 참조인 경우 포함 | @@ -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` 에 필드별 메시지) | @@ -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` 에 사유) | @@ -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` 에 필드별 메시지) | @@ -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` 내부) - +_이 엔드포인트는 표준 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 +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` 내부) - +_이 엔드포인트는 표준 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 +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` 가 반환된다. **에러 응답** -_대표 에러 없음 (공개 조회). _ +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 200 | OK (빈 본문) | 활성 확장이 없거나 병합할 에셋이 없는 경우 — 오류가 아니라 빈 번들이다 | + +> 개별 확장의 병합이 실패하면 그 확장만 건너뛰고 나머지는 그대로 병합된다(실패 격리). 건너뛴 사실은 서버 로그(warning)에 남는다. @@ -2172,15 +2207,36 @@ Accept: application/json **응답 필드** (`data` 내부) - +_이 엔드포인트는 표준 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 +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` 가 반환된다. **에러 응답** -_대표 에러 없음 (공개 조회). _ +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 200 | OK (빈 본문) | 활성 확장이 없거나 병합할 에셋이 없는 경우 — 오류가 아니라 빈 번들이다 | + +> 개별 확장의 병합이 실패하면 그 확장만 건너뛰고 나머지는 그대로 병합된다(실패 격리). 건너뛴 사실은 서버 로그(warning)에 남는다. @@ -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 +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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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/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` 에 필드별 메시지) | 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` 에 필드별 메시지) | @@ -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 +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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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` 에 필드별 메시지) | @@ -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/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`)이 없는 경우 | @@ -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`) | diff --git a/docs/backend/api/settings.md b/docs/backend/api/settings.md index c2f7a7b3..a7157a6b 100644 --- a/docs/backend/api/settings.md +++ b/docs/backend/api/settings.md @@ -72,7 +72,7 @@ _단건 응답: `data` 객체의 필드._ | 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/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 검증이 같은 값을 공유) | +| _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 등 — 권한 맵 기반) | **응답 예시** @@ -510,7 +510,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -623,7 +623,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 500 | Internal Server Error | 백업 파일 생성에 실패한 경우 (`settings.backup_failed`) | @@ -672,7 +672,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | --- | --- | --- | | 400 | Bad Request | 백업이 수행되지 않은 경우 (`settings.backup_failed`) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 500 | Internal Server Error | 백업 처리 중 예외가 발생한 경우 (`settings.backup_error`) | @@ -721,7 +721,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`) | @@ -782,7 +782,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`) 또는 다운로드·압축 해제 실패 | @@ -836,7 +836,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | @@ -898,7 +898,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`) | @@ -957,7 +957,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`) | @@ -1007,7 +1007,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}` | PHP OPcache 상태 — `loaded`(확장 로드 여부) / `enabled`(런타임 활성화 여부, `opcache.enable` 설정 기준) | +| 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`) | @@ -1236,7 +1236,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`) | @@ -1330,7 +1330,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 인증 실패·연결 거부 등)가 담김 | @@ -1449,7 +1449,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` 내부) - +이 엔드포인트는 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 +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'; +``` **에러 응답** -_대표 에러 없음 (공개 조회). _ +_에러 응답이 정의되어 있지 않다. 이 라우트는 DB 에 접근하지 않으므로 설치 전에도 응답하며, 도달하기만 하면 항상 `200` 이다. 도달하지 못해 `404`/`5xx` 가 관측되면 그것은 에러가 아니라 **판정 입력**이다 (아래 설명 참조)._ -**설명** +**설명** + +서버(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` 내부) - +확장자 없는 대조군(`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` | 성공 판정용. 상태코드가 아니라 **이 토큰의 존재**로 판정한다 | **응답 예시** - +애플리케이션까지 도달했을 때 (`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 가 된다. **에러 응답** -_대표 에러 없음 (공개 조회). _ +_에러 응답이 정의되어 있지 않다. 이 라우트는 DB 에 접근하지 않으므로 애플리케이션에 도달하면 항상 `200` 이다. `404`/`5xx` 는 에러가 아니라 **판정 입력**이며, 대조군이 성공했다면 `extensionless` 모드로 확정한다._ -**설명** +**설명** + +자산 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..3a25bdec 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`) | @@ -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` 내부) - +_`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 +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` 내부) - +_이 엔드포인트는 표준 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 +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` 내부) - +_`data` 는 템플릿의 `components.json` 내용을 그대로 담은 **컴포넌트 맵**이다 (고정 필드 집합이 아니라 컴포넌트명 → 정의 매핑)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| (컴포넌트명) | object | `{"type":"basic","tag":"div"}` | 컴포넌트 정의. 키는 레이아웃 JSON 의 `name` 과 일치한다 | + +> 활성/`_bundled` 어디에도 `components.json` 이 없으면 `404`(`templates.layout_not_found`)다. **응답 예시** - +```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` 내부) - +_`data` 객체의 필드 (`EditorSpecAssembler::assemble`)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-admin_basic` | 요청한 템플릿 식별자 (요청값 반향) | +| spec | object \| null | `{"palette":[…],"styleControls":{…}}` | 합본된 편집기 스펙. 분할 매니페스트(`editor-spec/` + `$include`)는 **활성 디렉토리 기준**으로 합쳐지며(`_bundled` 폴백 없음), 미분할 원본 파일은 그대로 반환된다. 스펙이 없으면 `null` (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` 내부) - +_`data` 객체의 필드._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-admin_basic` | 요청한 템플릿 식별자 (요청값 반향) | +| permissions | array | `[{"identifier":"core.users.read","name":"사용자 조회"}]` | 레이아웃 조건식에 쓸 수 있는 권한 후보 목록 (현재 로케일로 해석된 표시명 포함) | **응답 예시** - +```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` 내부) - +_`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 +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 | 아니오 | — | | -| page_type | query | string | 아니오 | — | | +| 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` 내부) - +_`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 +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` 내부) - +_이 엔드포인트는 표준 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 +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 +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 조회했습니다.", + "data": { + "Card": { "type": "composite", "props": { "title": "string" } } + } +} +``` **에러 응답** @@ -97396,11 +97573,35 @@ Accept: application/json **응답 필드** (`data` 내부) - +_`data` 는 템플릿의 `template.json` 매니페스트 내용이다 (고정 필드 집합이 아니라 매니페스트 그대로)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-basic` | 템플릿 식별자 | +| version | string | `1.1.1` | 템플릿 버전 | +| type | string | `user` | 템플릿 유형 (`admin`/`user`) | +| (그 외 매니페스트 키) | mixed | — | `template.json` 이 선언한 나머지 키가 그대로 실린다 | + +> **활성 템플릿만** 조회된다 — 비활성이거나 매니페스트가 없으면 `404`. 응답은 1시간 캐시된다. **응답 예시** - +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 조회했습니다.", + "data": { + "identifier": "sirsoft-basic", + "name": "Sirsoft Basic", + "version": "1.1.1", + "type": "user" + } +} +``` **에러 응답** @@ -189529,11 +189730,37 @@ Accept: application/json **응답 필드** (`data` 내부) - +_`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 +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 22c638e3..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` 에 예외 메시지) | @@ -456,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`) | @@ -1018,17 +1018,19 @@ HTTP/1.1 200 ```json { "success": true, - "message": "사용자가 삭제되었습니다.", + "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` 에 필드별 메시지) | @@ -1111,7 +1113,7 @@ _단건 응답: `data` 객체의 필드._ | withdrawn_at | null | `null` | withdrawn 일시 | | blocked_at | null | `null` | blocked 일시 | | failed_login_attempts | integer | `0` | 연속 로그인 실패 횟수 | -| locked_permanently | boolean | `false` | 무기한 잠금 여부 (보안 설정의 잠금 시간이 `0` 이면 자동 해제 없이 관리자가 직접 풀어야 한다) | +| locked_permanently | boolean | `false` | 영구 잠금 여부. true 면 `locked_until` 과 무관하게 잠금이 유지되며, 해제는 성공 로그인 또는 관리자의 잠금 해제로만 이뤄진다 (잠금 시간 설정이 `0`= 무기한일 때 세워진다) | | locked_until | null | `null` | 계정 잠금 해제 시각 (NULL = 잠금 없음) | | is_locked | boolean | `false` | locked 여부 | | notify_post_complete | boolean | `false` | 게시글 작성 완료 알림 수신 여부 (게시판 모듈 알림 설정) | @@ -1446,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/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/extension/module-assets.md b/docs/extension/module-assets.md index abaee990..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`)도 동일 필드를 지원합니다. #### 에셋 필드 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/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/lang-packs/_bundled/g7-core-ja/CHANGELOG.md b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md index 749b9dca..99917d75 100644 --- a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.0.6] - 2026-08-12 +## [1.0.6] - 2026-08-13 ### Added @@ -14,6 +14,8 @@ - 웹소켓 서버(백엔드 발송용) endpoint 연결 실패 안내 일본어 번역 추가 (`settings.websocket_server_test_failed`). - 공개 자산 스토리지(공개 이미지 직접 URL 서빙) 설정의 드라이버 라벨과 검증 메시지 일본어 번역을 추가했습니다. 환경설정 > 드라이버 탭의 새 설정이 일본어 로케일에서 자연스럽게 표시됩니다. - 설정 항목 단위 저장의 값 형식 안내 일본어 번역을 추가했습니다 (`validation.setting.value.*`) — 켜기/끄기·숫자 설정에 맞지 않는 값을 저장할 때의 안내가 일본어 로케일에서 표시됩니다. +- 슈퍼 관리자 계정·역할 수정 상한 및 권한 부여 상한 위반 시 표시되는 예외 메시지(`cannot_modify_super_admin`, `cannot_grant_unheld_permission`, `cannot_modify_protected_role`)의 일본어 번역을 추가했습니다. +- 레이아웃 저장 시 위험 표현식·외부 리소스 URL 거부 안내(`layout.dangerous_expression`, `layout.external_resource_url`)의 일본어 번역을 추가했습니다. ### Changed 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/validation.php b/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php index 81bb1823..ba6553f3 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 フィールドは文字列である必要があります。', 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 0ee29cfc..283e70bf 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/CHANGELOG.md @@ -9,6 +9,7 @@ ### Added - 게시판 유형 삭제 등 관리 작업이 서버 오류로 실패했을 때 표시되는 안내 문구의 일본어 번역을 추가했습니다. +- 비공개(비밀) 게시글 목록 마스킹 시 표시되는 제목 플레이스홀더(`secret_post_title`)의 일본어 번역을 추가했습니다. ### Changed 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 56f6afea..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' => '管理者によってブロック処理された投稿です。', 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/validation.php b/lang/en/validation.php index ab66bcaf..18a1ed1b 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.', 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/validation.php b/lang/ko/validation.php index b69a2281..760fc033 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 필드는 문자열이어야 합니다.', 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` 내부) - +_목록 응답: `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 +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` 에 예외 메시지) | -**설명** +**설명** + +메모 목록을 페이지 단위로 조회한다. 학습용 샘플 모듈의 표준 목록 엔드포인트로, 코어의 `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` 내부) - +_단건 응답: `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 +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` 에 예외 메시지) | -**설명** +**설명** + +메모를 생성한다. 검증은 `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` 내부) - +_삭제 응답에는 페이로드가 없다. 컨트롤러가 `success(메시지)` 만 호출하므로 `data` 는 `null` 이다._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| (없음) | null | `null` | 삭제 성공 시 `data` 는 항상 `null`. 결과 판정은 `success` 와 상태코드로 한다 | **응답 예시** - +```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` 에 예외 메시지) | -**설명** +**설명** + +메모를 삭제한다. 컨트롤러가 먼저 `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` 내부) - +_단건 응답: `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종 | **응답 예시** - +```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` 에 예외 메시지) | -**설명** +**설명** + +메모 단건을 조회하는 관리자 엔드포인트다. 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` 내부) - +_단건 응답: `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종 | **응답 예시** - +```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` 에 예외 메시지) | -**설명** +**설명** + +메모를 수정한다. `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/sirsoft-board/CHANGELOG.md b/modules/_bundled/sirsoft-board/CHANGELOG.md index d117e103..b061d4f9 100644 --- a/modules/_bundled/sirsoft-board/CHANGELOG.md +++ b/modules/_bundled/sirsoft-board/CHANGELOG.md @@ -6,6 +6,13 @@ ## [1.0.4] - 2026-08-12 +### Security + +- 비밀글의 내용이 열람 권한 없이 새어 나가던 경로를 모두 막았습니다. 게시글 상세 화면은 비밀글 내용을 가렸지만, 상품 문의 목록·비밀글의 댓글 목록·비밀글의 첨부파일(다운로드/미리보기)은 게시글의 비밀 여부를 확인하지 않아 주소만 알면 원문·첨부를 볼 수 있었습니다. 이제 이 경로 전부에서 작성자 본인 또는 게시판 관리 권한(비밀글 열람)을 가진 요청에만 내용을 제공하고, 그 외에는 내용·제목·답변·첨부를 가립니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 회원 프로필의 작성글 목록과 "내가 댓글 단 글" 활동 목록에서 다른 사람의 비밀글·블라인드 글 **본문**이 로그인 없이도 나가던 문제를 수정했습니다. 두 목록은 본문 앞부분을 함께 싣는데 그것을 가리는 설정이 실제로는 한 번도 켜지지 않았습니다. 이제 본인이 볼 때만 본문이 보이고, 다른 사람이 볼 때는 비워집니다. 글의 제목과 목록에서의 표시(비밀글·블라인드 배지)는 게시판 목록과 동일하게 그대로 유지됩니다. (KVE-2026-1914) +- 첨부파일 삭제·순서 변경에 담당 범위 제한을 적용했습니다. 회원 화면은 작성자 본인만 삭제하도록 막고 있었지만 관리 화면에는 같은 확인이 없었고, 순서 변경은 양쪽 모두 확인이 없었습니다. 순서는 목록 전체에 대한 하나의 값이라 범위 밖 대상이 하나라도 섞이면 요청 전체를 거부합니다. (KVE-2026-1919) +- 게시판을 찾을 수 없을 때 비밀글 보호가 통과되던 문제를 수정했습니다. 첨부파일 서빙과 댓글 목록은 부모 글을 찾지 못하면 검사를 건너뛰고 진행해, 게이트가 있어야 할 자리가 비어 있었습니다. 이제 부모 글을 확인할 수 없으면 차단합니다. + ### Changed - 글 저장·수정 시 비밀글 여부 값을 문자열(`"true"`/`"false"`)로 보내는 클라이언트도 수용하도록 해석을 관대화했습니다. 해석할 수 없는 값은 종전과 동일하게 거부됩니다. @@ -21,6 +28,7 @@ - 신고 반려 누적 제한 안내 문구를 실제 동작에 맞게 정정했습니다. "설정 건수를 초과하면 차단"으로 적혀 있었지만 실제로는 설정 건수에 도달하는 순간부터 차단됩니다 — 5건으로 설정하면 5번째 반려부터 신고가 막힙니다. - 관리자가 게시판 설정을 저장해도 백그라운드 작업에는 이전 설정이 계속 적용되던 문제를 수정했습니다. (#109 @Tuwasduliebst 님께서 제보해주셨습니다.) - 신고 현황 목록에서 항목을 선택한 뒤 검색하거나 페이지를 넘기면, 화면에서 사라진 항목이 선택된 채로 남아 일괄 처리 대상에 포함되던 문제를 수정했습니다. 이제 일괄 처리 대상은 언제나 화면에 보이면서 체크된 항목뿐입니다. +- 댓글 목록을 불러올 때 댓글마다 원글을 반복 조회하던 비효율을 제거해 응답을 더 빠르게 했습니다. 댓글이 많은 글일수록 개선 폭이 큽니다. ## [1.0.3] - 2026-08-10 diff --git a/modules/_bundled/sirsoft-board/docs/api/board.md b/modules/_bundled/sirsoft-board/docs/api/board.md index 321b6b63..2bc7ce05 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` 를 얹으면 내부 검색이 수행됩니다. 매칭이 아주 많을 수 있으므로 총 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 4926220b..6e81fc50 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/AttachmentController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/AttachmentController.php @@ -118,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); @@ -149,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/User/CommentController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php index 608772d4..ab19c115 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php @@ -14,6 +14,8 @@ 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 +34,8 @@ class CommentController extends PublicBaseController */ public function __construct( private CommentService $commentService, - private BoardService $boardService + private BoardService $boardService, + private PostRepositoryInterface $postRepository ) { parent::__construct(); } @@ -57,6 +60,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( diff --git a/modules/_bundled/sirsoft-board/src/Http/Resources/CommentResource.php b/modules/_bundled/sirsoft-board/src/Http/Resources/CommentResource.php index 3dddc4dd..fc644bc2 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 []; } @@ -251,6 +257,29 @@ class CommentResource extends BaseApiResource // 콘텐츠 필터링 메서드 // ========================================================================= + /** + * 부모 게시글(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 +289,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/PostResource.php b/modules/_bundled/sirsoft-board/src/Http/Resources/PostResource.php index 9a570876..cc6bbc2c 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; @@ -686,30 +688,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/EcommerceInquiryHookListener.php b/modules/_bundled/sirsoft-board/src/Listeners/EcommerceInquiryHookListener.php index 6a9c816b..1c5053e4 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; @@ -90,7 +91,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface /** * 기본 훅 핸들러 (HookListenerInterface 필수 메서드) * - * @param mixed ...$args 훅 인자 + * @param mixed ...$args 훅 인자 * @return void */ public function handle(...$args): void @@ -116,9 +117,11 @@ 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'; } // parent_id 있으면 답변글 → 부모 Post 제목으로 Re: 원글제목 설정 @@ -151,13 +154,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 +181,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 +194,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 +253,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 +294,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 +302,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 +311,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,7 +339,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."); } // 이커머스 경로: 알림 발송 SKIP (createPost와 동일한 skip_notification 패턴) @@ -323,7 +347,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } catch (ModelNotFoundException $e) { Log::warning('EcommerceInquiryHookListener: Post 삭제 실패 - 게시글 또는 게시판 없음', [ 'post_id' => $postId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -332,7 +356,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 +391,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 +400,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 +409,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,7 +443,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."); } // 이커머스 경로: 알림 발송 SKIP (createPost와 동일한 skip_notification 패턴) @@ -429,7 +453,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( @@ -438,7 +462,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( diff --git a/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php b/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php index 1549224b..c2bf9e29 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php @@ -77,6 +77,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 정렬) * diff --git a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php index 7f3cb3e3..63e6d31c 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Board\Repositories\Contracts; use Illuminate\Database\Eloquent\Collection; use Modules\Sirsoft\Board\Models\Attachment; +use Modules\Sirsoft\Board\Models\Post; /** * 게시판 첨부파일 Repository 인터페이스 @@ -39,6 +40,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 정렬) * diff --git a/modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php b/modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php index 83614e87..91c2b3b4 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,15 @@ 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; // 공개 프로필용 필터 (비밀글 제외, 공개 게시글만) + // 열람자 관점. 이 값이 대상 사용자와 다르면(비로그인 포함) **타인 관점**이므로 + // 비밀글·미발행글을 내보내지 않는다. + // + // 종전에는 `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 +1107,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 +1124,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 +1134,7 @@ class PostRepository implements PostRepositoryInterface ?int $boardIdFilter, array $excludeBoardIds, ?string $search, - bool $isPublic, + bool $isOwnView, string $orderColumn, string $orderDirection, int $perPage, @@ -1145,11 +1156,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 +1186,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 +1208,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 +1238,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 +1325,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 +1347,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 ?? '')), ]; }); diff --git a/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php b/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php index 5109ed07..5d9f96cf 100644 --- a/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php +++ b/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php @@ -16,6 +16,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 +346,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 +420,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 +431,8 @@ class AttachmentService return false; } + $this->assertAttachmentWithinScope($slug, $attachment, $context); + // 삭제 후 재정렬을 위해 정보 저장 $postId = $attachment->post_id; $collection = $attachment->collection; @@ -425,10 +468,13 @@ class AttachmentService * * @param string $slug 게시판 슬러그 * @param array $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 +486,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 $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 +620,9 @@ class AttachmentService // 삭제된 게시글의 첨부파일은 관리 권한자만 접근 $this->assertDeletedPostAttachmentAccess($slug, $attachment); + // 비밀글 첨부파일은 열람 권한자만 접근 + $this->assertSecretPostAttachmentAccess($slug, $attachment); + // 다운로드 활동이력 기록 훅 // 권한/삭제글 가드 통과 후 발화 → 차단된 시도는 기록하지 않음. // $context('user'|'admin')는 로그 부가정보용 (log_type 은 요청 경로로 자동 결정). @@ -580,6 +686,9 @@ class AttachmentService // 삭제된 게시글의 첨부파일은 관리 권한자만 접근 $this->assertDeletedPostAttachmentAccess($slug, $attachment); + // 비밀글 첨부파일은 열람 권한자만 접근 + $this->assertSecretPostAttachmentAccess($slug, $attachment); + return $this->storage->response( 'attachments', $attachment->path, @@ -612,6 +721,9 @@ class AttachmentService // 삭제된 게시글의 첨부파일은 관리 권한자만 접근 $this->assertDeletedPostAttachmentAccess($slug, $attachment); + // 비밀글 첨부파일은 열람 권한자만 접근 + $this->assertSecretPostAttachmentAccess($slug, $attachment); + // 파일 존재 확인 if (! $this->storage->exists('attachments', $attachment->path)) { Log::error('게시판 첨부파일 스토리지에 없음', [ diff --git a/modules/_bundled/sirsoft-board/src/Services/PostService.php b/modules/_bundled/sirsoft-board/src/Services/PostService.php index 676b41a4..8ff881c8 100644 --- a/modules/_bundled/sirsoft-board/src/Services/PostService.php +++ b/modules/_bundled/sirsoft-board/src/Services/PostService.php @@ -882,9 +882,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 +1073,10 @@ class PostService } } + // 본인 활동 화면(`/me/board-activities`)이므로 열람자 = 대상 본인이다. + // 저장소는 이 값이 없으면 타인 관점으로 보고 비밀글·미발행글을 걸러낸다(fail-closed). + $filters['viewer_id'] = $userId; + $result = $this->postRepository->getUserActivities($userId, $filters, $perPage); // 캐시 미적중 시 paginate 결과의 total을 캐시에 저장 @@ -1105,9 +1112,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 +1126,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..5788f1d1 --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Support/SecretContentGate.php @@ -0,0 +1,101 @@ +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); + } + + /** + * 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\\'); + } +} diff --git a/modules/_bundled/sirsoft-board/src/lang/en/messages.php b/modules/_bundled/sirsoft-board/src/lang/en/messages.php index 8ba56284..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.', diff --git a/modules/_bundled/sirsoft-board/src/lang/ko/messages.php b/modules/_bundled/sirsoft-board/src/lang/ko/messages.php index c9c24c49..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' => '관리자에 의해 블라인드 처리된 게시글입니다.', diff --git a/modules/_bundled/sirsoft-board/src/routes/api.php b/modules/_bundled/sirsoft-board/src/routes/api.php index 2ca4aee6..7a05d231 100644 --- a/modules/_bundled/sirsoft-board/src/routes/api.php +++ b/modules/_bundled/sirsoft-board/src/routes/api.php @@ -504,8 +504,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/User/SecretPostAttachmentAccessTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostAttachmentAccessTest.php new file mode 100644 index 00000000..95f85de5 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostAttachmentAccessTest.php @@ -0,0 +1,309 @@ + $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(), '게이트 통과 시 서버 오류가 아니어야 합니다'); + } +} 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 @@ + $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 @@ + $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/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/Unit/Listeners/EcommerceInquiryHookListenerTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Listeners/EcommerceInquiryHookListenerTest.php index fdefc73a..f44a05fb 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; @@ -146,6 +148,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 반환 */ @@ -214,6 +270,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 // ========================================== diff --git a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md index 8ac8c5ae..39d6698e 100644 --- a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md +++ b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md @@ -6,6 +6,12 @@ ## [1.1.1] - 2026-08-12 +### Security + +- 비밀 상품 문의의 내용이 작성자·관리자가 아닌 사람에게도 노출되던 문제를 막았습니다. 문의 목록은 게시글의 비밀 여부를 서버에서 확인해, 열람 권한이 없는 요청에는 내용·제목·답변·첨부를 가립니다. (목록의 '비밀글 숨김' 옵션과 무관하게 서버가 요청자 신원으로 노출 여부를 결정합니다.) (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 관리자가 숨긴 리뷰의 이미지가 주소만 알면 계속 조회되던 문제를 수정했습니다. 이제 숨김 처리된 리뷰의 이미지는 제공되지 않습니다. 리뷰가 삭제되어 상태를 확인할 수 없는 이미지도 함께 차단합니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 상품·주문·리뷰·브랜드·쿠폰·배송정책·추가배송비 관리에 담당 범위 제한을 적용했습니다. 이 권한들은 "본인이 등록한 것만" 처럼 범위를 좁혀 위임할 수 있는데, 그 제한이 실제로는 어느 관리 화면에서도 적용되지 않았습니다. 목록에서 여러 건을 한 번에 처리하는 일괄 기능뿐 아니라, 항목을 하나씩 수정·삭제하는 화면에서도 마찬가지였습니다. 이제 모든 경로에서 대상마다 범위를 확인하며, 일괄 기능은 범위 밖 대상이 하나라도 섞이면 요청 전체를 거부하고 아무것도 변경하지 않습니다. 범위 제한 없이 위임받은 관리자의 작업은 종전처럼 정상 동작합니다. (KVE-2026-1919) + ### Added - 리뷰 이미지 업로드에 파일 가공 필터 훅 제공 — 확장에서 상품·카테고리 이미지와 동일하게 리뷰 이미지도 업로드 시점에 변환(압축·리사이즈·포맷 변경)할 수 있습니다. (#96 @lyg-kaban 님께서 건의해주셨습니다.) @@ -67,6 +73,7 @@ #### 기타 - 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 추가배송비(도서산간) 템플릿 상세 응답의 편집 권한 표시를 배송정책 권한 기준으로 통일했습니다. 이전에는 상세 응답만 다른 권한(설정 관리)을 기준으로 삼아, 배송정책 관리 권한만 가진 관리자에게 편집 가능 여부가 실제 권한과 다르게 표시될 수 있었습니다. #### 배송비 구간 diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md index ad399777..5faacd21 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md @@ -283,12 +283,12 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -_단건 응답: 삭제 결과 요약 (`data` 객체)._ +_단건 응답: 삭제 결과 요약 (`BrandService::deleteBrand()` 가 반환한 배열 — Resource 로 감싸지 않는다)._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| brand_id | integer | `2` | 삭제된 브랜드의 기본 키 | -| products_count | integer | `0` | 삭제 시점에 이 브랜드를 쓰던 상품 수 (연결 상품이 있으면 삭제가 거부되므로 언제나 0) | +| brand_id | integer | `1` | 삭제된 브랜드의 ID | +| products_count | integer | `0` | 삭제 시점의 연결 상품 수. **항상 `0`** 이다 — 연결 상품이 하나라도 있으면 삭제 자체가 차단되므로 성공 응답에서는 0 외의 값이 나올 수 없다 | **응답 예시** @@ -301,7 +301,7 @@ HTTP/1.1 200 "success": true, "message": "브랜드가 삭제되었습니다.", "data": { - "brand_id": 2, + "brand_id": 1, "products_count": 0 } } @@ -312,9 +312,9 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.delete`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.delete`)이 없거나, 대상 브랜드가 요청자의 **스코프 밖**인 경우 (`auth.scope_denied`). 존재하지 않는 ID 도 스코프 판정에서 먼저 걸려 403 이 된다 | +| 400 | Bad Request | 연결된 상품이 있어 삭제가 차단된 경우(`exceptions.brand_has_products` — 상품 수 포함), 또는 삭제 처리 중 예외 발생 (`exceptions.operation_failed`) | +| 404 | Not Found | path 파라미터 형식이 라우트에 매칭되지 않는 경우 | @@ -425,7 +425,6 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | -| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | @@ -455,7 +454,25 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -_단건 응답: `data` 객체의 필드 (`BrandResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/brands/{id} (브랜드 수정)** 응답 필드 표와 동일합니다._ +_단건 응답: `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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 | **응답 예시** @@ -466,23 +483,25 @@ HTTP/1.1 200 ```json { "success": true, - "message": "브랜드 정보를 조회했습니다.", + "message": "브랜드를 조회했습니다.", "data": { - "id": 1, + "id": 20, "name": { - "ko": "API 문서 샘플 브랜드", - "en": "API Doc Sample Brand" + "ko": "ASUS", + "en": "ASUS" }, - "localized_name": "API 문서 샘플 브랜드", - "slug": "apidoc-sample-brand", - "url": "apidoc-sample-brand", + "localized_name": "ASUS", + "slug": "asus", + "url": "asus", "website": "https://www.asus.com", - "sort_order": 0, + "sort_order": 20, "is_active": true, "icon": "tag", - "created_at": "2026-07-08 10:44:49", - "updated_at": "2026-07-08 15:00:16", - "products_count": 0, + "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, @@ -498,7 +517,7 @@ HTTP/1.1 200 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.read`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `id` 의 브랜드가 없는 경우 (`messages.brands.not_found`) | @@ -528,10 +547,21 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -_단건 응답: `data` 객체의 필드 (`BrandResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/brands/{id} (브랜드 수정)** 응답 필드 표와 동일합니다._ `is_active` 가 반전된 상태로 반환됩니다. +_단건 응답: `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` 로 판정한다. **응답 예시** +비활성으로 토글된 경우: + ```http HTTP/1.1 200 ``` @@ -541,21 +571,23 @@ HTTP/1.1 200 "success": true, "message": "브랜드 상태가 변경되었습니다.", "data": { - "id": 1, + "id": 20, "name": { - "ko": "API 문서 샘플 브랜드", - "en": "API Doc Sample Brand" + "ko": "ASUS", + "en": "ASUS" }, - "localized_name": "API 문서 샘플 브랜드", - "slug": "apidoc-sample-brand", - "url": "apidoc-sample-brand", + "localized_name": "ASUS", + "slug": "asus", + "url": "asus", "website": "https://www.asus.com", - "sort_order": 0, - "is_active": true, + "sort_order": 20, + "is_active": false, "icon": "tag", - "created_at": "2026-07-08 10:44:49", - "updated_at": "2026-07-08 15:00:16", - "products_count": 0, + "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, @@ -571,8 +603,8 @@ HTTP/1.1 200 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | +| 400 | Bad Request | 해당 `id` 의 브랜드가 없거나 토글 처리 중 예외 발생 (`exceptions.operation_failed`) | +| 404 | Not Found | path 파라미터 형식이 라우트에 매칭되지 않는 경우 | 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[]` 배열 항목의 필드. 페이지네이션 없는 **응답 예시** - +```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}` 에 해당하는 마일리지 거래가 없는 경우 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/products.md b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md index f2e7f94b..58f95aa8 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/products.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md @@ -3418,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 가 됩니다. **보안 경계가 아니라 단순 표시 필터입니다** — 비밀 문의의 노출/마스킹은 이 값과 무관하게 서버가 요청자 신원으로 결정합니다 | **요청 예시** @@ -3521,7 +3521,7 @@ HTTP/1.1 200 -**설명** 상품의 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 diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md index 33091217..8fc59073 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md @@ -73,10 +73,12 @@ 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`) | -**설명** 리뷰에 첨부된 이미지를 해시(12자) 기반으로 공개 서빙합니다. 인증이 필요 없으며, `ReviewImageController@download` 가 `ProductReviewImageService::download()` 로 해시에 해당하는 이미지를 찾아 스트림(`StreamedResponse`)으로 반환합니다. 해시에 해당하는 이미지가 없으면 404 를 반환합니다. `` 등에서 리뷰 이미지 원본을 표시할 때 사용하며, 실제 파일 경로를 노출하지 않고 해시로만 접근하게 합니다. +**설명** 리뷰에 첨부된 이미지를 해시(12자) 기반으로 서빙합니다. `ReviewImageController@download` 가 `ProductReviewImageService::download()` 로 해시에 해당하는 이미지를 찾아 스트림(`StreamedResponse`)으로 반환합니다. `` 등에서 리뷰 이미지 원본을 표시할 때 사용하며, 실제 파일 경로를 노출하지 않고 해시로만 접근하게 합니다. + +이미지 서빙은 **부모 리뷰가 노출(VISIBLE) 상태일 때로 한정**됩니다(KVE-2026-1914). 숨김·블라인드·대기 상태 리뷰의 이미지는 해시가 유효해도 서빙되지 않고 404 를 반환합니다 — 본문이 감춰진 리뷰의 이미지가 해시만으로 노출되는 것을 막고, 존재 자체를 드러내지 않기 위해 "이미지 없음"과 동일한 404 로 응답합니다. 노출 상태 리뷰의 이미지는 별도 인증 없이 공개 접근할 수 있습니다. diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md index 1b50a11a..f086d054 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md @@ -259,11 +259,11 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -_단건 응답: `data` 객체의 필드._ +_단건 응답: 삭제 성공 플래그만 반환한다 (Resource 로 감싸지 않는다)._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| deleted | boolean | `true` | 삭제 완료 여부 (컨트롤러가 고정값 `true` 로 내려준다) | +| deleted | boolean | `true` | 삭제 성공 여부. 성공 응답에서는 항상 `true` 이며, 실패는 `500` 으로 갈린다 | **응답 예시** @@ -281,13 +281,16 @@ HTTP/1.1 200 } ``` +> 삭제 전 컨트롤러가 `images` 관계를 로드한다 — 첨부 이미지 파일까지 함께 정리하기 위해서다. DB CASCADE 에 맡기지 않고 Service 가 명시적으로 삭제하므로 훅 발화와 파일 정리가 보장된다. + **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 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`) | @@ -362,55 +365,48 @@ HTTP/1.1 200 "success": true, "message": "리뷰를 조회했습니다.", "data": { - "id": 12, - "product_id": 3, - "order_option_id": 45, - "user_id": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", + "id": 99, + "product_id": 320, + "order_option_id": 859, + "user_id": "a231747f-e82e-4cf2-9ae1-a261849dce40", "user": { - "uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", - "name": "실측 예시값", - "email": "실측 예시값" + "uuid": "a231747f-e82e-4cf2-9ae1-a261849dce40", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" }, "product": { - "id": 3, - "product_code": "실측 예시값", - "name": "실측 예시값", - "thumbnail_url": "실측 예시값" + "id": 320, + "name": "API 문서 샘플 상품", + "thumbnail_url": null }, - "option_snapshot": { - "option_name": "실측 예시값" - }, - "option_snapshot_label": "실측 예시값", + "option_snapshot": "{\"id\":104,\"option_code\":\"FWACBAVCBKCD\"}", + "option_snapshot_label": "", "rating": 5, - "content": "실측 예시값", + "content": "Molestiae repellendus accusantium omnis.", "content_mode": "text", "status": "visible", - "status_label": "노출", - "status_badge_color": "green", + "status_label": "전시중", + "status_badge_color": "blue", "images": [], "image_count": 0, "orderOption": { - "id": 45, - "order_id": 4, - "order_number": "20260728-0111050215", + "id": 859, + "order_id": 455, + "order_number": "ORD-20260707-000123", "quantity": 1, - "created_at": "2026-08-05 09:46:56" + "ordered_at": "2026-07-07 14:40:00" }, - "has_reply": true, - "has_reply_label": "답변완료", - "has_reply_badge_color": "green", - "reply_content": "실측 예시값", + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, "reply_content_mode": "text", - "reply_admin_uuid": "6d1f0d0e-2a1b-4c3d-8e9f-0a1b2c3d4e5f", - "reply_admin": { - "uuid": "6d1f0d0e-2a1b-4c3d-8e9f-0a1b2c3d4e5f", - "name": "실측 예시값", - "email": "실측 예시값" - }, - "replied_at": "2026-08-05 09:46:56", - "reply_updated_at": "2026-08-05 09:46:56", - "created_at": "2026-08-05 09:46:56", - "updated_at": "2026-08-05 09:46:56", + "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 @@ -425,7 +421,8 @@ HTTP/1.1 200 | --- | --- | --- | | 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`) | @@ -492,57 +489,34 @@ HTTP/1.1 200 ```json { "success": true, - "message": "답변이 삭제되었습니다.", + "message": "판매자 답변이 삭제되었습니다.", "data": { - "id": 12, - "product_id": 3, - "order_option_id": 45, - "user_id": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", + "id": 1, + "product_id": 1, + "order_option_id": 1, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", "user": { - "uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", - "name": "실측 예시값", - "email": "실측 예시값" + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" }, - "product": { - "id": 3, - "product_code": "실측 예시값", - "name": "실측 예시값", - "thumbnail_url": "실측 예시값" - }, - "option_snapshot": { - "option_name": "실측 예시값" - }, - "option_snapshot_label": "실측 예시값", + "option_snapshot": "{\"id\":104,\"option_code\":\"FWACBAVCBKCD\"}", + "option_snapshot_label": "", "rating": 5, - "content": "실측 예시값", + "content": "Alias quas iusto dolorem eum eveniet.", "content_mode": "text", "status": "visible", - "status_label": "노출", - "status_badge_color": "green", - "images": [], - "image_count": 0, - "orderOption": { - "id": 45, - "order_id": 4, - "order_number": "20260728-0111050215", - "quantity": 1, - "created_at": "2026-08-05 09:46:56" - }, - "has_reply": true, - "has_reply_label": "답변완료", - "has_reply_badge_color": "green", - "reply_content": "실측 예시값", + "status_label": "전시중", + "status_badge_color": "blue", + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, "reply_content_mode": "text", - "reply_admin_uuid": "6d1f0d0e-2a1b-4c3d-8e9f-0a1b2c3d4e5f", - "reply_admin": { - "uuid": "6d1f0d0e-2a1b-4c3d-8e9f-0a1b2c3d4e5f", - "name": "실측 예시값", - "email": "실측 예시값" - }, - "replied_at": "2026-08-05 09:46:56", - "reply_updated_at": "2026-08-05 09:46:56", - "created_at": "2026-08-05 09:46:56", - "updated_at": "2026-08-05 09:46:56", + "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 @@ -551,13 +525,16 @@ HTTP/1.1 200 } ``` +> 답변이 지워진 결과가 응답에 그대로 반영된다 — `has_reply` 는 `false`, `reply_content`·`replied_at`·`reply_updated_at` 은 `null` 로 비워진다. 리뷰 본문(`content`·`rating`·`status`)은 그대로 유지된다. + **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 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`) | @@ -634,57 +611,34 @@ HTTP/1.1 200 ```json { "success": true, - "message": "답변이 저장되었습니다.", + "message": "판매자 답변이 저장되었습니다.", "data": { - "id": 12, - "product_id": 3, - "order_option_id": 45, - "user_id": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", + "id": 1, + "product_id": 1, + "order_option_id": 1, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", "user": { - "uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", - "name": "실측 예시값", - "email": "실측 예시값" + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" }, - "product": { - "id": 3, - "product_code": "실측 예시값", - "name": "실측 예시값", - "thumbnail_url": "실측 예시값" - }, - "option_snapshot": { - "option_name": "실측 예시값" - }, - "option_snapshot_label": "실측 예시값", + "option_snapshot": "{\"id\":104,\"option_code\":\"FWACBAVCBKCD\"}", + "option_snapshot_label": "", "rating": 5, - "content": "실측 예시값", + "content": "Alias quas iusto dolorem eum eveniet.", "content_mode": "text", "status": "visible", - "status_label": "노출", - "status_badge_color": "green", - "images": [], - "image_count": 0, - "orderOption": { - "id": 45, - "order_id": 4, - "order_number": "20260728-0111050215", - "quantity": 1, - "created_at": "2026-08-05 09:46:56" - }, + "status_label": "전시중", + "status_badge_color": "blue", "has_reply": true, "has_reply_label": "답변완료", "has_reply_badge_color": "green", - "reply_content": "실측 예시값", + "reply_content": "소중한 후기 감사합니다. 앞으로도 좋은 상품으로 보답하겠습니다.", "reply_content_mode": "text", - "reply_admin_uuid": "6d1f0d0e-2a1b-4c3d-8e9f-0a1b2c3d4e5f", - "reply_admin": { - "uuid": "6d1f0d0e-2a1b-4c3d-8e9f-0a1b2c3d4e5f", - "name": "실측 예시값", - "email": "실측 예시값" - }, - "replied_at": "2026-08-05 09:46:56", - "reply_updated_at": "2026-08-05 09:46:56", - "created_at": "2026-08-05 09:46:56", - "updated_at": "2026-08-05 09:46:56", + "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 @@ -693,14 +647,17 @@ HTTP/1.1 200 } ``` +> 답변 저장 결과가 응답에 반영된다 — `has_reply` 는 `true`, `has_reply_badge_color` 는 `green` 으로 바뀌고 `replied_at`·`reply_updated_at` 이 채워진다. 이미 답변이 있던 리뷰를 다시 호출하면 내용이 **갱신**되며(중복 답변이 생기지 않는다) `reply_updated_at` 만 새로 갱신된다. 답변 작성자는 요청한 관리자(`Auth::id()`)로 기록된다. + **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 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`) | @@ -937,12 +894,12 @@ HTTP/1.1 201 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 작성 자격 미충족 시 `ReviewNotWritableException` 이 발생하고, 그 사유 식별자(`order_option_not_found` / `not_own_order` / `not_confirmed` / `deadline_passed` / `already_written`)에 대응하는 다국어 키로 해석된 문구가 `message` 에 담긴다 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 작성 자격 미충족 시 `ReviewNotWritableException`(RuntimeException) 이 발생해 사유 코드가 담긴 메시지로 응답 (`order_option_not_found` / `not_own_order` / `not_confirmed` / `deadline_passed` / `already_written`) | | 500 | Internal Server Error | 리뷰 생성 중 예외 발생 (`messages.reviews.create_failed` — "리뷰 작성에 실패했습니다.") | -**설명** 로그인 회원이 구매한 상품에 리뷰를 작성합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, `ProductReviewService::createReview()` 가 로그인 사용자(`Auth::id()`)를 작성자로 하여 `product_id`·`order_option_id`·별점(1~5)·내용(10~2000자)으로 리뷰를 생성하고 201 로 반환합니다. 본인 주문이 아니거나 이미 작성했거나 작성 조건을 만족하지 못하면 서비스가 `ReviewNotWritableException` 을 던지고, 컨트롤러가 그 사유에 대응하는 다국어 키로 422 응답을 만듭니다. 마이페이지 리뷰 작성 폼에서 사용합니다. +**설명** 로그인 회원이 구매한 상품에 리뷰를 작성합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, `ProductReviewService::createReview()` 가 로그인 사용자(`Auth::id()`)를 작성자로 하여 `product_id`·`order_option_id`·별점(1~5)·내용(10~2000자)으로 리뷰를 생성하고 201 로 반환합니다. 본인 주문이 아니거나 이미 작성했거나 작성 조건을 만족하지 못하면 서비스가 `RuntimeException` 을 던져 422 로 응답합니다. 마이페이지 리뷰 작성 폼에서 사용합니다. ### GET /api/modules/sirsoft-ecommerce/user/reviews/can-write/{orderOptionId} @@ -968,19 +925,44 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -_단건 응답: `data` 객체의 필드._ +_단건 응답: 작성 자격 판정 결과 (`ProductReviewService::canWrite()` 가 반환한 배열 — Resource 로 감싸지 않는다)._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| can_write | boolean | `false` | 이 주문 옵션에 리뷰를 작성할 수 있는지 | -| reason | string\|null | `"already_written"` | 작성 불가 사유 식별자. `order_option_not_found` / `not_own_order` / `not_confirmed` / `deadline_passed` / `already_written` 중 하나이며, 작성 가능하면 `null` | +| 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` | 이 주문 옵션으로 이미 리뷰를 작성함 | **응답 예시** +작성 가능한 경우: + ```http HTTP/1.1 200 ``` +```json +{ + "success": true, + "message": "리뷰 작성 가능 여부를 확인했습니다.", + "data": { + "can_write": true, + "reason": null + } +} +``` + +작성 불가한 경우 (이미 작성함): + ```json { "success": true, @@ -992,12 +974,14 @@ HTTP/1.1 200 } ``` +> 작성 불가는 **에러가 아니라 정상 응답**이다. 존재하지 않는 주문 옵션이나 남의 주문도 `404`/`403` 이 아니라 `200` + `can_write: false` 로 응답하므로, 이 엔드포인트로 주문 존재 여부를 탐색할 수 없다. + **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 판정 중 예외 발생 (`messages.reviews.can_write_check_failed`) | @@ -1027,11 +1011,11 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -_단건 응답: `data` 객체의 필드._ +_단건 응답: 삭제 성공 플래그만 반환한다 (관리자 삭제 엔드포인트와 같은 형태)._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| deleted | boolean | `true` | 삭제 완료 여부 (컨트롤러가 고정값 `true` 로 내려준다) | +| deleted | boolean | `true` | 삭제 성공 여부. 성공 응답에서는 항상 `true` | **응답 예시** @@ -1049,13 +1033,16 @@ HTTP/1.1 200 } ``` +> 삭제 전 `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`) | @@ -1151,12 +1138,12 @@ HTTP/1.1 201 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없거나, 대상 리뷰가 로그인 사용자의 리뷰가 아닌 경우 (`messages.reviews.forbidden` — "권한이 없습니다.") | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | `image` 파일 누락·허용 형식/용량 위반 (`error.errors` 에 필드별 메시지) 또는 리뷰당 최대 첨부 개수 초과 시 `ReviewImageUploadLimitException` — 최대 개수는 리뷰 설정 `review_settings.max_images`(기본 5) 기준이며, 응답 문구는 그 상한을 파라미터로 받는 다국어 키로 만들어진다 | +| 422 | Unprocessable Entity | `image` 파일 누락·허용 형식/용량 위반 (`error.errors` 에 필드별 메시지) 또는 리뷰당 최대 첨부 개수 초과 시 `ReviewImageUploadLimitException`(RuntimeException) — 최대 개수는 리뷰 설정 `review_settings.max_images`(기본 5) 기준 | | 500 | Internal Server Error | 업로드 처리 중 예외 발생 (`messages.reviews.image_upload_failed` — "리뷰 이미지 업로드에 실패했습니다.") | -**설명** 로그인 회원이 자신의 리뷰에 이미지를 첨부합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, 컨트롤러가 `review->user_id` 로 본인 소유를 확인(불일치 시 403)한 뒤 `ProductReviewImageService::upload()` 가 업로드된 이미지(최대 10MB)를 저장하고 201 로 이미지 리소스를 반환합니다. 리뷰당 첨부 개수 상한을 넘기면 서비스가 `ReviewImageUploadLimitException` 을 던지고, 컨트롤러가 상한값을 파라미터로 실은 다국어 키로 422 응답을 만듭니다. 포토 리뷰 작성 시 이미지를 추가할 때 사용합니다. +**설명** 로그인 회원이 자신의 리뷰에 이미지를 첨부합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, 컨트롤러가 `review->user_id` 로 본인 소유를 확인(불일치 시 403)한 뒤 `ProductReviewImageService::upload()` 가 업로드된 이미지(최대 10MB)를 저장하고 201 로 이미지 리소스를 반환합니다. 파일 형식/크기 등 제약 위반 시 서비스가 `RuntimeException` 을 던져 422 로 응답합니다. 포토 리뷰 작성 시 이미지를 추가할 때 사용합니다. ### DELETE /api/modules/sirsoft-ecommerce/user/reviews/{review}/images/{image} 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 702f04d0..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 */ @@ -580,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(); @@ -590,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(); @@ -664,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(); 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/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 $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/ExtraFeeTemplateRepository.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ExtraFeeTemplateRepository.php index 177d9d27..d12a4b13 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} */ diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php index 3520c8a3..e9c687d3 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php @@ -9,12 +9,16 @@ 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; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 브랜드 서비스 */ class BrandService { + use ReappliesPermissionScope; + public function __construct( protected BrandRepositoryInterface $repository ) {} @@ -108,6 +112,12 @@ class BrandService */ public function updateBrand(int $id, array $data): Brand { + $brandForScope = $this->repository->findById($id); + if (! $brandForScope) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + $this->assertWithinScope($brandForScope, 'sirsoft-ecommerce.brands.update'); + $brand = $this->repository->findById($id); if (! $brand) { @@ -149,6 +159,12 @@ class BrandService */ public function toggleStatus(int $id): Brand { + $brandForScope = $this->repository->findById($id); + if (! $brandForScope) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + $this->assertWithinScope($brandForScope, 'sirsoft-ecommerce.brands.update'); + $brand = $this->repository->findById($id); if (! $brand) { @@ -180,6 +196,12 @@ class BrandService */ public function deleteBrand(int $id): array { + $brandForScope = $this->repository->findById($id); + if (! $brandForScope) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + $this->assertWithinScope($brandForScope, 'sirsoft-ecommerce.brands.delete'); + $brand = $this->repository->findById($id); if (! $brand) { diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php index 68beef60..7a71ae2d 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php @@ -12,12 +12,16 @@ 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; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 쿠폰 서비스 */ class CouponService { + use ReappliesPermissionScope; + public function __construct( protected CouponRepositoryInterface $repository, protected UserCouponService $userCouponService, @@ -140,6 +144,12 @@ class CouponService */ public function updateCoupon(int $id, array $data): Coupon { + $coupon = $this->repository->findById($id); + if (! $coupon) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + $this->assertWithinScope($coupon, 'sirsoft-ecommerce.promotion-coupon.update'); + $coupon = $this->repository->findById($id); if (! $coupon) { @@ -197,6 +207,12 @@ class CouponService */ public function deleteCoupon(int $id): array { + $coupon = $this->repository->findById($id); + if (! $coupon) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + $this->assertWithinScope($coupon, 'sirsoft-ecommerce.promotion-coupon.delete'); + $coupon = $this->repository->findById($id); if (! $coupon) { @@ -232,11 +248,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 = $this->repository->findByIdsKeyed($ids)->map->toArray()->all(); + $snapshots = $targets->keyBy('id')->map->toArray()->all(); $count = DB::transaction(function () use ($ids, $issueStatus) { return $this->repository->bulkUpdateIssueStatus($ids, $issueStatus); @@ -262,6 +283,12 @@ class CouponService */ public function issueDirectly(int $couponId, array $userIds): array { + $coupon = $this->repository->findById($couponId); + if (! $coupon) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + $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) { 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/OrderService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderService.php index df13558e..76a59686 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderService.php @@ -19,12 +19,15 @@ 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 +164,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 +355,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 +397,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 +515,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 +559,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 +641,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()) { diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php index 7d5ca46f..581f6cba 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php @@ -108,7 +108,10 @@ class ProductInquiryService } } - // 비밀글 제외 필터 적용 (Post의 is_secret 기준) + // 비밀글 원문 마스킹은 게시판 훅(getByIds)이 요청자 신원 기준으로 서버측에서 + // 이미 수행한다(KVE-2026-1914, SecretContentGate SSoT). 아래 exclude_secret 은 + // 보안 판정이 아니라 단순 "비밀글 행 숨김" 표시 필터일 뿐이며, 노출 여부는 + // 클라이언트 파라미터와 무관하게 서버가 결정한다. if ($excludeSecret) { $pivots = $pivots->filter(function ($pivot) use ($posts) { $post = $posts[$pivot->inquirable_id] ?? null; @@ -132,21 +135,37 @@ 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(); @@ -222,6 +241,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( @@ -600,6 +625,11 @@ class ProductInquiryService $boardSlug = $this->getInquiryBoardSlug(); + // 클라이언트 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, [ diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php index fdca7881..eedd0ae0 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php @@ -9,6 +9,7 @@ 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; @@ -165,6 +166,18 @@ class ProductReviewImageService return null; } + // 숨김(HIDDEN) 리뷰의 이미지는 서빙하지 않는다(KVE-2026-1914 S-2). + // 관리자가 숨긴 리뷰의 이미지가 해시만으로 공개 서빙되던 불일치를 정합화한다. + // + // 부모 리뷰를 못 읽으면 **가린다**(fail-closed). ProductReview 는 소프트 삭제되므로 + // 관계 조회가 null 을 돌려줄 수 있는데, 그때 게이트가 성립하지 않아 통과하면 + // "부모가 사라진 이미지는 무조건 공개" 가 된다 — 첨부 게이트가 채택한 방향과 반대다. + // 현재는 리뷰 삭제 시 이미지도 함께 삭제돼 여기까지 오지 않지만, 그 두 서비스가 + // 계속 동기화된다는 암묵 불변식에 보안을 걸지 않는다. + if (! $image->review || $image->review->status !== ReviewStatus::VISIBLE) { + return null; + } + $response = $this->storageForRow($image->disk)->response( 'images', $image->path, diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php index 96fbd709..80ae8027 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php @@ -17,6 +17,7 @@ use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductReviewImageRepositor 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; /** * 상품 리뷰 서비스 @@ -25,6 +26,7 @@ use Modules\Sirsoft\Ecommerce\Support\ReviewWritePolicy; */ class ProductReviewService { + use ReappliesPermissionScope; use ResolvesRowStorage; /** @@ -168,6 +170,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]); } @@ -200,6 +204,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', @@ -217,6 +223,8 @@ 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) { @@ -251,6 +259,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); } @@ -264,6 +274,8 @@ class ProductReviewService { $reviews = $this->repository->getByIdsWithImages($ids); + $this->assertAllWithinScope($reviews, 'sirsoft-ecommerce.reviews.delete'); + // 삭제 전 스냅샷 캡처 (after_bulk_delete 훅에 전달) $snapshots = $reviews->keyBy('id')->map->toArray()->all(); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductService.php index 7d3716b5..3ce90222 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; + /** * 검색 정렬 이름 → [실제 컬럼, 방향] 선언 * @@ -301,6 +304,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 +410,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); @@ -489,6 +496,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 +531,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 +565,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 +597,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); 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/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 @@ + $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/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/Public/PublicProductInquiryControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/PublicProductInquiryControllerTest.php index 4efa496f..8e7466e9 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 @@ -128,6 +128,195 @@ 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']); + } + #[Test] public function board_slug_미설정_시_빈_목록과_inquiry_available_false를_반환한다(): void { @@ -183,6 +372,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/ReviewImageControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/ReviewImageControllerTest.php index 9378da95..bc2624ce 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 @@ -9,6 +9,7 @@ use App\Models\Role; use App\Models\User; use Illuminate\Http\UploadedFile; 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 +20,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 +38,7 @@ class ReviewImageControllerTest extends ModuleTestCase private ProductReview $review; - /** @var \Mockery\MockInterface&StorageInterface */ + /** @var MockInterface&StorageInterface */ private $storageMock; protected function setUp(): void @@ -351,4 +353,61 @@ 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 리뷰 이미지는 서빙되어야 합니다'); + } } 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..cf31d09f 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; @@ -69,7 +70,7 @@ class UserProductInquiryControllerTest extends ModuleTestCase ); $this->inquiry = ProductInquiry::factory()->create([ - 'user_id' => $this->user->id, + 'user_id' => $this->user->id, 'product_id' => $this->product->id, 'is_answered' => false, ]); @@ -237,11 +238,58 @@ class UserProductInquiryControllerTest extends ModuleTestCase $response->assertStatus(201); $this->assertDatabaseHas('ecommerce_product_inquiries', [ - 'id' => $this->inquiry->id, + 'id' => $this->inquiry->id, 'is_answered' => true, ]); } + /** + * 서비스가 클라이언트 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 +298,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 +317,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 +339,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 +357,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 +369,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/Security/ServiceScopeReapplyTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Security/ServiceScopeReapplyTest.php new file mode 100644 index 00000000..50101965 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Security/ServiceScopeReapplyTest.php @@ -0,0 +1,207 @@ +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); + } + + /** + * 소유자를 지정해 배송정책을 만듭니다 (팩토리 부재 — 기존 테스트와 같은 직접 생성). + * + * @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/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/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/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/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php index 01095a94..07308ab9 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php @@ -164,7 +164,7 @@ class EcommerceSettingsServiceCurrencyTombstoneTest extends ModuleTestCase $this->assertArrayHasKey('removed_default_currencies', $saved, '삭제 기록이 저장되지 않았습니다.'); $this->assertSame(['JPY', 'CNY', 'EUR'], $saved['removed_default_currencies']); - // 비연속 키가 JSON 객체로 직렬화되지 않도록 array_values 재정렬 (CLAUDE.md 규칙) + // 비연속 키가 JSON 객체로 직렬화되지 않도록 array_values 재정렬 (코딩 규약) $this->assertSame([0, 1, 2], array_keys($saved['removed_default_currencies'])); } 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/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/scenarios/service-scope-reapply.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/service-scope-reapply.yaml new file mode 100644 index 00000000..a5d3ee51 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/service-scope-reapply.yaml @@ -0,0 +1,35 @@ +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 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Security/ServiceScopeReapplyTest.php diff --git a/modules/_bundled/sirsoft-page/CHANGELOG.md b/modules/_bundled/sirsoft-page/CHANGELOG.md index 3684f08e..514da00b 100644 --- a/modules/_bundled/sirsoft-page/CHANGELOG.md +++ b/modules/_bundled/sirsoft-page/CHANGELOG.md @@ -6,6 +6,11 @@ ## [1.0.3] - 2026-08-13 +### Security + +- 미발행(초안) 페이지에 붙은 이미지 첨부의 미리보기가 주소만 알면 누구에게나 열리던 문제를 수정했습니다. 다운로드는 이미 발행 상태를 확인하고 있었지만 미리보기는 확인하지 않아, 발행 전 콘텐츠의 이미지가 새어 나갈 수 있었습니다. 이제 미리보기도 다운로드와 같은 기준을 적용해, 발행된 페이지의 첨부는 누구나, 미발행 페이지의 첨부는 페이지 조회 권한이 있는 관리자만 미리볼 수 있습니다. 편집 중인 초안의 썸네일은 편집 권한이 있는 관리자에게 종전처럼 정상 표시됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 페이지 일괄 발행/발행취소와 첨부파일 삭제·순서 변경에 담당 범위 제한을 적용했습니다. 페이지를 하나씩 여는 화면에는 이 확인이 있었지만 목록에서 여러 건을 한 번에 처리하는 경로와 첨부 관리 경로에는 없어, "본인이 만든 페이지만" 으로 범위를 좁혀 위임받은 관리자가 담당 밖 페이지까지 발행 상태를 바꾸거나 첨부를 지울 수 있었습니다. 이제 대상마다 같은 기준으로 확인하며, 범위 밖 대상이 섞이면 요청 전체를 거부하고 아무것도 변경하지 않습니다. 범위 제한 없이 위임받은 관리자의 작업은 종전처럼 정상 동작합니다. (KVE-2026-1919) + ### Changed - 첨부 개수 상한 초과 안내 문구를 만드는 시점이 응답을 만드는 시점과 어긋나 있던 것을 맞췄습니다. 표시되는 문구와 상태는 종전과 동일합니다. diff --git a/modules/_bundled/sirsoft-page/docs/api/pages.md b/modules/_bundled/sirsoft-page/docs/api/pages.md index 4c8a12c4..6beacd3d 100644 --- a/modules/_bundled/sirsoft-page/docs/api/pages.md +++ b/modules/_bundled/sirsoft-page/docs/api/pages.md @@ -1230,11 +1230,11 @@ Content-Disposition: inline | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 404 | Not Found | 해시에 해당하는 첨부가 없는 경우(`첨부파일을 찾을 수 없습니다.`), 첨부가 이미지가 아니거나 스토리지에 실제 파일이 없는 경우(`첨부파일이 스토리지에 존재하지 않습니다.`). 미리보기는 발행/미발행과 무관하게 공개 서빙되므로 권한에 의한 404 는 없습니다 | +| 404 | Not Found | 해시에 해당하는 첨부가 없는 경우(`첨부파일을 찾을 수 없습니다.`), 미발행 페이지의 첨부를 `sirsoft-page.pages.read` 권한 없이 요청한 경우(존재를 숨기기 위해 동일하게 404), 첨부가 이미지가 아니거나 스토리지에 실제 파일이 없는 경우(`첨부파일이 스토리지에 존재하지 않습니다.`) | -**설명** 이미지 첨부 썸네일을 인라인으로 미리봅니다(해시 기반, 12자). 썸네일 `` 는 토큰을 실을 수 없으므로 발행/미발행과 무관하게 공개 서빙합니다. 미발행 콘텐츠의 썸네일은 해시를 보유해야만 조회 가능(비추측성)하며, 실제 파일 다운로드는 `download` 의 권한 게이트로 보호됩니다. 이미지 스트리밍 응답이므로 실측 대상이 아닙니다. +**설명** 이미지 첨부 썸네일을 인라인으로 미리봅니다(해시 기반, 12자). `download` 와 동일한 발행상태 게이트를 적용합니다 — 발행된 페이지의 썸네일은 누구나, 미발행 페이지의 썸네일은 `sirsoft-page.pages.read` 권한 관리자만 미리볼 수 있습니다. 편집 중인 초안(미발행)의 `` 썸네일은 편집 권한을 가진 관리자에게 정상 노출되고, 무인가 사용자에게는 미발행 썸네일이 차단됩니다. 이미지 스트리밍 응답이므로 실측 대상이 아닙니다. ### GET /api/modules/sirsoft-page/pages/{slug} 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..92241e1e 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; * * 첨부파일 서빙(다운로드/미리보기)을 처리합니다. 썸네일 ·다운로드는 브라우저 직접 * GET 이라 토큰을 실을 수 없으므로 공개 hash 라우트로 단일화합니다. - * - 미리보기(썸네일): 공개 서빙 (미발행 콘텐츠도 hash 보유 시 조회 가능 — 트레이드오프 수용) + * - 미리보기(썸네일): 발행 첨부는 누구나, 미발행 첨부는 pages.read 관리자만 (download 와 동일 게이트) * - 다운로드: 발행 첨부는 누구나, 미발행 첨부는 pages.read 관리자만 (파일 보호) + * + * preview 와 download 는 동일한 발행상태 게이트를 공유한다 — 한쪽만 열려 있으면 + * 미발행 콘텐츠가 무인가로 새는 경로가 남는다(preview↔download 정합). */ class PublicPageAttachmentController extends PublicBaseController { @@ -60,20 +63,34 @@ class PublicPageAttachmentController extends PublicBaseController /** * 이미지 첨부파일을 미리봅니다 (해시 기반, inline). * - * 썸네일 는 토큰을 실을 수 없으므로 발행/미발행 무관 공개 서빙합니다. - * 미발행 콘텐츠의 썸네일은 hash 를 보유해야만 조회 가능하며(비추측성), 파일 - * 다운로드는 download() 의 권한 게이트로 보호됩니다. + * 발행된 페이지의 썸네일은 누구나, 미발행 페이지의 썸네일은 페이지 조회 권한 + * (sirsoft-page.pages.read) 관리자만 미리볼 수 있습니다(download 와 동일 게이트). + * 편집 중인 초안(미발행)의 썸네일은 편집 권한을 가진 관리자가 보므로 + * 정상 노출되며, 무인가 사용자에게만 미발행 썸네일이 차단됩니다. * + * @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 와 동일 게이트) + $published = $attachment->page && $attachment->page->published; + $canReadUnpublished = $request->user()?->hasPermission( + 'sirsoft-page.pages.read', + PermissionType::Admin + ) ?? false; + + if (! $published && ! $canReadUnpublished) { + 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/Services/PageAttachmentService.php b/modules/_bundled/sirsoft-page/src/Services/PageAttachmentService.php index 0b3edee5..750eb722 100644 --- a/modules/_bundled/sirsoft-page/src/Services/PageAttachmentService.php +++ b/modules/_bundled/sirsoft-page/src/Services/PageAttachmentService.php @@ -4,6 +4,7 @@ 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; @@ -14,6 +15,7 @@ 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 +193,8 @@ class PageAttachmentService */ public function deleteAttachment(PageAttachment $attachment): bool { + $this->assertPageScope($attachment); + HookManager::doAction('sirsoft-page.attachment.before_delete', $attachment); // 물리 파일 삭제 @@ -212,6 +216,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 +227,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 $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/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'); - // 첨부 이미지 미리보기 (공개 - 토큰 없는 직접 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/User/PublicPageAttachmentAccessTest.php b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageAttachmentAccessTest.php new file mode 100644 index 00000000..ef061343 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageAttachmentAccessTest.php @@ -0,0 +1,198 @@ +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); + } +} 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/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md b/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md index d6f5e49a..9a7bdb1c 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md @@ -15,6 +15,11 @@ - 코어 최소 요구 버전을 7.0.7 로 상향했습니다. +### Changed + +- 코어 7.0.7의 외부 스크립트 보안 정책 강화에 맞춰, 에디터가 사용하는 CKEditor CDN 주소(`cdn.ckeditor.com`)를 이 플러그인의 신뢰 출처로 선언했습니다. 정책이 강화된 이후에도 위지윅 에디터가 정상적으로 로드됩니다. +- 신뢰 출처 선언 기능이 코어 7.0.7에서 도입되었으므로, 설치에 필요한 최소 코어 버전을 7.0.7로 조정했습니다. + ## [1.0.1] - 2026-08-10 ### Changed diff --git a/plugins/_bundled/sirsoft-ckeditor5/plugin.json b/plugins/_bundled/sirsoft-ckeditor5/plugin.json index bf904f78..a2c61edf 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/plugin.json +++ b/plugins/_bundled/sirsoft-ckeditor5/plugin.json @@ -18,6 +18,9 @@ }, "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-daum_postcode/CHANGELOG.md b/plugins/_bundled/sirsoft-daum_postcode/CHANGELOG.md index e720dab6..6be5a375 100644 --- a/plugins/_bundled/sirsoft-daum_postcode/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-daum_postcode/CHANGELOG.md @@ -4,6 +4,13 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-13 + +### Changed + +- 코어 7.0.7의 외부 스크립트 보안 정책 강화에 맞춰, 주소 검색에 쓰는 Daum 우편번호 스크립트 주소(`t1.daumcdn.net`)를 이 플러그인의 신뢰 출처로 선언했습니다. 정책이 강화된 이후에도 주소 검색 창이 정상적으로 열립니다. +- 신뢰 출처 선언 기능이 코어 7.0.7에서 도입되었으므로, 설치에 필요한 최소 코어 버전을 7.0.7로 조정했습니다. + ## [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-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-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/public/build/core/template-engine.min.js b/public/build/core/template-engine.min.js index 0065a8c1..5b1c2a66 100644 --- a/public/build/core/template-engine.min.js +++ b/public/build/core/template-engine.min.js @@ -1,20 +1,23 @@ -var _T=Object.defineProperty;var AT=(Qt,Xr,Jr)=>Xr in Qt?_T(Qt,Xr,{enumerable:!0,configurable:!0,writable:!0,value:Jr}):Qt[Xr]=Jr;var $=(Qt,Xr,Jr)=>AT(Qt,typeof Xr!="symbol"?Xr+"":Xr,Jr);(function(Qt){"use strict";function Xr(o,e){for(var n=0;na[i]})}}}return Object.freeze(Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}))}function Jr(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var td={exports:{}},Ye={};var Fg;function YS(){if(Fg)return Ye;Fg=1;var o=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),c=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),y=Symbol.for("react.activity"),S=Symbol.iterator;function v(N){return N===null||typeof N!="object"?null:(N=S&&N[S]||N["@@iterator"],typeof N=="function"?N:null)}var _={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,x={};function O(N,R,de){this.props=N,this.context=R,this.refs=x,this.updater=de||_}O.prototype.isReactComponent={},O.prototype.setState=function(N,R){if(typeof N!="object"&&typeof N!="function"&&N!=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,N,R,"setState")},O.prototype.forceUpdate=function(N){this.updater.enqueueForceUpdate(this,N,"forceUpdate")};function M(){}M.prototype=O.prototype;function T(N,R,de){this.props=N,this.context=R,this.refs=x,this.updater=de||_}var D=T.prototype=new M;D.constructor=T,A(D,O.prototype),D.isPureReactComponent=!0;var z=Array.isArray;function P(){}var q={H:null,A:null,T:null,S:null},W=Object.prototype.hasOwnProperty;function he(N,R,de){var fe=de.ref;return{$$typeof:o,type:N,key:R,ref:fe!==void 0?fe:null,props:de}}function Se(N,R){return he(N.type,R,N.props)}function be(N){return typeof N=="object"&&N!==null&&N.$$typeof===o}function Ue(N){var R={"=":"=0",":":"=2"};return"$"+N.replace(/[=:]/g,function(de){return R[de]})}var Fe=/\/+/g;function Ge(N,R){return typeof N=="object"&&N!==null&&N.key!=null?Ue(""+N.key):R.toString(36)}function wt(N){switch(N.status){case"fulfilled":return N.value;case"rejected":throw N.reason;default:switch(typeof N.status=="string"?N.then(P,P):(N.status="pending",N.then(function(R){N.status==="pending"&&(N.status="fulfilled",N.value=R)},function(R){N.status==="pending"&&(N.status="rejected",N.reason=R)})),N.status){case"fulfilled":return N.value;case"rejected":throw N.reason}}throw N}function J(N,R,de,fe,xe){var ee=typeof N;(ee==="undefined"||ee==="boolean")&&(N=null);var _e=!1;if(N===null)_e=!0;else switch(ee){case"bigint":case"string":case"number":_e=!0;break;case"object":switch(N.$$typeof){case o:case e:_e=!0;break;case m:return _e=N._init,J(_e(N._payload),R,de,fe,xe)}}if(_e)return xe=xe(N),_e=fe===""?"."+Ge(N,0):fe,z(xe)?(de="",_e!=null&&(de=_e.replace(Fe,"$&/")+"/"),J(xe,R,de,"",function(Ie){return Ie})):xe!=null&&(be(xe)&&(xe=Se(xe,de+(xe.key==null||N&&N.key===xe.key?"":(""+xe.key).replace(Fe,"$&/")+"/")+_e)),R.push(xe)),1;_e=0;var Be=fe===""?".":fe+":";if(z(N))for(var ke=0;ke>>1,pe=J[te];if(0>>1;tei(de,Me))fei(xe,de)?(J[te]=xe,J[fe]=Me,te=fe):(J[te]=de,J[R]=Me,te=R);else if(fei(xe,Me))J[te]=xe,J[fe]=Me,te=fe;else break e}}return ue}function i(J,ue){var Me=J.sortIndex-ue.sortIndex;return Me!==0?Me:J.id-ue.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,S=3,v=!1,_=!1,A=!1,x=!1,O=typeof setTimeout=="function"?setTimeout:null,M=typeof clearTimeout=="function"?clearTimeout:null,T=typeof setImmediate<"u"?setImmediate:null;function D(J){for(var ue=n(g);ue!==null;){if(ue.callback===null)a(g);else if(ue.startTime<=J)a(g),ue.sortIndex=ue.expirationTime,e(f,ue);else break;ue=n(g)}}function z(J){if(A=!1,D(J),!_)if(n(f)!==null)_=!0,P||(P=!0,Ue());else{var ue=n(g);ue!==null&&wt(z,ue.startTime-J)}}var P=!1,q=-1,W=5,he=-1;function Se(){return x?!0:!(o.unstable_now()-heJ&&Se());){var te=y.callback;if(typeof te=="function"){y.callback=null,S=y.priorityLevel;var pe=te(y.expirationTime<=J);if(J=o.unstable_now(),typeof pe=="function"){y.callback=pe,D(J),ue=!0;break t}y===n(f)&&a(f),D(J)}else a(f);y=n(f)}if(y!==null)ue=!0;else{var N=n(g);N!==null&&wt(z,N.startTime-J),ue=!1}}break e}finally{y=null,S=Me,v=!1}ue=void 0}}finally{ue?Ue():P=!1}}}var Ue;if(typeof T=="function")Ue=function(){T(be)};else if(typeof MessageChannel<"u"){var Fe=new MessageChannel,Ge=Fe.port2;Fe.port1.onmessage=be,Ue=function(){Ge.postMessage(null)}}else Ue=function(){O(be,0)};function wt(J,ue){q=O(function(){J(o.unstable_now())},ue)}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||125te?(J.sortIndex=Me,e(g,J),n(f)===null&&J===n(g)&&(A?(M(q),q=-1):A=!0,wt(z,Me-te))):(J.sortIndex=pe,e(f,J),_||v||(_=!0,P||(P=!0,Ue()))),J},o.unstable_shouldYield=Se,o.unstable_wrapCallback=function(J){var ue=S;return function(){var Me=S;S=ue;try{return J.apply(this,arguments)}finally{S=Me}}}})(id)),id}var Yg;function JS(){return Yg||(Yg=1,ad.exports=XS()),ad.exports}var sd={exports:{}},_n={};var Xg;function QS(){if(Xg)return _n;Xg=1;var o=nd();function e(f){var g="https://react.dev/errors/"+f;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(e){console.error(e)}}return o(),sd.exports=QS(),sd.exports}var Zg;function ZS(){if(Zg)return Zs;Zg=1;var o=JS(),e=nd(),n=Qg();function a(t){var r="https://react.dev/errors/"+t;if(1pe||(t.current=te[pe],te[pe]=null,pe--)}function de(t,r){pe++,te[pe]=t.current,t.current=r}var fe=N(null),xe=N(null),ee=N(null),_e=N(null);function Be(t,r){switch(de(ee,r),de(xe,t),de(fe,null),r.nodeType){case 9:case 11:t=(t=r.documentElement)&&(t=t.namespaceURI)?mS(t):0;break;default:if(t=r.tagName,r=r.namespaceURI)r=mS(r),t=yS(r,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}R(fe),de(fe,t)}function ke(){R(fe),R(xe),R(ee)}function Ie(t){t.memoizedState!==null&&de(_e,t);var r=fe.current,s=yS(r,t.type);r!==s&&(de(xe,t),de(fe,s))}function Ct(t){xe.current===t&&(R(fe),R(xe)),_e.current===t&&(R(_e),Ol._currentValue=Me)}var ot,Ot;function vt(t){if(ot===void 0)try{throw Error()}catch(s){var r=s.stack.trim().match(/\n( *(at )?)/);ot=r&&r[1]||"",Ot=-1)":-1h||U[u]!==X[h]){var ae=` -`+U[u].replace(" at new "," at ");return t.displayName&&ae.includes("")&&(ae=ae.replace("",t.displayName)),ae}while(1<=u&&0<=h);break}}}finally{_t=!1,Error.prepareStackTrace=s}return(s=t?t.displayName||t.name:"")?vt(s):""}function nt(t,r){switch(t.tag){case 26:case 27:case 5:return vt(t.type);case 16:return vt("Lazy");case 13:return t.child!==r&&r!==null?vt("Suspense Fallback"):vt("Suspense");case 19:return vt("SuspenseList");case 0:case 15:return Oe(t.type,!1);case 11:return Oe(t.type.render,!1);case 1:return Oe(t.type,!0);case 31:return vt("Activity");default:return""}}function sn(t){try{var r="",s=null;do r+=nt(t,s),s=t,t=t.return;while(t);return r}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;na[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 td={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>>1,ge=J[Z];if(0>>1;Zi(ce,Le))dei(ye,ce)?(J[Z]=ye,J[de]=Le,Z=de):(J[Z]=ce,J[T]=Le,Z=T);else if(dei(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()-peJ&&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||125Z?(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}}}})(id)),id}var sg;function hw(){return sg||(sg=1,ad.exports=fw()),ad.exports}var sd={exports:{}},Cn={};var og;function pw(){if(og)return Cn;og=1;var s=nd();function e(f){var h="https://react.dev/errors/"+f;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(s)}catch(e){console.error(e)}}return s(),sd.exports=pw(),sd.exports}var ug;function gw(){if(ug)return Zs;ug=1;var s=hw(),e=nd(),n=cg();function a(t){var r="https://react.dev/errors/"+t;if(1ge||(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)":-1p||z[u]!==X[p]){var re=` +`+z[u].replace(" at new "," at ");return t.displayName&&re.includes("")&&(re=re.replace("",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 Mt=Object.prototype.hasOwnProperty,Vt=o.unstable_scheduleCallback,on=o.unstable_cancelCallback,ln=o.unstable_shouldYield,Br=o.unstable_requestPaint,Ft=o.unstable_now,Ci=o.unstable_getCurrentPriorityLevel,na=o.unstable_ImmediatePriority,yn=o.unstable_UserBlockingPriority,On=o.unstable_NormalPriority,rt=o.unstable_LowPriority,Hn=o.unstable_IdlePriority,bn=o.log,ra=o.unstable_setDisableYieldValue,sr=null,cn=null;function Tn(t){if(typeof bn=="function"&&ra(t),cn&&typeof cn.setStrictMode=="function")try{cn.setStrictMode(sr,t)}catch{}}var un=Math.clz32?Math.clz32:ce,V=Math.log,se=Math.LN2;function ce(t){return t>>>=0,t===0?32:31-(V(t)/se|0)|0}var Z=256,ve=262144,ge=4194304;function Ce(t){var r=t&42;if(r!==0)return r;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Ae(t,r,s){var u=t.pendingLanes;if(u===0)return 0;var h=0,p=t.suspendedLanes,C=t.pingedLanes;t=t.warmLanes;var k=u&134217727;return k!==0?(u=k&~p,u!==0?h=Ce(u):(C&=k,C!==0?h=Ce(C):s||(s=k&~t,s!==0&&(h=Ce(s))))):(k=u&~p,k!==0?h=Ce(k):C!==0?h=Ce(C):s||(s=u&~t,s!==0&&(h=Ce(s)))),h===0?0:r!==0&&r!==h&&(r&p)===0&&(p=h&-h,s=r&-r,p>=s||p===32&&(s&4194048)!==0)?r:h}function Ke(t,r){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&r)===0}function Te(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 $e(){var t=ge;return ge<<=1,(ge&62914560)===0&&(ge=4194304),t}function Re(t){for(var r=[],s=0;31>s;s++)r.push(t);return r}function et(t,r){t.pendingLanes|=r,r!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function je(t,r,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,U=t.expirationTimes,X=t.hiddenUpdates;for(s=C&~s;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Lf=/[\n"\\]/g;function zn(t){return t.replace(Lf,function(r){return"\\"+r.charCodeAt(0).toString(16)+" "})}function vs(t,r,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"),r!=null?C==="number"?(r===0&&t.value===""||t.value!=r)&&(t.value=""+kn(r)):t.value!==""+kn(r)&&(t.value=""+kn(r)):C!=="submit"&&C!=="reset"||t.removeAttribute("value"),r!=null?ia(t,C,kn(r)):s!=null?ia(t,C,kn(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=""+kn(k):t.removeAttribute("name")}function Ai(t,r,s,u,h,p,C,k){if(p!=null&&typeof p!="function"&&typeof p!="symbol"&&typeof p!="boolean"&&(t.type=p),r!=null||s!=null){if(!(p!=="submit"&&p!=="reset"||r!=null)){Uo(t);return}s=s!=null?""+kn(s):"",r=r!=null?""+kn(r):s,k||r===t.value||(t.value=r),t.defaultValue=r}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),Uo(t)}function ia(t,r,s){r==="number"&&ja(t.ownerDocument)===t||t.defaultValue===""+s||(t.defaultValue=""+s)}function za(t,r,s,u){if(t=t.options,r){r={};for(var h=0;h"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Vo=!1;if(kr)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 Vr=null,Fo=null,ws=null;function Pc(){if(ws)return ws;var t,r=Fo,s=r.length,u,h="value"in Vr?Vr.value:Vr.textContent,p=h.length;for(t=0;t=re),We=" ",dt=!1;function pt(t,r){switch(t){case"keyup":return L.indexOf(r.keyCode)!==-1;case"keydown":return r.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Pt(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Or=!1;function Xf(t,r){switch(t){case"compositionend":return Pt(r);case"keypress":return r.which!==32?null:(dt=!0,We);case"textInput":return t=r.data,t===We&&dt?null:t;default:return null}}function Wc(t,r){if(Or)return t==="compositionend"||!I&&pt(t,r)?(t=Pc(),ws=Fo=Vr=null,Or=!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)return{node:s,offset:r-t};t=u}e:{for(;s;){if(s.nextSibling){s=s.nextSibling;break e}s=s.parentNode}s=void 0}s=$y(s)}}function Iy(t,r){return t&&r?t===r?!0:t&&t.nodeType===3?!1:r&&r.nodeType===3?Iy(t,r.parentNode):"contains"in t?t.contains(r):t.compareDocumentPosition?!!(t.compareDocumentPosition(r)&16):!1:!1}function Hy(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var r=ja(t.document);r instanceof t.HTMLIFrameElement;){try{var s=typeof r.contentWindow.location.href=="string"}catch{s=!1}if(s)t=r.contentWindow;else break;r=ja(t.document)}return r}function th(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 rx=kr&&"documentMode"in document&&11>=document.documentMode,As=null,nh=null,nl=null,rh=!1;function jy(t,r,s){var u=s.window===s?s.document:s.nodeType===9?s:s.ownerDocument;rh||As==null||As!==ja(u)||(u=As,"selectionStart"in u&&th(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=Uu(nh,"onSelect"),0>=C,h-=C,Fr=1<<32-un(r)+h|s<Qe?(ct=Ne,Ne=null):ct=Ne.sibling;var yt=Q(K,Ne,Y[Qe],oe);if(yt===null){Ne===null&&(Ne=ct);break}t&&Ne&&yt.alternate===null&&r(K,Ne),G=p(yt,G,Qe),mt===null?Pe=yt:mt.sibling=yt,mt=yt,Ne=ct}if(Qe===Y.length)return s(K,Ne),ft&&ca(K,Qe),Pe;if(Ne===null){for(;QeQe?(ct=Ne,Ne=null):ct=Ne.sibling;var di=Q(K,Ne,yt.value,oe);if(di===null){Ne===null&&(Ne=ct);break}t&&Ne&&di.alternate===null&&r(K,Ne),G=p(di,G,Qe),mt===null?Pe=di:mt.sibling=di,mt=di,Ne=ct}if(yt.done)return s(K,Ne),ft&&ca(K,Qe),Pe;if(Ne===null){for(;!yt.done;Qe++,yt=Y.next())yt=le(K,yt.value,oe),yt!==null&&(G=p(yt,G,Qe),mt===null?Pe=yt:mt.sibling=yt,mt=yt);return ft&&ca(K,Qe),Pe}for(Ne=u(Ne);!yt.done;Qe++,yt=Y.next())yt=ne(Ne,K,Qe,yt.value,oe),yt!==null&&(t&&yt.alternate!==null&&Ne.delete(yt.key===null?Qe:yt.key),G=p(yt,G,Qe),mt===null?Pe=yt:mt.sibling=yt,mt=yt);return t&&Ne.forEach(function(ET){return r(K,ET)}),ft&&ca(K,Qe),Pe}function Dt(K,G,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;G!==null;){if(G.key===Pe){if(Pe=Y.type,Pe===A){if(G.tag===7){s(K,G.sibling),oe=h(G,Y.props.children),oe.return=K,K=oe;break e}}else if(G.elementType===Pe||typeof Pe=="object"&&Pe!==null&&Pe.$$typeof===W&&qi(Pe)===G.type){s(K,G.sibling),oe=h(G,Y.props),ll(oe,Y),oe.return=K,K=oe;break e}s(K,G);break}else r(K,G);G=G.sibling}Y.type===A?(oe=ji(Y.props.children,K.mode,oe,Y.key),oe.return=K,K=oe):(oe=ru(Y.type,Y.key,Y.props,null,K.mode,oe),ll(oe,Y),oe.return=K,K=oe)}return C(K);case _:e:{for(Pe=Y.key;G!==null;){if(G.key===Pe)if(G.tag===4&&G.stateNode.containerInfo===Y.containerInfo&&G.stateNode.implementation===Y.implementation){s(K,G.sibling),oe=h(G,Y.children||[]),oe.return=K,K=oe;break e}else{s(K,G);break}else r(K,G);G=G.sibling}oe=uh(Y,K.mode,oe),oe.return=K,K=oe}return C(K);case W:return Y=qi(Y),Dt(K,G,Y,oe)}if(wt(Y))return De(K,G,Y,oe);if(Ue(Y)){if(Pe=Ue(Y),typeof Pe!="function")throw Error(a(150));return Y=Pe.call(Y),qe(K,G,Y,oe)}if(typeof Y.then=="function")return Dt(K,G,uu(Y),oe);if(Y.$$typeof===T)return Dt(K,G,su(K,Y),oe);du(K,Y)}return typeof Y=="string"&&Y!==""||typeof Y=="number"||typeof Y=="bigint"?(Y=""+Y,G!==null&&G.tag===6?(s(K,G.sibling),oe=h(G,Y),oe.return=K,K=oe):(s(K,G),oe=ch(Y,K.mode,oe),oe.return=K,K=oe),C(K)):s(K,G)}return function(K,G,Y,oe){try{ol=0;var Pe=Dt(K,G,Y,oe);return Is=null,Pe}catch(Ne){if(Ne===Ns||Ne===lu)throw Ne;var mt=Xn(29,Ne,null,K.mode);return mt.lanes=oe,mt.return=K,mt}}}var Vi=ob(!0),lb=ob(!1),Wa=!1;function Ch(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Eh(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 Ya(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Xa(t,r,s){var u=t.updateQueue;if(u===null)return null;if(u=u.shared,(St&2)!==0){var h=u.pending;return h===null?r.next=r:(r.next=h.next,h.next=r),u.pending=r,r=nu(t),Vy(t,null,s),r}return tu(t,u,r,s),nu(t)}function cl(t,r,s){if(r=r.updateQueue,r!==null&&(r=r.shared,(s&4194048)!==0)){var u=r.lanes;u&=t.pendingLanes,s|=u,r.lanes=s,we(t,s)}}function _h(t,r){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=r:p=p.next=r}else h=p=r;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=r:t.next=r,s.lastBaseUpdate=r}var Ah=!1;function ul(){if(Ah){var t=$s;if(t!==null)throw t}}function dl(t,r,s,u){Ah=!1;var h=t.updateQueue;Wa=!1;var p=h.firstBaseUpdate,C=h.lastBaseUpdate,k=h.shared.pending;if(k!==null){h.shared.pending=null;var U=k,X=U.next;U.next=null,C===null?p=X:C.next=X,C=U;var ae=t.alternate;ae!==null&&(ae=ae.updateQueue,k=ae.lastBaseUpdate,k!==C&&(k===null?ae.firstBaseUpdate=X:k.next=X,ae.lastBaseUpdate=U))}if(p!==null){var le=h.baseState;C=0,ae=X=U=null,k=p;do{var Q=k.lane&-536870913,ne=Q!==k.lane;if(ne?(lt&Q)===Q:(u&Q)===Q){Q!==0&&Q===Ms&&(Ah=!0),ae!==null&&(ae=ae.next={lane:0,tag:k.tag,payload:k.payload,callback:null,next:null});e:{var De=t,qe=k;Q=r;var Dt=s;switch(qe.tag){case 1:if(De=qe.payload,typeof De=="function"){le=De.call(Dt,le,Q);break e}le=De;break e;case 3:De.flags=De.flags&-65537|128;case 0:if(De=qe.payload,Q=typeof De=="function"?De.call(Dt,le,Q):De,Q==null)break e;le=y({},le,Q);break e;case 2:Wa=!0}}Q=k.callback,Q!==null&&(t.flags|=64,ne&&(t.flags|=8192),ne=h.callbacks,ne===null?h.callbacks=[Q]:ne.push(Q))}else ne={lane:Q,tag:k.tag,payload:k.payload,callback:k.callback,next:null},ae===null?(X=ae=ne,U=le):ae=ae.next=ne,C|=Q;if(k=k.next,k===null){if(k=h.shared.pending,k===null)break;ne=k,k=ne.next,ne.next=null,h.lastBaseUpdate=ne,h.shared.pending=null}}while(!0);ae===null&&(U=le),h.baseState=U,h.firstBaseUpdate=X,h.lastBaseUpdate=ae,p===null&&(h.shared.lanes=0),ti|=C,t.lanes=C,t.memoizedState=le}}function cb(t,r){if(typeof t!="function")throw Error(a(191,t));t.call(r)}function ub(t,r){var s=t.callbacks;if(s!==null)for(t.callbacks=null,t=0;tp?p:8;var C=J.T,k={};J.T=k,Gh(t,!1,r,s);try{var U=h(),X=J.S;if(X!==null&&X(k,U),U!==null&&typeof U=="object"&&typeof U.then=="function"){var ae=fx(U,u);gl(t,r,ae,tr(t))}else gl(t,r,u,tr(t))}catch(le){gl(t,r,{then:function(){},status:"rejected",reason:le},tr())}finally{ue.p=p,C!==null&&k.types!==null&&(C.types=k.types),J.T=C}}function bx(){}function Bh(t,r,s,u){if(t.tag!==5)throw Error(a(476));var h=Bb(t).queue;Pb(t,h,r,Me,s===null?bx:function(){return qb(t),s(u)})}function Bb(t){var r=t.memoizedState;if(r!==null)return r;r={memoizedState:Me,baseState:Me,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ha,lastRenderedState:Me},next:null};var s={};return r.next={memoizedState:s,baseState:s,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ha,lastRenderedState:s},next:null},t.memoizedState=r,t=t.alternate,t!==null&&(t.memoizedState=r),r}function qb(t){var r=Bb(t);r.next===null&&(r=t.alternate.memoizedState),gl(t,r.next.queue,{},tr())}function qh(){return wn(Ol)}function Gb(){return Jt().memoizedState}function Vb(){return Jt().memoizedState}function vx(t){for(var r=t.return;r!==null;){switch(r.tag){case 24:case 3:var s=tr();t=Ya(s);var u=Xa(r,t,s);u!==null&&(Gn(u,r,s),cl(u,r,s)),r={cache:bh()},t.payload=r;return}r=r.return}}function Sx(t,r,s){var u=tr();s={lane:u,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null},wu(t)?Kb(r,s):(s=oh(t,r,s,u),s!==null&&(Gn(s,t,u),Wb(s,r,u)))}function Fb(t,r,s){var u=tr();gl(t,r,s,u)}function gl(t,r,s,u){var h={lane:u,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null};if(wu(t))Kb(r,h);else{var p=t.alternate;if(t.lanes===0&&(p===null||p.lanes===0)&&(p=r.lastRenderedReducer,p!==null))try{var C=r.lastRenderedState,k=p(C,s);if(h.hasEagerState=!0,h.eagerState=k,Yn(k,C))return tu(t,r,h,0),Lt===null&&eu(),!1}catch{}if(s=oh(t,r,h,u),s!==null)return Gn(s,t,u),Wb(s,r,u),!0}return!1}function Gh(t,r,s,u){if(u={lane:2,revertLane:Cg(),gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null},wu(t)){if(r)throw Error(a(479))}else r=oh(t,s,u,2),r!==null&&Gn(r,t,2)}function wu(t){var r=t.alternate;return t===Je||r!==null&&r===Je}function Kb(t,r){js=gu=!0;var s=t.pending;s===null?r.next=r:(r.next=s.next,s.next=r),t.pending=r}function Wb(t,r,s){if((s&4194048)!==0){var u=r.lanes;u&=t.pendingLanes,s|=u,r.lanes=s,we(t,s)}}var pl={readContext:wn,use:yu,useCallback:Kt,useContext:Kt,useEffect:Kt,useImperativeHandle:Kt,useLayoutEffect:Kt,useInsertionEffect:Kt,useMemo:Kt,useReducer:Kt,useRef:Kt,useState:Kt,useDebugValue:Kt,useDeferredValue:Kt,useTransition:Kt,useSyncExternalStore:Kt,useId:Kt,useHostTransitionStatus:Kt,useFormState:Kt,useActionState:Kt,useOptimistic:Kt,useMemoCache:Kt,useCacheRefresh:Kt};pl.useEffectEvent=Kt;var Yb={readContext:wn,use:yu,useCallback:function(t,r){return Mn().memoizedState=[t,r===void 0?null:r],t},useContext:wn,useEffect:Lb,useImperativeHandle:function(t,r,s){s=s!=null?s.concat([t]):null,vu(4194308,4,Ib.bind(null,r,t),s)},useLayoutEffect:function(t,r){return vu(4194308,4,t,r)},useInsertionEffect:function(t,r){vu(4,2,t,r)},useMemo:function(t,r){var s=Mn();r=r===void 0?null:r;var u=t();if(Fi){Tn(!0);try{t()}finally{Tn(!1)}}return s.memoizedState=[u,r],u},useReducer:function(t,r,s){var u=Mn();if(s!==void 0){var h=s(r);if(Fi){Tn(!0);try{s(r)}finally{Tn(!1)}}}else h=r;return u.memoizedState=u.baseState=h,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:h},u.queue=t,t=t.dispatch=Sx.bind(null,Je,t),[u.memoizedState,t]},useRef:function(t){var r=Mn();return t={current:t},r.memoizedState=t},useState:function(t){t=Hh(t);var r=t.queue,s=Fb.bind(null,Je,r);return r.dispatch=s,[t.memoizedState,s]},useDebugValue:Uh,useDeferredValue:function(t,r){var s=Mn();return Ph(s,t,r)},useTransition:function(){var t=Hh(!1);return t=Pb.bind(null,Je,t.queue,!0,!1),Mn().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,r,s){var u=Je,h=Mn();if(ft){if(s===void 0)throw Error(a(407));s=s()}else{if(s=r(),Lt===null)throw Error(a(349));(lt&127)!==0||mb(u,r,s)}h.memoizedState=s;var p={value:s,getSnapshot:r};return h.queue=p,Lb(bb.bind(null,u,p,t),[t]),u.flags|=2048,Us(9,{destroy:void 0},yb.bind(null,u,p,s,r),null),s},useId:function(){var t=Mn(),r=Lt.identifierPrefix;if(ft){var s=Kr,u=Fr;s=(u&~(1<<32-un(u)-1)).toString(32)+s,r="_"+r+"R_"+s,s=pu++,0<\/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[Ut]=r,p[Rn]=u;e:for(C=r.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===r)break e;for(;C.sibling===null;){if(C.return===null||C.return===r)break e;C=C.return}C.sibling.return=C.return,C=C.sibling}r.stateNode=p;e:switch(En(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&&pa(r)}}return Nt(r),ag(r,r.type,t===null?null:t.memoizedProps,r.pendingProps,s),null;case 6:if(t&&r.stateNode!=null)t.memoizedProps!==u&&pa(r);else{if(typeof u!="string"&&r.stateNode===null)throw Error(a(166));if(t=ee.current,Os(r)){if(t=r.stateNode,s=r.memoizedProps,u=null,h=Sn,h!==null)switch(h.tag){case 27:case 5:u=h.memoizedProps}t[Ut]=r,t=!!(t.nodeValue===s||u!==null&&u.suppressHydrationWarning===!0||gS(t.nodeValue,s)),t||Fa(r,!0)}else t=Pu(t).createTextNode(u),t[Ut]=r,r.stateNode=t}return Nt(r),null;case 31:if(s=r.memoizedState,t===null||t.memoizedState!==null){if(u=Os(r),s!==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[Ut]=r}else zi(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;Nt(r),t=!1}else s=gh(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=s),t=!0;if(!t)return r.flags&256?(Qn(r),r):(Qn(r),null);if((r.flags&128)!==0)throw Error(a(558))}return Nt(r),null;case 13:if(u=r.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(h=Os(r),u!==null&&u.dehydrated!==null){if(t===null){if(!h)throw Error(a(318));if(h=r.memoizedState,h=h!==null?h.dehydrated:null,!h)throw Error(a(317));h[Ut]=r}else zi(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;Nt(r),h=!1}else h=gh(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=h),h=!0;if(!h)return r.flags&256?(Qn(r),r):(Qn(r),null)}return Qn(r),(r.flags&128)!==0?(r.lanes=s,r):(s=u!==null,t=t!==null&&t.memoizedState!==null,s&&(u=r.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&&(r.child.flags|=8192),xu(r,r.updateQueue),Nt(r),null);case 4:return ke(),t===null&&xg(r.stateNode.containerInfo),Nt(r),null;case 10:return da(r.type),Nt(r),null;case 19:if(R(Xt),u=r.memoizedState,u===null)return Nt(r),null;if(h=(r.flags&128)!==0,p=u.rendering,p===null)if(h)yl(u,!1);else{if(Wt!==0||t!==null&&(t.flags&128)!==0)for(t=r.child;t!==null;){if(p=hu(t),p!==null){for(r.flags|=128,yl(u,!1),t=p.updateQueue,r.updateQueue=t,xu(r,t),r.subtreeFlags=0,t=s,s=r.child;s!==null;)Fy(s,t),s=s.sibling;return de(Xt,Xt.current&1|2),ft&&ca(r,u.treeForkCount),r.child}t=t.sibling}u.tail!==null&&Ft()>Ou&&(r.flags|=128,h=!0,yl(u,!1),r.lanes=4194304)}else{if(!h)if(t=hu(p),t!==null){if(r.flags|=128,h=!0,t=t.updateQueue,r.updateQueue=t,xu(r,t),yl(u,!0),u.tail===null&&u.tailMode==="hidden"&&!p.alternate&&!ft)return Nt(r),null}else 2*Ft()-u.renderingStartTime>Ou&&s!==536870912&&(r.flags|=128,h=!0,yl(u,!1),r.lanes=4194304);u.isBackwards?(p.sibling=r.child,r.child=p):(t=u.last,t!==null?t.sibling=p:r.child=p,u.last=p)}return u.tail!==null?(t=u.tail,u.rendering=t,u.tail=t.sibling,u.renderingStartTime=Ft(),t.sibling=null,s=Xt.current,de(Xt,h?s&1|2:s&1),ft&&ca(r,u.treeForkCount),t):(Nt(r),null);case 22:case 23:return Qn(r),Th(),u=r.memoizedState!==null,t!==null?t.memoizedState!==null!==u&&(r.flags|=8192):u&&(r.flags|=8192),u?(s&536870912)!==0&&(r.flags&128)===0&&(Nt(r),r.subtreeFlags&6&&(r.flags|=8192)):Nt(r),s=r.updateQueue,s!==null&&xu(r,s.retryQueue),s=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(s=t.memoizedState.cachePool.pool),u=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(u=r.memoizedState.cachePool.pool),u!==s&&(r.flags|=2048),t!==null&&R(Bi),null;case 24:return s=null,t!==null&&(s=t.memoizedState.cache),r.memoizedState.cache!==s&&(r.flags|=2048),da(en),Nt(r),null;case 25:return null;case 30:return null}throw Error(a(156,r.tag))}function Ax(t,r){switch(fh(r),r.tag){case 1:return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 3:return da(en),ke(),t=r.flags,(t&65536)!==0&&(t&128)===0?(r.flags=t&-65537|128,r):null;case 26:case 27:case 5:return Ct(r),null;case 31:if(r.memoizedState!==null){if(Qn(r),r.alternate===null)throw Error(a(340));zi()}return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 13:if(Qn(r),t=r.memoizedState,t!==null&&t.dehydrated!==null){if(r.alternate===null)throw Error(a(340));zi()}return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 19:return R(Xt),null;case 4:return ke(),null;case 10:return da(r.type),null;case 22:case 23:return Qn(r),Th(),t!==null&&R(Bi),t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 24:return da(en),null;case 25:return null;default:return null}}function vv(t,r){switch(fh(r),r.tag){case 3:da(en),ke();break;case 26:case 27:case 5:Ct(r);break;case 4:ke();break;case 31:r.memoizedState!==null&&Qn(r);break;case 13:Qn(r);break;case 19:R(Xt);break;case 10:da(r.type);break;case 22:case 23:Qn(r),Th(),t!==null&&R(Bi);break;case 24:da(en)}}function bl(t,r){try{var s=r.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){xt(r,r.return,k)}}function Za(t,r,s){try{var u=r.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=r;var U=s,X=k;try{X()}catch(ae){xt(h,U,ae)}}}u=u.next}while(u!==p)}}catch(ae){xt(r,r.return,ae)}}function Sv(t){var r=t.updateQueue;if(r!==null){var s=t.stateNode;try{ub(r,s)}catch(u){xt(t,t.return,u)}}}function wv(t,r,s){s.props=Ki(t.type,t.memoizedProps),s.state=t.memoizedState;try{s.componentWillUnmount()}catch(u){xt(t,r,u)}}function vl(t,r){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){xt(t,r,h)}}function Wr(t,r){var s=t.ref,u=t.refCleanup;if(s!==null)if(typeof u=="function")try{u()}catch(h){xt(t,r,h)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof s=="function")try{s(null)}catch(h){xt(t,r,h)}else s.current=null}function Cv(t){var r=t.type,s=t.memoizedProps,u=t.stateNode;try{e:switch(r){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){xt(t,t.return,h)}}function ig(t,r,s){try{var u=t.stateNode;Kx(u,t.type,s,r),u[Rn]=r}catch(h){xt(t,t.return,h)}}function Ev(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&si(t.type)||t.tag===4}function sg(t){e:for(;;){for(;t.sibling===null;){if(t.return===null||Ev(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&&si(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 og(t,r,s){var u=t.tag;if(u===5||u===6)t=t.stateNode,r?(s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s).insertBefore(t,r):(r=s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s,r.appendChild(t),s=s._reactRootContainer,s!=null||r.onclick!==null||(r.onclick=Rr));else if(u!==4&&(u===27&&si(t.type)&&(s=t.stateNode,r=null),t=t.child,t!==null))for(og(t,r,s),t=t.sibling;t!==null;)og(t,r,s),t=t.sibling}function Tu(t,r,s){var u=t.tag;if(u===5||u===6)t=t.stateNode,r?s.insertBefore(t,r):s.appendChild(t);else if(u!==4&&(u===27&&si(t.type)&&(s=t.stateNode),t=t.child,t!==null))for(Tu(t,r,s),t=t.sibling;t!==null;)Tu(t,r,s),t=t.sibling}function _v(t){var r=t.stateNode,s=t.memoizedProps;try{for(var u=t.type,h=r.attributes;h.length;)r.removeAttributeNode(h[0]);En(r,u,s),r[Ut]=t,r[Rn]=s}catch(p){xt(t,t.return,p)}}var ma=!1,rn=!1,lg=!1,Av=typeof WeakSet=="function"?WeakSet:Set,pn=null;function xx(t,r){if(t=t.containerInfo,kg=Wu,t=Hy(t),th(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,U=-1,X=0,ae=0,le=t,Q=null;t:for(;;){for(var ne;le!==s||h!==0&&le.nodeType!==3||(k=C+h),le!==p||u!==0&&le.nodeType!==3||(U=C+u),le.nodeType===3&&(C+=le.nodeValue.length),(ne=le.firstChild)!==null;)Q=le,le=ne;for(;;){if(le===t)break t;if(Q===s&&++X===h&&(k=C),Q===p&&++ae===u&&(U=C),(ne=le.nextSibling)!==null)break;le=Q,Q=le.parentNode}le=ne}s=k===-1||U===-1?null:{start:k,end:U}}else s=null}s=s||{start:0,end:0}}else s=null;for(Dg={focusedElem:t,selectionRange:s},Wu=!1,pn=r;pn!==null;)if(r=pn,t=r.child,(r.subtreeFlags&1028)!==0&&t!==null)t.return=r,pn=t;else for(;pn!==null;){switch(r=pn,p=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(s=0;s title"))),En(p,u,s),p[Ut]=t,Zt(p),u=p;break e;case"link":var C=OS("link","href",h).get(u+(s.href||""));if(C){for(var k=0;kDt&&(C=Dt,Dt=qe,qe=C);var K=Ny(k,qe),G=Ny(k,Dt);if(K&&G&&(ne.rangeCount!==1||ne.anchorNode!==K.node||ne.anchorOffset!==K.offset||ne.focusNode!==G.node||ne.focusOffset!==G.offset)){var Y=le.createRange();Y.setStart(K.node,K.offset),ne.removeAllRanges(),qe>Dt?(ne.addRange(Y),ne.extend(G.node,G.offset)):(Y.setEnd(G.node,G.offset),ne.addRange(Y))}}}}for(le=[],ne=k;ne=ne.parentNode;)ne.nodeType===1&&le.push({element:ne,left:ne.scrollLeft,top:ne.scrollTop});for(typeof k.focus=="function"&&k.focus(),k=0;ks?32:s,J.T=null,s=pg,pg=null;var p=ri,C=wa;if(fn=0,Vs=ri=null,wa=0,(St&6)!==0)throw Error(a(331));var k=St;if(St|=4,Iv(p.current),Mv(p,p.current,C,s),St=k,Al(0,!1),cn&&typeof cn.onPostCommitFiberRoot=="function")try{cn.onPostCommitFiberRoot(sr,p)}catch{}return!0}finally{ue.p=h,J.T=u,eS(t,r)}}function nS(t,r,s){r=cr(s,r),r=Wh(t.stateNode,r,2),t=Xa(t,r,2),t!==null&&(et(t,2),Yr(t))}function xt(t,r,s){if(t.tag===3)nS(t,t,s);else for(;r!==null;){if(r.tag===3){nS(r,t,s);break}else if(r.tag===1){var u=r.stateNode;if(typeof r.type.getDerivedStateFromError=="function"||typeof u.componentDidCatch=="function"&&(ni===null||!ni.has(u))){t=cr(s,t),s=rv(2),u=Xa(r,s,2),u!==null&&(av(s,u,r,t),et(u,2),Yr(u));break}}r=r.return}}function vg(t,r,s){var u=t.pingCache;if(u===null){u=t.pingCache=new kx;var h=new Set;u.set(r,h)}else h=u.get(r),h===void 0&&(h=new Set,u.set(r,h));h.has(s)||(dg=!0,h.add(s),t=$x.bind(null,t,r,s),r.then(t,t))}function $x(t,r,s){var u=t.pingCache;u!==null&&u.delete(r),t.pingedLanes|=t.suspendedLanes&s,t.warmLanes&=~s,Lt===t&&(lt&s)===s&&(Wt===4||Wt===3&&(lt&62914560)===lt&&300>Ft()-Du?(St&2)===0&&Fs(t,0):fg|=s,Gs===lt&&(Gs=0)),Yr(t)}function rS(t,r){r===0&&(r=$e()),t=Hi(t,r),t!==null&&(et(t,r),Yr(t))}function Nx(t){var r=t.memoizedState,s=0;r!==null&&(s=r.retryLane),rS(t,s)}function Ix(t,r){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(r),rS(t,s)}function Hx(t,r){return Vt(t,r)}var Hu=null,Ws=null,Sg=!1,ju=!1,wg=!1,ii=0;function Yr(t){t!==Ws&&t.next===null&&(Ws===null?Hu=Ws=t:Ws=Ws.next=t),ju=!0,Sg||(Sg=!0,zx())}function Al(t,r){if(!wg&&ju){wg=!0;do for(var s=!1,u=Hu;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-un(42|t)+1)-1,p&=h&~(C&~k),p=p&201326741?p&201326741|1:p?p|2:0}p!==0&&(s=!0,oS(u,p))}else p=lt,p=Ae(u,u===Lt?p:0,u.cancelPendingCommit!==null||u.timeoutHandle!==-1),(p&3)===0||Ke(u,p)||(s=!0,oS(u,p));u=u.next}while(s);wg=!1}}function jx(){aS()}function aS(){ju=Sg=!1;var t=0;ii!==0&&Yx()&&(t=ii);for(var r=Ft(),s=null,u=Hu;u!==null;){var h=u.next,p=iS(u,r);p===0?(u.next=null,s===null?Hu=h:s.next=h,h===null&&(Ws=s)):(s=u,(t!==0||(p&3)!==0)&&(ju=!0)),u=h}fn!==0&&fn!==5||Al(t),ii!==0&&(ii=0)}function iS(t,r){for(var s=t.suspendedLanes,u=t.pingedLanes,h=t.expirationTimes,p=t.pendingLanes&-62914561;0k)break;var ae=U.transferSize,le=U.initiatorType;ae&&pS(le)&&(U=U.responseEnd,C+=ae*(U"u"?null:document;function TS(t,r,s){var u=Ys;if(u&&typeof r=="string"&&r){var h=zn(r);h='link[rel="'+t+'"][href="'+h+'"]',typeof s=="string"&&(h+='[crossorigin="'+s+'"]'),xS.has(h)||(xS.add(h),t={rel:t,crossOrigin:s,href:r},u.querySelector(h)===null&&(r=u.createElement("link"),En(r,"link",t),Zt(r),u.head.appendChild(r)))}}function aT(t){Ca.D(t),TS("dns-prefetch",t,null)}function iT(t,r){Ca.C(t,r),TS("preconnect",t,r)}function sT(t,r,s){Ca.L(t,r,s);var u=Ys;if(u&&t&&r){var h='link[rel="preload"][as="'+zn(r)+'"]';r==="image"&&s&&s.imageSrcSet?(h+='[imagesrcset="'+zn(s.imageSrcSet)+'"]',typeof s.imageSizes=="string"&&(h+='[imagesizes="'+zn(s.imageSizes)+'"]')):h+='[href="'+zn(t)+'"]';var p=h;switch(r){case"style":p=Xs(t);break;case"script":p=Js(t)}pr.has(p)||(t=y({rel:"preload",href:r==="image"&&s&&s.imageSrcSet?void 0:t,as:r},s),pr.set(p,t),u.querySelector(h)!==null||r==="style"&&u.querySelector(kl(p))||r==="script"&&u.querySelector(Dl(p))||(r=u.createElement("link"),En(r,"link",t),Zt(r),u.head.appendChild(r)))}}function oT(t,r){Ca.m(t,r);var s=Ys;if(s&&t){var u=r&&typeof r.as=="string"?r.as:"script",h='link[rel="modulepreload"][as="'+zn(u)+'"][href="'+zn(t)+'"]',p=h;switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":p=Js(t)}if(!pr.has(p)&&(t=y({rel:"modulepreload",href:t},r),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(Dl(p)))return}u=s.createElement("link"),En(u,"link",t),Zt(u),s.head.appendChild(u)}}}function lT(t,r,s){Ca.S(t,r,s);var u=Ys;if(u&&t){var h=Ia(u).hoistableStyles,p=Xs(t);r=r||"default";var C=h.get(p);if(!C){var k={loading:0,preload:null};if(C=u.querySelector(kl(p)))k.loading=5;else{t=y({rel:"stylesheet",href:t,"data-precedence":r},s),(s=pr.get(p))&&Hg(t,s);var U=C=u.createElement("link");Zt(U),En(U,"link",t),U._p=new Promise(function(X,ae){U.onload=X,U.onerror=ae}),U.addEventListener("load",function(){k.loading|=1}),U.addEventListener("error",function(){k.loading|=2}),k.loading|=4,qu(C,r,u)}C={type:"stylesheet",instance:C,count:1,state:k},h.set(p,C)}}}function cT(t,r){Ca.X(t,r);var s=Ys;if(s&&t){var u=Ia(s).hoistableScripts,h=Js(t),p=u.get(h);p||(p=s.querySelector(Dl(h)),p||(t=y({src:t,async:!0},r),(r=pr.get(h))&&jg(t,r),p=s.createElement("script"),Zt(p),En(p,"link",t),s.head.appendChild(p)),p={type:"script",instance:p,count:1,state:null},u.set(h,p))}}function uT(t,r){Ca.M(t,r);var s=Ys;if(s&&t){var u=Ia(s).hoistableScripts,h=Js(t),p=u.get(h);p||(p=s.querySelector(Dl(h)),p||(t=y({src:t,async:!0,type:"module"},r),(r=pr.get(h))&&jg(t,r),p=s.createElement("script"),Zt(p),En(p,"link",t),s.head.appendChild(p)),p={type:"script",instance:p,count:1,state:null},u.set(h,p))}}function RS(t,r,s,u){var h=(h=ee.current)?Bu(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"?(r=Xs(s.href),s=Ia(h).hoistableStyles,u=s.get(r),u||(u={type:"style",instance:null,count:0,state:null},s.set(r,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=Xs(s.href);var p=Ia(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(kl(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||dT(h,t,s,C.state))),r&&u===null)throw Error(a(528,""));return C}if(r&&u!==null)throw Error(a(529,""));return null;case"script":return r=s.async,s=s.src,typeof s=="string"&&r&&typeof r!="function"&&typeof r!="symbol"?(r=Js(s),s=Ia(h).hoistableScripts,u=s.get(r),u||(u={type:"script",instance:null,count:0,state:null},s.set(r,u)),u):{type:"void",instance:null,count:0,state:null};default:throw Error(a(444,t))}}function Xs(t){return'href="'+zn(t)+'"'}function kl(t){return'link[rel="stylesheet"]['+t+"]"}function kS(t){return y({},t,{"data-precedence":t.precedence,precedence:null})}function dT(t,r,s,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}),En(r,"link",s),Zt(r),t.head.appendChild(r))}function Js(t){return'[src="'+zn(t)+'"]'}function Dl(t){return"script[async]"+t}function DS(t,r,s){if(r.count++,r.instance===null)switch(r.type){case"style":var u=t.querySelector('style[data-href~="'+zn(s.href)+'"]');if(u)return r.instance=u,Zt(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"),Zt(u),En(u,"style",h),qu(u,s.precedence,t),r.instance=u;case"stylesheet":h=Xs(s.href);var p=t.querySelector(kl(h));if(p)return r.state.loading|=4,r.instance=p,Zt(p),p;u=kS(s),(h=pr.get(h))&&Hg(u,h),p=(t.ownerDocument||t).createElement("link"),Zt(p);var C=p;return C._p=new Promise(function(k,U){C.onload=k,C.onerror=U}),En(p,"link",u),r.state.loading|=4,qu(p,s.precedence,t),r.instance=p;case"script":return p=Js(s.src),(h=t.querySelector(Dl(p)))?(r.instance=h,Zt(h),h):(u=s,(h=pr.get(p))&&(u=y({},s),jg(u,h)),t=t.ownerDocument||t,h=t.createElement("script"),Zt(h),En(h,"link",u),t.head.appendChild(h),r.instance=h);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,qu(u,s.precedence,t));return r.instance}function qu(t,r,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 title"):null)}function fT(t,r,s){if(s===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 MS(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function hT(t,r,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=Xs(u.href),p=r.querySelector(kl(h));if(p){r=p._p,r!==null&&typeof r=="object"&&typeof r.then=="function"&&(t.count++,t=Vu.bind(t),r.then(t,t)),s.state.loading|=4,s.instance=p,Zt(p);return}p=r.ownerDocument||r,u=kS(u),(h=pr.get(h))&&Hg(u,h),p=p.createElement("link"),Zt(p);var C=p;C._p=new Promise(function(k,U){C.onload=k,C.onerror=U}),En(p,"link",u),s.instance=p}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(s,r),(r=s.state.preload)&&(s.state.loading&3)===0&&(t.count++,s=Vu.bind(t),r.addEventListener("load",s),r.addEventListener("error",s))}}var zg=0;function gT(t,r){return t.stylesheets&&t.count===0&&Ku(t,t.stylesheets),0zg?50:800)+r);return t.unsuspend=s,function(){t.unsuspend=null,clearTimeout(u),clearTimeout(h)}}:null}function Vu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Ku(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Fu=null;function Ku(t,r){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Fu=new Map,r.forEach(pT,t),Fu=null,Vu.call(t))}function pT(t,r){if(!(r.state.loading&4)){var s=Fu.get(t);if(s)var u=s.get(null);else{s=new Map,Fu.set(t,s);for(var h=t.querySelectorAll("link[data-precedence],style[data-precedence]"),p=0;p"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(e){console.error(e)}}return o(),rd.exports=ZS(),rd.exports}var od=ew();const ld=Jr(od),eo=[],tw=50;let cd=!1;function tp(){try{return window.G7Core?.devTools}catch{return}}function Xi(o,e,n){const a=tp();if(cd&&a?.isEnabled?.()){a.trackLog?.(o,e,n);return}cd||(o==="error"||o==="warn")&&eo.length{n.isDebugEnabled()&&console.log(e,...a),Xi("log",o,a)},warn:(...a)=>{n.isDebugEnabled()&&console.warn(e,...a),Xi("warn",o,a)},error:(...a)=>{n.isDebugEnabled()&&console.error(e,...a),Xi("error",o,a)}}}to.getInstance();const Ji=ht("networkResilience"),np=2,rp=300,ap=2e3,nw=15e3;function dd(o){return o?.name==="AbortError"}function ip(o){return dd(o)?!1:o instanceof TypeError}let Nl=!1,sp=!1;function fd(){return Nl}function rw(){typeof window>"u"||sp||(sp=!0,window.addEventListener("pagehide",()=>{Nl=!0}),window.addEventListener("pageshow",o=>{o.persisted&&(Nl=!1)}),window.addEventListener("beforeunload",()=>{Nl=!0}))}function op(o,e,n){const a=Math.min(e*Math.pow(2,o),n),i=a*.25*(Math.random()*2-1);return Math.max(0,Math.round(a+i))}function lp(o){return new Promise(e=>setTimeout(e,o))}async function Il(o,e={}){const{retries:n=np,baseDelayMs:a=rp,maxDelayMs:i=ap,timeoutMs:l=nw,label:c=o,init:d}=e,f=n+1;let g;for(let m=0;m0?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(()=>{y=!0,v.abort()},l)}return await fetch(o,_)}catch(_){if(g=_,dd(_)&&!y||!(y||ip(_)))throw _;if(fd())throw Ji.warn(`Document unloading, aborting retries: ${c}`),_;if(m===f-1)throw Ji.warn(`All ${f} attempts failed: ${c}`,_),_;const O=op(m,a,i);Ji.warn(`Network failure (attempt ${m+1}/${f}), retrying in ${O}ms: ${c}`),await lp(O)}finally{S!==void 0&&clearTimeout(S)}}throw g}async function cp(o,e={},n={}){const{retries:a=np,baseDelayMs:i=rp,maxDelayMs:l=ap,label:c=o}=n,d=a+1;for(let f=0;f{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=()=>n(),i.onerror=()=>a(new Error(`Failed to load script: ${o}`)),document.head.appendChild(i)})}const up="extension",Qi="extensionless",iw="file";function sw(){const o=globalThis?.G7Config;if(globalThis?.__g7AssetUrlMode===Qi)return Qi;const n=o?.assetUrlMode;return n===Qi||n===up?n:o?.settings?.general?.asset_url_mode===Qi?Qi:up}function dp(){return sw()===Qi}function Nr(o,e,n,a){const i=o.replace(/\/+$/,""),l=e.replace(/^\.+/,""),c=dp()?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 Ea(o){if(!o||!dp())return o;const e=globalThis?.location?.origin,n=e&&o.startsWith(e)?o.slice(e.length):o;if(!n.startsWith("/api/"))return o;const[a,i]=ow(n),l=a.match(/^\/api\/(modules|plugins)\/bundle\.(js|css)$/);if(l)return fp(`/api/${l[1]}/bundle/${l[2]}`,i);const c=a.match(/^\/api\/(templates|modules|plugins)\/assets\/([^/]+)\/(.+)$/);if(c){const[,f,g,m]=c,y=`${iw}=${encodeURIComponent(decodeURIComponent(m))}`;return`/api/${f}/assets/${g}?${y}${i?`&${i}`:""}`}const d=a.match(/^(\/api\/.+)\.(json|js|css)$/);return d?fp(d[1],i):o}function ow(o){const e=o.indexOf("?");return e===-1?[o,""]:[o.slice(0,e),o.slice(e+1)]}function fp(o,e){return e?`${o}?${e}`:o}const Ir=ht("ComponentRegistry");class hn extends Error{constructor(n,a,i){super(n);$(this,"code");$(this,"details");this.code=a,this.details=i,this.name="ComponentRegistryError"}}const Wn=class Wn{constructor(){$(this,"registry",{});$(this,"manifest",null);$(this,"loadingState","idle");$(this,"error",null);$(this,"templateId",null);$(this,"templateType",null)}static getInstance(){return Wn.instance||(Wn.instance=new Wn),Wn.instance}static createIsolatedInstance(){return new Wn}static resetInstance(){Wn.instance=null}async loadComponents(e,n){if(this.loadingState==="loading")throw new hn("Components are already being loaded","LOADING_IN_PROGRESS");if(this.loadingState==="loaded"&&this.templateId===e&&this.templateType===n){Ir.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",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 hn(`Failed to load components: ${this.error.message}`,"LOAD_FAILED",{originalError:this.error})}}async loadManifest(){try{if(!this.templateId)throw new hn("Template ID not set","TEMPLATE_ID_NOT_SET");const e=`${this.templateId}:${this.templateType}`;if(Wn.manifestCache.has(e)){this.manifest=Wn.manifestCache.get(e),Ir.log("Manifest loaded from cache:",this.manifest.templateId);return}const n=Nr(`/api/templates/${this.templateId}/components`,"json"),a=await Il(n,{label:"components.json"});if(!a.ok)throw new hn(`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,Wn.manifestCache.set(e,i),Ir.log("Manifest loaded and cached:",i.templateId)}catch(e){throw e instanceof hn?e:new hn("Failed to load component manifest","MANIFEST_LOAD_FAILED",{originalError:e})}}validateManifest(e){if(!e.version)throw new hn("Manifest missing required field: version","MANIFEST_INVALID",{field:"version"});if(!e.templateId)throw new hn("Manifest missing required field: templateId","MANIFEST_INVALID",{field:"templateId"});if(!e.components||typeof e.components!="object")throw new hn("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 hn(`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 hn(`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 hn(`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 hn("Template ID not set","TEMPLATE_ID_NOT_SET");const e=this.getGlobalVariableName(),n=window[e];if(!n||typeof n!="object")throw new hn(`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),Ir.log("Component bundle loaded from global variable:",e)}catch(e){throw e instanceof hn?e:new hn("Failed to load component bundle","BUNDLE_LOAD_FAILED",{originalError:e})}}getGlobalVariableName(){if(!this.templateId)throw new hn("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 hn("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){Ir.warn(`Component '${c}' not found in bundle`);continue}this.registerComponent(c,d,l)}}}registerComponent(e,n,a){this.registry[e]&&Ir.warn(`Component '${e}' already registered, overwriting`);const i=Xe.memo(n);this.registry[e]={component:i,metadata:a},Ir.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,Ir.log("Registry cleared")}};$(Wn,"instance",null),$(Wn,"manifestCache",new Map);let mr=Wn;const hp=ht("TranslationEngine");class hd extends Error{constructor(n,a,i){super(n);$(this,"key");$(this,"locale");this.key=a,this.locale=i,this.name="TranslationError"}}const In=class In{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 In.instance||(In.instance=new In(e)),In.instance}static resetInstance(){In.instance=null}setCacheVersion(e){this.cacheVersion!==e&&(hp.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=Nr(`${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 hd(`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 hd(`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"="+this.translate(m,n,void 0,a)),i===d)break;c++}return i.replace(In.TRANSLATION_PATTERN,(d,f,g)=>{const{cleanedParams:m,trailing:y}=this.separateTrailingText(g);return this.translate(f,n,m,a)+y})}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 g=this.getNestedValue(f,e);if(g!==null)return g}}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 g=0;g(l.push(f),`__PLACEHOLDER_${l.length-1}__`)).matchAll(In.PARAM_PATTERN);for(const f of d){const[,g,m]=f,y=m.replace(/__PLACEHOLDER_(\d+)__/g,(S,v)=>l[parseInt(v,10)]);a[g.trim()]=this.resolveParamValue(y.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=Sd.evaluateExpression(a,n);return String(l??"")}catch(l){return hp.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}}};$(In,"instance",null),$(In,"TRANSLATION_PATTERN",/\$t:(?:defer:)?([a-zA-Z0-9._-]+)(\|(?:(?!\$t:).)+)?/g),$(In,"NESTED_TRANSLATION_PARAM_PATTERN",/=(\$t:([a-zA-Z0-9._-]+))(?=[|&\s]|$)/g),$(In,"PARAM_PATTERN",/([^=|&]+)=([^|&]+)/g);let _a=In,gd=null;function lw(o){return gd||(gd=new _a(o)),gd}const gp=Object.freeze(Object.defineProperty({__proto__:null,TranslationEngine:_a,TranslationError:hd,getTranslationEngine:lw},Symbol.toStringTag,{value:"Module"})),Zi=ht("PipeRegistry");function pp(o,e){const n=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(n)).replace("YY",String(n).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 cw(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 pd(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 no={date:{fn:(o,e="YYYY-MM-DD")=>{const n=pd(o);return n?pp(n,e):""},description:"날짜 포맷 (기본: YYYY-MM-DD)"},datetime:{fn:(o,e="YYYY-MM-DD HH:mm")=>{const n=pd(o);return n?pp(n,e):""},description:"날짜+시간 포맷 (기본: YYYY-MM-DD HH:mm)"},relativeTime:{fn:(o,e="ko")=>{const n=pd(o);return n?cw(n,e):""},description:'상대 시간 표시 (예: "3분 전")'},number:{fn:(o,e)=>{const n=Number(o);if(isNaN(n))return String(o??"");const a={};return e!==void 0&&(a.minimumFractionDigits=e,a.maximumFractionDigits=e),n.toLocaleString(void 0,a)},description:"숫자 포맷 (천단위 구분, 선택적 소수점)"},truncate:{fn:(o,e=100,n="...")=>typeof o!="string"?String(o??""):o.length<=e?o:o.slice(0,e)+n,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,n)=>Array.isArray(o)?Array.isArray(e)?o.filter(a=>{const i=n?a?.[n]: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:"다국어 객체에서 로케일에 맞는 값 추출"}},es=new Map,Si=class Si{static getInstance(){return Si.instance||(Si.instance=new Si),Si.instance}register(e,n,a){if(no[e]){Zi.warn(`[PipeRegistry] 내장 파이프를 덮어쓸 수 없습니다: ${e}`);return}es.set(e,{fn:n,description:a}),Zi.log(`[PipeRegistry] 커스텀 파이프 등록됨: ${e}`)}unregister(e){if(no[e])return Zi.warn(`[PipeRegistry] 내장 파이프는 해제할 수 없습니다: ${e}`),!1;const n=es.delete(e);return n&&Zi.log(`[PipeRegistry] 커스텀 파이프 해제됨: ${e}`),n}get(e){const n=no[e];return n?n.fn:es.get(e)?.fn}has(e){return!!no[e]||es.has(e)}execute(e,n,a=[]){const i=this.get(e);if(!i)return Zi.warn(`[PipeRegistry] 알 수 없는 파이프: ${e}`),n;try{return i(n,...a)}catch(l){return Zi.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 es.entries())e.push({name:n,description:a.description,type:"custom"});return e.sort((n,a)=>n.name.localeCompare(a.name))}clearCustomPipes(){es.clear()}};$(Si,"instance",null);let Hl=Si;Hl.getInstance();function uw(o){const e=o.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 g=0;g0?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(mp(c.trim())),c="";continue}c+=m}return c.trim()&&l.push(mp(c.trim())),{name:a,args:l}}function mp(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 dw(o){const e=[];let n="",a=null,i=0;for(let l=0;l0?o[l-1]:"",f=l0?o[a-1]:"",c=a0){a--;continue}return null}}return a===0?e.trim():null}function hw(o){const e=[];if(typeof o!="string")return e;for(let n=0;n0){i--;continue}if(o[c+1]==="}"){l=c;break}break}}l!==-1&&(e.push({start:n,end:l+2,expr:o.slice(n+2,l)}),n=l+1)}return e}function ro(o){return/[?:|&!+\-*/<>=()[\]{}]/.test(o)}function yp(o){const e=typeof o=="string"?o.trim():"";return e===""?"empty":md.has(e)?"literal":Vn(e)?"pipe":ro(e)?"expression":"path"}function yd(o,e,n,a){const i=typeof o=="string"?o.trim():"",l=yp(i),c=a?.skipCache?{skipCache:!0}:void 0;if(l==="empty"){a?.onEmpty?.(o);return}if(l==="literal")return md.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 nr="raw:";function bp(o){return o.startsWith(nr)?o.slice(nr.length):o}const ao="﷐",fi="﷑",xa="﷒";function io(o){return ao+o+fi}function jl(o){return o.length>=2&&o.charCodeAt(0)===64976&&o.charCodeAt(o.length-1)===64977}function zl(o){return o.includes(ao)}function bd(o){return o.slice(1,-1)}function ts(o){if(typeof o=="string")return io(o);if(Array.isArray(o))return o.map(ts);if(o&&typeof o=="object"){const e={};for(const[n,a]of Object.entries(o))e[n]=ts(a);return e}return o}function Ta(o){if(typeof o=="string")return jl(o)?bd(o):zl(o)||o.includes(fi)?o.split(ao).join("").split(fi).join(""):o;if(Array.isArray(o)){let e=!1;const n=o.map(a=>{const i=Ta(a);return i!==a&&(e=!0),i});return e?n:o}if(o&&typeof o=="object"){if(o.$$typeof!==void 0)return o;let e=!1;const n={};for(const[a,i]of Object.entries(o)){const l=Ta(i);l!==i&&(e=!0),n[a]=l}return e?n:o}return o}const Ra=ht("DataBindingEngine"),gw=/^[$A-Za-z_][$A-Za-z0-9_]*$/,pw=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 so(){try{return window.G7Core?.devTools}catch{return}}class vd extends Error{constructor(n,a,i){super(n);$(this,"path");$(this,"context");this.path=a,this.context=i,this.name="DataBindingError"}}const ta=class ta{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,n){this.isRenderCycleCacheActive()&&this.renderCycleCache.set(e,n)}resolveBindings(e,n,a,i){const l={...this.defaultOptions,...a},c=so(),d=n._computed&&!n.$computed?{...n,$computed:n._computed}:n,f=(S,v)=>{const _=v.trim();let A=!1,x=_;if(_.startsWith(nr)&&(A=!0,x=_.slice(nr.length)),Vn(x))try{const q=this.evaluatePipeExpression(x,d,l,{...i,method:"resolveBindings",displayExpression:`{{${_}}}`}),W=this.formatValue(q);return A?io(W):W}catch(q){return Ra.error("Pipe expression evaluation failed:",x,q),S}if(ro(x)){const q=c?.isEnabled()?performance.now():0;try{let W,he=!1;if(l.skipCache)W=this.evaluateExpression(x,d);else{const be=`expr:${x}`,Ue=this.getFromRenderCycleCache(be);Ue!==void 0?(W=Ue,he=!0):(W=this.evaluateExpression(x,d),this.saveToRenderCycleCache(be,W))}if(c?.isEnabled()){const be=performance.now()-q;c.trackExpressionEval({expression:`{{${_}}}`,result:this.sanitizeResultForTracking(W),resultType:this.getResultType(W),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:he,duration:be,method:"resolveBindings",skipCache:l.skipCache})}const Se=this.formatValue(W);return A?io(Se):Se}catch(W){return Ra.error("Expression evaluation failed:",x,W),S}}if(l.skipCache){const q=c?.isEnabled()?performance.now():0,W=this.resolvePath(x,d,l);if(c?.isEnabled()){const Se=performance.now()-q;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 he=this.formatValue(W);return A?io(he):he}const M=x.startsWith("_global")||x.startsWith("_local")||x.startsWith("_isolated")||x.startsWith("$parent"),T=c?.isEnabled()?performance.now():0;let D,z=!1;if(M){const q=this.getFromRenderCycleCache(x);q!==void 0?(D=q,z=!0):(D=this.resolvePath(x,d,l),this.saveToRenderCycleCache(x,D))}else{const q=this.getFromCache(x);q!==void 0?(D=q,z=!0):(D=this.resolvePath(x,d,l),this.saveToCache(x,D))}if(c?.isEnabled()){const q=performance.now()-T;c.trackExpressionEval({expression:`{{${_}}}`,result:this.sanitizeResultForTracking(D),resultType:this.getResultType(D),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:z,duration:q,method:"resolveBindings",skipCache:l.skipCache})}const P=this.formatValue(D);return A?io(P):P},g=hw(e);if(g.length===0)return e;let m="",y=0;for(const S of g)m+=e.slice(y,S.start),m+=f(e.slice(S.start,S.end),S.expr),y=S.end;return m+e.slice(y)}evaluatePipeExpression(e,n,a,i){const l={...this.defaultOptions,...a},c=so(),d=c?.isEnabled()?performance.now():0,f=n._computed&&!n.$computed?{...n,$computed:n._computed}:n,[g,m]=dw(e);let y,S=!1;const v=ro(g),_=g.startsWith("_global")||g.startsWith("_local")||g.startsWith("_isolated")||g.startsWith("$parent");if(l.skipCache)v?y=this.evaluateExpression(g,f):y=this.resolvePath(g,f,l);else if(v){const x=this.getFromRenderCycleCache(`expr:${g}`);x!==void 0?(y=x,S=!0):(y=this.evaluateExpression(g,f),this.saveToRenderCycleCache(`expr:${g}`,y))}else if(_){const x=this.getFromRenderCycleCache(g);x!==void 0?(y=x,S=!0):(y=this.resolvePath(g,f,l),this.saveToRenderCycleCache(g,y))}else{const x=this.getFromCache(g);x!==void 0?(y=x,S=!0):(y=this.resolvePath(g,f,l),this.saveToCache(g,y))}const A=fw(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:S,duration:x,method:i?.method??"evaluatePipeExpression",skipCache:l.skipCache})}return A}resolve(e,n,a,i){const l={...this.defaultOptions,...a},c=so(),d=c?.isEnabled()?performance.now():0;if(l.skipCache){const y=this.resolvePath(e,n,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,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:g,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()===""){Ra.warn("resolvePath: 빈 경로가 전달되었습니다 — undefined 로 해석합니다.");return}if(a.maxDepth&&i>a.maxDepth){if(a.detectCircular)throw new vd(`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;fta.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(g){Ra.warn(`resolveObject: 값 해석 실패 (key: ${d}):`,g),i[d]=void 0}}return i}resolveObjectEntry(e,n,a,i,l){if(typeof a=="string"){const c=Aa(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(yp(g)){case"empty":Ra.warn(`resolveObject: 빈 바인딩 \`{{}}\` (key: ${n}) — undefined 로 해석합니다.`),m=void 0;break;case"literal":m=md.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[n]=f&&m!=null?ts(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(),this.expressionFnCache.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>ta.CACHE_EXPIRY&&this.cache.delete(n)}getCacheStats(){const e=Date.now();let n=0;for(const a of this.cache.values())e-a.timestamp>ta.CACHE_EXPIRY&&n++;return{size:this.cache.size,expired:n}}evaluateExpression(e,n,a){const i=so(),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 D of d)D in f||(f[D]=void 0);n._computed&&!f.$computed&&(f.$computed=n._computed);let g=n.$templateId,m=n.$locale;if(typeof window<"u"&&(!g||!m)){const D=window.__templateApp?.getConfig?.();g??(g=D?.templateId),m??(m=D?.locale)}const y=m||"ko";f.$localized=(D,z)=>{if(D==null&&!z)return"";if(typeof D=="string")return D;if(D&&typeof D=="object"&&D[y])return D[y];if(z&&typeof z=="string"){const P=f.$t?f.$t(z):z;if(P&&P!==z)return P}return D&&typeof D=="object"?D.ko||D.en||Object.values(D)[0]||"":D==null?"":String(D)};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,D=>{const z=Math.random()*16|0;return(D==="x"?z:z&3|8).toString(16)});const v={templateId:g||"",locale:y};f.$t=D=>{if(!D||typeof D!="string")return"";try{return _a.getInstance().translate(D,v)}catch(z){return Ra.warn("$t() translation failed for key:",D,z),D}},f.$get=(D,z,P=void 0)=>{if(D==null)return P;const q=Array.isArray(z)?z:[z];if(q.length===0)return D;let W=D;for(const he of q){if(W==null||he==null)return P;W=W[he]}return W??P};const _=[],A=[];for(const[D,z]of Object.entries(f))!gw.test(D)||pw.has(D)||(_.push(D),A.push(z));const x=`${c}|${_.join(",")}`;let O=this.expressionFnCache.get(x);const M=!!O;O||(O=new Function(..._,`return (${c});`),this.expressionFnCache.set(x,O));const T=O(...A);if(i?.isEnabled()){const D=performance.now()-l;i.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(T),resultType:this.getResultType(T),componentId:a?.componentId,componentName:a?.componentName,propName:a?.propName,fromCache:M,duration:D,method:"evaluateExpression",skipCache:a?.skipCache})}return T}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(/(?(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=/(?{n!==null&&clearTimeout(n),n=setTimeout(()=>{o(...a)},e)})}class yw{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=mw(()=>{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(Pl.has(e))return Pl.get(e)??null;let n=null;if(Ul[e])n={...Ul[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 Pl.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:!!Ul[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(),Pl.clear()}_setWidthForTesting(e){this.currentWidth=e,this.notifySubscribers()}}const hi=new yw,oo=ht("ConditionEvaluator");function bw(o,e,n,a){return yd(bp(o),e,n,{skipCache:!0,onEmpty:()=>{oo.warn(`${a}: 빈 바인딩 \`{{}}\` — undefined 로 해석합니다.`)}})}function vp(o,e,n){if(!o)return!0;try{const a=Aa(o);let i;if(a!==null?i=bw(a,e,n,"evaluateStringCondition"):i=n.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 oo.warn(`evaluateStringCondition: 조건 평가 실패: ${o}`,a),!1}}function Bl(o,e,n){if(typeof o=="string")return vp(o,e,n);if("and"in o){if(!Array.isArray(o.and)||o.and.length===0)return oo.warn("evaluateConditionExpression: AND 그룹이 비어있습니다"),!0;for(const a of o.and)if(!Bl(a,e,n))return!1;return!0}if("or"in o){if(!Array.isArray(o.or)||o.or.length===0)return oo.warn("evaluateConditionExpression: OR 그룹이 비어있습니다"),!1;for(const a of o.or)if(Bl(a,e,n))return!0;return!1}return oo.warn("evaluateConditionExpression: 알 수 없는 조건 형식",o),!1}function Sp(o,e,n){if(!Array.isArray(o)||o.length===0)return{matched:!1,branchIndex:-1};for(let a=0;a{rr.warn(`evaluateIfCondition: 빈 바인딩 \`{{}}\` (컴포넌트: ${a||"unknown"}) — undefined 로 해석합니다.`)}}):l=n.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 ns(o,e,n,a){if(o.if!==void 0)return _p(o.if,e,n,a);if(o.condition!==void 0)return _p(o.condition,e,n,a);if(o.conditions===void 0)return!0;const i=o.conditions;try{return vw(i)?Bl(i,e,n):Sw(i)?Sp(i,e,n).matched:(rr.warn(`evaluateRenderCondition: 알 수 없는 conditions 형식 (컴포넌트: ${a||"unknown"})`),!0)}catch(l){return rr.warn(`evaluateRenderCondition: conditions 평가 실패 (컴포넌트: ${a||"unknown"}):`,l),!1}}function Ap(o){if(!o.responsive)return o;const e=hi.getWidth(),n=hi.getMatchingKey(o.responsive,e);if(!n)return o;const a=o.responsive[n];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 lo(o,e,n,a,i){if(!o||o.length===0)return[];const l=i?.bindingEngine??new Dn,c=i?.translationEngine??_a.getInstance(),d=i?.translationContext??{templateId:"",locale:"ko"},f=Cw(e,i?.componentContext),g=(v,_)=>{if(typeof v=="string")return jl(v)||zl(v)||!/\$t:[a-zA-Z0-9._-]+/.test(v)?v:c.resolveTranslations(v,d,_);if(Array.isArray(v))return v.map(A=>g(A,_));if(v&&typeof v=="object"){const A={};for(const[x,O]of Object.entries(v))A[x]=g(O,_);return A}return v},m=(v,_)=>{if(typeof v=="string"){if(jl(v))return bd(v);if(zl(v)){const x=[],O=v.replace(new RegExp(`${ao}([^${fi}]*)${fi}`,"g"),(T,D)=>(x.push(D),`${xa}${x.length-1}${xa}`));let M=O;return/\$t:[a-zA-Z0-9._-]+/.test(O)&&(M=c.resolveTranslations(O,d,_)),M.replace(new RegExp(`${xa}(\\d+)${xa}`,"g"),(T,D)=>x[parseInt(D)])}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=Ep(v);if(A!==null){let x=!1,O=A;A.startsWith(nr)&&(x=!0,O=A.slice(nr.length));let M=!1,T=yd(O,_,l,{skipCache:!0,onError:D=>{rr.warn("renderItemChildren: 표현식 평가 실패:",D),M=!0},onEmpty:()=>{rr.warn("renderItemChildren: 빈 바인딩 `{{}}` 은 해석하지 않습니다.")}});return M?void 0:(x||(T=g(T,_)),x&&T!=null?ts(T):T)}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,O]of Object.entries(v))A[x]=m(O,_);return A}return v},y=(v,_)=>{if(!v)return{};const A={};for(const[x,O]of Object.entries(v))A[x]=m(O,_);return A},S=(v,_,A)=>{const x=Ap(v),O=x.iteration;if(!O)return[];const M=wd(O.source,A,l);if(!Array.isArray(M))return rr.warn(`renderItemChildren: iteration.source가 배열이 아닙니다: ${O.source}`),[];const T=wp();if(T?.isEnabled()){const z=`${_}-iteration`;T.trackIteration(z,O.source,O.item_var,O.index_var,M.length)}const D=n[x.name];return D?M.flatMap((z,P)=>{const q={...A,[O.item_var]:z,[`${O.item_var}_index`]:P,...O.index_var?{[O.index_var]:P}:{}},W=ns({if:x.if,condition:x.condition,conditions:x.conditions},q,l,x.id);if(x.if&&T?.isEnabled()){const wt=`${_}-iter-${P}-if`;T.trackIfCondition(wt,x.if,W,x.name)}if(!W)return[];const he=x.id?String(Ta(m(x.id,q))):void 0,Se=`${_}-iter-${P}`,be=i?.getRemountKey?i.getRemountKey(he,Se):Se,Ue=y(x.props,q),Fe=Cd(Ue,x.actions,q,{actionDispatcher:i?.actionDispatcher,componentContext:i?.componentContext});let Ge=null;return x.text!==void 0?Ge=m(x.text,q):x.children&&x.children.length>0&&(Ge=lo(x.children,q,n,be,i)),T?.isEnabled()&&T.trackRender(x.name),[Xe.createElement(D,{key:be,...Ta(Fe)},Ta(Ge))]}):(rr.warn(`renderItemChildren: 컴포넌트를 찾을 수 없습니다: ${x.name}`),[])};return o.flatMap((v,_)=>{const A=Ap(v),x=A.id?String(Ta(m(A.id,f))):void 0,O=a?`${a}-${x||_}`:x||`child-${_}`,M=i?.getRemountKey?i.getRemountKey(x,O):O;if(A.iteration)return S(A,M,f);const T=ns({if:A.if,condition:A.condition,conditions:A.conditions},f,l,A.id),D=wp();if(A.if&&D?.isEnabled()){const he=`${M}-if`;D.trackIfCondition(he,A.if,T,A.name)}if(!T)return[];const z=n[A.name];if(!z)return rr.warn(`renderItemChildren: 컴포넌트를 찾을 수 없습니다: ${A.name}`),[];const P=y(A.props,f),q=Cd(P,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=lo(A.children,f,n,M,i)),D?.isEnabled()&&D.trackRender(A.name),[Xe.createElement(z,{key:M,...Ta(q)},Ta(W))]})}function rs(o,e,n,a){let i,l;return n instanceof Dn?(i=n,l=a):(i=Cp,l=n),i.resolveBindings(o,e,l)}function Ew(o,e,n,a){const i=n??Cp,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 xp(o,e){return function(){return o.apply(e,arguments)}}const{toString:_w}=Object.prototype,{getPrototypeOf:as}=Object,{iterator:co,toStringTag:Tp}=Symbol,ql=(({hasOwnProperty:o})=>(e,n)=>o.call(e,n))(Object.prototype),uo=(o,e)=>{let n=o;const a=[];for(;n!=null&&n!==Object.prototype;){if(a.indexOf(n)!==-1)return!1;if(a.push(n),ql(n,e))return!0;n=as(n)}return!1},Aw=(o,e)=>o!=null&&uo(o,e)?o[e]:void 0,Ed=(o=>e=>{const n=_w.call(e);return o[n]||(o[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),yr=o=>(o=o.toLowerCase(),e=>Ed(e)===o),Gl=o=>e=>typeof e===o,{isArray:gi}=Array,is=Gl("undefined");function ss(o){return o!==null&&!is(o)&&o.constructor!==null&&!is(o.constructor)&&$n(o.constructor.isBuffer)&&o.constructor.isBuffer(o)}const Rp=yr("ArrayBuffer");function xw(o){let e;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?e=ArrayBuffer.isView(o):e=o&&o.buffer&&Rp(o.buffer),e}const Tw=Gl("string"),$n=Gl("function"),kp=Gl("number"),os=o=>o!==null&&typeof o=="object",Rw=o=>o===!0||o===!1,Vl=o=>{if(!os(o))return!1;const e=as(o);return(e===null||e===Object.prototype||as(e)===null)&&!uo(o,Tp)&&!uo(o,co)},kw=o=>{if(!os(o)||ss(o))return!1;try{return Object.keys(o).length===0&&Object.getPrototypeOf(o)===Object.prototype}catch{return!1}},Dw=yr("Date"),Ow=yr("File"),Lw=o=>!!(o&&typeof o.uri<"u"),Mw=o=>o&&typeof o.getParts<"u",$w=yr("Blob"),Nw=yr("FileList"),Iw=o=>os(o)&&$n(o.pipe);function Hw(){return typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{}}const Dp=Hw(),Op=typeof Dp.FormData<"u"?Dp.FormData:void 0,jw=o=>{if(!o)return!1;if(Op&&o instanceof Op)return!0;const e=as(o);if(!e||e===Object.prototype||!$n(o.append))return!1;const n=Ed(o);return n==="formdata"||n==="object"&&$n(o.toString)&&o.toString()==="[object FormData]"},zw=yr("URLSearchParams"),[Uw,Pw,Bw,qw]=["ReadableStream","Request","Response","Headers"].map(yr),Gw=o=>o.trim?o.trim():o.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function fo(o,e,{allOwnKeys:n=!1}={}){if(o===null||typeof o>"u")return;let a,i;if(typeof o!="object"&&(o=[o]),gi(o))for(a=0,i=o.length;a0;)if(i=n[a],e===i.toLowerCase())return i;return null}const pi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,Mp=o=>!is(o)&&o!==pi;function _d(...o){const{caseless:e,skipUndefined:n}=Mp(this)&&this||{},a={},i=(l,c)=>{if(c==="__proto__"||c==="constructor"||c==="prototype")return;const d=e&&typeof c=="string"&&Lp(a,c)||c,f=ql(a,d)?a[d]:void 0;Vl(f)&&Vl(l)?a[d]=_d(f,l):Vl(l)?a[d]=_d({},l):gi(l)?a[d]=l.slice():(!n||!is(l))&&(a[d]=l)};for(let l=0,c=o.length;l(fo(e,(i,l)=>{n&&$n(i)?Object.defineProperty(o,l,{__proto__:null,value:xp(i,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(o,l,{__proto__:null,value:i,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:a}),o),Fw=o=>(o.charCodeAt(0)===65279&&(o=o.slice(1)),o),Kw=(o,e,n,a)=>{o.prototype=Object.create(e.prototype,a),Object.defineProperty(o.prototype,"constructor",{__proto__:null,value:o,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(o,"super",{__proto__:null,value:e.prototype}),n&&Object.assign(o.prototype,n)},Ww=(o,e,n,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=n!==!1&&as(o)}while(o&&(!n||n(o,e))&&o!==Object.prototype);return e},Yw=(o,e,n)=>{o=String(o),(n===void 0||n>o.length)&&(n=o.length),n-=e.length;const a=o.indexOf(e,n);return a!==-1&&a===n},Xw=o=>{if(!o)return null;if(gi(o))return o;let e=o.length;if(!kp(e))return null;const n=new Array(e);for(;e-- >0;)n[e]=o[e];return n},Jw=(o=>e=>o&&e instanceof o)(typeof Uint8Array<"u"&&as(Uint8Array)),Qw=(o,e)=>{const a=(o&&o[co]).call(o);let i;for(;(i=a.next())&&!i.done;){const l=i.value;e.call(o,l[0],l[1])}},Zw=(o,e)=>{let n;const a=[];for(;(n=o.exec(e))!==null;)a.push(n);return a},e0=yr("HTMLFormElement"),t0=o=>o.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,a,i){return a.toUpperCase()+i}),{propertyIsEnumerable:n0}=Object.prototype,r0=yr("RegExp"),$p=(o,e)=>{const n=Object.getOwnPropertyDescriptors(o),a={};fo(n,(i,l)=>{let c;(c=e(i,l,o))!==!1&&(a[l]=c||i)}),Object.defineProperties(o,a)},a0=o=>{$p(o,(e,n)=>{if($n(o)&&["arguments","caller","callee"].includes(n))return!1;const a=o[n];if($n(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+"'")})}})},i0=(o,e)=>{const n={},a=i=>{i.forEach(l=>{n[l]=!0})};return gi(o)?a(o):a(String(o).split(e)),n},s0=()=>{},o0=(o,e)=>o!=null&&Number.isFinite(o=+o)?o:e;function l0(o){return!!(o&&$n(o.append)&&o[Tp]==="FormData"&&o[co])}const c0=o=>{const e=new WeakSet,n=a=>{if(os(a)){if(e.has(a))return;if(ss(a))return a;if(!("toJSON"in a)){e.add(a);const i=gi(a)?[]:{};return fo(a,(l,c)=>{const d=n(l);!is(d)&&(i[c]=d)}),e.delete(a),i}}return a};return n(o)},u0=yr("AsyncFunction"),d0=o=>o&&(os(o)||$n(o))&&$n(o.then)&&$n(o.catch),Np=((o,e)=>o?setImmediate:e?((n,a)=>(pi.addEventListener("message",({source:i,data:l})=>{i===pi&&l===n&&a.length&&a.shift()()},!1),i=>{a.push(i),pi.postMessage(n,"*")}))(`axios@${Math.random()}`,[]):n=>setTimeout(n))(typeof setImmediate=="function",$n(pi.postMessage)),f0=typeof queueMicrotask<"u"?queueMicrotask.bind(pi):typeof process<"u"&&process.nextTick||Np,Ip=o=>o!=null&&$n(o[co]),F={isArray:gi,isArrayBuffer:Rp,isBuffer:ss,isFormData:jw,isArrayBufferView:xw,isString:Tw,isNumber:kp,isBoolean:Rw,isObject:os,isPlainObject:Vl,isEmptyObject:kw,isReadableStream:Uw,isRequest:Pw,isResponse:Bw,isHeaders:qw,isUndefined:is,isDate:Dw,isFile:Ow,isReactNativeBlob:Lw,isReactNative:Mw,isBlob:$w,isRegExp:r0,isFunction:$n,isStream:Iw,isURLSearchParams:zw,isTypedArray:Jw,isFileList:Nw,forEach:fo,merge:_d,extend:Vw,trim:Gw,stripBOM:Fw,inherits:Kw,toFlatObject:Ww,kindOf:Ed,kindOfTest:yr,endsWith:Yw,toArray:Xw,forEachEntry:Qw,matchAll:Zw,isHTMLForm:e0,hasOwnProperty:ql,hasOwnProp:ql,hasOwnInPrototypeChain:uo,getSafeProp:Aw,reduceDescriptors:$p,freezeMethods:a0,toObjectSet:i0,toCamelCase:t0,noop:s0,toFiniteNumber:o0,findKey:Lp,global:pi,isContextDefined:Mp,isSpecCompliantForm:l0,toJSONObject:c0,isAsyncFn:u0,isThenable:d0,setImmediate:Np,asap:f0,isIterable:Ip,isSafeIterable:o=>o!=null&&uo(o,co)&&Ip(o)},h0=F.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"]),g0=o=>{const e={};let n,a,i;return o&&o.split(` -`).forEach(function(c){i=c.indexOf(":"),n=c.substring(0,i).trim().toLowerCase(),a=c.substring(i+1).trim(),!(!n||e[n]&&h0[n])&&(n==="set-cookie"?e[n]?e[n].push(a):e[n]=[a]:e[n]=e[n]?e[n]+", "+a:a)}),e};function p0(o){let e=0,n=o.length;for(;ee;){const a=o.charCodeAt(n-1);if(a!==9&&a!==32)break;n-=1}return e===0&&n===o.length?o:o.slice(e,n)}const m0=new RegExp("[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+","g"),y0=new RegExp("[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+","g");function Ad(o,e){return F.isArray(o)?o.map(n=>Ad(n,e)):p0(String(o).replace(e,""))}const b0=o=>Ad(o,m0),v0=o=>Ad(o,y0);function Hp(o){const e=Object.create(null);return F.forEach(o.toJSON(),(n,a)=>{e[a]=v0(n)}),e}const jp=Symbol("internals");function ho(o){return o&&String(o).trim().toLowerCase()}function Fl(o){return o===!1||o==null?o:F.isArray(o)?o.map(Fl):b0(String(o))}function S0(o){const e=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let a;for(;a=n.exec(o);)e[a[1]]=a[2];return e}const w0=o=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(o.trim());function xd(o,e,n,a,i){if(F.isFunction(a))return a.call(this,e,n);if(i&&(e=n),!!F.isString(e)){if(F.isString(a))return e.indexOf(a)!==-1;if(F.isRegExp(a))return a.test(e)}}function C0(o){return o.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,n,a)=>n.toUpperCase()+a)}function E0(o,e){const n=F.toCamelCase(" "+e);["get","set","has"].forEach(a=>{Object.defineProperty(o,a+n,{__proto__:null,value:function(i,l,c){return this[a].call(this,e,i,l,c)},configurable:!0})})}let An=class{constructor(e){e&&this.set(e)}set(e,n,a){const i=this;function l(d,f,g){const m=ho(f);if(!m)return;const y=F.findKey(i,m);(!y||i[y]===void 0||g===!0||g===void 0&&i[y]!==!1)&&(i[y||f]=Fl(d))}const c=(d,f)=>F.forEach(d,(g,m)=>l(g,m,f));if(F.isPlainObject(e)||e instanceof this.constructor)c(e,n);else if(F.isString(e)&&(e=e.trim())&&!w0(e))c(g0(e),n);else if(F.isObject(e)&&F.isSafeIterable(e)){let d=Object.create(null),f,g;for(const m of e){if(!F.isArray(m))throw new TypeError("Object iterator must return a key-value pair");g=m[0],F.hasOwnProp(d,g)?(f=d[g],d[g]=F.isArray(f)?[...f,m[1]]:[f,m[1]]):d[g]=m[1]}c(d,n)}else e!=null&&l(n,e,a);return this}get(e,n){if(e=ho(e),e){const a=F.findKey(this,e);if(a){const i=this[a];if(!n)return i;if(n===!0)return S0(i);if(F.isFunction(n))return n.call(this,i,a);if(F.isRegExp(n))return n.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,n){if(e=ho(e),e){const a=F.findKey(this,e);return!!(a&&this[a]!==void 0&&(!n||xd(this,this[a],a,n)))}return!1}delete(e,n){const a=this;let i=!1;function l(c){if(c=ho(c),c){const d=F.findKey(a,c);d&&(!n||xd(a,a[d],d,n))&&(delete a[d],i=!0)}}return F.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||xd(this,this[l],l,e,!0))&&(delete this[l],i=!0)}return i}normalize(e){const n=this,a={};return F.forEach(this,(i,l)=>{const c=F.findKey(a,l);if(c){n[c]=Fl(i),delete n[l];return}const d=e?C0(l):String(l).trim();d!==l&&delete n[l],n[d]=Fl(i),a[d]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const n=Object.create(null);return F.forEach(this,(a,i)=>{a!=null&&a!==!1&&(n[i]=e&&F.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[jp]=this[jp]={accessors:{}}).accessors,i=this.prototype;function l(c){const d=ho(c);a[d]||(E0(i,c),a[d]=!0)}return F.isArray(e)?e.forEach(l):l(e),this}};An.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),F.reduceDescriptors(An.prototype,({value:o},e)=>{let n=e[0].toUpperCase()+e.slice(1);return{get:()=>o,set(a){this[n]=a}}}),F.freezeMethods(An);const _0="[REDACTED ****]";function A0(o){if(F.hasOwnProp(o,"toJSON"))return!0;let e=Object.getPrototypeOf(o);for(;e&&e!==Object.prototype;){if(F.hasOwnProp(e,"toJSON"))return!0;e=Object.getPrototypeOf(e)}return!1}function x0(o,e){const n=new Set(e.map(l=>String(l).toLowerCase())),a=[],i=l=>{if(l===null||typeof l!="object"||F.isBuffer(l))return l;if(a.indexOf(l)!==-1)return;l instanceof An&&(l=l.toJSON()),a.push(l);let c;if(F.isArray(l))c=[],l.forEach((d,f)=>{const g=i(d);F.isUndefined(g)||(c[f]=g)});else{if(!F.isPlainObject(l)&&A0(l))return a.pop(),l;c=Object.create(null);for(const[d,f]of Object.entries(l)){const g=n.has(d.toLowerCase())?_0:i(f);F.isUndefined(g)||(c[d]=g)}}return a.pop(),c};return i(o)}let Ee=class KS extends Error{static from(e,n,a,i,l,c){const d=new KS(e.message,n||e.code,a,i,l);return Object.defineProperty(d,"cause",{__proto__:null,value:e,writable:!0,enumerable:!1,configurable:!0}),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&&F.hasOwnProp(e,"redact")?e.redact:void 0,a=F.isArray(n)&&n.length>0?x0(e,n):F.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}}};Ee.ERR_BAD_OPTION_VALUE="ERR_BAD_OPTION_VALUE",Ee.ERR_BAD_OPTION="ERR_BAD_OPTION",Ee.ECONNABORTED="ECONNABORTED",Ee.ETIMEDOUT="ETIMEDOUT",Ee.ECONNREFUSED="ECONNREFUSED",Ee.ERR_NETWORK="ERR_NETWORK",Ee.ERR_FR_TOO_MANY_REDIRECTS="ERR_FR_TOO_MANY_REDIRECTS",Ee.ERR_DEPRECATED="ERR_DEPRECATED",Ee.ERR_BAD_RESPONSE="ERR_BAD_RESPONSE",Ee.ERR_BAD_REQUEST="ERR_BAD_REQUEST",Ee.ERR_CANCELED="ERR_CANCELED",Ee.ERR_NOT_SUPPORT="ERR_NOT_SUPPORT",Ee.ERR_INVALID_URL="ERR_INVALID_URL",Ee.ERR_FORM_DATA_DEPTH_EXCEEDED="ERR_FORM_DATA_DEPTH_EXCEEDED";const T0=null,zp=100;function Td(o){return F.isPlainObject(o)||F.isArray(o)}function Up(o){return F.endsWith(o,"[]")?o.slice(0,-2):o}function Rd(o,e,n){return o?o.concat(e).map(function(i,l){return i=Up(i),!n&&l?"["+i+"]":i}).join(n?".":""):e}function R0(o){return F.isArray(o)&&!o.some(Td)}const k0=F.toFlatObject(F,{},null,function(e){return/^is[A-Z]/.test(e)});function Kl(o,e,n){if(!F.isObject(o))throw new TypeError("target must be an object");e=e||new FormData,n=F.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(M,T){return!F.isUndefined(T[M])});const a=n.metaTokens,i=n.visitor||_,l=n.dots,c=n.indexes,d=n.Blob||typeof Blob<"u"&&Blob,f=n.maxDepth===void 0?zp:n.maxDepth,g=d&&F.isSpecCompliantForm(e),m=[];if(!F.isFunction(i))throw new TypeError("visitor must be a function");function y(O){if(O===null)return"";if(F.isDate(O))return O.toISOString();if(F.isBoolean(O))return O.toString();if(!g&&F.isBlob(O))throw new Ee("Blob is not supported. Use a Buffer instead.");if(F.isArrayBuffer(O)||F.isTypedArray(O)){if(g&&typeof d=="function")return new d([O]);if(typeof Buffer<"u")return Buffer.from(O);throw new Ee("Blob is not supported. Use a Buffer instead.",Ee.ERR_NOT_SUPPORT)}return O}function S(O){if(O>f)throw new Ee("Object is too deeply nested ("+O+" levels). Max depth: "+f,Ee.ERR_FORM_DATA_DEPTH_EXCEEDED)}function v(O,M){if(f===1/0)return JSON.stringify(O);const T=[];return JSON.stringify(O,function(z,P){if(!F.isObject(P))return P;for(;T.length&&T[T.length-1]!==this;)T.pop();return T.push(P),S(M+T.length-1),P})}function _(O,M,T){let D=O;if(F.isReactNative(e)&&F.isReactNativeBlob(O))return e.append(Rd(T,M,l),y(O)),!1;if(O&&!T&&typeof O=="object"){if(F.endsWith(M,"{}"))M=a?M:M.slice(0,-2),O=v(O,1);else if(F.isArray(O)&&R0(O)||(F.isFileList(O)||F.endsWith(M,"[]"))&&(D=F.toArray(O)))return M=Up(M),D.forEach(function(P,q){!(F.isUndefined(P)||P===null)&&e.append(c===!0?Rd([M],q,l):c===null?M:M+"[]",y(P))}),!1}return Td(O)?!0:(e.append(Rd(T,M,l),y(O)),!1)}const A=Object.assign(k0,{defaultVisitor:_,convertValue:y,isVisitable:Td});function x(O,M,T=0){if(!F.isUndefined(O)){if(S(T),m.indexOf(O)!==-1)throw new Error("Circular reference detected in "+M.join("."));m.push(O),F.forEach(O,function(z,P){(!(F.isUndefined(z)||z===null)&&i.call(e,z,F.isString(P)?P.trim():P,M,A))===!0&&x(z,M?M.concat(P):[P],T+1)}),m.pop()}}if(!F.isObject(o))throw new TypeError("data must be an object");return x(o),e}function Pp(o){const e={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"};return encodeURIComponent(o).replace(/[!'()~]|%20/g,function(a){return e[a]})}function kd(o,e){this._pairs=[],o&&Kl(o,this,e)}const Bp=kd.prototype;Bp.append=function(e,n){this._pairs.push([e,n])},Bp.toString=function(e){const n=e?a=>e.call(this,a,Pp):Pp;return this._pairs.map(function(i){return n(i[0])+"="+n(i[1])},"").join("&")};function D0(o){return encodeURIComponent(o).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function qp(o,e,n){if(!e)return o;o=o||"";const a=F.isFunction(n)?{serialize:n}:n,i=F.getSafeProp(a,"encode")||D0,l=F.getSafeProp(a,"serialize");let c;if(l?c=l(e,a):c=F.isURLSearchParams(e)?e.toString():new kd(e,a).toString(i),c){const d=o.indexOf("#");d!==-1&&(o=o.slice(0,d)),o+=(o.indexOf("?")===-1?"?":"&")+c}return o}class Gp{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){F.forEach(this.handlers,function(a){a!==null&&e(a)})}}const Dd={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0,advertiseZstdAcceptEncoding:!1,validateStatusUndefinedResolves:!0},O0={isBrowser:!0,classes:{URLSearchParams:typeof URLSearchParams<"u"?URLSearchParams:kd,FormData:typeof FormData<"u"?FormData:null,Blob:typeof Blob<"u"?Blob:null},protocols:["http","https","file","blob","url","data"]},Od=typeof window<"u"&&typeof document<"u",Ld=typeof navigator=="object"&&navigator||void 0,L0=Od&&(!Ld||["ReactNative","NativeScript","NS"].indexOf(Ld.product)<0),M0=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",$0=Od&&window.location.href||"http://localhost",mn={...Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:Od,hasStandardBrowserEnv:L0,hasStandardBrowserWebWorkerEnv:M0,navigator:Ld,origin:$0},Symbol.toStringTag,{value:"Module"})),...O0};function N0(o,e){return Kl(o,new mn.classes.URLSearchParams,{visitor:function(n,a,i,l){return mn.isNode&&F.isBuffer(n)?(this.append(a,n.toString("base64")),!1):l.defaultVisitor.apply(this,arguments)},...e})}const Vp=zp;function Fp(o){if(o>Vp)throw new Ee("FormData field is too deeply nested ("+o+" levels). Max depth: "+Vp,Ee.ERR_FORM_DATA_DEPTH_EXCEEDED)}function I0(o){const e=[],n=/\w+|\[(\w*)]/g;let a;for(;(a=n.exec(o))!==null;)Fp(e.length),e.push(a[0]==="[]"?"":a[1]||a[0]);return e}function H0(o){const e={},n=Object.keys(o);let a;const i=n.length;let l;for(a=0;a=n.length;return c=!c&&F.isArray(i)?i.length:c,f?(F.hasOwnProp(i,c)?i[c]=F.isArray(i[c])?i[c].concat(a):[i[c],a]:i[c]=a,!d):((!F.hasOwnProp(i,c)||!F.isObject(i[c]))&&(i[c]=[]),e(n,a,i[c],l)&&F.isArray(i[c])&&(i[c]=H0(i[c])),!d)}if(F.isFormData(o)&&F.isFunction(o.entries)){const n={};return F.forEachEntry(o,(a,i)=>{e(I0(a),i,n,0)}),n}return null}const ls=(o,e)=>o!=null&&F.hasOwnProp(o,e)?o[e]:void 0;function j0(o,e,n){if(F.isString(o))try{return(e||JSON.parse)(o),F.trim(o)}catch(a){if(a.name!=="SyntaxError")throw a}return(n||JSON.stringify)(o)}const go={transitional:Dd,adapter:["xhr","http","fetch"],transformRequest:[function(e,n){const a=n.getContentType()||"",i=a.indexOf("application/json")>-1,l=F.isObject(e);if(l&&F.isHTMLForm(e)&&(e=new FormData(e)),F.isFormData(e))return i?JSON.stringify(Kp(e)):e;if(F.isArrayBuffer(e)||F.isBuffer(e)||F.isStream(e)||F.isFile(e)||F.isBlob(e)||F.isReadableStream(e))return e;if(F.isArrayBufferView(e))return e.buffer;if(F.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 N0(e,f).toString();if((d=F.isFileList(e))||a.indexOf("multipart/form-data")>-1){const g=ls(this,"env"),m=g&&g.FormData;return Kl(d?{"files[]":e}:e,m&&new m,f)}}return l||i?(n.setContentType("application/json",!1),j0(e)):e}],transformResponse:[function(e){const n=ls(this,"transitional")||go.transitional,a=n&&n.forcedJSONParsing,i=ls(this,"responseType"),l=i==="json";if(F.isResponse(e)||F.isReadableStream(e))return e;if(e&&F.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"?Ee.from(f,Ee.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:mn.classes.FormData,Blob:mn.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};F.forEach(["delete","get","head","post","put","patch","query"],o=>{go.headers[o]={}});function Md(o,e){const n=this||go,a=e||n,i=An.from(a.headers);let l=a.data;return F.forEach(o,function(d){l=d.call(n,l,i.normalize(),e?e.status:void 0)}),i.normalize(),l}function Wp(o){return!!(o&&o.__CANCEL__)}let po=class extends Ee{constructor(e,n,a){super(e??"canceled",Ee.ERR_CANCELED,n,a),this.name="CanceledError",this.__CANCEL__=!0}};function Yp(o,e,n){const a=n.config.validateStatus;!n.status||!a||a(n.status)?o(n):e(new Ee("Request failed with status code "+n.status,n.status>=400&&n.status<500?Ee.ERR_BAD_REQUEST:Ee.ERR_BAD_RESPONSE,n.config,n.request,n))}function z0(o){const e=/^([-+\w]{1,25}):(?:\/\/)?/.exec(o);return e&&e[1]||""}function U0(o,e){o=o||10;const n=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),n[i]=f,a[i]=g;let y=l,S=0;for(;y!==i;)S+=n[y++],y=y%o;if(i=(i+1)%o,i===l&&(l=(l+1)%o),g-c{n=m,i=null,l&&(clearTimeout(l),l=null),o(...g)};return[(...g)=>{const m=Date.now(),y=m-n;y>=a?c(g,m):(i=g,l||(l=setTimeout(()=>{l=null,c(i)},a-y)))},()=>i&&c(i)]}const Wl=(o,e,n=3)=>{let a=0;const i=U0(50,250);return P0(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,g=Math.max(0,f-a),m=i(g);a=Math.max(a,f);const y={loaded:f,total:d,progress:d?f/d:void 0,bytes:g,rate:m||void 0,estimated:m&&d?(d-f)/m:void 0,event:l,lengthComputable:d!=null,[e?"download":"upload"]:!0};o(y)},n)},Xp=(o,e)=>{const n=o!=null;return[a=>e[0]({lengthComputable:n,total:o,loaded:a}),e[1]]},Jp=o=>(...e)=>F.asap(()=>o(...e)),B0=mn.hasStandardBrowserEnv?((o,e)=>n=>(n=new URL(n,mn.origin),o.protocol===n.protocol&&o.host===n.host&&(e||o.port===n.port)))(new URL(mn.origin),mn.navigator&&/(msie|trident)/i.test(mn.navigator.userAgent)):()=>!0,q0=mn.hasStandardBrowserEnv?{write(o,e,n,a,i,l,c){if(typeof document>"u")return;const d=[`${o}=${encodeURIComponent(e)}`];F.isNumber(n)&&d.push(`expires=${new Date(n).toUTCString()}`),F.isString(a)&&d.push(`path=${a}`),F.isString(i)&&d.push(`domain=${i}`),l===!0&&d.push("secure"),F.isString(c)&&d.push(`SameSite=${c}`),document.cookie=d.join("; ")},read(o){if(typeof document>"u")return null;const e=document.cookie.split(";");for(let n=0;no instanceof An?{...o}:o;function mi(o,e){o=o||{},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(m,y,S,v){return F.isPlainObject(m)&&F.isPlainObject(y)?F.merge.call({caseless:v},m,y):F.isPlainObject(y)?F.merge({},y):F.isArray(y)?y.slice():y}function i(m,y,S,v){if(F.isUndefined(y)){if(!F.isUndefined(m))return a(void 0,m,S,v)}else return a(m,y,S,v)}function l(m,y){if(!F.isUndefined(y))return a(void 0,y)}function c(m,y){if(F.isUndefined(y)){if(!F.isUndefined(m))return a(void 0,m)}else return a(void 0,y)}function d(m){const y=F.hasOwnProp(e,"transitional")?e.transitional:void 0;if(!F.isUndefined(y))if(F.isPlainObject(y)){if(F.hasOwnProp(y,m))return y[m]}else return;const S=F.hasOwnProp(o,"transitional")?o.transitional:void 0;if(F.isPlainObject(S)&&F.hasOwnProp(S,m))return S[m]}function f(m,y,S){if(F.hasOwnProp(e,S))return a(m,y);if(F.hasOwnProp(o,S))return a(void 0,m)}const g={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:f,headers:(m,y,S)=>i(em(m),em(y),S,!0)};return F.forEach(Object.keys({...o,...e}),function(y){if(y==="__proto__"||y==="constructor"||y==="prototype")return;const S=F.hasOwnProp(g,y)?g[y]:i,v=F.hasOwnProp(o,y)?o[y]:void 0,_=F.hasOwnProp(e,y)?e[y]:void 0,A=S(v,_,y);F.isUndefined(A)&&S!==f||(n[y]=A)}),F.hasOwnProp(e,"validateStatus")&&F.isUndefined(e.validateStatus)&&d("validateStatusUndefinedResolves")===!1&&(F.hasOwnProp(o,"validateStatus")?n.validateStatus=a(void 0,o.validateStatus):delete n.validateStatus),n}const X0=["content-type","content-length"];function J0(o,e,n){if(n!=="content-only"){o.set(e);return}Object.entries(e||{}).forEach(([a,i])=>{X0.includes(a.toLowerCase())&&o.set(a,i)})}const Q0=o=>encodeURIComponent(o).replace(/%([0-9A-F]{2})/gi,(e,n)=>String.fromCharCode(parseInt(n,16)));function tm(o){const e=mi({},o),n=S=>F.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"),g=n("baseURL"),m=n("allowAbsoluteUrls"),y=n("url");if(e.headers=d=An.from(d),e.url=qp(Zp(g,y,m,e),n("params"),n("paramsSerializer")),f){const S=F.getSafeProp(f,"username")||"",v=F.getSafeProp(f,"password")||"";try{d.set("Authorization","Basic "+btoa(S+":"+(v?Q0(v):"")))}catch(_){throw Ee.from(_,Ee.ERR_BAD_OPTION_VALUE,o)}}if(F.isFormData(a)&&(mn.hasStandardBrowserEnv||mn.hasStandardBrowserWebWorkerEnv||F.isReactNative(a)?d.setContentType(void 0):F.isFunction(a.getHeaders)&&J0(d,a.getHeaders(),n("formDataHeaderPolicy"))),mn.hasStandardBrowserEnv&&(F.isFunction(i)&&(i=i(e)),i===!0||i==null&&B0(e.url))){const v=l&&c&&q0.read(c);v&&d.set(l,v)}return e}const Z0=typeof XMLHttpRequest<"u"&&function(o){return new Promise(function(n,a){const i=tm(o);let l=i.data;const c=An.from(i.headers).normalize();let{responseType:d,onUploadProgress:f,onDownloadProgress:g}=i,m,y,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 O(){if(!x)return;const T=An.from("getAllResponseHeaders"in x&&x.getAllResponseHeaders()),z={data:!d||d==="text"||d==="json"?x.responseText:x.response,status:x.status,statusText:x.statusText,headers:T,config:o,request:x};Yp(function(q){n(q),A()},function(q){a(q),A()},z),x=null}"onloadend"in x?x.onloadend=O:x.onreadystatechange=function(){!x||x.readyState!==4||x.status===0&&!(x.responseURL&&x.responseURL.startsWith("file:"))||setTimeout(O)},x.onabort=function(){x&&(a(new Ee("Request aborted",Ee.ECONNABORTED,o,x)),A(),x=null)},x.onerror=function(D){const z=D&&D.message?D.message:"Network Error",P=new Ee(z,Ee.ERR_NETWORK,o,x);P.event=D||null,a(P),A(),x=null},x.ontimeout=function(){let D=i.timeout?"timeout of "+i.timeout+"ms exceeded":"timeout exceeded";const z=i.transitional||Dd;i.timeoutErrorMessage&&(D=i.timeoutErrorMessage),a(new Ee(D,z.clarifyTimeoutError?Ee.ETIMEDOUT:Ee.ECONNABORTED,o,x)),A(),x=null},l===void 0&&c.setContentType(null),"setRequestHeader"in x&&F.forEach(Hp(c),function(D,z){x.setRequestHeader(z,D)}),F.isUndefined(i.withCredentials)||(x.withCredentials=!!i.withCredentials),d&&d!=="json"&&(x.responseType=i.responseType),g&&([S,_]=Wl(g,!0),x.addEventListener("progress",S)),f&&x.upload&&([y,v]=Wl(f),x.upload.addEventListener("progress",y),x.upload.addEventListener("loadend",v)),(i.cancelToken||i.signal)&&(m=T=>{x&&(a(!T||T.type?new po(null,o,x):T),x.abort(),A(),x=null)},i.cancelToken&&i.cancelToken.subscribe(m),i.signal&&(i.signal.aborted?m():i.signal.addEventListener("abort",m)));const M=z0(i.url);if(M&&!mn.protocols.includes(M)){a(new Ee("Unsupported protocol "+M+":",Ee.ERR_BAD_REQUEST,o)),A();return}x.send(l||null)})},eC=(o,e)=>{if(o=o?o.filter(Boolean):[],!e&&!o.length)return;const n=new AbortController;let a=!1;const i=function(f){if(!a){a=!0,c();const g=f instanceof Error?f:this.reason;n.abort(g instanceof Ee?g:new po(g instanceof Error?g.message:g))}};let l=e&&setTimeout(()=>{l=null,i(new Ee(`timeout of ${e}ms exceeded`,Ee.ETIMEDOUT))},e);const c=()=>{o&&(l&&clearTimeout(l),l=null,o.forEach(f=>{f.unsubscribe?f.unsubscribe(i):f.removeEventListener("abort",i)}),o=null)};o.forEach(f=>f.addEventListener("abort",i,{once:!0}));const{signal:d}=n;return d.unsubscribe=()=>F.asap(c),d},tC=function*(o,e){let n=o.byteLength;if(n{const i=nC(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(n){let S=l+=y;n(S)}f.enqueue(new Uint8Array(m))}catch(g){throw d(g),g}},cancel(f){return d(f),i.return()}},{highWaterMark:2})},Yl=o=>o>=48&&o<=57||o>=65&&o<=70||o>=97&&o<=102,aC=(o,e,n)=>e+2v>=2&&a.charCodeAt(v-2)===37&&a.charCodeAt(v-1)===51&&(a.charCodeAt(v)===68||a.charCodeAt(v)===100);g>=0&&(a.charCodeAt(g)===61?(f++,g--):m(g)&&(f++,g-=3)),f===1&&g>=0&&(a.charCodeAt(g)===61||m(g))&&f++;const S=Math.floor(c/4)*3-(f||0);return S>0?S:0}let l=0;for(let c=0,d=a.length;c=55296&&f<=56319&&c+1=56320&&g<=57343?(l+=4,c++):l+=3}else l+=3}return l}const $d="1.18.1",rm=64*1024,{isFunction:Xl}=F,sC=o=>encodeURIComponent(o).replace(/%([0-9A-F]{2})/gi,(e,n)=>String.fromCharCode(parseInt(n,16))),am=o=>{if(!F.isString(o))return o;try{return decodeURIComponent(o)}catch{return o}},im=(o,...e)=>{try{return!!o(...e)}catch{return!1}},oC=o=>{const e=o.indexOf("://");let n=o;return e!==-1&&(n=n.slice(e+3)),n.includes("@")||n.includes(":")},lC=o=>{const e=F.global!==void 0&&F.global!==null?F.global:globalThis,{ReadableStream:n,TextEncoder:a}=e;o=F.merge.call({skipUndefined:!0},{Request:e.Request,Response:e.Response},o);const{fetch:i,Request:l,Response:c}=o,d=i?Xl(i):typeof fetch=="function",f=Xl(l),g=Xl(c);if(!d)return!1;const m=d&&Xl(n),y=d&&(typeof a=="function"?(O=>M=>O.encode(M))(new a):async O=>new Uint8Array(await new l(O).arrayBuffer())),S=f&&m&&im(()=>{let O=!1;const M=new l(mn.origin,{body:new n,method:"POST",get duplex(){return O=!0,"half"}}),T=M.headers.has("Content-Type");return M.body!=null&&M.body.cancel(),O&&!T}),v=g&&m&&im(()=>F.isReadableStream(new c("").body)),_={stream:v&&(O=>O.body)};d&&["text","arrayBuffer","blob","formData","stream"].forEach(O=>{!_[O]&&(_[O]=(M,T)=>{let D=M&&M[O];if(D)return D.call(M);throw new Ee(`Response type '${O}' is not supported`,Ee.ERR_NOT_SUPPORT,T)})});const A=async O=>{if(O==null)return 0;if(F.isBlob(O))return O.size;if(F.isSpecCompliantForm(O))return(await new l(mn.origin,{method:"POST",body:O}).arrayBuffer()).byteLength;if(F.isArrayBufferView(O)||F.isArrayBuffer(O))return O.byteLength;if(F.isURLSearchParams(O)&&(O=O+""),F.isString(O))return(await y(O)).byteLength},x=async(O,M)=>{const T=F.toFiniteNumber(O.getContentLength());return T??A(M)};return async O=>{let{url:M,method:T,data:D,signal:z,cancelToken:P,timeout:q,onDownloadProgress:W,onUploadProgress:he,responseType:Se,headers:be,withCredentials:Ue="same-origin",fetchOptions:Fe,maxContentLength:Ge,maxBodyLength:wt}=tm(O);const J=F.isNumber(Ge)&&Ge>-1,ue=F.isNumber(wt)&&wt>-1,Me=ee=>F.hasOwnProp(O,ee)?O[ee]:void 0;let te=i||fetch;Se=Se?(Se+"").toLowerCase():"text";let pe=eC([z,P&&P.toAbortSignal()],q),N=null;const R=pe&&pe.unsubscribe&&(()=>{pe.unsubscribe()});let de,fe=null;const xe=()=>new Ee("Request body larger than maxBodyLength limit",Ee.ERR_BAD_REQUEST,O,N);try{let ee;const _e=Me("auth");if(_e){const Oe=F.getSafeProp(_e,"username")||"",nt=F.getSafeProp(_e,"password")||"";ee={username:Oe,password:nt}}if(oC(M)){const Oe=new URL(M,mn.origin);if(!ee&&(Oe.username||Oe.password)){const nt=am(Oe.username),sn=am(Oe.password);ee={username:nt,password:sn}}(Oe.username||Oe.password)&&(Oe.username="",Oe.password="",M=Oe.href)}if(ee&&(be.delete("authorization"),be.set("Authorization","Basic "+btoa(sC((ee.username||"")+":"+(ee.password||""))))),J&&typeof M=="string"&&M.startsWith("data:")&&iC(M)>Ge)throw new Ee("maxContentLength size of "+Ge+" exceeded",Ee.ERR_BAD_RESPONSE,O,N);if(ue&&T!=="get"&&T!=="head"){const Oe=await A(D);if(typeof Oe=="number"&&isFinite(Oe)&&(de=Oe,Oe>wt))throw xe()}const Be=ue&&(F.isReadableStream(D)||F.isStream(D)),ke=(Oe,nt,sn)=>nm(Oe,rm,Mt=>{if(ue&&Mt>wt)throw fe=xe();nt&&nt(Mt)},sn);if(S&&T!=="get"&&T!=="head"&&(he||Be)){if(de=de??await x(be,D),de!==0||Be){let Oe=new l(M,{method:"POST",body:D,duplex:"half"}),nt;if(F.isFormData(D)&&(nt=Oe.headers.get("content-type"))&&be.setContentType(nt),Oe.body){const[sn,Mt]=he&&Xp(de,Wl(Jp(he)))||[];D=ke(Oe.body,sn,Mt)}}}else if(Be&&!f&&m&&T!=="get"&&T!=="head")D=ke(D);else if(Be&&f&&!S&&T!=="get"&&T!=="head")throw new Ee("Stream request bodies are not supported by the current fetch implementation",Ee.ERR_NOT_SUPPORT,O,N);F.isString(Ue)||(Ue=Ue?"include":"omit");const Ie=f&&"credentials"in l.prototype;if(F.isFormData(D)){const Oe=be.getContentType();Oe&&/^multipart\/form-data/i.test(Oe)&&!/boundary=/i.test(Oe)&&be.delete("content-type")}be.set("User-Agent","axios/"+$d,!1);const Ct={...Fe,signal:pe,method:T.toUpperCase(),headers:Hp(be.normalize()),body:D,duplex:"half",credentials:Ie?Ue:void 0};N=f&&new l(M,Ct);let ot=await(f?te(N,Fe):te(M,Ct));const Ot=An.from(ot.headers);if(J){const Oe=F.toFiniteNumber(Ot.getContentLength());if(Oe!=null&&Oe>Ge)throw new Ee("maxContentLength size of "+Ge+" exceeded",Ee.ERR_BAD_RESPONSE,O,N)}const vt=v&&(Se==="stream"||Se==="response");if(v&&ot.body&&(W||J||vt&&R)){const Oe={};["status","statusText","headers"].forEach(ln=>{Oe[ln]=ot[ln]});const nt=F.toFiniteNumber(Ot.getContentLength()),[sn,Mt]=W&&Xp(nt,Wl(Jp(W),!0))||[];let Vt=0;const on=ln=>{if(J&&(Vt=ln,Vt>Ge))throw new Ee("maxContentLength size of "+Ge+" exceeded",Ee.ERR_BAD_RESPONSE,O,N);sn&&sn(ln)};ot=new c(nm(ot.body,rm,on,()=>{Mt&&Mt(),R&&R()}),Oe)}Se=Se||"text";let _t=await _[F.findKey(_,Se)||"text"](ot,O);if(J&&!v&&!vt){let Oe;if(_t!=null&&(typeof _t.byteLength=="number"?Oe=_t.byteLength:typeof _t.size=="number"?Oe=_t.size:typeof _t=="string"&&(Oe=typeof a=="function"?new a().encode(_t).byteLength:_t.length)),typeof Oe=="number"&&Oe>Ge)throw new Ee("maxContentLength size of "+Ge+" exceeded",Ee.ERR_BAD_RESPONSE,O,N)}return!vt&&R&&R(),await new Promise((Oe,nt)=>{Yp(Oe,nt,{data:_t,headers:An.from(ot.headers),status:ot.status,statusText:ot.statusText,config:O,request:N})})}catch(ee){if(R&&R(),pe&&pe.aborted&&pe.reason instanceof Ee){const _e=pe.reason;throw _e.config=O,N&&(_e.request=N),ee!==_e&&Object.defineProperty(_e,"cause",{__proto__:null,value:ee,writable:!0,enumerable:!1,configurable:!0}),_e}if(fe)throw N&&!fe.request&&(fe.request=N),fe;if(ee instanceof Ee)throw N&&!ee.request&&(ee.request=N),ee;if(ee&&ee.name==="TypeError"&&/Load failed|fetch/i.test(ee.message)){const _e=new Ee("Network Error",Ee.ERR_NETWORK,O,N,ee&&ee.response);throw Object.defineProperty(_e,"cause",{__proto__:null,value:ee.cause||ee,writable:!0,enumerable:!1,configurable:!0}),_e}throw Ee.from(ee,ee&&ee.code,O,N,ee&&ee.response)}}},cC=new Map,sm=o=>{let e=o&&o.env||{};const{fetch:n,Request:a,Response:i}=e,l=[a,i,n];let c=l.length,d=c,f,g,m=cC;for(;d--;)f=l[d],g=m.get(f),g===void 0&&m.set(f,g=d?new Map:lC(e)),m=g;return g};sm();const Nd={http:T0,xhr:Z0,fetch:{get:sm}};F.forEach(Nd,(o,e)=>{if(o){try{Object.defineProperty(o,"name",{__proto__:null,value:e})}catch{}Object.defineProperty(o,"adapterName",{__proto__:null,value:e})}});const om=o=>`- ${o}`,uC=o=>F.isFunction(o)||o===null||o===!1;function dC(o,e){o=F.isArray(o)?o:[o];const{length:n}=o;let a,i;const l={};for(let c=0;c`adapter ${f} `+(g===!1?"is not supported by the environment":"is not available in the build"));let d=n?c.length>1?`since : -`+c.map(om).join(` -`):" "+om(c[0]):"as no adapter specified";throw new Ee("There is no suitable adapter to dispatch the request "+d,Ee.ERR_NOT_SUPPORT)}return i}const lm={getAdapter:dC,adapters:Nd};function Id(o){if(o.cancelToken&&o.cancelToken.throwIfRequested(),o.signal&&o.signal.aborted)throw new po(null,o)}function cm(o){return Id(o),o.headers=An.from(o.headers),o.data=Md.call(o,o.transformRequest),["post","put","patch"].indexOf(o.method)!==-1&&o.headers.setContentType("application/x-www-form-urlencoded",!1),lm.getAdapter(o.adapter||go.adapter,o)(o).then(function(a){Id(o),o.response=a;try{a.data=Md.call(o,o.transformResponse,a)}finally{delete o.response}return a.headers=An.from(a.headers),a},function(a){if(!Wp(a)&&(Id(o),a&&a.response)){o.response=a.response;try{a.response.data=Md.call(o,o.transformResponse,a.response)}finally{delete o.response}a.response.headers=An.from(a.response.headers)}return Promise.reject(a)})}const Jl={};["object","boolean","number","function","string","symbol"].forEach((o,e)=>{Jl[o]=function(a){return typeof a===o||"a"+(e<1?"n ":" ")+o}});const um={};Jl.transitional=function(e,n,a){function i(l,c){return"[Axios v"+$d+"] Transitional option '"+l+"'"+c+(a?". "+a:"")}return(l,c,d)=>{if(e===!1)throw new Ee(i(c," has been removed"+(n?" in "+n:"")),Ee.ERR_DEPRECATED);return n&&!um[c]&&(um[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}},Jl.spelling=function(e){return(n,a)=>(console.warn(`${a} is likely a misspelling of ${e}`),!0)};function fC(o,e,n){if(typeof o!="object"||o===null)throw new Ee("options must be an object",Ee.ERR_BAD_OPTION_VALUE);const a=Object.keys(o);let i=a.length;for(;i-- >0;){const l=a[i],c=Object.prototype.hasOwnProperty.call(e,l)?e[l]:void 0;if(c){const d=o[l],f=d===void 0||c(d,l,o);if(f!==!0)throw new Ee("option "+l+" must be "+f,Ee.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new Ee("Unknown option "+l,Ee.ERR_BAD_OPTION)}}const Ql={assertOptions:fC,validators:Jl},xn=Ql.validators;let yi=class{constructor(e){this.defaults=e||{},this.interceptors={request:new Gp,response:new Gp}}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(` +`+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"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"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 Pc(){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=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 Wc(t,r){if(Lr)return t==="compositionend"||!I&>(t,r)?(t=Pc(),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)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=Uu(hh,"onSelect"),0>=w,p-=w,Wr=1<<32-on(r)+p|o<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(;JeJe?(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=ru(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,uu(Y),oe);if(Y.$$typeof===k)return kt(F,q,su(F,Y),oe);du(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===lu)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=nu(t),sb(t,null,o),r}return tu(t,u,r,o),nu(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;tg?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},wu(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(wu(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 tu(t,r,p,0),Rt===null&&eu(),!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},wu(t)){if(r)throw Error(a(479))}else r=bh(t,o,u,2),r!==null&&Bn(r,t,2)}function wu(t){var r=t.alternate;return t===Xe||r!==null&&r===Xe}function lv(t,r){Hs=pu=!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:yu,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:yu,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,vu(4194308,4,Jb.bind(null,r,t),o)},useLayoutEffect:function(t,r){return vu(4194308,4,t,r)},useInsertionEffect:function(t,r){vu(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=gu++,0<\/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=Pu(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),xu(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=hu(t),g!==null){for(r.flags|=128,yl(u,!1),t=g.updateQueue,r.updateQueue=t,xu(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()>Ou&&(r.flags|=128,p=!0,yl(u,!1),r.lanes=4194304)}else{if(!p)if(t=hu(g),t!==null){if(r.flags|=128,p=!0,t=t.updateQueue,r.updateQueue=t,xu(r,t),yl(u,!0),u.tail===null&&u.tailMode==="hidden"&&!g.alternate&&!ut)return Lt(r),null}else 2*qt()-u.renderingStartTime>Ou&&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&&xu(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 Tu(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(Tu(t,r,o),t=t.sibling;t!==null;)Tu(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=Wu,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},Wu=!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 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;Rkt&&(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;Ro?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()-Du?(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 ju=null,Ws=null,Op=!1,Hu=!1,Lp=!1,oi=0;function Jr(t){t!==Ws&&t.next===null&&(Ws===null?ju=Ws=t:Ws=Ws.next=t),Hu=!0,Op||(Op=!0,eT())}function Al(t,r){if(!Lp&&Hu){Lp=!0;do for(var o=!1,u=ju;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(){Hu=Op=!1;var t=0;oi!==0&&uT()&&(t=oi);for(var r=qt(),o=null,u=ju;u!==null;){var p=u.next,g=SS(u,r);g===0?(u.next=null,o===null?ju=p:o.next=p,p===null&&(Ws=o)):(o=u,(t!==0||(g&3)!==0)&&(Hu=!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;0R)break;var re=z.transferSize,le=z.initiatorType;re&&DS(le)&&(z=z.responseEnd,w+=re*(z"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,qu(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)?Bu(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),qu(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,qu(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,qu(u,o.precedence,t));return r.instance}function qu(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 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=Vu.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=Vu.bind(t),r.addEventListener("load",o),r.addEventListener("error",o))}}var Xp=0;function RT(t,r){return t.stylesheets&&t.count===0&&Ku(t,t.stylesheets),0Xp?50:800)+r);return t.unsuspend=o,function(){t.unsuspend=null,clearTimeout(u),clearTimeout(p)}}:null}function Vu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Ku(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Fu=null;function Ku(t,r){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Fu=new Map,r.forEach(DT,t),Fu=null,Vu.call(t))}function DT(t,r){if(!(r.state.loading&4)){var o=Fu.get(t);if(o)var u=o.get(null);else{o=new Map,Fu.set(t,o);for(var p=t.querySelectorAll("link[data-precedence],style[data-precedence]"),g=0;g"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(s)}catch(e){console.error(e)}}return s(),rd.exports=gw(),rd.exports}var od=mw();const ld=Zr(od),eo=[],yw=50;let cd=!1;function fg(){try{return window.G7Core?.devTools}catch{return}}function Qi(s,e,n){const a=fg();if(cd&&a?.isEnabled?.()){a.trackLog?.(s,e,n);return}cd||(s==="error"||s==="warn")&&eo.length{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 dd(s){return s?.name==="AbortError"}function mg(s){return dd(s)?!1:s instanceof TypeError}let Nl=!1,yg=!1;function fd(){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;m0?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=_,dd(_)&&!b||!(b||mg(_)))throw _;if(fd())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{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 hd 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 hd(`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 hd(`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"="+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(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,pd=null;function _w(s){return pd||(pd=new xa(s)),pd}const Ag=Object.freeze(Object.defineProperty({__proto__:null,TranslationEngine:xa,TranslationError:hd,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 gd(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=gd(s);return n?xg(n,e):""},description:"날짜 포맷 (기본: YYYY-MM-DD)"},datetime:{fn:(s,e="YYYY-MM-DD HH:mm")=>{const n=gd(s);return n?xg(n,e):""},description:"날짜+시간 포맷 (기본: YYYY-MM-DD HH:mm)"},relativeTime:{fn:(s,e="ko")=>{const n=gd(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;h0?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;l0?s[l-1]:"",f=l0?s[a-1]:"",c=a0){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;n0){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":md.has(e)?"literal":qn(e)?"pipe":ro(e)?"expression":"path"}function yd(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 md.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 bd(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)?bd(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}),vd={Math,JSON,Date,Array,Object:Ow,Number,String,Boolean,Set,Map,WeakSet,WeakMap,parseInt,parseFloat,isNaN,isFinite},Sd={Date,Set,Map,WeakSet,WeakMap,Array,Number,String,Boolean,Object},Og=new Set([...Object.values(vd),...Object.values(Sd)].filter(s=>s!==null&&(typeof s=="object"||typeof s=="function"))),wd=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(;a0;){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)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"?(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.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)&&!wd.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();if(!n||n.type!=="ident"||wd.has(String(n.value)))return null;const a=String(n.value);this.pos+=1;let i=null;if(this.isPunct("=")&&(this.pos+=1,i=this.parseExpression()),e.push({name:a,default:i}),this.isPunct(",")){this.pos+=1;continue}break}return e}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||lMg(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(wd.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:""}" 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(vd,s))return vd[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":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=Sd[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;a0}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;fra.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=md.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(/(?(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=/(?{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(Pl.has(e))return Pl.get(e)??null;let n=null;if(Ul[e])n={...Ul[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 Pl.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:!!Ul[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(),Pl.clear()}_setWidthForTesting(e){this.currentWidth=e,this.notifySubscribers()}}const mi=new Pw,lo=dt("ConditionEvaluator");function Bw(s,e,n,a){return yd(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 Bl(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(!Bl(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(Bl(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{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)?Bl(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 bd(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=yd(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:ql}=Object,{iterator:Gl,toStringTag:Kg}=Symbol,Vl=(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=>Vl(e)===s),Fl=s=>e=>typeof e===s,{isArray:ss}=Array,os=Fl("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=Fl("string"),Ln=Fl("function"),Yg=Fl("number"),fo=s=>s!==null&&typeof s=="object",Jw=s=>s===!0||s===!1,Kl=s=>{if(Vl(s)!=="object")return!1;const e=ql(s);return(e===null||e===Object.prototype||Object.getPrototypeOf(e)===null)&&!(Kg in s)&&!(Gl 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=ql(s);if(!e||e===Object.prototype||!Ln(s.append))return!1;const n=Vl(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;a0;)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;Kl(f)&&Kl(l)?a[d]=Md(f,l):Kl(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(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&&ql(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"&&ql(Uint8Array)),w0=(s,e)=>{const a=(s&&s[Gl]).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[Gl])}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:Kl,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:Vl,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[Gl])},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(;ee;){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 Wl(s){return s===!1||s==null?s:K.isArray(s)?s.map(Wl):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]=Wl(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]=Wl(i),delete n[l];return}const d=e?G0(l):String(l).trim();d!==l&&delete n[l],n[d]=Wl(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 Yl(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&&Yl(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 Yl(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=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 Yl(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{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 Xl=(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;ns 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,_]=Xl(h,!0),x.addEventListener("progress",S)),f&&x.upload&&([b,v]=Xl(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{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=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=55296&&f<=56319&&c+1=56320&&h<=57343?(l+=4,c++):l+=3}else l+=3}return l}const Gd="1.16.1",bm=64*1024,{isFunction:Jl}=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?Jl(i):typeof fetch=="function",f=Jl(l),h=Jl(c);if(!d)return!1;const m=d&&Jl(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,Xl(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,Xl(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`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 Ql={};["object","boolean","number","function","string","symbol"].forEach((s,e)=>{Ql[s]=function(a){return typeof a===s||"a"+(e<1?"n ":" ")+s}});const _m={};Ql.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}},Ql.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 Zl={assertOptions:TC,validators:Ql},rr=Zl.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=mi(this.defaults,n);const{transitional:a,paramsSerializer:i,headers:l}=n;a!==void 0&&Ql.assertOptions(a,{silentJSONParsing:xn.transitional(xn.boolean),forcedJSONParsing:xn.transitional(xn.boolean),clarifyTimeoutError:xn.transitional(xn.boolean),legacyInterceptorReqResOrdering:xn.transitional(xn.boolean),advertiseZstdAcceptEncoding:xn.transitional(xn.boolean),validateStatusUndefinedResolves:xn.transitional(xn.boolean)},!1),i!=null&&(F.isFunction(i)?n.paramsSerializer={serialize:i}:Ql.assertOptions(i,{encode:xn.function,serialize:xn.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),Ql.assertOptions(n,{baseUrl:xn.spelling("baseURL"),withXsrfToken:xn.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let c=l&&F.merge(l.common,l[n.method]);l&&F.forEach(["delete","get","head","post","put","patch","query","common"],_=>{delete l[_]}),n.headers=An.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||Dd;x&&x.legacyInterceptorReqResOrdering?d.unshift(A.fulfilled,A.rejected):d.push(A.fulfilled,A.rejected)});const g=[];this.interceptors.response.forEach(function(A){g.push(A.fulfilled,A.rejected)});let m,y=0,S;if(!f){const _=[cm.bind(this),void 0];for(_.unshift(...d),_.push(...g),S=_.length,m=Promise.resolve(n);y{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 po(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 WS(function(i){e=i}),cancel:e}}};function gC(o){return function(n){return o.apply(null,n)}}function pC(o){return F.isObject(o)&&o.isAxiosError===!0}const Hd={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(Hd).forEach(([o,e])=>{Hd[e]=o});function dm(o){const e=new yi(o),n=xp(yi.prototype.request,e);return F.extend(n,yi.prototype,e,{allOwnKeys:!0}),F.extend(n,e,null,{allOwnKeys:!0}),n.create=function(i){return dm(mi(o,i))},n}const Yt=dm(go);Yt.Axios=yi,Yt.CanceledError=po,Yt.CancelToken=hC,Yt.isCancel=Wp,Yt.VERSION=$d,Yt.toFormData=Kl,Yt.AxiosError=Ee,Yt.Cancel=Yt.CanceledError,Yt.all=function(e){return Promise.all(e)},Yt.spread=gC,Yt.isAxiosError=pC,Yt.mergeConfig=mi,Yt.AxiosHeaders=An,Yt.formToJSON=o=>Kp(F.isHTMLForm(o)?new FormData(o):o),Yt.getAdapter=lm.getAdapter,Yt.HttpStatusCode=Hd,Yt.default=Yt;const{Axios:LT,AxiosError:MT,CanceledError:$T,isCancel:NT,CancelToken:IT,VERSION:HT,all:jT,Cancel:zT,isAxiosError:UT,spread:PT,toFormData:BT,AxiosHeaders:qT,HttpStatusCode:GT,formToJSON:VT,getAdapter:FT,mergeConfig:KT,create:WT}=Yt,jd="g7.identity.redirectStash",Zl=ht("IdentityGuardInterceptor");class Et{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){Zl.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??yC,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=mC(f.url,d.token);return fetch(g,{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 Zl.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(jd,JSON.stringify(a))}catch(i){Zl.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}}$(Et,"launcher",null),$(Et,"deferredResolver",null),$(Et,"domainNoticeShown",!1);function mC(o,e){if(!e)return o;try{const n=typeof window<"u"?window.location.origin:"http://localhost",a=new URL(o,n);return a.searchParams.set("verification_token",e),/^https?:\/\//i.test(o)?a.toString():`${a.pathname}${a.search}${a.hash}`}catch{const n=o.includes("?")?"&":"?";return`${o}${n}verification_token=${encodeURIComponent(e)}`}}const yC=async o=>{if(typeof window>"u")return{status:"cancelled"};if(o.render_hint==="external_redirect"||o.redirect_url)return Et.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 n=window.location.href,a={return_url:n,payload:o,stashed_at:Date.now()};try{window.sessionStorage?.setItem(jd,JSON.stringify(a))}catch{}const i=`/identity/challenge?return=${encodeURIComponent(n)}`;try{await e.dispatch({handler:"navigate",params:{path:i}})}catch(l){Zl.warn("navigate 실패 — window.location 으로 폴백",l),window.location.href=i}return new Promise(()=>{})},bC=ht("ApiClient");function zd(){return typeof window<"u"&&window.__G7_DEVTOOLS__?window.__G7_DEVTOOLS__:null}class vC{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=Yt.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=zd();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=zd();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 br.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(Et.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 g=n?.identity_target,m=await Et.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(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=zd();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){bC.error("Error in onError handler:",a)}}}let Ud=null;function Hr(){return Ud||(Ud=new vC),Ud}const mo=ht("AuthManager");function cs(o,e,n,a){try{window.G7Core?.devTools?.trackAuthEvent?.(o,e,n,a)}catch{}}const ec={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",ec.admin),this.config.set("user",ec.user)}static getInstance(){return Ar.instance||(Ar.instance=new Ar),Ar.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 mo.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 mo.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"?ec.admin.loginEndpoint:ec.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 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(S){mo.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}),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,cs("login",!1,g.message,{type:e,status:d.response?.status}),g}}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){mo.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 mo.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(){Ar.instance=null}};$(Ar,"instance"),$(Ar,"LOCALE_STORAGE_KEY","g7_locale");let br=Ar;const fm={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}}"}}},Qr={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,n){if(o.handler!=="showErrorPage"||o.params?.errorCode!==void 0)return o;const a=typeof e=="string"?parseInt(e,10):e,i=isNaN(a)?n:a;return i===void 0?o:{...o,params:{...o.params,errorCode:i}}}},vr=ht("ErrorHandlingResolver"),Ma=class Ma{constructor(){$(this,"templateErrorHandling",null);$(this,"layoutErrorHandling",null);$(this,"executeAction",null)}static getInstance(){return Ma.instance||(Ma.instance=new Ma),Ma.instance}static resetInstance(){Ma.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,n={}){const{errorHandling:a,onError:i}=n;vr.log("Resolving error:",e,{hasActionErrorHandling:!!a,hasActionOnError:!!i,hasLayoutConfig:!!this.layoutErrorHandling,hasTemplateConfig:!!this.templateErrorHandling});let l=Qr.findHandler(a,e);if(l){const c=this.getMatchedKey(a,e);return{handler:Qr.injectErrorCode(l,c,e),level:"action",matchedKey:c}}if(i){const c=Qr.normalizeOnError(i);if(c.length>0)return{handler:c.length===1?c[0]:{handler:"sequence",actions:c},level:"action",matchedKey:"onError"}}if(l=Qr.findHandler(this.layoutErrorHandling||void 0,e),l){const c=this.getMatchedKey(this.layoutErrorHandling,e);return{handler:Qr.injectErrorCode(l,c,e),level:"layout",matchedKey:c}}if(l=Qr.findHandler(this.templateErrorHandling||void 0,e),l){const c=this.getMatchedKey(this.templateErrorHandling,e);return{handler:Qr.injectErrorCode(l,c,e),level:"template",matchedKey:c}}if(l=Qr.findHandler(fm,e),l){const c=this.getMatchedKey(fm,e);return{handler:Qr.injectErrorCode(l,c,e),level:"system",matchedKey:c}}return{handler:null,level:null,matchedKey:null}}async execute(e,n){if(!this.executeAction){vr.error("Action executor is not set");return}vr.log("Executing handler:",e.handler,{errorContext:n});try{return await this.executeAction(e,{error:n})}catch(a){throw vr.error("Failed to execute handler:",a),a}}async resolveAndExecute(e,n,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,n)}}catch(l){return vr.error("Handler execution failed:",l),{handled:!1}}}getMatchedKey(e,n){return e[n]?n:e[String(n)]?String(n):"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}}};$(Ma,"instance",null);let Pd=Ma;function Sr(){return Pd.getInstance()}var Bd={exports:{}},yo={};var hm;function SC(){if(hm)return yo;hm=1;var o=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:o,type:a,key:c,ref:i!==void 0?i:null,props:l}}return yo.Fragment=e,yo.jsx=n,yo.jsxs=n,yo}var gm;function wC(){return gm||(gm=1,Bd.exports=SC()),Bd.exports}var gt=wC();const CC=Xr({__proto__:null,default:Jr(gt)},[gt]),qd=ht("ParentContextProvider"),pm=H.createContext(null);let tc=null;function Gd(){tc?(qd.log("[triggerModalParentUpdate] 모달 부모 컨텍스트 업데이트 트리거"),tc()):qd.warn("[triggerModalParentUpdate] Provider가 아직 마운트되지 않음")}const mm=({children:o})=>{const[e,n]=H.useState(0),a=H.useRef(!0),i=H.useCallback(()=>{const c=window.__g7LayoutContextStack||[];return c[c.length-1]?.dataContext},[]);H.useEffect(()=>(a.current=!0,tc=()=>{a.current&&(n(c=>c+1),qd.log("[ParentContextProvider] 버전 업데이트됨"))},()=>{a.current=!1,tc=null}),[]);const l={version:e,getParentDataContext:i};return gt.jsx(pm.Provider,{value:l,children:o})};function EC(){return H.useContext(pm)}const j=ht("ActionDispatcher");function _C(o,e,n,a){return n||(ip(o)||dd(o)?"$t:core.errors.network_request_failed":a||`Failed to execute action: ${e}`)}const ym=new Set(["navigate","navigateBack","navigateForward","replaceUrl","refresh","logout"]),AC=new Set(["redirect"]);function Fn(){try{return window.G7Core?.devTools}catch{return}}class Bt 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 bm={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 Dn,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=Fn();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 ym}static getPreviewSuppressedLayoutFeatures(){return AC}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=Fn();a?.isEnabled?.()&&a.trackNamedActionRef?.({actionRefName:e.actionRef,resolvedHandler:n.handler,timestamp:Date.now()});const{actionRef:i,type:l,key:c,event:d,...f}=e,g={...n,...Object.fromEntries(Object.entries(f).filter(([,m])=>m!==void 0)),type:l??n.type};return c!==void 0&&(g.key=c),d!==void 0&&(g.event=d),g}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=rs(c,n,{skipCache:!0});d!=null&&d!==""&&(a[l]=String(d))});return a}setupErrorHandling(){if(this.errorHandlingSetup)return;Sr().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?.()||{},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(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:g=2e3,delay:m=0,retryCount:y=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 O=setTimeout(()=>{M.disconnect(),A(null)},g),M=new MutationObserver(()=>{const T=document.querySelector(i);T&&(clearTimeout(O),M.disconnect(),A(T))});M.observe(document.body,{childList:!0,subtree:!0})}),!_){j.warn(`scrollIntoView: element not found for selector "${i}" after ${g}ms timeout`);return}}else{m>0&&await new Promise(x=>setTimeout(x,m)),_=document.querySelector(i);let A=0;for(;!_&&AsetTimeout(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(),O=A.getBoundingClientRect(),M=x.top-O.top+A.scrollTop,T=M+x.height;let D;switch(c){case"start":D=M;break;case"center":D=M-A.clientHeight/2+x.height/2;break;case"end":D=T-A.clientHeight;break;default:const z=x.topO.bottom;if(z)D=M;else if(P)D=T-A.clientHeight;else return;break}A.scrollTo({top:Math.max(0,D),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 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((S,v)=>{y.onload=()=>{j.log(`reloadModuleHandlers: Script loaded successfully for ${f}`),S()},y.onerror=()=>{j.error(`reloadModuleHandlers: Failed to load script for ${f}`),v(new Error(`Failed to load module script: ${g}`))},document.head.appendChild(y)}),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 g=`module-${f}`,m=document.getElementById(g);m&&(m.remove(),j.log(`reloadModuleHandlers: Removed script for ${f}`));const y=`module-css-${f}`,S=document.getElementById(y);S&&(S.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(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 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((S,v)=>{y.onload=()=>{j.log(`reloadPluginHandlers: Script loaded successfully for ${f}`),S()},y.onerror=()=>{j.error(`reloadPluginHandlers: Failed to load script for ${f}`),v(new Error(`Failed to load plugin script: ${g}`))},document.head.appendChild(y)}),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 g=`plugin-${f}`,m=document.getElementById(g);m&&(m.remove(),j.log(`reloadPluginHandlers: Removed script for ${f}`));const y=`plugin-css-${f}`,S=document.getElementById(y);S&&(S.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(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 g=(m,y=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((O,M)=>{const T=document.getElementById(m);T&&(j.log(`showErrorPage: Container #${m} found`),M.disconnect(),clearTimeout(A),S(T))});x.observe(document.body,{childList:!0,subtree:!0}),A=setTimeout(()=>{x.disconnect(),v(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(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 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(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),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(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 g=f.state.get()||{},y=(g.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(g._local?.modifiedProductIds||[]);S.add(i),this.globalStateUpdater({products:{...g.products,data:{...g.products?.data,data:y}},_local:{...g._local,modifiedProductIds:Array.from(S)}}),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(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=Fn();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??{},y=Date.now()-l;for(const S of Object.keys(c)){const v=c[S],_=m[S];v!==_&&y>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="${_}" (${y}ms ago)`))}}try{f.type!=="change"&&f.preventDefault();let g={};if(f.type==="submit"&&f.target instanceof HTMLFormElement){const S=f.target;new FormData(S).forEach((_,A)=>{g[A]=_})}const m={...this.defaultContext,data:{...n,form:g,_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 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 Bt&&g.originalError?g.originalError:g,y={...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,y)}}}}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=Fn(),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),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,n.data);i&&(c=this.sanitizeForDevTools(f));const g=e.target?this.resolveValue(e.target,n.data):void 0;if(e.handler==="apiCall"&&n.setState){d=this.generateActionId(e,g);const y=n.state?.loadingActions||{};n.setState({loadingActions:{...y,[d]:!0}})}if(this.previewMode&&ym.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,n);break;case"navigateBack":m=await this.handleNavigateBack();break;case"navigateForward":m=await this.handleNavigateForward();break;case"openWindow":{const _=f.path||g;m=await this.handleOpenWindow(_,f);break}case"replaceUrl":{const _=f.path||g||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(g,f,n,S,v);break;case"login":m=await this.handleLogin(g,f,n);break;case"logout":m=await this.handleLogout(g,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(g,f,n);break;case"openModal":m=await this.handleOpenModal(g,n);break;case"closeModal":m=await this.handleCloseModal(n);break;case"showAlert":m=await this.handleShowAlert(g,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:g,params:f},n);break}if(e.resultTo&&m!==void 0){const{target:y,key:S,merge:v}=e.resultTo,_=this.resolveValue(S,n.data),A=v==="replace"?"replace":v==="shallow"?"shallow":"deep";if(y==="_local"&&n.setState){const x=this.buildNestedUpdate(_,m),O=A!=="deep"?{...x,__mergeMode:A}:x;n.setState(O),j.log(`[resultTo] Saved to _local.${_} (merge=${A}):`,m)}else if(y==="_local"&&this.globalStateUpdater){const M=(window.G7Core?.state?.get()||{})._local||{},T=this.buildNestedUpdate(_,m);let D;A==="replace"?D=T:A==="shallow"?D={...M,...T}:D=this.deepMergeWithState(T,M),this.globalStateUpdater({_local:D}),j.log(`[resultTo] Saved to _local.${_} via globalStateUpdater (merge=${A}):`,m)}else if(y==="_global"&&this.globalStateUpdater){const x=this.buildNestedUpdate(_,m);this.globalStateUpdater(x),j.log(`[resultTo] Saved to _global.${_}:`,m)}else if(y==="_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=${y}, 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 y={...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},y):S.length===1&&await this.executeAction(S[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 Bt?f:new Bt(`Failed to execute action: ${e.handler}`,e,f instanceof Error?f:void 0),m=g.originalError?.response||{},y=m.data||{},S=g.originalError?.status||m.status||500;let v=_C(g.originalError??f,e.handler,y.message,g.message);this.translationEngine&&this.translationContext&&v.startsWith("$t:")&&(v=this.translationEngine.resolveTranslations(v,this.translationContext));const _={status:S,message:v,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(S,{errorHandling:e.errorHandling,onError:e.onError});if(x.handler){if(a?.isEnabled()&&i&&e.handler==="apiCall"){const M=window.G7Core,T={};M?.state?.get&&(T._global=M.state.get()),n.state&&(T._local=n.state),a.detectStaleClosure?.(i,`${e.handler} → onError`,T,"callback-state-capture",i)}const O={...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,O),{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"&&n.setState&&d){const f=n.state?.loadingActions||{},{[d]:g,...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,g]of Object.entries(n.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: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 Bt("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=y=>{if(typeof y!="object"||y===null)return!1;const S=y;return"container"in S||"to"in S||"block"in S||"offset"in S},d=(y,S,v=0)=>{y===window?window.scrollTo({top:S,left:v,behavior:l}):y.scrollTo({top:S,left:v,behavior:l})},f=(y,S,v,_)=>{if(S===window){y.scrollIntoView({behavior:l,block:v}),_&&window.scrollBy({top:-_,left:0,behavior:l});return}const A=S,x=y.getBoundingClientRect(),O=A.getBoundingClientRect(),M=A.scrollTop+(x.top-O.top);let T;v==="center"?T=M-(A.clientHeight-y.clientHeight)/2:v==="end"?T=M-(A.clientHeight-y.clientHeight):T=M,T-=_,A.scrollTo({top:Math.max(0,T),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(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 y=i,S=y.block??"start",v=typeof y.offset=="number"?y.offset:0;let _=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}_=x}const A=y.to??"top";if(A==="top"){_===window?g():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:O=0}=A;d(_,O-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"){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:S=0}=i;window.scrollTo({top:S,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,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 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: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 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 Bt("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 Et.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 Et.resolveDeferred(a),!0}async handleApiCall(e,n,a,i="none",l){const{method:c="GET",body:d,headers:f,contentType:g}=n;c!=="GET"&&c!=="HEAD"&&await this.ensureCsrfToken();const m=this.getCsrfTokenFromCookie();let y={};if(i==="required"||i==="optional"){const W=Hr().getToken();W&&(y={Authorization:`Bearer ${W}`})}let S=e;const v=n.query||(c==="GET"?d:null);if(v&&typeof v=="object"){const q=this.buildQueryString(v);if(q){const W=e.includes("?")?"&":"?";S=`${e}${W}${q}`}}const _=window.G7Core?.state?.getGlobal?.()||a.state?._global||{},A=window.G7Core?.state?.getLocal?.()||a.state?._local||{},x={_global:_,_local:A},O=this.getMatchingGlobalHeaders(e,x),M={};if(typeof window<"u"){const q=localStorage.getItem("g7_locale");q&&(M["Accept-Language"]=q)}const T=g==="multipart/form-data",D={method:c,headers:{...T?{}:{"Content-Type":"application/json"},Accept:"application/json",...M,...m&&{"X-XSRF-TOKEN":m},...y,...O,...f},credentials:"include"};if(d&&c!=="GET")if(T){const q=new FormData;for(const[W,he]of Object.entries(d))he instanceof File||he instanceof Blob?q.append(W,he):he!=null&&q.append(W,typeof he=="object"?JSON.stringify(he):String(he));D.body=q}else D.body=JSON.stringify(d);const z=Fn();let P=null;z?.isEnabled()&&(P=z.trackRequest(S,c));try{let q=await fetch(S,D),W;try{W=await q.json()}catch{W=null}if(Et.isIdentityRequired(q.status,W)){const he=await Et.handle(W,{body:D.body,headers:D.headers,credentials:D.credentials},l);if(he){q=he;try{W=await q.json()}catch{W=null}}}if(P&&z?.isEnabled()&&(z.completeRequest(P,q.status,W),P=null),!q.ok){const he=W||{},Se=new Error(he.message||`API call failed: ${q.statusText}`);throw Se.response=he,Se.status=q.status,Se.statusText=q.statusText,new Bt(he.message||`API call failed: ${q.statusText}`,void 0,Se)}return W}catch(q){throw P&&z?.isEnabled()&&z.failRequest(P,q instanceof Error?q.message:String(q)),q}}async handleLogin(e,n,a){const{body:i}=n;if(!i||!i.email||!i.password)throw new Bt("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 S=Object.keys(m).length>0?{headers:m}:void 0;return{user:await y.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 Bt(v,void 0,_)}}async handleLogout(e,n){await br.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=Fn(),g=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:g?.state?.get()||{},y=a==="global"?"_global":a.startsWith("_local.")?a:"_local",S=y==="_global"?m._global:y.startsWith("_local.")?this.getNestedProperty(m._local||{},y.slice(7)):m._local,v=f?.isEnabled()?f.startStateChange(y,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=g?.state?.get()||{},{__setStateId:O,__mergeMode:M,...T}=A,D=_==="deep"?this.deepMergeWithState(T,x):T,z={...D,...v?{__setStateId:v}:{},..._!=="deep"?{__mergeMode:_}:{}};return j.log("setState global:",z),this.globalStateUpdater(z,{render:c}),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"global",..._!=="deep"?{__mergeMode:_}:{},...D}}else if(a==="isolated"){const x=n.isolatedContext;if(x){const{__mergeMode:O,__setStateId:M,...T}=A;return x.mergeState(T,_),j.log("[handleSetState] isolated state updated:",T,"mergeMode:",_),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"isolated",...T}}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||{},T=_==="deep"?this.deepMergeWithState(A,M):A;return n.setState(T),v&&f&&setTimeout(()=>f.completeStateChange(v),0),T}else throw v&&f&&f.completeStateChange(v),new Bt("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 O=window.__g7LayoutContextStack||[];if(O.length>0){const M=i==="parent"?O[O.length-1]:O[0];if(M?.setState){const{__mergeMode:T,__setStateId:D,...z}=A,P=_==="deep"?this.deepMergeWithState(z,M.state||{}):z;return j.log(`[handleSetState] scope=${i}: 타겟 컨텍스트에 상태 업데이트`,P),M.setState(P),v&&f&&setTimeout(()=>f.completeStateChange(v),0),P}}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||{},T=_==="deep"?this.deepMergeWithState(A,{}):A;j.log("[handleSetState] convertedPayload (변경 필드만, dot notation 변환):",T),n.setState(T);const D=_==="deep"?this.deepMergeWithState(A,M):_==="shallow"?{...n.state||{},...A}:A,{__mergeMode:z,__setStateId:P,...q}=D;window.__g7PendingLocalState=q,j.log("[handleSetState] __g7PendingLocalState updated:",q),this.globalStateUpdater&&(this.globalStateUpdater({_local:q},{render:!1}),j.log("[handleSetState] _global._local synced (render:false):",q));const{__mergeMode:W,__setStateId:he,...Se}=A;if(_==="replace")window.__g7ForcedLocalFields=Se;else{const be=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=this.deepMergeWithState(Se,be)}return j.log("[handleSetState] __g7ForcedLocalFields updated:",Se),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"local",...D}}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 T=_==="deep"?this.deepMergeWithState(A,M):A;return j.log("[handleSetState] finalLocal (merged):",T),this.globalStateUpdater({_local:T},{render:c}),window.__g7PendingLocalState=T,j.log("[handleSetState] __g7PendingLocalState updated for globalStateUpdater path"),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"local",...T}}else throw v&&f&&f.completeStateChange(v),new Bt("setState function is not provided in context and globalStateUpdater is not set")}else if(a.startsWith("_local.")){const x=a.slice(7),{__mergeMode:O,__setStateId:M,...T}=A,D=T.value!==void 0?T.value:Object.values(T)[0];if(n.setState){const P=window.__g7PendingLocalState||n.state||{},q=this.createNestedUpdate(x,D,P),W={...P,...q};if(n.setState(q),this.globalStateUpdater&&i!=="parent"&&i!=="root"){const he=window.__templateApp?.getGlobalState?.()?._local,Se=he&&typeof he=="object"?{...he,...q}: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 Bt("setState function is not provided in context")}else{if(!n.setState)throw v&&f&&f.completeStateChange(v),new Bt("setState function is not provided in context");const O=window.__g7PendingLocalState||n.state||{},M=_==="deep"?this.deepMergeWithState(A,O):A;return n.setState(M),v&&f&&setTimeout(()=>f.completeStateChange(v),0),M}}async handleParentScopeSetState(e,n,a,i){const l=Fn(),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,S;if(m==="_local"||m==="_global")y=m,S=void 0;else if(m.startsWith("_local."))y="_local",S=m.slice(7);else if(m.startsWith("_global."))y="_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=y==="_global"?_.state?._global:_.state?._local,x=l?.isEnabled()?l.startStateChange(e,A,n,{actionId:i.actionId,handlerType:"setState",source:`target=${e}`}):void 0,{__mergeMode:O,__setStateId:M,...T}=n;if(y==="_global"){if(!this.globalStateUpdater){j.warn("[handleParentScopeSetState] globalStateUpdater가 설정되지 않았습니다."),x&&l&&l.completeStateChange(x);return}const D=c?.state?.get()||{};if(S){const z=T.value!==void 0?T.value:Object.values(T)[0],P=this.createNestedUpdate(S,z,D),q={...D,...P};j.log(`[handleParentScopeSetState] ${e}: 전역 상태 중첩 경로 업데이트`,q),this.globalStateUpdater(q)}else{const z=d==="deep"?this.deepMergeWithState(T,D):T;j.log(`[handleParentScopeSetState] ${e}: 전역 상태 업데이트 (mergeMode=${d})`,z),this.globalStateUpdater(d!=="deep"?{...z,__mergeMode:d}:z)}return x&&l&&setTimeout(()=>l.completeStateChange(x),0),{__target:e,...T}}else{if(!_.setState){j.warn(`[handleParentScopeSetState] 타겟 컨텍스트에 setState가 없습니다. target=${e}`),x&&l&&l.completeStateChange(x);return}const D=_.state||{};if(S){const z=T.value!==void 0?T.value:Object.values(T)[0],P=this.createNestedUpdate(S,z,D),q={...D,...P};return j.log(`[handleParentScopeSetState] ${e}: 로컬 상태 중첩 경로 업데이트`,P),_.setState(P),_.state=q,_.dataContext&&(_.dataContext._local=q),Gd(),x&&l&&setTimeout(()=>l.completeStateChange(x),0),q}else{const z=d!=="deep"?{...T,__mergeMode:d}:T,P=d==="replace"?T:d==="shallow"?{...D,...T}:this.deepMergeWithState(T,D);return j.log(`[handleParentScopeSetState] ${e}: 로컬 상태 업데이트 (mergeMode=${d})`,P),_.setState(z),_.state=P,_.dataContext&&(_.dataContext._local=P),Gd(),x&&l&&setTimeout(()=>l.completeStateChange(x),0),P}}}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{this.globalStateUpdater({_local:v})};m.push({state:y,setState:S,dataContext:{_local:y||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:`,y,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 g=Fn();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: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=Fn();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")&&Et.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 g={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||[],g];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=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},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(g=>{j.error(`[startInterval:${a}] tick action failed`,g)})},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=Fn(),l=i?.isEnabled()&&i.startSequenceExecution?.({eventType:e.type})||"";window.__g7SequenceLocalSync=void 0;const c=[];let d,f=n.state?{...n.state}:{},g=n.isolatedContext?.state?{...n.isolatedContext.state}:{},m=n.data?._computed||{};const y=window.G7Core,S=()=>({_global:y?.state?.get()||{},_local:{...f},_isolated:Object.keys(g).length>0?{...g}:void 0});for(let v=0;v0){const T={},D={...n.data,_local:f,_computed:T,$computed:T,_isolated:g};for(const[z,P]of Object.entries(M))if(typeof P=="string"){const q=P.trim();if(q.startsWith("{{")&&q.endsWith("}}"))try{const W=q.slice(2,-2).trim();T[z]=Vn(W)?this.bindingEngine.evaluatePipeExpression(W,D,{skipCache:!0}):this.bindingEngine.evaluateExpression(W,D,{skipCache:!0})}catch{T[z]=m[z]}}m=T,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 T=n.data?._computedDefinitions;if(T&&Object.keys(T).length>0){const D={},z={...n.data,_local:f,_computed:D,$computed:D,_isolated:g};for(const[P,q]of Object.entries(T))if(typeof q=="string"){const W=q.trim();if(W.startsWith("{{")&&W.endsWith("}}"))try{const he=W.slice(2,-2).trim();D[P]=Vn(he)?this.bindingEngine.evaluatePipeExpression(he,z,{skipCache:!0}):this.bindingEngine.evaluateExpression(he,z,{skipCache:!0})}catch{D[P]=m[P]}}m=D,j.log("[handleSequence] _computed recalculated after custom handler:",m)}}}l&&i?.isEnabled()&&i.captureSequenceActionAfter?.(l,v,S(),Date.now()-A,O.data)}catch(O){throw l&&i?.isEnabled()&&(i.captureSequenceActionAfter?.(l,v,S(),Date.now()-A,void 0,O),i.endSequenceExecution?.(l,O)),j.error(`sequence handler: action[${v}] failed:`,O),O}}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,g)=>{const m={...n,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,n,a){const{src:i,id:l,async:c=!0,defer:d=!1}=e;if(!i)throw new Bt('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,y)=>{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),y(new Bt(`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,g=e.embedTarget,m=e.callbackSetState,y=e.callbackAction;if(!i)throw new Bt('callExternal handler requires "constructor" parameter',n);const S=this.getNestedProperty(window,i);if(!S||typeof S!="function")throw new Bt(`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]=O=>{if(j.log(`callExternal: callback triggered for ${A}`,O),f&&typeof window<"u"&&window.G7Core?.componentEvent&&window.G7Core.componentEvent.emit(f,O),m&&a.setState){const M=z=>{const P={};for(const[q,W]of Object.entries(z))typeof W=="string"?P[q]=this.getNestedProperty(O,W):typeof W=="object"&&W!==null&&(P[q]=M(W));return P},T=M(m);j.log("callExternal: callbackSetState mapping result",T);const D=this.deepMergeWithState(T,a.state||{});j.log("callExternal: merged with existing state",D),a.setState(D)}else f&&a.setState&&a.setState({[`${f.replace(/:/g,"_")}_result`]:O});if(y){const M={...a,data:{...a.data,$event:O}},T=Array.isArray(y)?y:[y];for(const D of T)try{this.executeAction(D,M)}catch(z){j.error("callExternal: callbackAction failed:",z)}}});const _=new S(v);if(c&&typeof _[c]=="function"){if(j.log(`callExternal: calling method ${c}`),c==="embed"&&g){const A=document.querySelector(g);if(A)return _[c](A,...d);throw new Bt(`Embed target element not found: ${g}`,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,g=e.callbackAction;if(!i)throw new Bt('callExternalEmbed handler requires "constructor" parameter',n);const m=this.getNestedProperty(window,i);if(!m||typeof m!="function")throw new Bt(`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:y,closeLayer:S}=this.createEmbedLayer(f),v={...l};for(const[A,x]of Object.entries(l))x===!0&&(v[A]=O=>{if(j.log(`callExternalEmbed: callback triggered for ${A}`,O),S(),d&&typeof window<"u"&&window.G7Core?.componentEvent&&window.G7Core.componentEvent.emit(d,O),c&&a.setState){const M=z=>{const P={};for(const[q,W]of Object.entries(z))typeof W=="string"?P[q]=this.getNestedProperty(O,W):typeof W=="object"&&W!==null&&(P[q]=M(W));return P},T=M(c);j.log("callExternalEmbed: callbackSetState mapping result",T);const D=this.deepMergeWithState(T,a.state||{});j.log("callExternalEmbed: merged with existing state",D),a.setState(D)}if(g){const M={...a,data:{...a.data,$event:O}},T=Array.isArray(g)?g:[g];for(const D of T)try{this.executeAction(D,M)}catch(z){j.error("callExternalEmbed: callbackAction failed:",z)}}});const _=new m(v);if(typeof _.embed=="function")_.embed(y),j.log("callExternalEmbed: embedded in layer");else throw S(),new Bt(`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=` +`+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&&Zl.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}:Zl.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),Zl.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{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=Yl,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",ec=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){ec.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 ec.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){ec.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){ec.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 tc={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",tc.admin),this.config.set("user",tc.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"?tc.admin.loginEndpoint:tc.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 nc=null;function ef(){nc?(Zd.log("[triggerModalParentUpdate] 모달 부모 컨텍스트 업데이트 트리거"),nc()):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,nc=()=>{a.current&&(n(c=>c+1),Zd.log("[ParentContextProvider] 버전 업데이트됨"))},()=>{a.current=!1,nc=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)||dd(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(;!_&&AsetTimeout(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.topL.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{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;v0){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); @@ -47,11 +50,11 @@ Error generating stack: `+u.message+` justify-content: center; color: #666; transition: background-color 0.2s; - `,i.onmouseenter=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.2)"},i.onmouseleave=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.1)"};const l=()=>{n.parentNode&&n.parentNode.removeChild(n)};i.onclick=l;const c=d=>{this.isImeComposing(d)||d.key==="Escape"&&(l(),document.removeEventListener("keydown",c))};return document.addEventListener("keydown",c),n.onclick=d=>{d.target===n&&l()},a.appendChild(i),n.appendChild(a),document.body.appendChild(n),{overlay:n,layer:a,closeLayer:l}}getNestedProperty(e,n){return n.split(".").reduce((a,i)=>a&&a[i]!==void 0?a[i]:void 0,e)}async handleSaveToLocalStorage(e,n){const{key:a,value:i}=e;if(!a)return 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 Bt(`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=Aa(e);if(a===null){let c=n;if(e.includes("_global")){const f=window.G7Core?.state?.get()?._global;f&&(c={...n,_global:f})}if(e.includes("_computed")||e.includes("$computed")){const f=window.__g7ActionContext?.computedRef?.current;f&&Object.keys(f).length>0&&(c={...c,_computed:f,$computed:f})}return this.bindingEngine.resolveBindings(e,c,{skipCache:!0})}let i=a;i=i.replace(/\$args\.(\d+)/g,"$args[$1]");let l=n;if(i.includes("_global")){const d=window.G7Core?.state?.get()?._global;d&&(l={...n,_global:d})}if(i.includes("_computed")||i.includes("$computed")){const d=window.__g7ActionContext?.computedRef?.current;d&&Object.keys(d).length>0&&(l={...l,_computed:d,$computed:d})}i.includes("_global.modules")&&(j.log("[evaluateExpression] expression:",i),j.log("[evaluateExpression] dataContext._global:",l._global),j.log("[evaluateExpression] dataContext._global?.modules:",l._global?.modules));try{const c=Vn(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 Bt&&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=Fn();i?.isEnabled()&&i.trackHandlerRegistration(e,a?.category??"custom",a?.description,a?.source)}unregisterHandler(e){this.customHandlers.delete(e);const n=Fn();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()}`,g=this.extractEventData(n),m=g._changedKeys;if(m&&Array.isArray(m)&&g?.target?.value&&typeof g.target.value=="object"&&!Array.isArray(g.target.value)){const _=this.debounceAccumulatedValues.get(f);if(_){const A={..._};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 S=Fn();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,g,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,g,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 y=this.resolveActionRef(m),S=y.event?this.normalizeEventPropName(y.event):this.getEventHandlerName(y.type);l.has(S)||l.set(S,[]),l.get(S).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]=(...S)=>{for(const v of y){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}`,O=_&&typeof _=="object"&&!("preventDefault"in _)&&"target"in _&&_.target!==null;let M;if(A)M=_;else if(O){const D={type:"custom",target:_.target,preventDefault:()=>{},stopPropagation:()=>{}};_._changedKeys&&(D._changedKeys=_._changedKeys),M=D}else M=new Event("custom");const T=v.event?{...n,$args:S}:n;this.handleDebouncedAction(v,M,T,a,x,S)}else if(this.flushPendingDebounceTimers(),v.event){const x={...n,$args:S},O=this.resolveEventForHandler(_,A);this.createHandler(v,x,a)(O)}else if(A)this.createHandler(v,n,a)(_);else if(_&&typeof _=="object"&&!("preventDefault"in _)&&"target"in _&&_.target!==null){const O={type:"custom",target:_.target,preventDefault:()=>{},stopPropagation:()=>{}};this.createHandler(v,n,a)(O)}}}}return i}getEventHandlerName(e){return bm[e]||`on${e.charAt(0).toUpperCase()}${e.slice(1)}`}normalizeEventPropName(e){return bm[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),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)}};$(bs,"loadedScripts",new Set);let bo=bs,nc=null;function vm(o){return nc||(nc=new bo(o)),nc}function xC(o){nc=o}const Sm=ht("TransitionManager");class TC{constructor(){$(this,"isPending",!1);$(this,"subscribers",new Set)}setPending(e){this.isPending!==e&&(Sm.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(){Sm.log("notifySubscribers:",{isPending:this.isPending,subscriberCount:this.subscribers.size}),this.subscribers.forEach(e=>{e(this.isPending)})}clearSubscribers(){this.subscribers.clear()}}const jr=new TC,rc=ht("TransitionContext"),wm=H.createContext({isTransitioning:!1}),Vd=({children:o})=>{const[e,n]=H.useState(()=>{const i=jr.getIsPending();return rc.log("Initial state:",i),i});H.useEffect(()=>{rc.log("Subscribing to TransitionManager");const i=jr.subscribe(l=>{rc.log("Received isPending update:",l),n(l)});return()=>{rc.log("Unsubscribing from TransitionManager"),i()}},[]);const a=H.useMemo(()=>({isTransitioning:e}),[e]);return gt.jsx(wm.Provider,{value:a,children:o})},Cm=()=>H.useContext(wm),ac="__g7LocalInitTracking";function Fd(){const o=globalThis;return o[ac]||(o[ac]={hash:"",timestamp:void 0,consumed:void 0}),o[ac]}function RC(){const o=globalThis;o[ac]={hash:"",timestamp:void 0,consumed:void 0}}function kC(o){Fd().consumed=o}function DC(o){return Fd().consumed===o}function OC(o){return o.globalTrackedKey!==o.trackingKey?"apply":o.instanceHandledKey===o.trackingKey?"skip":"prune"}function LC(o,e){return typeof o=="number"&&typeof e=="number"?Math.max(o,e):e??o}function MC(o,e){if(e===void 0)return o;if(!(o!==null&&typeof o=="object"&&e!==null&&typeof e=="object"&&!DC(o)))return e;const{_forceLocalInit:a,...i}=o,{_forceLocalInit:l,...c}=e,d={...i,...c},f=LC(a,l);return f!==void 0&&(d._forceLocalInit=f),d}function $C(o){return o<768?"mobile":o<1024?"tablet":"desktop"}function Em(o){const e=$C(o);return{width:o,isMobile:e==="mobile",isTablet:e==="tablet",isDesktop:e==="desktop",matchedPreset:e}}const Kd=H.createContext(Em(1024)),ic=({children:o,overrideWidth:e})=>{const[n,a]=H.useState(()=>hi.getWidth());H.useEffect(()=>{const c=hi.subscribe(d=>{a(d)});return()=>{c()}},[]);const i=e??n,l=H.useMemo(()=>Em(i),[i]);return gt.jsx(Kd.Provider,{value:l,children:o})},Wd=()=>H.useContext(Kd);window.G7Core?.createLogger?.("Core:State");function NC(o,e,n,a){const{syncOnControlledChange:i=!0,isEqual:l}=a??{},c=o!==void 0;H.useRef(c);const[d,f]=H.useState(o!==void 0?o:e),g=d,m=H.useRef(o),y=H.useRef(!1);H.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 S=H.useCallback(v=>{const _=typeof v=="function"?v(g):v;(l?l(g,_):g===_)||(y.current=!0,f(_),n?.(_),requestAnimationFrame(()=>{y.current=!1}))},[g,c,n,l]);return[g,S]}function IC(o,e){return o===e?!0:o.length!==e.length?!1:o.every((n,a)=>n===e[a])}function _m(o,e){if(o===e)return!0;const n=Object.keys(o),a=Object.keys(e);return n.length!==a.length?!1:n.every(i=>o[i]===e[i])}const Am=H.createContext({}),HC=({children:o,value:e})=>gt.jsx(Am.Provider,{value:e,children:o}),jC=()=>H.useContext(Am),zC=(o,e)=>{if(!o)return;const n=e.split(".");let a=o;for(const i of n){if(a==null)return;a=a[i]}return a},UC=(o,e,n)=>{const a=e.split(".");if(a.length===1)return{...o,[a[0]]:n};const i={...o};let l=i;for(let c=0;c{us.warn("IsolatedStateContext not available: setState called outside IsolatedStateProvider")},getState:()=>{},mergeState:()=>{us.warn("IsolatedStateContext not available: mergeState called outside IsolatedStateProvider")},stateRef:{current:{}},scopeId:""},xm=H.createContext(PC);let BC=0;function qC(){return`isolated-${++BC}-${Date.now().toString(36)}`}function Tm(o,e){if(e==null)return o;if(typeof e!="object"||Array.isArray(e))return e;const n={...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)?n[a]=Tm(l,i):n[a]=i}return n}function GC(o,e){return e?e.split(".").reduce((n,a)=>n?.[a],o):o}function VC(o,e,n){const a=e.split("."),i={...o};let l=i;for(let c=0;c{const i=H.useRef(e||qC()).current,[l,c]=H.useState(o),d=H.useRef(l);d.current=l;const f=H.useCallback(v=>GC(d.current,v),[]),g=H.useCallback((v,_)=>{c(A=>{const x=VC(A,v,_);return us.log(`[${i}] setState: ${v} =`,_),x})},[i]),m=H.useCallback((v,_="deep")=>{c(A=>{let x;return _==="replace"?x=v:_==="shallow"?x={...A,...v}:x=Tm(A,v),us.log(`[${i}] mergeState (${_}):`,v),x})},[i]),y=H.useMemo(()=>({state:l,setState:g,getState:f,mergeState:m,stateRef:d,scopeId:i}),[l,g,f,m,i]),S=H.useMemo(()=>({state:d.current,setState:g,getState:f,mergeState:m}),[g,f,m]);return H.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]),H.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]),gt.jsx(xm.Provider,{value:y,children:n})},KC=()=>{const o=H.useContext(xm);return o.scopeId?o:null};var ka=Qg();function WC(){for(var o=arguments.length,e=new Array(o),n=0;na=>{e.forEach(i=>i(a))},e)}const sc=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function ds(o){const e=Object.prototype.toString.call(o);return e==="[object Window]"||e==="[object global]"}function Yd(o){return"nodeType"in o}function Nn(o){var e,n;return o?ds(o)?o:Yd(o)&&(e=(n=o.ownerDocument)==null?void 0:n.defaultView)!=null?e:window:window}function Xd(o){const{Document:e}=Nn(o);return o instanceof e}function vo(o){return ds(o)?!1:o instanceof Nn(o).HTMLElement}function Rm(o){return o instanceof Nn(o).SVGElement}function fs(o){return o?ds(o)?o.document:Yd(o)?Xd(o)?o:vo(o)||Rm(o)?o.ownerDocument:document:document:document}const zr=sc?H.useLayoutEffect:H.useEffect;function Jd(o){const e=H.useRef(o);return zr(()=>{e.current=o}),H.useCallback(function(){for(var n=arguments.length,a=new Array(n),i=0;i{o.current=setInterval(a,i)},[]),n=H.useCallback(()=>{o.current!==null&&(clearInterval(o.current),o.current=null)},[]);return[e,n]}function So(o,e){e===void 0&&(e=[o]);const n=H.useRef(o);return zr(()=>{n.current!==o&&(n.current=o)},e),n}function wo(o,e){const n=H.useRef();return H.useMemo(()=>{const a=o(n.current);return n.current=a,a},[...e])}function oc(o){const e=Jd(o),n=H.useRef(null),a=H.useCallback(i=>{i!==n.current&&e?.(i,n.current),n.current=i},[]);return[n,a]}function Qd(o){const e=H.useRef();return H.useEffect(()=>{e.current=o},[o]),e.current}let Zd={};function Co(o,e){return H.useMemo(()=>{if(e)return e;const n=Zd[o]==null?0:Zd[o]+1;return Zd[o]=n,o+"-"+n},[o,e])}function km(o){return function(e){for(var n=arguments.length,a=new Array(n>1?n-1:0),i=1;i{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 hs=km(1),Eo=km(-1);function XC(o){return"clientX"in o&&"clientY"in o}function ef(o){if(!o)return!1;const{KeyboardEvent:e}=Nn(o.target);return e&&o instanceof e}function JC(o){if(!o)return!1;const{TouchEvent:e}=Nn(o.target);return e&&o instanceof e}function tf(o){if(JC(o)){if(o.touches&&o.touches.length){const{clientX:e,clientY:n}=o.touches[0];return{x:e,y:n}}else if(o.changedTouches&&o.changedTouches.length){const{clientX:e,clientY:n}=o.changedTouches[0];return{x:e,y:n}}}return XC(o)?{x:o.clientX,y:o.clientY}:null}const _o=Object.freeze({Translate:{toString(o){if(!o)return;const{x:e,y:n}=o;return"translate3d("+(e?Math.round(e):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(o){if(!o)return;const{scaleX:e,scaleY:n}=o;return"scaleX("+e+") scaleY("+n+")"}},Transform:{toString(o){if(o)return[_o.Translate.toString(o),_o.Scale.toString(o)].join(" ")}},Transition:{toString(o){let{property:e,duration:n,easing:a}=o;return e+" "+n+"ms "+a}}}),Dm="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function QC(o){return o.matches(Dm)?o:o.querySelector(Dm)}const ZC={display:"none"};function eE(o){let{id:e,value:n}=o;return Xe.createElement("div",{id:e,style:ZC},n)}function tE(o){let{id:e,announcement:n,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 Xe.createElement("div",{id:e,style:i,role:"status","aria-live":a,"aria-atomic":!0},n)}function nE(){const[o,e]=H.useState("");return{announce:H.useCallback(a=>{a!=null&&e(a)},[]),announcement:o}}const Om=H.createContext(null);function rE(o){const e=H.useContext(Om);H.useEffect(()=>{if(!e)throw new Error("useDndMonitor must be used within a children of ");return e(o)},[o,e])}function aE(){const[o]=H.useState(()=>new Set),e=H.useCallback(a=>(o.add(a),()=>o.delete(a)),[o]);return[H.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 iE={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,rc=null;function Mm(s){return rc||(rc=new vo(s)),rc}function PC(s){rc=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,ac=dt("TransitionContext"),Nm=N.createContext({isTransitioning:!1}),tf=({children:s})=>{const[e,n]=N.useState(()=>{const i=zr.getIsPending();return ac.log("Initial state:",i),i});N.useEffect(()=>{ac.log("Subscribing to TransitionManager");const i=zr.subscribe(l=>{ac.log("Received isPending update:",l),n(l)});return()=>{ac.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),ic="__g7LocalInitTracking";function nf(){const s=globalThis;return s[ic]||(s[ic]={hash:"",timestamp:void 0,consumed:void 0}),s[ic]}function qC(){const s=globalThis;s[ic]={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)),sc=({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{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{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;na=>{e.forEach(i=>i(a))},e)}const oc=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=oc?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{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 lc(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{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 ");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. - `},sE={onDragStart(o){let{active:e}=o;return"Picked up draggable item "+e.id+"."},onDragOver(o){let{active:e,over:n}=o;return n?"Draggable item "+e.id+" was moved over droppable area "+n.id+".":"Draggable item "+e.id+" is no longer over a droppable area."},onDragEnd(o){let{active:e,over:n}=o;return n?"Draggable item "+e.id+" was dropped over droppable area "+n.id:"Draggable item "+e.id+" was dropped."},onDragCancel(o){let{active:e}=o;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function oE(o){let{announcements:e=sE,container:n,hiddenTextDescribedById:a,screenReaderInstructions:i=iE}=o;const{announce:l,announcement:c}=nE(),d=Co("DndLiveRegion"),[f,g]=H.useState(!1);if(H.useEffect(()=>{g(!0)},[]),rE(H.useMemo(()=>({onDragStart(y){let{active:S}=y;l(e.onDragStart({active:S}))},onDragMove(y){let{active:S,over:v}=y;e.onDragMove&&l(e.onDragMove({active:S,over:v}))},onDragOver(y){let{active:S,over:v}=y;l(e.onDragOver({active:S,over:v}))},onDragEnd(y){let{active:S,over:v}=y;l(e.onDragEnd({active:S,over:v}))},onDragCancel(y){let{active:S,over:v}=y;l(e.onDragCancel({active:S,over:v}))}}),[l,e])),!f)return null;const m=Xe.createElement(Xe.Fragment,null,Xe.createElement(eE,{id:a,value:i.draggable}),Xe.createElement(tE,{id:d,announcement:c}));return n?ka.createPortal(m,n):m}var an;(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"})(an||(an={}));function lc(){}function Lm(o,e){return H.useMemo(()=>({sensor:o,options:e??{}}),[o,e])}function lE(){for(var o=arguments.length,e=new Array(o),n=0;n[...e].filter(a=>a!=null),[...e])}const wr=Object.freeze({x:0,y:0});function Mm(o,e){return Math.sqrt(Math.pow(o.x-e.x,2)+Math.pow(o.y-e.y,2))}function $m(o,e){let{data:{value:n}}=o,{data:{value:a}}=e;return n-a}function cE(o,e){let{data:{value:n}}=o,{data:{value:a}}=e;return a-n}function Nm(o){let{left:e,top:n,height:a,width:i}=o;return[{x:e,y:n},{x:e+i,y:n},{x:e,y:n+a},{x:e+i,y:n+a}]}function Im(o,e){if(!o||o.length===0)return null;const[n]=o;return n[e]}function Hm(o,e,n){return e===void 0&&(e=o.left),n===void 0&&(n=o.top),{x:e+o.width*.5,y:n+o.height*.5}}const uE=o=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=o;const i=Hm(e,e.left,e.top),l=[];for(const c of a){const{id:d}=c,f=n.get(d);if(f){const g=Mm(Hm(f),i);l.push({id:d,data:{droppableContainer:c,value:g}})}}return l.sort($m)},dE=o=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=o;const i=Nm(e),l=[];for(const c of a){const{id:d}=c,f=n.get(d);if(f){const g=Nm(f),m=i.reduce((S,v,_)=>S+Mm(g[_],v),0),y=Number((m/4).toFixed(4));l.push({id:d,data:{droppableContainer:c,value:y}})}}return l.sort($m)};function fE(o,e){const n=Math.max(e.top,o.top),a=Math.max(e.left,o.left),i=Math.min(e.left+e.width,o.left+o.width),l=Math.min(e.top+e.height,o.top+o.height),c=i-a,d=l-n;if(a{let{collisionRect:e,droppableRects:n,droppableContainers:a}=o;const i=[];for(const l of a){const{id:c}=l,d=n.get(c);if(d){const f=fE(d,e);f>0&&i.push({id:c,data:{droppableContainer:l,value:f}})}}return i.sort(cE)};function gE(o,e,n){return{...o,scaleX:e&&n?e.width/n.width:1,scaleY:e&&n?e.height/n.height:1}}function jm(o,e){return o&&e?{x:o.left-e.left,y:o.top-e.top}:wr}function pE(o){return function(n){for(var a=arguments.length,i=new Array(a>1?a-1:0),l=1;l({...c,top:c.top+o*d.y,bottom:c.bottom+o*d.y,left:c.left+o*d.x,right:c.right+o*d.x}),{...n})}}const mE=pE(1);function yE(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 bE(o,e,n){const a=yE(e);if(!a)return o;const{scaleX:i,scaleY:l,x:c,y:d}=a,f=o.left-c-(1-i)*parseFloat(n),g=o.top-d-(1-l)*parseFloat(n.slice(n.indexOf(" ")+1)),m=i?o.width/i:o.width,y=l?o.height/l:o.height;return{width:m,height:y,top:g,right:f+m,bottom:g+y,left:f}}const vE={ignoreTransform:!1};function gs(o,e){e===void 0&&(e=vE);let n=o.getBoundingClientRect();if(e.ignoreTransform){const{transform:g,transformOrigin:m}=Nn(o).getComputedStyle(o);g&&(n=bE(n,g,m))}const{top:a,left:i,width:l,height:c,bottom:d,right:f}=n;return{top:a,left:i,width:l,height:c,bottom:d,right:f}}function zm(o){return gs(o,{ignoreTransform:!0})}function SE(o){const e=o.innerWidth,n=o.innerHeight;return{top:0,left:0,right:e,bottom:n,width:e,height:n}}function wE(o,e){return e===void 0&&(e=Nn(o).getComputedStyle(o)),e.position==="fixed"}function CE(o,e){e===void 0&&(e=Nn(o).getComputedStyle(o));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(i=>{const l=e[i];return typeof l=="string"?n.test(l):!1})}function cc(o,e){const n=[];function a(i){if(e!=null&&n.length>=e||!i)return n;if(Xd(i)&&i.scrollingElement!=null&&!n.includes(i.scrollingElement))return n.push(i.scrollingElement),n;if(!vo(i)||Rm(i)||n.includes(i))return n;const l=Nn(o).getComputedStyle(i);return i!==o&&CE(i,l)&&n.push(i),wE(i,l)?n:a(i.parentNode)}return o?a(o):n}function Um(o){const[e]=cc(o,1);return e??null}function nf(o){return!sc||!o?null:ds(o)?o:Yd(o)?Xd(o)||o===fs(o).scrollingElement?window:vo(o)?o:null:null}function Pm(o){return ds(o)?o.scrollX:o.scrollLeft}function Bm(o){return ds(o)?o.scrollY:o.scrollTop}function rf(o){return{x:Pm(o),y:Bm(o)}}var gn;(function(o){o[o.Forward=1]="Forward",o[o.Backward=-1]="Backward"})(gn||(gn={}));function qm(o){return!sc||!o?!1:o===document.scrollingElement}function Gm(o){const e={x:0,y:0},n=qm(o)?{height:window.innerHeight,width:window.innerWidth}:{height:o.clientHeight,width:o.clientWidth},a={x:o.scrollWidth-n.width,y:o.scrollHeight-n.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 EE={x:.2,y:.2};function _E(o,e,n,a,i){let{top:l,left:c,right:d,bottom:f}=n;a===void 0&&(a=10),i===void 0&&(i=EE);const{isTop:g,isBottom:m,isLeft:y,isRight:S}=Gm(o),v={x:0,y:0},_={x:0,y:0},A={height:e.height*i.y,width:e.width*i.x};return!g&&l<=e.top+A.height?(v.y=gn.Backward,_.y=a*Math.abs((e.top+A.height-l)/A.height)):!m&&f>=e.bottom-A.height&&(v.y=gn.Forward,_.y=a*Math.abs((e.bottom-A.height-f)/A.height)),!S&&d>=e.right-A.width?(v.x=gn.Forward,_.x=a*Math.abs((e.right-A.width-d)/A.width)):!y&&c<=e.left+A.width&&(v.x=gn.Backward,_.x=a*Math.abs((e.left+A.width-c)/A.width)),{direction:v,speed:_}}function AE(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:n,right:a,bottom:i}=o.getBoundingClientRect();return{top:e,left:n,right:a,bottom:i,width:o.clientWidth,height:o.clientHeight}}function Vm(o){return o.reduce((e,n)=>hs(e,rf(n)),wr)}function xE(o){return o.reduce((e,n)=>e+Pm(n),0)}function TE(o){return o.reduce((e,n)=>e+Bm(n),0)}function RE(o,e){if(e===void 0&&(e=gs),!o)return;const{top:n,left:a,bottom:i,right:l}=e(o);Um(o)&&(i<=0||l<=0||n>=window.innerHeight||a>=window.innerWidth)&&o.scrollIntoView({block:"center",inline:"center"})}const kE=[["x",["left","right"],xE],["y",["top","bottom"],TE]];class af{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=cc(n),i=Vm(a);this.rect={...e},this.width=e.width,this.height=e.height;for(const[l,c,d]of kE)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 Ao{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 DE(o){const{EventTarget:e}=Nn(o);return o instanceof e?o:fs(o)}function sf(o,e){const n=Math.abs(o.x),a=Math.abs(o.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(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 Fm(o){o.preventDefault()}function OE(o){o.stopPropagation()}var ut;(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"})(ut||(ut={}));const Km={start:[ut.Space,ut.Enter],cancel:[ut.Esc],end:[ut.Space,ut.Enter,ut.Tab]},LE=(o,e)=>{let{currentCoordinates:n}=e;switch(o.code){case ut.Right:return{...n,x:n.x+25};case ut.Left:return{...n,x:n.x-25};case ut.Down:return{...n,y:n.y+25};case ut.Up:return{...n,y:n.y-25}}};class of{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 Ao(fs(n)),this.windowListeners=new Ao(Nn(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&&RE(a),n(wr)}handleKeyDown(e){if(ef(e)){const{active:n,context:a,options:i}=this.props,{keyboardCodes:l=Km,coordinateGetter:c=LE,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:n,context:a.current,currentCoordinates:m});if(y){const S=Eo(y,m),v={x:0,y:0},{scrollableAncestors:_}=a.current;for(const A of _){const x=e.code,{isTop:O,isRight:M,isLeft:T,isBottom:D,maxScroll:z,minScroll:P}=Gm(A),q=AE(A),W={x:Math.min(x===ut.Right?q.right-q.width/2:q.right,Math.max(x===ut.Right?q.left:q.left+q.width/2,y.x)),y:Math.min(x===ut.Down?q.bottom-q.height/2:q.bottom,Math.max(x===ut.Down?q.top:q.top+q.height/2,y.y))},he=x===ut.Right&&!M||x===ut.Left&&!T,Se=x===ut.Down&&!D||x===ut.Up&&!O;if(he&&W.x!==y.x){const be=A.scrollLeft+S.x,Ue=x===ut.Right&&be<=z.x||x===ut.Left&&be>=P.x;if(Ue&&!S.y){A.scrollTo({left:be,behavior:d});return}Ue?v.x=A.scrollLeft-be:v.x=x===ut.Right?A.scrollLeft-z.x:A.scrollLeft-P.x,v.x&&A.scrollBy({left:-v.x,behavior:d});break}else if(Se&&W.y!==y.y){const be=A.scrollTop+S.y,Ue=x===ut.Down&&be<=z.y||x===ut.Up&&be>=P.y;if(Ue&&!S.x){A.scrollTo({top:be,behavior:d});return}Ue?v.y=A.scrollTop-be:v.y=x===ut.Down?A.scrollTop-z.y:A.scrollTop-P.y,v.y&&A.scrollBy({top:-v.y,behavior:d});break}}this.handleMove(e,hs(Eo(y,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()}}of.activators=[{eventName:"onKeyDown",handler:(o,e,n)=>{let{keyboardCodes:a=Km,onActivation:i}=e,{active:l}=n;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 Wm(o){return!!(o&&"distance"in o)}function Ym(o){return!!(o&&"delay"in o)}class lf{constructor(e,n,a){var i;a===void 0&&(a=DE(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 Ao(this.document),this.listeners=new Ao(a),this.windowListeners=new Ao(Nn(c)),this.initialCoordinates=(i=tf(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: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,Fm),this.windowListeners.add(ar.VisibilityChange,this.handleCancel),this.windowListeners.add(ar.ContextMenu,Fm),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(Ym(n)){this.timeoutId=setTimeout(this.handleStart,n.delay),this.handlePending(n);return}if(Wm(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,OE,{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=tf(e))!=null?n:wr,g=Eo(i,f);if(!a&&d){if(Wm(d)){if(d.tolerance!=null&&sf(g,d.tolerance))return this.handleCancel();if(sf(g,d.distance))return this.handleStart()}if(Ym(d)&&sf(g,d.tolerance))return this.handleCancel();this.handlePending(d,g);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===ut.Esc&&this.handleCancel()}removeTextSelection(){var e;(e=this.document.getSelection())==null||e.removeAllRanges()}}const ME={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class cf extends lf{constructor(e){const{event:n}=e,a=fs(n.target);super(e,ME,a)}}cf.activators=[{eventName:"onPointerDown",handler:(o,e)=>{let{nativeEvent:n}=o,{onActivation:a}=e;return!n.isPrimary||n.button!==0?!1:(a?.({event:n}),!0)}}];const $E={move:{name:"mousemove"},end:{name:"mouseup"}};var uf;(function(o){o[o.RightClick=2]="RightClick"})(uf||(uf={}));class NE extends lf{constructor(e){super(e,$E,fs(e.event.target))}}NE.activators=[{eventName:"onMouseDown",handler:(o,e)=>{let{nativeEvent:n}=o,{onActivation:a}=e;return n.button===uf.RightClick?!1:(a?.({event:n}),!0)}}];const df={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class IE extends lf{constructor(e){super(e,df)}static setup(){return window.addEventListener(df.move.name,e,{capture:!1,passive:!1}),function(){window.removeEventListener(df.move.name,e)};function e(){}}}IE.activators=[{eventName:"onTouchStart",handler:(o,e)=>{let{nativeEvent:n}=o,{onActivation:a}=e;const{touches:i}=n;return i.length>1?!1:(a?.({event:n}),!0)}}];var xo;(function(o){o[o.Pointer=0]="Pointer",o[o.DraggableRect=1]="DraggableRect"})(xo||(xo={}));var uc;(function(o){o[o.TreeOrder=0]="TreeOrder",o[o.ReversedTreeOrder=1]="ReversedTreeOrder"})(uc||(uc={}));function HE(o){let{acceleration:e,activator:n=xo.Pointer,canScroll:a,draggingRect:i,enabled:l,interval:c=5,order:d=uc.TreeOrder,pointerCoordinates:f,scrollableAncestors:g,scrollableAncestorRects:m,delta:y,threshold:S}=o;const v=zE({delta:y,disabled:!l}),[_,A]=YC(),x=H.useRef({x:0,y:0}),O=H.useRef({x:0,y:0}),M=H.useMemo(()=>{switch(n){case xo.Pointer:return f?{top:f.y,bottom:f.y,left:f.x,right:f.x}:null;case xo.DraggableRect:return i}},[n,i,f]),T=H.useRef(null),D=H.useCallback(()=>{const P=T.current;if(!P)return;const q=x.current.x*O.current.x,W=x.current.y*O.current.y;P.scrollBy(q,W)},[]),z=H.useMemo(()=>d===uc.TreeOrder?[...g].reverse():g,[d,g]);H.useEffect(()=>{if(!l||!g.length||!M){A();return}for(const P of z){if(a?.(P)===!1)continue;const q=g.indexOf(P),W=m[q];if(!W)continue;const{direction:he,speed:Se}=_E(P,W,M,e,S);for(const be of["x","y"])v[be][he[be]]||(Se[be]=0,he[be]=0);if(Se.x>0||Se.y>0){A(),T.current=P,_(D,c),x.current=Se,O.current=he;return}}x.current={x:0,y:0},O.current={x:0,y:0},A()},[e,D,a,A,l,c,JSON.stringify(M),JSON.stringify(v),_,g,z,m,JSON.stringify(S)])}const jE={x:{[gn.Backward]:!1,[gn.Forward]:!1},y:{[gn.Backward]:!1,[gn.Forward]:!1}};function zE(o){let{delta:e,disabled:n}=o;const a=Qd(e);return wo(i=>{if(n||!a||!i)return jE;const l={x:Math.sign(e.x-a.x),y:Math.sign(e.y-a.y)};return{x:{[gn.Backward]:i.x[gn.Backward]||l.x===-1,[gn.Forward]:i.x[gn.Forward]||l.x===1},y:{[gn.Backward]:i.y[gn.Backward]||l.y===-1,[gn.Forward]:i.y[gn.Forward]||l.y===1}}},[n,e,a])}function UE(o,e){const n=e!=null?o.get(e):void 0,a=n?n.node.current:null;return wo(i=>{var l;return e==null?null:(l=a??i)!=null?l:null},[a,e])}function PE(o,e){return H.useMemo(()=>o.reduce((n,a)=>{const{sensor:i}=a,l=i.activators.map(c=>({eventName:c.eventName,handler:e(c.handler,a)}));return[...n,...l]},[]),[o,e])}var To;(function(o){o[o.Always=0]="Always",o[o.BeforeDragging=1]="BeforeDragging",o[o.WhileDragging=2]="WhileDragging"})(To||(To={}));var ff;(function(o){o.Optimized="optimized"})(ff||(ff={}));const Xm=new Map;function BE(o,e){let{dragging:n,dependencies:a,config:i}=e;const[l,c]=H.useState(null),{frequency:d,measure:f,strategy:g}=i,m=H.useRef(o),y=x(),S=So(y),v=H.useCallback(function(O){O===void 0&&(O=[]),!S.current&&c(M=>M===null?O:M.concat(O.filter(T=>!M.includes(T))))},[S]),_=H.useRef(null),A=wo(O=>{if(y&&!n)return Xm;if(!O||O===Xm||m.current!==o||l!=null){const M=new Map;for(let T of o){if(!T)continue;if(l&&l.length>0&&!l.includes(T.id)&&T.rect.current){M.set(T.id,T.rect.current);continue}const D=T.node.current,z=D?new af(f(D),D):null;T.rect.current=z,z&&M.set(T.id,z)}return M}return O},[o,l,n,y,f]);return H.useEffect(()=>{m.current=o},[o]),H.useEffect(()=>{y||v()},[n,y]),H.useEffect(()=>{l&&l.length>0&&c(null)},[JSON.stringify(l)]),H.useEffect(()=>{y||typeof d!="number"||_.current!==null||(_.current=setTimeout(()=>{v(),_.current=null},d))},[d,y,v,...a]),{droppableRects:A,measureDroppableContainers:v,measuringScheduled:l!=null};function x(){switch(g){case To.Always:return!1;case To.BeforeDragging:return n;default:return!n}}}function Jm(o,e){return wo(n=>o?n||(typeof e=="function"?e(o):o):null,[e,o])}function qE(o,e){return Jm(o,e)}function GE(o){let{callback:e,disabled:n}=o;const a=Jd(e),i=H.useMemo(()=>{if(n||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:l}=window;return new l(a)},[a,n]);return H.useEffect(()=>()=>i?.disconnect(),[i]),i}function dc(o){let{callback:e,disabled:n}=o;const a=Jd(e),i=H.useMemo(()=>{if(n||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:l}=window;return new l(a)},[n]);return H.useEffect(()=>()=>i?.disconnect(),[i]),i}function VE(o){return new af(gs(o),o)}function Qm(o,e,n){e===void 0&&(e=VE);const[a,i]=H.useState(null);function l(){i(f=>{if(!o)return null;if(o.isConnected===!1){var g;return(g=f??n)!=null?g:null}const m=e(o);return JSON.stringify(f)===JSON.stringify(m)?f:m})}const c=GE({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=dc({callback:l});return zr(()=>{l(),o?(d?.observe(o),c?.observe(document.body,{childList:!0,subtree:!0})):(d?.disconnect(),c?.disconnect())},[o]),a}function FE(o){const e=Jm(o);return jm(o,e)}const Zm=[];function KE(o){const e=H.useRef(o),n=wo(a=>o?a&&a!==Zm&&o&&e.current&&o.parentNode===e.current.parentNode?a:cc(o):Zm,[o]);return H.useEffect(()=>{e.current=o},[o]),n}function WE(o){const[e,n]=H.useState(null),a=H.useRef(o),i=H.useCallback(l=>{const c=nf(l.target);c&&n(d=>d?(d.set(c,rf(c)),new Map(d)):null)},[]);return H.useEffect(()=>{const l=a.current;if(o!==l){c(l);const d=o.map(f=>{const g=nf(f);return g?(g.addEventListener("scroll",i,{passive:!0}),[g,rf(g)]):null}).filter(f=>f!=null);n(d.length?new Map(d):null),a.current=o}return()=>{c(o),c(l)};function c(d){d.forEach(f=>{const g=nf(f);g?.removeEventListener("scroll",i)})}},[i,o]),H.useMemo(()=>o.length?e?Array.from(e.values()).reduce((l,c)=>hs(l,c),wr):Vm(o):wr,[o,e])}function ey(o,e){e===void 0&&(e=[]);const n=H.useRef(null);return H.useEffect(()=>{n.current=null},e),H.useEffect(()=>{const a=o!==wr;a&&!n.current&&(n.current=o),!a&&n.current&&(n.current=null)},[o]),n.current?Eo(o,n.current):wr}function YE(o){H.useEffect(()=>{if(!sc)return;const e=o.map(n=>{let{sensor:a}=n;return a.setup==null?void 0:a.setup()});return()=>{for(const n of e)n?.()}},o.map(e=>{let{sensor:n}=e;return n}))}function XE(o,e){return H.useMemo(()=>o.reduce((n,a)=>{let{eventName:i,handler:l}=a;return n[i]=c=>{l(c,e)},n},{}),[o,e])}function ty(o){return H.useMemo(()=>o?SE(o):null,[o])}const ny=[];function JE(o,e){e===void 0&&(e=gs);const[n]=o,a=ty(n?Nn(n):null),[i,l]=H.useState(ny);function c(){l(()=>o.length?o.map(f=>qm(f)?a:new af(e(f),f)):ny)}const d=dc({callback:c});return zr(()=>{d?.disconnect(),c(),o.forEach(f=>d?.observe(f))},[o]),i}function QE(o){if(!o)return null;if(o.children.length>1)return o;const e=o.children[0];return vo(e)?e:o}function ZE(o){let{measure:e}=o;const[n,a]=H.useState(null),i=H.useCallback(g=>{for(const{target:m}of g)if(vo(m)){a(y=>{const S=e(m);return y?{...y,width:S.width,height:S.height}:S});break}},[e]),l=dc({callback:i}),c=H.useCallback(g=>{const m=QE(g);l?.disconnect(),m&&l?.observe(m),a(m?e(m):null)},[e,l]),[d,f]=oc(c);return H.useMemo(()=>({nodeRef:d,rect:n,setRef:f}),[n,d,f])}const e_=[{sensor:cf,options:{}},{sensor:of,options:{}}],t_={current:{}},fc={draggable:{measure:zm},droppable:{measure:zm,strategy:To.WhileDragging,frequency:ff.Optimized},dragOverlay:{measure:gs}};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 n_={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:lc},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:fc,measureDroppableContainers:lc,windowRect:null,measuringScheduled:!1},r_={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:lc,draggableNodes:new Map,over:null,measureDroppableContainers:lc},hc=H.createContext(r_),ry=H.createContext(n_);function a_(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new Ro}}}function i_(o,e){switch(e.type){case an.DragStart:return{...o,draggable:{...o.draggable,initialCoordinates:e.initialCoordinates,active:e.active}};case an.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 an.DragEnd:case an.DragCancel:return{...o,draggable:{...o.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case an.RegisterDroppable:{const{element:n}=e,{id:a}=n,i=new Ro(o.droppable.containers);return i.set(a,n),{...o,droppable:{...o.droppable,containers:i}}}case an.SetDroppableDisabled:{const{id:n,key:a,disabled:i}=e,l=o.droppable.containers.get(n);if(!l||a!==l.key)return o;const c=new Ro(o.droppable.containers);return c.set(n,{...l,disabled:i}),{...o,droppable:{...o.droppable,containers:c}}}case an.UnregisterDroppable:{const{id:n,key:a}=e,i=o.droppable.containers.get(n);if(!i||a!==i.key)return o;const l=new Ro(o.droppable.containers);return l.delete(n),{...o,droppable:{...o.droppable,containers:l}}}default:return o}}function s_(o){let{disabled:e}=o;const{active:n,activatorEvent:a,draggableNodes:i}=H.useContext(hc),l=Qd(a),c=Qd(n?.id);return H.useEffect(()=>{if(!e&&!a&&l&&c!=null){if(!ef(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=QC(m);if(y){y.focus();break}}})}},[a,e,i,c,l]),null}function o_(o,e){let{transform:n,...a}=e;return o!=null&&o.length?o.reduce((i,l)=>l({transform:i,...a}),n):n}function l_(o){return H.useMemo(()=>({draggable:{...fc.draggable,...o?.draggable},droppable:{...fc.droppable,...o?.droppable},dragOverlay:{...fc.dragOverlay,...o?.dragOverlay}}),[o?.draggable,o?.droppable,o?.dragOverlay])}function c_(o){let{activeNode:e,measure:n,initialRect:a,config:i=!0}=o;const l=H.useRef(!1),{x:c,y:d}=typeof i=="boolean"?{x:i,y:i}:i;zr(()=>{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=n(g),y=jm(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 S=Um(g);S&&S.scrollBy({top:y.y,left:y.x})}},[e,c,d,a,n])}const ay=H.createContext({...wr,scaleX:1,scaleY:1});var Da;(function(o){o[o.Uninitialized=0]="Uninitialized",o[o.Initializing=1]="Initializing",o[o.Initialized=2]="Initialized"})(Da||(Da={}));const u_=H.memo(function(e){var n,a,i,l;let{id:c,accessibility:d,autoScroll:f=!0,children:g,sensors:m=e_,collisionDetection:y=hE,measuring:S,modifiers:v,..._}=e;const A=H.useReducer(i_,void 0,a_),[x,O]=A,[M,T]=aE(),[D,z]=H.useState(Da.Uninitialized),P=D===Da.Initialized,{draggable:{active:q,nodes:W,translate:he},droppable:{containers:Se}}=x,be=q!=null?W.get(q):null,Ue=H.useRef({initial:null,translated:null}),Fe=H.useMemo(()=>{var ge;return q!=null?{id:q,data:(ge=be?.data)!=null?ge:t_,rect:Ue}:null},[q,be]),Ge=H.useRef(null),[wt,J]=H.useState(null),[ue,Me]=H.useState(null),te=So(_,Object.values(_)),pe=Co("DndDescribedBy",c),N=H.useMemo(()=>Se.getEnabled(),[Se]),R=l_(S),{droppableRects:de,measureDroppableContainers:fe,measuringScheduled:xe}=BE(N,{dragging:P,dependencies:[he.x,he.y],config:R.droppable}),ee=UE(W,q),_e=H.useMemo(()=>ue?tf(ue):null,[ue]),Be=ve(),ke=qE(ee,R.draggable.measure);c_({activeNode:q!=null?W.get(q):null,config:Be.layoutShiftCompensation,initialRect:ke,measure:R.draggable.measure});const Ie=Qm(ee,R.draggable.measure,ke),Ct=Qm(ee?ee.parentElement:null),ot=H.useRef({activatorEvent:null,active:null,activeNode:ee,collisionRect:null,collisions:null,droppableRects:de,draggableNodes:W,draggingNode:null,draggingNodeRect:null,droppableContainers:Se,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),Ot=Se.getNodeFor((n=ot.current.over)==null?void 0:n.id),vt=ZE({measure:R.dragOverlay.measure}),_t=(a=vt.nodeRef.current)!=null?a:ee,Oe=P?(i=vt.rect)!=null?i:Ie:null,nt=!!(vt.nodeRef.current&&vt.rect),sn=FE(nt?null:Ie),Mt=ty(_t?Nn(_t):null),Vt=KE(P?Ot??ee:null),on=JE(Vt),ln=o_(v,{transform:{x:he.x-sn.x,y:he.y-sn.y,scaleX:1,scaleY:1},activatorEvent:ue,active:Fe,activeNodeRect:Ie,containerNodeRect:Ct,draggingNodeRect:Oe,over:ot.current.over,overlayNodeRect:vt.rect,scrollableAncestors:Vt,scrollableAncestorRects:on,windowRect:Mt}),Br=_e?hs(_e,he):null,Ft=WE(Vt),Ci=ey(Ft),na=ey(Ft,[Ie]),yn=hs(ln,Ci),On=Oe?mE(Oe,ln):null,rt=Fe&&On?y({active:Fe,collisionRect:On,droppableRects:de,droppableContainers:N,pointerCoordinates:Br}):null,Hn=Im(rt,"id"),[bn,ra]=H.useState(null),sr=nt?ln:hs(ln,na),cn=gE(sr,(l=bn?.rect)!=null?l:null,Ie),Tn=H.useRef(null),un=H.useCallback((ge,Ce)=>{let{sensor:Ae,options:Ke}=Ce;if(Ge.current==null)return;const Te=W.get(Ge.current);if(!Te)return;const $e=ge.nativeEvent,Re=new Ae({active:Ge.current,activeNode:Te,event:$e,options:Ke,context:ot,onAbort(je){if(!W.get(je))return;const{onDragAbort:we}=te.current,He={id:je};we?.(He),M({type:"onDragAbort",event:He})},onPending(je,Ve,we,He){if(!W.get(je))return;const{onDragPending:Tt}=te.current,It={id:je,constraint:Ve,initialCoordinates:we,offset:He};Tt?.(It),M({type:"onDragPending",event:It})},onStart(je){const Ve=Ge.current;if(Ve==null)return;const we=W.get(Ve);if(!we)return;const{onDragStart:He}=te.current,tt={activatorEvent:$e,active:{id:Ve,data:we.data,rect:Ue}};ka.unstable_batchedUpdates(()=>{He?.(tt),z(Da.Initializing),O({type:an.DragStart,initialCoordinates:je,active:Ve}),M({type:"onDragStart",event:tt}),J(Tn.current),Me($e)})},onMove(je){O({type:an.DragMove,coordinates:je})},onEnd:et(an.DragEnd),onCancel:et(an.DragCancel)});Tn.current=Re;function et(je){return async function(){const{active:we,collisions:He,over:tt,scrollAdjustedTranslate:Tt}=ot.current;let It=null;if(we&&Tt){const{cancelDrop:Ln}=te.current;It={activatorEvent:$e,active:we,collisions:He,delta:Tt,over:tt},je===an.DragEnd&&typeof Ln=="function"&&await Promise.resolve(Ln(It))&&(je=an.DragCancel)}Ge.current=null,ka.unstable_batchedUpdates(()=>{O({type:je}),z(Da.Uninitialized),ra(null),J(null),Me(null),Tn.current=null;const Ln=je===an.DragEnd?"onDragEnd":"onDragCancel";if(It){const zt=te.current[Ln];zt?.(It),M({type:Ln,event:It})}})}}},[W]),V=H.useCallback((ge,Ce)=>(Ae,Ke)=>{const Te=Ae.nativeEvent,$e=W.get(Ke);if(Ge.current!==null||!$e||Te.dndKit||Te.defaultPrevented)return;const Re={active:$e};ge(Ae,Ce.options,Re)===!0&&(Te.dndKit={capturedBy:Ce.sensor},Ge.current=Ke,un(Ae,Ce))},[W,un]),se=PE(m,V);YE(m),zr(()=>{Ie&&D===Da.Initializing&&z(Da.Initialized)},[Ie,D]),H.useEffect(()=>{const{onDragMove:ge}=te.current,{active:Ce,activatorEvent:Ae,collisions:Ke,over:Te}=ot.current;if(!Ce||!Ae)return;const $e={active:Ce,activatorEvent:Ae,collisions:Ke,delta:{x:yn.x,y:yn.y},over:Te};ka.unstable_batchedUpdates(()=>{ge?.($e),M({type:"onDragMove",event:$e})})},[yn.x,yn.y]),H.useEffect(()=>{const{active:ge,activatorEvent:Ce,collisions:Ae,droppableContainers:Ke,scrollAdjustedTranslate:Te}=ot.current;if(!ge||Ge.current==null||!Ce||!Te)return;const{onDragOver:$e}=te.current,Re=Ke.get(Hn),et=Re&&Re.rect.current?{id:Re.id,rect:Re.rect.current,data:Re.data,disabled:Re.disabled}:null,je={active:ge,activatorEvent:Ce,collisions:Ae,delta:{x:Te.x,y:Te.y},over:et};ka.unstable_batchedUpdates(()=>{ra(et),$e?.(je),M({type:"onDragOver",event:je})})},[Hn]),zr(()=>{ot.current={activatorEvent:ue,active:Fe,activeNode:ee,collisionRect:On,collisions:rt,droppableRects:de,draggableNodes:W,draggingNode:_t,draggingNodeRect:Oe,droppableContainers:Se,over:bn,scrollableAncestors:Vt,scrollAdjustedTranslate:yn},Ue.current={initial:Oe,translated:On}},[Fe,ee,rt,On,W,_t,Oe,de,Se,bn,Vt,yn]),HE({...Be,delta:he,draggingRect:On,pointerCoordinates:Br,scrollableAncestors:Vt,scrollableAncestorRects:on});const ce=H.useMemo(()=>({active:Fe,activeNode:ee,activeNodeRect:Ie,activatorEvent:ue,collisions:rt,containerNodeRect:Ct,dragOverlay:vt,draggableNodes:W,droppableContainers:Se,droppableRects:de,over:bn,measureDroppableContainers:fe,scrollableAncestors:Vt,scrollableAncestorRects:on,measuringConfiguration:R,measuringScheduled:xe,windowRect:Mt}),[Fe,ee,Ie,ue,rt,Ct,vt,W,Se,de,bn,fe,Vt,on,R,xe,Mt]),Z=H.useMemo(()=>({activatorEvent:ue,activators:se,active:Fe,activeNodeRect:Ie,ariaDescribedById:{draggable:pe},dispatch:O,draggableNodes:W,over:bn,measureDroppableContainers:fe}),[ue,se,Fe,Ie,O,pe,W,bn,fe]);return Xe.createElement(Om.Provider,{value:T},Xe.createElement(hc.Provider,{value:Z},Xe.createElement(ry.Provider,{value:ce},Xe.createElement(ay.Provider,{value:cn},g)),Xe.createElement(s_,{disabled:d?.restoreFocus===!1})),Xe.createElement(oE,{...d,hiddenTextDescribedById:pe}));function ve(){const ge=wt?.autoScrollEnabled===!1,Ce=typeof f=="object"?f.enabled===!1:f===!1,Ae=P&&!ge&&!Ce;return typeof f=="object"?{...f,enabled:Ae}:{enabled:Ae}}}),d_=H.createContext(null),iy="button",f_="Draggable";function h_(o){let{id:e,data:n,disabled:a=!1,attributes:i}=o;const l=Co(f_),{activators:c,activatorEvent:d,active:f,activeNodeRect:g,ariaDescribedById:m,draggableNodes:y,over:S}=H.useContext(hc),{role:v=iy,roleDescription:_="draggable",tabIndex:A=0}=i??{},x=f?.id===e,O=H.useContext(x?ay:d_),[M,T]=oc(),[D,z]=oc(),P=XE(c,e),q=So(n);zr(()=>(y.set(e,{id:e,key:l,node:M,activatorNode:D,data:q}),()=>{const he=y.get(e);he&&he.key===l&&y.delete(e)}),[y,e]);const W=H.useMemo(()=>({role:v,tabIndex:A,"aria-disabled":a,"aria-pressed":x&&v===iy?!0:void 0,"aria-roledescription":_,"aria-describedby":m.draggable}),[a,v,A,x,_,m.draggable]);return{active:f,activatorEvent:d,activeNodeRect:g,attributes:W,isDragging:x,listeners:a?void 0:P,node:M,over:S,setNodeRef:T,setActivatorNodeRef:z,transform:O}}function g_(){return H.useContext(ry)}const p_="Droppable",m_={timeout:25};function y_(o){let{data:e,disabled:n=!1,id:a,resizeObserverConfig:i}=o;const l=Co(p_),{active:c,dispatch:d,over:f,measureDroppableContainers:g}=H.useContext(hc),m=H.useRef({disabled:n}),y=H.useRef(!1),S=H.useRef(null),v=H.useRef(null),{disabled:_,updateMeasurementsFor:A,timeout:x}={...m_,...i},O=So(A??a),M=H.useCallback(()=>{if(!y.current){y.current=!0;return}v.current!=null&&clearTimeout(v.current),v.current=setTimeout(()=>{g(Array.isArray(O.current)?O.current:[O.current]),v.current=null},x)},[x]),T=dc({callback:M,disabled:_||!c}),D=H.useCallback((W,he)=>{T&&(he&&(T.unobserve(he),y.current=!1),W&&T.observe(W))},[T]),[z,P]=oc(D),q=So(e);return H.useEffect(()=>{!T||!z.current||(T.disconnect(),y.current=!1,T.observe(z.current))},[z,T]),H.useEffect(()=>(d({type:an.RegisterDroppable,element:{id:a,key:l,disabled:n,node:z,rect:S,data:q}}),()=>d({type:an.UnregisterDroppable,key:l,id:a})),[a]),H.useEffect(()=>{n!==m.current.disabled&&(d({type:an.SetDroppableDisabled,id:a,key:l,disabled:n}),m.current.disabled=n)},[a,l,n,d]),{active:c,rect:S,isOver:f?.id===a,node:z,over:f,setNodeRef:P}}function hf(o,e,n){const a=o.slice();return a.splice(n<0?a.length+n:n,0,a.splice(e,1)[0]),a}function b_(o,e){return o.reduce((n,a,i)=>{const l=e.get(a);return l&&(n[i]=l),n},Array(o.length))}function gc(o){return o!==null&&o>=0}function v_(o,e){if(o===e)return!0;if(o.length!==e.length)return!1;for(let n=0;n{var e;let{rects:n,activeNodeRect:a,activeIndex:i,overIndex:l,index:c}=o;const d=(e=n[i])!=null?e:a;if(!d)return null;const f=C_(n,c,i);if(c===i){const g=n[l];return g?{x:ii&&c<=l?{x:-d.width-f,y:0,...pc}:c=l?{x:d.width+f,y:0,...pc}:{x:0,y:0,...pc}};function C_(o,e,n){const a=o[e],i=o[e-1],l=o[e+1];return!a||!i&&!l?0:n{let{rects:e,activeIndex:n,overIndex:a,index:i}=o;const l=hf(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}},mc={scaleX:1,scaleY:1},E_=o=>{var e;let{activeIndex:n,activeNodeRect:a,index:i,rects:l,overIndex:c}=o;const d=(e=l[n])!=null?e:a;if(!d)return null;if(i===n){const g=l[c];return g?{x:0,y:nn&&i<=c?{x:0,y:-d.height-f,...mc}:i=c?{x:0,y:d.height+f,...mc}:{x:0,y:0,...mc}};function __(o,e,n){const a=o[e],i=o[e-1],l=o[e+1];return a?na.map(P=>typeof P=="object"&&"id"in P?P.id:P),[a]),_=c!=null,A=c?v.indexOf(c.id):-1,x=g?v.indexOf(g.id):-1,O=H.useRef(v),M=!v_(v,O.current),T=x!==-1&&A===-1||M,D=S_(l);zr(()=>{M&&_&&m(v)},[M,v,_,m]),H.useEffect(()=>{O.current=v},[v]);const z=H.useMemo(()=>({activeIndex:A,containerId:y,disabled:D,disableTransforms:T,items:v,overIndex:x,useDragOverlay:S,sortedRects:b_(v,f),strategy:i}),[A,y,D.draggable,D.droppable,T,v,x,f,S,i]);return Xe.createElement(oy.Provider,{value:z},e)}const x_=o=>{let{id:e,items:n,activeIndex:a,overIndex:i}=o;return hf(n,a,i).indexOf(e)},T_=o=>{let{containerId:e,isSorting:n,wasDragging:a,index:i,items:l,newIndex:c,previousItems:d,previousContainerId:f,transition:g}=o;return!g||!a||d!==l&&i===c?!1:n?!0:c!==i&&e===f},R_={duration:200,easing:"ease"},ly="transform",k_=_o.Transition.toString({property:ly,duration:0,easing:"linear"}),D_={roleDescription:"sortable"};function O_(o){let{disabled:e,index:n,node:a,rect:i}=o;const[l,c]=H.useState(null),d=H.useRef(n);return zr(()=>{if(!e&&n!==d.current&&a.current){const f=i.current;if(f){const g=gs(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)}}n!==d.current&&(d.current=n)},[e,n,a,i]),H.useEffect(()=>{l&&c(null)},[l]),l}function L_(o){let{animateLayoutChanges:e=T_,attributes:n,disabled:a,data:i,getNewIndex:l=x_,id:c,strategy:d,resizeObserverConfig:f,transition:g=R_}=o;const{items:m,containerId:y,activeIndex:S,disabled:v,disableTransforms:_,sortedRects:A,overIndex:x,useDragOverlay:O,strategy:M}=H.useContext(oy),T=M_(a,v),D=m.indexOf(c),z=H.useMemo(()=>({sortable:{containerId:y,index:D,items:m},...i}),[y,i,D,m]),P=H.useMemo(()=>m.slice(m.indexOf(c)),[m,c]),{rect:q,node:W,isOver:he,setNodeRef:Se}=y_({id:c,data:z,disabled:T.droppable,resizeObserverConfig:{updateMeasurementsFor:P,...f}}),{active:be,activatorEvent:Ue,activeNodeRect:Fe,attributes:Ge,setNodeRef:wt,listeners:J,isDragging:ue,over:Me,setActivatorNodeRef:te,transform:pe}=h_({id:c,data:z,attributes:{...D_,...n},disabled:T.draggable}),N=WC(Se,wt),R=!!be,de=R&&!_&&gc(S)&&gc(x),fe=!O&&ue,xe=fe&&de?pe:null,_e=de?xe??(d??M)({rects:A,activeNodeRect:Fe,activeIndex:S,overIndex:x,index:D}):null,Be=gc(S)&&gc(x)?l({id:c,items:m,activeIndex:S,overIndex:x}):D,ke=be?.id,Ie=H.useRef({activeId:ke,items:m,newIndex:Be,containerId:y}),Ct=m!==Ie.current.items,ot=e({active:be,containerId:y,isDragging:ue,isSorting:R,id:c,index:D,items:m,newIndex:Ie.current.newIndex,previousItems:Ie.current.items,previousContainerId:Ie.current.containerId,transition:g,wasDragging:Ie.current.activeId!=null}),Ot=O_({disabled:!ot,index:D,node:W,rect:q});return H.useEffect(()=>{R&&Ie.current.newIndex!==Be&&(Ie.current.newIndex=Be),y!==Ie.current.containerId&&(Ie.current.containerId=y),m!==Ie.current.items&&(Ie.current.items=m)},[R,Be,y,m]),H.useEffect(()=>{if(ke===Ie.current.activeId)return;if(ke!=null&&Ie.current.activeId==null){Ie.current.activeId=ke;return}const _t=setTimeout(()=>{Ie.current.activeId=ke},50);return()=>clearTimeout(_t)},[ke]),{active:be,activeIndex:S,attributes:Ge,data:z,rect:q,index:D,newIndex:Be,items:m,isOver:he,isSorting:R,isDragging:ue,listeners:J,node:W,overIndex:x,over:Me,setNodeRef:N,setActivatorNodeRef:te,setDroppableNodeRef:Se,setDraggableNodeRef:wt,transform:Ot??_e,transition:vt()};function vt(){if(Ot||Ct&&Ie.current.newIndex===D)return k_;if(!(fe&&!ef(Ue)||!g)&&(R||ot))return _o.Transition.toString({...g,property:ly})}}function M_(o,e){var n,a;return typeof o=="boolean"?{draggable:o,droppable:!1}:{draggable:(n=o?.draggable)!=null?n:e.draggable,droppable:(a=o?.droppable)!=null?a:e.droppable}}function yc(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 $_=[ut.Down,ut.Right,ut.Up,ut.Left],N_=(o,e)=>{let{context:{active:n,collisionRect:a,droppableRects:i,droppableContainers:l,over:c,scrollableAncestors:d}}=e;if($_.includes(o.code)){if(o.preventDefault(),!n||!a)return;const f=[];l.getEnabled().forEach(y=>{if(!y||y!=null&&y.disabled)return;const S=i.get(y.id);if(S)switch(o.code){case ut.Down:a.topS.top&&f.push(y);break;case ut.Left:a.left>S.left&&f.push(y);break;case ut.Right:a.left1&&(m=g[1].id),m!=null){const y=l.get(n.id),S=l.get(m),v=S?i.get(S.id):null,_=S?.node.current;if(_&&v&&y&&S){const x=cc(_).some((P,q)=>d[q]!==P),O=cy(y,S),M=I_(y,S),T=x||!O?{x:0,y:0}:{x:M?a.width-v.width:0,y:M?a.height-v.height:0},D={x:v.left,y:v.top};return T.x&&T.y?D:Eo(D,T)}}}};function cy(o,e){return!yc(o)||!yc(e)?!1:o.data.current.sortable.containerId===e.data.current.sortable.containerId}function I_(o,e){return!yc(o)||!yc(e)||!cy(o,e)?!1:o.data.current.sortable.index{const d=H.useId(),f=H.useRef({inProgress:!1,lastSortTime:0,lastActiveId:null}),g=lE(Lm(cf,{activationConstraint:{distance:5}}),Lm(of,{coordinateGetter:N_})),m=H.useCallback(v=>{f.current={inProgress:!0,lastSortTime:Date.now(),lastActiveId:v.active.id},l?.({activeId:v.active.id})},[l]),y=H.useCallback(v=>{const{active:_,over:A}=v,x=Date.now();if(!A||_.id===A.id){f.current.inProgress=!1;return}const{lastSortTime:O,lastActiveId:M,inProgress:T}=f.current;if(M===_.id&&x-O<500&&!T)return;f.current={inProgress:!1,lastSortTime:x,lastActiveId:_.id};const D=o.findIndex(P=>String(P[e])===String(_.id)),z=o.findIndex(P=>String(P[e])===String(A.id));if(D!==-1&&z!==-1&&D!==z){const P=hf(o,D,z);i?.(P,{oldIndex:D,newIndex:z})}},[o,e,i]),S=o.map(v=>String(v[e]));return gt.jsx(u_,{id:d,sensors:g,collisionDetection:uE,onDragStart:m,onDragEnd:y,children:gt.jsx(A_,{items:S,strategy:H_[n],children:a})},c!==void 0?`${d}-v${c}`:d)},uy=H.createContext(null),z_=({value:o,children:e})=>gt.jsx(uy.Provider,{value:o,children:e}),U_=()=>H.useContext(uy),P_=({id:o,handle:e,as:n="div",children:a})=>{const{attributes:i,listeners:l,setNodeRef:c,transform:d,transition:f,isDragging:g}=L_({id:o}),m={transform:_o.Transform.toString(d),transition:f,opacity:g?.5:1,position:"relative"};return gt.jsx(n,{ref:c,style:m,...i,...e?{}:l,"data-sortable-item":!0,"data-sortable-id":String(o),"data-dragging":g,children:gt.jsx(z_,{value:{listeners:l,handle:e,isDragging:g},children:a})})},st=ht("DynamicRenderer");function Zr(){try{return window.G7Core?.devTools}catch{return}}function pf(o,e,n,a){if(typeof o=="string"){if(/\$t:[a-zA-Z_][a-zA-Z0-9_.\-]*/.test(o))try{return e.resolveTranslations(o,n,a)}catch{return o}return o}if(Array.isArray(o))return o.map(i=>pf(i,e,n,a));if(o&&typeof o=="object"){const i={};for(const[l,c]of Object.entries(o))i[l]=pf(c,e,n,a);return i}return o}function B_(o){const e=Object.keys(o);return e.length>0&&e.every(n=>/^\d+$/.test(n))}const Cr=(o,e)=>{const n=["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(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)&&B_(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 S=parseInt(m,10);S>=0&&S=g.length&&(g[S]=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},bc=(o,e)=>{let n=null;const a=()=>(n===null&&(n={...o}),n);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=bc(c,l);if(d===c)continue;Object.keys(d).length===0?delete a()[i]:a()[i]=d}else delete a()[i]}return n??o},q_=o=>{const e={},n=o.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,g)=>g.toUpperCase());e[d]=c}}return e};class G_ extends H.Component{constructor(e){super(e),this.state={hasError:!1,error:null}}static getDerivedStateFromError(e){return{hasError:!0,error:e}}componentDidCatch(e,n){st.error(`컴포넌트 렌더링 에러 (ID: ${this.props.componentId}, Name: ${this.props.componentName}):`,e,n)}render(){return this.state.hasError?this.props.fallback?this.props.fallback:gt.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:[gt.jsx("div",{className:"font-semibold mb-1",children:"컴포넌트 로드 실패"}),gt.jsxs("div",{className:"text-xs opacity-80",children:[this.props.componentName&&`[${this.props.componentName}] `,"데이터를 표시할 수 없습니다."]}),this.state.error&>.jsxs("details",{className:"mt-2",children:[gt.jsx("summary",{className:"cursor-pointer text-xs opacity-70 hover:opacity-100",children:"상세 정보"}),gt.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 Ur=H.memo(({componentDef:o,dataContext:e,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:d,parentFormContextProp:f,parentDataContext:g,isRootRenderer:m=!1,isInsideIteration:y=!1,isEditMode:S=!1,onComponentSelect:v,onComponentHover:_,onDragStart:A,onDragEnd:x,componentPath:O,layoutKey:M})=>{const[T,D]=H.useState({loadingActions:{}});H.useLayoutEffect(()=>{d||(i.startRenderCycle(),window.__g7SetLocalOverrideKeys||(window.__g7ForcedLocalFields=void 0),window.__g7PendingLocalState=null)});const z=H.useRef(null);H.useLayoutEffect(()=>{if(!d&&e._localInit&&typeof e._localInit=="object"){const{_forceLocalInit:V,_merge:se,...ce}=e._localInit,Z=`${JSON.stringify(ce)}:${V||"no-force"}`;if(z.current!==Z){z.current=Z;const ve=se||"shallow",Ce=window.G7Core?.state?.get?.()?._local||{},Ae=window.__g7PendingLocalState||{},Ke={...Ce,...Ae};ve==="replace"?window.__g7PendingLocalState={loadingActions:Ke.loadingActions||{},...ce,hasChanges:!1}:ve==="deep"?window.__g7PendingLocalState=Cr(Ke,{...ce,hasChanges:!1}):window.__g7PendingLocalState={...Ke,...ce,hasChanges:!1};const Te=window.__g7SetLocalOverrideKeys;Te&&typeof Te=="object"&&(ve==="replace"?window.__g7SetLocalOverrideKeys={...ce,hasChanges:!1}:window.__g7SetLocalOverrideKeys=Cr(Te,{...ce,hasChanges:!1}))}}}),H.useLayoutEffect(()=>{if(!d){const V=window.__g7SetLocalOverrideKeys;if(V){D(ve=>bc(ve,V));const se=V,ce=window.__g7ForcedLocalFields,Z=window.__g7LastSetLocalSnapshot;queueMicrotask(()=>{window.__g7SetLocalOverrideKeys===se&&(window.__g7SetLocalOverrideKeys=void 0),window.__g7ForcedLocalFields===ce&&(window.__g7ForcedLocalFields=void 0),window.__g7LastSetLocalSnapshot===Z&&(window.__g7LastSetLocalSnapshot=void 0)})}}},[e._local]);const P=H.useRef(M);H.useEffect(()=>{o._fromBase&&M&&P.current!==M&&(P.current=M,D({loadingActions:{}}))},[M,o._fromBase]);const q=H.useRef(""),W=H.useRef(!1),he=H.useRef(null),Se=H.useRef(null),be=H.useRef({}),Ue=H.useRef({}),Fe=H.useRef(g),Ge=KC(),wt=EC(),J=wt?.getParentDataContext()??g;H.useEffect(()=>{if(e._localInit&&typeof e._localInit=="object"){const{_forceLocalInit:V,...se}=e._localInit,ce=JSON.stringify(se),Z=V!==void 0,ve=Fd(),ge=`${ce}:${V||"no-force"}`,Ce=OC({globalTrackedKey:ve.hash,instanceHandledKey:Se.current,trackingKey:ge});if(Ce==="apply"){Se.current=ge,ve.hash=ge,ve.timestamp=V,q.current=ce,Z&&(W.current=!0);const{_merge:Ke,...Te}=se,$e=Ke||"shallow";D(Re=>$e==="replace"?{loadingActions:Re.loadingActions||{},...Te,hasChanges:!1}:$e==="deep"?Cr(Re,{...Te,hasChanges:!1}):{...Re,...Te,hasChanges:!1}),e._globalSetState&&e._globalSetState(Re=>{const et=Re?._local||{};let je;return $e==="replace"?je={...Te,hasChanges:!1}:$e==="deep"?je=Cr(et,{...Te,hasChanges:!1}):je={...et,...Te,hasChanges:!1},{...Re,_local:je}}),i.invalidateCacheByKeys(["_local"]),st.log("_localInit applied (data changed):",Object.keys(se))}else if(Ce==="prune"){Se.current=ge;const{_merge:Ke,...Te}=se;D(Re=>bc(Re,Te)),bc(T,Te)!==T&&(i.invalidateCacheByKeys(["_local"]),st.log("_localInit pruned (applied by another renderer):",Object.keys(Te)))}he.current=e._localInit,kC(e._localInit)}},[e._localInit,g,i]);const{isTransitioning:ue}=Cm(),Me=jC(),te=f??Me,{width:pe}=Wd(),N=U_(),R=H.useMemo(()=>{let V=o;if(o.type==="iterator"&&o.data){const{data:Z,itemName:ve,indexName:ge,...Ce}=o;V={...Ce,iteration:{source:Z,item_var:ve||"item",index_var:ge}}}if(!V.responsive)return V;const se=hi.getMatchingKey(V.responsive,pe);if(!se)return V;const ce=V.responsive[se];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?se:void 0}},[o,pe]),de=H.useCallback(V=>{const se=V?.__setStateId;if(D(ce=>{const Z=window.__g7PendingLocalState,ge=window.__g7SetLocalOverrideKeys||Z,Ce=ge?Cr(ce,ge):ce;if(typeof V=="function"){const Re=V(Ce);return Cr(Ce,Re)}const{__mergeMode:Ae,__setStateId:Ke,...Te}=V;let $e;if(Ae==="replace"?$e=Te:Ae==="shallow"?$e={...Ce,...Te}:$e=Cr(Ce,Te),Ae!==void 0){const Re=window.__g7ForcedLocalFields;Re&&(window.__g7ForcedLocalFields={...Re,...Te})}return $e}),se){const ce=Zr();if(ce?.isEnabled()){const Z=R.id||"unknown",ve=R.name||"Unknown";setTimeout(()=>{ce.trackComponentRender?.(Z,ve,0,["_local"],[]),ce.completeStateChange?.(se)},0)}}},[R.id,R.name]),fe=d?.state??T,xe=d?.setState??de,ee=H.useMemo(()=>{const V=e._localInit&&e._localInit!==he.current;let se;V&&e._local?se=e._local:se=e._local?Cr(e._local,fe):fe;const ce=window.__g7ForcedLocalFields;ce&&(se=Cr(se,ce));const Z={...e,_local:se},ve=e._computedDefinitions;if(ve&&Object.keys(ve).length>0){const ge=new Dn,Ce={...Z,...Ge?{_isolated:Ge.state}:{}},Ae={},Ke=Zr(),Te=Ke?.isEnabled()??!1;for(const[$e,Re]of Object.entries(ve)){const et=Te?Date.now():0,je=Te?e._computed?.[$e]:void 0;let Ve;try{if(typeof Re=="string")if(Re.startsWith("{{")&&Re.endsWith("}}")){const we=Re.slice(2,-2).trim(),He=Vn(we)?ge.evaluatePipeExpression(we,Ce,{skipCache:!0}):ge.evaluateExpression(we,Ce,{skipCache:!0});Ae[$e]=He}else Ae[$e]=Re;else if(Re&&typeof Re=="object"&&"$switch"in Re){const we=ge.resolveSwitch(Re,Ce,{skipCache:!0});Ae[$e]=we}}catch(we){st.warn(`Failed to recalculate computed value: ${$e}`,we),Ae[$e]=e._computed?.[$e],Ve=we instanceof Error?we.message:String(we)}if(Te&&(Ve||je!==Ae[$e])){const He=Date.now()-et;Ke.trackComputedProperty?.($e,typeof Re=="string"?Re:JSON.stringify(Re),[{type:"_local",path:"_local",value:Z._local}],Ae[$e],He,R.id,Ve),je!==Ae[$e]&&Ke.trackComputedRecalc?.($e,"dependency-change",je,Ae[$e],He,{type:"_local",path:"_local"},R.id)}}Z._computed=Ae}if(Ge&&(Z._isolated=Ge.state),J&&(Z.$parent=J),o.extensionPointProps){const ge=i.resolveObject(o.extensionPointProps,Z,y?{skipCache:!0}:void 0);Z.extensionPointProps=S&&l?pf(ge,l,n,Z):ge}return o.extensionPointCallbacks&&(Z.extensionPointCallbacks=o.extensionPointCallbacks),S&&(Z.$templateId===void 0&&n?.templateId&&(Z.$templateId=n.templateId),Z.$locale===void 0&&n?.locale&&(Z.$locale=n.locale)),Z},[e,fe,Ge?.state,J,wt?.version,o.extensionPointProps,o.extensionPointCallbacks]);be.current=ee._local,Ue.current=ee._computed||{},Fe.current=J,H.useEffect(()=>{const V=Zr();if(!V?.isEnabled())return;const se=ee._local;se&&V.updateLocalState(se);const ce=ee._computed;ce&&V.updateComputedState(ce),J&&V.updateParentContext?.(J);const Z=R.id||`comp-${Date.now()}`,ve=R.name||"Unknown",ge={global:Object.keys(e._global||{}),local:Object.keys(se||{}),context:d?Object.keys(d.state||{}):[]},Ce={type:d?"componentContext":"dynamicState",hasComponentContext:!!d,parentId:d?"parent":void 0};V.trackComponentStateSource?.(Z,ve,ge,Ce),fe&&Object.keys(fe).length>0&&V.trackDynamicState?.(Z,fe);const Ae=!!(R.expandChildren||R.props?.expandChildren);V.trackContextFlow?.(Z,ve,!!d,Ae,!0)},[ee._local,e,R.id,R.name,d,fe,R.expandChildren,R.props?.expandChildren,J]);const _e=H.useMemo(()=>({state:{...ee._local},setState:xe,stateRef:be,computedRef:Ue,isolatedContext:Ge,parentDataContext:ee}),[ee._local,xe,T,Ge,ee]),Be=H.useMemo(()=>{const V=ns({if:R.if,condition:R.condition,conditions:R.conditions},ee,i,R.id);if(R.if||R.condition!==void 0||R.conditions){const ce=Zr();ce?.isEnabled()&&R.if&&ce.trackIfCondition(R.id||`if-${Date.now()}`,R.if,V)}return V},[R.if,R.conditions,ee,i,R.id]),ke=H.useMemo(()=>{if(!R.slot)return null;const V=R.slot;if(V.startsWith("{{")&&V.endsWith("}}")){const se=V.slice(2,-2).trim();try{const ce={componentId:R.id,componentName:R.name,propName:"slot"},Z=Vn(se)?i.evaluatePipeExpression(se,ee,void 0,ce):i.evaluateExpression(se,ee,ce);return st.log(`[Slot] 표현식 평가: "${se}" => "${Z}" (컴포넌트: ${R.id})`),typeof Z=="string"?Z:null}catch(ce){return st.warn(`slot 표현식 평가 실패 (컴포넌트: ${R.id}):`,ce),null}}return st.log(`[Slot] 정적 슬롯 ID: "${V}" (컴포넌트: ${R.id})`),V},[R.slot,R.id,ee,i]),Ie=H.useMemo(()=>!ke||!R.id?"":`${R.id}-${ke}-${JSON.stringify(R.slotOrder??0)}`,[ke,R.id,R.slotOrder]),Ct=H.useRef(null);H.useEffect(()=>{const V=window.__slotContextValue,se=V?.isEnabled??!1;if(R.slot&&st.log(`[Slot Registration] 컴포넌트: ${R.id}, isSlotEnabled: ${se}, shouldRender: ${Be}, resolvedSlotId: ${ke}`),!se||!Be){R.slot&&st.log(`[Slot Registration] 등록 스킵 - isSlotEnabled: ${se}, shouldRender: ${Be}`),Ct.current&&R.id&&V&&(V.unregisterFromSlot(Ct.current,R.id),Ct.current=null);return}if(!ke||!R.id){R.slot&&st.log(`[Slot Registration] 등록 스킵 - resolvedSlotId: ${ke}, id: ${R.id}`),Ct.current&&R.id&&V&&(V.unregisterFromSlot(Ct.current,R.id),Ct.current=null);return}Ct.current&&Ct.current!==ke&&V.unregisterFromSlot(Ct.current,R.id);const ce={...R,slot:void 0,slotOrder:void 0};return V.registerToSlot(ke,R.id,{componentDef:ce,dataContext:e,order:R.slotOrder??0,parentFormContext:f??null,getParentComponentContext:()=>_e,translationContext:n,registrationKey:Ie}),Ct.current=ke,()=>{const Z=window.__slotContextValue;ke&&R.id&&Z&&Z.unregisterFromSlot(ke,R.id)}},[Be,ke,R,e,f,_e,n,Ie]);const ot=H.useCallback(V=>{if(typeof V=="string"){if(jl(V))return bd(V);if(zl(V)){const se=[],ce=V.replace(new RegExp(`${ao}([^${fi}]*)${fi}`,"g"),(ve,ge)=>(se.push(ge),`${xa}${se.length-1}${xa}`));let Z=ce;return/\$t:[a-zA-Z0-9._-]+/.test(ce)&&(Z=l.resolveTranslations(ce,n,ee)),Z.replace(new RegExp(`${xa}(\\d+)${xa}`,"g"),(ve,ge)=>se[parseInt(ge)])}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,ee)}return V}if(Array.isArray(V))return V.map(se=>ot(se));if(V&&typeof V=="object"){const se={};for(const[ce,Z]of Object.entries(V))se[ce]=ot(Z);return se}return V},[l,n,ee]),Ot=H.useCallback(V=>ro(V),[]),vt=H.useCallback(V=>Aa(V),[]),_t=H.useMemo(()=>{let V={...R.props||{}};const se=["cellChildren","expandChildren","expandContext","render"],Z=a.getMetadata(R.name)?.skipBindingKeys||[],ve=[...new Set([...se,...Z])],ge=y?{skipCache:!0}:void 0,Ce={...ge??{},skipBindingKeys:ve},Ae=we=>{if(ge)return ge;if(we.startsWith("_computed")||we.startsWith("$computed"))return{skipCache:!0}};for(const[we,He]of Object.entries(V))if(!ve.includes(we))if(typeof He=="string"){const tt=vt(He);if(tt!==null){let Tt=!1,It=tt;tt.startsWith(nr)&&(Tt=!0,It=tt.slice(nr.length));const Ln={componentId:R.id,componentName:R.name,propName:we};let zt;if(Vn(It))try{zt=i.evaluatePipeExpression(It,ee,Ae(It),Ln)}catch(Ut){st.warn(`파이프 표현식 평가 실패 (컴포넌트: ${R.id}, prop: ${we}):`,Ut),zt=void 0}else if(Ot(It))try{zt=i.evaluateExpression(It,ee,Ln)}catch(Ut){st.warn(`표현식 평가 실패 (컴포넌트: ${R.id}, prop: ${we}):`,Ut),zt=void 0}else zt=i.resolve(It,ee,Ae(It),Ln);V[we]=Tt&&zt!=null?ts(zt):zt}else{const Tt={componentId:R.id,componentName:R.name,propName:we};V[we]=i.resolveBindings(He,ee,ge,Tt)}}else Array.isArray(He)?V[we]=He.map(tt=>typeof tt=="string"?i.resolveBindings(tt,ee,ge):typeof tt=="object"&&tt!==null?i.resolveObject(tt,ee,Ce):tt):He&&typeof He=="object"&&(V[we]=i.resolveObject(He,ee,Ce));for(const[we,He]of Object.entries(V))V[we]=ot(He);if(typeof V.style=="string"&&(V.style=q_(V.style)),V=Cd(V,S?void 0:R.actions,ee,{componentContext:_e,actionDispatcher:c}),R.classMap){const we=Ew(R.classMap,ee,i,{skipCache:!0});if(we){const He=V.className||"";V.className=He?`${He} ${we}`:we}}R.style&&(V.style=R.style),R.actions&&R.actions.length>0&&R.type==="composite"&&Object.assign(V,fe),R.component_layout&&(V.__componentLayoutDefs=R.component_layout);const Ke=R.expandChildren||R.props?.expandChildren,Te=R.props?.cellChildren,$e=R.children&&R.children.length>0,Re=R.component_layout,et=R.slot,je=R.props?.modalId,Ve=R.type==="composite";return(Ke||Te||$e||Re||et||je||Ve)&&(V.__componentContext=_e),V},[R.props,R.actions,R.style,R.classMap,R.id,ee,n,i,l,_e,fe,ot,Ot,vt,y]),Oe=H.useRef(null),nt=H.useMemo(()=>Oe.current&&_m(Oe.current,_t)?Oe.current:(Oe.current=_t,_t),[_t]),sn=H.useMemo(()=>{const V=R.id;if(typeof V!="string"||!V.includes("{{"))return V;const se=i.resolveBindings(V,ee,{skipCache:!0});return typeof se=="string"?se:V},[R.id,ee,i]),Mt=H.useMemo(()=>{const V=R.dataKey,se=R.trackChanges,ce=R.debounce;if(V){const Z=V.startsWith("_global."),ve=Z?V.slice(8):V,ge=Z?e._global:ee._local;return{dataKey:ve,trackChanges:se??!1,debounce:ce,setState:Z?Ae=>{e._globalSetState?e._globalSetState(Ae):st.warn("_global.formData 사용 시 _globalSetState가 필요합니다.")}:xe,state:ge??{},_isGlobal:Z}}if(te.dataKey)return te},[R.dataKey,R.trackChanges,R.debounce,R.id,xe,ee._local,e._global,e._globalSetState,te]),Vt=H.useRef(new Set);H.useEffect(()=>{Vt.current.clear()},[e._global?._remountKeys]);const on=H.useCallback((V,se)=>{const ce=e._global?._remountKeys;return V&&ce?.[V]&&!Vt.current.has(V)?(Vt.current.add(V),`${V}-remount-${ce[V]}`):V||se},[e._global?._remountKeys]),ln=H.useMemo(()=>{if(R.text!==void 0){if(typeof R.text=="string"){const V=R.text;let se=V;const ce={componentId:R.id,componentName:R.name,propName:"text",skipCache:!0},Z=vt(V);if(Z!==null){let ve=!1,ge=Z;if(Z.startsWith(nr)&&(ve=!0,ge=Z.slice(nr.length)),Vn(ge))try{se=i.evaluatePipeExpression(ge,ee,{skipCache:!0},ce)}catch(Ce){st.warn(`text 파이프 표현식 평가 실패 (컴포넌트: ${R.id}):`,Ce),se=""}else if(Ot(ge))try{se=i.evaluateExpression(ge,ee,ce)}catch(Ce){st.warn(`text 표현식 평가 실패 (컴포넌트: ${R.id}):`,Ce),se=""}else se=i.resolve(ge,ee,{skipCache:!0},ce);ve&&se!=null&&(se=ts(se))}else V.includes("{{")&&(se=i.resolveBindings(V,ee,{skipCache:!0},ce));return se=ot(se),se??""}return R.text}return!R.children||R.children.length===0?null:R.children.map((V,se)=>{if(typeof V=="string")return V;const ce=R.__responsiveChildrenKey,Z=ce?`responsive.${ce}.children`:"children",ve=O?`${O}.${Z}.${se}`:`${se}`,ge=V._fromBase?V.id||`child-${se}`:on(V.id,`child-${se}`),Ce=!V._fromBase&&M?`${ge}_${M}`:ge;return gt.jsx(Ur,{componentDef:V,dataContext:ee,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:_e,parentFormContextProp:Mt,isInsideIteration:y,isEditMode:S,onComponentSelect:v,onComponentHover:_,componentPath:ve,onDragStart:A,onDragEnd:x,layoutKey:M},Ce)})},[R.text,R.children,R.id,e,ee,n,a,i,l,c,_e,Mt,vt,Ot,ot,on,y,S,v,_,O,A,x,M]),Br=H.useMemo(()=>{if(!R.iteration)return null;try{const V=wd(R.iteration.source,ee,i);if(!Array.isArray(V))return st.warn(`iteration source가 배열이 아닙니다 (컴포넌트: ${R.id}):`,V),null;const se=Zr();return se?.isEnabled()&&se.trackIteration(R.id||`iteration-${Date.now()}`,R.iteration.source,R.iteration.item_var,R.iteration.index_var,V.length),V.map((ce,Z)=>{const ve={...ee,[R.iteration.item_var]:ce,[`${R.iteration.item_var}_index`]:Z};R.iteration.index_var&&(ve[R.iteration.index_var]=Z);const ge=Z===0||Z===V.length-1;se?.isEnabled()&&ge&&se.trackNestedContext?.({componentId:`${R.id||"unknown"}-iter-${Z}`,componentType:"iteration",parentContext:{available:Object.keys(ee),values:{}},ownContext:{added:[R.iteration.item_var,...R.iteration.index_var?[R.iteration.index_var]:[]],values:{[R.iteration.item_var]:ce,...R.iteration.index_var?{[R.iteration.index_var]:Z}:{}}},depth:1});const Ce={...R,iteration:void 0,responsive:void 0},Ae=O?`${O}.iteration.${Z}`:`iteration.${Z}`,Ke=`${on(R.id,"item")}-${Z}`;return gt.jsx(Ur,{componentDef:Ce,dataContext:ve,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:_e,parentFormContextProp:Mt,isInsideIteration:!0,isEditMode:S,onComponentSelect:v,onComponentHover:_,componentPath:Ae,onDragStart:A,onDragEnd:x,layoutKey:M},Ke)})}catch(V){return st.error(`iteration 렌더링 실패 (컴포넌트: ${R.id}):`,V),null}},[R.iteration,R.children,R.id,e,ee,n,a,i,l,c,_e,Mt,on,S,v,_,O,A,x,M]),[Ft,Ci]=H.useState(0),na=H.useMemo(()=>{if(!R.sortable||!R.itemTemplate)return null;const{sortable:V,itemTemplate:se}=R;try{const ce=wd(V.source,ee,i);if(!Array.isArray(ce))return st.warn(`sortable source가 배열이 아닙니다 (컴포넌트: ${R.id}):`,ce),null;if(ce.length===0)return null;const Z=V.itemKey||"id",ve=V.strategy||"verticalList",ge=V.itemVar||"$item",Ce=V.indexVar||"$index",Ae=V.handle,Ke=V.wrapperElement,Te=(et,je)=>{const Ve=R.actions?.filter(we=>we.event==="onSortEnd"||we.type==="onSortEnd");if(Ci(we=>we+1),Ve&&Ve.length>0&&c){const we={...ee,$sortedItems:et,$sortEvent:je};Ve.forEach(He=>{try{const tt=c.createHandler(He,we,_e),Tt=new Event("sortend");tt(Tt)}catch(tt){st.error(`onSortEnd action failed: ${He.handler}`,tt)}})}},$e=et=>{const je=R.actions?.filter(Ve=>Ve.event==="onSortStart"||Ve.type==="onSortStart");if(je&&je.length>0&&c){const Ve={...ee,$activeId:et.activeId};je.forEach(we=>{try{const He=c.createHandler(we,Ve,_e),tt=new Event("sortstart");He(tt)}catch(He){st.error(`onSortStart action failed: ${we.handler}`,He)}})}},Re=ce.map((et,je)=>{const Ve=String(et[Z]),we={...ee,[ge]:et,[Ce]:je},He=O?`${O}.sortable.${je}`:`sortable.${je}`,tt=`${on(R.id,"sortable-item")}-v${Ft}-${Ve}`;return gt.jsx(P_,{id:Ve,handle:Ae,...Ke?{as:Ke}:{},children:(Ke&&se.children?se.children:[se]).map((Tt,It)=>gt.jsx(Ur,{componentDef:Tt,dataContext:we,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:_e,parentFormContextProp:void 0,isInsideIteration:!0,isEditMode:S,onComponentSelect:v,onComponentHover:_,componentPath:He,onDragStart:A,onDragEnd:x,layoutKey:M},`${tt}-child-${It}`))},tt)});return gt.jsx(j_,{items:ce,itemKey:Z,strategy:ve,onSortEnd:Te,onSortStart:$e,sortVersion:Ft,children:Re})}catch(ce){return st.error(`sortable 렌더링 실패 (컴포넌트: ${R.id}):`,ce),null}},[R.sortable,R.itemTemplate,R.actions,R.id,e,ee,n,a,i,l,c,_e,on,S,v,_,O,A,x,Ft,M]),yn=H.useRef(!1);H.useEffect(()=>{if(yn.current)return;yn.current=!0;const V=Zr();if(V?.isEnabled()&&V.trackMount(o.id,{name:o.name,type:o.type||"component",props:nt}),o.lifecycle?.onMount&&o.lifecycle.onMount.length>0)for(const ce of o.lifecycle.onMount)try{const Z=c.createHandler(ce,ee,_e),ve=new Event("mount");Z(ve),st.log(`Lifecycle onMount executed: ${ce.handler} (Component: ${o.id})`)}catch(Z){st.error(`Lifecycle onMount failed: ${ce.handler} (Component: ${o.id})`,Z)}const se=[];if(o.onComponentEvent&&o.onComponentEvent.length>0){const ce=window.G7Core;if(ce?.componentEvent?.on)for(const Z of o.onComponentEvent){if(!Z.event){st.warn(`onComponentEvent: event name is required (Component: ${o.id})`);continue}const ve=async Ce=>{try{const Ae={...ee,_eventData:Ce},Te=await c.createHandler({handler:Z.handler,params:Z.params,onSuccess:Z.onSuccess,onError:Z.onError},Ae,_e)(new CustomEvent(Z.event,{detail:Ce}));return st.log(`onComponentEvent "${Z.event}" handled: ${Z.handler} (Component: ${o.id})`,{eventData:Ce,result:Te}),Te}catch(Ae){throw st.error(`onComponentEvent "${Z.event}" failed: ${Z.handler} (Component: ${o.id})`,Ae),Ae}},ge=ce.componentEvent.on(Z.event,ve);se.push(ge),st.log(`onComponentEvent: Subscribed to "${Z.event}" (Component: ${o.id})`)}else st.warn(`onComponentEvent: G7Core.componentEvent is not available (Component: ${o.id})`)}return()=>{const ce=Zr();ce?.isEnabled()&&ce.trackUnmount(o.id);for(const Z of se)try{Z()}catch(ve){st.error(`onComponentEvent: Failed to unsubscribe (Component: ${o.id})`,ve)}if(se.length>0&&st.log(`onComponentEvent: Unsubscribed ${se.length} event(s) (Component: ${o.id})`),o.lifecycle?.onUnmount&&o.lifecycle.onUnmount.length>0)for(const Z of o.lifecycle.onUnmount)try{const ve=c.createHandler(Z,ee,_e),ge=new Event("unmount");ve(ge),st.log(`Lifecycle onUnmount executed: ${Z.handler} (Component: ${o.id})`)}catch(ve){st.error(`Lifecycle onUnmount failed: ${Z.handler} (Component: ${o.id})`,ve)}}},[]);const On=H.useMemo(()=>{const V=nt.dataKey,se=nt.trackChanges,ce=nt.debounce;return V?{dataKey:V,trackChanges:se??!1,debounce:ce,setState:xe,state:fe}:null},[nt.dataKey,nt.trackChanges,nt.debounce,xe,fe]);H.useEffect(()=>{const V=R.dataKey||nt.dataKey;if(!V)return;const se=Zr();if(!se?.isEnabled())return;const ce=[],Z=ge=>{if(ge)for(const Ce of ge){const Ae=Ce?.props?.name;if(Ae&&ce.push({name:Ae,type:Ce.name||Ce.type||"unknown"}),Ce?.children&&Z(Ce.children),Ce?.slots)for(const Ke of Object.values(Ce.slots))Array.isArray(Ke)&&Z(Ke)}};if(o.children&&Z(o.children),o.slots)for(const ge of Object.values(o.slots))Array.isArray(ge)&&Z(ge);const ve=o.id||`form-${V}`;return se.trackForm(ve,V,ce),()=>{se.untrackForm(ve)}},[R,nt.dataKey,o.id,o.children]);const rt=H.useRef(null),Hn=H.useRef(void 0);H.useEffect(()=>{const V=nt?.name;if(!V||!te.dataKey||!te.setState||nt?.autoBinding===!1)return;const se=`${te.dataKey}.${V}`,ce=window.__g7AutoBindingPaths??new Map;return window.__g7AutoBindingPaths=ce,ce.set(se,(ce.get(se)??0)+1),()=>{const Z=ce.get(se)??0;Z<=1?ce.delete(se):ce.set(se,Z-1)}},[nt?.name,nt?.autoBinding,te.dataKey,te.setState]);const bn=H.useMemo(()=>{const{dataKey:V,trackChanges:se,debounce:ce,...Z}=nt,ve=Z.name;if(!ve||!te.dataKey||!te.setState)return Z;if(Z.autoBinding===!1){const{autoBinding:Ve,...we}=Z;return we}const ge=`${te.dataKey}.${ve}`,Ce=zC(te.state,ge),Ae=Ce!==void 0?Ce:Z.value,Te=a.getMetadata(o.name)?.bindingType,$e=typeof Ae=="boolean"&&(Te==="checked"||Te==="checkable"&&["checkbox","radio"].includes(Z?.type)),Re=te.debounce,et=Ve=>{const we=te.state||{},He=UC(we,ge,Ve);if(te.trackChanges&&(He.hasChanges=!0),window.__g7PendingLocalState=He,te.setState(He),te._isGlobal)return;const tt=window.G7Core,Tt={[ge]:Ve};te.trackChanges&&(Tt.hasChanges=!0),tt?.state?.setLocal?.(Tt,{render:!1})},je=(Ve,we)=>{Re&&Re>0?(rt.current&&clearTimeout(rt.current),rt.current=setTimeout(()=>{et(Ve),rt.current=null,we&&(we(),window.__g7PendingLocalState=null)},Re)):(et(Ve),we&&(we(),window.__g7PendingLocalState=null))};if($e)return{...Z,checked:Ae,onChange:we=>{const He=we.target.checked;et(He),Z.onChange&&Z.onChange(we)}};{const Ve=He=>{const tt=He?.target?.value!==void 0?He.target.value:He;Re&&Re>0&&(Hn.current=tt);const Tt=He?.target?{target:{value:tt,name:He.target.name,type:He.target.type,checked:He.target.checked},currentTarget:He.currentTarget?{value:tt,name:He.currentTarget.name}:void 0}:He;je(tt,()=>{Z.onChange&&Z.onChange(Tt)})};let we;return Hn.current!==void 0?rt.current!==null?we=Hn.current:String(Ae??"")===String(Hn.current)?(Hn.current=void 0,we=Ae??""):we=Hn.current:we=Ae??"",{...Z,value:we,onChange:Ve}}},[nt,te,te.state,te.debounce]),ra=H.useMemo(()=>{const V=Zr();if(V?.isEnabled()&&V.trackRender(o.name),!Be)return R.iteration?Br:null;if(R.sortable&&R.itemTemplate)return na;if(R.iteration)return Br;if(R.type==="extension_point"){const{layout:Te,columns:$e,gap:Re,className:et,...je}=R.props||{};let Ve=et||"";return Te==="grid"&&$e?Ve=`${Ve} grid grid-cols-${$e} gap-${Re||4}`.trim():Te==="flex"&&(Ve=`${Ve} flex flex-col gap-${Re||4}`.trim()),gt.jsx("div",{id:sn,className:Ve||void 0,...je,children:ln})}const se=a.getComponent(R.name);if(!se)return st.error(`컴포넌트를 찾을 수 없습니다: ${R.name} (ID: ${R.id})`,"componentDef:",JSON.stringify(R,null,2)),null;const ce=e._global?._remountKeys,Z={...bn,id:bn.id??sn,...ce&&Object.keys(ce).length>0&&{__remountTrigger:JSON.stringify(ce)}};if(N?.listeners&&N.handle&&bn["data-drag-handle"]!==void 0&&(Object.assign(Z,N.listeners),Z.style={...Z.style,cursor:N.isDragging?"grabbing":"grab"}),S){const Te=R.id||`auto_${R.name||R.type||"unknown"}_${Math.random().toString(36).substring(2,8)}`,$e=je=>{typeof je.preventDefault=="function"&&je.preventDefault(),v&&(je.stopPropagation(),v(Te,je))},Re={"data-editor-id":Te,"data-editor-name":R.name,"data-editor-type":R.type,onClick:$e};O&&(Re["data-editor-path"]=O);const et=R.__editorSlotName??R.slot;typeof et=="string"&&(Re["data-editor-slot"]=et),_&&(Re.onMouseMove=je=>{je.stopPropagation(),_(Te,je)},Re.onMouseLeave=je=>{_(null,je)}),Object.assign(Z,Re),Z.editorAttrs=Re}const ve=R.type==="basic",ge=R.type==="layout",Ae=ve||ge?(()=>{const Te={};for(const[$e,Re]of Object.entries(Z))$e.startsWith("__")||$e==="editorAttrs"&&ve||(Te[$e]=Re);return Te})():Z;let Ke=gt.jsx(se,{...Ae,children:ln});if(Mt&&(Ke=gt.jsx(HC,{value:Mt,children:Ke})),R.isolatedState){const Te=typeof R.isolatedState=="object"?R.isolatedState:{},$e=e?._isolatedInit,Re=$e?{...Te,...$e}:Te;Ke=gt.jsx(FC,{initialState:Re,scopeId:R.isolatedScopeId,children:Ke})}return Ke},[Be,R.iteration,R.sortable,R.itemTemplate,R.type,R.name,R.id,R.props,R.isolatedState,R.isolatedScopeId,a,bn,ln,Br,na,N,On,S,v,_,A,x,O,e._global?._remountKeys]),sr=H.useMemo(()=>{const V=R.blur_until_loaded;if(!V||e?._global?.__isPreview)return!1;if(typeof V=="string"){const Z=vt(V);if(Z!==null)try{const ve={componentId:R.id,componentName:R.name,propName:"blur_until_loaded"};return!!i.evaluateExpression(Z,ee,ve)}catch(ve){return st.warn(`blur_until_loaded 표현식 평가 실패 (컴포넌트: ${R.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(Ce=>e[Ce]===void 0);if(ue)return!0;const Z=["route","query","_global","_local","_dataSourceErrors"];return Object.keys(e).filter(ge=>!Z.includes(ge)&&!ge.startsWith("_")).some(ge=>e[ge]===void 0)}if(ue)return!0;const se=["route","query","_global","_local","_dataSourceErrors"];return Object.keys(e).filter(Z=>!se.includes(Z)&&!Z.startsWith("_")).some(Z=>e[Z]===void 0)},[R.blur_until_loaded,R.id,ue,e,ee,i,vt]);if(sr){const V=R.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,ge)=>(ve[ge]=e[ge]===void 0?"UNDEFINED":"LOADED",ve),{}),Z=Object.keys(e).filter(ve=>!ve.startsWith("_"));st.log(`[BLUR APPLIED] Component: ${R.id}, data_sources status:`,ce,"Available keys:",Z)}else st.log(`[BLUR APPLIED] Component: ${R.id}, blur_until_loaded:`,V)}const cn=H.useMemo(()=>{if(!R.blur_until_loaded)return{wrapperClasses:"",innerClasses:""};const V=_t?.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-)/],ce=V.split(/\s+/).filter(Boolean),Z=[],ve=[];for(const ge of ce)se.some(Ae=>Ae.test(ge))?Z.push(ge):ve.push(ge);return{wrapperClasses:Z.join(" "),innerClasses:ve.join(" ")}},[R.blur_until_loaded,_t?.className]),Tn=window.__slotContextValue;return ke&&Tn?.isEnabled?null:gt.jsx(G_,{componentId:o.id,componentName:o.name,children:R.blur_until_loaded?gt.jsx("div",{className:[cn.wrapperClasses,sr?"opacity-50 blur-sm transition-all duration-300 pointer-events-none":void 0].filter(Boolean).join(" ")||void 0,children:ra}):ra})});Ur.displayName="DynamicRenderer",typeof window<"u"&&(window.__DynamicRenderer=Ur);function V_(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 F_(o){try{return new URL(o,typeof window<"u"?window.location.origin:"http://localhost").pathname}catch{return o.split("?")[0]}}const wi=class wi{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 wi.instance||(wi.instance=new wi),wi.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],g=n?.[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 S=f===void 0?"__undefined__":JSON.stringify(f),v=g===void 0?"__undefined__":JSON.stringify(g);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 g of e)g.decision==="cache_hit"?n++: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=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(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 n)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,S)=>y+S,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: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=F_(e),c=V_(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 g=[];let m=!0;return f.name||(g.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||(g.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:g}});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"}`,g={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,g)}trackComputedRecalc(e,n,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:n,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,n){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 S=this.computedProperties.get(`computed-${m}-${n||"global"}`),v=[];if(S)for(const _ of S.dependencies)_.type==="computed"?v.push(f(_.path,y+1)):v.push({name:_.path,type:_.type,children:[],depth:y+1});return i.pop(),a.delete(m),{name:m,type:"computed",children:v,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()),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 g of this.computedRecalcLogs)e[g.trigger]++,g.valueChanged||l++,n[g.computedName]=(n[g.computedName]||0)+1,a[g.computedName]||(a[g.computedName]=[]),a[g.computedName].push(g.computationTime),i+=g.computationTime;const c=Object.entries(n).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,S)=>y+S,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 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(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 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,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(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 g=a.values[d];if(JSON.stringify(g)!==JSON.stringify(f)){const m=[],y=[];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"&&y.push(S.componentName));n.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 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 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: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 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 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: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(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 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,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 g=e[f],m=n[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(),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(g=>g.status==="success").length,i=e.filter(g=>g.status==="error").length,l=e.reduce((g,m)=>g+m.actions.length,0),c=n>0?e.reduce((g,m)=>g+m.totalDuration,0)/n: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: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[g,m]of c){const y=this.getValueByPath(a,g),S=d-m.capturedAt;if(!this.isDeepEqual(m.value,y)&&S>0){const v=this.createStaleClosureWarning(i,n,g,m.value,m.capturedAt,y,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,g,m){const y=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:y,description:S,suggestion:v,docLink:_,actionId:g,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],[,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: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}}};$(wi,"instance");let Oa=wi;const Kn=ht("LayoutLoader");class Er extends Error{constructor(n,a,i){super(n);$(this,"code");$(this,"details");this.code=a,this.details=i,this.name="LayoutLoaderError"}}class mf{constructor(e){$(this,"componentRegistry");$(this,"currentLayout",null);$(this,"layoutCache",new Map);$(this,"cacheVersion",0);this.componentRegistry=e}setCacheVersion(e){this.cacheVersion!==e&&(Kn.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)){Kn.log("Loading layout from cache:",n);const l=await this.layoutCache.get(a),c=JSON.parse(JSON.stringify(l));return this.currentLayout=c,Oa.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=Nr(`/api/layouts/preview/${S}`,"json",i)}else l=Nr(`/api/layouts/${e}/${n}`,"json",i);Kn.log("Fetching layout from API:",l);const c=Hr(),d=a?null:c.getToken(),f={Accept:"application/json"};d&&(f.Authorization=`Bearer ${d}`);const g=await Il(l,{init:{headers:f},label:`layout: ${n}`});let m;try{m=await g.json()}catch{m=null}if(!g.ok){if(g.status===401&&!a&&d){Kn.log("Token invalid, removing and retrying without token"),c.removeToken();try{return await this.fetchLayout(e,n,!0)}catch(v){throw v instanceof Er&&v.details?.status===401?new Er(v.message,v.code,{...v.details,hadToken:!0}):v}}const S=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:S})}const y=m.data||m;return this.validateLayoutData(y),this.currentLayout=y,Oa.getInstance().trackLayoutLoad(n,e,y,"api"),Kn.log("Layout fetched and cached successfully:",y.layout_name),y}catch(i){const l=`${e}:${n}`;throw this.layoutCache.delete(l),i instanceof Er?i:new Er("Failed to load layout","LOAD_FAILED",{originalError:i})}}prefetchLayout(e,n){return this.loadLayout(e,n)}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});Kn.log("Layout data validation passed")}renderLayout(e,n){try{const a=n||this.currentLayout;if(!a)throw new Er("No layout data to render","NO_LAYOUT_DATA");Kn.log("Rendering layout:",a.layout_name),e.innerHTML="",this.renderComponents(e,a.components),Kn.log("Layout rendered successfully")}catch(a){Kn.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 Kn.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 Kn.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=` + `},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 cc(){}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[...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{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({...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 uc(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]=uc(s,1);return e??null}function hf(s){return!oc||!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!oc||!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=uc(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 dc;(function(s){s[s.TreeOrder=0]="TreeOrder",s[s.ReversedTreeOrder=1]="ReversedTreeOrder"})(dc||(dc={}));function QE(s){let{acceleration:e,activator:n=To.Pointer,canScroll:a,draggingRect:i,enabled:l,interval:c=5,order:d=dc.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===dc.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 fc(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=fc({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:uc(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(!oc)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=fc({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=fc({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]=lc(c);return N.useMemo(()=>({nodeRef:d,rect:n,setRef:f}),[n,d,f])}const g_=[{sensor:vf,options:{}},{sensor:yf,options:{}}],m_={current:{}},hc={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:cc},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:hc,measureDroppableContainers:cc,windowRect:null,measuringScheduled:!1},b_={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:cc,draggableNodes:new Map,over:null,measureDroppableContainers:cc},pc=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(pc),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:{...hc.draggable,...s?.draggable},droppable:{...hc.droppable,...s?.droppable},dragOverlay:{...hc.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(pc.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(pc),{role:v=vy,roleDescription:_="draggable",tabIndex:A=0}=i??{},x=f?.id===e,L=N.useContext(x?by:x_),[M,k]=lc(),[O,B]=lc(),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(pc),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=fc({callback:M,disabled:_||!c}),O=N.useCallback((W,pe)=>{k&&(pe&&(k.unobserve(pe),b.current=!1),W&&k.observe(W))},[k]),[B,G]=lc(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 gc(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{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:ii&&c<=l?{x:-d.width-f,y:0,...mc}:c=l?{x:d.width+f,y:0,...mc}:{x:0,y:0,...mc}};function j_(s,e,n){const a=s[e],i=s[e-1],l=s[e+1];return!a||!i&&!l?0:n{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}},yc={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:nn&&i<=c?{x:0,y:-d.height-f,...yc}:i=c?{x:0,y:d.height+f,...yc}:{x:0,y:0,...yc}};function z_(s,e,n){const a=s[e],i=s[e-1],l=s[e+1];return a?na.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&&!_&&gc(S)&&gc(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=gc(S)&&gc(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 bc(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.topS.top&&f.push(b);break;case lt.Left:a.left>S.left&&f.push(b);break;case lt.Right:a.left1&&(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=uc(_).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!bc(s)||!bc(e)?!1:s.data.current.sortable.containerId===e.data.current.sortable.containerId}function J_(s,e){return!bc(s)||!bc(e)||!Ey(s,e)?!1:s.data.current.sortable.index{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&&(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},vc=(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=vc(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=>vc(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=>vc(Re,ke)),vc(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 bd(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=`

Component not found: ${e}

@@ -60,7 +63,7 @@ Error generating stack: `+u.message+`

Component Error: ${e}

${n?.message||"Unknown error"}

- `,a}renderErrorState(e,n){const a=n instanceof Error?n.message:String(n),i=n instanceof Er?n.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=`
페이지 새로고침
- `,Kn.error("Error state rendered:",i,a)}getCurrentLayout(){return this.currentLayout}clear(){this.currentLayout=null,this.layoutCache.clear(),Kn.log("Layout data and cache cleared")}}const K_=ht("TranslationContext"),yf=H.createContext(null),vc=({children:o,translationEngine:e,translationContext:n})=>{const a=H.useMemo(()=>(l,c)=>{if(c){const d="|"+Object.entries(c).map(([f,g])=>`${f}=${g}`).join("|");return e.translate(l,n,d)}return e.translate(l,n)},[e,n]),i=H.useMemo(()=>({translationEngine:e,translationContext:n,t:a}),[e,n,a]);return gt.jsx(yf.Provider,{value:i,children:o})},dy=()=>{const o=H.useContext(yf);return o||(K_.warn("TranslationProvider 외부에서 호출되었습니다. 키를 그대로 반환합니다."),{t:e=>e,translationEngine:null,translationContext:null})},La=ht("SlotContext"),W_={registerToSlot:()=>{La.warn("SlotContext not available: registerToSlot called outside SlotProvider")},unregisterFromSlot:()=>{La.warn("SlotContext not available: unregisterFromSlot called outside SlotProvider")},getSlotComponents:()=>[],subscribeToSlot:()=>()=>{},clearAllSlots:()=>{},isEnabled:!1},Y_=H.createContext(W_),bf=({children:o})=>{const e=H.useRef(new Map),n=H.useRef(new Map),a=H.useCallback(m=>{const y=n.current.get(m);y&&y.forEach(S=>{try{S()}catch(v){La.error(`Slot subscriber error (slotId: ${m}):`,v)}})},[]),i=H.useCallback((m,y,S)=>{if(!m||!y){La.warn("registerToSlot: slotId and componentId are required");return}e.current.has(m)||e.current.set(m,new Map);const v=e.current.get(m),_=v.get(y);_&&_.registrationKey===S.registrationKey||(v.set(y,S),La.log(`Component registered to slot: ${y} -> ${m} (order: ${S.order})`),a(m))},[a]),l=H.useCallback((m,y)=>{const S=e.current.get(m);S&&S.has(y)&&(S.delete(y),La.log(`Component unregistered from slot: ${y} <- ${m}`),S.size===0&&e.current.delete(m),a(m))},[a]),c=H.useCallback(m=>{const y=e.current.get(m);return y?Array.from(y.values()).sort((S,v)=>S.order-v.order):[]},[]),d=H.useCallback((m,y)=>{n.current.has(m)||n.current.set(m,new Set);const S=n.current.get(m);return S.add(y),()=>{S.delete(y),S.size===0&&n.current.delete(m)}},[]),f=H.useCallback(()=>{e.current.forEach((m,y)=>{a(y)}),e.current.clear(),n.current.clear(),La.log("All slots cleared")},[a]),g=H.useMemo(()=>({registerToSlot:i,unregisterFromSlot:l,getSlotComponents:c,subscribeToSlot:d,clearAllSlots:f,isEnabled:!0}),[i,l,c,d,f]);return H.useLayoutEffect(()=>{typeof window<"u"&&(window.__slotContextValue=g,La.log("SlotContext exposed to window.__slotContextValue"))},[g]),gt.jsx(Y_.Provider,{value:g,children:o})};class vf{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 fy{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 X_(o){try{return Reflect.construct(String,[],o),!0}catch{return!1}}class Sf extends vf{constructor(e,n,a){super(),this.name=n,this.pusher=e,this.options=a,this.eventFormatter=new fy(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 hy extends Sf{whisper(e,n){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,n),this}}class J_ extends Sf{whisper(e,n){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,n),this}}class Q_ extends hy{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 gy extends vf{constructor(e,n,a){super(),this.events={},this.listeners={},this.name=n,this.socket=e,this.options=a,this.eventFormatter=new fy(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 py extends gy{whisper(e,n){return this.socket.emit("client event",{channel:this.name,event:`client-${e}`,data:n}),this}}class Z_ extends py{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 Sc extends vf{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 my extends Sc{whisper(e,n){return this}}class eA extends Sc{whisper(e,n){return this}}class tA extends my{here(e){return this}joining(e){return this}whisper(e,n){return this}leaving(e){return this}}const Tc=class Tc{constructor(e){this.setOptions(e),this.connect()}setOptions(e){this.options={...Tc._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(){return typeof window<"u"&&window.Laravel?.csrfToken?window.Laravel.csrfToken:this.options.csrfToken?this.options.csrfToken:typeof document<"u"&&typeof document.querySelector=="function"?document.querySelector('meta[name="csrf-token"]')?.getAttribute("content")??null:null}};Tc._defaultOptions={auth:{headers:{}},authEndpoint:"/broadcasting/auth",userAuthentication:{endpoint:"/broadcasting/user-auth",headers:{}},csrfToken:null,bearerToken:null,host:null,key:null,namespace:"App.Events"};let ko=Tc;class wc extends ko{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 Sf(this.pusher,e,this.options)),this.channels[e]}privateChannel(e){return this.channels["private-"+e]||(this.channels["private-"+e]=new hy(this.pusher,"private-"+e,this.options)),this.channels["private-"+e]}encryptedPrivateChannel(e){return this.channels["private-encrypted-"+e]||(this.channels["private-encrypted-"+e]=new J_(this.pusher,"private-encrypted-"+e,this.options)),this.channels["private-encrypted-"+e]}presenceChannel(e){return this.channels["presence-"+e]||(this.channels["presence-"+e]=new Q_(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 nA extends ko{constructor(){super(...arguments),this.channels={}}connect(){const 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 gy(this.socket,e,this.options)),this.channels[e]}privateChannel(e){return this.channels["private-"+e]||(this.channels["private-"+e]=new py(this.socket,"private-"+e,this.options)),this.channels["private-"+e]}presenceChannel(e){return this.channels["presence-"+e]||(this.channels["presence-"+e]=new Z_(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 yy extends ko{constructor(){super(...arguments),this.channels={}}connect(){}listen(e,n,a){return new Sc}channel(e){return new Sc}privateChannel(e){return new my}encryptedPrivateChannel(e){return new eA}presenceChannel(e){return new tA}leave(e){}leaveChannel(e){}socketId(){return"fake-socket-id"}connectionStatus(){return"connected"}onConnectionChange(e){return()=>{}}disconnect(){}}class rA{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 wc({...this.options,cluster:""});else if(this.options.broadcaster==="pusher")this.connector=new wc(this.options);else if(this.options.broadcaster==="ably")this.connector=new wc({...this.options,cluster:"",broadcaster:"pusher"});else if(this.options.broadcaster==="socket.io")this.connector=new nA(this.options);else if(this.options.broadcaster==="null")this.connector=new yy(this.options);else if(typeof this.options.broadcaster=="function"&&X_(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 wc||e instanceof yy}socketId(){return this.connector.socketId()}connectionStatus(){return this.connector.connectionStatus()}registerInterceptors(){typeof Vue<"u"&&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 wf={exports:{}};var by;function aA(){return by||(by=1,(function(o,e){(function(a,i){o.exports=i()})(self,()=>(()=>{var n={594(c,d){var f=this&&this.__extends||(function(){var M=function(T,D){return M=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(z,P){z.__proto__=P}||function(z,P){for(var q in P)P.hasOwnProperty(q)&&(z[q]=P[q])},M(T,D)};return function(T,D){M(T,D);function z(){this.constructor=T}T.prototype=D===null?Object.create(D):(z.prototype=D.prototype,new z)}})();Object.defineProperty(d,"__esModule",{value:!0});var g=256,m=(function(){function M(T){T===void 0&&(T="="),this._paddingCharacter=T}return M.prototype.encodedLength=function(T){return this._paddingCharacter?(T+2)/3*4|0:(T*8+5)/6|0},M.prototype.encode=function(T){for(var D="",z=0;z>>18&63),D+=this._encodeByte(P>>>12&63),D+=this._encodeByte(P>>>6&63),D+=this._encodeByte(P>>>0&63)}var q=T.length-z;if(q>0){var P=T[z]<<16|(q===2?T[z+1]<<8:0);D+=this._encodeByte(P>>>18&63),D+=this._encodeByte(P>>>12&63),q===2?D+=this._encodeByte(P>>>6&63):D+=this._paddingCharacter||"",D+=this._paddingCharacter||""}return D},M.prototype.maxDecodedLength=function(T){return this._paddingCharacter?T/4*3|0:(T*6+7)/8|0},M.prototype.decodedLength=function(T){return this.maxDecodedLength(T.length-this._getPaddingLength(T))},M.prototype.decode=function(T){if(T.length===0)return new Uint8Array(0);for(var D=this._getPaddingLength(T),z=T.length-D,P=new Uint8Array(this.maxDecodedLength(z)),q=0,W=0,he=0,Se=0,be=0,Ue=0,Fe=0;W>>4,P[q++]=be<<4|Ue>>>2,P[q++]=Ue<<6|Fe,he|=Se&g,he|=be&g,he|=Ue&g,he|=Fe&g;if(W>>4,he|=Se&g,he|=be&g),W>>2,he|=Ue&g),W>>8&6,D+=51-T>>>8&-75,D+=61-T>>>8&-15,D+=62-T>>>8&3,String.fromCharCode(D)},M.prototype._decodeChar=function(T){var D=g;return D+=(42-T&T-44)>>>8&-g+T-43+62,D+=(46-T&T-48)>>>8&-g+T-47+63,D+=(47-T&T-58)>>>8&-g+T-48+52,D+=(64-T&T-91)>>>8&-g+T-65+0,D+=(96-T&T-123)>>>8&-g+T-97+26,D},M.prototype._getPaddingLength=function(T){var D=0;if(this._paddingCharacter){for(var z=T.length-1;z>=0&&T[z]===this._paddingCharacter;z--)D++;if(T.length<4||D>2)throw new Error("Base64Coder: incorrect padding")}return D},M})();d.Coder=m;var y=new m;function S(M){return y.encode(M)}d.encode=S;function v(M){return y.decode(M)}d.decode=v;var _=(function(M){f(T,M);function T(){return M!==null&&M.apply(this,arguments)||this}return T.prototype._encodeByte=function(D){var z=D;return z+=65,z+=25-D>>>8&6,z+=51-D>>>8&-75,z+=61-D>>>8&-13,z+=62-D>>>8&49,String.fromCharCode(z)},T.prototype._decodeChar=function(D){var z=g;return z+=(44-D&D-46)>>>8&-g+D-45+62,z+=(94-D&D-96)>>>8&-g+D-95+63,z+=(47-D&D-58)>>>8&-g+D-48+52,z+=(64-D&D-91)>>>8&-g+D-65+0,z+=(96-D&D-123)>>>8&-g+D-97+26,z},T})(m);d.URLSafeCoder=_;var A=new _;function x(M){return A.encode(M)}d.encodeURLSafe=x;function O(M){return A.decode(M)}d.decodeURLSafe=O,d.encodedLength=function(M){return y.encodedLength(M)},d.maxDecodedLength=function(M){return y.maxDecodedLength(M)},d.decodedLength=function(M){return y.decodedLength(M)}},978(c,d){var f="utf8: invalid source encoding";function g(m){for(var y=[],S=0;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],O=m[++S];if((A&192)!==128||(x&192)!==128||(O&192)!==128)throw new Error(f);v=(v&15)<<18|(A&63)<<12|(x&63)<<6|O&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,y.push(String.fromCharCode(55296|v>>10)),v=56320|v&1023}}y.push(String.fromCharCode(v))}return y.join("")}d.D4=g},721(c,d,f){c.exports=f(207).default},207(c,d,f){f.d(d,{default:()=>_s});class g{constructor(b,w){this.lastId=0,this.prefix=b,this.name=w}create(b){this.lastId++;var w=this.lastId,L=this.prefix+w,I=this.name+"["+w+"]",re=!1,me=function(){re||(b.apply(null,arguments),re=!0)};return this[w]=me,{number:w,id:L,name:I,callback:me}}remove(b){delete this[b.number]}}var m=new g("_pusher_script_","Pusher.ScriptReceivers"),y={VERSION:"8.6.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=y;class v{constructor(b){this.options=b,this.receivers=b.receivers||m,this.loading={}}load(b,w,L){var I=this;if(I.loading[b]&&I.loading[b].length>0)I.loading[b].push(L);else{I.loading[b]=[L];var re=ze.createScriptRequest(I.getPath(b,w)),me=I.receivers.create(function(Le){if(I.receivers.remove(me),I.loading[b]){var We=I.loading[b];delete I.loading[b];for(var dt=function(Pt){Pt||re.cleanup()},pt=0;pt>>6)+J(128|b&63):J(224|b>>>12&15)+J(128|b>>>6&63)+J(128|b&63)},te=function(E){return E.replace(/[^\x00-\x7F]/g,Me)},pe=function(E){var b=[0,2,1][E.length%3],w=E.charCodeAt(0)<<16|(E.length>1?E.charCodeAt(1):0)<<8|(E.length>2?E.charCodeAt(2):0),L=[ue.charAt(w>>>18),ue.charAt(w>>>12&63),b>=2?"=":ue.charAt(w>>>6&63),b>=1?"=":ue.charAt(w&63)];return L.join("")},N=typeof window<"u"&&window.btoa||function(E){return E.replace(/[\s\S]{1,3}/g,pe)};class R{constructor(b,w,L,I){this.clear=w,this.timer=b(()=>{this.timer&&(this.timer=I(this.timer))},L)}isRunning(){return this.timer!==null}ensureAborted(){this.timer&&(this.clear(this.timer),this.timer=null)}}const de=R;function fe(E){window.clearTimeout(E)}function xe(E){window.clearInterval(E)}class ee extends de{constructor(b,w){super(setTimeout,fe,b,function(L){return w(),null})}}class _e extends de{constructor(b,w){super(setInterval,xe,b,function(L){return w(),L})}}var Be={now(){return Date.now?Date.now():new Date().valueOf()},defer(E){return new ee(0,E)},method(E,...b){var w=Array.prototype.slice.call(arguments,1);return function(L){return L[E].apply(L,w.concat(arguments))}}};const ke=Be;function Ie(E,...b){for(var w=0;w{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,...w){var L=Ct.apply(this,arguments);_s.log?_s.log(L):_s.logToConsole&&b.bind(this)(L)}}const rt=new On;var Hn=function(E,b,w,L,I){(w.headers!==void 0||w.headersProvider!=null)&&rt.warn(`To send headers with the ${L.toString()} request, you must use AJAX, rather than JSONP.`);var re=E.nextAuthCallbackID.toString();E.nextAuthCallbackID++;var me=E.getDocument(),Le=me.createElement("script");E.auth_callbacks[re]=function(pt){I(null,pt)};var We="Pusher.auth_callbacks['"+re+"']";Le.src=w.endpoint+"?callback="+encodeURIComponent(We)+"&"+b;var dt=me.getElementsByTagName("head")[0]||me.documentElement;dt.insertBefore(Le,dt.firstChild)};const bn=Hn;class ra{constructor(b){this.src=b}send(b){var w=this,L="Error loading "+w.src;w.script=document.createElement("script"),w.script.id=b.id,w.script.src=w.src,w.script.type="text/javascript",w.script.charset="UTF-8",w.script.addEventListener?(w.script.onerror=function(){b.callback(L)},w.script.onload=function(){b.callback(null)}):w.script.onreadystatechange=function(){(w.script.readyState==="loaded"||w.script.readyState==="complete")&&b.callback(null)},w.script.async===void 0&&document.attachEvent&&/opera/i.test(navigator.userAgent)?(w.errorScript=document.createElement("script"),w.errorScript.id=b.id+"_error",w.errorScript.text=b.name+"('"+L+"');",w.script.async=w.errorScript.async=!1):w.script.async=!0;var I=document.getElementsByTagName("head")[0];I.insertBefore(w.script,I.firstChild),w.errorScript&&I.insertBefore(w.errorScript,w.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 sr{constructor(b,w){this.url=b,this.data=w}send(b){if(!this.request){var w=Ci(this.data),L=this.url+"/"+b.number+"?"+w;this.request=ze.createScriptRequest(L),this.request.send(b)}}cleanup(){this.request&&this.request.cleanup()}}var cn=function(E,b){return function(w,L){var I="http"+(b?"s":"")+"://",re=I+(E.host||E.options.host)+E.options.path,me=ze.createJSONPRequest(re,w),Le=ze.ScriptReceivers.create(function(We,dt){m.remove(Le),me.cleanup(),dt&&dt.host&&(E.host=dt.host),L&&L(We,dt)});me.send(Le)}},Tn={name:"jsonp",getAgent:cn};const un=Tn;function V(E,b,w){var L=E+(b.useTLS?"s":""),I=b.useTLS?b.hostTLS:b.hostNonTLS;return L+"://"+I+w}function se(E,b){var w="/app/"+E,L="?protocol="+S.PROTOCOL+"&client=js&version="+S.VERSION+(b?"&"+b:"");return w+L}var ce={getInitial:function(E,b){var w=(b.httpPath||"")+se(E,"flash=false");return V("ws",b,w)}},Z={getInitial:function(E,b){var w=(b.httpPath||"/pusher")+se(E);return V("http",b,w)}},ve={getInitial:function(E,b){return V("http",b,b.httpPath||"/pusher")},getPath:function(E,b){return se(E)}};class ge{constructor(){this._callbacks={}}get(b){return this._callbacks[Ce(b)]}add(b,w,L){var I=Ce(b);this._callbacks[I]=this._callbacks[I]||[],this._callbacks[I].push({fn:w,context:L})}remove(b,w,L){if(!b&&!w&&!L){this._callbacks={};return}var I=b?[Ce(b)]:vt(this._callbacks);w||L?this.removeCallback(I,w,L):this.removeAllCallbacks(I)}removeCallback(b,w,L){Oe(b,function(I){this._callbacks[I]=Mt(this._callbacks[I]||[],function(re){return w&&w!==re.fn||L&&L!==re.context}),this._callbacks[I].length===0&&delete this._callbacks[I]},this)}removeAllCallbacks(b){Oe(b,function(w){delete this._callbacks[w]},this)}}function Ce(E){return"_"+E}class Ae{constructor(b){this.callbacks=new ge,this.global_callbacks=[],this.failThrough=b}bind(b,w,L){return this.callbacks.add(b,w,L),this}bind_global(b){return this.global_callbacks.push(b),this}unbind(b,w,L){return this.callbacks.remove(b,w,L),this}unbind_global(b){return b?(this.global_callbacks=Mt(this.global_callbacks||[],w=>w!==b),this):(this.global_callbacks=[],this)}unbind_all(){return this.unbind(),this.unbind_global(),this}emit(b,w,L){for(var I=0;I0)for(var I=0;I{this.onError(w),this.changeState("closed")}),!1}return this.bindListeners(),rt.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"?(ke.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,w){this.state=b,this.timeline.info(this.buildTimelineMessage({state:b,params:w})),this.emit(b,w)}buildTimelineMessage(b){return Ie({cid:this.id},b)}}class Te{constructor(b){this.hooks=b}isSupported(b){return this.hooks.isSupported(b)}createConnection(b,w,L,I){return new Ke(this.hooks,b,w,L,I)}}var $e=new Te({urls:ce,handlesActivityChecks:!1,supportsPing:!1,isInitialized:function(){return!!ze.getWebSocketAPI()},isSupported:function(){return!!ze.getWebSocketAPI()},getSocket:function(E){return ze.createWebSocket(E)}}),Re={urls:Z,handlesActivityChecks:!1,supportsPing:!0,isInitialized:function(){return!0}},et=Ie({getSocket:function(E){return ze.HTTPFactory.createStreamingSocket(E)}},Re),je=Ie({getSocket:function(E){return ze.HTTPFactory.createPollingSocket(E)}},Re),Ve={isSupported:function(){return ze.isXHRSupported()}},we=new Te(Ie({},et,Ve)),He=new Te(Ie({},je,Ve)),tt={ws:$e,xhr_streaming:we,xhr_polling:He};const Tt=tt;var It=new Te({file:"sockjs",urls:ve,handlesActivityChecks:!0,supportsPing:!1,isSupported:function(){return!0},isInitialized:function(){return window.SockJS!==void 0},getSocket:function(E,b){return new window.SockJS(E,null,{js_path:A.getPath("sockjs",{useTLS:b.useTLS}),ignore_null_origin:b.ignoreNullOrigin})},beforeOpen:function(E,b){E.send(JSON.stringify({path:b}))}}),Ln={isSupported:function(E){var b=ze.isXDRSupported(E.useTLS);return b}},zt=new Te(Ie({},et,Ln)),Ut=new Te(Ie({},je,Ln));Tt.xdr_streaming=zt,Tt.xdr_polling=Ut,Tt.sockjs=It;const Rn=Tt;class $a extends Ae{constructor(){super();var b=this;typeof window<"u"&&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 jo=new $a;class Rf{constructor(b,w,L){this.manager=b,this.transport=w,this.minPingDelay=L.minPingDelay,this.maxPingDelay=L.maxPingDelay,this.pingDelay=void 0}createConnection(b,w,L,I){I=Ie({},I,{activityTimeout:this.pingDelay});var re=this.transport.createConnection(b,w,L,I),me=null,Le=function(){re.unbind("open",Le),re.bind("closed",We),me=ke.now()},We=dt=>{if(re.unbind("closed",We),dt.code===1002||dt.code===1003)this.manager.reportDeath();else if(!dt.wasClean&&me){var pt=ke.now()-me;pt<2*this.maxPingDelay&&(this.manager.reportDeath(),this.pingDelay=Math.max(pt/2,this.minPingDelay))}};return re.bind("open",Le),re}isSupported(b){return this.manager.isAlive()&&this.transport.isSupported(b)}}const kc={decodeMessage:function(E){try{var b=JSON.parse(E.data),w=b.data;if(typeof w=="string")try{w=JSON.parse(b.data)}catch{}var L={event:b.event,channel:b.channel,data:w};return b.user_id&&(L.user_id=b.user_id),L}catch(I){throw{type:"MessageParseError",error:I,data:E.data}}},encodeMessage:function(E){return JSON.stringify(E)},processHandshake:function(E){var b=kc.decodeMessage(E);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(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}},qr=kc;class Ei extends Ae{constructor(b,w){super(),this.id=b,this.transport=w,this.activityTimeout=w.activityTimeout,this.bindListeners()}handlesActivityChecks(){return this.transport.handlesActivityChecks()}send(b){return this.transport.send(b)}send_event(b,w,L){var I={event:b,data:w};return L&&(I.channel=L),rt.debug("Event sent",I),this.send(qr.encodeMessage(I))}ping(){this.transport.supportsPing()?this.transport.ping():this.send_event("pusher:ping",{})}close(){this.transport.close()}bindListeners(){var b={message:L=>{var I;try{I=qr.decodeMessage(L)}catch(re){this.emit("error",{type:"MessageParseError",error:re,data:L.data})}if(I!==void 0){switch(rt.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:L=>{this.emit("error",L)},closed:L=>{w(),L&&L.code&&this.handleCloseEvent(L),this.transport=null,this.emit("closed")}},w=()=>{Ot(b,(L,I)=>{this.transport.unbind(I,L)})};Ot(b,(L,I)=>{this.transport.bind(I,L)})}handleCloseEvent(b){var w=qr.getCloseAction(b),L=qr.getCloseError(b);L&&this.emit("error",L),w&&this.emit(w,{action:w,error:L})}}class zo{constructor(b,w){this.transport=b,this.callback=w,this.bindListeners()}close(){this.unbindListeners(),this.transport.close()}bindListeners(){this.onMessage=b=>{this.unbindListeners();var w;try{w=qr.processHandshake(b)}catch(L){this.finish("error",{error:L}),this.transport.close();return}w.action==="connected"?this.finish("connected",{connection:new Ei(w.id,this.transport),activityTimeout:w.activityTimeout}):(this.finish(w.action,{error:w.error}),this.transport.close())},this.onClosed=b=>{this.unbindListeners();var w=qr.getCloseAction(b)||"backoff",L=qr.getCloseError(b);this.finish(w,{error:L})},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,w){this.callback(Ie({transport:this.transport,action:b},w))}}class Na{constructor(b,w){this.timeline=b,this.options=w||{}}send(b,w){this.timeline.isEmpty()||this.timeline.send(ze.TimelineTransport.getAgent(this,b),w)}}class Gr extends Ae{constructor(b,w){super(function(L,I){rt.debug("No callbacks on "+b+" for "+L)}),this.name=b,this.pusher=w,this.subscribed=!1,this.subscriptionPending=!1,this.subscriptionCancelled=!1}authorize(b,w){return w(null,{auth:""})}trigger(b,w){if(b.indexOf("client-")!==0)throw new D("Event '"+b+"' does not start with 'client-'");if(!this.subscribed){var L=M.buildLogSuffix("triggeringClientEvents");rt.warn(`Client event triggered before channel 'subscription_succeeded' event . ${L}`)}return this.pusher.send_event(b,w,this.name)}disconnect(){this.subscribed=!1,this.subscriptionPending=!1}handleEvent(b){var w=b.event,L=b.data;if(w==="pusher_internal:subscription_succeeded")this.handleSubscriptionSucceededEvent(b);else if(w==="pusher_internal:subscription_count")this.handleSubscriptionCountEvent(b);else if(w.indexOf("pusher_internal:")!==0){var I={};this.emit(w,L,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,w)=>{b?(this.subscriptionPending=!1,rt.error(b.toString()),this.emit("pusher:subscription_error",Object.assign({},{type:"AuthError",error:b.message},b instanceof Ue?{status:b.status}:{}))):this.pusher.send_event("pusher:subscribe",{auth:w.auth,channel_data:w.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 aa extends Gr{authorize(b,w){return this.pusher.config.channelAuthorizer({channelName:this.name,socketId:b},w)}}class Ia{constructor(){this.reset()}get(b){return Object.prototype.hasOwnProperty.call(this.members,b)?{id:b,info:this.members[b]}:null}each(b){Ot(this.members,(w,L)=>{b(this.get(L))})}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 w=this.get(b.user_id);return w&&(delete this.members[b.user_id],this.count--),w}reset(){this.members={},this.count=0,this.myID=null,this.me=null}}var Zt=function(E,b,w,L){function I(re){return re instanceof w?re:new w(function(me){me(re)})}return new(w||(w=Promise))(function(re,me){function Le(pt){try{dt(L.next(pt))}catch(Pt){me(Pt)}}function We(pt){try{dt(L.throw(pt))}catch(Pt){me(Pt)}}function dt(pt){pt.done?re(pt.value):I(pt.value).then(Le,We)}dt((L=L.apply(E,b||[])).next())})};class Dc extends aa{constructor(b,w){super(b,w),this.members=new Ia}authorize(b,w){super.authorize(b,(L,I)=>Zt(this,void 0,void 0,function*(){if(!L)if(I=I,I.channel_data!=null){var re=JSON.parse(I.channel_data);this.members.setMyID(re.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");rt.error(`Invalid auth response for channel '${this.name}', expected 'channel_data' field. ${me}, or the user should be signed in.`),w("Invalid auth response");return}w(L,I)}))}handleEvent(b){var w=b.event;if(w.indexOf("pusher_internal:")===0)this.handleInternalEvent(b);else{var L=b.data,I={};b.user_id&&(I.user_id=b.user_id),this.emit(w,L,I)}}handleInternalEvent(b){var w=b.event,L=b.data;switch(w){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(L);this.emit("pusher:member_added",I);break;case"pusher_internal:member_removed":var re=this.members.removeMember(L);re&&this.emit("pusher:member_removed",re);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 Oc=f(978),Tr=f(594);class Ha extends aa{constructor(b,w,L){super(b,w),this.key=null,this.nacl=L}authorize(b,w){super.authorize(b,(L,I)=>{if(L){w(L,I);return}let re=I.shared_secret;if(!re){w(new Error(`No shared_secret key in auth payload for encrypted channel: ${this.name}`),null);return}this.key=(0,Tr.decode)(re),delete I.shared_secret,w(null,I)})}trigger(b,w){throw new he("Client events are not currently supported for encrypted channels")}handleEvent(b){var w=b.event,L=b.data;if(w.indexOf("pusher_internal:")===0||w.indexOf("pusher:")===0){super.handleEvent(b);return}this.handleEncryptedEvent(w,L)}handleEncryptedEvent(b,w){if(!this.key){rt.debug("Received encrypted event before key has been retrieved from the authEndpoint");return}if(!w.ciphertext||!w.nonce){rt.error("Unexpected format for encrypted event, expected object with `ciphertext` and `nonce` fields, got: "+w);return}let L=(0,Tr.decode)(w.ciphertext);if(L.length{if(me){rt.error(`Failed to make a request to the authEndpoint: ${Le}. Unable to fetch new key, so dropping encrypted event`);return}if(re=this.nacl.secretbox.open(L,I,this.key),re===null){rt.error("Failed to decrypt event with new key. Dropping encrypted event");return}this.emit(b,this.getDataToEmit(re))});return}this.emit(b,this.getDataToEmit(re))}getDataToEmit(b){let w=(0,Oc.D4)(b);try{return JSON.parse(w)}catch{return w}}}class kf extends Ae{constructor(b,w){super(),this.state="initialized",this.connection=null,this.key=b,this.options=w,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 L=ze.getNetwork();L.bind("online",()=>{this.timeline.info({netinfo:"online"}),(this.state==="connecting"||this.state==="unavailable")&&this.retryIn(0)}),L.bind("offline",()=>{this.timeline.info({netinfo:"offline"}),this.connection&&this.sendActivityCheck()}),this.updateStrategy()}switchCluster(b){this.key=b,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(b){return this.connection?this.connection.send(b):!1}send_event(b,w,L){return this.connection?this.connection.send_event(b,w,L):!1}disconnect(){this.disconnectInternally(),this.updateState("disconnected")}isUsingTLS(){return this.usingTLS}startConnecting(){var b=(w,L)=>{w?this.runner=this.strategy.connect(0,b):L.action==="error"?(this.emit("error",{type:"HandshakeError",error:L.error}),this.timeline.error({handshakeError:L.error})):(this.abortConnecting(),this.handshakeCallbacks[L.action](L))};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 ee(b||0,()=>{this.disconnectInternally(),this.connect()})}clearRetryTimer(){this.retryTimer&&(this.retryTimer.ensureAborted(),this.retryTimer=null)}setUnavailableTimer(){this.unavailableTimer=new ee(this.options.unavailableTimeout,()=>{this.updateState("unavailable")})}clearUnavailableTimer(){this.unavailableTimer&&this.unavailableTimer.ensureAborted()}sendActivityCheck(){this.stopActivityCheck(),this.connection.ping(),this.activityTimer=new ee(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 ee(this.activityTimeout,()=>{this.sendActivityCheck()}))}stopActivityCheck(){this.activityTimer&&this.activityTimer.ensureAborted()}buildConnectionCallbacks(b){return Ie({},b,{message:w=>{this.resetActivityCheck(),this.emit("message",w)},ping:()=>{this.send_event("pusher:pong",{})},activity:()=>{this.resetActivityCheck()},error:w=>{this.emit("error",w)},closed:()=>{this.abandonConnection(),this.shouldRetry()&&this.retryIn(1e3)}})}buildHandshakeCallbacks(b){return Ie({},b,{connected:w=>{this.activityTimeout=Math.min(this.options.activityTimeout,w.activityTimeout,w.connection.activityTimeout||1/0),this.clearUnavailableTimer(),this.setConnection(w.connection),this.socket_id=this.connection.id,this.updateState("connected",{socket_id:this.socket_id})}})}buildErrorCallbacks(){let b=w=>L=>{L.error&&this.emit("error",{type:"WebSocketError",error:L.error}),w(L)};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 w in this.connectionCallbacks)this.connection.bind(w,this.connectionCallbacks[w]);this.resetActivityCheck()}abandonConnection(){if(this.connection){this.stopActivityCheck();for(var b in this.connectionCallbacks)this.connection.unbind(b,this.connectionCallbacks[b]);var w=this.connection;return this.connection=null,w}}updateState(b,w){var L=this.state;if(this.state=b,L!==b){var I=b;I==="connected"&&(I+=" with new socket ID "+w.socket_id),rt.debug("State changed",L+" -> "+I),this.timeline.info({state:b,params:w}),this.emit("state_change",{previous:L,current:b}),this.emit(b,w)}}shouldRetry(){return this.state==="connecting"||this.state==="connected"}}class Lc{constructor(){this.channels={}}add(b,w){return this.channels[b]||(this.channels[b]=Mc(b,w)),this.channels[b]}all(){return _t(this.channels)}find(b){return this.channels[b]}remove(b){var w=this.channels[b];return delete this.channels[b],w}disconnect(){Ot(this.channels,function(b){b.disconnect()})}}function Mc(E,b){if(E.indexOf("private-encrypted-")===0){if(b.config.nacl)return jn.createEncryptedChannel(E,b,b.config.nacl);let w="Tried to subscribe to a private-encrypted- channel but no nacl implementation available",L=M.buildLogSuffix("encryptedChannelSupport");throw new he(`${w}. ${L}`)}else{if(E.indexOf("private-")===0)return jn.createPrivateChannel(E,b);if(E.indexOf("presence-")===0)return jn.createPresenceChannel(E,b);if(E.indexOf("#")===0)throw new z('Cannot create a channel with name "'+E+'".');return jn.createChannel(E,b)}}var Df={createChannels(){return new Lc},createConnectionManager(E,b){return new kf(E,b)},createChannel(E,b){return new Gr(E,b)},createPrivateChannel(E,b){return new aa(E,b)},createPresenceChannel(E,b){return new Dc(E,b)},createEncryptedChannel(E,b,w){return new Ha(E,b,w)},createTimelineSender(E,b){return new Na(E,b)},createHandshake(E,b){return new zo(E,b)},createAssistantToTheTransportManager(E,b,w){return new Rf(E,b,w)}};const jn=Df;class _i{constructor(b){this.options=b||{},this.livesLeft=this.options.lives||1/0}getAssistant(b){return jn.createAssistantToTheTransportManager(this,b,{minPingDelay:this.options.minPingDelay,maxPingDelay:this.options.maxPingDelay})}isAlive(){return this.livesLeft>0}reportDeath(){this.livesLeft-=1}}class vn{constructor(b,w){this.strategies=b,this.loop=!!w.loop,this.failFast=!!w.failFast,this.timeout=w.timeout,this.timeoutLimit=w.timeoutLimit}isSupported(){return ln(this.strategies,ke.method("isSupported"))}connect(b,w){var L=this.strategies,I=0,re=this.timeout,me=null,Le=(We,dt)=>{dt?w(null,dt):(I=I+1,this.loop&&(I=I%L.length),I0&&(re=new ee(L.timeout,function(){me.abort(),I(!0)})),me=b.connect(w,function(Le,We){Le&&re&&re.isRunning()&&!L.failFast||(re&&re.ensureAborted(),I(Le,We))}),{abort:function(){re&&re.ensureAborted(),me.abort()},forceMinPriority:function(Le){me.forceMinPriority(Le)}}}}class kn{constructor(b){this.strategies=b}isSupported(){return ln(this.strategies,ke.method("isSupported"))}connect(b,w){return $c(this.strategies,b,function(L,I){return function(re,me){if(I[L].error=re,re){Of(I)&&w(!0);return}Oe(I,function(Le){Le.forceMinPriority(me.transport.priority)}),w(null,me)}})}}function $c(E,b,w){var L=nt(E,function(I,re,me,Le){return I.connect(b,w(re,Le))});return{abort:function(){Oe(L,Uo)},forceMinPriority:function(I){Oe(L,function(re){re.forceMinPriority(I)})}}}function Of(E){return Br(E,function(b){return!!b.error})}function Uo(E){!E.error&&!E.aborted&&(E.abort(),E.aborted=!0)}class Nc{constructor(b,w,L){this.strategy=b,this.transports=w,this.ttl=L.ttl||18e5,this.usingTLS=L.useTLS,this.timeline=L.timeline}isSupported(){return this.strategy.isSupported()}connect(b,w){var L=this.usingTLS,I=Lf(L),re=I&&I.cacheSkipCount?I.cacheSkipCount:0,me=[this.strategy];if(I&&I.timestamp+this.ttl>=ke.now()){var Le=this.transports[I.transport];Le&&(["ws","wss"].includes(I.transport)||re>3?(this.timeline.info({cached:!0,transport:I.transport,latency:I.latency}),me.push(new vn([Le],{timeout:I.latency*2+1e3,failFast:!0}))):re++)}var We=ke.now(),dt=me.pop().connect(b,function pt(Pt,Or){Pt?(vs(L),me.length>0?(We=ke.now(),dt=me.pop().connect(b,pt)):w(Pt)):(zn(L,Or.transport.name,ke.now()-We,re),w(null,Or))});return{abort:function(){dt.abort()},forceMinPriority:function(pt){b=pt,dt&&dt.forceMinPriority(pt)}}}}function ja(E){return"pusherTransport"+(E?"TLS":"NonTLS")}function Lf(E){var b=ze.getLocalStorage();if(b)try{var w=b[ja(E)];if(w)return JSON.parse(w)}catch{vs(E)}return null}function zn(E,b,w,L){var I=ze.getLocalStorage();if(I)try{I[ja(E)]=yn({timestamp:ke.now(),transport:b,latency:w,cacheSkipCount:L})}catch{}}function vs(E){var b=ze.getLocalStorage();if(b)try{delete b[ja(E)]}catch{}}class Ai{constructor(b,{delay:w}){this.strategy=b,this.options={delay:w}}isSupported(){return this.strategy.isSupported()}connect(b,w){var L=this.strategy,I,re=new ee(this.options.delay,function(){I=L.connect(b,w)});return{abort:function(){re.ensureAborted(),I&&I.abort()},forceMinPriority:function(me){b=me,I&&I.forceMinPriority(me)}}}}class ia{constructor(b,w,L){this.test=b,this.trueBranch=w,this.falseBranch=L}isSupported(){var b=this.test()?this.trueBranch:this.falseBranch;return b.isSupported()}connect(b,w){var L=this.test()?this.trueBranch:this.falseBranch;return L.connect(b,w)}}class za{constructor(b){this.strategy=b}isSupported(){return this.strategy.isSupported()}connect(b,w){var L=this.strategy.connect(b,function(I,re){re&&L.abort(),w(I,re)});return L}}function Ua(E){return function(){return E.isSupported()}}var Ic=function(E,b,w){var L={};function I(el,Xc,Jc,Zf,eh){var Qc=w(E,el,Xc,Jc,Zf,eh);return L[el]=Qc,Qc}var re=Object.assign({},b,{hostNonTLS:E.wsHost+":"+E.wsPort,hostTLS:E.wsHost+":"+E.wssPort,httpPath:E.wsPath}),me=Object.assign({},re,{useTLS:!0}),Le=Object.assign({},b,{hostNonTLS:E.httpHost+":"+E.httpPort,hostTLS:E.httpHost+":"+E.httpsPort,httpPath:E.httpPath}),We={loop:!0,timeout:15e3,timeoutLimit:6e4},dt=new _i({minPingDelay:1e4,maxPingDelay:E.activityTimeout}),pt=new _i({lives:2,minPingDelay:1e4,maxPingDelay:E.activityTimeout}),Pt=I("ws","ws",3,re,dt),Or=I("wss","ws",3,me,dt),Xf=I("sockjs","sockjs",1,Le),Wc=I("xhr_streaming","xhr_streaming",1,Le,pt),Jf=I("xdr_streaming","xdr_streaming",1,Le,pt),Jo=I("xhr_polling","xhr_polling",1,Le),Yc=I("xdr_polling","xdr_polling",1,Le),qa=new vn([Pt],We),Oi=new vn([Or],We),Qf=new vn([Xf],We),Li=new vn([new ia(Ua(Wc),Wc,Jf)],We),Qo=new vn([new ia(Ua(Jo),Jo,Yc)],We),Zo=new vn([new ia(Ua(Li),new kn([Li,new Ai(Qo,{delay:4e3})]),Qo)],We),Mi=new ia(Ua(Zo),Zo,Qf),$i;return b.useTLS?$i=new kn([qa,new Ai(Mi,{delay:2e3})]):$i=new kn([qa,new Ai(Oi,{delay:2e3}),new Ai(Mi,{delay:5e3})]),new Nc(new za(new ia(Ua(Pt),$i,Mi)),L,{ttl:18e5,timeline:b.timeline,useTLS:b.useTLS})};const Pa=Ic;function Mf(){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(b,w){E.hooks.isInitialized()?(E.changeState("initialized"),w(!0)):(b&&E.onError(b),E.onClose(),w(!1))})):E.onClose()}var Hc={getRequest:function(E){var b=new window.XDomainRequest;return b.ontimeout=function(){E.emit("error",new P),E.close()},b.onerror=function(w){E.emit("error",w),E.close()},b.onprogress=function(){b.responseText&&b.responseText.length>0&&E.onChunk(200,b.responseText)},b.onload=function(){b.responseText&&b.responseText.length>0&&E.onChunk(200,b.responseText),E.emit("finished",200),E.close()},b},abortRequest:function(E){E.ontimeout=E.onerror=E.onprogress=E.onload=null,E.abort()}};const jc=Hc,Po=256*1024;class $f extends Ae{constructor(b,w,L){super(),this.hooks=b,this.method=w,this.url=L}start(b){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(b)}close(){this.unloader&&(ze.removeUnloadListener(this.unloader),this.unloader=null),this.xhr&&(this.hooks.abortRequest(this.xhr),this.xhr=null)}onChunk(b,w){for(;;){var L=this.advanceBuffer(w);if(L)this.emit("chunk",{status:b,data:L});else break}this.isBufferTooLong(w)&&this.emit("buffer_too_long")}advanceBuffer(b){var w=b.slice(this.position),L=w.indexOf(` -`);return L!==-1?(this.position+=L+1,w.slice(0,L)):null}isBufferTooLong(b){return this.position===b.length&&b.length>Po}}var Bo;(function(E){E[E.CONNECTING=0]="CONNECTING",E[E.OPEN=1]="OPEN",E[E.CLOSED=3]="CLOSED"})(Bo||(Bo={}));const or=Bo;var Rr=1;class qo{constructor(b,w){this.hooks=b,this.session=Ss(1e3)+"/"+Uc(8),this.location=Go(w),this.readyState=or.CONNECTING,this.openStream()}send(b){return this.sendRaw(JSON.stringify([b]))}ping(){this.hooks.sendHeartbeat(this)}close(b,w){this.onClose(b,w,!0)}sendRaw(b){if(this.readyState===or.OPEN)try{return ze.createSocketRequest("POST",sa(Ba(this.location,this.session))).start(b),!0}catch{return!1}else return!1}reconnect(){this.closeStream(),this.openStream()}onClose(b,w,L){this.closeStream(),this.readyState=or.CLOSED,this.onclose&&this.onclose({code:b,reason:w,wasClean:L})}onChunk(b){if(b.status===200){this.readyState===or.OPEN&&this.onActivity();var w,L=b.data.slice(0,1);switch(L){case"o":w=JSON.parse(b.data.slice(1)||"{}"),this.onOpen(w);break;case"a":w=JSON.parse(b.data.slice(1)||"[]");for(var I=0;I{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){ke.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 Go(E){var b=/([^\?]*)\/*(\??.*)/.exec(E);return{base:b[1],queryString:b[2]}}function Ba(E,b){return E.base+"/"+b+"/xhr_send"}function sa(E){var b=E.indexOf("?")===-1?"?":"&";return E+b+"t="+ +new Date+"&n="+Rr++}function zc(E,b){var w=/(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(E);return w[1]+b+w[3]}function Ss(E){return ze.randomInt(E)}function Uc(E){for(var b=[],w=0;w0&&E.onChunk(w.status,w.responseText);break;case 4:w.responseText&&w.responseText.length>0&&E.onChunk(w.status,w.responseText),E.emit("finished",w.status),E.close();break}},w},abortRequest:function(E){E.onreadystatechange=null,E.abort()}};const ws=Fo;var Pc={createStreamingSocket(E){return this.createSocket(Vo,E)},createPollingSocket(E){return this.createSocket(Vr,E)},createSocket(E,b){return new xi(E,b)},createXHR(E,b){return this.createRequest(ws,E,b)},createRequest(E,b,w){return new $f(E,b,w)}};const Ri=Pc;Ri.createXDR=function(E,b){return this.createRequest(jc,E,b)};var Bc={nextAuthCallbackID:1,auth_callbacks:{},ScriptReceivers:m,DependenciesReceivers:_,getDefaultStrategy:Pa,Transports:Rn,transportConnectionInitializer:Mf,HTTPFactory:Ri,TimelineTransport:un,getXHRAPI(){return window.XMLHttpRequest},getWebSocketAPI(){return window.WebSocket||window.MozWebSocket},setup(E){if(typeof window<"u"){window.Pusher=E;var b=()=>{this.onDocumentBody(E.ready)};window.JSON?b():A.load("json2",{},b)}},getDocument(){return document},getProtocol(){return this.getDocument().location.protocol},getAuthorizers(){return{ajax:Ge,jsonp:bn}},onDocumentBody(E){document.body?E():setTimeout(()=>{this.onDocumentBody(E)},0)},createJSONPRequest(E,b){return new sr(E,b)},createScriptRequest(E){return new ra(E)},getLocalStorage(){try{return window.localStorage}catch{return}},createXHR(){return this.getXHRAPI()?this.createXMLHttpRequest():this.createMicrosoftXHR()},createXMLHttpRequest(){var E=this.getXHRAPI();return new E},createMicrosoftXHR(){return new ActiveXObject("Microsoft.XMLHTTP")},getNetwork(){return jo},createWebSocket(E){var b=this.getWebSocketAPI();return new b(E)},createSocketRequest(E,b){if(this.isXHRSupported())return this.HTTPFactory.createXHR(E,b);if(this.isXDRSupported(b.indexOf("https:")===0))return this.HTTPFactory.createXDR(E,b);throw"Cross-origin HTTP requests are not supported"},isXHRSupported(){var E=this.getXHRAPI();return!!E&&new E().withCredentials!==void 0},isXDRSupported(E){var b=E?"https:":"http:",w=this.getProtocol();return!!window.XDomainRequest&&w===b},addUnloadListener(E){window.addEventListener!==void 0?window.addEventListener("pagehide",E,!1):window.attachEvent!==void 0&&window.attachEvent("onunload",E)},removeUnloadListener(E){window.addEventListener!==void 0?window.removeEventListener("pagehide",E,!1):window.detachEvent!==void 0&&window.detachEvent("onunload",E)},randomInt(E){const b=window.crypto||window.msCrypto,w=Math.floor(Math.pow(2,32)/E)*E;let L;do L=b.getRandomValues(new Uint32Array(1))[0];while(L>=w);return L%E}};const ze=Bc;var Dr;(function(E){E[E.ERROR=3]="ERROR",E[E.INFO=6]="INFO",E[E.DEBUG=7]="DEBUG"})(Dr||(Dr={}));const oa=Dr;class ki{constructor(b,w,L){this.key=b,this.session=w,this.events=[],this.options=L||{},this.sent=0,this.uniqueID=0}log(b,w){b<=this.options.level&&(this.events.push(Ie({},w,{timestamp:ke.now()})),this.options.limit&&this.events.length>this.options.limit&&this.events.shift())}error(b){this.log(oa.ERROR,b)}info(b){this.log(oa.INFO,b)}debug(b){this.log(oa.DEBUG,b)}isEmpty(){return this.events.length===0}send(b,w){var L=Ie({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(L,(I,re)=>{I||this.sent++,w&&w(I,re)}),!0}generateUniqueID(){return this.uniqueID++,this.uniqueID}}class Nf{constructor(b,w,L,I){this.name=b,this.priority=w,this.transport=L,this.options=I||{}}isSupported(){return this.transport.isSupported({useTLS:this.options.useTLS})}connect(b,w){if(this.isSupported()){if(this.priority{L||(pt(),re?re.close():I.close())},forceMinPriority:Pt=>{L||this.priority{var w="socket_id="+encodeURIComponent(E.socketId);for(var L in b.params)w+="&"+encodeURIComponent(L)+"="+encodeURIComponent(b.params[L]);if(b.paramsProvider!=null){let I=b.paramsProvider();for(var L in I)w+="&"+encodeURIComponent(L)+"="+encodeURIComponent(I[L])}return w},Hf=E=>{if(typeof ze.getAuthorizers()[E.transport]>"u")throw`'${E.transport}' is not a recognized auth transport`;return(b,w)=>{const L=If(b,E);ze.getAuthorizers()[E.transport](ze,L,E,T.UserAuthentication,w)}},Yo=(E,b)=>{var w="socket_id="+encodeURIComponent(E.socketId);w+="&channel_name="+encodeURIComponent(E.channelName);for(var L in b.params)w+="&"+encodeURIComponent(L)+"="+encodeURIComponent(b.params[L]);if(b.paramsProvider!=null){let I=b.paramsProvider();for(var L in I)w+="&"+encodeURIComponent(L)+"="+encodeURIComponent(I[L])}return w},jf=E=>{if(typeof ze.getAuthorizers()[E.transport]>"u")throw`'${E.transport}' is not a recognized auth transport`;return(b,w)=>{const L=Yo(b,E);ze.getAuthorizers()[E.transport](ze,L,E,T.ChannelAuthorization,w)}},zf=(E,b,w)=>{const L={authTransport:b.transport,authEndpoint:b.endpoint,auth:{params:b.params,headers:b.headers}};return(I,re)=>{const me=E.channel(I.channelName);w(me,L).authorize(I.socketId,re)}};function Gc(E,b){let w={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:qf(E),httpHost:Uf(E),useTLS:Bf(E),wsHost:Vc(E),userAuthenticator:Xo(E),channelAuthorizer:Vf(E,b)};return"disabledTransports"in E&&(w.disabledTransports=E.disabledTransports),"enabledTransports"in E&&(w.enabledTransports=E.enabledTransports),"ignoreNullOrigin"in E&&(w.ignoreNullOrigin=E.ignoreNullOrigin),"timelineParams"in E&&(w.timelineParams=E.timelineParams),"nacl"in E&&(w.nacl=E.nacl),w}function Uf(E){return E.httpHost?E.httpHost:E.cluster?`sockjs-${E.cluster}.pusher.com`:S.httpHost}function Vc(E){return E.wsHost?E.wsHost:Pf(E.cluster)}function Pf(E){return`ws-${E}.pusher.com`}function Bf(E){return ze.getProtocol()==="https:"?!0:E.forceTLS!==!1}function qf(E){return"enableStats"in E?E.enableStats:"disableStats"in E?!E.disableStats:!1}const Fc=E=>"customHandler"in E&&E.customHandler!=null;function Xo(E){const b=Object.assign(Object.assign({},S.userAuthentication),E.userAuthentication);return Fc(b)?b.customHandler:Hf(b)}function Gf(E,b){let w;return"channelAuthorization"in E?w=Object.assign(Object.assign({},S.channelAuthorization),E.channelAuthorization):(w={transport:E.authTransport||S.authTransport,endpoint:E.authEndpoint||S.authEndpoint},"auth"in E&&("params"in E.auth&&(w.params=E.auth.params),"headers"in E.auth&&(w.headers=E.auth.headers)),"authorizer"in E&&(w.customHandler=zf(b,w,E.authorizer))),w}function Vf(E,b){const w=Gf(E,b);return Fc(w)?w.customHandler:jf(w)}class Ff extends Ae{constructor(b){super(function(w,L){rt.debug(`No callbacks on watchlist events for ${w}`)}),this.pusher=b,this.bindWatchlistInternalEvent()}handleEvent(b){b.data.events.forEach(w=>{this.emit(w.name,w)})}bindWatchlistInternalEvent(){this.pusher.connection.bind("message",b=>{var w=b.event;w==="pusher_internal:watchlist_events"&&this.handleEvent(b)})}}function Kc(){let E,b;return{promise:new Promise((L,I)=>{E=L,b=I}),resolve:E,reject:b}}const Kf=Kc;class Wf extends Ae{constructor(b){super(function(w,L){rt.debug("No callbacks on user for "+w)}),this.signin_requested=!1,this.user_data=null,this.serverToUserChannel=null,this.signinDonePromise=null,this._signinDoneResolve=null,this._onAuthorize=(w,L)=>{if(w){rt.warn(`Error during signin: ${w}`),this.emit("pusher:signin_error",Object.assign({},{type:"AuthError",error:w.message},w instanceof Ue?{status:w.status}:{})),this._cleanup();return}this.pusher.send_event("pusher:signin",{auth:L.auth,user_data:L.user_data})},this.pusher=b,this.pusher.connection.bind("state_change",({previous:w,current:L})=>{w!=="connected"&&L==="connected"&&this._signin(),w==="connected"&&L!=="connected"&&(this._cleanup(),this._newSigninPromiseIfNeeded())}),this.watchlist=new Ff(b),this.pusher.connection.bind("message",w=>{var L=w.event;L==="pusher:signin_success"&&this._onSigninSuccess(w.data),this.serverToUserChannel&&this.serverToUserChannel.name===w.channel&&this.serverToUserChannel.handleEvent(w)})}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{rt.error(`Failed parsing user data after signin: ${b.user_data}`),this._cleanup();return}if(typeof this.user_data.id!="string"||this.user_data.id===""){rt.error(`user_data doesn't contain an id. user_data: ${this.user_data}`),this._cleanup();return}this._signinDoneResolve(),this._subscribeChannels()}_subscribeChannels(){const b=w=>{w.subscriptionPending&&w.subscriptionCancelled?w.reinstateSubscription():!w.subscriptionPending&&this.pusher.connection.state==="connected"&&w.subscribe()};this.serverToUserChannel=new Gr(`#server-to-user-${this.user_data.id}`,this.pusher),this.serverToUserChannel.bind_global((w,L)=>{w.indexOf("pusher_internal:")===0||w.indexOf("pusher:")===0||this.emit(w,L)}),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:w}=Kf();b.done=!1;const L=()=>{b.done=!0};b.then(L).catch(L),this.signinDonePromise=b,this._signinDoneResolve=w}}class dn{static ready(){dn.isReady=!0;for(var b=0,w=dn.instances.length;bze.getDefaultStrategy(this.config,I,Di);this.connection=jn.createConnectionManager(this.key,{getStrategy:L,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 re=I.event,me=re.indexOf("pusher_internal:")===0;if(I.channel){var Le=this.channel(I.channel);Le&&Le.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=>{rt.warn(I)}),dn.instances.push(this),this.timeline.info({instances:dn.instances.length}),this.user=new Wf(this),dn.isReady&&this.connect()}switchCluster(b){const{appKey:w,cluster:L}=b;this.key=w,this.options=Object.assign(Object.assign({},this.options),{cluster:L}),this.config=Gc(this.options,this),this.connection.switchCluster(this.key)}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(),w=this.timelineSender;this.timelineSenderTimer=new _e(6e4,function(){w.send(b)})}}disconnect(){this.connection.disconnect(),this.timelineSenderTimer&&(this.timelineSenderTimer.ensureAborted(),this.timelineSenderTimer=null)}bind(b,w,L){return this.global_emitter.bind(b,w,L),this}unbind(b,w,L){return this.global_emitter.unbind(b,w,L),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 w=this.channels.add(b,this);return w.subscriptionPending&&w.subscriptionCancelled?w.reinstateSubscription():!w.subscriptionPending&&this.connection.state==="connected"&&w.subscribe(),w}unsubscribe(b){var w=this.channels.find(b);w&&w.subscriptionPending?w.cancelSubscription():(w=this.channels.remove(b),w&&w.subscribed&&w.unsubscribe())}send_event(b,w,L){return this.connection.send_event(b,w,L)}shouldUseTLS(){return this.config.useTLS}signin(){this.user.signin()}}dn.instances=[],dn.isReady=!1,dn.logToConsole=!1,dn.Runtime=ze,dn.ScriptReceivers=ze.ScriptReceivers,dn.DependenciesReceivers=ze.DependenciesReceivers,dn.auth_callbacks=ze.auth_callbacks;const _s=dn;function Yf(E){if(E==null)throw"You must pass your app key when you instantiate Pusher."}ze.setup(dn)}},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})())})(wf)),wf.exports}var iA=aA();const vy=Jr(iA),qt=ht("WebSocketManager");class sA{constructor(){$(this,"echo",null);$(this,"subscriptions",new Map);$(this,"initialized",!1);$(this,"config",null)}configure(e){if(this.initialized){qt.warn("[WebSocketManager] 이미 초기화되어 설정을 변경할 수 없습니다.");return}this.config=e,qt.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){qt.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";qt.log("[WebSocketManager] 연결 설정:",{host:n,port:c,scheme:i,useTLS:d,authEndpoint:l}),window.Pusher=vy;const f=localStorage.getItem("auth_token"),g={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"}}};qt.log("[WebSocketManager] Pusher 옵션:",g);const m=new vy(e,g);m.connection.bind("connecting",()=>{qt.log("[WebSocketManager] 연결 시도 중...")}),m.connection.bind("connected",()=>{qt.log("[WebSocketManager] 연결 성공! Socket ID:",m.connection.socket_id)}),m.connection.bind("failed",()=>{qt.error("[WebSocketManager] 연결 실패 - WebSocket을 사용할 수 없습니다.")}),m.connection.bind("error",y=>{qt.error("[WebSocketManager] 연결 오류:",y)}),m.connection.bind("state_change",y=>{qt.log(`[WebSocketManager] 상태 변경: ${y.previous} → ${y.current}`)}),m.connection.bind("unavailable",()=>{qt.error("[WebSocketManager] WebSocket 사용 불가 - 연결할 수 없습니다.")}),m.connection.bind("disconnected",()=>{qt.warn("[WebSocketManager] 연결이 끊어졌습니다.")}),qt.log("[WebSocketManager] 초기 연결 상태:",m.connection.state),this.echo=new rA({broadcaster:"reverb",client:m}),qt.log("[WebSocketManager] 연결 시작 시도..."),m.connect(),window.Echo=this.echo,this.initialized=!0,qt.log("[WebSocketManager] Echo 초기화 완료")}subscribe(e,n,a,i={}){if(this.initialize(),!this.echo)return qt.warn("[WebSocketManager] Echo가 초기화되지 않았습니다."),"";const{channelType:l="private"}=i,c=`${e}:${n}`;if(this.subscriptions.has(c))return qt.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),qt.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){qt.warn(`[WebSocketManager] stopListening 실패: ${e}`,l)}this.subscriptions.delete(e),qt.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)),qt.log(`[WebSocketManager] 채널 구독 해제: ${e}`)}disconnect(){this.echo&&(this.echo.disconnect(),this.subscriptions.clear(),this.initialized=!1,qt.log("[WebSocketManager] 연결 종료"))}getEcho(){return this.echo}isInitialized(){return this.initialized}isConfigured(){return this.config!==null&&!!this.config.appKey}getSubscriptionCount(){return this.subscriptions.size}}const Cc=new sA,bt=ht("DataSourceManager"),Sy=new Dn;function Ec(o,e){const n=Aa(o);if(n!==null)try{return Vn(n)?Sy.evaluatePipeExpression(n,e,{skipCache:!0}):Sy.evaluateExpression(n,e)}catch(a){return bt.error("Failed to evaluate param expression:",n,a),o}return rs(o,e,{skipCache:!0})}function wy(o){const e={};for(const n of o.keys()){if(n in e)continue;const a=o.getAll(n);a.length>1?e[n]=a:a.length===1&&(n.endsWith("[]")?e[n]=a:e[n]=a[0])}return e}function oA(){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 Dn}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=Ec(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}ns({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(ns({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(g=>(d||g.auto_fetch!==!1)&&g.type!=="websocket");return await Promise.all(f.map(async g=>{try{const m=await this.fetchDataSource(g,n,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 S={status:y,message:`Error condition matched for ${g.id}`,data:m},v=Sr(),_=v.resolve(y,{errorHandling:g.errorHandling,onError:g.onError});if(_.handler)try{await v.execute(_.handler,S)}catch(A){bt.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()}),bt.log(`Using fallback data for ${g.id} (errorCondition)`));return}if(g.onSuccess)try{const S={data:m,sourceId:g.id};await this.executeOnSuccessHandler(g.onSuccess,S)}catch(S){bt.error(`Error executing onSuccess handler for ${g.id}:`,S)}}catch(m){bt.error(`Failed to fetch data source: ${g.id}`,m);const y=m?.response?.status||m?.status||500,S=m?.response?.data,v=S?.message||m.message||"Unknown error",_={status:y,message:v,errors:S?.errors,data:S,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,_)}catch(O){bt.error(`Error executing error handler for ${g.id}:`,O)}g.fallback!==void 0&&(c[g.id]=g.fallback,this.dataCache.set(g.id,{data:g.fallback,timestamp:Date.now()}),bt.log(`Using fallback data for ${g.id}`)),this.options.onError&&this.options.onError(m,g)}})),c}async fetchDataSourcesWithResults(e,n={},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(", ");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 y=await this.fetchDataSource(m,n,a,i,l);this.dataCache.set(m.id,{data:y,timestamp:Date.now()});const S=this.checkErrorCondition(m,y);if(S!==null){const v={status:S,message:`Error condition matched for ${m.id}`,data:y},_=Sr(),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(O=>{bt.error(`Error executing errorCondition handler for ${m.id}:`,O)}),x=!0;else try{await _.execute(A.handler,v),x=!0}catch(O){bt.error(`Error executing errorCondition handler for ${m.id}:`,O)}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:y,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:y}}catch(y){bt.error(`Failed to fetch data source: ${m.id}`,y);const S=y?.response?.status||y?.status||500,v=y?.response?.data,_=v?.message||y.message||"Unknown error",A={status:S,message:_,errors:v?.errors,data:v,statusText:y?.response?.statusText},x=Sr(),O=x.resolve(S,{errorHandling:m.errorHandling,onError:m.onError});let M=!1;if(O.handler)if(m.loading_strategy==="blocking")x.execute(O.handler,A).catch(T=>{bt.error(`Error executing error handler for ${m.id}:`,T)}),M=!0;else try{await x.execute(O.handler,A),M=!0}catch(T){bt.error(`Error executing error handler for ${m.id}:`,T)}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(y,m),{id:m.id,state:"error",error:y,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 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(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 g={data:d,sourceId:c.id};await this.executeOnSuccessHandler(c.onSuccess,g)}catch(g){bt.error(`Error executing onSuccess handler for ${c.id}:`,g)}i&&i(c.id,d)}).catch(async d=>{bt.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},S=Sr(),v=S.resolve(f,{errorHandling:c.errorHandling,onError:c.onError});if(v.handler)try{await S.execute(v.handler,y)}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 wy(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:wy(a)};i&&(c._global=i),l&&(c._local=l);let d=rs(e.endpoint,c,{skipCache:!0});const f=e.method||"GET",m=(e.contentType?typeof e.contentType=="string"&&e.contentType.includes("{{")?rs(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(([_,A])=>{typeof A=="string"&&(y[_]=Ec(A,c))}),Object.keys(y).forEach(_=>{y[_]===""&&delete y[_]});const S=oA();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(y).length>0){const A=new URLSearchParams;Object.entries(y).forEach(([O,M])=>{Array.isArray(M)?M.forEach(T=>A.append(O,String(T))):M!=null&&A.append(O,String(M))});const x=A.toString();_=x?`${d}?${x}`:d}v=S.trackRequest(_,f,{requestBody:f!=="GET"?y:void 0,dataSourceId:e.id}),S.trackDataSourceLoading(e.id)}try{let _;const A=e.auth_mode??(e.auth_required===!0?"required":"none"),x=br.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 O=A==="required"||A==="optional"&&x;if(O){const M=Hr(),T=d.startsWith("/api/")?d.substring(4):d.startsWith("/api")?d.substring(4)||"/":d;this.options.onError&&bt.log(`[DataSourceManager] Normalized endpoint: ${d} -> ${T}`);const D=this.getMatchingGlobalHeaders(d,c),z={};e.headers&&Object.entries(e.headers).forEach(([he,Se])=>{const be=Ec(Se,c);be!=null&&be!==""&&(z[he]=String(be))});const P={...D,...z},q=Object.keys(P).length>0,W=m?this.toFormData(y):y;switch(f){case"GET":_=await M.get(T,{params:y,...q&&{headers:P}});break;case"POST":q?_=await M.post(T,W,{headers:P}):_=await M.post(T,W);break;case"PUT":q?_=await M.put(T,W,{headers:P}):_=await M.put(T,W);break;case"PATCH":q?_=await M.patch(T,W,{headers:P}):_=await M.patch(T,W);break;case"DELETE":_=await M.delete(T,{params:y,...q&&{headers:P}});break;default:throw new Error(`Unsupported HTTP method: ${f}`)}}else{let M=d;if(f==="GET"){const W=new URLSearchParams(y).toString();M=W?`${d}?${W}`:d}const T={...m?{}:{"Content-Type":"application/json"},Accept:"application/json"};if(typeof window<"u"){const W=localStorage.getItem("g7_locale");W&&(T["Accept-Language"]=W)}const D=this.getMatchingGlobalHeaders(d,c);Object.assign(T,D),e.headers&&Object.entries(e.headers).forEach(([W,he])=>{const Se=Ec(he,c);Se!=null&&Se!==""&&(T[W]=String(Se))});let z;f!=="GET"&&(m?(z=this.toFormData(y),delete T["Content-Type"]):z=JSON.stringify(y));const P=await fetch(M,{method:f,headers:T,body:z});let q;try{q=await P.json()}catch{q=null}if(v&&S?.isEnabled()&&(S.completeRequest(v,P.status,q),v=null),!P.ok){S?.isEnabled()&&S.trackDataSourceError(e.id,`HTTP ${P.status}: ${P.statusText}`);const W=new Error(`HTTP error! status: ${P.status}`);throw W.response={status:P.status,statusText:P.statusText,data:q},W.status=P.status,W}_=q,S?.isEnabled()&&S.trackDataSourceLoaded(e.id,_)}return O&&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=rs(c.channel,a,{skipCache:!0}),f=rs(c.event,a,{skipCache:!0})}catch(y){bt.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)){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=Cc.subscribe(d,f,y=>{const S=c.target_source||c.id;this.dataCache.set(S,{data:y,timestamp:Date.now()}),n(S,y)},{channelType:c.channel_type||"private"});m&&i.push(m)}),i}unsubscribeWebSockets(e){e.length!==0&&e.forEach(n=>{Cc.unsubscribe(n)})}async executeOnSuccessHandler(e,n){const a=this.options.actionDispatcher??vm();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 _r=Rc;const lA=new _r,bi=ht("ModalDataSourceWrapper");function cA(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 Cy=({isOpen:o,modalId:e,dataSources:n,dataContext:a,globalStateUpdater:i,bindingEngine:l,debug:c=!1,children:d})=>{const[f,g]=H.useState(!1),[m,y]=H.useState(!1),S=H.useRef(!1),v=H.useRef(null);v.current||(v.current=new _r);const _=H.useCallback(async()=>{if(!(!n||n.length===0)&&v.current){g(!0);try{const A=a.route||{},x=new URLSearchParams(window.location.search),O=n.map(T=>{let D={...T};if(T.endpoint&&l){const z=l.resolveBindings(T.endpoint,a,{skipCache:!0});D.endpoint=z}if(T.params&&l){const z={};Object.entries(T.params).forEach(([P,q])=>{typeof q=="string"?z[P]=l.resolveBindings(q,a,{skipCache:!0}):z[P]=q}),D.params=z}return D});c&&bi.log(`[ModalDataSourceWrapper] Modal "${e}" fetching data sources:`,O.map(T=>({id:T.id,endpoint:T.endpoint})));const M=await v.current.fetchDataSourcesWithResults(O,A,x);i&&M.forEach(T=>{if(T.state==="success"&&T.data!==void 0){const D=O.find(z=>z.id===T.id);if(D?.initGlobal){const z=T.data?.data??T.data,P=Array.isArray(D.initGlobal)?D.initGlobal:[D.initGlobal];for(const q of P)if(typeof q=="string")i({[q]:z}),c&&bi.log(`[ModalDataSourceWrapper] Modal "${e}" initGlobal: ${T.id}.data -> _global.${q}`);else if(typeof q=="object"&&q.key){const{key:W,path:he}=q,Se=he?cA(z,he):z;i({[W]:Se}),c&&bi.log(`Modal "${e}" initGlobal: ${T.id}.data${he?"."+he:""} -> _global.${W}`)}}}else T.state==="error"&&bi.error(`Modal "${e}" data source error:`,T.id,T.error)}),c&&bi.log(`Modal "${e}" data sources fetched successfully`)}catch(A){bi.error(`Modal "${e}" data source fetch error:`,A)}finally{g(!1),y(!0)}}},[n,a,i,l,e,c]);return H.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]),H.useEffect(()=>{!o&&S.current&&(y(!1),c&&bi.log(`[ModalDataSourceWrapper] Modal "${e}" closed, reset fetch state`)),o&&!S.current&&n&&n.length>0&&!m&&_(),S.current=o},[o,n,m,_,e,c]),gt.jsx(gt.Fragment,{children:d})};class _c 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 uA extends _c{constructor(){super("TEMPLATE_NOT_FOUND","$t:core.errors.template_not_found",{showStack:!1,icon:"fa-paint-brush",recoverable:!1}),this.name="TemplateNotFoundError"}}function Ey(o){if(o instanceof _c)return o;if(o instanceof Error){const e=new _c("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 _c("TEMPLATE_INIT_ERROR","$t:core.errors.template_init_error",{showStack:!1,icon:"fa-circle-exclamation",recoverable:!0})}const dA=ht("ErrorDisplay");class Cf{static render(e,n){const a=document.getElementById(e);if(!a){dA.error(`Container #${e} not found`);return}const i=n.debug&&n.showStack&&n.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),Sc=({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 wc 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 wc{whisper(e,n){return this}}class gA extends wc{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 Cc 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 wc}channel(e){return new wc}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 Cc({...this.options,cluster:""});else if(this.options.broadcaster==="pusher")this.connector=new Cc(this.options);else if(this.options.broadcaster==="ably")this.connector=new Cc({...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 Cc||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>>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>>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>>4,pe|=Se&h,pe|=we&h),W>>2,pe|=Ue&h),W>>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)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>>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{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;I0)for(var I=0;I{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 Rc={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=Rc.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=Rc;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 Dc 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 Oc=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{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,Oc.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 Lc{constructor(){this.channels={}}add(y,C){return this.channels[y]||(this.channels[y]=Mc(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 Mc(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 Lc},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 Dc(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),I0&&(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 $c(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 $c(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 Nc{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 Ic=function(E,y,C){var D={};function I(el,Xc,Jc,uh,dh){var Qc=C(E,el,Xc,Jc,uh,dh);return D[el]=Qc,Qc}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),Wc=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),Yc=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(Wc),Wc,lh)],Ke),Qo=new yn([new oa(Ba(Jo),Jo,Yc)],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 Nc(new Pa(new oa(Ba(zt),Ii,Ni)),D,{ttl:18e5,timeline:y.timeline,useTLS:y.useTLS})};const qa=Ic;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 jc={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 Hc=jc,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)+"/"+Uc(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{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 zc(E,y){var C=/(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(E);return C[1]+y+C[3]}function Ss(E){return ze.randomInt(E)}function Uc(E){for(var y=[],C=0;C0&&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 Pc={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=Pc;Di.createXDR=function(E,y){return this.createRequest(Hc,E,y)};var Bc={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=Bc;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{D||(gt(),ne?ne.close():I.close())},forceMinPriority:zt=>{D||this.priority{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 Gc(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:Vc(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 Vc(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 Fc=E=>"customHandler"in E&&E.customHandler!=null;function Xo(E){const y=Object.assign(Object.assign({},S.userAuthentication),E.userAuthentication);return Fc(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 Fc(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 Kc(){let E,y;return{promise:new Promise((D,I)=>{E=D,y=I}),resolve:E,reject:y}}const ah=Kc;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;yze.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=Gc(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 Ec=new wA,bt=dt("DataSourceManager"),Ny=new Tn;function _c(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 kc=class kc{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=_c(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[_]=_c(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=_c(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=_c(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>kc.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=Ec.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=>{Ec.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}}};$(kc,"DATA_CACHE_TTL",3e5);let xr=kc;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 Ac 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 Ac{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 Ac)return s;if(s instanceof Error){const e=new Ac("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 Ac("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; @@ -224,20 +227,4 @@ Error generating stack: `+u.message+` `:""} - `}static renderFromError(e,n,a,i=!1,l,c,d){let f,g;if(n.userMessageKey.startsWith("$t:"))if(c&&l)try{f=c.translate(n.userMessageKey,l)}catch{f=n.userMessageKey.replace("$t:","")}else f=n.userMessageKey.replace("$t:","");else f=n.userMessageKey;if(d){const m=d;m.details?.apiMessage?g=m.details.apiMessage:m.response?.data?.message&&(g=m.response.data.message)}this.render(e,{title:a,message:f,detailMessage:g,icon:n.icon,showStack:n.showStack,stackTrace:n.stack,showReloadButton:n.recoverable,debug:i})}static escapeHtml(e){if(!e)return"";const n=document.createElement("div");return n.textContent=e,n.innerHTML}}const ps=ht("Router");class fA{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(Nr(`/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,ps.log(`Loaded ${this.routes.length} routes${e?` (v=${e})`:""}`);else throw new Error("Invalid routes data format")}catch(n){throw ps.error("Error loading routes:",n),n}}setRoutes(e){this.routes=e,ps.log(`Set ${this.routes.length} routes`)}match(e){const n=this.normalizePathname(e);for(const a of this.routes){const i=this.matchPattern(a.path,n);if(i!==null)return{route:a,params:i}}return null}normalizePathname(e){return e.length>1&&e.endsWith("/")?e.replace(/\/+$/,"")||"/":e}matchPattern(e,n){const a=[];let i=e.replace(/:([^/]+)/g,(f,g)=>(a.push(g),"([^/]+)"));i.startsWith("*/")&&(i="(?:/[^/]+)?"+i.slice(1)),i=i.replace(/\//g,"\\/");const l=new RegExp(`^${i}$`),c=n.match(l);if(!c)return null;const d={};return a.forEach((f,g)=>{d[f]=c[g+1]}),d}getRoutes(){return[...this.routes]}getAuthType(e,n){return e.auth_type?e.auth_type:n.startsWith("/admin")?"admin":"user"}async navigateToCurrentPath(){const e=window.location.pathname,n=window.location.search,a=this.match(e);if(!a){ps.warn(`No route matched for path: ${e}`),this.emit("routeNotFound",e);return}if(a.route.redirect){ps.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+n);ps.log(`Not authenticated, redirecting to: ${g}`),window.location.href=g;return}}const i=new URLSearchParams(n),l={};for(const c of i.keys()){if(c in l)continue;const d=i.getAll(c);d.length>1?l[c]=d:d.length===1&&(c.endsWith("[]")?l[c]=d:l[c]=d[0])}await this.emit("routeChange",{path:e,layout:a.route.layout,endpoint:a.route.endpoint,params:{...a.route.params||{},...a.params},query:l,auth_required:a.route.auth_required,auth_type:a.route.auth_type,meta:a.route.meta})}navigate(e){if(this.isNavigating){this.pendingNavigation=e;return}this.executeNavigation(e)}async executeNavigation(e){this.isNavigating=!0,this.pendingNavigation=null;try{window.history.pushState({},"",e),await this.navigateToCurrentPath()}finally{if(this.isNavigating=!1,this.pendingNavigation){const n=this.pendingNavigation;this.executeNavigation(n)}}}}const hA=/^\/admin\/layout-editor\/([^/?#]+)\/?$/;function _y(o){const e=hA.exec(o);return e?{templateIdentifier:e[1]}:null}const Gt=ht("ErrorPageHandler");class gA{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&&Gt.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&&Gt.warn("Invalid template config response"),this.configLoaded=!0,null;const a=n.data;return!a.error_config||!a.error_config.layouts?(this.debug&&Gt.warn("No error_config found in template.json"),this.configLoaded=!0,null):(this.errorConfig=a.error_config,this.configLoaded=!0,this.debug&&Gt.log("Error config loaded:",this.errorConfig),this.errorConfig)}catch(e){return Gt.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&&Gt.warn("No error config available, cannot render error page"),!1;const i=a.layouts[e]||a.layouts[String(e)];if(!i)return this.debug&&Gt.warn(`No layout defined for error code: ${e}`),!1;this.debug&&Gt.log(`Loading error layout: ${i} for code: ${e}`);const l=await this.layoutLoader.loadLayout(this.templateId,i);if(!l)return this.debug&&Gt.error(`Failed to load error layout: ${i}`),!1;this.debug&&Gt.log("Error layout loaded:",l);let c={};const d=l.data_sources||[];if(d.length>0){this.debug&&Gt.log("Fetching data sources:",d.map(v=>v.id));const m=d.filter(v=>v.loading_strategy==="blocking"),y=d.filter(v=>!v.loading_strategy||v.loading_strategy==="progressive"),S=[...m,...y];if(S.length>0)try{c=await this.dataSourceManager.fetchDataSources(S,{},new URLSearchParams),this.debug&&Gt.log("Data sources fetched:",Object.keys(c))}catch(v){Gt.error("Failed to fetch data sources:",v)}}const f={};d.length>0&&(this.processInitOptions(d,c,f),this.debug&&Gt.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:n,layoutJson:l,dataContext:g,translationContext:{templateId:this.templateId,locale:this.locale}}),this.debug&&Gt.log(`Error page ${e} rendered successfully`),!0}catch(a){return Gt.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,Gt.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,Gt.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,Gt.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,Gt.log(`initGlobal: ${i.id}.data${m?"."+m:""} -> _global.${g}`)}}}}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&&Gt.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&&Gt.log("Config cache cleared")}}const jt=ht("ModuleAssetLoader");class pA{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){jt.log("No module assets to load");return}const n=[...e].sort((d,f)=>d.priority-f.priority);jt.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){jt.warn(`Some module assets failed to load (${c.length}/${l.length}); continuing with the rest`,this.getFailedJsAssets());return}jt.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){jt.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)){jt.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=()=>{jt.log(`Bundle CSS loaded: ${e}`),this.registerLoadedAsset(`bundle-${e}`,{type:"css",element:l}),i()},l.onerror=()=>{jt.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)){jt.log(`Bundle JS already loaded: ${e}`);return}const i=this.loadingPromises.get(a);if(i)return jt.log(`Bundle JS already loading: ${e}`),i;const l=cp(n,{id:a},{label:`bundle JS: ${e}`}).then(()=>{jt.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 jt.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)){jt.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=()=>{jt.log(`CSS loaded: ${e}`),this.registerLoadedAsset(e,{type:"css",element:c}),i()},c.onerror=()=>{jt.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)){jt.log(`JS already loaded: ${e}`);return}const i=this.loadingPromises.get(e);if(i)return jt.log(`JS already loading: ${e}`),i;const l=cp(n,{id:a},{label:`JS: ${e}`}).then(()=>{jt.log(`JS loaded: ${e}`);const c=document.getElementById(a);c&&this.registerLoadedAsset(e,{type:"js",element:c}),this.loadingPromises.delete(e)}).catch(c=>{throw jt.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){jt.log(`No assets to unload for: ${e}`);return}n.forEach(a=>{a.element.parentNode&&(a.element.parentNode.removeChild(a.element),jt.log(`${a.type.toUpperCase()} unloaded: ${e}`))}),this.loadedAssets.delete(e),jt.log(`All assets unloaded for: ${e}`)}unloadAllAssets(){Array.from(this.loadedAssets.keys()).forEach(n=>{this.unloadExtensionAsset(n)}),jt.log("All module assets unloaded")}isLoaded(e){return this.loadedAssets.has(e)}getLoadedModules(){return Array.from(this.loadedAssets.keys())}}let Ef=null;function _f(){return Ef||(Ef=new pA),Ef}function mA(){if(typeof window>"u")return[];const o=window.G7Config;if(!o?.moduleAssets)return[];const e=[];for(const[n,a]of Object.entries(o.moduleAssets)){const i=a;e.push({identifier:n,js:i.js?Ea(i.js):i.js,css:i.css?Ea(i.css):i.css,priority:i.priority,external:i.external})}return e}function yA(){if(typeof window>"u")return null;const o=window.G7Config;if(!o?.bundleUrls)return null;const e=o.bundleUrls;return{moduleJs:e.moduleJs?Ea(e.moduleJs):e.moduleJs,moduleCss:e.moduleCss?Ea(e.moduleCss):e.moduleCss,pluginJs:e.pluginJs?Ea(e.pluginJs):e.pluginJs,pluginCss:e.pluginCss?Ea(e.pluginCss):e.pluginCss}}function bA(){if(typeof window>"u")return[];const o=window.G7Config;if(!o?.pluginAssets)return[];const e=[];for(const[n,a]of Object.entries(o.pluginAssets)){const i=a;e.push({identifier:n,js:i.js?Ea(i.js):i.js,css:i.css?Ea(i.css):i.css,priority:i.priority,external:i.external})}return e}const Af=ht("SystemBannerManager");class Ac{static show(e){this.banners.set(e.id,e),this.render(),Af.log(`Banner shown: ${e.id}`)}static hide(e){this.banners.delete(e)&&(this.render(),Af.log(`Banner hidden: ${e}`))}static hideAll(){this.banners.clear(),this.render(),Af.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`
${l}
`}).join(""),requestAnimationFrame(()=>{e&&this.adjustAppPadding(e.offsetHeight)})}static adjustAppPadding(e){const n=document.getElementById("app");n&&(n.style.paddingTop=e>0?`${e}px`:"")}}$(Ac,"banners",new Map),$(Ac,"containerId","g7-system-banners");const B=ht("TemplateApp");function Ay(o){const e={};for(const n of o.keys()){if(n in e)continue;const a=o.getAll(n);a.length>1?e[n]=a:a.length===1&&(n.endsWith("[]")?e[n]=a:e[n]=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 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,B.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,B.log("Loaded plugin settings from G7Config:",Object.keys(e.plugins))),e.modules&&(this.globalState.modules=e.modules,B.log("Loaded module settings from G7Config:",Object.keys(e.modules))),e.appConfig&&(this.globalState.appConfig=e.appConfig,B.log("Loaded appConfig from G7Config:",Object.keys(e.appConfig)))}}resolveRouteExpressions(e){const n=new Dn,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("{{")&&(B.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("{{")&&(B.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(xr.LOCALE_STORAGE_KEY)&&(localStorage.setItem(xr.LOCALE_STORAGE_KEY,a),B.log(`Migrated locale from '${n}' to '${xr.LOCALE_STORAGE_KEY}'`)),localStorage.removeItem(n)}catch(a){B.warn(`Failed to migrate locale key '${n}':`,a)}}async init(){try{to.getInstance().setDebug(this.config.debug),B.log("Initializing with config:",this.config),rw();const e=mr.getInstance(),n=br.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(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 B.error("Error loading routes:",A),A}),n.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=>(B.warn("Error loading template config:",A),null))]);if(B.log("Template Engine initialized"),B.log("ComponentRegistry loaded"),B.log("Routes data loaded"),B.log("User info preloaded"),B.log("Template config loaded:",f),f?.cache_version!==void 0){const A=this.loadCacheVersionFromStorage();if(this.extensionCacheVersion=f.cache_version,this.saveCacheVersionToStorage(this.extensionCacheVersion),B.log("Extension cache version:",this.extensionCacheVersion),A!==null&&A!==this.extensionCacheVersion){B.log("Cache version changed, reloading routes...");const x=await Il(Nr(`/api/templates/${this.config.templateId}/routes`,"json",this.extensionCacheVersion),{label:"routes.json (reload)"}).then(O=>{if(!O.ok)throw new Error(`Failed to reload routes: ${O.statusText}`);return O.json()}).then(O=>{if(!O.success)throw new Error("Failed to reload routes from API");return O.data});Array.isArray(x.routes)&&(x.routes=this.resolveRouteExpressions(x.routes),Object.assign(c,x),B.log("Routes reloaded with new cache version"));try{const{TranslationEngine:O}=await Promise.resolve().then(()=>gp),M=O.getInstance();M.setCacheVersion(this.extensionCacheVersion);const T=this.config.locale||"ko",D="en";await M.loadTranslations(this.config.templateId,T,"/api",!0),T!==D&&await M.loadTranslations(this.config.templateId,D,"/api",!0),B.log("Translations reloaded with new cache version")}catch(O){B.error("Failed to reload translations:",O)}}}f?.errorHandling&&(this.templateErrorHandling=f.errorHandling,Sr().setTemplateConfig(this.templateErrorHandling),B.log("Template errorHandling registered:",this.templateErrorHandling)),n.on("logout",()=>{B.log("User logged out")}),n.on("authStateChange",A=>{B.log("Auth state changed:",A)}),B.log("AuthManager event handlers registered");const g=Hr(),m=this.config.templateType==="admin"?"admin":"user",y=n.getConfig(m);g.setOnUnauthorized(()=>{B.log("Unauthorized - redirecting to login page"),g.removeToken();const A=window.location.pathname+window.location.search,x=n.getLoginRedirectUrl(m,A,"session_expired");window.location.href=x}),B.log("ApiClient onUnauthorized callback registered"),this.layoutLoader=new mf(e),this.extensionCacheVersion>0&&this.layoutLoader.setCacheVersion(this.extensionCacheVersion),B.log("LayoutLoader initialized:",this.layoutLoader);const S=new _r({onUnauthorized:()=>{B.warn("Unauthorized request in error page")}});if(this.errorPageHandler=new gA({templateId:this.config.templateId,layoutLoader:this.layoutLoader,locale:this.config.locale,debug:this.config.debug,renderFunction:ms,dataSourceManager:S,globalState:this.globalState}),B.log("ErrorPageHandler initialized with DataSourceManager"),await this.handleServerError()){B.log("Server error handled, skipping normal initialization");return}if(this.router=new fA(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");B.log("Router initialized:",this.router),B.log("Routes set:",this.router.getRoutes());const{getActionDispatcher:v}=await Promise.resolve().then(()=>Pr),_=v();if(_&&(_.setDefaultContext({navigate:A=>this.router?.navigate(A)}),_.setGlobalStateUpdater((A,x)=>this.setGlobalState(A,x)),B.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"&&_y(window.location.pathname)){B.log("Layout editor mode detected — skipping router match"),await ms({containerId:"app",layoutJson:{components:[]},dataContext:{},translationContext:{templateId:this.config.templateId,locale:this.config.locale}}),B.log("Template App initialized in layout editor mode");return}this.router.navigateToCurrentPath(),B.log("Template App initialized successfully")}catch(e){B.error("Initialization failed:",e),this.showInitError(e)}}async loadExtensionAssets(){try{const e=_f(),n=yA();if(!n){await this.loadExtensionAssetsIndividually();return}await e.loadBundle("module",n.moduleJs,n.moduleCss),await e.loadBundle("plugin",n.pluginJs,n.pluginCss),B.log("Extension bundle assets loaded successfully")}catch(e){B.warn("Failed to load extension assets:",e)}}async loadExtensionAssetsIndividually(){const e=_f(),n=mA();n.length>0&&(B.log("Loading module assets (individual fallback):",n.map(i=>i.identifier)),await e.loadActiveExtensionAssets(n));const a=bA();a.length>0&&(B.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(B.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){B.log("Route change cancelled (newer request exists):",n);return}B.log("Layout loaded:",l);const c=Sr();l.errorHandling?(c.setLayoutConfig(l.errorHandling),B.log("Layout errorHandling registered:",l.errorHandling)):c.clearLayoutConfig();const d=l.data_sources||[],f=e.query||{},g=new URLSearchParams;for(const[te,pe]of Object.entries(f))if(Array.isArray(pe))for(const N of pe)g.append(te,N);else g.set(te,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:S}=await Promise.resolve().then(()=>Pr),v=new y;if(i){const te=S();te&&te.setPreviewMode(!0),this.setGlobalState({__isPreview:!0}),Ac.show({id:"preview-mode",message:{ko:"⚠ 미리보기 모드 — 페이지 이동이 비활성화됩니다",en:"⚠ Preview Mode — Navigation is disabled"},background:"linear-gradient(90deg, #f59e0b, #d97706)",color:"white"}),B.log("Preview mode activated")}else{const te=S();te?.isPreviewMode()&&(te.setPreviewMode(!1),Ac.hide("preview-mode"))}if(this.currentGlobalHeaders=l.globalHeaders||[],this.currentGlobalHeaders.length>0){v.setGlobalHeaders(this.currentGlobalHeaders);const te=S();te&&te.setGlobalHeaders(this.currentGlobalHeaders),B.log("globalHeaders set:",this.currentGlobalHeaders.map(pe=>pe.pattern))}if(l.named_actions&&Object.keys(l.named_actions).length>0){const te=S();te&&te.setNamedActions(l.named_actions)}const _=v.filterByCondition(d,m);d.length!==_.length&&B.log("Data sources filtered by condition:",{before:d.map(te=>te.id),after:_.map(te=>te.id)});const A=_.filter(te=>te.loading_strategy==="blocking"),x=_.filter(te=>(te.loading_strategy||"progressive")!=="blocking"&&te.type!=="websocket"),O=Array.isArray(l.transition_overlay?.wait_for)?l.transition_overlay.wait_for:[],M=O.length>0&&_.some(te=>O.includes(te.id)&&te.type!=="websocket"&&(te.loading_strategy||"progressive")!=="background");if((A.length>0||M)&&l.transition_overlay){const te=typeof l.transition_overlay=="boolean"?{enabled:l.transition_overlay,style:"opaque"}:l.transition_overlay;te.enabled&&te.style==="skeleton"&&te.skeleton?.component&&te.target?this.renderSkeletonOverlay(te.target,te.skeleton,l,te.fallback_target):te.enabled&&te.style==="spinner"&&te.target&&this.renderSpinnerOverlay(te.target,te.spinner,te.fallback_target)}let T={},D={},z={},P={};this.currentWebSocketSubscriptions.length>0&&(B.log("Unsubscribing previous WebSocket subscriptions:",this.currentWebSocketSubscriptions),v.unsubscribeWebSockets(this.currentWebSocketSubscriptions),this.currentWebSocketSubscriptions=[]),this.currentDataSources=_,this.currentRawDataSources=d,this.currentRouteParams=e.params||{},this.currentQueryParams=g,B.log(`handleRouteChange #${n} - queryObject:`,f),B.log(`handleRouteChange #${n} - queryParams:`,g.toString()),A.length>0&&(B.log("Fetching blocking data sources:",A.map(pe=>pe.id)),(await v.fetchDataSourcesWithResults(A,e.params||{},g)).forEach(pe=>{if(pe.state==="success"&&pe.data!==void 0)T[pe.id]=pe.data;else if(pe.state==="error"&&pe.error){const N=pe.error,R=N.response?.data?.message;D[pe.id]={message:R||pe.error.message,status:N.response?.status||N.status}}}),this.processInitOptions(A,T,z,P),this.currentFetchedData={...T},B.log("Blocking data loaded:",Object.keys(T)),Object.keys(D).length>0&&B.log("Data source errors:",D),Object.keys(z).length>0&&B.log("Local state init (blocking):",Object.keys(z)),Object.keys(P).length>0&&B.log("Isolated state init (blocking):",Object.keys(P)));const q=x.length>0;if(q&&(jr.setPending(!0),B.log("Transition started before rendering")),n!==this.currentRouteChangeId){B.log("Route change cancelled after blocking data (newer request exists):",n);return}const{getState:W}=await Promise.resolve().then(()=>Pr),he=W().currentDataContext||{},Se=x.map(te=>te.id),be={};if(q){Se.forEach(pe=>{he[pe]!==void 0?be[pe]=he[pe]:be[pe]=void 0});const te=Object.keys(be).filter(pe=>be[pe]!==void 0);te.length>0&&B.log("Preserving previous progressive data:",te),B.log("Progressive data sources initialized:",Se)}const Ue=l.defines||{},Fe={...be,...e.params,...T,route:{...e.params||{},path:e.path},query:f,_global:{...this.globalState},_globalSetState:te=>this.setGlobalState(te),_dataSourceErrors:Object.keys(D).length>0?D:void 0,_localInit:Object.keys(z).length>0?z:void 0,_isolatedInit:Object.keys(P).length>0?P:void 0,_defines:Object.keys(Ue).length>0?Ue:void 0};if(l.computed&&Object.keys(l.computed).length>0){const te=this.calculateComputed(l.computed,Fe);Object.keys(te).length>0&&(Fe._computed=te,this.globalState._computed=te,B.log("Computed values calculated:",Object.keys(te))),Fe._computedDefinitions=l.computed}const Ge=l.layout_name||e.layout;this.currentLayoutName!==""&&this.currentLayoutName!==Ge&&(B.log("_local reset due to layout change:",{from:this.currentLayoutName,to:Ge}),this.globalState._local={},RC()),this.currentLayoutName=Ge;const J=l.initLocal||l.state;if(J&&Object.keys(J).length>0){this.globalState._local||(this.globalState._local={});for(const[te,pe]of Object.entries(J))this.globalState._local[te]===void 0&&(this.globalState._local[te]=JSON.parse(JSON.stringify(pe)));Fe._local={...this.globalState._local},B.log("initLocal applied to _local:",Object.keys(J))}if(l.initGlobal&&Object.keys(l.initGlobal).length>0){for(const[te,pe]of Object.entries(l.initGlobal))this.globalState[te]===void 0&&(this.globalState[te]=JSON.parse(JSON.stringify(pe)));B.log("initGlobal applied to _global:",Object.keys(l.initGlobal))}if(l.initIsolated&&Object.keys(l.initIsolated).length>0){for(const[te,pe]of Object.entries(l.initIsolated))P[te]===void 0&&(P[te]=JSON.parse(JSON.stringify(pe)));B.log("initIsolated applied:",Object.keys(l.initIsolated))}Fe._global={...this.globalState,layoutWarnings:l.warnings||[]};const ue=l.initActions||l.init_actions;if(ue&&ue.length>0){await this.executeInitActions(ue,Fe),B.log("initActions executed before render");const te=this.globalState;if(te._local){if(Object.keys(z).length>0){for(const[pe,N]of Object.entries(z))te._local[pe]!==void 0?te._local[pe]=this.deepMerge(te._local[pe],N):te._local[pe]=N;B.log("localInit merged into _local after initActions:",Object.keys(z))}Fe._local=te._local,B.log("_local merged into dataContext:",te._local)}if(Fe._global={...this.globalState,layoutWarnings:l.warnings||[]},B.log("_global merged into dataContext after initActions"),l.computed&&Object.keys(l.computed).length>0){const pe=this.calculateComputed(l.computed,Fe);Object.keys(pe).length>0&&(Fe._computed=pe,this.globalState._computed=pe,B.log("Computed values recalculated after init_actions:",Object.keys(pe)))}if(Object.keys(this.currentFetchedData).length>0){for(const[pe,N]of Object.entries(this.currentFetchedData))Fe[pe]=N;B.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:Fe,translationContext:{templateId:this.config.templateId,locale:this.config.locale}}),B.log("Initial render complete with blocking data"),this.reattachSpinnerOverlay(),x.length>0){if(n!==this.currentRouteChangeId){B.log("Route change cancelled before progressive fetch (newer request exists):",n),jr.setPending(!1),this.hideTransitionOverlay();return}B.log("Fetching progressive/background data sources:",x.map(N=>N.id));const{updateTemplateData:te,getState:pe}=await Promise.resolve().then(()=>Pr);try{const N=x.map(async R=>{try{const fe=(await v.fetchDataSourcesWithResults([R],e.params||{},g,this.globalState))[0];if(!fe){B.log(`Data source ${R.id} skipped (no fetch result)`);return}if(n!==this.currentRouteChangeId){B.log(`Data source ${R.id} fetch cancelled (newer request exists)`);return}if(fe.state==="success"&&fe.data!==void 0){B.log(`Progressive data source loaded: ${R.id}`),this.currentFetchedData[R.id]=fe.data;const xe={};this.processInitOptions([R],{[R.id]:fe.data},xe);const ee=pe();if(ee.bindingEngine){const Be=[R.id];R.initGlobal&&Be.push("_global"),R.initLocal&&Be.push("_local"),ee.bindingEngine.invalidateCacheByKeys(Be)}const _e={[R.id]:fe.data};Object.keys(xe).length>0&&(_e._localInit=xe,Object.assign(z,xe)),R.initGlobal&&(_e._global={...this.globalState}),te(_e)}else if(fe.state==="error"&&fe.error){const xe=fe.error,ee=xe.response?.data?.message;D[R.id]={message:ee||fe.error.message,status:xe.response?.status||xe.status},B.log(`Progressive data source error: ${R.id}`,D[R.id]),te({[R.id]:null,_dataSourceErrors:{...D}})}}catch(de){B.error(`Failed to fetch data source: ${R.id}`,de)}});if(await Promise.all(N),n!==this.currentRouteChangeId){B.log("Route change cancelled after progressive fetch (newer request exists):",n);return}B.log("All progressive data sources loaded")}finally{jr.setPending(!1),this.hideTransitionOverlay()}}else this.hideTransitionOverlay();const Me=_.filter(te=>te.type==="websocket");if(Me.length>0){B.log("Subscribing WebSocket data sources:",Me.map(R=>R.id));const{updateTemplateData:te,getState:pe}=await Promise.resolve().then(()=>Pr),N={...this.currentFetchedData,...e.params,route:{...e.params||{},path:e.path},query:f,_global:{...this.globalState}};B.log("WebSocket binding context keys:",Object.keys(N)),this.currentWebSocketSubscriptions=v.subscribeWebSockets(_,(R,de)=>{B.log(`WebSocket data received for: ${R}`,de),this.currentFetchedData[R]=de;const fe=_.find(Be=>Be.id===R),xe=pe();if(xe.bindingEngine){const Be=[R];fe?.initGlobal&&Be.push("_global"),fe?.initLocal&&Be.push("_local"),xe.bindingEngine.invalidateCacheByKeys(Be)}te({[R]:de});const _e=_.find(Be=>Be.type==="websocket"&&(Be.target_source||Be.id)===R)?.onReceive;Array.isArray(_e)&&_e.length>0&&window.G7Core?.dispatch&&(async()=>{for(const ke of _e)try{const Ie=this.getActionDispatcher?.();Ie&&await Ie.dispatchAction(ke,{navigate:this.getRouter?.()?(Ct,ot)=>this.getRouter().navigate(Ct,ot):void 0,setState:Ct=>this.setGlobalState(Ct),state:this.globalState,data:{...this.globalState,$args:[de],$event:de},_isDispatchFallbackContext:!0})}catch(Ie){B.error(`WebSocket onReceive action failed for ${R}:`,Ie)}})()},N),B.log("WebSocket subscriptions established:",this.currentWebSocketSubscriptions)}B.log("Layout rendered successfully")}catch(a){B.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(),B.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 g=f;if(c.initLocalDefaults&&typeof c.initLocalDefaults=="object"){const y=this.evaluateDefaults(c.initLocalDefaults,n);g={...y,...f},B.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),B.log(`initLocal (merged): ${c.id}.data -> _local.${c.initLocal}`)):(a[c.initLocal]=g,B.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 v=this.evaluateDefaults(c.initLocalDefaults,n);y={...v,...y},B.log(`initLocalDefaults applied for ${g}:`,Object.keys(v))}const S=this.globalState._local?.[g];S!==void 0&&typeof S=="object"&&typeof y=="object"?(a[g]=this.deepMerge(S,y),B.log(`initLocal (merged): ${c.id}.data${m?"."+m:""} -> _local.${g}`)):(a[g]=y,B.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:S}of m){const v=this.extractValueByPathOrExpression(f,S,c.id);if(y.includes(".")){const _=y.split(".")[0];a[_]===void 0&&this.globalState._local?.[_]!==void 0&&(a[_]=JSON.parse(JSON.stringify(this.globalState._local[_]))),this.setValueAtPath(a,y,v,g),B.log(`initLocal map (${g}): ${c.id} ${S} -> _local.${y}`)}else{const _=this.globalState._local?.[y];g==="replace"?(a[y]=v,B.log(`initLocal map (replace): ${c.id} ${S} -> _local.${y}`)):g==="shallow"?_!==void 0&&typeof _=="object"&&typeof v=="object"?(a[y]={..._,...v},B.log(`initLocal map (shallow): ${c.id} ${S} -> _local.${y}`)):(a[y]=v,B.log(`initLocal map: ${c.id} ${S} -> _local.${y}`)):_!==void 0&&typeof _=="object"&&typeof v=="object"?(a[y]=this.deepMerge(_,v),B.log(`initLocal map (deep): ${c.id} ${S} -> _local.${y}`)):(a[y]=v,B.log(`initLocal map: ${c.id} ${S} -> _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"){B.warn(`initGlobal map value must be string, got ${typeof y} for key ${m}`);continue}const S=this.extractValueByPathOrExpression(f,y,c.id),v=this.globalState[m];v!==void 0&&typeof v=="object"&&typeof S=="object"?(this.globalState[m]=this.deepMerge(v,S),B.log(`initGlobal map (merged): ${c.id} ${y} -> _global.${m}`)):(this.globalState[m]=S,B.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 S=this.globalState[y];S!==void 0&&typeof S=="object"&&typeof f=="object"?(this.globalState[y]=this.deepMerge(S,f),B.log(`initGlobal (merged): ${c.id}.data -> _global.${y}`)):(this.globalState[y]=f,B.log(`initGlobal: ${c.id}.data -> _global.${y}`))}else if(typeof y=="object"&&y.key){const{key:S,path:v}=y,_=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,_),B.log(`initGlobal (merged): ${c.id}.data${v?"."+v:""} -> _global.${S}`)):(this.globalState[S]=_,B.log(`initGlobal: ${c.id}.data${v?"."+v:""} -> _global.${S}`))}}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),B.log(`initIsolated (merged): ${c.id}.data -> _isolated.${c.initIsolated}`)):(i[c.initIsolated]=f,B.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,S=i[g];S!==void 0&&typeof S=="object"&&typeof y=="object"?(i[g]=this.deepMerge(S,y),B.log(`initIsolated (merged): ${c.id}.data${m?"."+m:""} -> _isolated.${g}`)):(i[g]=y,B.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"){B.warn(`initIsolated map value must be string, got ${typeof m} for key ${g}`);continue}const y=this.extractValueByPathOrExpression(f,m,c.id),S=i[g];S!==void 0&&typeof S=="object"&&typeof y=="object"?(i[g]=this.deepMerge(S,y),B.log(`initIsolated map (merged): ${c.id} ${m} -> _isolated.${g}`)):(i[g]=y,B.log(`initIsolated map: ${c.id} ${m} -> _isolated.${g}`))}}})}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){B.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,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,n,a){const i=Aa(n);if(i!==null){const l=new Dn,c={data:e,_global:this.globalState,_local:this.globalState._local||{}};try{const d=Vn(i)?l.evaluatePipeExpression(i,c,{skipCache:!0}):l.evaluateExpression(i,c);return B.log(`initLocal/initGlobal expression evaluated: ${n} -> `,d),d}catch(d){B.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)&&!ns({if:i.if,conditions:i.conditions},n,this.bindingEngine,`script:${i.id}`)){B.log(`Script skipped (condition not met): ${i.id}`);continue}if(document.getElementById(i.id)){B.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=()=>{B.log(`Script loaded successfully: ${i.id}`),d()},g.onerror=()=>{B.warn(`Failed to load script: ${i.id} (${i.src})`),d()},document.head.appendChild(g)});a.push(c)}a.length>0&&(await Promise.all(a),B.log(`All scripts loaded: ${e.filter(i=>!document.getElementById(i.id)||a.length>0).map(i=>i.id).join(", ")}`))}evaluateScriptCondition(e,n){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; - } - } - `)(n)}return!!e}catch(a){return B.warn(`Failed to evaluate script condition: ${e}`,a),!1}}showInitError(e){if(fd()){B.warn("Init failed while document is unloading — skipping error screen",e);return}const n=Ey(e);Cf.renderFromError("app",n,"초기화 실패",this.config.debug,{templateId:this.config.templateId,locale:this.config.locale},void 0,e)}showTransitionOverlay(e,n){const a=typeof e=="boolean"?{enabled:e,style:"opaque",target:void 0,skeleton:void 0,spinner:void 0}:{enabled:e.enabled,style:e.style||"opaque",target:e.target,skeleton:e.skeleton,spinner:e.spinner,fallback_target:e.fallback_target};if(!a.enabled)return;if(this.hideTransitionOverlay(),a.style==="skeleton"&&a.skeleton?.component&&a.target&&n){this.renderSkeletonOverlay(a.target,a.skeleton,n,a.fallback_target);return}if(a.style==="spinner"&&a.target){this.renderSpinnerOverlay(a.target,a.spinner,a.fallback_target);return}const i=document.documentElement.classList.contains("dark");let l,c="";switch(a.style){case"blur":l=i?"rgba(17,24,39,0.3)":"rgba(255,255,255,0.3)",c="backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);";break;case"fade":l=i?"rgba(17,24,39,0.8)":"rgba(255,255,255,0.8)";break;case"skeleton":l=i?"rgb(17,24,39)":"rgb(249,250,251)";break;default:l=i?"rgb(17,24,39)":"rgb(249,250,251)";break}if(a.target){const d=`#${CSS.escape(a.target)}`,f=document.createElement("style");f.id="g7-transition-overlay",f.textContent=`${d}{position:relative;z-index:0;}${d}::after{content:'';position:absolute;inset:0;background:${l};${c}z-index:2147483647;pointer-events:none;}`,document.head.appendChild(f),this.transitionOverlayEl=f}else{const d=document.createElement("div");d.id="g7-transition-overlay",d.setAttribute("aria-hidden","true"),d.style.position="fixed",d.style.inset="0",d.style.zIndex="9999",d.style.pointerEvents="none",d.style.background=l;for(const f of c.split(";")){const g=f.indexOf(":");g!==-1&&d.style.setProperty(f.slice(0,g).trim(),f.slice(g+1).trim())}document.body.appendChild(d),this.transitionOverlayEl=d}}hideTransitionOverlay(){this.transitionOverlayEl&&(this.transitionOverlayEl.remove(),this.transitionOverlayEl=null),this.hideSkeletonOverlay()}renderSkeletonOverlay(e,n,a,i){const c=mr.getInstance().getComponent(n.component);if(!c){B.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){B.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,S=`#${CSS.escape(y)}`,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 T=d.getBoundingClientRect(),D=window.scrollX||document.documentElement.scrollLeft,z=window.scrollY||document.documentElement.scrollTop;_.style.cssText=["position:absolute",`top:${T.top+z}px`,`left:${T.left+D}px`,`width:${T.width}px`,`height:${T.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 T=f==="fallback"?i:e;x=this.findComponentChildrenById(A,T)}const O=od.createRoot(_);this.skeletonOverlayRoot=O,ka.flushSync(()=>{O.render(Xe.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})`;B.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){B.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=n?.text||window.G7Core?.t?.("nav.loading")||"";this._spinnerState={target:e,fallbackTarget:a,spinnerConfig:n,resolvedText:g},this._mountSpinnerInTarget(i);const m=l==="fullpage"?"fullpage (#app)":l==="fallback"?`fallback (#${a})`:`target (#${e})`,y=n?.component||"default spinner";B.log(`Spinner overlay rendered [${m}] with "${y}"`)}_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=mr.getInstance().getComponent(n.component);if(c){const d=od.createRoot(i);this.skeletonOverlayRoot=d,ka.flushSync(()=>{d.render(Xe.createElement(c,{options:{text:a}}))});return}B.log(`Spinner component "${n.component}" not found in registry, using default spinner`)}i.innerHTML='
'}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),B.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=!!Hr().getToken()||e.details?.hadToken===!0,f=br.getInstance().getLoginRedirectUrl(l,c,d?"session_expired":void 0);window.location.href=f;return}const n=Ey(e);Cf.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;B.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"))?(B.log(`${e.code} error page rendered successfully`),!0):(B.log(`Falling back to ErrorDisplay for ${e.code}`),this.showInitError(new Error(`Service unavailable (${e.code})`)),!0)}catch(n){return B.error(`Failed to render ${e.code} page:`,n),this.showInitError(new Error(`Service unavailable (${e.code})`)),!0}}async handleRouteNotFound(e){B.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"))){B.log("404 error page rendered successfully");return}B.log("Falling back to ErrorDisplay for 404"),this.showRouteError(new Error(`Page not found: ${e}`))}catch(n){B.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(){B.log("reloadExtensionState: start");let e;try{const n=await fetch(Nr(`/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){B.warn("reloadExtensionState: failed to fetch config.json",n)}e!==void 0&&e!==this.extensionCacheVersion&&(B.log(`reloadExtensionState: cache version ${this.extensionCacheVersion} -> ${e}`),this.extensionCacheVersion=e,this.saveCacheVersionToStorage(e));try{this.router&&(await this.router.loadRoutes(this.extensionCacheVersion),B.log("reloadExtensionState: routes reloaded"))}catch(n){B.error("reloadExtensionState: routes reload failed",n)}try{this.layoutLoader&&(this.extensionCacheVersion>0&&this.layoutLoader.setCacheVersion(this.extensionCacheVersion),this.layoutLoader.clear(),B.log("reloadExtensionState: layout cache cleared"))}catch(n){B.error("reloadExtensionState: layout cache clear failed",n)}try{const{TranslationEngine:n}=await Promise.resolve().then(()=>gp),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),B.log("reloadExtensionState: translations reloaded")}catch(n){B.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}}),B.log("reloadExtensionState: supportedLocales refreshed",i)}}}catch(n){B.warn("reloadExtensionState: supportedLocales refresh failed",n)}B.log("reloadExtensionState: done")}getActionDispatcher(){return ea().actionDispatcher}async changeLocale(e){try{if(B.log("Changing locale to:",e),this.config.locale===e){B.log("Locale is already",e);return}this.config.locale=e,this.saveLocaleToStorage(e),await this.saveLocaleToDatabase(e),this.layoutLoader&&(this.layoutLoader.clear(),B.log("Layout cache cleared")),this.errorPageHandler&&(this.errorPageHandler.updateLocale(e),B.log("ErrorPageHandler locale updated")),xc(),await No({templateId:this.config.templateId,templateType:this.config.templateType,locale:e,debug:this.config.debug,cacheVersion:this.extensionCacheVersion}),B.log("Template Engine re-initialized with new locale");const{getActionDispatcher:n}=await Promise.resolve().then(()=>Pr),a=n();a&&(a.setDefaultContext({navigate:i=>this.router?.navigate(i)}),a.setGlobalStateUpdater((i,l)=>this.setGlobalState(i,l)),B.log("Navigate function and setGlobalState re-injected to ActionDispatcher")),this.reinitializeModuleHandlers(),B.log("Module handlers re-initialized"),this.reinitializeTemplateHandlers(),B.log("Template handlers re-initialized"),this.reinitializePluginHandlers(),B.log("Plugin handlers re-initialized"),this.router&&this.router.navigateToCurrentPath(),B.log("Locale changed successfully to",e)}catch(n){throw B.error("Failed to change locale:",n),n}}loadLocaleFromStorage(){try{return localStorage.getItem(xr.LOCALE_STORAGE_KEY)}catch(e){return B.warn("Failed to load locale from storage:",e),null}}saveLocaleToStorage(e){try{localStorage.setItem(xr.LOCALE_STORAGE_KEY,e)}catch(n){B.warn("Failed to save locale to storage:",n)}}async saveLocaleToDatabase(e){const n=localStorage.getItem("auth_token");if(!n){B.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?B.log("Locale saved to DB:",e):B.warn("Failed to save locale to DB (UI will still change):",c.statusText)}catch(l){B.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(xr.CACHE_VERSION_STORAGE_KEY);return e?parseInt(e,10):null}catch(e){return B.warn("Failed to load cache version from storage:",e),null}}saveCacheVersionToStorage(e){try{localStorage.setItem(xr.CACHE_VERSION_STORAGE_KEY,String(e))}catch(n){B.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(),B.log(`Module handler re-initialized: ${a}`)}catch(i){B.warn(`Failed to re-initialize module handlers for ${a}:`,i)}})}reinitializeTemplateHandlers(){if(typeof window>"u")return;const e=window.G7TemplateHandlers;if(!e){B.warn("Template handlers not found on window.G7TemplateHandlers");return}const n=this.getActionDispatcher();if(!n){B.warn("ActionDispatcher not available for template handler registration");return}Object.entries(e).forEach(([a,i])=>{n.registerHandler(a,i)}),B.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(),B.log(`Plugin handler re-initialized: ${a}`)}catch(i){B.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},B.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(()=>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,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){B.warn(`Data source not found: ${e}`);return}B.log(`Refetching data source: ${e}`,n?.sync?"(sync mode)":"",n?.globalStateOverride?"(with global override)":"",n?.localStateOverride?"(with local override)":""),jr.setPending(!0);try{const i=new _r;this.currentGlobalHeaders.length>0&&i.setGlobalHeaders(this.currentGlobalHeaders);const l=ea(),c=l.currentDataContext?._global||{},d=n?.globalStateOverride?{...c,...n.globalStateOverride}:c,f=l.currentDataContext?._local||{},g=n?.localStateOverride?{...f,...n.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 S={};this.processInitOptions([a],{[e]:y.data},S);const v=ea();if(v.bindingEngine){const A=[e];a.initGlobal&&A.push("_global"),a.initLocal&&A.push("_local"),v.bindingEngine.invalidateCacheByKeys(A)}const _={[e]:y.data};return Object.keys(S).length>0&&(_._localInit=S),a.initGlobal&&(_._global={...this.globalState}),ys(_,n?.sync?{sync:!0}:void 0),B.log(`Data source refetched successfully: ${e}`),y.data}else if(y.state==="error"){B.error(`Failed to refetch data source: ${e}`,y.error);return}}catch(i){B.error(`Error refetching data source: ${e}`,i);return}finally{jr.setPending(!1)}}registerModalDataSources(e,n){this.modalDataSources.set(e,n),B.log(`Modal data sources registered: ${e} (${n.length} sources)`)}unregisterModalDataSources(e){this.modalDataSources.delete(e),B.log(`Modal data sources unregistered: ${e}`)}getDataSource(e){return this.currentFetchedData[e]}setDataSource(e,n,a){if(!e){B.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(()=>Pr).then(({updateTemplateData:c})=>{c({[e]:this.currentFetchedData[e]},l?{sync:!0}:void 0)}),B.log(`setDataSource: Updated ${e}`,i?"(merged)":"(replaced)")}updateDataSourceItem(e,n,a,i,l){const{idField:c="id",merge:d=!0,skipRender:f=!1}=l||{},g=this.currentFetchedData[e];if(!g)return B.warn(`updateDataSourceItem: DataSource '${e}' not found`),!1;const m=this.parseItemPath(n);let y=g;for(const _ of m)if(y=y?.[_],y===void 0)return B.warn(`updateDataSourceItem: Path '${n}' not found in dataSource`),!1;if(!Array.isArray(y))return B.warn(`updateDataSourceItem: Target at '${n}' is not an array`),!1;const S=y.findIndex(_=>String(_[c])===String(a));if(S===-1)return B.warn(`updateDataSourceItem: Item with ${c}='${a}' not found`),!1;d?y[S]=this.deepMerge(y[S],i):y[S]={...y[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(()=>Pr).then(({updateTemplateData:_})=>{_({[e]:this.currentFetchedData[e]},{sync:!1})}),B.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 g=0;g0?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){B.log("No auto_fetch data sources to refetch");return}B.log(`Refetching ${g.length} auto_fetch data sources`),jr.setPending(!0);const m=ea().currentLayoutJson,y=m?.transition_overlay,S=Array.isArray(y?.wait_for)?y.wait_for:[],v=g.some(A=>(A.loading_strategy||"progressive")==="blocking"),_=S.length>0&&g.some(A=>S.includes(A.id)&&A.type!=="websocket"&&(A.loading_strategy||"progressive")!=="background");if((v||_)&&y&&typeof y=="object"){const A=n?.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=ea(),O=x.currentDataContext?._global||{},M=await A.fetchDataSourcesWithResults(g,this.currentRouteParams,this.currentQueryParams,O,void 0,{ignoreAutoFetch:!1}),T={},D={},z=new Map(g.map(P=>[P.id,P]));for(const P of M){const q=z.get(P.id);if(!q){B.warn(`Refetch result id not found in autoFetchDataSources: ${P.id}`);continue}const W=P.id;P.state==="success"&&P.data!==void 0?(this.currentFetchedData[W]=P.data,T[W]=P.data,this.processInitOptions([q],{[W]:P.data},D),B.log(`Data source refetched: ${W}`)):P.state==="error"&&B.error(`Failed to refetch data source: ${W}`,P.error)}if(x.bindingEngine){const P=Object.keys(T);P.push("query"),x.bindingEngine.invalidateCacheByKeys(P)}if(Object.keys(D).length>0&&(T._localInit=D),Object.keys(T).length>0){if(T.query=Ay(this.currentQueryParams),x.currentLayoutJson?.computed&&Object.keys(x.currentLayoutJson.computed).length>0){const P={...x.currentDataContext,...T,query:T.query},q=this.calculateComputed(x.currentLayoutJson.computed,P);Object.keys(q).length>0&&(T._computed=q,this.globalState._computed=q,B.log("Computed values recalculated in updateQueryParams:",Object.keys(q)))}ys(T,{sync:!0}),B.log("Template data updated with new query params")}}catch(A){B.error("Error in updateQueryParams:",A)}finally{jr.setPending(!1),this.hideTransitionOverlay()}}calculateComputed(e,n){const a={},i=new Dn;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,B.log(`Computed ${l}:`,d)}catch(d){B.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 new Function("ctx",` - with(ctx) { - try { - return ${e}; - } catch (e) { - return undefined; - } - } - `)(n)}catch(a){B.warn(`Expression evaluation failed: ${e}`,a);return}}async executeInitActions(e,n={}){const{getActionDispatcher:a}=await Promise.resolve().then(()=>Pr),i=a();if(!i){B.warn("ActionDispatcher not available for init_actions");return}B.log("Executing init_actions:",e.map(d=>d.handler)),B.log("Init actions dataContext:",n),B.log("Init actions dataContext._global:",n._global);const l=e.map(d=>d.handler).filter(d=>d.includes("."));l.length>0&&await this.waitForHandlers(i,l);let c={...n};for(const d of e)try{const f={type:"click",handler:d.handler,target:d.target,params:d.params,resultTo:d.resultTo,onSuccess:d.onSuccess,onError:d.onError,if:d.if,conditions:d.conditions,auth_mode:d.auth_mode,auth_required:d.auth_required},g=i.createHandler(f,c),m=new Event("init");await g(m),B.log(`Init action executed: ${d.handler}`);const y=this.globalState;c={...c,_global:{...y},_local:y._local||c._local||{}},B.log(`Data context refreshed after ${d.handler}:`,{cartKey:c._global?.cartKey})}catch(f){B.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()){B.log("All module handlers already registered");return}const d=_f().getFailedJsAssets();if(d.length>0){const f=d.some(y=>y==="module"||y==="plugin"),g=n.filter(y=>!e.customHandlers?.has(y)),m=g.length>0&&g.every(y=>d.some(S=>y.startsWith(`${S}.`)));if(f||m){B.warn("Extension asset load failed — not waiting for handlers that will never register:",{pending:g,failedAssets:d});return}}return B.log("Waiting for module handlers:",n),new Promise(f=>{const g=()=>{if(c()){B.log("All module handlers now registered"),f();return}if(Date.now()-i>=a){const m=n.filter(y=>!e.customHandlers?.has(y));B.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 Do=xr;function xf(o){const e=new Do(o);return o.websocket&&(Cc.configure(o.websocket),B.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=xf,window.G7Core.dataSource={refetch:async(o,e)=>{const n=window.__templateApp;if(!n){B.warn("TemplateApp not initialized (G7Core.dataSource.refetch)");return}return n.refetchDataSource(o,e)},get:o=>{const e=window.__templateApp;if(!e){B.warn("TemplateApp not initialized (G7Core.dataSource.get)");return}return e.getDataSource(o)}});function vA(o={}){const{checked:e,value:n,name:a,type:i="checkbox"}=o,c=n!==void 0?n:i==="checkbox"||i==="radio"?e??!1:String(e??""),d={checked:e??!1,value:c,name:a??"",type:i};return{target:d,currentTarget:d,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new Event("change"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"change",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{}}}function SA(o={}){const{button:e=0,clientX:n=0,clientY:a=0}=o;return{button:e,clientX:n,clientY:a,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new MouseEvent("click"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"click",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{},target:document.createElement("div"),currentTarget:document.createElement("div")}}function wA(){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 CA(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 EA={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 xy(o){const e=o.replace(/^(dark:|hover:|focus:|active:|disabled:|group-hover:|sm:|md:|lg:|xl:|2xl:)+/,"");for(const[n,a]of Object.entries(EA))if(a.test(e))return n;return null}function Ty(o){const e=o.match(/^((dark:|hover:|focus:|active:|disabled:|group-hover:|sm:|md:|lg:|xl:|2xl:)+)/);return e?e[1]:""}function _A(o,e){if(!e||e.trim()==="")return o;if(!o||o.trim()==="")return e;const n=o.split(/\s+/).filter(Boolean),a=e.split(/\s+/).filter(Boolean),i=new Map;for(const c of a){const d=xy(c);if(d){const f=Ty(c),g=`${d}:${f}`;i.set(g,c)}}return[...n.filter(c=>{const d=xy(c);if(!d)return!0;const f=Ty(c),g=`${d}:${f}`;return!i.has(g)}),...a].join(" ")}function AA(o){return Object.entries(o).filter(([,e])=>e).map(([e])=>e).join(" ")}function xA(...o){return o.filter(e=>typeof e=="string"&&e.length>0).join(" ")}const ye=ht("G7CoreGlobals");function TA(){window.React=Xe,window.ReactDOM={...ld,createPortal:ka.createPortal,unstable_batchedUpdates:o=>o()},window.ReactJSXRuntime=CC,ye.log("전역 객체 window.React, window.ReactDOM, window.ReactJSXRuntime에 노출됨")}function RA(o){const e=new Map,n=Oa.getInstance();o.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 g=>{try{return await g(i)}catch(m){throw ye.error(`componentEvent: Error in listener for "${a}":`,m),m}}))}catch(g){d=g instanceof Error?g:new Error(String(g))}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()}},ye.log("전역 객체 window.G7Core.componentEvent에 노출됨")}function kA(o){o.__runtime||(o.__runtime={DynamicRenderer:Ur,ComponentRegistry:mr,TranslationEngine:_a,DataSourceManager:_r,dataSourceManager:lA,DataBindingEngine:Dn,dataBindingEngine:Sd,ActionDispatcher:bo,TranslationReactContext:yf,TranslationProvider:vc,useTranslation:dy,ResponsiveContext:Kd,ResponsiveProvider:ic,useResponsive:Wd,responsiveManager:hi,BREAKPOINT_PRESETS:Ul,AuthManager:br,createLogger:ht,G7DevToolsCore:Oa},ye.log("전역 객체 window.G7Core.__runtime(코어 런타임 표면)에 노출됨"))}function DA(o){if(o.layoutEditor&&o.layoutEditor.__isStub!==!0||o.layoutEditor&&o.layoutEditor.__isStub===!0)return;const e=[],n=[];o.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)}},ye.log("전역 객체 window.G7Core.layoutEditor 예약 접수함(stub) 노출됨")}function OA(o,e){o.useTranslation=dy,o.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)},ye.log("전역 객체 window.G7Core.useTranslation에 노출됨")}function LA(o,e){o.createChangeEvent=vA,o.createClickEvent=SA,o.createSubmitEvent=wA,o.createKeyboardEvent=CA,o.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)}),o.createLogger=ht,ye.log("전역 객체 window.G7Core.createLogger에 노출됨"),o.renderItemChildren=(n,a,i,l,c)=>{const d=e.getState(),m={_global:window.__templateApp?.getGlobalState?.()||{},_local:a._local||{},_computed:a._computed||{},...a},y=new Set,S=(_,A)=>{const x=m._global?._remountKeys;return _&&x?.[_]&&!y.has(_)?(y.add(_),`${_}-remount-${x[_]}`):_||A},v={translationContext:d.translationContext,translationEngine:d.translationEngine,bindingEngine:d.bindingEngine,actionDispatcher:d.actionDispatcher,getRemountKey:S,...c};return lo(n,m,i,l,v)},o.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??Sd;return vp(n,d,f)},o.getComponentMap=()=>mr.getInstance().getComponentMap(),o.renderComponentLayout=(n,a,i)=>{if(!n||!Array.isArray(n)||n.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=(S,v)=>{const _=g._global?._remountKeys;return S&&_?.[S]&&!m.has(S)?(m.add(S),`${S}-remount-${_[S]}`):S||v};return lo(n,g,l,i,{translationContext:c.translationContext,translationEngine:c.translationEngine,bindingEngine:c.bindingEngine,actionDispatcher:c.actionDispatcher,getRemountKey:y})},o.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||mr.getInstance().getComponentMap(),g=e.getState(),m=o.state?.get?.()||{},y=c?.stateRef?.current??c?.state??{},S={...m._local||{},...y},v=c?.computedRef?.current??m._computed??{},_=g.bindingEngine??o.getDataBindingEngine?.(),A={...m,_local:S,_computed:v,row:i,item:i,$item:i};let x={};if(l&&typeof l=="object"&&_)try{for(const[D,z]of Object.entries(l))if(typeof z=="string"){const P=Aa(z);if(P!==null)try{x[D]=Vn(P)?_.evaluatePipeExpression(P,A,{skipCache:!0}):_.evaluateExpression(P,A,{skipCache:!0})}catch{x[D]=void 0}else x[D]=_.resolveBindings(z,A,{skipCache:!0})}else x[D]=z}catch(D){ye.warn("renderExpandContent: expandContext 평가 오류:",D)}const O={row:i,item:i,$item:i,_local:S,_global:m._global||{},_computed:m._computed||{},...x},M=new Set,T=(D,z)=>{const P=O._global?._remountKeys;return D&&P?.[D]&&!M.has(D)?(M.add(D),`${D}-remount-${P[D]}`):D||z};return lo(a,O,f,d,{translationContext:g.translationContext,translationEngine:g.translationEngine??void 0,bindingEngine:g.bindingEngine??void 0,actionDispatcher:g.actionDispatcher,componentContext:c,getRemountKey:T})},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},ye.log("전역 객체 window.G7Core.renderItemChildren에 노출됨"),ye.log("전역 객체 window.G7Core.renderComponentLayout에 노출됨"),ye.log("전역 객체 window.G7Core.renderExpandContent에 노출됨"),ye.log("전역 객체 window.G7Core.$get에 노출됨")}function MA(o){o.dispatch=async(e,n)=>{const a=window.__templateApp;if(!a)return ye.warn("G7Core.dispatch: TemplateApp이 초기화되지 않았습니다."),{success:!1,error:new Error("TemplateApp이 초기화되지 않았습니다.")};const i=a.getActionDispatcher?.();if(!i)return ye.warn("G7Core.dispatch: ActionDispatcher를 찾을 수 없습니다."),{success:!1,error:new Error("ActionDispatcher를 찾을 수 없습니다.")};const l=a.getRouter?.(),c=a.getGlobalState?.(),d=a.setGlobalState?.bind(a),g=n?.componentContext||window.__g7ActionContext,m=window.__g7PendingLocalState;let y=g?.state??c;m&&g&&(y=m,ye.log("[dispatch] Using __g7PendingLocalState for context.state:",m));let S=g?.data??c;m&&g?.data?._local&&(S={...g.data,_local:m},ye.log("[dispatch] Updated context.data._local with pendingLocalState"));const v={navigate:l?(_,A)=>l.navigate(_,A):void 0,setState:g?.setState??d,state:y,data:S,_isDispatchFallbackContext:!g?.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:O,...M}=e;i.dispatchAction(M,v)}),{success:!0,debounced:!0}}try{return await i.dispatchAction(e,v)}catch(_){return ye.error("G7Core.dispatch: 액션 실행 오류:",_),{success:!1,error:_ instanceof Error?_:new Error(String(_))}}},ye.log("전역 객체 window.G7Core.dispatch에 노출됨")}function Oo(o){const e={};for(const[n,a]of Object.entries(o))if(n.includes(".")){const i=n.split(".");let l=e;for(let c=0;c0&&e.every(n=>/^\d+$/.test(n))}function ky(o,e){const n={...o};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]=ky(n[a],e[a])):n[a]=e[a];return n}function ir(o,e){if(Array.isArray(o)&&!Array.isArray(e)&&$A(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&&(l[f]=d)}return l}}const n={...o};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 NA(o){o.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 ye.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&&Ry(Oo(e)).some(M=>x.has(M))&&(ye.log("[setLocal] render:false + 자동바인딩 경로 겹침 감지 → render:true 자동 승격 (engine-v1.43.0)"),n={...n,render:!0})}if(n?.debounce){const x=a?.getActionDispatcher?.();if(x){const O=n.debounceKey||`setLocal-${Object.keys(e).join(",")}`,{debounce:M,debounceKey:T,...D}=n;x.debouncedCall(O,n.debounce,()=>{o.state.setLocal(e,D)});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 O=i==="parent"?x[x.length-1]:x[0];if(O?.setState){O.setState(M=>l==="replace"?d:l==="shallow"?{...M||{},...d}:ir(M||{},d)),ye.log(`[setLocal] scope=${i}: 타겟 컨텍스트에 상태 업데이트 (mergeMode=${l})`,d);return}}ye.warn(`[setLocal] scope=${i}: 레이아웃 컨텍스트 스택이 비어있습니다. current로 폴백합니다.`)}const f=window.__g7PendingLocalState,g=a?.getGlobalState?.()?._local||{},S=!((window.__g7LayoutContextStack||[]).length>0)&&c?.state?c.state:void 0,v=S?ky(g,S):g,_=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}),ye.log(`[setLocal] globalLocal updated via setGlobalState (mergeMode=${l}, render=${n?.render??!0}):`,d)),window.__g7PendingLocalState=A,ye.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(ye.log("[setLocal] __g7ForcedLocalFields updated:",d),l==="replace")window.__g7SetLocalOverrideKeys=d;else{const x=window.__g7SetLocalOverrideKeys||{};window.__g7SetLocalOverrideKeys=ir(x,d)}ye.log("[setLocal] __g7SetLocalOverrideKeys updated:",d),c?.setState&&c.setState(x=>l==="replace"?d:l==="shallow"?{...x||{},...d}:ir(x||{},d)),a?.setGlobalState||ye.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 ye.warn("G7Core.state.update: TemplateApp이 초기화되지 않았습니다.")},subscribe:e=>{const n=window.__templateApp;return n?.onGlobalStateChange?n.onGlobalStateChange(e):(ye.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 y=window.__g7IsolatedStates?.[i];y?.mergeState?y.mergeState(f,d):ye.warn(`G7Core.state.setIsolated: scopeId '${i}'를 찾을 수 없습니다.`);return}const g=window.__g7ActionContext;g?.isolatedContext?.mergeState?g.isolatedContext.mergeState(f,d):ye.warn("G7Core.state.setIsolated: 액션 컨텍스트에 isolatedContext가 없습니다.")},getParent:()=>{const e=window.__g7LayoutContextStack||[];if(e.length===0)return ye.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||o.state.get()||{},setState:n.setState}},setParentLocal:(e,n,a)=>{const i=window.__g7LayoutContextStack||[];if(i.length===0){ye.warn("[setParentLocal] 레이아웃 컨텍스트 스택이 비어있습니다.");return}const l=i[i.length-1];if(!l?.setState){ye.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",g=Oo(c),m=l.state?._local!==void 0,y=m?l.state._local:l.state||{};let S;f==="replace"?S=g:f==="shallow"?S={...y,...g}:S=ir(y,g),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=g;else{const _=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=ir(_,g)}Gd()},setParentGlobal:(e,n)=>{let a;typeof e=="string"?a={[e]:n}:a=e,o.state.set(a),ye.log("[setParentGlobal] 전역 상태 업데이트:",a)}},window.__g7IsolatedStates||(window.__g7IsolatedStates={}),o.state.getGlobal=o.state.get,o.state.setGlobal=o.state.set,ye.log("전역 객체 window.G7Core.state에 노출됨")}function IA(o){o.dataSource={get:e=>window.__templateApp?.getDataSource?.(e),set:(e,n,a)=>{const i=window.__templateApp;i?.setDataSource?i.setDataSource(e,n,a):ye.warn("G7Core.dataSource.set: TemplateApp이 초기화되지 않았습니다.")},refetch:async(e,n)=>{const a=window.__templateApp;if(a?.refetchDataSource)return a.refetchDataSource(e,n);ye.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):(ye.warn("G7Core.dataSource.updateItem: TemplateApp이 초기화되지 않았습니다."),!1)},updateData:(e,n,a,i="append")=>{const l=window.__templateApp;if(!l?.getDataSource||!l?.setDataSource)return ye.warn("G7Core.dataSource.updateData: TemplateApp이 초기화되지 않았습니다."),!1;const c=l.getDataSource(e);if(!c)return ye.warn(`G7Core.dataSource.updateData: 데이터 소스 '${e}'를 찾을 수 없습니다.`),!1;let d,f=c,g=null;if(n){const y=n.split(".");g=y.pop();for(const S of y)if(f&&typeof f=="object"&&S in f)f=f[S];else return ye.warn(`G7Core.dataSource.updateData: 경로 '${n}'를 찾을 수 없습니다.`),!1;d=f[g]}else d=c;if(!Array.isArray(d))return ye.warn("G7Core.dataSource.updateData: 대상 경로의 데이터가 배열이 아닙니다."),!1;if(!Array.isArray(a))return ye.warn("G7Core.dataSource.updateData: newData가 배열이 아닙니다."),!1;const m=i==="prepend"?[...a,...d]:[...d,...a];return n&&g?(f[g]=m,l.setDataSource(e,c,{merge:!1})):l.setDataSource(e,m,{merge:!1}),ye.log(`G7Core.dataSource.updateData: '${e}'에 ${a.length}개 항목 ${i==="prepend"?"앞에":"뒤에"} 추가됨`),!0}},ye.log("전역 객체 window.G7Core.dataSource에 노출됨")}function HA(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 n=>{const a=window.__templateApp;a?.changeLocale?await a.changeLocale(n):ye.warn("G7Core.locale.change: TemplateApp이 초기화되지 않았습니다.")}},ye.log("전역 객체 window.G7Core.locale에 노출됨")}function jA(o){o.toast={show:(e,n)=>{o.dispatch({handler:"toast",params:{message:e,type:n?.type||"info",...n?.duration&&{duration:n.duration}}})},success:(e,n)=>{o.toast.show(e,{type:"success",duration:n})},error:(e,n)=>{o.toast.show(e,{type:"error",duration:n})},warning:(e,n)=>{o.toast.show(e,{type:"warning",duration:n})},info:(e,n)=>{o.toast.show(e,{type:"info",duration:n})}},ye.log("전역 객체 window.G7Core.toast에 노출됨")}function zA(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 n=o.state.get();return n._global?.activeModal===e||n._global?.modalStack?.includes(e)||!1},getStack:()=>o.state.get()._global?.modalStack||[]},ye.log("전역 객체 window.G7Core.modal에 노출됨")}function UA(o){o.style={mergeClasses:_A,conditionalClass:AA,joinClasses:xA},ye.log("전역 객체 window.G7Core.style에 노출됨")}function PA(o,e){const{webSocketManager:n}=e;o.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()},ye.log("전역 객체 window.G7Core.websocket에 노출됨")}function BA(o,e){const{transitionManager:n}=e;o.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}},ye.log("전역 객체 window.G7Core.navigation에 노출됨")}function qA(o){o.plugin={getSettings:e=>window.G7Config?.plugins?.[e],get:(e,n,a)=>window.G7Config?.plugins?.[e]?.[n]??a,getAll:()=>window.G7Config?.plugins??{}},ye.log("전역 객체 window.G7Core.plugin에 노출됨")}function GA(o){o.module={getSettings:e=>window.G7Config?.modules?.[e],get:(e,n,a)=>window.G7Config?.modules?.[e]?.[n]??a,getAll:()=>window.G7Config?.modules??{}},ye.log("전역 객체 window.G7Core.module에 노출됨")}function VA(o,e){const{transitionManager:n,responsiveManager:a}=e;o.AuthManager=br,o.api=Hr(),o.TransitionManager=n,o.useTransitionState=Cm,o.ResponsiveManager=a,o.useResponsive=Wd,o.useControllableState=NC,o.shallowArrayEqual=IC,o.shallowObjectEqual=_m,o.updateQueryParams=async(i,l)=>{const c=window.__templateApp;if(c?.updateQueryParams)return c.updateQueryParams(i,l);ye.warn("G7Core.updateQueryParams: TemplateApp이 초기화되지 않았습니다.")},ye.log("전역 객체 window.G7Core.AuthManager에 노출됨"),ye.log("전역 객체 window.G7Core.api에 노출됨"),ye.log("전역 객체 window.G7Core.ResponsiveManager에 노출됨"),ye.log("전역 객체 window.G7Core.useResponsive에 노출됨"),ye.log("전역 객체 window.G7Core.useControllableState에 노출됨"),ye.log("전역 객체 window.G7Core.updateQueryParams에 노출됨")}function FA(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,ye.log("전역 객체 window.G7Core 슬롯 API에 노출됨 (getSlotContext, getDynamicRenderer 등)")}function KA(o){o.identity={setLauncher:Et.setLauncher.bind(Et),handle:Et.handle.bind(Et),isIdentityRequired:Et.isIdentityRequired.bind(Et),hasLauncher:Et.hasLauncher.bind(Et),markDomainNoticeShown:Et.markDomainNoticeShown.bind(Et),redirectExternally:Et.redirectExternally.bind(Et),createDeferred:Et.createDeferred.bind(Et),resolveDeferred:Et.resolveDeferred.bind(Et),reset:Et.reset.bind(Et),redirectStashKey:jd},ye.log("전역 객체 window.G7Core 본인인증(IDV) API에 노출됨 (identity)")}function WA(o){if(typeof window>"u")return;TA(),window.G7Core||(window.G7Core={});const e=window.G7Core;kA(e),RA(e),DA(e),VA(e,o),OA(e,o),LA(e,o),MA(e),NA(e),IA(e),HA(e,o),jA(e),zA(e),UA(e),PA(e,o),BA(e,o),qA(e),GA(e),FA(e,o),KA(e),ye.log("G7Core 전역 객체 초기화 완료")}function YA(){const o=window.G7Core;if(!o){ye.warn("G7Core가 초기화되지 않았습니다.");return}const e=()=>Oa.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,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=n,ye.log("G7Core.devTools 인터페이스 초기화 완료")}let Lo=null;function XA(){const o=window.G7Core;return o?.__devtools?Promise.resolve(o.__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 JA(){const o=window.G7Core;if(!o){ye.warn("G7Core가 초기화되지 않았습니다.");return}const e=Oa.getInstance();if(e.initialize(),!e.isEnabled()){window.G7DevTools={isEnabled:()=>!1,enable:()=>{ye.log("환경설정 > 고급 설정 > 디버그 모드를 켜세요")}},ud(),ye.log("G7DevTools 비활성화됨 (디버그 모드 꺼짐)");return}QA(o,e)}async function QA(o,e){let n;try{n=await XA()}catch(g){ye.warn("DevTools 번들 로드 실패 — 최소 API 로 폴백:",g),window.G7DevTools={isEnabled:()=>!1,enable:()=>{}},ud();return}const{DiagnosticEngine:a,getServerConnector:i,getStyleTracker:l}=n;YA(),ud();try{l().enable(),ye.log("StyleTracker 활성화됨")}catch(g){ye.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){ye.warn("DataBindingEngine이 없습니다.");return}return m.evaluateExpression(g,e.getState())},getCacheStats:()=>e.getCacheStats(),clearStats:()=>{e.resetCacheStats(),ye.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,ye.log("G7DevTools 전역 객체 초기화 완료 (window.G7DevTools)"),ZA(n.DevToolsPanel)}function ZA(o){if(document.getElementById("g7-devtools-root")){ye.log("DevToolsPanel 이미 렌더링됨");return}const e=document.createElement("div");e.id="g7-devtools-root",document.body.appendChild(e);try{ld.createRoot(e).render(Xe.createElement(o)),ye.log("DevToolsPanel UI 렌더링 완료")}catch(n){ye.error("DevToolsPanel UI 렌더링 실패:",n)}}const Ze=ht("TemplateEngine"),ie={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 ex(o){try{Ze.log("템플릿 메타데이터 로드 중...",o);const e=o.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:o,locales:["ko","en"],name:{ko:o,en:o},description:{ko:"",en:""},version:"1.0.0",type:"admin"})}catch(e){return Ze.error("템플릿 메타데이터 로드 실패",e),{identifier:o,locales:["ko","en"],name:{ko:o,en:o},description:{ko:"",en:""},version:"1.0.0",type:"admin"}}}function Dy(){const e=(typeof window<"u"?window.__templateApp:void 0)?.globalState?.appConfig?.supportedLocales,n=ie.templateMetadata?.locales;return{$locale:ie.locale,$locales:Array.isArray(e)&&e.length>0?e:n||["ko","en"],$templateLocales:n||["ko","en"],$templateId:ie.templateId}}async function No(o){try{if(Ze.log("템플릿 엔진 초기화 시작",o),ie.isInitialized)throw new Error("템플릿 엔진이 이미 초기화되었습니다. destroyTemplate()을 먼저 호출하세요.");if(!o.templateId)throw new uA;Mo=o.debug??!1,typeof window<"u"&&(window.G7Config||(window.G7Config={}),window.G7Config.debug=Mo),JA(),ie.templateId=o.templateId,ie.locale=o.locale||"ko",Ze.log("엔진 인스턴스 생성 중..."),ie.registry=mr.getInstance(),ie.bindingEngine=new Dn,ie.translationEngine=_a.getInstance(),o.cacheVersion!==void 0&&o.cacheVersion>0&&ie.translationEngine.setCacheVersion(o.cacheVersion),ie.translationContext={templateId:o.templateId,locale:ie.locale},ie.actionDispatcher=new bo({},ie.translationEngine,ie.translationContext),xC(ie.actionDispatcher),Ze.log("다국어 파일 로드 중...",o.templateId,ie.locale);const e="en",n=[ie.translationEngine.loadTranslations(o.templateId,ie.locale).then(()=>{Ze.log(`다국어 파일 로드 완료: ${ie.locale}`)}).catch(a=>{Ze.warn(`다국어 파일 로드 실패 (${ie.locale}):`,a instanceof Error?a.message:a)})];ie.locale!==e&&n.push(ie.translationEngine.loadTranslations(o.templateId,e).then(()=>{Ze.log(`폴백 다국어 파일 로드 완료: ${e}`)}).catch(a=>{Ze.warn(`폴백 다국어 파일 로드 실패 (${e}):`,a instanceof Error?a.message:a)})),await Promise.allSettled(n),Ze.log("템플릿 메타데이터 로드 중...",o.templateId),ie.templateMetadata=await ex(o.templateId),ie.isInitialized=!0,Ze.log("템플릿 엔진 초기화 완료 (ComponentRegistry는 별도 로드)")}catch(e){throw Ze.error("템플릿 엔진 초기화 실패",e),ie.isInitialized=!1,ie.registry=null,ie.bindingEngine=null,ie.translationEngine=null,ie.actionDispatcher=null,e}}let Io=null;function Tf(){const o=window.G7Core;return o?.__LayoutEditorChrome?Promise.resolve(o.__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(o){try{if(Ze.log("템플릿 렌더링 시작",o),!ie.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 n=Dy();if(ie.containerId=o.containerId,ie.currentLayoutJson=o.layoutJson,ie.currentDataContext={...n,...o.dataContext||{}},ie.translationContext=o.translationContext||{templateId:ie.templateId||"",locale:ie.locale},ie.reactRoot||(Ze.log("React Root 생성"),ie.reactRoot=ld.createRoot(e)),typeof window<"u"){const l=_y(window.location.pathname);if(l){Ze.log("레이아웃 편집기 모드 진입",l);let c;try{c=await Tf()}catch(f){Ze.error("레이아웃 편집기 번들 로드 실패",f),Cf.render(o.containerId,{title:"레이아웃 편집기 로드 실패",message:f instanceof Error?f.message:"편집기 번들을 불러오지 못했습니다.",icon:"fas fa-triangle-exclamation",showStack:!1,showReloadButton:!0,debug:Mo});return}const d=Xe.createElement(c,{templateIdentifier:l.templateIdentifier,initialLocale:ie.locale});ie.reactRoot.render(Xe.createElement(vc,{translationEngine:ie.translationEngine,translationContext:ie.translationContext,children:Xe.createElement(Vd,{children:Xe.createElement(ic,{children:Xe.createElement(bf,{children:d})})})}));return}}const a=o.layoutJson.components||[];if(a.length===0){Ze.warn("렌더링할 컴포넌트가 없습니다.");return}const i=o.layoutJson.modals||[];if(Ze.log("modals 배열:",i),Ze.log("modals 개수:",i.length),Ze.log("DynamicRenderer로 렌더링 시작"),ie.reactRoot.render(Xe.createElement(vc,{translationEngine:ie.translationEngine,translationContext:ie.translationContext},Xe.createElement(Vd,null,Xe.createElement(ic,null,Xe.createElement(bf,null,[...a.map((l,c)=>{const d=ie.currentLayoutJson?.layout_name||"";return Xe.createElement(Ur,{key:d&&!l._fromBase?`${l.id}_${d}`:l.id,componentDef:l,dataContext:ie.currentDataContext,translationContext:ie.translationContext,registry:ie.registry,bindingEngine:ie.bindingEngine,translationEngine:ie.translationEngine,actionDispatcher:ie.actionDispatcher,isRootRenderer:c===0,layoutKey:d})}),Xe.createElement(mm,{key:"__modal_parent_context_provider"},i.map(l=>{const c=ie.currentDataContext._global?.modalStack||[],f=c.includes(l.id)||ie.currentDataContext._global?.activeModal===l.id,g=c.indexOf(l.id),m=g>=0?50+g:50,y=window.__g7LayoutContextStack||[],v=y[y.length-1]?.dataContext,_=Xe.createElement(Ur,{key:`modal_${l.id}_renderer`,componentDef:{...l,props:{...l.props,isOpen:f,style:{...l.props?.style,zIndex:m},onClose:ie.actionDispatcher?.createHandler({type:"click",handler:"closeModal"},ie.currentDataContext)}},dataContext:ie.currentDataContext,translationContext:ie.translationContext,registry:ie.registry,bindingEngine:ie.bindingEngine,translationEngine:ie.translationEngine,actionDispatcher:ie.actionDispatcher,parentDataContext:v});return l.data_sources&&l.data_sources.length>0?Xe.createElement(Cy,{key:`modal_${l.id}`,isOpen:f,modalId:l.id,dataSources:l.data_sources,dataContext:ie.currentDataContext,globalStateUpdater:ie.actionDispatcher?.getGlobalStateUpdater(),bindingEngine:ie.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(o,e){try{if(Ze.log("템플릿 데이터 업데이트 시작",Object.keys(o)),!ie.isInitialized)throw new Error("템플릿 엔진이 초기화되지 않았습니다.");if(!ie.reactRoot||!ie.currentLayoutJson){Ze.log("React 미준비 - 데이터 업데이트 큐잉:",Object.keys(o)),$o.push({data:o,options:e});return}const n=Dy(),a={...ie.currentDataContext._global||{},...o._global||{}},i=MC(ie.currentDataContext._localInit,o._localInit);ie.currentDataContext={...n,...ie.currentDataContext,...o,_global:a,_localInit:i},a._local!==void 0&&(ie.currentDataContext._local=a._local),a._computed!==void 0&&(ie.currentDataContext._computed=a._computed);const l=ie.currentLayoutJson.components||[],c=ie.currentLayoutJson.modals||[];Ze.log("updateTemplateData - modals 배열:",c),Ze.log("updateTemplateData - modals 개수:",c.length),Ze.log("updateTemplateData - activeModal:",ie.currentDataContext._global?.activeModal);const d=()=>{ie.reactRoot.render(Xe.createElement(vc,{translationEngine:ie.translationEngine,translationContext:ie.translationContext},Xe.createElement(Vd,null,Xe.createElement(ic,null,Xe.createElement(bf,null,[...l.map((f,g)=>{const m=ie.currentLayoutJson?.layout_name||"";return Xe.createElement(Ur,{key:m&&!f._fromBase?`${f.id}_${m}`:f.id,componentDef:f,dataContext:ie.currentDataContext,translationContext:ie.translationContext,registry:ie.registry,bindingEngine:ie.bindingEngine,translationEngine:ie.translationEngine,actionDispatcher:ie.actionDispatcher,isRootRenderer:g===0,layoutKey:m})}),Xe.createElement(mm,{key:"__modal_parent_context_provider_update"},c.map(f=>{const g=ie.currentDataContext._global?.modalStack||[],y=g.includes(f.id)||ie.currentDataContext._global?.activeModal===f.id,S=g.indexOf(f.id),v=S>=0?50+S:50,_=window.__g7LayoutContextStack||[],x=_[_.length-1]?.dataContext,O=Xe.createElement(Ur,{key:`modal_${f.id}_renderer`,componentDef:{...f,props:{...f.props,isOpen:y,style:{...f.props?.style,zIndex:v},onClose:ie.actionDispatcher?.createHandler({type:"click",handler:"closeModal"},ie.currentDataContext)}},dataContext:ie.currentDataContext,translationContext:ie.translationContext,registry:ie.registry,bindingEngine:ie.bindingEngine,translationEngine:ie.translationEngine,actionDispatcher:ie.actionDispatcher,parentDataContext:x});return f.data_sources&&f.data_sources.length>0?Xe.createElement(Cy,{key:`modal_${f.id}`,isOpen:y,modalId:f.id,dataSources:f.data_sources,dataContext:ie.currentDataContext,globalStateUpdater:ie.actionDispatcher?.getGlobalStateUpdater(),bindingEngine:ie.bindingEngine,debug:Mo,children:O}):O}))])))))};e?.sync?(Ze.log("재렌더링 시작 (sync mode - 즉시 렌더링)"),d()):(Ze.log("재렌더링 시작 (with startTransition)"),H.startTransition(()=>{d()})),Ze.log("템플릿 데이터 업데이트 완료")}catch(n){throw Ze.error("템플릿 데이터 업데이트 실패",n),n}}function xc(){try{Ze.log("템플릿 정리 시작"),ie.reactRoot&&(Ze.log("React Root 언마운트"),ie.reactRoot.unmount(),ie.reactRoot=null),ie.templateId=null,ie.locale="ko",ie.isInitialized=!1,ie.containerId=null,ie.currentLayoutJson=null,ie.currentDataContext={},ie.translationContext={templateId:"",locale:"ko"},ie.registry=null,ie.bindingEngine=null,ie.translationEngine=null,ie.actionDispatcher=null,ie.templateMetadata=null,Ze.log("템플릿 정리 완료")}catch(o){throw Ze.error("템플릿 정리 실패",o),o}}function ea(){return Object.freeze({...ie})}function Oy(){return ie.actionDispatcher}const Ho={initTemplateEngine:No,renderTemplate:ms,updateTemplateData:ys,destroyTemplate:xc,getState:ea};typeof window<"u"&&(window.G7Core||(window.G7Core={}),window.G7Core.TemplateEngine=Ho,WA({getState:()=>({translationEngine:ie.translationEngine,translationContext:ie.translationContext,bindingEngine:ie.bindingEngine,actionDispatcher:ie.actionDispatcher,templateMetadata:ie.templateMetadata}),transitionManager:jr,responsiveManager:hi,webSocketManager:Cc}),Ze.log("전역 객체 window.G7Core.TemplateEngine에 노출됨"));const Pr=Object.freeze(Object.defineProperty({__proto__:null,DataSourceManager:_r,LayoutLoader:mf,TemplateApp:Do,TemplateEngine:Ho,default:Ho,destroyTemplate:xc,getActionDispatcher:Oy,getState:ea,initTemplateApp:xf,initTemplateEngine:No,loadLayoutEditorBundle:Tf,renderTemplate:ms,updateTemplateData:ys},Symbol.toStringTag,{value:"Module"}));Qt.DataSourceManager=_r,Qt.LayoutLoader=mf,Qt.TemplateApp=Do,Qt.TemplateEngine=Ho,Qt.default=Ho,Qt.destroyTemplate=xc,Qt.getActionDispatcher=Oy,Qt.getState=ea,Qt.initTemplateApp=xf,Qt.initTemplateEngine=No,Qt.loadLayoutEditorBundle=Tf,Qt.renderTemplate=ms,Qt.updateTemplateData=ys,Object.defineProperties(Qt,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})})(this.G7Core=this.G7Core||{}); + `}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 xc{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`
${l}
`}).join(""),requestAnimationFrame(()=>{e&&this.adjustAppPadding(e.offsetHeight)})}static adjustAppPadding(e){const n=document.getElementById("app");n&&(n.style.paddingTop=e>0?`${e}px`:"")}}$(xc,"banners",new Map),$(xc,"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}),xc.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),xc.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(fd()){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=od.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=od.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='
'}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")),Tc(),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;h0?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&&(Ec.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={...ld,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:Sc,useTranslation:Ay,ResponsiveContext:rf,ResponsiveProvider:sc,useResponsive:af,responsiveManager:mi,BREAKPOINT_PRESETS:Ul,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;c0&&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&&(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("환경설정 > 고급 설정 > 디버그 모드를 켜세요")}},ud(),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:()=>{}},ud();return}const{DiagnosticEngine:a,getServerConnector:i,getStyleTracker:l}=n;ux(),ud();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{ld.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=ld.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(Sc,{translationEngine:ae.translationEngine,translationContext:ae.translationContext,children:Ye.createElement(tf,{children:Ye.createElement(sc,{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(Sc,{translationEngine:ae.translationEngine,translationContext:ae.translationContext},Ye.createElement(tf,null,Ye.createElement(sc,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(Sc,{translationEngine:ae.translationEngine,translationContext:ae.translationContext},Ye.createElement(tf,null,Ye.createElement(sc,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 Tc(){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:Tc,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:Ec}),Ze.log("전역 객체 window.G7Core.TemplateEngine에 노출됨"));const Br=Object.freeze(Object.defineProperty({__proto__:null,DataSourceManager:xr,LayoutLoader:xf,TemplateApp:Do,TemplateEngine:jo,default:jo,destroyTemplate:Tc,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=Tc,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((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): 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): 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('../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 = {}): any => + (a as any).evaluateComputedExpression(expr, ctx); + + // scripts[].if 싱크: {{...}} 형태의 조건을 받아 boolean 을 돌려준다 + const condition = (a: TemplateApp, cond: string, ctx: Record = {}): boolean => + (a as any).evaluateScriptCondition(cond, ctx); + + beforeEach(() => { + document.body.innerHTML = '
'; + 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('../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 = '
'; + 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/template-engine/CHANGELOG.md b/resources/js/core/template-engine/CHANGELOG.md index 717a5852..6f190494 100644 --- a/resources/js/core/template-engine/CHANGELOG.md +++ b/resources/js/core/template-engine/CHANGELOG.md @@ -5,6 +5,78 @@ > > 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) +## [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 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 = new Map(); - /** - * 표현식 함수 캐시 - * - * key: 전처리된 표현식, value: 컴파일된 Function - * - Function 생성자 비용이 높으므로 동일 표현식 재사용 시 캐시에서 조회 - * - 표현식 자체만 캐싱하고, 컨텍스트 값은 실행 시 전달 - */ - private expressionFnCache: Map = 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..8e2ecae6 --- /dev/null +++ b/resources/js/core/template-engine/SafeExpressionEvaluator.ts @@ -0,0 +1,1856 @@ +/** + * 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(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; + + 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)[key]); + } + } + + return dest; + }, + freeze: (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 = { + 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 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 = 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 }; + +/** 함수/화살표 파라미터 (기본값 지원) */ +interface Param { + name: string; + 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; + 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 = { + '??': 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 : ''}"`); + } + 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(); + if (!p || p.type !== 'ident' || FORBIDDEN_KEYWORDS.has(String(p.value))) { + return null; + } + const name = String(p.value); + this.pos += 1; + let def: Node | null = null; + if (this.isPunct('=')) { + this.pos += 1; // = + // 기본값은 assignment 레벨 식(중첩 arrow 허용). parseExpression 은 top-level + // 콤마를 소비하지 않으므로 다음 파라미터 구분자 ',' 는 그대로 남는다. + def = this.parseExpression(); + } + params.push({ name, default: def }); + if (this.isPunct(',')) { + this.pos += 1; + continue; + } + break; + } + return params; + } + + 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 : ''}" 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)[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)[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) 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); + } + 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) out.push(x); + } + } else { + out.push(evalNode(el, scope)); + } + } + return out; + } + + case 'Object': { + const out: Record = {}; + 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)) { + defineOwn(out, k, (v as Record)[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)[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) { + 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, 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): 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..dc5d8c86 --- /dev/null +++ b/resources/js/core/template-engine/__tests__/SafeExpressionEvaluator.test.ts @@ -0,0 +1,804 @@ +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 = {}): 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)[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('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; + expect(result._tab).toBe('basic_info'); + expect((result.inquiry as Record).board_slug).toBe('inquiry'); + expect((result.basic_info as Record).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).pwned; + delete (Array.prototype as Record).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).pwned).toBeUndefined(); + expect(({} as Record).pwned).toBeUndefined(); + }); + + /** @effects sandbox_escape_blocked */ + it('배열 프로토타입도 동일하게 보호된다', () => { + expect(() => evalx("[].__lookupGetter__('__pro' + 'to__').call([])")).toThrow(); + expect((Array.prototype as Record).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 = { 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..660ea9e2 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`] = `471`; 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']", 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 b7ad1c4a..b0f75ec2 100644 --- a/resources/js/core/template-engine/__tests__/troubleshooting-components.test.ts +++ b/resources/js/core/template-engine/__tests__/troubleshooting-components.test.ts @@ -1605,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 (렌더 결과 검증) @@ -1624,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/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 = {}): 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 컴포넌트명 형식의 태그 배지 표시 (
,