Merge pull request from gnuboard:HeuJung/issue486

HeuJung/issue486
This commit is contained in:
정정홍 authored and GitHub committed 2026-07-23 22:31:51 +09:00
commit 9dda22f8e4
127 files changed
+6714 -832

No files matched your search

+12
View File
@@ -301,6 +301,18 @@ Icon 은 `<i>` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은
| `AuthManager.updateConfig({ loginPath: '//evil.com/...' })` (protocol-relative) | `//` 시작 금지 (open redirect 방지) |
| 401 에러 페이지(`errors/401.json`)에서 직접 로그인 리다이렉트 구현 | 코어 `TemplateApp.showRouteError` 가드에 위임 (자동 처리) |
### 정적 확장자 라우트 / 자산 URL 생성
| 금지 | 올바른 사용 |
|------|------------|
| `Route::get('{id}/routes.json', ...)` (`.js`/`.css`/`.json`/`.map` 단일 등록) | `Route::dualSuffix('{id}/routes', 'json', ...)` — 확장자 형태 + 확장자 없는 형태 동시 등록 |
| `Route::get('bundle.js', ...)` (접미사가 종류를 구분해 제거 불가) | `Route::dualSuffixSegment('bundle', 'js', ...)` (`bundle.js` + `bundle/js`) |
| `Route::get('assets/{id}/{path}', ...)` (와일드카드 자산) | `Route::dualAsset('assets/{id}', ...)` (`.../{path}` + `?file=` 쿼리) |
| 서버에서 `'/api/templates/assets/'.$id.'/'.$path` 문자열 조립 | `App\Support\AssetUrl::templateAsset($id, $path)` |
| 프론트에서 `` `/api/templates/${id}/routes.json` `` 템플릿 리터럴 조립 | `resources/js/core/support/assetUrl.ts` 의 `suffixed()` / `templateAsset()` 등 |
정규식 location 은 프리픽스 location 보다 먼저 매칭되므로, 정적 최적화 블록(`location ~* \.(js|css|json)$`)이 있는 서버에서는 확장자 붙은 동적 응답이 `try_files ... /index.php` 폴백 기회 없이 404 가 된다. 서버측 `AssetUrl` 과 프론트측 `assetUrl.ts` 는 동일 규칙을 공유하므로 한쪽만 바꾸면 그 자산만 404 가 된다. 상세: [routing.md](docs/backend/routing.md) "정적 확장자로 끝나는 동적 엔드포인트", [api/README.md](docs/backend/api/README.md) "자산 URL 이중 모드".
### Listener 데이터 접근
| 금지 | 올바른 사용 |
+5
View File
@@ -18,6 +18,8 @@
- 관리자가 사이트맵 전체 재생성을 실행하면, 완료되거나 실패했을 때 실행한 관리자에게 알림을 보냅니다. 재생성이 오래 걸려 화면을 떠나 있어도 결과를 놓치지 않습니다. (기본은 앱 내 알림이며 관리자 알림 설정에서 이메일도 켤 수 있습니다. 매일 자동 생성이나 글·상품 변경에 따른 재생성은 알림을 보내지 않습니다.)
- 설치할 때 `root` 같은 데이터베이스 최고 권한 계정을 입력하면 설치가 진행되지 않고, 왜 사용할 수 없는지와 어떻게 해야 하는지를 함께 안내합니다. 최고 권한 계정 정보가 유출되면 데이터베이스 전체가 위험해지기 때문이며, 사이트 전용 데이터베이스 계정을 새로 만들어 필요한 권한만 부여해 입력하시면 됩니다. 읽기 전용 데이터베이스를 따로 쓰는 경우에도 동일하게 적용됩니다.
- 일부 서버 설정에서 모든 페이지가 백지로 나오던 문제에 대응했습니다. 서버에 `.js`·`.css`·`.json` 주소를 가로채는 정적 파일 최적화 설정(aaPanel·CyberPanel·Plesk 기본 템플릿 등)이 있으면 G7 의 일부 주소가 PHP 에 전달되지 못해 화면이 뜨지 않았습니다. 이제 G7 이 확장자 없는 주소도 함께 제공하며, 화면이 뜨지 않으면 브라우저가 이를 감지해 자동으로 전환하므로 서버 설정을 바꾸지 않아도 사이트가 표시됩니다. (sir.kr 커뮤니티의 hang 님께서 제보해주셨습니다.)
- 자산 파일 주소 방식을 설치 마법사가 자동으로 감지해 반영하고, 설치 후에도 관리자 > 환경설정 > 일반에서 바꿀 수 있습니다. 설치 2단계 서버 요구사항 화면에서 감지 결과를 미리 확인할 수 있고, 3단계에서 직접 고를 수도 있습니다. 화면이 아예 뜨지 않는 상황을 위해 `php artisan g7:asset-url-mode` 명령으로도 확인·전환할 수 있습니다. 검색엔진 봇은 자바스크립트를 실행하지 않으므로, 이 설정을 확정해 두면 봇에게도 올바른 주소가 전달됩니다.
- 서버에 OPcache가 켜져 있는지를 설치 화면과 관리자 환경설정 > 정보에서 확인할 수 있습니다. OPcache는 PHP 코드 해석 결과를 재사용해 사이트 응답 속도를 크게 높여 주는 기능으로, 꺼져 있으면 성능이 떨어진다는 안내와 함께 설정 방법을 알려 드립니다. 꺼져 있어도 설치는 그대로 진행되므로 설치가 막히지 않습니다.
### Changed
@@ -26,6 +28,8 @@
### Fixed
- 데이터베이스 연결이 일시적으로 끊기거나 확장 설치가 중간에 실패한 직후 확장 목록을 다시 만들면, 설치된 모듈·플러그인 정보가 통째로 비워지면서 게시판·쇼핑몰 등 모든 확장 화면이 500 오류를 내던 문제를 수정했습니다. 이제 확장 폴더가 그대로 남아 있는데 목록만 비어 있으면 기존 정보를 지우지 않고 보존하며, 왜 갱신하지 않았는지 로그에 남깁니다.
- 확장 목록이 손상된 상태에서 복구 명령을 실행하면 목록은 되살아나지만 확장 기능 연결이 비어 있는 채로 남아, 명령을 두 번 실행해야 완전히 복구되던 문제를 수정했습니다. 이제 한 번 실행으로 모두 복구됩니다.
- 데이터베이스 계정 설정이 잘못되었을 때 아무 설명 없이 빈 화면만 뜨던 문제를 수정했습니다. 이전에는 최고 권한 계정을 쓰거나 사용자명이 비어 있으면 사이트 기능이 통째로 동작을 멈추면서도 화면에는 아무 안내가 없어 원인을 알 수 없었습니다. 이제 무엇이 잘못되었고 어떻게 고쳐야 하는지 설명하는 전용 안내 화면이 나타납니다. 서버 관리 명령은 평소대로 실행되므로 이 상태에서도 설정을 고칠 수 있습니다.
- SEO 설정의 캐시 항목(SEO 캐시 사용·유지 시간, Sitemap 캐시 유지 시간)이 저장해도 적용되지 않던 문제를 수정했습니다. 이제 고급 설정의 캐시 값이 기준이 되고, SEO 설정에서 값을 지정하면 그 값이 우선하며, 비워두면 고급 설정을 따릅니다. 업그레이드 시 기존에 기본값과 다르게 지정해 두셨던 값은 그대로 살아나 이제부터 실제로 적용되므로 SEO 캐시 동작이 달라질 수 있습니다. 기본값 그대로였던 항목은 "지정 안 함"으로 정리되어 동작이 바뀌지 않습니다.
- 한글로 쓴 설명이 글자 수 제한에 걸리지 않는데도 "너무 깁니다" 로 거부되던 문제를 수정했습니다. 글자 수를 바이트 단위로 세는 바람에 한글 한 글자가 세 글자로 계산되어, 500자 제한에서 167자만 넘어도 저장이 막혔습니다. 역할·권한·모듈·플러그인·템플릿 설명에 모두 해당합니다.
@@ -36,6 +40,7 @@
- 일부 안내가 실제 문장 대신 내부 식별자(`messages.auth.unauthenticated` 같은 문자열)로 표시되던 문제를 수정했습니다. 로그인하지 않았거나 역할 권한이 없어 요청이 거절될 때, 메뉴에 역할을 지정할 때, 확장 식별자를 잘못 입력했을 때, 활동 로그를 기간·유형으로 검색할 때가 해당됩니다.
- 플러그인을 GitHub 주소나 ZIP 파일로 설치할 때의 입력 안내가 내부 식별자로 표시되던 문제를 수정했습니다. 주소 형식이 잘못됐거나, ZIP이 아닌 파일을 올렸거나, 파일 용량을 초과했을 때 정상 문장으로 안내합니다.
- 레이아웃 편집기 화면이 정상 동작하는데도 서버가 "페이지 없음"(404)으로 응답하던 문제를 수정했습니다. 화면은 그대로 보였지만 브라우저 캐싱과 서버 접속 기록에서 없는 페이지로 취급되던 상태입니다.
- 주소 끝에 슬래시가 붙은 형태(예: `/admin/`)로 접속하면 화면이 "페이지 없음"(404)으로 뜨던 문제를 수정했습니다. 특히 로그인이 풀린 상태에서 이렇게 접속하면 로그인 화면으로 넘어가지 않고 404가 나타났습니다. 이제 끝 슬래시가 있어도 정상적으로 같은 페이지로 연결됩니다.
## [7.0.5] - 2026-07-16
+43
View File
@@ -77,6 +77,49 @@ server {
}
```
#### 정적 파일 최적화 블록을 함께 쓰는 경우
아래처럼 확장자로 캐시 규칙을 거는 블록(aaPanel · CyberPanel · Plesk 기본 템플릿에
포함되어 있습니다)을 함께 사용한다면 주의가 필요합니다.
```nginx
location ~* \.(js|css|json|png|jpg|svg|woff2?)$ {
expires max;
access_log off;
}
```
nginx 는 **정규식 location 을 프리픽스 location(`location /`)보다 먼저** 매칭합니다.
G7 은 일부 동적 엔드포인트에 `.js` · `.css` · `.json` 확장자를 쓰므로, 위 블록 안에
PHP 핸들러가 없으면 그 요청들이 `try_files ... /index.php` 폴백에 닿지 못하고
nginx 가 직접 파일을 찾다가 404 를 반환합니다. 증상은 **모든 페이지가 백지**입니다.
해결 방법은 두 가지이며, 아무것도 하지 않아도 G7 이 스스로 복구합니다.
1. **그대로 두기** — 브라우저가 자산 로드 실패를 감지하면 확장자 없는 주소로 자동
전환합니다. 설치 마법사도 설치 시점에 이를 감지해 설정에 반영합니다.
설치 후 확정하려면 `php artisan g7:asset-url-mode extensionless` 를 실행하거나
관리자 > 환경설정 > 일반에서 "자산 파일 주소 방식" 을 변경하세요.
(검색엔진 봇은 JavaScript 를 실행하지 않으므로, SEO 를 쓴다면 이 확정을 권장합니다.)
2. **`/api/` 를 정규식 블록보다 우선시키기** — `^~` 는 정규식 location 보다 우선하므로
아래 블록을 추가하면 `/api/` 요청이 항상 PHP 로 갑니다. 확장자 기반 캐시 최적화를
정적 파일에만 그대로 유지할 수 있습니다.
```nginx
location ^~ /api/ {
try_files $uri $uri/ /index.php?$query_string;
}
```
서버가 동적 응답을 가로채는지는 아래 두 요청을 비교해 확인할 수 있습니다.
첫 번째만 실패하면 가로채는 것입니다.
```bash
curl -i https://example.com/api/system/asset-probe.js
curl -i https://example.com/api/system/asset-probe
```
### 3단계: 설치 마법사 실행
브라우저에서 접속합니다.
@@ -0,0 +1,125 @@
<?php
namespace App\Console\Commands;
use App\Seo\Contracts\SeoCacheManagerInterface;
use App\Services\SettingsService;
use App\Support\AssetUrl;
use Illuminate\Console\Command;
/**
* 자산 URL 모드 조회/전환 Artisan 커맨드 (이슈 #486 §9)
*
* 정적 최적화 블록(`location ~* \.(js|css|json)$`)이 동적 응답을 가로채는 서버에서는
* **관리자 화면 자체가 뜨지 않는다**. "브라우저로 설정을 바꾸세요" 는 순환 참조이므로
* CLI 탈출구가 반드시 필요하다.
*
* ```
* php artisan g7:asset-url-mode # 현재 모드 + 진단 안내
* php artisan g7:asset-url-mode extensionless # 전환
* ```
*/
class AssetUrlModeCommand extends Command
{
/**
* @var string 커맨드 시그니처
*/
protected $signature = 'g7:asset-url-mode
{mode? : 전환할 모드 (extension | extensionless). 생략 시 현재 모드만 출력}';
/**
* @var string 커맨드 설명
*/
protected $description = '자산 URL 모드를 조회하거나 전환합니다 (정적 최적화 서버 대응)';
/**
* 커맨드를 실행합니다.
*
* @param SettingsService $settingsService 환경설정 서비스
* @param SeoCacheManagerInterface $seoCacheManager SEO 캐시 매니저
* @return int 종료 코드
*/
public function handle(SettingsService $settingsService, SeoCacheManagerInterface $seoCacheManager): int
{
$current = AssetUrl::mode();
$requested = $this->argument('mode');
if ($requested === null) {
$this->showStatus($current);
return Command::SUCCESS;
}
if (! in_array($requested, [AssetUrl::MODE_EXTENSION, AssetUrl::MODE_EXTENSIONLESS], true)) {
$this->error(__('asset_url_mode.invalid', ['mode' => $requested]));
return Command::FAILURE;
}
if ($requested === $current) {
$this->info(__('asset_url_mode.already', ['mode' => $current]));
return Command::SUCCESS;
}
$saved = $settingsService->saveSettings([
'_tab' => 'general',
'general' => ['asset_url_mode' => $requested],
]);
if (! $saved) {
$this->error(__('asset_url_mode.save_failed'));
return Command::FAILURE;
}
// SEO 프리렌더 캐시에는 생성 시점의 자산 URL 이 그대로 구워져 있다.
// 모드를 바꾸면 그 URL 들이 전부 어긋나므로 함께 비운다 (계획서 §알려진 한계).
$seoCacheManager->clearAll();
$this->info(__('asset_url_mode.switched', ['from' => $current, 'to' => $requested]));
$this->line(__('asset_url_mode.seo_cache_cleared'));
return Command::SUCCESS;
}
/**
* 현재 모드와 진단 안내를 출력합니다.
*
* @param string $current 현재 모드
*/
private function showStatus(string $current): void
{
$this->line('');
$this->line(' '.__('asset_url_mode.current', ['mode' => $current]));
$this->line('');
// audit:allow asset-url-builder-required reason: 두 모드의 URL 형태를 나란히
// 보여주는 대조표라 빌더로 생성할 수 없다 (빌더는 현재 모드 하나만 만든다).
$rows = $current === AssetUrl::MODE_EXTENSION
? [
['/api/templates/{id}/routes.json', '/api/templates/{id}/routes'],
['/api/modules/bundle.js', '/api/modules/bundle/js'],
['/api/templates/assets/{id}/js/a.js', '/api/templates/assets/{id}?file=js/a.js'],
]
: [
['/api/templates/{id}/routes', '/api/templates/{id}/routes.json'],
['/api/modules/bundle/js', '/api/modules/bundle.js'],
['/api/templates/assets/{id}?file=js/a.js', '/api/templates/assets/{id}/js/a.js'],
];
$this->table(
[__('asset_url_mode.table.in_use'), __('asset_url_mode.table.alternative')],
$rows,
);
$this->line(' '.__('asset_url_mode.diagnose_title'));
$this->line(' curl -i '.rtrim(config('app.url'), '/').'/api/system/asset-probe.js');
$this->line(' curl -i '.rtrim(config('app.url'), '/').'/api/system/asset-probe');
$this->line('');
$this->line(' '.__('asset_url_mode.diagnose_hint'));
$this->line('');
$this->line(' '.__('asset_url_mode.switch_hint'));
$this->line('');
}
}
@@ -41,6 +41,12 @@ class UpdateAutoloadCommand extends Command
$this->info('오토로드 파일이 성공적으로 생성되었습니다.');
$this->line(' → bootstrap/cache/autoload-extensions.php');
// 방금 생성한 PSR-4 매핑을 현재 프로세스의 ClassLoader 에 즉시 반영.
// 이 커맨드는 매핑이 깨진 상태를 복구하는 경로이기도 한데, 재등록 없이 아래
// loadModules/loadPlugins 를 호출하면 부팅 시점의 (깨진) 매핑으로 확장 클래스를
// 찾지 못해 훅 캐시가 modules/plugins 0건으로 생성된다 — 모든 확장 훅이 조용히 소실.
$extensionManager->reregisterRuntimeAutoload();
// 오토로드 캐시와 동일 생명주기의 정적 훅 매핑 캐시도 함께 재생성.
// 코어 업데이트(clearAllCaches → 이 커맨드) 시 코어 리스너 추가/변경/삭제가
// 캐시에 반영되도록 보장한다 (미갱신 시 stale 매핑으로 훅 발화 누락/과잉 위험).
+73 -1
View File
@@ -106,7 +106,7 @@ class ExtensionManager
* 매핑을 즉시 유효화한다. 기존 매핑에 경로가 추가되거나 새 네임스페이스가
* 등록되며, 동일 매핑은 중복 없이 merge 된다.
*/
protected function reregisterRuntimeAutoload(): void
public function reregisterRuntimeAutoload(): void
{
if (! class_exists(ClassLoader::class, false)) {
return;
@@ -135,6 +135,14 @@ class ExtensionManager
*/
public function generateAutoloadFile(): void
{
// 테스트 환경에서는 실 캐시 파일을 재생성하지 않는다.
// testing DB 의 활성 확장 구성은 개발 DB 와 다르므로, 어떤 경로로든 실 캐시가
// 재생성되면 개발 환경 PSR-4 매핑이 testing 기준으로 교체되어 모든 모듈/플러그인
// 라우트가 500 이 된다. 생성 로직 자체를 검증하는 테스트는 경로를 치환해 사용한다.
if ($this->writesRealCacheFileDuringTests()) {
return;
}
// 모듈별 오토로드 수집
$moduleAutoloads = $this->collectModuleAutoloads();
@@ -170,6 +178,24 @@ class ExtensionManager
// 즉시 경로를 반환하도록 한다(성능). 클래스 로딩은 여전히 lazy — 사용 시점에만 include.
$srcClassmap = $this->buildSourceClassmap($psr4);
// 공집합 산출물로 정상 매핑을 덮어쓰지 않는다.
// 이 매핑은 "DB 설치 목록 × 디스크 활성 디렉토리" 교집합이라, DB 쪽만 일시적으로
// 비어도(테이블 부재 · 설치 실패로 행 미기록 · 다른 DB 접속) 교집합이 공집합이 된다.
// 디스크에 설치 형태의 확장 디렉토리가 남아 있는데 매핑이 0건이면 DB 쪽 조회를
// 신뢰할 수 없다는 뜻이므로, 기존 파일을 보존하고 경고만 남긴다.
// (정상적인 0건 — 신규 설치 직후 · 마지막 확장 삭제 — 은 디스크도 함께 비어 통과)
$extensionDirectories = empty($psr4) ? $this->findInstalledExtensionDirectories() : [];
if (empty($psr4) && ! empty($extensionDirectories)) {
Log::warning('확장 오토로드 매핑이 0건으로 산출되어 기존 캐시를 보존합니다', [
'path' => $this->autoloadFilePath,
'extension_directories' => $extensionDirectories,
'hint' => '확장 테이블(modules/plugins) 조회 결과가 비어 있습니다. DB 연결·마이그레이션 상태를 확인한 뒤 extension:update-autoload 를 다시 실행하세요.',
]);
return;
}
// 파일 내용 생성
$content = $this->buildAutoloadFileContent($psr4, $classmap, $files, $vendorAutoloads, $srcClassmap);
@@ -192,6 +218,52 @@ class ExtensionManager
]);
}
/**
* 현재 호출이 테스트 환경에서 실 캐시 파일을 건드리는지 판정합니다.
*
* 경로가 치환된(임시 경로) 인스턴스는 생성 로직 검증용이므로 통과시킨다.
*
* @return bool 테스트 환경에서 실 캐시 파일을 쓰려는 경우 true
*/
protected function writesRealCacheFileDuringTests(): bool
{
return app()->environment('testing')
&& $this->autoloadFilePath === base_path('bootstrap/cache/autoload-extensions.php');
}
/**
* 디스크에서 설치 형태를 갖춘 확장 디렉토리를 찾습니다.
*
* `_bundled` / `_pending` 등 내부 디렉토리는 제외하고, composer.json 을 가진
* 활성 디렉토리만 센다 (설치된 확장이 남긴 흔적).
*
* @return array<int, string> 확장 식별자 목록 (예: `modules/sirsoft-board`)
*/
protected function findInstalledExtensionDirectories(): array
{
$found = [];
foreach (['modules' => $this->modulesPath, 'plugins' => $this->pluginsPath] as $type => $basePath) {
if (! File::isDirectory($basePath)) {
continue;
}
foreach (File::directories($basePath) as $dir) {
$name = basename($dir);
if (str_starts_with($name, '_')) {
continue;
}
if (File::exists($dir.'/composer.json')) {
$found[] = $type.'/'.$name;
}
}
}
return $found;
}
/**
* 오토로드 파일 내용을 생성합니다.
*
+23 -11
View File
@@ -41,6 +41,7 @@ use App\Models\Plugin;
use App\Models\Template;
use App\Providers\CoreServiceProvider;
use App\Services\LayoutExtensionService;
use App\Support\AssetUrl;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
@@ -1066,8 +1067,8 @@ class ModuleManager implements ModuleManagerInterface
if (isset($builtPaths['js']) || isset($builtPaths['css'])) {
$assets = [
'js' => isset($builtPaths['js']) ? "/api/modules/assets/{$identifier}/".$builtPaths['js'] : null,
'css' => isset($builtPaths['css']) ? "/api/modules/assets/{$identifier}/".$builtPaths['css'] : null,
'js' => isset($builtPaths['js']) ? AssetUrl::moduleAsset($identifier, $builtPaths['js']) : null,
'css' => isset($builtPaths['css']) ? AssetUrl::moduleAsset($identifier, $builtPaths['css']) : null,
'priority' => $loadingConfig['priority'] ?? 100,
];
}
@@ -1156,8 +1157,8 @@ class ModuleManager implements ModuleManagerInterface
if (isset($builtPaths['js']) || isset($builtPaths['css'])) {
$assets = [
'js' => isset($builtPaths['js']) ? "/api/modules/assets/{$identifier}/".$builtPaths['js'] : null,
'css' => isset($builtPaths['css']) ? "/api/modules/assets/{$identifier}/".$builtPaths['css'] : null,
'js' => isset($builtPaths['js']) ? AssetUrl::moduleAsset($identifier, $builtPaths['js']) : null,
'css' => isset($builtPaths['css']) ? AssetUrl::moduleAsset($identifier, $builtPaths['css']) : null,
'priority' => $loadingConfig['priority'] ?? 100,
];
}
@@ -1304,8 +1305,8 @@ class ModuleManager implements ModuleManagerInterface
if (isset($builtPaths['js']) || isset($builtPaths['css'])) {
$assets = [
'js' => isset($builtPaths['js']) ? "/api/modules/assets/{$identifier}/".$builtPaths['js'] : null,
'css' => isset($builtPaths['css']) ? "/api/modules/assets/{$identifier}/".$builtPaths['css'] : null,
'js' => isset($builtPaths['js']) ? AssetUrl::moduleAsset($identifier, $builtPaths['js']) : null,
'css' => isset($builtPaths['css']) ? AssetUrl::moduleAsset($identifier, $builtPaths['css']) : null,
'priority' => $loadingConfig['priority'] ?? 100,
];
}
@@ -1427,11 +1428,22 @@ class ModuleManager implements ModuleManagerInterface
return null;
}
try {
require_once $moduleFile;
$namespace = $this->convertDirectoryToNamespace($moduleName);
$moduleClass = "Modules\\{$namespace}\\Module";
$namespace = $this->convertDirectoryToNamespace($moduleName);
$moduleClass = "Modules\\{$namespace}\\Module";
try {
// 같은 모듈의 활성 디렉토리 사본이 이미 로드돼 있으면 `_bundled`/`_pending`
// 파일을 그대로 require 할 수 없다 — 같은 FQN 을 두 번 선언하게 되어
// "Cannot declare class ..., because the name is already in use" 로 죽는다.
// 이것은 Error 라 아래 catch(\Exception) 에 걸리지 않아 프로세스가 그대로 종료된다.
//
// 파일 내용을 임시 클래스명으로 eval 해 번들 쪽 메타데이터를 얻는다
// (loadModuleFromDirectory / getFreshModuleInstance 와 동일한 처리).
if (class_exists($moduleClass, false)) {
return $this->evalFreshModule($moduleFile, $moduleClass, dirname($moduleFile));
}
require_once $moduleFile;
if (class_exists($moduleClass)) {
$module = new $moduleClass;
@@ -1439,7 +1451,7 @@ class ModuleManager implements ModuleManagerInterface
return $module;
}
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
Log::debug("Failed to load bundled module instance for {$moduleName}: ".$e->getMessage());
}
+22 -11
View File
@@ -41,6 +41,7 @@ use App\Models\Template;
use App\Providers\CoreServiceProvider;
use App\Services\DriverRegistryService;
use App\Services\LayoutExtensionService;
use App\Support\AssetUrl;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
@@ -1094,8 +1095,8 @@ class PluginManager implements PluginManagerInterface
if (isset($builtPaths['js']) || isset($builtPaths['css'])) {
$assets = [
'js' => isset($builtPaths['js']) ? "/api/plugins/assets/{$identifier}/".$builtPaths['js'] : null,
'css' => isset($builtPaths['css']) ? "/api/plugins/assets/{$identifier}/".$builtPaths['css'] : null,
'js' => isset($builtPaths['js']) ? AssetUrl::pluginAsset($identifier, $builtPaths['js']) : null,
'css' => isset($builtPaths['css']) ? AssetUrl::pluginAsset($identifier, $builtPaths['css']) : null,
'priority' => $loadingConfig['priority'] ?? 100,
];
}
@@ -1190,8 +1191,8 @@ class PluginManager implements PluginManagerInterface
if (isset($builtPaths['js']) || isset($builtPaths['css'])) {
$assets = [
'js' => isset($builtPaths['js']) ? "/api/plugins/assets/{$identifier}/".$builtPaths['js'] : null,
'css' => isset($builtPaths['css']) ? "/api/plugins/assets/{$identifier}/".$builtPaths['css'] : null,
'js' => isset($builtPaths['js']) ? AssetUrl::pluginAsset($identifier, $builtPaths['js']) : null,
'css' => isset($builtPaths['css']) ? AssetUrl::pluginAsset($identifier, $builtPaths['css']) : null,
'priority' => $loadingConfig['priority'] ?? 100,
];
}
@@ -1268,8 +1269,8 @@ class PluginManager implements PluginManagerInterface
if (isset($builtPaths['js']) || isset($builtPaths['css'])) {
$assets = [
'js' => isset($builtPaths['js']) ? "/api/plugins/assets/{$identifier}/".$builtPaths['js'] : null,
'css' => isset($builtPaths['css']) ? "/api/plugins/assets/{$identifier}/".$builtPaths['css'] : null,
'js' => isset($builtPaths['js']) ? AssetUrl::pluginAsset($identifier, $builtPaths['js']) : null,
'css' => isset($builtPaths['css']) ? AssetUrl::pluginAsset($identifier, $builtPaths['css']) : null,
'priority' => $loadingConfig['priority'] ?? 100,
];
}
@@ -1428,11 +1429,21 @@ class PluginManager implements PluginManagerInterface
return null;
}
try {
require_once $pluginFile;
$namespace = $this->convertDirectoryToNamespace($pluginName);
$pluginClass = "Plugins\\{$namespace}\\Plugin";
$namespace = $this->convertDirectoryToNamespace($pluginName);
$pluginClass = "Plugins\\{$namespace}\\Plugin";
try {
// 같은 플러그인의 활성 디렉토리 사본이 이미 로드돼 있으면 `_bundled`/`_pending`
// 파일을 그대로 require 할 수 없다 — 같은 FQN 을 두 번 선언하게 되어
// "Cannot declare class ..., because the name is already in use" 로 죽는다.
// 이것은 Error 라 아래 catch(\Exception) 에 걸리지 않아 프로세스가 그대로 종료된다.
//
// 파일 내용을 임시 클래스명으로 eval 해 번들 쪽 메타데이터를 얻는다.
if (class_exists($pluginClass, false)) {
return $this->evalFreshPlugin($pluginFile, $pluginClass, dirname($pluginFile));
}
require_once $pluginFile;
if (class_exists($pluginClass)) {
$plugin = new $pluginClass;
@@ -1440,7 +1451,7 @@ class PluginManager implements PluginManagerInterface
return $plugin;
}
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
Log::debug("Failed to load bundled plugin instance for {$pluginName}: ".$e->getMessage());
}
@@ -8,6 +8,7 @@ use App\Extension\Helpers\EditorSpecAssembler;
use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Services\PermissionService;
use App\Services\TemplateService;
use App\Support\AssetUrl;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@@ -88,17 +89,19 @@ class AdminTemplateAssetController extends AdminBaseController
}
$extensionCacheVersion = (int) app(CacheInterface::class)->get('ext.cache_version', 0);
$version = $extensionCacheVersion > 0 ? "?v={$extensionCacheVersion}" : '';
$version = $extensionCacheVersion > 0 ? $extensionCacheVersion : null;
return $this->success(
__('templates.messages.editor_assets_retrieved'),
[
'identifier' => $identifier,
'js' => ["/api/templates/assets/{$identifier}/js/components.iife.js{$version}"],
'js' => [AssetUrl::templateAsset($identifier, 'js/components.iife.js', $version)],
// CSS 는 편집기 전용 엔드포인트로 — 다크 셀렉터를 프리뷰 마커로 치환해 서빙
// 일반 자산 서빙은 원본.
// URI 가 `components` 가 아니라 `component-styles` 인 이유: 확장자를 떼면
// `editor/components.json` 의 확장자 없는 형태와 충돌한다.
'css' => $cssAvailable
? ["/api/admin/templates/{$identifier}/editor/components.css{$version}"]
? [AssetUrl::suffixed("/api/admin/templates/{$identifier}/editor/component-styles", 'css', $version)]
: [],
'manifest_present' => true,
'manifest_source' => $jsSource,
@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\Api\Public;
use App\Http\Controllers\Api\Base\PublicBaseController;
use Illuminate\Http\Response;
/**
* 자산 URL 모드 감지 프로브.
*
* 서버(nginx/Apache)의 정적 최적화 블록이 확장자 붙은 동적 응답을 가로채는지
* 판정하기 위한 대조 엔드포인트다. 브라우저가 아래 두 URL 을 쌍으로 요청한다.
*
* ```text
* GET /api/system/asset-probe.js → 확장자 형태 (정적 블록의 표적)
* GET /api/system/asset-probe → 대조군
* ```
*
* | probe.js | probe | 판정 |
* |---|---|---|
* | 성공 | 성공 | `extension` — 확장자 유지 가능 |
* | 실패 | 성공 | `extensionless` — 정적 블록 가로채기 확정 |
* | 실패 | 실패 | 모드 문제 아님 (PHP/라우팅 장애) — 별도 안내 |
*
* ## 판정은 상태코드가 아니라 본문이다
*
* 클라이언트는 `res.ok && body.includes(PROBE_TOKEN)` 로 판정해야 한다.
* 상태코드만 보면 "404 대신 200 + 에러 HTML" 이나 catch-all 200 페이지를
* 반환하는 설정에서 영원히 `extension` 으로 오판해, 재감지를 몇 번 눌러도
* 같은 오답이 나온다.
*
* ## 감지는 반드시 브라우저에서 수행한다
*
* 서버측에서 자기 `APP_URL` 로 curl 하면 loopback 이 nginx vhost·SSL·프록시
* 체인을 우회하거나 다른 vhost 를 타서 오판한다.
*
* ## 실물 파일을 두지 않는다
*
* `public/` 하위에 실제 `asset-probe.js` 파일이 존재하면 nginx 가 그것을
* 성공적으로 서빙해 거짓 양성이 된다. 본 라우트는 반드시 실물 파일이 없는
* 경로여야 한다.
*/
class AssetProbeController extends PublicBaseController
{
/**
* 프로브 성공 판정용 매직 토큰.
*
* 클라이언트·테스트가 응답 본문에서 이 문자열을 찾아 성공을 판정한다.
*/
public const PROBE_TOKEN = 'G7_ASSET_PROBE_OK';
/**
* 프로브 응답을 반환합니다.
*
* DB 에 접근하지 않으며(설치 전에도 응답 가능) 캐시되지 않습니다.
* ResponseHelper 의 JSON 봉투를 쓰지 않는 이유: 이 엔드포인트의 목적은
* "정적 자산으로 오인될 응답"을 실제로 흉내내는 것이므로, 자바스크립트
* Content-Type 과 본문 형태를 그대로 유지해야 대표성이 있다.
*
* @return Response 매직 토큰을 담은 자바스크립트 응답
*/
public function probe(): Response
{
$body = "/* G7 asset URL mode probe */\n"
."window.__g7AssetProbe = '".self::PROBE_TOKEN."';\n";
return response($body, 200, [
'Content-Type' => 'application/javascript; charset=utf-8',
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma' => 'no-cache',
'X-Content-Type-Options' => 'nosniff',
]);
}
}
@@ -71,14 +71,16 @@ class PublicModuleController extends PublicBaseController
*
* @param ServeModuleAssetRequest $request 검증된 요청 (경로, 확장자 검증 완료)
* @param string $identifier 모듈 식별자 (vendor-module 형식)
* @param string $path 에셋 경로 (dist/js/module.iife.js 등)
* @return BinaryFileResponse|JsonResponse|Response 파일 응답 또는 에러 응답
*/
public function serveAsset(
ServeModuleAssetRequest $request,
string $identifier,
string $path
string $identifier
): BinaryFileResponse|JsonResponse|Response {
// 파일 경로는 FormRequest 에서 받는다 — 확장자 모드는 `{path}` 라우트 세그먼트,
// 확장자 없는 모드는 `?file=` 쿼리로 오며 prepareForValidation() 이 이를 흡수한다.
$path = (string) $request->validated('path');
// FormRequest에서 이미 보안 검증 완료
// API 사용량 기록
$this->logApiUsage('modules.assets', ['identifier' => $identifier, 'path' => $path]);
@@ -70,14 +70,16 @@ class PublicPluginController extends PublicBaseController
*
* @param ServePluginAssetRequest $request 검증된 요청 (경로, 확장자 검증 완료)
* @param string $identifier 플러그인 식별자 (vendor-plugin 형식)
* @param string $path 에셋 경로 (dist/js/plugin.iife.js 등)
* @return BinaryFileResponse|JsonResponse|Response 파일 응답 또는 에러 응답
*/
public function serveAsset(
ServePluginAssetRequest $request,
string $identifier,
string $path
string $identifier
): BinaryFileResponse|JsonResponse|Response {
// 파일 경로는 FormRequest 에서 받는다 — 확장자 모드는 `{path}` 라우트 세그먼트,
// 확장자 없는 모드는 `?file=` 쿼리로 오며 prepareForValidation() 이 이를 흡수한다.
$path = (string) $request->validated('path');
// FormRequest에서 이미 보안 검증 완료
// API 사용량 기록
$this->logApiUsage('plugins.assets', ['identifier' => $identifier, 'path' => $path]);
@@ -88,13 +88,16 @@ class PublicTemplateController extends PublicBaseController
/**
* 템플릿 정적 파일 서빙
*
* @param ServeTemplateAssetRequest $request 요청 (FormRequest 검증)
* @param ServeTemplateAssetRequest $request 요청 (FormRequest 검증, 파일 경로 `path` 포함)
* @param string $identifier 템플릿 식별자
* @param string $path 요청 경로
* @return BinaryFileResponse|JsonResponse|Response 파일 응답 또는 에러
*/
public function serveAsset(ServeTemplateAssetRequest $request, string $identifier, string $path): BinaryFileResponse|JsonResponse|Response
public function serveAsset(ServeTemplateAssetRequest $request, string $identifier): BinaryFileResponse|JsonResponse|Response
{
// 파일 경로는 FormRequest 에서 받는다 — 확장자 모드는 `{path}` 라우트 세그먼트,
// 확장자 없는 모드는 `?file=` 쿼리로 오며 prepareForValidation() 이 이를 흡수한다.
$path = (string) $request->validated('path');
// FormRequest에서 이미 보안 검증 완료
// API 사용량 기록
$this->logApiUsage('templates.assets', ['identifier' => $identifier, 'path' => $path]);
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Public\Module;
use App\Rules\AllowedModuleFileType;
use App\Rules\SafeModulePath;
use App\Support\Routing\DualExtensionRoute;
use Illuminate\Foundation\Http\FormRequest;
class ServeModuleAssetRequest extends FormRequest
@@ -12,6 +13,8 @@ class ServeModuleAssetRequest extends FormRequest
* 사용자가 이 요청을 수행할 권한이 있는지 확인
*
* 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다.
*
* @return bool 항상 true (권한은 미들웨어 책임)
*/
public function authorize(): bool
{
@@ -21,8 +24,11 @@ class ServeModuleAssetRequest extends FormRequest
/**
* 요청에 적용할 검증 규칙
*
* @return array<string, mixed>
* @return array<string, mixed> 검증 규칙 배열
*/
// audit:allow core-formrequest-hook-filter reason: 자산 서빙 경로 검증은 파일시스템
// 화이트리스트(SafeModulePath + AllowedModuleFileType)가 유일한 방어선이다.
// 확장이 필터로 규칙을 대체할 수 있으면 경로 탈출·임의 파일 읽기가 열린다.
public function rules(): array
{
// 모듈 식별자로부터 기준 경로 구성 (모듈 루트)
@@ -35,7 +41,7 @@ class ServeModuleAssetRequest extends FormRequest
'required',
'string',
new SafeModulePath($basePath),
new AllowedModuleFileType(),
new AllowedModuleFileType,
],
];
}
@@ -45,10 +51,11 @@ class ServeModuleAssetRequest extends FormRequest
*/
protected function prepareForValidation(): void
{
// 라우트 파라미터를 검증 데이터에 병합
// 라우트 파라미터를 검증 데이터에 병합.
// 확장자 없는 모드에서는 파일 경로가 `?file=` 쿼리로 온다 (경로 확장자 회피).
$this->merge([
'identifier' => $this->route('identifier'),
'path' => $this->route('path'),
'path' => $this->route('path') ?? $this->query(DualExtensionRoute::FILE_QUERY_PARAM),
]);
}
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Public\Plugin;
use App\Rules\AllowedPluginFileType;
use App\Rules\SafePluginPath;
use App\Support\Routing\DualExtensionRoute;
use Illuminate\Foundation\Http\FormRequest;
class ServePluginAssetRequest extends FormRequest
@@ -12,6 +13,8 @@ class ServePluginAssetRequest extends FormRequest
* 사용자가 이 요청을 수행할 권한이 있는지 확인
*
* 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다.
*
* @return bool 항상 true (권한은 미들웨어 책임)
*/
public function authorize(): bool
{
@@ -21,8 +24,11 @@ class ServePluginAssetRequest extends FormRequest
/**
* 요청에 적용할 검증 규칙
*
* @return array<string, mixed>
* @return array<string, mixed> 검증 규칙 배열
*/
// audit:allow core-formrequest-hook-filter reason: 자산 서빙 경로 검증은 파일시스템
// 화이트리스트(SafePluginPath + AllowedPluginFileType)가 유일한 방어선이다.
// 확장이 필터로 규칙을 대체할 수 있으면 경로 탈출·임의 파일 읽기가 열린다.
public function rules(): array
{
// 플러그인 식별자로부터 기준 경로 구성 (플러그인 루트)
@@ -35,7 +41,7 @@ class ServePluginAssetRequest extends FormRequest
'required',
'string',
new SafePluginPath($basePath),
new AllowedPluginFileType(),
new AllowedPluginFileType,
],
];
}
@@ -48,7 +54,7 @@ class ServePluginAssetRequest extends FormRequest
// 라우트 파라미터를 검증 데이터에 병합
$this->merge([
'identifier' => $this->route('identifier'),
'path' => $this->route('path'),
'path' => $this->route('path') ?? $this->query(DualExtensionRoute::FILE_QUERY_PARAM),
]);
}
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Public\Template;
use App\Rules\AllowedTemplateFileType;
use App\Rules\SafeTemplatePath;
use App\Support\Routing\DualExtensionRoute;
use Illuminate\Foundation\Http\FormRequest;
class ServeTemplateAssetRequest extends FormRequest
@@ -12,6 +13,8 @@ class ServeTemplateAssetRequest extends FormRequest
* 사용자가 이 요청을 수행할 권한이 있는지 확인
*
* 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다.
*
* @return bool 항상 true (권한은 미들웨어 책임)
*/
public function authorize(): bool
{
@@ -20,7 +23,13 @@ class ServeTemplateAssetRequest extends FormRequest
/**
* 요청에 적용할 검증 규칙
*
* @return array<string, mixed> 검증 규칙 배열
*/
// audit:allow core-formrequest-hook-filter reason: 자산 서빙 경로 검증은 파일시스템
// 화이트리스트(SafeTemplatePath + AllowedTemplateFileType)가 유일한 방어선이다.
// 확장이 필터로 규칙을 대체할 수 있으면 경로 탈출·임의 파일 읽기가 열린다.
// 확장 가능한 "동적 필드" 가 없는 요청이라 룰의 취지(필드 확장)도 해당하지 않는다.
public function rules(): array
{
// 템플릿 식별자로부터 기준 경로 구성
@@ -33,7 +42,7 @@ class ServeTemplateAssetRequest extends FormRequest
'required',
'string',
new SafeTemplatePath($basePath),
new AllowedTemplateFileType(),
new AllowedTemplateFileType,
],
];
}
@@ -43,10 +52,13 @@ class ServeTemplateAssetRequest extends FormRequest
*/
protected function prepareForValidation(): void
{
// 라우트 파라미터를 검증 데이터에 병합
// 라우트 파라미터를 검증 데이터에 병합.
// 확장자 없는 모드에서는 파일 경로가 경로 세그먼트가 아니라 `?file=` 쿼리로 온다
// (nginx 정적 최적화 블록이 URL 경로의 확장자만 보고 가로채는 것을 회피).
// 어느 형태로 오든 컨트롤러는 동일하게 `path` 만 본다.
$this->merge([
'identifier' => $this->route('identifier'),
'path' => $this->route('path'),
'path' => $this->route('path') ?? $this->query(DualExtensionRoute::FILE_QUERY_PARAM),
]);
}
@@ -64,4 +76,4 @@ class ServeTemplateAssetRequest extends FormRequest
'path.string' => __('validation.asset.path.string'),
];
}
}
}
@@ -5,6 +5,7 @@ namespace App\Http\Requests\Settings;
use App\Extension\HookManager;
use App\Models\Attachment;
use App\Search\Engines\DatabaseFulltextEngine;
use App\Support\AssetUrl;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
@@ -174,6 +175,13 @@ class SaveSettingsRequest extends FormRequest
'general.language' => $this->getTabRules($tab, 'general', [Rule::in(config('app.supported_locales', ['ko', 'en']))]),
'general.currency' => ['nullable', 'string', 'max:10'],
'general.maintenance_mode' => ['nullable', 'boolean'],
// 자산 URL 방식 — 정적 최적화 서버 대응 (이슈 #486).
// 임의 문자열이 들어오면 AssetUrl::mode() 가 기본값으로 폴백하지만,
// 저장 단계에서 막아 설정 파일에 뜻 모를 값이 남지 않게 한다.
'general.asset_url_mode' => ['nullable', 'string', Rule::in([
AssetUrl::MODE_EXTENSION,
AssetUrl::MODE_EXTENSIONLESS,
])],
'general.site_logo' => ['nullable', 'array'],
'general.site_logo.*' => ['integer', Rule::exists(Attachment::class, 'id')],
+2 -153
View File
@@ -8,17 +8,18 @@ use App\Extension\PluginManager;
use App\Extension\TemplateManager;
use App\Extension\Traits\ClearsTemplateCaches;
use App\Http\View\Composers\Traits\CollectsActiveExtensionMeta;
use App\Http\View\Composers\Traits\CollectsExtensionAssets;
use App\Http\View\Composers\Traits\CollectsTemplateExternals;
use App\Services\ModuleSettingsService;
use App\Services\PluginSettingsService;
use App\Services\SettingsService;
use App\Services\TemplateService;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class TemplateComposer
{
use CollectsActiveExtensionMeta;
use CollectsExtensionAssets;
use CollectsTemplateExternals;
/**
@@ -115,156 +116,4 @@ class TemplateComposer
$view->with('appConfig', $appConfig);
$view->with('templateExternals', $templateExternals);
}
/**
* 확장 프론트엔드 병합 번들 URL을 계산합니다.
*
* 활성 global 에셋(JS/CSS)이 하나라도 있는 타입만 해당 번들 URL을 채우고,
* 없으면 null을 두어 프론트가 로드를 스킵하게 한다. URL은 same-origin
* (`/api/...`)이어야 gdpr preblocker 의 same-origin 통과 규칙에 자기 차단되지
* 않는다(CDN 호스팅 금지).
*
* @param array $moduleAssets 수집된 모듈 개별 에셋 맵
* @param array $pluginAssets 수집된 플러그인 개별 에셋 맵
* @param int $version 확장 캐시 버전
* @return array{moduleJs: ?string, moduleCss: ?string, pluginJs: ?string, pluginCss: ?string}
*/
private function buildExtensionBundleUrls(array $moduleAssets, array $pluginAssets, int $version): array
{
$hasJs = static fn (array $assets): bool => ! empty(array_filter($assets, fn ($a) => ! empty($a['js'])));
$hasCss = static fn (array $assets): bool => ! empty(array_filter($assets, fn ($a) => ! empty($a['css'])));
return [
'moduleJs' => $hasJs($moduleAssets) ? "/api/modules/bundle.js?v={$version}" : null,
'moduleCss' => $hasCss($moduleAssets) ? "/api/modules/bundle.css?v={$version}" : null,
'pluginJs' => $hasJs($pluginAssets) ? "/api/plugins/bundle.js?v={$version}" : null,
'pluginCss' => $hasCss($pluginAssets) ? "/api/plugins/bundle.css?v={$version}" : null,
];
}
/**
* 활성화된 모듈의 프론트엔드 에셋 정보를 수집합니다.
*
* @return array<string, array{js?: string, css?: string, priority: int, external?: array}>
*/
private function collectModuleAssets(): array
{
$assets = [];
try {
// ModuleManager에서 활성화된 모듈 목록 조회
$activeModules = $this->moduleManager->getActiveModules();
// 캐시 버전 조회 (브라우저 캐시 무효화용)
$cacheVersion = ClearsTemplateCaches::getExtensionCacheVersion();
foreach ($activeModules as $identifier => $module) {
// 모듈에 에셋이 있는지 확인
if (! $module->hasAssets()) {
continue;
}
$builtPaths = $module->getBuiltAssetPaths();
$loadingConfig = $module->getAssetLoadingConfig();
$assetConfig = $module->getAssets();
// global 전략인 경우에만 수집 (layout, lazy는 레이아웃에서 처리)
if ($loadingConfig['strategy'] !== 'global') {
continue;
}
$moduleAsset = [
'priority' => $loadingConfig['priority'],
];
// JS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['js'])) {
$moduleAsset['js'] = "/api/modules/assets/{$identifier}/".$builtPaths['js']."?v={$cacheVersion}";
}
// CSS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['css'])) {
$moduleAsset['css'] = "/api/modules/assets/{$identifier}/".$builtPaths['css']."?v={$cacheVersion}";
}
// 외부 스크립트 (조건부 로드용)
if (! empty($assetConfig['external'])) {
$moduleAsset['external'] = $assetConfig['external'];
}
$assets[$identifier] = $moduleAsset;
}
// 우선순위 기준 정렬 (낮을수록 먼저)
uasort($assets, fn ($a, $b) => $a['priority'] <=> $b['priority']);
} catch (\Exception $e) {
// 에러 발생 시 빈 배열 반환 (에셋 로드 실패해도 앱 진행)
Log::warning('Failed to collect module assets: '.$e->getMessage());
}
return $assets;
}
/**
* 활성화된 플러그인의 프론트엔드 에셋 정보를 수집합니다.
*
* @return array<string, array{js?: string, css?: string, priority: int, external?: array}>
*/
private function collectPluginAssets(): array
{
$assets = [];
try {
// PluginManager에서 활성화된 플러그인 목록 조회
$activePlugins = $this->pluginManager->getActivePlugins();
// 캐시 버전 조회 (브라우저 캐시 무효화용)
$cacheVersion = ClearsTemplateCaches::getExtensionCacheVersion();
foreach ($activePlugins as $identifier => $plugin) {
// 플러그인에 에셋이 있는지 확인
if (! $plugin->hasAssets()) {
continue;
}
$builtPaths = $plugin->getBuiltAssetPaths();
$loadingConfig = $plugin->getAssetLoadingConfig();
$assetConfig = $plugin->getAssets();
// global 전략인 경우에만 수집 (layout, lazy는 레이아웃에서 처리)
if ($loadingConfig['strategy'] !== 'global') {
continue;
}
$pluginAsset = [
'priority' => $loadingConfig['priority'],
];
// JS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['js'])) {
$pluginAsset['js'] = "/api/plugins/assets/{$identifier}/".$builtPaths['js']."?v={$cacheVersion}";
}
// CSS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['css'])) {
$pluginAsset['css'] = "/api/plugins/assets/{$identifier}/".$builtPaths['css']."?v={$cacheVersion}";
}
// 외부 스크립트 (조건부 로드용)
if (! empty($assetConfig['external'])) {
$pluginAsset['external'] = $assetConfig['external'];
}
$assets[$identifier] = $pluginAsset;
}
// 우선순위 기준 정렬 (낮을수록 먼저)
uasort($assets, fn ($a, $b) => $a['priority'] <=> $b['priority']);
} catch (\Exception $e) {
// 에러 발생 시 빈 배열 반환 (에셋 로드 실패해도 앱 진행)
Log::warning('Failed to collect plugin assets: '.$e->getMessage());
}
return $assets;
}
}
@@ -0,0 +1,143 @@
<?php
namespace App\Http\View\Composers\Traits;
use App\Extension\Traits\ClearsTemplateCaches;
use App\Support\AssetUrl;
use Illuminate\Support\Facades\Log;
/**
* 활성 확장(모듈·플러그인)의 프론트엔드 에셋 수집 및 번들 URL 생성.
*
* `TemplateComposer`(admin)와 `UserTemplateComposer`(user)가 공유한다. 두 클래스는
* 이 세 메서드를 주석까지 통째로 중복 보유하고 있었고(123줄), 그래서 한쪽만 고치면
* 다른 쪽이 조용히 어긋나는 구조였다. 트레이트로 승격해 그 가능성을 제거한다.
*
* URL 생성은 전부 `AssetUrl` 을 경유한다 — 자산 URL 이중 모드(확장자 유지/제거)를
* 한 곳에서 결정하기 위함이다.
*
* 사용하는 클래스는 `$moduleManager` / `$pluginManager` 프로퍼티를 가져야 한다.
*
* @see AssetUrl
*/
trait CollectsExtensionAssets
{
/**
* 확장 병합 번들 URL 을 생성합니다.
*
* 병합 번들은 same-origin(`/api/...`)이어야 한다. 외부 origin/CDN 은 GDPR
* 프리블로커에 자기 차단되므로 호스팅하지 않는다.
*
* @param array<string, mixed> $moduleAssets 수집된 모듈 개별 에셋 맵
* @param array<string, mixed> $pluginAssets 수집된 플러그인 개별 에셋 맵
* @param int $version 확장 캐시 버전
* @return array{moduleJs: ?string, moduleCss: ?string, pluginJs: ?string, pluginCss: ?string} 번들 URL 맵
*/
private function buildExtensionBundleUrls(array $moduleAssets, array $pluginAssets, int $version): array
{
$hasJs = static fn (array $assets): bool => ! empty(array_filter($assets, fn ($a) => ! empty($a['js'])));
$hasCss = static fn (array $assets): bool => ! empty(array_filter($assets, fn ($a) => ! empty($a['css'])));
return [
'moduleJs' => $hasJs($moduleAssets) ? AssetUrl::extensionBundle('modules', 'js', $version) : null,
'moduleCss' => $hasCss($moduleAssets) ? AssetUrl::extensionBundle('modules', 'css', $version) : null,
'pluginJs' => $hasJs($pluginAssets) ? AssetUrl::extensionBundle('plugins', 'js', $version) : null,
'pluginCss' => $hasCss($pluginAssets) ? AssetUrl::extensionBundle('plugins', 'css', $version) : null,
];
}
/**
* 활성화된 모듈의 프론트엔드 에셋 정보를 수집합니다.
*
* @return array<string, array{js?: string, css?: string, priority: int, external?: array}> 식별자별 에셋 맵
*/
private function collectModuleAssets(): array
{
return $this->collectExtensionAssets(
'modules',
fn () => $this->moduleManager->getActiveModules(),
'Failed to collect module assets: '
);
}
/**
* 활성화된 플러그인의 프론트엔드 에셋 정보를 수집합니다.
*
* @return array<string, array{js?: string, css?: string, priority: int, external?: array}> 식별자별 에셋 맵
*/
private function collectPluginAssets(): array
{
return $this->collectExtensionAssets(
'plugins',
fn () => $this->pluginManager->getActivePlugins(),
'Failed to collect plugin assets: '
);
}
/**
* 확장 에셋 수집 공통 구현.
*
* 모듈/플러그인은 수집 대상 조회 방법과 로그 문구만 다르고 나머지 로직이 동일하다.
*
* @param string $type `modules` 또는 `plugins`
* @param callable $activeResolver 활성 확장 목록을 반환하는 콜백
* @param string $logPrefix 실패 시 경고 로그 접두사
* @return array<string, array{js?: string, css?: string, priority: int, external?: array}> 식별자별 에셋 맵
*/
private function collectExtensionAssets(string $type, callable $activeResolver, string $logPrefix): array
{
$assets = [];
try {
$active = $activeResolver();
// 캐시 버전 조회 (브라우저 캐시 무효화용)
$cacheVersion = ClearsTemplateCaches::getExtensionCacheVersion();
foreach ($active as $identifier => $extension) {
// 확장에 에셋이 있는지 확인
if (! $extension->hasAssets()) {
continue;
}
$builtPaths = $extension->getBuiltAssetPaths();
$loadingConfig = $extension->getAssetLoadingConfig();
$assetConfig = $extension->getAssets();
// global 전략인 경우에만 수집 (layout, lazy는 레이아웃에서 처리)
if ($loadingConfig['strategy'] !== 'global') {
continue;
}
$asset = [
'priority' => $loadingConfig['priority'],
];
// JS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['js'])) {
$asset['js'] = AssetUrl::extensionAsset($type, $identifier, $builtPaths['js'], $cacheVersion);
}
// CSS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['css'])) {
$asset['css'] = AssetUrl::extensionAsset($type, $identifier, $builtPaths['css'], $cacheVersion);
}
// 외부 스크립트 (조건부 로드용)
if (! empty($assetConfig['external'])) {
$asset['external'] = $assetConfig['external'];
}
$assets[$identifier] = $asset;
}
// 우선순위 기준 정렬 (낮을수록 먼저)
uasort($assets, fn ($a, $b) => $a['priority'] <=> $b['priority']);
} catch (\Exception $e) {
// 에러 발생 시 빈 배열 반환 (에셋 로드 실패해도 앱 진행)
Log::warning($logPrefix.$e->getMessage());
}
return $assets;
}
}
@@ -8,12 +8,12 @@ use App\Extension\PluginManager;
use App\Extension\TemplateManager;
use App\Extension\Traits\ClearsTemplateCaches;
use App\Http\View\Composers\Traits\CollectsActiveExtensionMeta;
use App\Http\View\Composers\Traits\CollectsExtensionAssets;
use App\Http\View\Composers\Traits\CollectsTemplateExternals;
use App\Services\ModuleSettingsService;
use App\Services\PluginSettingsService;
use App\Services\SettingsService;
use App\Services\TemplateService;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
/**
@@ -24,6 +24,7 @@ use Illuminate\View\View;
class UserTemplateComposer
{
use CollectsActiveExtensionMeta;
use CollectsExtensionAssets;
use CollectsTemplateExternals;
/**
@@ -121,156 +122,4 @@ class UserTemplateComposer
$view->with('appConfig', $appConfig);
$view->with('templateExternals', $templateExternals);
}
/**
* 확장 프론트엔드 병합 번들 URL을 계산합니다.
*
* 활성 global 에셋(JS/CSS)이 하나라도 있는 타입만 해당 번들 URL을 채우고,
* 없으면 null을 두어 프론트가 로드를 스킵하게 한다. URL은 same-origin
* (`/api/...`)이어야 gdpr preblocker 의 same-origin 통과 규칙에 자기 차단되지
* 않는다(CDN 호스팅 금지).
*
* @param array $moduleAssets 수집된 모듈 개별 에셋 맵
* @param array $pluginAssets 수집된 플러그인 개별 에셋 맵
* @param int $version 확장 캐시 버전
* @return array{moduleJs: ?string, moduleCss: ?string, pluginJs: ?string, pluginCss: ?string}
*/
private function buildExtensionBundleUrls(array $moduleAssets, array $pluginAssets, int $version): array
{
$hasJs = static fn (array $assets): bool => ! empty(array_filter($assets, fn ($a) => ! empty($a['js'])));
$hasCss = static fn (array $assets): bool => ! empty(array_filter($assets, fn ($a) => ! empty($a['css'])));
return [
'moduleJs' => $hasJs($moduleAssets) ? "/api/modules/bundle.js?v={$version}" : null,
'moduleCss' => $hasCss($moduleAssets) ? "/api/modules/bundle.css?v={$version}" : null,
'pluginJs' => $hasJs($pluginAssets) ? "/api/plugins/bundle.js?v={$version}" : null,
'pluginCss' => $hasCss($pluginAssets) ? "/api/plugins/bundle.css?v={$version}" : null,
];
}
/**
* 활성화된 모듈의 프론트엔드 에셋 정보를 수집합니다.
*
* @return array<string, array{js?: string, css?: string, priority: int, external?: array}>
*/
private function collectModuleAssets(): array
{
$assets = [];
try {
// ModuleManager에서 활성화된 모듈 목록 조회
$activeModules = $this->moduleManager->getActiveModules();
// 캐시 버전 조회 (브라우저 캐시 무효화용)
$cacheVersion = ClearsTemplateCaches::getExtensionCacheVersion();
foreach ($activeModules as $identifier => $module) {
// 모듈에 에셋이 있는지 확인
if (! $module->hasAssets()) {
continue;
}
$builtPaths = $module->getBuiltAssetPaths();
$loadingConfig = $module->getAssetLoadingConfig();
$assetConfig = $module->getAssets();
// global 전략인 경우에만 수집 (layout, lazy는 레이아웃에서 처리)
if ($loadingConfig['strategy'] !== 'global') {
continue;
}
$moduleAsset = [
'priority' => $loadingConfig['priority'],
];
// JS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['js'])) {
$moduleAsset['js'] = "/api/modules/assets/{$identifier}/".$builtPaths['js']."?v={$cacheVersion}";
}
// CSS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['css'])) {
$moduleAsset['css'] = "/api/modules/assets/{$identifier}/".$builtPaths['css']."?v={$cacheVersion}";
}
// 외부 스크립트 (조건부 로드용)
if (! empty($assetConfig['external'])) {
$moduleAsset['external'] = $assetConfig['external'];
}
$assets[$identifier] = $moduleAsset;
}
// 우선순위 기준 정렬 (낮을수록 먼저)
uasort($assets, fn ($a, $b) => $a['priority'] <=> $b['priority']);
} catch (\Exception $e) {
// 에러 발생 시 빈 배열 반환 (에셋 로드 실패해도 앱 진행)
Log::warning('Failed to collect module assets: '.$e->getMessage());
}
return $assets;
}
/**
* 활성화된 플러그인의 프론트엔드 에셋 정보를 수집합니다.
*
* @return array<string, array{js?: string, css?: string, priority: int, external?: array}>
*/
private function collectPluginAssets(): array
{
$assets = [];
try {
// PluginManager에서 활성화된 플러그인 목록 조회
$activePlugins = $this->pluginManager->getActivePlugins();
// 캐시 버전 조회 (브라우저 캐시 무효화용)
$cacheVersion = ClearsTemplateCaches::getExtensionCacheVersion();
foreach ($activePlugins as $identifier => $plugin) {
// 플러그인에 에셋이 있는지 확인
if (! $plugin->hasAssets()) {
continue;
}
$builtPaths = $plugin->getBuiltAssetPaths();
$loadingConfig = $plugin->getAssetLoadingConfig();
$assetConfig = $plugin->getAssets();
// global 전략인 경우에만 수집 (layout, lazy는 레이아웃에서 처리)
if ($loadingConfig['strategy'] !== 'global') {
continue;
}
$pluginAsset = [
'priority' => $loadingConfig['priority'],
];
// JS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['js'])) {
$pluginAsset['js'] = "/api/plugins/assets/{$identifier}/".$builtPaths['js']."?v={$cacheVersion}";
}
// CSS 빌드 경로 (캐시 버전 파라미터 추가)
if (! empty($builtPaths['css'])) {
$pluginAsset['css'] = "/api/plugins/assets/{$identifier}/".$builtPaths['css']."?v={$cacheVersion}";
}
// 외부 스크립트 (조건부 로드용)
if (! empty($assetConfig['external'])) {
$pluginAsset['external'] = $assetConfig['external'];
}
$assets[$identifier] = $pluginAsset;
}
// 우선순위 기준 정렬 (낮을수록 먼저)
uasort($assets, fn ($a, $b) => $a['priority'] <=> $b['priority']);
} catch (\Exception $e) {
// 에러 발생 시 빈 배열 반환 (에셋 로드 실패해도 앱 진행)
Log::warning('Failed to collect plugin assets: '.$e->getMessage());
}
return $assets;
}
}
+32 -13
View File
@@ -2,19 +2,31 @@
namespace App\Providers;
use App\Contracts\Extension\HookManagerInterface;
use App\Contracts\Extension\ModuleManagerInterface;
use App\Contracts\Extension\PluginManagerInterface;
use App\Contracts\Notifications\ChannelReadinessCheckerInterface;
use App\Extension\HookManager;
use App\Extension\ModuleManager;
use App\Extension\PluginManager;
use App\Http\View\Composers\TemplateComposer;
use App\Http\View\Composers\UserTemplateComposer;
use App\Listeners\ExtensionCompatibilityAlertListener;
use App\Notifications\NotificationChannelManager;
use App\Services\ChannelReadinessService;
use App\Services\GeoIpService;
use App\Support\Routing\DualExtensionRoute;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Http\Request;
use Illuminate\Notifications\ChannelManager;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Boost\BoostServiceProvider;
class AppServiceProvider extends ServiceProvider
{
@@ -23,48 +35,55 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
// 자산 URL 이중 모드 Route 매크로 (dualSuffix / dualAsset).
// boot() 가 아니라 register() 에서 등록하는 이유: 라우트 파일은 프레임워크의
// 라우팅 부트스트랩(boot 단계)에서 로드되므로, 프로바이더 간 boot 순서에
// 의존하면 매크로 미정의 시점에 라우트가 로드될 수 있다. 모든 프로바이더의
// register() 는 어떤 boot() 보다 먼저 실행되므로 여기가 유일하게 안전한 지점이다.
DualExtensionRoute::register();
// NOTE: Faker 부재 시 FakerShim 대체는 app/Support/SampleData/bootstrap.php 에서 처리
// (composer autoload.files 진입점 — vendor/autoload.php 로드 직후 실행되어
// Laravel 의 fake() 헬퍼 정의 시점에 \Faker\Factory 가 이미 alias 되어 있음)
// 알림 발송 공통 디스패처 — 채널 독립 발송 + 발송 전후 G7 훅 실행
$this->app->singleton(
\Illuminate\Notifications\ChannelManager::class,
fn ($app) => new \App\Notifications\NotificationChannelManager($app)
ChannelManager::class,
fn ($app) => new NotificationChannelManager($app)
);
// 채널 Readiness 검증 — 미설정 채널 발송 사전 차단
$this->app->singleton(
\App\Contracts\Notifications\ChannelReadinessCheckerInterface::class,
\App\Services\ChannelReadinessService::class
ChannelReadinessCheckerInterface::class,
ChannelReadinessService::class
);
// TODO: TemplateManagerInterface 바인딩을 추가해야 함
// PluginManagerInterface 바인딩
$this->app->bind(
\App\Contracts\Extension\PluginManagerInterface::class,
\App\Extension\PluginManager::class
PluginManagerInterface::class,
PluginManager::class
);
// ModuleManagerInterface 바인딩
$this->app->bind(
\App\Contracts\Extension\ModuleManagerInterface::class,
\App\Extension\ModuleManager::class
ModuleManagerInterface::class,
ModuleManager::class
);
// HookManagerInterface 바인딩
$this->app->bind(
\App\Contracts\Extension\HookManagerInterface::class,
\App\Extension\HookManager::class
HookManagerInterface::class,
HookManager::class
);
// GeoIpService 싱글톤 등록
$this->app->singleton(\App\Services\GeoIpService::class);
$this->app->singleton(GeoIpService::class);
// Laravel Boost (개발 전용 - dont-discover 대상, 클래스 존재 시에만 등록)
if (class_exists(\Laravel\Boost\BoostServiceProvider::class)) {
$this->app->register(\Laravel\Boost\BoostServiceProvider::class);
if (class_exists(BoostServiceProvider::class)) {
$this->app->register(BoostServiceProvider::class);
}
}
+14 -3
View File
@@ -39,11 +39,12 @@ class AllowedModuleFileType implements ValidationRule
}
$extension = strtolower(pathinfo($value, PATHINFO_EXTENSION));
$allowed = self::getAllowedExtensions();
if (! in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
if (! in_array($extension, $allowed, true)) {
$fail(__('validation.module_path.file_type_not_allowed', [
'extension' => $extension,
'allowed' => implode(', ', self::ALLOWED_EXTENSIONS),
'allowed' => implode(', ', $allowed),
]));
return;
@@ -53,10 +54,20 @@ class AllowedModuleFileType implements ValidationRule
/**
* 허용된 확장자 목록 반환
*
* @return array<string>
* 로컬 개발 환경에서만 소스맵(`map`)을 덧붙인다. dev 빌드 산출물은
* `//# sourceMappingURL` 이 개별 에셋 서빙 URL 을 가리키므로 서빙되어야
* 브라우저 콘솔에 404 가 남지 않는다. 반면 소스맵에는 원본 코드 전문
* (`sourcesContent`)이 담겨 있고 이 화이트리스트가 에셋 서빙의 유일한
* 방어선이므로, 운영 환경에서는 어떤 경우에도 서빙하지 않는다.
*
* @return array<string> 허용된 확장자 목록
*/
public static function getAllowedExtensions(): array
{
if (app()->environment('local')) {
return [...self::ALLOWED_EXTENSIONS, 'map'];
}
return self::ALLOWED_EXTENSIONS;
}
}
+14 -3
View File
@@ -39,11 +39,12 @@ class AllowedPluginFileType implements ValidationRule
}
$extension = strtolower(pathinfo($value, PATHINFO_EXTENSION));
$allowed = self::getAllowedExtensions();
if (! in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
if (! in_array($extension, $allowed, true)) {
$fail(__('validation.plugin_path.file_type_not_allowed', [
'extension' => $extension,
'allowed' => implode(', ', self::ALLOWED_EXTENSIONS),
'allowed' => implode(', ', $allowed),
]));
return;
@@ -53,10 +54,20 @@ class AllowedPluginFileType implements ValidationRule
/**
* 허용된 확장자 목록 반환
*
* @return array<string>
* 로컬 개발 환경에서만 소스맵(`map`)을 덧붙인다. dev 빌드 산출물은
* `//# sourceMappingURL` 이 개별 에셋 서빙 URL 을 가리키므로 서빙되어야
* 브라우저 콘솔에 404 가 남지 않는다. 반면 소스맵에는 원본 코드 전문
* (`sourcesContent`)이 담겨 있고 이 화이트리스트가 에셋 서빙의 유일한
* 방어선이므로, 운영 환경에서는 어떤 경우에도 서빙하지 않는다.
*
* @return array<string> 허용된 확장자 목록
*/
public static function getAllowedExtensions(): array
{
if (app()->environment('local')) {
return [...self::ALLOWED_EXTENSIONS, 'map'];
}
return self::ALLOWED_EXTENSIONS;
}
}
+17 -4
View File
@@ -32,18 +32,21 @@ class AllowedTemplateFileType implements ValidationRule
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value)) {
if (! is_string($value)) {
$fail(__('validation.template_path.must_be_string'));
return;
}
$extension = strtolower(pathinfo($value, PATHINFO_EXTENSION));
$allowed = self::getAllowedExtensions();
if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
if (! in_array($extension, $allowed, true)) {
$fail(__('validation.template_path.file_type_not_allowed', [
'extension' => $extension,
'allowed' => implode(', ', self::ALLOWED_EXTENSIONS),
'allowed' => implode(', ', $allowed),
]));
return;
}
}
@@ -51,10 +54,20 @@ class AllowedTemplateFileType implements ValidationRule
/**
* 허용된 확장자 목록 반환
*
* @return array
* 로컬 개발 환경에서만 소스맵(`map`)을 덧붙인다. dev 빌드 산출물은
* `//# sourceMappingURL` 이 개별 에셋 서빙 URL 을 가리키므로 서빙되어야
* 브라우저 콘솔에 404 가 남지 않는다. 반면 소스맵에는 원본 코드 전문
* (`sourcesContent`)이 담겨 있고 이 화이트리스트가 에셋 서빙의 유일한
* 방어선이므로, 운영 환경에서는 어떤 경우에도 서빙하지 않는다.
*
* @return array<string> 허용된 확장자 목록
*/
public static function getAllowedExtensions(): array
{
if (app()->environment('local')) {
return [...self::ALLOWED_EXTENSIONS, 'map'];
}
return self::ALLOWED_EXTENSIONS;
}
}
+2 -1
View File
@@ -12,6 +12,7 @@ use App\Services\LayoutService;
use App\Services\PluginSettingsService;
use App\Services\SettingsService;
use App\Services\TemplateService;
use App\Support\AssetUrl;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\View;
@@ -689,7 +690,7 @@ class SeoRenderer implements SeoRendererInterface
foreach ($cssPaths as $cssPath) {
// dist/ 접두사 제거 (서빙 경로에서는 dist가 자동 추가됨)
$servePath = preg_replace('#^dist/#', '', $cssPath);
$urls[] = '/api/templates/assets/'.$templateIdentifier.'/'.$servePath;
$urls[] = AssetUrl::templateAsset($templateIdentifier, $servePath);
}
return $urls;
+6 -1
View File
@@ -7,6 +7,7 @@ use App\Extension\PluginManager;
use App\Extension\Storage\CoreStorageDriver;
use App\Extension\Traits\ClearsTemplateCaches;
use App\Http\View\Composers\TemplateComposer;
use App\Support\AssetUrl;
use Illuminate\Support\Facades\Log;
/**
@@ -414,7 +415,11 @@ class ExtensionBundleService
return preg_replace_callback($pattern, function (array $m) use ($typeSegment, $identifier) {
$mapFile = ltrim($m[1], './');
return '//# sourceMappingURL=/api/'.$typeSegment.'/assets/'.$identifier.'/dist/js/'.basename($mapFile);
return '//# sourceMappingURL='.AssetUrl::extensionAsset(
$typeSegment,
$identifier,
'dist/js/'.basename($mapFile)
);
}, $content) ?? $content;
}
+36
View File
@@ -7,6 +7,7 @@ use App\Contracts\Repositories\AttachmentRepositoryInterface;
use App\Contracts\Repositories\ConfigRepositoryInterface;
use App\Extension\HookManager;
use App\Http\Resources\AttachmentResource;
use App\Seo\Contracts\SeoCacheManagerInterface;
use App\Support\ConfigCacheHelper;
use App\Support\OpcacheStatus;
use Illuminate\Support\Facades\Artisan;
@@ -44,6 +45,27 @@ class SettingsService
ConfigCacheHelper::rebuild();
}
/**
* 자산 URL 방식 변경에 따라 SEO 프리렌더 캐시를 비웁니다.
*
* SEO 캐시에는 생성 시점의 자산 URL 이 문자열로 구워져 있어, 모드가 바뀌면
* 그 URL 들이 전부 어긋난다. 사람 방문자는 브라우저 자가 복구가 살리지만
* 검색엔진 봇은 JavaScript 를 실행하지 않으므로 캐시를 비워 재생성시켜야 한다.
*
* 캐시 삭제 실패가 설정 저장 자체를 되돌리지는 않는다 — 설정은 이미 저장됐고,
* 캐시는 TTL 만료나 `seo:clear` 로도 회복 가능한 부수 상태다.
*/
private function clearSeoCacheForAssetUrlMode(): void
{
try {
app(SeoCacheManagerInterface::class)->clearAll();
} catch (\Throwable $e) {
Log::warning('자산 URL 방식 변경 후 SEO 캐시 삭제 실패 — seo:clear 로 수동 삭제 필요', [
'error' => $e->getMessage(),
]);
}
}
/**
* 모든 시스템 설정을 조회합니다.
*
@@ -476,12 +498,26 @@ class SettingsService
$existingSettings = $this->configRepository->getCategory($tab);
$mergedSettings = array_merge($existingSettings, $tabSettings);
// 자산 URL 방식이 바뀌는지 저장 **전에** 판정한다 (이슈 #486).
// 저장 후에는 이전 값을 알 수 없어 변경 여부를 판별할 수 없다.
$assetUrlModeChanged = $tab === 'general'
&& array_key_exists('asset_url_mode', $tabSettings)
&& ($existingSettings['asset_url_mode'] ?? null) !== $tabSettings['asset_url_mode'];
// 해당 카테고리 설정 저장
$result = $this->configRepository->saveCategory($tab, $mergedSettings);
if ($result) {
$this->invalidateSettingsCache();
// SEO 프리렌더 캐시에는 생성 시점의 자산 URL 이 그대로 구워져 있다.
// 모드가 바뀌면 그 URL 들이 전부 어긋나는데, 봇은 JavaScript 를 실행하지
// 않아 브라우저 자가 복구가 닿지 않는다 → 캐시를 비워 재생성시킨다.
// CLI(`g7:asset-url-mode`)와 동일한 처리 (계획서 §알려진 한계).
if ($assetUrlModeChanged) {
$this->clearSeoCacheForAssetUrlMode();
}
// drivers 탭은 queue/broadcasting/cache 등 long-running worker에 영향
// SettingsServiceProvider는 worker boot 시점에 한 번만 config 적용하므로
// 워커가 정상 종료 후 재시작되도록 신호 전송 (cache 기반, 즉시 종료 X)
+11 -5
View File
@@ -1739,13 +1739,19 @@ class TemplateService
*/
private function sanitizePath(string $path): string
{
// ../ 및 ..\ 패턴 제거
$path = str_replace(['../', '..\\'], '', $path);
// ../ 및 ..\ 패턴 제거 — 결과가 안정될 때까지 반복한다.
//
// 1회성 치환이면 제거 자체가 새 패턴을 만들어낸다:
// '....//' → 가운데 '../' 제거 → '../' (탈출 시퀀스 복원)
// 현재는 FormRequest 의 realpath 검사가 앞단에서 막고 있으나,
// 방어 계층이 하나 무력한 상태로 두지 않는다.
do {
$previous = $path;
$path = str_replace(['../', '..\\'], '', $path);
} while ($path !== $previous);
// 절대 경로 방지
$path = ltrim($path, '/\\');
return $path;
return ltrim($path, '/\\');
}
/**
+252
View File
@@ -0,0 +1,252 @@
<?php
namespace App\Support;
use App\Support\Routing\DualExtensionRoute;
/**
* 자산·동적 엔드포인트 URL 생성기 (서버측 SSoT).
*
* ## 왜 필요한가
*
* G7 은 동적 API 엔드포인트에 정적 파일 확장자를 붙여 쓴다. 그런데 nginx/Apache 의
* 표준적 정적 최적화 블록(`location ~* \.(js|css|json)$`)은 URL 마지막 확장자로
* 분기하며, nginx 에서 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로
* `try_files ... /index.php` 폴백이 실행될 기회가 없다. 그런 환경에서는 확장자 붙은
* 동적 응답이 PHP 에 도달하지 못하고 404 가 된다.
*
* 라우트는 이미 두 형태로 등록되어 있다(`DualExtensionRoute`). 남은 문제는 **URL 을
* 만드는 쪽**이 13개 지점에 흩어져 하드코딩되어 있었다는 것이다. 한 곳만 빠뜨려도
* 그 자산만 404 가 되고, 어느 지점이 빠졌는지는 화면이 죽어야 알 수 있다.
* 그래서 생성 경로를 여기 하나로 모은다.
*
* ## 모드
*
* `general.asset_url_mode` 설정값을 따른다.
*
* | 모드 | 의미 |
* |---|---|
* | `extension` (기본) | 확장자 유지 — 정상 환경. 확장자 기반 캐시/gzip 최적화를 보존한다 |
* | `extensionless` | 확장자 제거 — 정적 블록이 가로채는 환경 |
*
* 기본값이 `extension` 인 이유는 계획서 §"채택 방향" 을 따른다. 확장자를 일괄 제거하면
* `expires max` / `gzip_static` / CDN TTL 규칙이 함께 걸린 다수의 정상 환경에서
* 그 최적화를 전부 잃는다.
*
* @see DualExtensionRoute 라우트 이중 등록
*/
class AssetUrl
{
/**
* 확장자 유지 모드 식별자.
*/
public const MODE_EXTENSION = 'extension';
/**
* 확장자 제거 모드 식별자.
*/
public const MODE_EXTENSIONLESS = 'extensionless';
/**
* 확장자 없는 자산 URL 에서 파일 경로를 담는 쿼리 파라미터명.
*/
public const FILE_QUERY_PARAM = 'file';
/**
* 테스트/렌더 단위에서 모드를 강제하기 위한 오버라이드 값.
*
* null 이면 설정값을 조회한다.
*/
private static ?string $modeOverride = null;
/**
* 현재 자산 URL 모드를 반환합니다.
*
* 설정 조회가 실패해도(설치 전·마이그레이션 전 등) 예외를 던지지 않고
* 기본 모드로 폴백한다 — 이 값은 blade 렌더 경로에서 읽히므로 여기서
* 터지면 화면 전체가 죽는다.
*
* @return string `extension` 또는 `extensionless`
*/
public static function mode(): string
{
if (self::$modeOverride !== null) {
return self::$modeOverride;
}
try {
$mode = g7_core_settings('general.asset_url_mode', self::MODE_EXTENSION);
} catch (\Throwable $e) {
return self::MODE_EXTENSION;
}
return $mode === self::MODE_EXTENSIONLESS ? self::MODE_EXTENSIONLESS : self::MODE_EXTENSION;
}
/**
* 현재 모드가 확장자 없는 모드인지 여부를 반환합니다.
*
* @return bool 확장자 없는 모드이면 true
*/
public static function isExtensionless(): bool
{
return self::mode() === self::MODE_EXTENSIONLESS;
}
/**
* 모드를 강제로 지정합니다 (테스트 전용).
*
* @param string|null $mode 강제할 모드. null 이면 오버라이드 해제
*/
public static function forceMode(?string $mode): void
{
self::$modeOverride = $mode;
}
/**
* 템플릿 자산 URL 을 생성합니다.
*
* 템플릿은 서버가 `dist/` 를 자동 부가하므로 `$path` 는 `dist/` 를 포함하지 않는다
* (`TemplateService::getAssetFilePath`). 모듈/플러그인과 비대칭이므로 주의.
*
* @param string $identifier 템플릿 식별자
* @param string $path `dist/` 이하 파일 경로 (예: `js/components.iife.js`)
* @param int|string|null $version 캐시 무효화 버전 (null 이면 미부착)
* @return string 생성된 URL
*/
public static function templateAsset(string $identifier, string $path, int|string|null $version = null): string
{
return self::asset('templates', $identifier, $path, $version);
}
/**
* 모듈 자산 URL 을 생성합니다.
*
* 모듈은 모듈 루트 기준이라 `$path` 에 `dist/` 를 직접 포함해야 한다
* (`ModuleService::getAssetFilePath`).
*
* @param string $identifier 모듈 식별자
* @param string $path 모듈 루트 기준 파일 경로 (예: `dist/js/x.iife.js`)
* @param int|string|null $version 캐시 무효화 버전 (null 이면 미부착)
* @return string 생성된 URL
*/
public static function moduleAsset(string $identifier, string $path, int|string|null $version = null): string
{
return self::asset('modules', $identifier, $path, $version);
}
/**
* 플러그인 자산 URL 을 생성합니다.
*
* @param string $identifier 플러그인 식별자
* @param string $path 플러그인 루트 기준 파일 경로 (예: `dist/js/x.iife.js`)
* @param int|string|null $version 캐시 무효화 버전 (null 이면 미부착)
* @return string 생성된 URL
*/
public static function pluginAsset(string $identifier, string $path, int|string|null $version = null): string
{
return self::asset('plugins', $identifier, $path, $version);
}
/**
* 확장 타입을 인자로 받는 자산 URL 생성기.
*
* 모듈/플러그인을 같은 코드 경로로 처리하는 호출부(공용 트레이트 등)용.
* 타입이 컴파일 시점에 정해져 있으면 `moduleAsset()` / `pluginAsset()` 를 쓴다.
*
* @param string $type `templates` / `modules` / `plugins`
* @param string $identifier 확장 식별자
* @param string $path 파일 경로
* @param int|string|null $version 캐시 무효화 버전 (null 이면 미부착)
* @return string 생성된 URL
*/
public static function extensionAsset(string $type, string $identifier, string $path, int|string|null $version = null): string
{
return self::asset($type, $identifier, $path, $version);
}
/**
* 확장 병합 번들 URL 을 생성합니다.
*
* 접미사(js/css)가 번들 종류를 구분하므로 제거할 수 없다.
* 확장자 없는 모드에서는 경로 세그먼트로 내린다 (`bundle.js` → `bundle/js`).
*
* @param string $type `modules` 또는 `plugins`
* @param string $kind `js` 또는 `css`
* @param int|string|null $version 캐시 무효화 버전 (null 이면 미부착)
* @return string 생성된 URL
*/
public static function extensionBundle(string $type, string $kind, int|string|null $version = null): string
{
$base = self::isExtensionless()
? "/api/{$type}/bundle/{$kind}"
: "/api/{$type}/bundle.{$kind}";
return $base.self::versionQuery($version);
}
/**
* 고정 접미사를 갖는 동적 엔드포인트 URL 을 생성합니다.
*
* 확장자 없는 모드에서는 접미사를 제거한다 (`routes.json` → `routes`).
*
* @param string $path 접미사를 제외한 경로 (예: `/api/templates/foo/routes`)
* @param string $suffix 접미사 (예: `json`)
* @param int|string|null $version 캐시 무효화 버전 (null 이면 미부착)
* @return string 생성된 URL
*/
public static function suffixed(string $path, string $suffix, int|string|null $version = null): string
{
$base = rtrim($path, '/');
$normalized = ltrim($suffix, '.');
$url = self::isExtensionless() ? $base : $base.'.'.$normalized;
return $url.self::versionQuery($version);
}
/**
* 확장 자산 URL 을 생성하는 공통 구현.
*
* 확장자 없는 모드에서는 파일 경로를 `?file=` 쿼리로 옮긴다. 경로가 곧 파일명이라
* 접미사만 떼어낼 수 없기 때문이며, nginx 의 location 정규식이 쿼리스트링을 제외한
* 경로에만 매칭되므로 이 형태가 안전하다.
*
* @param string $type `templates` / `modules` / `plugins`
* @param string $identifier 확장 식별자
* @param string $path 파일 경로
* @param int|string|null $version 캐시 무효화 버전
* @return string 생성된 URL
*/
private static function asset(string $type, string $identifier, string $path, int|string|null $version): string
{
$path = ltrim($path, '/');
if (! self::isExtensionless()) {
return "/api/{$type}/assets/{$identifier}/{$path}".self::versionQuery($version);
}
$query = self::FILE_QUERY_PARAM.'='.rawurlencode($path);
if ($version !== null && $version !== '') {
$query .= '&v='.$version;
}
return "/api/{$type}/assets/{$identifier}?{$query}";
}
/**
* 캐시 무효화 쿼리스트링을 생성합니다.
*
* @param int|string|null $version 버전 값
* @return string `?v=...` 또는 빈 문자열
*/
private static function versionQuery(int|string|null $version): string
{
if ($version === null || $version === '') {
return '';
}
return '?v='.$version;
}
}
-4
View File
@@ -33,8 +33,6 @@ class ConfigCacheHelper
*
* 재생성 실패(권한/디스크 등)는 치명적이지 않다 — config:clear 로 stale 캐시는
* 이미 제거되어 다음 요청이 fresh config 로 안전하게 부팅되므로, 경고만 남기고 넘어간다.
*
* @return void
*/
public static function rebuild(): void
{
@@ -62,8 +60,6 @@ class ConfigCacheHelper
* config 캐시만 제거합니다 (재생성 없음).
*
* 재생성이 부적절한 특수 경로(예: 설치 직전 초기화)를 위한 보조 진입점.
*
* @return void
*/
public static function clear(): void
{
+11 -11
View File
@@ -64,7 +64,7 @@ class PhpBinaryResolver
$checkedPaths = [];
// PHP_BINARY 상수 (현재 웹 서버가 사용하는 PHP)
if (defined('PHP_BINARY') && PHP_BINARY !== '' && !in_array(PHP_BINARY, $checkedPaths, true)) {
if (defined('PHP_BINARY') && PHP_BINARY !== '' && ! in_array(PHP_BINARY, $checkedPaths, true)) {
$checkedPaths[] = PHP_BINARY;
$result = self::validate(PHP_BINARY);
if ($result['valid']) {
@@ -79,7 +79,7 @@ class PhpBinaryResolver
}
$checkedPaths[] = $path;
if (!file_exists($path)) {
if (! file_exists($path)) {
continue;
}
@@ -90,7 +90,7 @@ class PhpBinaryResolver
}
// 시스템 PATH의 'php'
if (!in_array('php', $checkedPaths, true)) {
if (! in_array('php', $checkedPaths, true)) {
$checkedPaths[] = 'php';
$result = self::validate('php');
if ($result['valid']) {
@@ -104,7 +104,7 @@ class PhpBinaryResolver
/**
* 지정된 PHP 바이너리 경로의 유효성을 검증합니다.
*
* @param string $path PHP 바이너리 경로
* @param string $path PHP 바이너리 경로
* @return array{valid: bool, version: string|null, message: string} 검증 결과
*/
public static function validate(string $path): array
@@ -118,7 +118,7 @@ class PhpBinaryResolver
}
// 절대 경로인 경우 파일 존재 여부 확인
if ($path !== 'php' && !file_exists($path)) {
if ($path !== 'php' && ! file_exists($path)) {
return [
'valid' => false,
'version' => null,
@@ -127,7 +127,7 @@ class PhpBinaryResolver
}
// 명령어 실행하여 버전 확인
$command = escapeshellarg($path) . ' --version 2>&1';
$command = escapeshellarg($path).' --version 2>&1';
$output = [];
$returnCode = -1;
@@ -150,14 +150,14 @@ class PhpBinaryResolver
return [
'valid' => true,
'version' => $version,
'message' => "PHP {$version} (최소 요구 버전 " . self::MIN_PHP_VERSION . ' 충족)',
'message' => "PHP {$version} (최소 요구 버전 ".self::MIN_PHP_VERSION.' 충족)',
];
}
return [
'valid' => false,
'version' => $version,
'message' => "PHP {$version}은 최소 요구 버전 " . self::MIN_PHP_VERSION . '을 충족하지 않습니다.',
'message' => "PHP {$version}은 최소 요구 버전 ".self::MIN_PHP_VERSION.'을 충족하지 않습니다.',
];
}
@@ -171,8 +171,8 @@ class PhpBinaryResolver
/**
* Composer 실행 명령어를 생성합니다.
*
* @param string $phpBinary PHP 바이너리 경로
* @param string $composerBinary Composer 바이너리 경로 (빈 문자열이면 시스템 PATH 사용)
* @param string $phpBinary PHP 바이너리 경로
* @param string $composerBinary Composer 바이너리 경로 (빈 문자열이면 시스템 PATH 사용)
* @return string 실행 가능한 Composer 명령어
*/
public static function buildComposerCommand(string $phpBinary = 'php', string $composerBinary = ''): string
@@ -184,7 +184,7 @@ class PhpBinaryResolver
}
return str_ends_with($composerBinary, '.phar')
? escapeshellarg($phpBinary) . ' ' . escapeshellarg($composerBinary)
? escapeshellarg($phpBinary).' '.escapeshellarg($composerBinary)
: escapeshellarg($composerBinary);
}
+147
View File
@@ -0,0 +1,147 @@
<?php
namespace App\Support\Routing;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Route;
/**
* 동적 응답 라우트를 "확장자 형태"와 "확장자 없는 형태" 두 벌로 동시 등록하는 Route 매크로.
*
* ## 배경
*
* G7 은 동적 API 엔드포인트에 정적 파일 확장자를 붙여 쓴다 (`/api/templates/{id}/routes.json`).
* 그런데 nginx/Apache 의 표준적 정적 최적화 블록은 URL 마지막 확장자로 분기하며,
* nginx 에서 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로
* `try_files ... /index.php` 폴백이 실행될 기회 자체가 없다.
*
* ```nginx
* location ~* \.(js|css|json)$ { expires max; access_log off; }
* ```
*
* 이 설정 하에서는 PHP 로 요청이 넘어가지 않고 nginx 가 직접 파일시스템을 열려 시도해
* 404 가 되며, G7 은 부트스트랩 자산부터 죽는다. 관례를 깬 쪽이 G7 이므로 해소 책임도
* G7 에 있다는 판단으로, 두 형태를 모두 서빙하고 환경에 따라 택일한다.
*
* ## 규칙
*
* - 고정 접미사 엔드포인트는 접미사를 뗀다 (`routes.json` → `routes`)
* - 와일드카드 자산 라우트는 경로가 곧 파일명이라 뗄 수 없으므로 쿼리로 옮긴다
* (`assets/{id}/js/a.js` → `assets/{id}?file=js/a.js`).
* nginx 의 location 정규식은 쿼리스트링을 제외한 경로에만 매칭되므로 안전하다.
* - 두 형태 모두 영구 유지한다. 확장자 형태를 제거하면 URL 을 하드코딩한
* 서드파티 확장이 깨진다.
*
* ## 등록 순서
*
* 확장자 형태를 항상 **먼저** 등록한다. 확장자 없는 형태가 더 느슨한 패턴이라
* (예: `layouts/{tpl}/{layoutName}` 의 `layoutName` 정규식은 `.` 를 포함해 greedy)
* 순서가 뒤집히면 확장자 없는 라우트가 `.json` 요청까지 삼킨다.
*
* @see DualRouteProxy
*/
class DualExtensionRoute
{
/**
* 확장자 없는 자산 라우트에서 파일 경로를 담는 쿼리 파라미터명.
*/
public const FILE_QUERY_PARAM = 'file';
/**
* Route 매크로 3종(`dualSuffix` / `dualSuffixSegment` / `dualAsset`)을 등록합니다.
*
* `AppServiceProvider::register()` 에서 호출됩니다. boot() 가 아닌 이유:
* 라우트 파일은 프레임워크 라우팅 부트스트랩(boot 단계)에서 로드되므로,
* 프로바이더 간 boot 순서에 의존하면 매크로 미정의 시점에 라우트가 로드될 수 있다.
* 모든 프로바이더의 register() 는 어떤 boot() 보다 먼저 실행된다.
*/
public static function register(): void
{
static::registerDualSuffix();
static::registerDualSuffixSegment();
static::registerDualAsset();
}
/**
* `Route::dualSuffix()` 매크로를 등록합니다.
*
* 고정 접미사를 갖는 엔드포인트를 두 형태로 등록합니다.
*
* ```php
* Route::dualSuffix('templates/{identifier}/routes', 'json', [Ctrl::class, 'getRoutes'])
* ->name('api.public.templates.routes');
* // → GET templates/{identifier}/routes.json (name: api.public.templates.routes)
* // → GET templates/{identifier}/routes (name: api.public.templates.routes.extensionless)
* ```
*/
private static function registerDualSuffix(): void
{
Route::macro('dualSuffix', function (string $uri, string $suffix, mixed $action): DualRouteProxy {
/** @var Router $this */
$base = rtrim($uri, '/');
// 확장자 형태를 먼저 등록 (더 구체적인 패턴이 우선 매칭되어야 함)
$extension = $this->get($base.'.'.ltrim($suffix, '.'), $action);
$extensionless = $this->get($base, $action);
return new DualRouteProxy($extension, $extensionless);
});
}
/**
* `Route::dualSuffixSegment()` 매크로를 등록합니다.
*
* 접미사 자체가 의미를 담아 **뗄 수 없는** 엔드포인트용입니다.
* `bundle.js` 와 `bundle.css` 는 접미사를 제거하면 둘 다 `bundle` 이 되어 충돌하므로,
* 접미사를 삭제하는 대신 경로 세그먼트로 내린다.
*
* ```php
* Route::dualSuffixSegment('modules/bundle', 'js', [Ctrl::class, 'serveBundleJs'])
* ->name('api.public.modules.bundle.js');
* // → GET modules/bundle.js (name: api.public.modules.bundle.js)
* // → GET modules/bundle/js (name: api.public.modules.bundle.js.extensionless)
* ```
*/
private static function registerDualSuffixSegment(): void
{
Route::macro('dualSuffixSegment', function (string $uri, string $suffix, mixed $action): DualRouteProxy {
/** @var Router $this */
$base = rtrim($uri, '/');
$normalized = ltrim($suffix, '.');
$extension = $this->get($base.'.'.$normalized, $action);
$extensionless = $this->get($base.'/'.$normalized, $action);
return new DualRouteProxy($extension, $extensionless);
});
}
/**
* `Route::dualAsset()` 매크로를 등록합니다.
*
* 와일드카드 파일 경로를 갖는 자산 서빙 엔드포인트를 두 형태로 등록합니다.
* 확장자 형태는 `{path}` 세그먼트로, 확장자 없는 형태는 `?file=` 쿼리로 경로를 받으며
* 양쪽 모두 동일 컨트롤러로 들어갑니다 (흡수는 각 FormRequest 의
* `prepareForValidation()` 이 담당).
*
* ```php
* Route::dualAsset('templates/assets/{identifier}', [Ctrl::class, 'serveAsset'])
* ->name('api.public.templates.assets');
* // → GET templates/assets/{identifier}/{path} where path = .*
* // → GET templates/assets/{identifier} (?file=js/a.js)
* ```
*/
private static function registerDualAsset(): void
{
Route::macro('dualAsset', function (string $uri, mixed $action): DualRouteProxy {
/** @var Router $this */
$base = rtrim($uri, '/');
// 확장자 형태를 먼저 등록 (경로 세그먼트가 더 구체적)
$extension = $this->get($base.'/{path}', $action)->where('path', '.*');
$extensionless = $this->get($base, $action);
return new DualRouteProxy($extension, $extensionless);
});
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace App\Support\Routing;
use Illuminate\Routing\Route;
/**
* 이중 등록된 두 라우트(확장자 형태 · 확장자 없는 형태)를 하나처럼 체이닝하기 위한 프록시.
*
* `Route::dualSuffix()` / `Route::dualAsset()` 매크로가 반환하며, `name()` 을 제외한
* 모든 호출(`middleware()`, `where()`, `defaults()` 등)을 양쪽 라우트에 그대로 전달한다.
* 한쪽에만 적용되어 조용히 어긋나는 상황을 구조적으로 차단하는 것이 목적이다.
*
* @mixin Route
*/
class DualRouteProxy
{
/**
* 확장자 없는 형태의 라우트 이름에 붙는 접미사.
*
* 두 라우트에 같은 이름을 주면 Laravel 의 이름 조회가 나중에 등록된 쪽으로
* 덮여 `route()` 결과가 조용히 바뀐다. 확장자 형태가 기존 이름을 유지하고
* 확장자 없는 형태만 접미사를 갖는다 (하위호환).
*/
public const EXTENSIONLESS_NAME_SUFFIX = '.extensionless';
/**
* @param Route $extension 확장자 형태 라우트 (예: `templates/{id}/routes.json`)
* @param Route $extensionless 확장자 없는 형태 라우트 (예: `templates/{id}/routes`)
*/
public function __construct(
private readonly Route $extension,
private readonly Route $extensionless,
) {}
/**
* 확장자 형태 라우트를 반환합니다.
*
* @return Route 확장자 형태 라우트 인스턴스
*/
public function extensionRoute(): Route
{
return $this->extension;
}
/**
* 확장자 없는 형태 라우트를 반환합니다.
*
* @return Route 확장자 없는 형태 라우트 인스턴스
*/
public function extensionlessRoute(): Route
{
return $this->extensionless;
}
/**
* 두 라우트에 이름을 부여합니다.
*
* 확장자 형태는 전달된 이름을 그대로, 확장자 없는 형태는
* `.extensionless` 접미사를 붙여 등록합니다.
*
* @param string $name 라우트 이름
* @return self 체이닝을 위한 자기 자신
*/
public function name(string $name): self
{
$this->extension->name($name);
$this->extensionless->name($name.self::EXTENSIONLESS_NAME_SUFFIX);
return $this;
}
/**
* 그 외 모든 호출을 두 라우트에 동일하게 전달합니다.
*
* @param string $method 호출된 메서드명
* @param array<int, mixed> $arguments 전달 인자
* @return self 체이닝을 위한 자기 자신
*/
public function __call(string $method, array $arguments): self
{
$this->extension->{$method}(...$arguments);
$this->extensionless->{$method}(...$arguments);
return $this;
}
}
+2 -2
View File
@@ -236,8 +236,8 @@ class FakerShim
public function uuid(): string
{
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
$data[6] = chr(ord($data[6]) & 0x0F | 0x40);
$data[8] = chr(ord($data[8]) & 0x3F | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
+11 -7
View File
@@ -1,5 +1,10 @@
<?php
use App\Support\SampleData\FakerFactoryShim;
use App\Support\SampleData\FakerShim;
use Faker\Factory;
use Faker\Generator;
/**
* FakerShim Bootstrap
*
@@ -16,16 +21,15 @@
* - 매 요청당 1회 (PHP-FPM 워커 수명 동안 캐시되지는 않음 — autoload.files 는 매 요청 require)
* - 비용: file_exists ~100ns + class_alias ~500ns (Faker 부재 시) = 총 1μs 미만
*/
$fakerInstalled = file_exists(__DIR__.'/../../../vendor/fakerphp/faker/src/Faker/Factory.php');
if (! $fakerInstalled) {
// Faker 미설치: Shim 을 실제 Faker 클래스명으로 alias
if (! class_exists(\Faker\Factory::class, false)) {
class_alias(\App\Support\SampleData\FakerFactoryShim::class, \Faker\Factory::class);
if (! class_exists(Factory::class, false)) {
class_alias(FakerFactoryShim::class, Factory::class);
}
if (! class_exists(\Faker\Generator::class, false)) {
class_alias(\App\Support\SampleData\FakerShim::class, \Faker\Generator::class);
if (! class_exists(Generator::class, false)) {
class_alias(FakerShim::class, Generator::class);
}
}
@@ -37,7 +41,7 @@ if (! function_exists('fake')) {
* FakerShim 기반 fake() 대체 구현
*
* @param string|null $locale 로케일 (기본: config('app.faker_locale') 또는 'ko_KR')
* @return \Faker\Generator (class_alias 로 App\Support\SampleData\FakerShim 과 동일)
* @return Generator (class_alias 로 App\Support\SampleData\FakerShim 과 동일)
*/
function fake($locale = null)
{
@@ -45,6 +49,6 @@ if (! function_exists('fake')) {
$locale = app('config')->get('app.faker_locale', 'ko_KR');
}
return \Faker\Factory::create($locale ?? 'ko_KR');
return Factory::create($locale ?? 'ko_KR');
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ class UmaskHelper
* 그룹 공유 친화 umask(0002) 로 조정한다. 조건이 맞지 않으면 no-op.
*
* @param string $storagePath 판정 기준 디렉토리 (일반적으로 `base_path('storage')`)
* @return int|null 새 umask 로 전환한 경우 이전 umask 값. 조건 불충족 시 null.
* @return int|null 새 umask 로 전환한 경우 이전 umask 값. 조건 불충족 시 null.
*/
public static function configureForGroupSharing(string $storagePath): ?int
{
+4 -2
View File
@@ -14,7 +14,8 @@
"language": "ko",
"currency": "KRW",
"maintenance_mode": false,
"site_logo": []
"site_logo": [],
"asset_url_mode": "extension"
},
"security": {
"force_https": false,
@@ -174,7 +175,8 @@
"language": { "type": "string", "sensitive": false, "expose": false },
"timezone": { "type": "string", "sensitive": false, "expose": false },
"site_logo": { "type": "array", "sensitive": false, "transform": "attachments" },
"site_logo_url": { "type": "string", "sensitive": false, "computed": true, "source": "site_logo", "transform": "first_image_url" }
"site_logo_url": { "type": "string", "sensitive": false, "computed": true, "source": "site_logo", "transform": "first_image_url" },
"asset_url_mode": { "type": "string", "sensitive": false }
}
},
"security": {
+46
View File
@@ -105,6 +105,52 @@ Authorization: Bearer {YOUR_TOKEN}
}
```
### 자산 URL 이중 모드
정적 파일 확장자(`.js` / `.css` / `.json`)로 끝나는 동적 엔드포인트는 **확장자 없는 형태를 함께 제공**합니다.
아래 문서에 실린 URI 는 확장자 형태를 기준으로 표기하지만, 각 엔드포인트는 대응하는 확장자 없는 형태로도
동일한 응답·동일한 권한 가드로 호출할 수 있습니다.
이유는 서버 설정입니다. nginx/Apache 의 표준적 정적 최적화 블록은 URL 마지막 확장자로 분기하며,
nginx 에서 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로 `try_files ... /index.php` 폴백이
실행될 기회가 없습니다. 그런 환경에서는 확장자 붙은 동적 응답이 PHP 에 도달하지 못하고 404 가 됩니다.
```nginx
location ~* \.(js|css|json)$ { expires max; access_log off; }
```
| 확장자 형태 | 확장자 없는 형태 | 변환 규칙 |
| --- | --- | --- |
| `/api/templates/{id}/routes.json` | `/api/templates/{id}/routes` | 접미사 제거 |
| `/api/layouts/{tpl}/{layout}.json` | `/api/layouts/{tpl}/{layout}` | 접미사 제거 |
| `/api/modules/bundle.js` | `/api/modules/bundle/js` | 접미사를 경로 세그먼트로 (js/css 구분이 필요) |
| `/api/templates/assets/{id}/js/a.js` | `/api/templates/assets/{id}?file=js/a.js` | 파일 경로를 `file` 쿼리로 (경로가 곧 파일명이라 제거 불가) |
`file` 쿼리 형태가 안전한 이유는 nginx 의 location 정규식이 쿼리스트링을 제외한 경로에만 매칭되기 때문입니다.
확장자 없는 형태에도 경로 탈출 방어와 확장자 화이트리스트가 동일하게 적용됩니다.
두 형태는 **모두 영구 유지**됩니다. 확장자 형태를 제거하면 URL 을 하드코딩한 서드파티 확장이 깨집니다.
어느 형태를 쓸지는 서버 환경에 따라 결정되며, 다음 프로브 엔드포인트로 판정합니다.
| 메서드 | URI | 인증/권한 | 설명 |
| --- | --- | --- | --- |
| GET | `/api/system/asset-probe.js` | 공개 (인증 불필요) | 확장자 형태 프로브 |
| GET | `/api/system/asset-probe` | 공개 (인증 불필요) | 대조군 |
두 URL 을 **브라우저에서** 쌍으로 요청합니다(서버측 loopback curl 은 vhost·프록시 체인을 우회해 오판합니다).
응답은 `application/javascript` 이며 본문에 매직 토큰 `G7_ASSET_PROBE_OK` 를 담습니다. DB 에 접근하지 않고
`Cache-Control: no-store` 로 캐시되지 않습니다.
| `asset-probe.js` | `asset-probe` | 판정 |
| --- | --- | --- |
| 성공 | 성공 | 확장자 형태 사용 가능 |
| 실패 | 성공 | 정적 블록 가로채기 확정 — 확장자 없는 형태 사용 |
| 실패 | 실패 | 모드 문제가 아님 (PHP/라우팅 장애) |
성공 판정은 상태코드가 아니라 **본문의 매직 토큰과 Content-Type** 으로 합니다. 상태코드만 보면
"404 대신 200 + 에러 HTML" 이나 catch-all 200 페이지를 반환하는 설정에서 영원히 오판합니다.
## 코어 API 레퍼런스
<!-- @generated:start:api-readme-index -->
+4 -2
View File
@@ -821,7 +821,7 @@ Authorization: Bearer {YOUR_TOKEN}
**설명** 데이터소스 websocket 후보용으로 등록된 브로드캐스트 채널/이벤트 카탈로그를 반환합니다(BroadcastCatalogService::collect). 편집기 전용 가드 하에서만 노출되며(admin 전역 broadcast 회피), `identifier`는 라우트 일관성용이고 카탈로그는 설치본 전역 기준입니다. `core.templates.layouts.edit` 권한이 필요합니다.
### GET /api/admin/templates/{identifier}/editor/components.css
### GET /api/admin/templates/{identifier}/editor/component-styles.css
<!-- @generated:start:api.admin.templates.editor-css -->
- **라우트명**: `api.admin.templates.editor-css`
- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@serveEditorCss`
@@ -836,7 +836,7 @@ Authorization: Bearer {YOUR_TOKEN}
**요청 예시**
```http
GET /api/admin/templates/{identifier}/editor/components.css HTTP/1.1
GET /api/admin/templates/{identifier}/editor/component-styles.css HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
@@ -862,6 +862,8 @@ Authorization: Bearer {YOUR_TOKEN}
**설명** 레이아웃 편집기 프리뷰 전용 CSS를 서빙합니다. 편집기 진입 시에만 components.css의 다크 조상 셀렉터를 프리뷰 마커로 치환하고(editor-spec `darkMode.previewIsolation` 규칙), 필요 시 `@layer` 래퍼를 평탄화해 라이트/다크 프리뷰를 격리합니다. 변환 결과는 캐시 버전+파일 mtime 키로 캐시하며, CSS 부재 시 빈 응답으로 폴백합니다. `core.templates.layouts.edit` 권한이 필요합니다.
URI 가 `editor/components.css` 가 아니라 `editor/component-styles.css` 인 이유는 [자산 URL 이중 모드](README.md#자산-url-이중-모드) 때문입니다. 확장자를 뗀 형태(`editor/components`)가 `editor/components.json` 의 확장자 없는 형태와 충돌하므로 CSS 쪽 URI 를 분리했습니다. 확장자 없는 형태는 `editor/component-styles` 입니다.
### GET /api/admin/templates/{identifier}/editor/components.json
<!-- @generated:start:api.admin.templates.editor-components -->
+46
View File
@@ -12,6 +12,7 @@
3. URL: /api/admin/*, /api/auth/*, /api/public/*
4. 권한: permission: 또는 Middleware에서 체크
5. REST 패턴: index, store, show, update, destroy
6. .js/.css/.json/.map 로 끝나는 라우트 = dualSuffix/dualSuffixSegment/dualAsset 매크로 필수
```
---
@@ -101,6 +102,51 @@ GET /api/admin/plugins/{identifier}/license # 플러그인 LICENSE 반환
GET /api/admin/templates/{identifier}/license # 템플릿 LICENSE 반환
```
### 정적 확장자로 끝나는 동적 엔드포인트
`.js` / `.css` / `.json` / `.map` 으로 끝나는 라우트는 `Route::get()` 으로 단일 등록하지 않는다.
`Route::dualSuffix()` / `dualSuffixSegment()` / `dualAsset()` 매크로로 **확장자 형태와 확장자 없는 형태를
동시에** 등록한다. 코어 라우트와 확장 라우트 파일 모두에 적용된다.
nginx/Apache 의 표준적 정적 최적화 블록은 URL 마지막 확장자로 분기하며, nginx 에서 정규식 location 은
프리픽스 location 보다 먼저 매칭된다. 그래서 `try_files ... /index.php` 폴백이 실행될 기회 없이 nginx 가
직접 파일시스템을 열려 시도해 404 가 된다. aaPanel / CyberPanel / Plesk 기본 템플릿에 들어있는 블록이라
드물지 않으며, 그런 서버에서는 해당 엔드포인트가 통째로 죽는다.
```nginx
location ~* \.(js|css|json)$ { expires max; access_log off; }
```
```php
// 잘못된 등록 — 확장자 없는 형태가 생기지 않는다
Route::get('{identifier}/routes.json', [Ctrl::class, 'getRoutes'])->name('...');
// 접미사 제거형 — routes.json + routes
Route::dualSuffix('{identifier}/routes', 'json', [Ctrl::class, 'getRoutes'])
->name('api.public.templates.routes');
// 세그먼트 강등형 — 접미사가 종류를 구분해 제거 불가 (bundle.js + bundle/js)
Route::dualSuffixSegment('bundle', 'js', [Ctrl::class, 'serveBundleJs'])
->name('api.public.modules.bundle.js');
// 쿼리 이동형 — 경로가 곧 파일명 (assets/{id}/js/a.js + assets/{id}?file=js/a.js)
Route::dualAsset('assets/{identifier}', [Ctrl::class, 'serveAsset'])
->name('api.public.templates.assets');
```
매크로가 반환하는 프록시는 `name()` 을 양쪽에 적용하며(확장자 없는 쪽은 `.extensionless` 접미사),
`middleware()` 등 나머지 호출도 두 라우트에 함께 전달한다. 한쪽에만 걸리는 가드가 생기지 않는다.
URL 을 만드는 쪽도 문자열로 직접 조립하지 않는다. 서버는 `App\Support\AssetUrl`, 프론트엔드는
`resources/js/core/support/assetUrl.ts` 를 경유해야 현재 모드에 맞는 형태가 나온다. 두 구현은 동일 규칙을
공유하므로 한쪽만 바꾸면 서버가 만든 URL 과 클라이언트가 만든 URL 이 어긋나 그 자산만 404 가 된다.
자동 차단: audit 룰 `dynamic-route-static-extension` (error). 면제는 인라인 주석
`// audit:allow dynamic-route-static-extension reason: ...`.
두 형태는 모두 영구 유지한다 — 확장자 형태를 제거하면 URL 을 하드코딩한 서드파티 확장이 깨진다.
엔드포인트별 변환 규칙 표와 프로브 판정 절차: [API 레퍼런스 진입점](./api/README.md) "자산 URL 이중 모드".
---
## 권한 체크
+2
View File
@@ -434,6 +434,8 @@ php artisan template:cache-clear # 전체 번들 파일 정리 포함
프로덕션은 version-in-path 디스크 캐시, 비프로덕션(dev/watch)은 캐시 없이 매 요청 concat(rebuild 즉시 반영). `_bundled` 수정 후에는 `{type}:update {id} --force` 로 활성 반영 후 version bump 로 번들이 재생성된다.
> 개별 에셋 서빙 라우트(`/api/{type}/assets/...`, `*.map` 포함)는 소스맵·static 참조를 위해 존치한다.
> 다만 `*.map` 의 **실제 서빙은 `local` 환경에서만** 허용된다 — 소스맵에는 원본 코드 전문이
> 담기므로 운영에서는 확장자 화이트리스트가 차단한다. 상세: [template-security.md](template-security.md) "소스맵 (`map`) — 로컬 개발 환경 전용".
### 전송 압축 (gzip)
+17 -3
View File
@@ -266,14 +266,28 @@ class TemplateService
|--------|------|
| `js` | JavaScript |
| `mjs` | ES Module JavaScript |
| `js.map` | JavaScript 소스맵 |
#### 스타일 파일
| 확장자 | 설명 |
|--------|------|
| `css` | Cascading Style Sheets |
| `css.map` | CSS 소스맵 |
#### 소스맵 (`map`) — 로컬 개발 환경 전용
소스맵에는 원본 TS/TSX 전문(`sourcesContent`)이 담기고, 이 확장자 화이트리스트가 확장 에셋
서빙의 유일한 방어선이다. 따라서 `map` 은 `local` 환경에서만 허용 목록에 덧붙고, 그 외
환경에서는 어떤 경우에도 서빙하지 않는다.
로컬에서 허용하는 이유는 dev 빌드 산출물의 `//# sourceMappingURL` 이 개별 에셋 서빙 URL 을
가리키기 때문이다 — 차단하면 브라우저 콘솔에 404 가 쌓인다. `--production` 빌드는 소스맵을
생성하지 않으므로(`G7_BUILD_SOURCEMAP=0`) 운영에서는 참조 자체가 없다.
판정 지점은 `Allowed{Module,Plugin,Template}FileType::getAllowedExtensions()` 한 곳이며,
`validate()` 도 이 게터를 참조한다(상수 직접 참조 금지 — 환경 분기가 우회된다).
`public/build/**` 는 docroot 라 이 화이트리스트를 거치지 않는다. `--production` 재빌드는
기존 `.map` 을 지우지 않으므로 직접 삭제해야 한다.
#### 폰트 파일
@@ -365,7 +379,7 @@ private function getMimeType(string $extension): string
|------|------|
| ✅ 화이트리스트에 명시된 확장자만 허용 | 목록 외 차단 |
| ✅ 대소문자 구분 없이 검증 | `strtolower()` 사용 |
| ✅ 소스맵 파일 특별 처리 | `.js.map`, `.css.map` |
| ✅ 소스맵은 `local` 에서만 허용 | 운영에서 서빙 시 원본 코드 전문 노출 |
| 실행 파일 확장자 금지 | `.php`, `.sh`, `.exe` 등 |
| ❌ 서버 설정 파일 금지 | `.htaccess`, `.env` 등 |
| ❌ 화이트리스트 외 모든 확장자 차단 | 기본 거부 정책 |
+1 -1
View File
@@ -608,7 +608,7 @@ Tailwind 는 빌드 시 safelist 에 없는 임의값 클래스(`dark:text-[#hex
}
```
코어 CSS 서빙 API(`/api/admin/templates/{id}/editor/components.css`)가 편집기 진입
코어 CSS 서빙 API(`/api/admin/templates/{id}/editor/component-styles.css`)가 편집기 진입
시에만 이 선언대로 CSS 의 다크 조상 셀렉터(`rewriteSelector`)를 프리뷰 전용 마커
(`replaceWith`)로 치환해 서빙한다. 일반 사용자 페이지 CSS 는 원본 그대로다
(사용자 페이지 무영향). `strategy: "none"` 또는 미선언이면 편집기 다크 탭이 비노출된다.
@@ -22,6 +22,8 @@
### Added
- 자산 파일 주소 방식 확인·전환 명령(`g7:asset-url-mode`) 출력 문구 일본어 번역 추가 (`asset_url_mode.*`) — 현재 방식과 서버 진단 안내, 전환 결과 메시지가 일본어 로케일에서 자연스럽게 표시됩니다.
- 데이터베이스 계정 설정 오류 안내 화면의 문구 일본어 번역 추가 (`database_credential.*`) — 최고 권한 계정을 사용하거나 사용자명이 비어 있을 때 표시되는 안내와 복구 방법이 일본어 로케일에서 자연스럽게 표시됩니다.
- 사이트맵 생성이 실패했을 때 표시되는 안내 문구 일본어 번역 추가 (`exceptions.seo.*`) — 사이트맵 파일을 저장하지 못하거나 압축에 실패한 경우의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
- 사이트맵 재생성 예약·준비 중·생성 실패 안내 문구 일본어 번역 추가 (`seo.sitemap_*`) — 재생성을 예약했을 때, 아직 준비 중일 때, 생성에 실패했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
@@ -0,0 +1,17 @@
<?php
return [
'current' => '現在のアセット URL 方式: :mode',
'table' => [
'in_use' => '使用中',
'alternative' => '変換時',
],
'diagnose_title' => 'サーバーが動的レスポンスをインターセプトしているかどうかを確認するには、以下の 2 つのリクエストを比較してください:',
'diagnose_hint' => '最初のリクエストのみ失敗し、2 番目が成功する場合は、静的最適化設定がインターセプトしています → 拡張子なしに変換してください。',
'switch_hint' => '変換: php artisan g7:asset-url-mode extensionless',
'invalid' => '不明な方式です: :mode (extension または extensionless)',
'already' => '既に :mode 方式を使用中です。',
'switched' => 'アセット URL 方式を :from → :to に変更しました。',
'save_failed' => '設定の保存に失敗しました。',
'seo_cache_cleared' => 'SEO キャッシュも一緒にクリアしました (焼き込まれたアセットアドレスが古い方式のため)。',
];
@@ -8,6 +8,9 @@
### Added
- 일반 설정의 에셋 파일 서빙 방식 항목 문구 일본어 번역 추가 (`settings.general.asset_url_mode*`) — 항목 이름과 설명, 선택지, 자동 감지 결과(확장자를 가로채는 서버 규칙 안내 포함)가 일본어 로케일에서 자연스럽게 표시됩니다.
- 대시보드의 에셋 파일 서빙 방식 불일치 안내 문구 일본어 번역 추가 (`dashboard.asset_url_mode_drift_*`) — 안내 제목과 설명, 환경설정 이동 문구가 일본어 로케일에서 자연스럽게 표시됩니다.
- 환경설정 > 정보 화면에 추가된 OPcache 상태 항목과 비활성 안내 문구 일본어 번역 추가 — 상태 표시와 성능 저하 안내가 일본어 로케일에서도 올바르게 표시됩니다. 비활성 상태 문구는 성능 저하의 체감 정도가 드러나도록 보강했습니다.
- 환경설정 > SEO 화면에 추가된 Sitemap 분할 기준(파일당 URL 수)·Sitemap 압축 항목의 이름·설명 일본어 번역 추가 — 새 항목이 일본어 로케일에서도 올바르게 표시됩니다.
- 환경설정 > SEO 화면에 추가된 Sitemap 다국어 대체 링크(hreflang) 항목의 이름·설명 일본어 번역 추가 — 새 항목이 일본어 로케일에서도 올바르게 표시됩니다.
@@ -139,7 +139,10 @@
"title": "最近の通知",
"subtitle": "最近送信された通知履歴",
"empty": "送信された通知がありません"
}
},
"asset_url_mode_drift_title": "アセットファイル配信方式がサーバー環境と異なります",
"asset_url_mode_drift_description": "保存された方式では一部のアドレスがサーバーに遮断され、画面が表示されない可能性があります。訪問者画面はブラウザが自動的に補正しますが、検索エンジンボットには補正が適用されないため、環境設定で検出結果を保存してください。",
"asset_url_mode_drift_action": "環境設定 > 一般で確認する"
},
"users": {
"page_title": "ユーザー管理",
@@ -1311,7 +1314,16 @@
"default_language": "デフォルト言語",
"select_language": "言語を選択してください",
"timezone": "タイムゾーン",
"select_timezone": "タイムゾーンを選択してください"
"select_timezone": "タイムゾーンを選択してください",
"asset_url_mode": "アセットファイル配信方式",
"asset_url_mode_help": "サーバー設定に応じて自動的に選択されます。画面が正常に表示されている場合は変更しないでください。",
"asset_url_mode_extension": "拡張子を使用 (推奨)",
"asset_url_mode_extensionless": "拡張子を使用しない",
"asset_url_mode_detect": "自動検出",
"asset_url_mode_detecting": "アセットファイル配信方式を検出中です...",
"asset_url_mode_detect_extension": "拡張子を使用できる環境です。",
"asset_url_mode_detect_extensionless": "静的ファイル最適化ルール(nginx の location ~* \\.(js|css|json|png|jpg|jpeg|gif|ico|svg|woff2?)$ ブロックなど)が拡張子アドレスをインターセプトしています。「拡張子を使用しない」に変更してから保存してください。",
"asset_url_mode_detect_unavailable": "検出に失敗しました。サーバーが応答しないため、方式を判定できません。"
},
"mail": {
"smtp_settings": "SMTP設定",
+22
View File
@@ -0,0 +1,22 @@
<?php
// 주의: 관리자 화면 문구는 여기 두지 않는다 — 템플릿 lang(admin.json)이 SSoT.
// 이 파일은 Artisan 커맨드(g7:asset-url-mode) 출력 전용이다.
return [
// CLI status
'current' => 'Current asset URL style: :mode',
'table' => [
'in_use' => 'In use',
'alternative' => 'After switching',
],
'diagnose_title' => 'To check whether your server intercepts dynamic responses, compare these two requests:',
'diagnose_hint' => 'If only the first one fails while the second succeeds, a static-optimization rule is intercepting it. Switch to extensionless.',
'switch_hint' => 'Switch with: php artisan g7:asset-url-mode extensionless',
// CLI switch
'invalid' => 'Unknown style: :mode (use extension or extensionless)',
'already' => 'Already using the :mode style.',
'switched' => 'Asset URL style changed from :from to :to.',
'save_failed' => 'Failed to save the setting.',
'seo_cache_cleared' => 'SEO cache was cleared as well (baked asset URLs used the old style).',
];
+22
View File
@@ -0,0 +1,22 @@
<?php
// 주의: 관리자 화면 문구는 여기 두지 않는다 — 템플릿 lang(admin.json)이 SSoT.
// 이 파일은 Artisan 커맨드(g7:asset-url-mode) 출력 전용이다.
return [
// CLI 조회
'current' => '현재 자산 URL 방식: :mode',
'table' => [
'in_use' => '사용 중',
'alternative' => '전환 시',
],
'diagnose_title' => '서버가 동적 응답을 가로채는지 확인하려면 아래 두 요청을 비교하세요:',
'diagnose_hint' => '첫 번째만 실패하고 두 번째가 성공하면 정적 최적화 설정이 가로채는 것입니다 → extensionless 로 전환하세요.',
'switch_hint' => '전환: php artisan g7:asset-url-mode extensionless',
// CLI 전환
'invalid' => '알 수 없는 방식입니다: :mode (extension 또는 extensionless)',
'already' => '이미 :mode 방식을 사용 중입니다.',
'switched' => '자산 URL 방식을 :from → :to 로 변경했습니다.',
'save_failed' => '설정 저장에 실패했습니다.',
'seo_cache_cleared' => 'SEO 캐시를 함께 비웠습니다 (구운 자산 주소가 옛 방식이므로).',
];
+1
View File
@@ -1558,3 +1558,4 @@ details[open] > .g7dt-summary::before {
}
`,ve=()=>{const d="g7-devtools-styles";if(document.getElementById(d))return;const t=document.createElement("style");t.id=d,t.textContent=be,document.head.appendChild(t)},le={state:"상태",actions:"액션",cache:"캐시",diagnose:"진단",performance:"성능",lifecycle:"라이프사이클",network:"네트워크",form:"Form",conditional:"조건부",expression:"표현식",handlers:"핸들러",events:"이벤트",datasources:"데이터소스",renders:"렌더링",styles:"스타일",auth:"인증",logs:"로그",layout:"레이아웃",changedetection:"변경감지",staleclosure:"Stale Closure",nestedcontext:"Context",modalscope:"모달스코프",namedactions:"Named Actions"},Ne=700,ue=500,re=400,ce=300,ne="g7-devtools-panel",te=d=>d===null||d===void 0?"g7dt-text-gray":typeof d=="string"?"g7dt-text-yellow":typeof d=="number"?"g7dt-text-cyan":typeof d=="boolean"?"g7dt-text-pink":Array.isArray(d)?"g7dt-text-orange":typeof d=="object"?"g7dt-text-blue":"g7dt-text-white",ye=d=>d===null?"null":d===void 0?"undefined":Array.isArray(d)?`Array(${d.length})`:typeof d=="object"?`Object(${Object.keys(d).length})`:typeof d,we=d=>d===null?"null":d===void 0?"undefined":typeof d=="string"?d.length>50?`"${d.substring(0,50)}..."`:`"${d}"`:typeof d=="boolean"?d?"true":"false":typeof d=="number"?String(d):"",oe=()=>{try{const d=localStorage.getItem(ne);if(d)return JSON.parse(d)}catch(d){console.warn("[G7DevTools] 설정 로드 실패:",d)}return{}},ie=d=>{try{const s={...oe(),...d};localStorage.setItem(ne,JSON.stringify(s))}catch(t){console.warn("[G7DevTools] 설정 저장 실패:",t)}},W=({keyName:d,value:t,path:s,expandedPaths:a,onToggle:g,searchTerm:r,depth:o=0})=>{const p=t!==null&&typeof t=="object",v=a.has(s),l=Array.isArray(t),f=r&&(d.toLowerCase().includes(r.toLowerCase())||typeof t=="string"&&t.toLowerCase().includes(r.toLowerCase())||typeof t=="number"&&String(t).includes(r)),c=y=>{y.stopPropagation(),p&&g(s)},h=p?Object.entries(t):[];return e.jsxs("div",{style:{marginLeft:o>0?"16px":"0"},children:[e.jsxs("div",{className:`g7dt-tree-node g7dt-flex g7dt-items-center g7dt-gap-1 ${f?"g7dt-ring-highlight":""}`,onClick:c,children:[p?e.jsx("span",{className:"g7dt-text-gray g7dt-w-4 g7dt-text-xs g7dt-select-none",style:{textAlign:"center"},children:v?"▼":"▶"}):e.jsx("span",{className:"g7dt-w-4"}),e.jsx("span",{className:"g7dt-text-purple g7dt-text-xs",children:d}),e.jsx("span",{className:"g7dt-text-gray-dark g7dt-text-xs",children:":"}),p?e.jsxs("span",{className:`g7dt-text-xs ${te(t)}`,children:[l?"[":"{",!v&&e.jsx("span",{className:"g7dt-text-gray",style:{margin:"0 4px"},children:ye(t)}),!v&&(l?"]":"}")]}):e.jsx("span",{className:`g7dt-text-xs ${te(t)}`,children:we(t)}),!p&&e.jsxs("span",{className:"g7dt-text-gray-dark g7dt-text-xs g7dt-ml-1",children:["(",typeof t,")"]})]}),p&&v&&e.jsxs("div",{className:"g7dt-border-l",style:{marginLeft:"8px"},children:[h.map(([y,n])=>e.jsx(W,{keyName:l?`[${y}]`:y,value:n,path:`${s}.${y}`,expandedPaths:a,onToggle:g,searchTerm:r,depth:o+1},`${s}.${y}`)),e.jsx("div",{className:"g7dt-ml-4 g7dt-text-xs",children:e.jsx("span",{className:te(t),children:l?"]":"}"})})]})]})},ke=({state:d,devTools:t})=>{const[s,a]=x.useState(new Set(["_global","_local","_computed","_isolated","$parent"])),[g,r]=x.useState(""),[o,p]=x.useState(!1),[v,l]=x.useState(!1),[f,c]=x.useState(null),h=x.useCallback(()=>{t&&c(t.getComputedDependencyTrackingInfo())},[t]);x.useEffect(()=>{if(v&&t){h();const w=setInterval(h,2e3);return()=>clearInterval(w)}},[v,t,h]);const y=w=>{switch(w){case"state-change":return"📊";case"prop-change":return"📥";case"dependency-change":return"🔗";case"manual":return"👆";case"initial":return"🚀";default:return"❓"}},n=x.useCallback(w=>{a(S=>{const z=new Set(S);return z.has(w)?z.delete(w):z.add(w),z})},[]),i=x.useCallback((w,S)=>{const z=[S];if(w!==null&&typeof w=="object")for(const[O,K]of Object.entries(w))z.push(...i(K,`${S}.${O}`));return z},[]),u=x.useCallback(()=>{if(o)a(new Set(["_global","_local","_computed","_isolated","$parent"]));else{const w=new Set;i(d._global,"_global").forEach(S=>w.add(S)),i(d._local,"_local").forEach(S=>w.add(S)),i(d._computed,"_computed").forEach(S=>w.add(S)),i(d._isolated,"_isolated").forEach(S=>w.add(S)),d.$parent&&i(d.$parent,"$parent").forEach(S=>w.add(S)),a(w)}p(!o)},[o,d,i]),m=Object.keys(d._global||{}).length,k=Object.keys(d._local||{}).length,b=Object.keys(d._computed||{}).length,N=Object.keys(d._isolated||{}).length,C=Object.keys(d.$parent||{}).length;return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-items-center g7dt-flex-shrink-0",children:[e.jsx("input",{type:"text",placeholder:"키 또는 값 검색...",value:g,onChange:w=>r(w.target.value),className:"g7dt-input g7dt-flex-1"}),e.jsx("button",{onClick:u,className:"g7dt-btn g7dt-bg-gray-700",children:o?"모두 접기":"모두 펼치기"})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-3 g7dt-text-xs g7dt-text-gray g7dt-flex-wrap g7dt-flex- Line truncated
... (truncated)`:v}catch{return"(직렬화 불가)"}};return e.jsxs("div",{className:"g7dt-space-y-4",children:[t.activeRequests.length>0&&e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-yellow g7dt-mb-2",children:["진행 중 (",t.activeRequests.length,")"]}),e.jsx("div",{className:"g7dt-space-y-1",children:t.activeRequests.map(o=>e.jsxs("details",{className:"g7dt-details",children:[e.jsxs("summary",{className:"g7dt-summary g7dt-pending-request",children:[e.jsx("span",{className:"g7dt-font-bold",children:o.method}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-lightgray g7dt-truncate",children:o.fullUrl||o.url})]}),e.jsxs("div",{className:"g7dt-card g7dt-mt-1 g7dt-text-xs",children:[a(o)&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("div",{className:"g7dt-text-blue g7dt-font-bold g7dt-mb-1",children:"쿼리 파라미터"}),e.jsx("div",{className:"g7dt-pl-2 g7dt-border-l-2 g7dt-border-blue",children:g(o.queryParams)})]}),o.requestBody&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-orange g7dt-font-bold g7dt-mb-1",children:"요청 본문"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-xs",children:r(o.requestBody,2e3)})]})]})]},o.id))})]}),e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"요청 이력"}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-scroll-list-lg",children:t.requestHistory.length===0?e.jsx("div",{className:"g7dt-empty-message",children:"요청 이력 없음"}):t.requestHistory.slice(-20).reverse().map(o=>e.jsxs("details",{className:"g7dt-details",children:[e.jsxs("summary",{className:"g7dt-summary g7dt-card g7dt-text-xs",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-w-full",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-font-bold",children:o.method}),e.jsx("span",{className:`g7dt-ml-2 ${s(o.status)}`,children:o.statusCode||o.status})]}),e.jsxs("span",{className:"g7dt-text-gray",children:[o.duration,"ms"]})]}),e.jsx("div",{className:"g7dt-text-gray g7dt-truncate g7dt-mt-1",children:o.fullUrl||o.url})]}),e.jsxs("div",{className:"g7dt-card g7dt-mt-1 g7dt-text-xs g7dt-ml-2 g7dt-border-l-2 g7dt-border-gray",children:[e.jsx("div",{className:"g7dt-mb-2",children:e.jsxs("div",{className:"g7dt-text-xs g7dt-space-y-1",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"URL:"})," ",e.jsx("span",{className:"g7dt-text-lightgray",children:o.fullUrl||o.url})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"상태:"})," ",e.jsx("span",{className:s(o.status),children:o.statusCode||o.status})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"소요시간:"})," ",e.jsxs("span",{className:"g7dt-text-lightgray",children:[o.duration,"ms"]})]}),o.dataSourceId&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"데이터소스:"})," ",e.jsx("span",{className:"g7dt-text-blue",children:o.dataSourceId})]}),o.error&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"에러:"})," ",e.jsx("span",{className:"g7dt-text-red",children:o.error})]})]})}),a(o)&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("div",{className:"g7dt-text-blue g7dt-font-bold g7dt-mb-1",children:"쿼리 파라미터"}),e.jsx("div",{className:"g7dt-pl-2 g7dt-border-l-2 g7dt-border-blue",children:g(o.queryParams)})]}),o.requestBody&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("div",{className:"g7dt-text-orange g7dt-font-bold g7dt-mb-1",children:"요청 본문"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-xs g7dt-max-h-40 g7dt-overflow-auto",children:r(o.requestBody,2e3)})]}),o.response!==void 0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-green g7dt-font-bold g7dt-mb-1",children:"응답 내용"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-xs g7dt-max-h-60 g7dt-overflow-auto",children:r(o.response,5e3)})]})]})]},o.id))})]}),t.pendingDataSources.length>0&&e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-blue g7dt-mb-2",children:["대기 중인 데이터소스 (",t.pendingDataSources.length,")"]}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-2",children:t.pendingDataSources.map(o=>e.jsx("span",{className:"g7dt-tag-blue",children:o},o))})]})]})},Ee=({devTools:d})=>{const t=d.getFormInfo(),[s,a]=x.useState(!1),[g,r]=x.useState(null),o=x.useCallback(()=>{r(d.getFormBindingValidationTrackingInfo())},[d]);x.useEffect(()=>{if(s){o();const l=setInterval(o,2e3);return()=>clearInterval(l)}},[s,o]);const p=l=>{switch(l){case"missing-datakey":return"🔑";case"missing-input-name":return"📝";case"context-not-propagated":return"🔗";case"sortable-context-break":return"📊";case"modal-context-isolation":return"🪟";case"duplicate-input-name":return"👯";case"value-type-mismatch":return"⚠️";case"binding-path-invalid":return"❌";default:return"❓"}},v=l=>{switch(l){case"error":return"g7dt- Line truncated
//# sourceMappingURL=devtools.min.js.map
+7 -6
View File
@@ -1,13 +1,14 @@
var eM=Object.defineProperty;var tM=(ln,i,u)=>i in ln?eM(ln,i,{enumerable:!0,configurable:!0,writable:!0,value:u}):ln[i]=u;var Ii=(ln,i,u)=>tM(ln,typeof i!="symbol"?i+"":i,u);(function(ln,i,u,Sn){"use strict";function Lt(){const e=typeof window<"u"?window.G7Core?.__runtime:void 0;if(!e)throw new Error("[layout-editor] window.G7Core.__runtime 이 없습니다. 코어 번들(template-engine.min.js)이 편집기 번들보다 먼저 로드되어야 합니다.");return e}const Ai=Lt();Ai.TranslationProvider;const tt=Ai.useTranslation;Ai.TranslationReactContext;function rt(){return typeof window>"u"?null:window.__g7Devtools??null}function wl(e,t){const n=rt();if(!n?.trackEditorState)return;const o={templateIdentifier:e.templateIdentifier,editMode:e.editMode,selectedRoutePath:e.selectedRoute?.path??null,selectedLayoutName:e.selectedRoute?.layoutName??null,routeTreeSize:e.routeTree.length,isRouteTreeCollapsed:e.isRouteTreeCollapsed,locale:e.locale,previewDevice:e.previewDevice,previewZoom:e.previewZoom,lastAction:t,timestamp:Date.now()};try{n.trackEditorState(o)}catch{}}function Bh(){const e=rt();if(e?.clearEditorStateData)try{e.clearEditorStateData()}catch{}}function Wh(e){const t=rt();if(t?.trackEditorSampleMatch)try{t.trackEditorSampleMatch(e)}catch{}}function jh(){const e=rt();if(e?.clearEditorSampleData)try{e.clearEditorSampleData()}catch{}}function Hh(e){const t=rt();if(t?.trackEditorHistoryEntry)try{t.trackEditorHistoryEntry(e)}catch{}}function Fh(){const e=rt();if(e?.clearEditorHistoryData)try{e.clearEditorHistoryData()}catch{}}function kl(e){const t=rt();if(t?.trackEditorSelection)try{t.trackEditorSelection(e)}catch{}}function Kh(){const e=rt();if(e?.clearEditorSelectionData)try{e.clearEditorSelectionData()}catch{}}function or(e){const t=rt();if(t?.trackEditorPropertyPatch)try{t.trackEditorPropertyPatch(e)}catch{}}function Cn(e){const t=rt();if(t?.trackEditorDnd)try{t.trackEditorDnd(e)}catch{}}function Vh(){const e=rt();if(e?.clearEditorDndData)try{e.clearEditorDndData()}catch{}}function st(e){const t=rt();if(t?.trackEditorDocument)try{t.trackEditorDocument(e)}catch{}}function Uh(){const e=rt();if(e?.clearEditorDocumentData)try{e.clearEditorDocumentData()}catch{}}function qh(e){const t=rt();if(t?.trackEditorSpecMerge)try{t.trackEditorSpecMerge(e)}catch{}}function Gh(){const e=rt();if(e?.clearEditorSpecMergeData)try{e.clearEditorSpecMergeData()}catch{}}function Ti(e){let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e.charCodeAt(n)>>>0;return t}function Et(e){const t=rt();if(t?.trackEditorI18n)try{t.trackEditorI18n(e)}catch{}}function Yh(){const e=rt();if(e?.clearEditorI18nData)try{e.clearEditorI18nData()}catch{}}function Sl(e){const t=rt();if(t?.trackPageState)try{t.trackPageState(e)}catch{}}function Xh(){const e=rt();if(e?.clearPageStateData)try{e.clearPageStateData()}catch{}}const Cl="g7le.routeTree.collapsed";function Jh(){if(typeof window>"u")return!1;try{return window.localStorage?.getItem(Cl)==="1"}catch{return!1}}function Zh(e){if(!(typeof window>"u"))try{window.localStorage?.setItem(Cl,e?"1":"0")}catch{}}function Qh(e,t){return{templateIdentifier:e,routeTree:[],routesError:null,selectedRoute:null,isRouteTreeCollapsed:Jh(),editMode:"route",returnRoute:null,returnEditMode:null,locale:t,previewDevice:"desktop",previewZoom:1,previewCustomWidth:1024,previewColorScheme:"light",history:{canUndo:!1,canRedo:!1},isPaletteOpen:!1,availableStates:[],activeStateId:null,layoutVersions:{},extensionVersions:{}}}function Rh(e,t){switch(t.type){case"SET_ROUTE_TREE":return{...e,routeTree:t.tree,routesError:null};case"SET_ROUTES_ERROR":return{...e,routesError:t.error};case"SELECT_ROUTE":return{...e,editMode:"route",selectedRoute:t.route,returnRoute:null,returnEditMode:null};case"TOGGLE_ROUTE_TREE":return{...e,isRouteTreeCollapsed:!e.isRouteTreeCollapsed};case"SET_LOCALE":return{...e,locale:t.locale};case"ENTER_BASE_EDIT":return{...e,editMode:"base",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__base__/${t.layoutName}`,layoutName:t.layoutName}};case"EXIT_BASE_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_MODAL_EDIT":return{...e,editMode:"modal",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__modal__/${t.modalId}`,layoutName:t.hostLayout}};case"EXIT_MODAL_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_EXTENSION_EDIT":return{...e,editMode:"extension",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__extension__/${t.extensionId}`,layoutName:t.extensionHost??null}};case"SET_RETURN_ROUTE":return e.editMode==="route"||e.returnRoute!==null?e:{...e,returnRoute:t.route};case"EXIT_EXTENSION_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_ITERATION_ITEM_EDIT":return{...e,editMode:"iteration_item",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,returnEditMode:e.ediLine truncated
var iM=Object.defineProperty;var sM=(dn,i,u)=>i in dn?iM(dn,i,{enumerable:!0,configurable:!0,writable:!0,value:u}):dn[i]=u;var Ti=(dn,i,u)=>sM(dn,typeof i!="symbol"?i+"":i,u);(function(dn,i,u,Cn){"use strict";function Lt(){const e=typeof window<"u"?window.G7Core?.__runtime:void 0;if(!e)throw new Error("[layout-editor] window.G7Core.__runtime 이 없습니다. 코어 번들(template-engine.min.js)이 편집기 번들보다 먼저 로드되어야 합니다.");return e}const Di=Lt();Di.TranslationProvider;const tt=Di.useTranslation;Di.TranslationReactContext;function rt(){return typeof window>"u"?null:window.__g7Devtools??null}function Sl(e,t){const n=rt();if(!n?.trackEditorState)return;const o={templateIdentifier:e.templateIdentifier,editMode:e.editMode,selectedRoutePath:e.selectedRoute?.path??null,selectedLayoutName:e.selectedRoute?.layoutName??null,routeTreeSize:e.routeTree.length,isRouteTreeCollapsed:e.isRouteTreeCollapsed,locale:e.locale,previewDevice:e.previewDevice,previewZoom:e.previewZoom,lastAction:t,timestamp:Date.now()};try{n.trackEditorState(o)}catch{}}function Hh(){const e=rt();if(e?.clearEditorStateData)try{e.clearEditorStateData()}catch{}}function Fh(e){const t=rt();if(t?.trackEditorSampleMatch)try{t.trackEditorSampleMatch(e)}catch{}}function Kh(){const e=rt();if(e?.clearEditorSampleData)try{e.clearEditorSampleData()}catch{}}function Vh(e){const t=rt();if(t?.trackEditorHistoryEntry)try{t.trackEditorHistoryEntry(e)}catch{}}function Uh(){const e=rt();if(e?.clearEditorHistoryData)try{e.clearEditorHistoryData()}catch{}}function Cl(e){const t=rt();if(t?.trackEditorSelection)try{t.trackEditorSelection(e)}catch{}}function qh(){const e=rt();if(e?.clearEditorSelectionData)try{e.clearEditorSelectionData()}catch{}}function ir(e){const t=rt();if(t?.trackEditorPropertyPatch)try{t.trackEditorPropertyPatch(e)}catch{}}function $n(e){const t=rt();if(t?.trackEditorDnd)try{t.trackEditorDnd(e)}catch{}}function Gh(){const e=rt();if(e?.clearEditorDndData)try{e.clearEditorDndData()}catch{}}function st(e){const t=rt();if(t?.trackEditorDocument)try{t.trackEditorDocument(e)}catch{}}function Yh(){const e=rt();if(e?.clearEditorDocumentData)try{e.clearEditorDocumentData()}catch{}}function Xh(e){const t=rt();if(t?.trackEditorSpecMerge)try{t.trackEditorSpecMerge(e)}catch{}}function Jh(){const e=rt();if(e?.clearEditorSpecMergeData)try{e.clearEditorSpecMergeData()}catch{}}function Ni(e){let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e.charCodeAt(n)>>>0;return t}function Et(e){const t=rt();if(t?.trackEditorI18n)try{t.trackEditorI18n(e)}catch{}}function Zh(){const e=rt();if(e?.clearEditorI18nData)try{e.clearEditorI18nData()}catch{}}function $l(e){const t=rt();if(t?.trackPageState)try{t.trackPageState(e)}catch{}}function Qh(){const e=rt();if(e?.clearPageStateData)try{e.clearPageStateData()}catch{}}const El="g7le.routeTree.collapsed";function Rh(){if(typeof window>"u")return!1;try{return window.localStorage?.getItem(El)==="1"}catch{return!1}}function ey(e){if(!(typeof window>"u"))try{window.localStorage?.setItem(El,e?"1":"0")}catch{}}function ty(e,t){return{templateIdentifier:e,routeTree:[],routesError:null,selectedRoute:null,isRouteTreeCollapsed:Rh(),editMode:"route",returnRoute:null,returnEditMode:null,locale:t,previewDevice:"desktop",previewZoom:1,previewCustomWidth:1024,previewColorScheme:"light",history:{canUndo:!1,canRedo:!1},isPaletteOpen:!1,availableStates:[],activeStateId:null,layoutVersions:{},extensionVersions:{}}}function ny(e,t){switch(t.type){case"SET_ROUTE_TREE":return{...e,routeTree:t.tree,routesError:null};case"SET_ROUTES_ERROR":return{...e,routesError:t.error};case"SELECT_ROUTE":return{...e,editMode:"route",selectedRoute:t.route,returnRoute:null,returnEditMode:null};case"TOGGLE_ROUTE_TREE":return{...e,isRouteTreeCollapsed:!e.isRouteTreeCollapsed};case"SET_LOCALE":return{...e,locale:t.locale};case"ENTER_BASE_EDIT":return{...e,editMode:"base",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__base__/${t.layoutName}`,layoutName:t.layoutName}};case"EXIT_BASE_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_MODAL_EDIT":return{...e,editMode:"modal",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__modal__/${t.modalId}`,layoutName:t.hostLayout}};case"EXIT_MODAL_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_EXTENSION_EDIT":return{...e,editMode:"extension",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__extension__/${t.extensionId}`,layoutName:t.extensionHost??null}};case"SET_RETURN_ROUTE":return e.editMode==="route"||e.returnRoute!==null?e:{...e,returnRoute:t.route};case"EXIT_EXTENSION_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_ITERATION_ITEM_EDIT":return{...e,editMode:"iteration_item",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,returnEditMode:e.ediLine truncated
"keyword": "",
"page": 1
}`,shapeErrorKey:"layout_editor.json_block.must_be_object",validate:_=>{const S=Object.keys(_??{}).filter(E=>!$s(E));return S.length?a("layout_editor.json_block.invalid_keys",{keys:S.join(", ")}):null}}):i.jsxs(i.Fragment,{children:[v.length===0?i.jsx("p",{"data-testid":`g7le-initstate-${e}-empty`,style:ES,children:a("layout_editor.initstate.no_values")}):v.map(_=>{const E=Ew(b,r,_)==="inherited",C=e==="isolated"&&l[_];return i.jsx(aS,{scope:e,itemKey:_,value:o[_],inherited:E,isOrphan:!!C,onChangeValue:N=>x(_,N),onRemove:()=>k(_),t:a},_)}),i.jsxs("div",{style:xS,children:[i.jsx("input",{type:"text","data-testid":`g7le-initstate-${e}-add-name`,value:d,placeholder:a("layout_editor.initstate.value_name"),onChange:_=>{c(_.target.value),y&&g(null)},onKeyDown:_=>{_.key==="Enter"&&(_.preventDefault(),w())},"aria-invalid":y?!0:void 0,style:{...wS,...y?kS:null}}),i.jsx("select",{"data-testid":`g7le-initstate-${e}-add-kind`,value:p,onChange:_=>f(_.target.value),"aria-label":a("layout_editor.initstate.value_kind"),style:CS,children:iS.map(_=>i.jsx("option",{value:_.kind,children:a(_.labelKey)},_.kind))}),i.jsxs("button",{type:"button","data-testid":`g7le-initstate-${e}-add`,onClick:w,style:$S,children:["+ ",a("layout_editor.initstate.add_value")]})]}),y?i.jsx("p",{"data-testid":`g7le-initstate-${e}-add-error`,style:SS,children:y}):null]})]})}function aS({scope:e,itemKey:t,value:n,inherited:o,isOrphan:r,onChangeValue:s,onRemove:a,t:l}){const[d,c]=u.useState(!1);let p;try{p=JSON.stringify(n,null,2)??String(n)}catch{p=String(n)}return i.jsxs("div",{"data-testid":`g7le-initstate-${e}-item-${t}`,style:yS,children:[i.jsxs("div",{style:bS,children:[o?i.jsx("span",{"data-testid":`g7le-initstate-inherited-${t}`,style:mS,children:"🔗"}):null,i.jsx("code",{style:vS,children:t}),i.jsx("span",{style:{flex:1}}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-code-${t}`,onClick:()=>c(f=>!f),style:Ws,"aria-label":l("layout_editor.value_tree.show_source"),title:l("layout_editor.value_tree.show_source"),children:"</>"}),o?i.jsx("button",{type:"button","data-testid":`g7le-initstate-revert-${t}`,onClick:a,style:Ws,children:l("layout_editor.initstate.revert")}):i.jsx("button",{type:"button","data-testid":`g7le-initstate-remove-${t}`,onClick:a,style:Ws,"aria-label":l("layout_editor.init_actions.remove"),children:"✕"})]}),d?i.jsx("pre",{"data-testid":`g7le-initstate-code-block-${t}`,style:AS,children:p}):null,i.jsx(Dn,{value:n,onChange:s,t:l,path:t,scope:e}),r?i.jsxs("p",{"data-testid":`g7le-initstate-isolated-orphan-${t}`,style:_S,children:["⚠ ",l("layout_editor.initstate.isolated_orphan")]}):null]})}const lS={display:"flex",flexDirection:"column",gap:14,minWidth:0},dS={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},cS={margin:0,fontSize:11,color:"#1d4ed8",background:"#eff6ff",padding:"6px 8px",borderRadius:6},uS={display:"flex",flexDirection:"column",gap:8,padding:10,border:"1px solid #e2e8f0",borderRadius:10,minWidth:0},pS={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},fS={fontSize:12,fontWeight:700,color:"#475569"},gS={margin:0,fontSize:11,color:"#94a3b8"},hS={padding:"2px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},yS={display:"flex",flexDirection:"column",gap:4,padding:"6px 8px",border:"1px solid #f1f5f9",borderRadius:6,minWidth:0},bS={display:"flex",alignItems:"center",gap:6},mS={fontSize:12},vS={fontSize:12,color:"#0f172a",fontFamily:"monospace"},Ws={border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",fontSize:11,padding:"2px 6px"},_S={margin:0,fontSize:11,color:"#b45309",background:"#fef3c7",padding:"4px 6px",borderRadius:6},xS={display:"flex",gap:6,alignItems:"center"},wS={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},kS={border:"1px solid #dc2626"},SS={margin:0,fontSize:11,color:"#dc2626"},CS={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#0f172a"},$S={padding:"2px 10px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},ES={margin:0,fontSize:12,color:"#94a3b8"},IS={margin:0,fontSize:11,color:"#94a3b8"},AS={margin:"2px 0",padding:"6px 8px",fontSize:11,fontFamily:"monospace",color:"#0f172a",background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:6,whiteSpace:"pre-wrap",wordBreak:"break-all",overflowX:"auto"},TS={self:"layout_editor.error_rows.source_self",inherited:"layout_editor.error_rows.source_inherited",template:"layout_editor.error_rows.source_template",none:"layout_editor.error_rows.source_none"};function js({value:e,onChange:t,t:n,codes:o,mode:r="local",sourceOf:s,renderActionList:a,testIdPrefix:l="g7le-error-rows"}){const d=o??Object.keys(e),c=u.useCallback((f,y)=>{t({...e,[f]:y})},[e,t]),p=u.useCallback(f=>{const y={...e};delete y[f],t(y)},[e,t]);return i.jsx("div",{className:l,"data-testid":l,children:d.map(f=>{const y=e[f],g= Line truncated
}`,shapeErrorKey:"layout_editor.json_block.must_be_object",validate:_=>{const S=Object.keys(_??{}).filter(E=>!Is(E));return S.length?a("layout_editor.json_block.invalid_keys",{keys:S.join(", ")}):null}}):i.jsxs(i.Fragment,{children:[v.length===0?i.jsx("p",{"data-testid":`g7le-initstate-${e}-empty`,style:NS,children:a("layout_editor.initstate.no_values")}):v.map(_=>{const E=Nw(b,r,_)==="inherited",C=e==="isolated"&&l[_];return i.jsx(pS,{scope:e,itemKey:_,value:o[_],inherited:E,isOrphan:!!C,onChangeValue:N=>x(_,N),onRemove:()=>k(_),t:a},_)}),i.jsxs("div",{style:$S,children:[i.jsx("input",{type:"text","data-testid":`g7le-initstate-${e}-add-name`,value:d,placeholder:a("layout_editor.initstate.value_name"),onChange:_=>{c(_.target.value),y&&g(null)},onKeyDown:_=>{_.key==="Enter"&&(_.preventDefault(),w())},"aria-invalid":y?!0:void 0,style:{...ES,...y?IS:null}}),i.jsx("select",{"data-testid":`g7le-initstate-${e}-add-kind`,value:p,onChange:_=>f(_.target.value),"aria-label":a("layout_editor.initstate.value_kind"),style:TS,children:cS.map(_=>i.jsx("option",{value:_.kind,children:a(_.labelKey)},_.kind))}),i.jsxs("button",{type:"button","data-testid":`g7le-initstate-${e}-add`,onClick:w,style:DS,children:["+ ",a("layout_editor.initstate.add_value")]})]}),y?i.jsx("p",{"data-testid":`g7le-initstate-${e}-add-error`,style:AS,children:y}):null]})]})}function pS({scope:e,itemKey:t,value:n,inherited:o,isOrphan:r,onChangeValue:s,onRemove:a,t:l}){const[d,c]=u.useState(!1);let p;try{p=JSON.stringify(n,null,2)??String(n)}catch{p=String(n)}return i.jsxs("div",{"data-testid":`g7le-initstate-${e}-item-${t}`,style:xS,children:[i.jsxs("div",{style:wS,children:[o?i.jsx("span",{"data-testid":`g7le-initstate-inherited-${t}`,style:kS,children:"🔗"}):null,i.jsx("code",{style:SS,children:t}),i.jsx("span",{style:{flex:1}}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-code-${t}`,onClick:()=>c(f=>!f),style:Hs,"aria-label":l("layout_editor.value_tree.show_source"),title:l("layout_editor.value_tree.show_source"),children:"</>"}),o?i.jsx("button",{type:"button","data-testid":`g7le-initstate-revert-${t}`,onClick:a,style:Hs,children:l("layout_editor.initstate.revert")}):i.jsx("button",{type:"button","data-testid":`g7le-initstate-remove-${t}`,onClick:a,style:Hs,"aria-label":l("layout_editor.init_actions.remove"),children:"✕"})]}),d?i.jsx("pre",{"data-testid":`g7le-initstate-code-block-${t}`,style:MS,children:p}):null,i.jsx(Nn,{value:n,onChange:s,t:l,path:t,scope:e}),r?i.jsxs("p",{"data-testid":`g7le-initstate-isolated-orphan-${t}`,style:CS,children:["⚠ ",l("layout_editor.initstate.isolated_orphan")]}):null]})}const fS={display:"flex",flexDirection:"column",gap:14,minWidth:0},gS={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},hS={margin:0,fontSize:11,color:"#1d4ed8",background:"#eff6ff",padding:"6px 8px",borderRadius:6},yS={display:"flex",flexDirection:"column",gap:8,padding:10,border:"1px solid #e2e8f0",borderRadius:10,minWidth:0},bS={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},mS={fontSize:12,fontWeight:700,color:"#475569"},vS={margin:0,fontSize:11,color:"#94a3b8"},_S={padding:"2px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},xS={display:"flex",flexDirection:"column",gap:4,padding:"6px 8px",border:"1px solid #f1f5f9",borderRadius:6,minWidth:0},wS={display:"flex",alignItems:"center",gap:6},kS={fontSize:12},SS={fontSize:12,color:"#0f172a",fontFamily:"monospace"},Hs={border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",fontSize:11,padding:"2px 6px"},CS={margin:0,fontSize:11,color:"#b45309",background:"#fef3c7",padding:"4px 6px",borderRadius:6},$S={display:"flex",gap:6,alignItems:"center"},ES={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},IS={border:"1px solid #dc2626"},AS={margin:0,fontSize:11,color:"#dc2626"},TS={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#0f172a"},DS={padding:"2px 10px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},NS={margin:0,fontSize:12,color:"#94a3b8"},zS={margin:0,fontSize:11,color:"#94a3b8"},MS={margin:"2px 0",padding:"6px 8px",fontSize:11,fontFamily:"monospace",color:"#0f172a",background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:6,whiteSpace:"pre-wrap",wordBreak:"break-all",overflowX:"auto"},LS={self:"layout_editor.error_rows.source_self",inherited:"layout_editor.error_rows.source_inherited",template:"layout_editor.error_rows.source_template",none:"layout_editor.error_rows.source_none"};function Fs({value:e,onChange:t,t:n,codes:o,mode:r="local",sourceOf:s,renderActionList:a,testIdPrefix:l="g7le-error-rows"}){const d=o??Object.keys(e),c=u.useCallback((f,y)=>{t({...e,[f]:y})},[e,t]),p=u.useCallback(f=>{const y={...e};delete y[f],t(y)},[e,t]);return i.jsx("div",{className:l,"data-testid":l,children:d.map(f=>{const y=e[f],g= Line truncated
"page": "{{query.page ?? 1}}"
}`,shapeErrorKey:"layout_editor.data_sources.error.params_object",invalidErrorKey:"layout_editor.data_sources.error.params_json",t:n,onChange:I=>x(P=>P&&{...P,paramsRawValue:I??{},paramsRawValid:!0}),onValidityChange:I=>x(P=>P&&{...P,paramsRawValid:I})}):i.jsxs("div",{"data-testid":"g7le-data-sources-field-params",children:[b.paramRows.length===0&&i.jsx("p",{"data-testid":"g7le-ds-params-empty",style:WC,children:n("layout_editor.data_sources.field.params_empty")}),i.jsx(Dr,{value:b.paramRows.map(I=>({key:I.key,value:I.value})),onChange:I=>x({...b,paramRows:I.map(P=>({key:P.key??"",value:P.value??""}))}),keyField:"key",valueField:"value",t:n,candidates:d,testidPrefix:"g7le-ds-params-kv",keyPlaceholder:n("layout_editor.data_sources.field.param_key_placeholder"),valuePlaceholder:"{{query.page ?? 1}}",addLabel:n("layout_editor.data_sources.field.param_add")}),(()=>{const I=qs(b.paramRows);return Object.keys(I).length===0?null:i.jsxs("div",{style:Au,children:[i.jsxs("button",{type:"button","data-testid":"g7le-ds-params-preview-toggle","aria-expanded":_,onClick:()=>S(P=>!P),style:Tu,children:[_?"▴":"▾"," ",n("layout_editor.data_sources.field.params_preview")]}),_&&i.jsx("pre",{"data-testid":"g7le-ds-params-preview",style:{...Du,marginTop:4},children:JSON.stringify(I,null,2)})]})})()]}),i.jsx("div",{style:{fontSize:11,color:"#94a3b8",marginTop:4},children:n("layout_editor.data_sources.field.params_hint")})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.fallback")}),b.fallbackEnabled?i.jsxs("div",{"data-testid":"g7le-data-sources-field-fallback",style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx(Dn,{value:b.fallbackValue===void 0?{}:b.fallbackValue,onChange:I=>x({...b,fallbackValue:I}),t:n,path:"fallback",scope:"ds-fallback",renderStringLeaf:I=>i.jsx(Ve,{value:I.value,onChange:I.onChange,t:n,candidates:d,testidPrefix:I.testidPrefix})}),(()=>{const I=b.fallbackValue===void 0?{}:b.fallbackValue;return i.jsxs("div",{style:Au,children:[i.jsxs("button",{type:"button","data-testid":"g7le-ds-fallback-preview-toggle","aria-expanded":E,onClick:()=>C(P=>!P),style:Tu,children:[E?"▴":"▾"," ",n("layout_editor.data_sources.field.fallback_preview")]}),E&&i.jsx("pre",{"data-testid":"g7le-ds-fallback-preview",style:{...Du,marginTop:4},children:JSON.stringify(I,null,2)})]})})(),i.jsx("button",{type:"button","data-testid":"g7le-data-sources-field-fallback-clear",onClick:()=>x({...b,fallbackEnabled:!1,fallbackValue:void 0}),style:{...on,padding:"2px 8px",fontSize:11,alignSelf:"flex-start",color:"#dc2626",borderColor:"#fecaca"},children:n("layout_editor.data_sources.field.fallback_clear")})]}):i.jsxs("button",{type:"button","data-testid":"g7le-data-sources-field-fallback-add",onClick:()=>x({...b,fallbackEnabled:!0,fallbackValue:{}}),style:{...on,padding:"4px 10px",display:"block"},children:["+ ",n("layout_editor.data_sources.field.fallback_add")]})]}),b.type==="websocket"&&i.jsxs("div",{"data-testid":"g7le-ds-ws-section",style:To,children:[i.jsx("div",{style:Do,children:n("layout_editor.data_sources.section.websocket")}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.channel")}),i.jsx("input",{"data-testid":"g7le-ds-ws-channel",style:jt,value:b.channel,onChange:I=>x({...b,channel:I.target.value}),placeholder:"core.admin.dashboard"})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.event")}),i.jsx("input",{"data-testid":"g7le-ds-ws-event",style:jt,value:b.event,onChange:I=>x({...b,event:I.target.value}),placeholder:"dashboard.stats.updated"})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.channel_type")}),i.jsx("select",{"data-testid":"g7le-ds-ws-channeltype",style:jt,value:b.channel_type,onChange:I=>x({...b,channel_type:I.target.value}),children:MC.map(I=>i.jsx("option",{value:I,children:I},I))})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.target_source")}),i.jsx("input",{"data-testid":"g7le-ds-ws-target",style:jt,value:b.target_source,onChange:I=>x({...b,target_source:I.target.value}),list:"g7le-ds-ws-target-list"}),i.jsx("datalist",{id:"g7le-ds-ws-target-list",children:h.filter(I=>I.id!==b.id).map(I=>typeof I.id=="string"?i.jsx("option",{value:I.id},I.id):null)})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.on_receive")}),i.jsx("div",{"data-testid":"g7le-ds-ws-onreceive",children:i.jsx(Gt,{actions:b.onReceive,onChange:I=>x({...b,onReceive:I}),t:n,recipes:s,candidatePools:z,chipContext:"payload",actionChipCandidates:l?.actionChipCandidates??null,testIdPrefix:"g7le-ds-onreceive-list"})})]})]}),i.jsxs("div",{style:To,children:[i.jsx("div",{style:Do,children:n("layout_editor.data_sources.section.on_success")}),i.jsx("div",{"data-testid":"g7le-ds-onsuccess",children:i.j Line truncated
}`,shapeErrorKey:"layout_editor.data_sources.error.params_object",invalidErrorKey:"layout_editor.data_sources.error.params_json",t:n,onChange:I=>x(P=>P&&{...P,paramsRawValue:I??{},paramsRawValid:!0}),onValidityChange:I=>x(P=>P&&{...P,paramsRawValid:I})}):i.jsxs("div",{"data-testid":"g7le-data-sources-field-params",children:[b.paramRows.length===0&&i.jsx("p",{"data-testid":"g7le-ds-params-empty",style:VC,children:n("layout_editor.data_sources.field.params_empty")}),i.jsx(zr,{value:b.paramRows.map(I=>({key:I.key,value:I.value})),onChange:I=>x({...b,paramRows:I.map(P=>({key:P.key??"",value:P.value??""}))}),keyField:"key",valueField:"value",t:n,candidates:d,testidPrefix:"g7le-ds-params-kv",keyPlaceholder:n("layout_editor.data_sources.field.param_key_placeholder"),valuePlaceholder:"{{query.page ?? 1}}",addLabel:n("layout_editor.data_sources.field.param_add")}),(()=>{const I=Ys(b.paramRows);return Object.keys(I).length===0?null:i.jsxs("div",{style:Nu,children:[i.jsxs("button",{type:"button","data-testid":"g7le-ds-params-preview-toggle","aria-expanded":_,onClick:()=>S(P=>!P),style:zu,children:[_?"▴":"▾"," ",n("layout_editor.data_sources.field.params_preview")]}),_&&i.jsx("pre",{"data-testid":"g7le-ds-params-preview",style:{...Mu,marginTop:4},children:JSON.stringify(I,null,2)})]})})()]}),i.jsx("div",{style:{fontSize:11,color:"#94a3b8",marginTop:4},children:n("layout_editor.data_sources.field.params_hint")})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.fallback")}),b.fallbackEnabled?i.jsxs("div",{"data-testid":"g7le-data-sources-field-fallback",style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx(Nn,{value:b.fallbackValue===void 0?{}:b.fallbackValue,onChange:I=>x({...b,fallbackValue:I}),t:n,path:"fallback",scope:"ds-fallback",renderStringLeaf:I=>i.jsx(Ve,{value:I.value,onChange:I.onChange,t:n,candidates:d,testidPrefix:I.testidPrefix})}),(()=>{const I=b.fallbackValue===void 0?{}:b.fallbackValue;return i.jsxs("div",{style:Nu,children:[i.jsxs("button",{type:"button","data-testid":"g7le-ds-fallback-preview-toggle","aria-expanded":E,onClick:()=>C(P=>!P),style:zu,children:[E?"▴":"▾"," ",n("layout_editor.data_sources.field.fallback_preview")]}),E&&i.jsx("pre",{"data-testid":"g7le-ds-fallback-preview",style:{...Mu,marginTop:4},children:JSON.stringify(I,null,2)})]})})(),i.jsx("button",{type:"button","data-testid":"g7le-data-sources-field-fallback-clear",onClick:()=>x({...b,fallbackEnabled:!1,fallbackValue:void 0}),style:{...rn,padding:"2px 8px",fontSize:11,alignSelf:"flex-start",color:"#dc2626",borderColor:"#fecaca"},children:n("layout_editor.data_sources.field.fallback_clear")})]}):i.jsxs("button",{type:"button","data-testid":"g7le-data-sources-field-fallback-add",onClick:()=>x({...b,fallbackEnabled:!0,fallbackValue:{}}),style:{...rn,padding:"4px 10px",display:"block"},children:["+ ",n("layout_editor.data_sources.field.fallback_add")]})]}),b.type==="websocket"&&i.jsxs("div",{"data-testid":"g7le-ds-ws-section",style:No,children:[i.jsx("div",{style:zo,children:n("layout_editor.data_sources.section.websocket")}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.channel")}),i.jsx("input",{"data-testid":"g7le-ds-ws-channel",style:jt,value:b.channel,onChange:I=>x({...b,channel:I.target.value}),placeholder:"core.admin.dashboard"})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.event")}),i.jsx("input",{"data-testid":"g7le-ds-ws-event",style:jt,value:b.event,onChange:I=>x({...b,event:I.target.value}),placeholder:"dashboard.stats.updated"})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.channel_type")}),i.jsx("select",{"data-testid":"g7le-ds-ws-channeltype",style:jt,value:b.channel_type,onChange:I=>x({...b,channel_type:I.target.value}),children:WC.map(I=>i.jsx("option",{value:I,children:I},I))})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.target_source")}),i.jsx("input",{"data-testid":"g7le-ds-ws-target",style:jt,value:b.target_source,onChange:I=>x({...b,target_source:I.target.value}),list:"g7le-ds-ws-target-list"}),i.jsx("datalist",{id:"g7le-ds-ws-target-list",children:h.filter(I=>I.id!==b.id).map(I=>typeof I.id=="string"?i.jsx("option",{value:I.id},I.id):null)})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.on_receive")}),i.jsx("div",{"data-testid":"g7le-ds-ws-onreceive",children:i.jsx(Gt,{actions:b.onReceive,onChange:I=>x({...b,onReceive:I}),t:n,recipes:s,candidatePools:z,chipContext:"payload",actionChipCandidates:l?.actionChipCandidates??null,testIdPrefix:"g7le-ds-onreceive-list"})})]})]}),i.jsxs("div",{style:No,children:[i.jsx("div",{style:zo,children:n("layout_editor.data_sources.section.on_success")}),i.jsx("div",{"data-testid":"g7le-ds-onsuccess",children:i.j Line truncated
`).split(`
`);return t.length>0&&t[t.length-1]===""&&t.pop(),t}const Fu=4e3;function m$(e,t,n=3){const o=Hu(e),r=Hu(t);let s=0;const a=Math.min(o.length,r.length);for(;s<a&&o[s]===r[s];)s++;let l=0;const d=Math.min(o.length,r.length)-s;for(;l<d&&o[o.length-1-l]===r[r.length-1-l];)l++;const c=o.slice(s,o.length-l),p=r.slice(s,r.length-l);if(c.length>Fu||p.length>Fu)return{hunks:[],addedCount:-1,removedCount:-1,identical:!1};const f=y$(c,p),y=b$(c,p,f),g=[];let m=s,h=s,v=0,b=0;for(let w=0;w<s;w++)g.push({kind:"context",oldLine:w+1,newLine:w+1,content:o[w]});for(const w of y)w.kind==="context"?(m++,h++,g.push({kind:"context",oldLine:m,newLine:h,content:w.content})):w.kind==="remove"?(m++,b++,g.push({kind:"remove",oldLine:m,newLine:null,content:w.content})):(h++,v++,g.push({kind:"add",oldLine:null,newLine:h,content:w.content}));for(let w=0;w<l;w++){const _=o.length-l+w,S=r.length-l+w;g.push({kind:"context",oldLine:_+1,newLine:S+1,content:o[_]})}const x=v===0&&b===0;return{hunks:v$(g,n),addedCount:v,removedCount:b,identical:x}}function v$(e,t){const n=[];for(let r=0;r<e.length;r++)e[r].kind!=="context"&&n.push(r);if(n.length===0)return[];const o=[];for(const r of n){const s=Math.max(0,r-t),a=Math.min(e.length-1,r+t),l=o[o.length-1];l&&s<=l.end+1?l.end=Math.max(l.end,a):o.push({start:s,end:a})}return o.map(r=>{const s=e.slice(r.start,r.end+1);let a=0,l=0,d=0,c=0;for(const g of s)g.oldLine!==null&&(a===0&&(a=g.oldLine),d++),g.newLine!==null&&(l===0&&(l=g.newLine),c++);const p=a||(d===0?0:1),f=l||(c===0?0:1);return{header:`@@ -${p},${d} +${f},${c} @@`,oldStart:p,oldCount:d,newStart:f,newCount:c,lines:s}})}function Ku(e){return JSON.stringify(Ys(e),null,2)}function Ys(e){if(Array.isArray(e))return e.map(Ys);if(e&&typeof e=="object"){const t=e,n={};for(const o of Object.keys(t).sort())n[o]=Ys(t[o]);return n}return e}const _$={display:"flex",flexDirection:"column",height:"100%",minHeight:0},x$={display:"flex",alignItems:"center",gap:8,padding:"12px 16px",borderBottom:"1px solid #e2e8f0",background:"#f8fafc"},w$={display:"inline-flex",alignItems:"center",gap:4,padding:"4px 10px",borderRadius:6,border:"1px solid #cbd5e1",background:"#ffffff",color:"#0f172a",fontSize:12,fontWeight:600,cursor:"pointer",outline:"none"},k$={marginLeft:"auto",border:"none",background:"transparent",fontSize:16,color:"#64748b",cursor:"pointer",outline:"none"},S$={display:"flex",alignItems:"center",gap:12,padding:"8px 16px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",fontSize:12,fontWeight:600},C$={flex:1,minHeight:0,overflow:"auto",background:"#ffffff",fontFamily:'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',fontSize:12,lineHeight:1.5},Vu={padding:"24px 12px",textAlign:"center",color:"#94a3b8",fontSize:13},$$={padding:"2px 12px",background:"#f1f5f9",color:"#64748b",borderTop:"1px solid #e2e8f0",borderBottom:"1px solid #e2e8f0",userSelect:"none"};function E$(e){return e==="add"?{row:{background:"#e6ffed"},gutter:{background:"#cdffd8",color:"#22863a"},sign:"+",signColor:"#22863a"}:e==="remove"?{row:{background:"#ffeef0"},gutter:{background:"#ffdce0",color:"#b31d28"},sign:"-",signColor:"#b31d28"}:{row:{background:"#ffffff"},gutter:{background:"#f8fafc",color:"#94a3b8"},sign:" ",signColor:"#94a3b8"}}const Uu={display:"inline-block",width:44,textAlign:"right",padding:"0 6px",color:"#94a3b8",userSelect:"none",borderRight:"1px solid #e2e8f0",whiteSpace:"nowrap"};function I$({oldVersion:e,newVersion:t,oldContent:n,newContent:o,t:r,onBack:s,onClose:a}){const l=u.useMemo(()=>{const c=Ku(n),p=Ku(o);return m$(c,p)},[n,o]),d=l.addedCount<0;return i.jsxs("div",{className:"g7le-version-diff","data-testid":"g7le-version-diff",style:_$,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:x$,children:[i.jsxs("button",{type:"button",onClick:s,style:w$,"data-testid":"g7le-version-diff-back",children:["← ",r("layout_editor.version_history.diff_back")]}),i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-version-diff-title",children:r("layout_editor.version_history.diff_title",{old:e,new:t})}),i.jsx("button",{type:"button","aria-label":r("layout_editor.version_history.close"),onClick:a,style:k$,"data-testid":"g7le-version-diff-close",children:"✕"})]}),!d&&i.jsxs("div",{style:S$,"data-testid":"g7le-version-diff-summary",children:[i.jsx("span",{style:{color:"#16a34a"},"data-testid":"g7le-version-diff-added",children:r("layout_editor.version_history.added",{count:Math.max(0,l.addedCount)})}),i.jsx("span",{style:{color:"#dc2626"},"data-testid":"g7le-version-diff-removed",children:r("layout_editor.version_history.removed",{count:Math.max(0,l.removedCount)})})]}),i.jsx("div",{style:C$,children:d?i.jsx("div",{style:Vu,"data-testid":"g7le-version-diff-too-large",children:r("layout_editor.version_history.diff_too_large")}):l.identical?i.jsx("div",{style:Vu,"data-testid":"g7le-version-diff-identical",children:r("layout_editor.version_history.diff_identical")}):l.hunks.map((c,p)=>i.jsxs("div",{"data-testid":`gLine truncated
`);return t.length>0&&t[t.length-1]===""&&t.pop(),t}const Uu=4e3;function k$(e,t,n=3){const o=Vu(e),r=Vu(t);let s=0;const a=Math.min(o.length,r.length);for(;s<a&&o[s]===r[s];)s++;let l=0;const d=Math.min(o.length,r.length)-s;for(;l<d&&o[o.length-1-l]===r[r.length-1-l];)l++;const c=o.slice(s,o.length-l),p=r.slice(s,r.length-l);if(c.length>Uu||p.length>Uu)return{hunks:[],addedCount:-1,removedCount:-1,identical:!1};const f=x$(c,p),y=w$(c,p,f),g=[];let m=s,h=s,v=0,b=0;for(let w=0;w<s;w++)g.push({kind:"context",oldLine:w+1,newLine:w+1,content:o[w]});for(const w of y)w.kind==="context"?(m++,h++,g.push({kind:"context",oldLine:m,newLine:h,content:w.content})):w.kind==="remove"?(m++,b++,g.push({kind:"remove",oldLine:m,newLine:null,content:w.content})):(h++,v++,g.push({kind:"add",oldLine:null,newLine:h,content:w.content}));for(let w=0;w<l;w++){const _=o.length-l+w,S=r.length-l+w;g.push({kind:"context",oldLine:_+1,newLine:S+1,content:o[_]})}const x=v===0&&b===0;return{hunks:S$(g,n),addedCount:v,removedCount:b,identical:x}}function S$(e,t){const n=[];for(let r=0;r<e.length;r++)e[r].kind!=="context"&&n.push(r);if(n.length===0)return[];const o=[];for(const r of n){const s=Math.max(0,r-t),a=Math.min(e.length-1,r+t),l=o[o.length-1];l&&s<=l.end+1?l.end=Math.max(l.end,a):o.push({start:s,end:a})}return o.map(r=>{const s=e.slice(r.start,r.end+1);let a=0,l=0,d=0,c=0;for(const g of s)g.oldLine!==null&&(a===0&&(a=g.oldLine),d++),g.newLine!==null&&(l===0&&(l=g.newLine),c++);const p=a||(d===0?0:1),f=l||(c===0?0:1);return{header:`@@ -${p},${d} +${f},${c} @@`,oldStart:p,oldCount:d,newStart:f,newCount:c,lines:s}})}function qu(e){return JSON.stringify(Js(e),null,2)}function Js(e){if(Array.isArray(e))return e.map(Js);if(e&&typeof e=="object"){const t=e,n={};for(const o of Object.keys(t).sort())n[o]=Js(t[o]);return n}return e}const C$={display:"flex",flexDirection:"column",height:"100%",minHeight:0},$$={display:"flex",alignItems:"center",gap:8,padding:"12px 16px",borderBottom:"1px solid #e2e8f0",background:"#f8fafc"},E$={display:"inline-flex",alignItems:"center",gap:4,padding:"4px 10px",borderRadius:6,border:"1px solid #cbd5e1",background:"#ffffff",color:"#0f172a",fontSize:12,fontWeight:600,cursor:"pointer",outline:"none"},I$={marginLeft:"auto",border:"none",background:"transparent",fontSize:16,color:"#64748b",cursor:"pointer",outline:"none"},A$={display:"flex",alignItems:"center",gap:12,padding:"8px 16px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",fontSize:12,fontWeight:600},T$={flex:1,minHeight:0,overflow:"auto",background:"#ffffff",fontFamily:'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',fontSize:12,lineHeight:1.5},Gu={padding:"24px 12px",textAlign:"center",color:"#94a3b8",fontSize:13},D$={padding:"2px 12px",background:"#f1f5f9",color:"#64748b",borderTop:"1px solid #e2e8f0",borderBottom:"1px solid #e2e8f0",userSelect:"none"};function N$(e){return e==="add"?{row:{background:"#e6ffed"},gutter:{background:"#cdffd8",color:"#22863a"},sign:"+",signColor:"#22863a"}:e==="remove"?{row:{background:"#ffeef0"},gutter:{background:"#ffdce0",color:"#b31d28"},sign:"-",signColor:"#b31d28"}:{row:{background:"#ffffff"},gutter:{background:"#f8fafc",color:"#94a3b8"},sign:" ",signColor:"#94a3b8"}}const Yu={display:"inline-block",width:44,textAlign:"right",padding:"0 6px",color:"#94a3b8",userSelect:"none",borderRight:"1px solid #e2e8f0",whiteSpace:"nowrap"};function z$({oldVersion:e,newVersion:t,oldContent:n,newContent:o,t:r,onBack:s,onClose:a}){const l=u.useMemo(()=>{const c=qu(n),p=qu(o);return k$(c,p)},[n,o]),d=l.addedCount<0;return i.jsxs("div",{className:"g7le-version-diff","data-testid":"g7le-version-diff",style:C$,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:$$,children:[i.jsxs("button",{type:"button",onClick:s,style:E$,"data-testid":"g7le-version-diff-back",children:["← ",r("layout_editor.version_history.diff_back")]}),i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-version-diff-title",children:r("layout_editor.version_history.diff_title",{old:e,new:t})}),i.jsx("button",{type:"button","aria-label":r("layout_editor.version_history.close"),onClick:a,style:I$,"data-testid":"g7le-version-diff-close",children:"✕"})]}),!d&&i.jsxs("div",{style:A$,"data-testid":"g7le-version-diff-summary",children:[i.jsx("span",{style:{color:"#16a34a"},"data-testid":"g7le-version-diff-added",children:r("layout_editor.version_history.added",{count:Math.max(0,l.addedCount)})}),i.jsx("span",{style:{color:"#dc2626"},"data-testid":"g7le-version-diff-removed",children:r("layout_editor.version_history.removed",{count:Math.max(0,l.removedCount)})})]}),i.jsx("div",{style:T$,children:d?i.jsx("div",{style:Gu,"data-testid":"g7le-version-diff-too-large",children:r("layout_editor.version_history.diff_too_large")}):l.identical?i.jsx("div",{style:Gu,"data-testid":"g7le-version-diff-identical",children:r("layout_editor.version_history.diff_identical")}):l.hunks.map((c,p)=>i.jsxs("div",{"data-testid":`gLine truncated
To pick up a draggable item, press the space bar.
While dragging, use the arrow keys to move the item.
Press space again to drop the item in its new position, or press escape to cancel.
`},I7={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was moved over droppable area "+n.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was dropped over droppable area "+n.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function A7(e){let{announcements:t=I7,container:n,hiddenTextDescribedById:o,screenReaderInstructions:r=E7}=e;const{announce:s,announcement:a}=S7(),l=Qr("DndLiveRegion"),[d,c]=u.useState(!1);if(u.useEffect(()=>{c(!0)},[]),C7(u.useMemo(()=>({onDragStart(f){let{active:y}=f;s(t.onDragStart({active:y}))},onDragMove(f){let{active:y,over:g}=f;t.onDragMove&&s(t.onDragMove({active:y,over:g}))},onDragOver(f){let{active:y,over:g}=f;s(t.onDragOver({active:y,over:g}))},onDragEnd(f){let{active:y,over:g}=f;s(t.onDragEnd({active:y,over:g}))},onDragCancel(f){let{active:y,over:g}=f;s(t.onDragCancel({active:y,over:g}))}}),[s,t])),!d)return null;const p=u.createElement(u.Fragment,null,u.createElement(w7,{id:o,value:r.draggable}),u.createElement(k7,{id:l,announcement:a}));return n?Sn.createPortal(p,n):p}var it;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(it||(it={}));function ti(){}function T7(e,t){return u.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function D7(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return u.useMemo(()=>[...t].filter(o=>o!=null),[...t])}const Ht=Object.freeze({x:0,y:0});function N7(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function z7(e,t){const n=ei(e);if(!n)return"0 0";const o={x:(n.x-t.left)/t.width*100,y:(n.y-t.top)/t.height*100};return o.x+"% "+o.y+"%"}function M7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return n-o}function L7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return o-n}function P7(e){let{left:t,top:n,height:o,width:r}=e;return[{x:t,y:n},{x:t+r,y:n},{x:t,y:n+o},{x:t+r,y:n+o}]}function O7(e,t){if(!e||e.length===0)return null;const[n]=e;return n[t]}function B7(e,t){const n=Math.max(t.top,e.top),o=Math.max(t.left,e.left),r=Math.min(t.left+t.width,e.left+e.width),s=Math.min(t.top+t.height,e.top+e.height),a=r-o,l=s-n;if(o<r&&n<s){const d=t.width*t.height,c=e.width*e.height,p=a*l,f=p/(d+c-p);return Number(f.toFixed(4))}return 0}const W7=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:o}=e;const r=[];for(const s of o){const{id:a}=s,l=n.get(a);if(l){const d=B7(l,t);d>0&&r.push({id:a,data:{droppableContainer:s,value:d}})}}return r.sort(L7)};function j7(e,t){const{top:n,left:o,bottom:r,right:s}=t;return n<=e.y&&e.y<=r&&o<=e.x&&e.x<=s}const H7=e=>{let{droppableContainers:t,droppableRects:n,pointerCoordinates:o}=e;if(!o)return[];const r=[];for(const s of t){const{id:a}=s,l=n.get(a);if(l&&j7(o,l)){const c=P7(l).reduce((f,y)=>f+N7(o,y),0),p=Number((c/4).toFixed(4));r.push({id:a,data:{droppableContainer:s,value:p}})}}return r.sort(M7)};function F7(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}function Op(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Ht}function K7(e){return function(n){for(var o=arguments.length,r=new Array(o>1?o-1:0),s=1;s<o;s++)r[s-1]=arguments[s];return r.reduce((a,l)=>({...a,top:a.top+e*l.y,bottom:a.bottom+e*l.y,left:a.left+e*l.x,right:a.right+e*l.x}),{...n})}}const V7=K7(1);function Bp(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function U7(e,t,n){const o=Bp(t);if(!o)return e;const{scaleX:r,scaleY:s,x:a,y:l}=o,d=e.left-a-(1-r)*parseFloat(n),c=e.top-l-(1-s)*parseFloat(n.slice(n.indexOf(" ")+1)),p=r?e.width/r:e.width,f=s?e.height/s:e.height;return{width:p,height:f,top:c,right:d+p,bottom:c+f,left:d}}const q7={ignoreTransform:!1};function Bo(e,t){t===void 0&&(t=q7);let n=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:p}=_t(e).getComputedStyle(e);c&&(n=U7(n,c,p))}const{top:o,left:r,width:s,height:a,bottom:l,right:d}=n;return{top:o,left:r,width:s,height:a,bottom:l,right:d}}function Wp(e){return Bo(e,{ignoreTransform:!0})}function G7(e){const t=e.innerWidth,n=e.innerHeight;return{top:0,left:0,right:t,bottom:n,width:t,height:n}}function Y7(e,t){return t===void 0&&(t=_t(e).getComputedStyle(e)),t.position==="fixed"}function X7(e,t){t===void 0&&(t=_t(e).getComputedStyle(e));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(r=>{const s=t[r];return typeof s=="string"?n.test(s):!1})}function aa(e,t){const n=[]; Line truncated
`:"",e.message]})]}),i.jsxs("div",{style:{display:"flex",justifyContent:"center",gap:8,flexWrap:"wrap"},children:[e.kind==="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-signin",onClick:a,style:Ha,children:t("layout_editor.access_error.action.sign_in")}),e.kind==="forbidden"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home",onClick:r,style:Ha,children:t("layout_editor.access_error.action.go_home")}),(e.kind==="not_found"||e.kind==="server_error"||e.kind==="network"||e.kind==="unknown")&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-retry",onClick:s,style:Ha,children:t("layout_editor.access_error.action.retry")}),e.kind!=="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home-secondary",onClick:r,style:h4,children:t("layout_editor.access_error.action.go_admin_home")})]})]})]})}const Ha={padding:"8px 16px",background:"#2563eb",color:"#fff",border:"1px solid #1d4ed8",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},h4={padding:"8px 16px",background:"#fff",color:"#475569",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13,fontWeight:500,cursor:"pointer"};function y4(e){const t=window;if(typeof t>"u")return{getSnapshot:()=>({globalState:{},localState:{}}),restore:()=>{}};const n=t.G7Core||{},o={templateApp:t.__templateApp,g7CoreState:n.state,g7CoreDispatch:n.dispatch,g7CoreData:n.data,g7CoreModal:n.modal,g7CoreToast:n.toast};let r={...e.initialGlobalState??{}};r._local||(r._local={});const s=[],a=()=>{const c={globalState:{...r},localState:{...r._local??{}}};for(const p of s)try{p(r)}catch{}e.onChange?.(c)},l={},d={getGlobalState:()=>r,setGlobalState:(c,p)=>{typeof c=="function"?r=c(r):r={...r,...c},a()},onGlobalStateChange:c=>(s.push(c),()=>{const p=s.indexOf(c);p>=0&&s.splice(p,1)}),get globalState(){return r},getActionDispatcher:()=>e.actionDispatcher,getDataSource:c=>l[c]??null,setDataSource:(c,p)=>{l[c]=p},refetchDataSource:async c=>null,updateDataSourceItem:(c,p,f)=>l[c]??null,getLocale:()=>e.locale,changeLocale:c=>{},getRouter:()=>({navigate:()=>{},replace:()=>{},push:()=>{}}),updateQueryParams:()=>{}};return t.__templateApp=d,n&&typeof n.dispatch=="function"&&(n.dispatch=(c,p)=>{try{return e.actionDispatcher.executeAction(c,p??{})}catch{return}}),n&&typeof n.modal=="object"&&n.modal!==null&&(n.modal={...n.modal,open:()=>{},close:()=>{},closeAll:()=>{}}),n&&typeof n.toast=="object"&&n.toast!==null&&(n.toast={...n.toast,push:()=>{},dismiss:()=>{}}),{getSnapshot:()=>({globalState:{...r},localState:{...r._local??{}}}),restore:()=>{t.__templateApp===d&&(t.__templateApp=o.templateApp),t.G7Core&&(o.g7CoreState!==void 0&&(t.G7Core.state=o.g7CoreState),o.g7CoreDispatch!==void 0&&(t.G7Core.dispatch=o.g7CoreDispatch),o.g7CoreData!==void 0&&(t.G7Core.data=o.g7CoreData),o.g7CoreModal!==void 0&&(t.G7Core.modal=o.g7CoreModal),o.g7CoreToast!==void 0&&(t.G7Core.toast=o.g7CoreToast)),s.length=0}}}const b4=new Set(["id","userId","postId","orderId","productId","categoryId","commentId","noticeId","inquiryId","addressId","wishlistId","notificationId","roleId","menuId"]);function m4(e){if(!e)return{};const t={},n=/:([a-zA-Z_][a-zA-Z0-9_]*)/g;let o;for(;(o=n.exec(e))!==null;){const r=o[1];t[r]=v4(r)}return t}function v4(e){return b4.has(e)||/Id$|_id$/.test(e)?"1":"sample"}const _4=ar("LayoutEditor");function Go(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function hi(e){if(Array.isArray(e))return e.map(t=>hi(t));if(Go(e)){const t={};for(const[n,o]of Object.entries(e))t[n]=hi(o);return t}return e}function pg(e,t="",n=new Set){for(const[o,r]of Object.entries(e)){const s=t?`${t}.${o}`:o;Go(r)?pg(r,s,n):n.add(s)}return n}function fg(e,t,n,o,r,s=""){for(const[a,l]of Object.entries(t)){const d=s?`${s}.${a}`:a;if(n.has(d)){r(`[LayoutEditor] sampleGlobal key conflict: extension '${o}' override of core key '${d}' ignored`);continue}const c=e[a];Go(l)&&Go(c)?fg(c,l,n,o,r,d):e[a]=hi(l)}}function Fa(e){const t=e.warn??(r=>_4.warn(r)),n=hi(e.coreSeed??{});e.isGuestOnly&&"currentUser"in n&&delete n.currentUser;const o=pg(n);for(const r of e.sources)Go(r.sampleGlobal)&&fg(n,r.sampleGlobal,o,r.id,t);return e.isGuestOnly&&"currentUser"in n&&delete n.currentUser,n}function x4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")&&(t[n]=o);return t}function w4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")||(t[n]=o);return t}function k4(e){if(typeof e!="string")return null;const t=/^\s*\{\{\s*([\s\S]*?)\s*\}\}\s*$/.exec(e);if(!t)return null;let n=t[1];const o=n.indexOf("??");return o>=0&&(n=n.slice(0,o)),n=n.replace(/\?\./g,".").replace(/\s+/g,""),n===""||!/^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(n)?null:n.split(".")}function S4(e,t){const n=k4(t);if(!n||n.length===0)return e;let o=e;for(let a=0;a<n.length;a++){if(o==null||typeof o!="obj Line truncated
`},z7={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was moved over droppable area "+n.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was dropped over droppable area "+n.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function M7(e){let{announcements:t=z7,container:n,hiddenTextDescribedById:o,screenReaderInstructions:r=N7}=e;const{announce:s,announcement:a}=A7(),l=ei("DndLiveRegion"),[d,c]=u.useState(!1);if(u.useEffect(()=>{c(!0)},[]),T7(u.useMemo(()=>({onDragStart(f){let{active:y}=f;s(t.onDragStart({active:y}))},onDragMove(f){let{active:y,over:g}=f;t.onDragMove&&s(t.onDragMove({active:y,over:g}))},onDragOver(f){let{active:y,over:g}=f;s(t.onDragOver({active:y,over:g}))},onDragEnd(f){let{active:y,over:g}=f;s(t.onDragEnd({active:y,over:g}))},onDragCancel(f){let{active:y,over:g}=f;s(t.onDragCancel({active:y,over:g}))}}),[s,t])),!d)return null;const p=u.createElement(u.Fragment,null,u.createElement(E7,{id:o,value:r.draggable}),u.createElement(I7,{id:l,announcement:a}));return n?Cn.createPortal(p,n):p}var it;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(it||(it={}));function oi(){}function L7(e,t){return u.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function P7(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return u.useMemo(()=>[...t].filter(o=>o!=null),[...t])}const Ht=Object.freeze({x:0,y:0});function O7(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function B7(e,t){const n=ni(e);if(!n)return"0 0";const o={x:(n.x-t.left)/t.width*100,y:(n.y-t.top)/t.height*100};return o.x+"% "+o.y+"%"}function W7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return n-o}function j7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return o-n}function H7(e){let{left:t,top:n,height:o,width:r}=e;return[{x:t,y:n},{x:t+r,y:n},{x:t,y:n+o},{x:t+r,y:n+o}]}function F7(e,t){if(!e||e.length===0)return null;const[n]=e;return n[t]}function K7(e,t){const n=Math.max(t.top,e.top),o=Math.max(t.left,e.left),r=Math.min(t.left+t.width,e.left+e.width),s=Math.min(t.top+t.height,e.top+e.height),a=r-o,l=s-n;if(o<r&&n<s){const d=t.width*t.height,c=e.width*e.height,p=a*l,f=p/(d+c-p);return Number(f.toFixed(4))}return 0}const V7=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:o}=e;const r=[];for(const s of o){const{id:a}=s,l=n.get(a);if(l){const d=K7(l,t);d>0&&r.push({id:a,data:{droppableContainer:s,value:d}})}}return r.sort(j7)};function U7(e,t){const{top:n,left:o,bottom:r,right:s}=t;return n<=e.y&&e.y<=r&&o<=e.x&&e.x<=s}const q7=e=>{let{droppableContainers:t,droppableRects:n,pointerCoordinates:o}=e;if(!o)return[];const r=[];for(const s of t){const{id:a}=s,l=n.get(a);if(l&&U7(o,l)){const c=H7(l).reduce((f,y)=>f+O7(o,y),0),p=Number((c/4).toFixed(4));r.push({id:a,data:{droppableContainer:s,value:p}})}}return r.sort(W7)};function G7(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}function jp(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Ht}function Y7(e){return function(n){for(var o=arguments.length,r=new Array(o>1?o-1:0),s=1;s<o;s++)r[s-1]=arguments[s];return r.reduce((a,l)=>({...a,top:a.top+e*l.y,bottom:a.bottom+e*l.y,left:a.left+e*l.x,right:a.right+e*l.x}),{...n})}}const X7=Y7(1);function Hp(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function J7(e,t,n){const o=Hp(t);if(!o)return e;const{scaleX:r,scaleY:s,x:a,y:l}=o,d=e.left-a-(1-r)*parseFloat(n),c=e.top-l-(1-s)*parseFloat(n.slice(n.indexOf(" ")+1)),p=r?e.width/r:e.width,f=s?e.height/s:e.height;return{width:p,height:f,top:c,right:d+p,bottom:c+f,left:d}}const Z7={ignoreTransform:!1};function jo(e,t){t===void 0&&(t=Z7);let n=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:p}=_t(e).getComputedStyle(e);c&&(n=J7(n,c,p))}const{top:o,left:r,width:s,height:a,bottom:l,right:d}=n;return{top:o,left:r,width:s,height:a,bottom:l,right:d}}function Fp(e){return jo(e,{ignoreTransform:!0})}function Q7(e){const t=e.innerWidth,n=e.innerHeight;return{top:0,left:0,right:t,bottom:n,width:t,height:n}}function R7(e,t){return t===void 0&&(t=_t(e).getComputedStyle(e)),t.position==="fixed"}function e2(e,t){t===void 0&&(t=_t(e).getComputedStyle(e));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(r=>{const s=t[r];return typeof s=="string"?n.test(s):!1})}function da(e,t){const n=[]; Line truncated
`:"",e.message]})]}),i.jsxs("div",{style:{display:"flex",justifyContent:"center",gap:8,flexWrap:"wrap"},children:[e.kind==="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-signin",onClick:a,style:Ka,children:t("layout_editor.access_error.action.sign_in")}),e.kind==="forbidden"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home",onClick:r,style:Ka,children:t("layout_editor.access_error.action.go_home")}),(e.kind==="not_found"||e.kind==="server_error"||e.kind==="network"||e.kind==="unknown")&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-retry",onClick:s,style:Ka,children:t("layout_editor.access_error.action.retry")}),e.kind!=="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home-secondary",onClick:r,style:_4,children:t("layout_editor.access_error.action.go_admin_home")})]})]})]})}const Ka={padding:"8px 16px",background:"#2563eb",color:"#fff",border:"1px solid #1d4ed8",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},_4={padding:"8px 16px",background:"#fff",color:"#475569",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13,fontWeight:500,cursor:"pointer"};function x4(e){const t=window;if(typeof t>"u")return{getSnapshot:()=>({globalState:{},localState:{}}),restore:()=>{}};const n=t.G7Core||{},o={templateApp:t.__templateApp,g7CoreState:n.state,g7CoreDispatch:n.dispatch,g7CoreData:n.data,g7CoreModal:n.modal,g7CoreToast:n.toast};let r={...e.initialGlobalState??{}};r._local||(r._local={});const s=[],a=()=>{const c={globalState:{...r},localState:{...r._local??{}}};for(const p of s)try{p(r)}catch{}e.onChange?.(c)},l={},d={getGlobalState:()=>r,setGlobalState:(c,p)=>{typeof c=="function"?r=c(r):r={...r,...c},a()},onGlobalStateChange:c=>(s.push(c),()=>{const p=s.indexOf(c);p>=0&&s.splice(p,1)}),get globalState(){return r},getActionDispatcher:()=>e.actionDispatcher,getDataSource:c=>l[c]??null,setDataSource:(c,p)=>{l[c]=p},refetchDataSource:async c=>null,updateDataSourceItem:(c,p,f)=>l[c]??null,getLocale:()=>e.locale,changeLocale:c=>{},getRouter:()=>({navigate:()=>{},replace:()=>{},push:()=>{}}),updateQueryParams:()=>{}};return t.__templateApp=d,n&&typeof n.dispatch=="function"&&(n.dispatch=(c,p)=>{try{return e.actionDispatcher.executeAction(c,p??{})}catch{return}}),n&&typeof n.modal=="object"&&n.modal!==null&&(n.modal={...n.modal,open:()=>{},close:()=>{},closeAll:()=>{}}),n&&typeof n.toast=="object"&&n.toast!==null&&(n.toast={...n.toast,push:()=>{},dismiss:()=>{}}),{getSnapshot:()=>({globalState:{...r},localState:{...r._local??{}}}),restore:()=>{t.__templateApp===d&&(t.__templateApp=o.templateApp),t.G7Core&&(o.g7CoreState!==void 0&&(t.G7Core.state=o.g7CoreState),o.g7CoreDispatch!==void 0&&(t.G7Core.dispatch=o.g7CoreDispatch),o.g7CoreData!==void 0&&(t.G7Core.data=o.g7CoreData),o.g7CoreModal!==void 0&&(t.G7Core.modal=o.g7CoreModal),o.g7CoreToast!==void 0&&(t.G7Core.toast=o.g7CoreToast)),s.length=0}}}const w4=new Set(["id","userId","postId","orderId","productId","categoryId","commentId","noticeId","inquiryId","addressId","wishlistId","notificationId","roleId","menuId"]);function k4(e){if(!e)return{};const t={},n=/:([a-zA-Z_][a-zA-Z0-9_]*)/g;let o;for(;(o=n.exec(e))!==null;){const r=o[1];t[r]=S4(r)}return t}function S4(e){return w4.has(e)||/Id$|_id$/.test(e)?"1":"sample"}const C4=dr("LayoutEditor");function Xo(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function bi(e){if(Array.isArray(e))return e.map(t=>bi(t));if(Xo(e)){const t={};for(const[n,o]of Object.entries(e))t[n]=bi(o);return t}return e}function hg(e,t="",n=new Set){for(const[o,r]of Object.entries(e)){const s=t?`${t}.${o}`:o;Xo(r)?hg(r,s,n):n.add(s)}return n}function yg(e,t,n,o,r,s=""){for(const[a,l]of Object.entries(t)){const d=s?`${s}.${a}`:a;if(n.has(d)){r(`[LayoutEditor] sampleGlobal key conflict: extension '${o}' override of core key '${d}' ignored`);continue}const c=e[a];Xo(l)&&Xo(c)?yg(c,l,n,o,r,d):e[a]=bi(l)}}function Va(e){const t=e.warn??(r=>C4.warn(r)),n=bi(e.coreSeed??{});e.isGuestOnly&&"currentUser"in n&&delete n.currentUser;const o=hg(n);for(const r of e.sources)Xo(r.sampleGlobal)&&yg(n,r.sampleGlobal,o,r.id,t);return e.isGuestOnly&&"currentUser"in n&&delete n.currentUser,n}function $4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")&&(t[n]=o);return t}function E4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")||(t[n]=o);return t}function I4(e){if(typeof e!="string")return null;const t=/^\s*\{\{\s*([\s\S]*?)\s*\}\}\s*$/.exec(e);if(!t)return null;let n=t[1];const o=n.indexOf("??");return o>=0&&(n=n.slice(0,o)),n=n.replace(/\?\./g,".").replace(/\s+/g,""),n===""||!/^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(n)?null:n.split(".")}function A4(e,t){const n=I4(t);if(!n||n.length===0)return e;let o=e;for(let a=0;a<n.length;a++){if(o==null||typeof o!="obj Line truncated
//# sourceMappingURL=layout-editor.min.js.map
+17 -16
View File
@@ -1,20 +1,20 @@
var Px=Object.defineProperty;var Gx=(Vt,Ka,Wa)=>Ka in Vt?Px(Vt,Ka,{enumerable:!0,configurable:!0,writable:!0,value:Wa}):Vt[Ka]=Wa;var $=(Vt,Ka,Wa)=>Gx(Vt,typeof Ka!="symbol"?Ka+"":Ka,Wa);(function(Vt){"use strict";function Ka(o,e){for(var a=0;a<e.length;a++){const r=e[a];if(typeof r!="string"&&!Array.isArray(r)){for(const i in r)if(i!=="default"&&!(i in o)){const l=Object.getOwnPropertyDescriptor(r,i);l&&Object.defineProperty(o,i,l.get?l:{enumerable:!0,get:()=>r[i]})}}}return Object.freeze(Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}))}function Wa(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var Bu={exports:{}},Ke={};var kg;function AS(){if(kg)return Ke;kg=1;var o=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),u=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),v=Symbol.iterator;function S(x){return x===null||typeof x!="object"?null:(x=v&&x[v]||x["@@iterator"],typeof x=="function"?x:null)}var E={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,T={};function L(x,K,re){this.props=x,this.context=K,this.refs=T,this.updater=re||E}L.prototype.isReactComponent={},L.prototype.setState=function(x,K){if(typeof x!="object"&&typeof x!="function"&&x!=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,x,K,"setState")},L.prototype.forceUpdate=function(x){this.updater.enqueueForceUpdate(this,x,"forceUpdate")};function M(){}M.prototype=L.prototype;function R(x,K,re){this.props=x,this.context=K,this.refs=T,this.updater=re||E}var D=R.prototype=new M;D.constructor=R,A(D,L.prototype),D.isPureReactComponent=!0;var U=Array.isArray;function j(){}var q={H:null,A:null,T:null,S:null},X=Object.prototype.hasOwnProperty;function ge(x,K,re){var de=re.ref;return{$$typeof:o,type:x,key:K,ref:de!==void 0?de:null,props:re}}function Ee(x,K){return ge(x.type,K,x.props)}function Ce(x){return typeof x=="object"&&x!==null&&x.$$typeof===o}function $e(x){var K={"=":"=0",":":"=2"};return"$"+x.replace(/[=:]/g,function(re){return K[re]})}var qe=/\/+/g;function Xe(x,K){return typeof x=="object"&&x!==null&&x.key!=null?$e(""+x.key):K.toString(36)}function ht(x){switch(x.status){case"fulfilled":return x.value;case"rejected":throw x.reason;default:switch(typeof x.status=="string"?x.then(j,j):(x.status="pending",x.then(function(K){x.status==="pending"&&(x.status="fulfilled",x.value=K)},function(K){x.status==="pending"&&(x.status="rejected",x.reason=K)})),x.status){case"fulfilled":return x.value;case"rejected":throw x.reason}}throw x}function Q(x,K,re,de,se){var me=typeof x;(me==="undefined"||me==="boolean")&&(x=null);var Ne=!1;if(x===null)Ne=!0;else switch(me){case"bigint":case"string":case"number":Ne=!0;break;case"object":switch(x.$$typeof){case o:case e:Ne=!0;break;case m:return Ne=x._init,Q(Ne(x._payload),K,re,de,se)}}if(Ne)return se=se(x),Ne=de===""?"."+Xe(x,0):de,U(se)?(re="",Ne!=null&&(re=Ne.replace(qe,"$&/")+"/"),Q(se,K,re,"",function(Ae){return Ae})):se!=null&&(Ce(se)&&(se=Ee(se,re+(se.key==null||x&&x.key===se.key?"":(""+se.key).replace(qe,"$&/")+"/")+Ne)),K.push(se)),1;Ne=0;var ve=de===""?".":de+":";if(U(x))for(var He=0;He<x.length;He++)de=x[He],me=ve+Xe(de,He),Ne+=Q(de,K,re,me,se);else if(He=S(x),typeof He=="function")for(x=He.call(x),He=0;!(de=x.next()).done;)de=de.value,me=ve+Xe(de,He++),Ne+=Q(de,K,re,me,se);else if(me==="object"){if(typeof x.then=="function")return Q(ht(x),K,re,de,se);throw K=String(x),Error("Objects are not valid as a React child (found: "+(K==="[object Object]"?"object with keys {"+Object.keys(x).join(", ")+"}":K)+"). If you meant to render a collection of children, use an array instead.")}return Ne}function fe(x,K,re){if(x==null)return x;var de=[],se=0;return Q(x,de,"","",function(me){return K.call(re,me,se++)}),de}function ye(x){if(x._status===-1){var K=x._result;K=K(),K.then(function(re){(x._status===0||x._status===-1)&&(x._status=1,x._result=re)},function(re){(x._status===0||x._status===-1)&&(x._status=2,x._result=re)}),x._status===-1&&(x._status=0,x._result=K)}if(x._status===1)return x._result.default;throw x._result}var te=typeof reportError=="function"?reportError:function(x){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var K=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof x=="object"&&x!==null&&typeof x.message=="string"?String(x.message):String(x),error:x});if(!window.dispatchEvent(K))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",x);return}console.error(x)},he={map:fe,forEach:function(x,KLine truncated
var Qx=Object.defineProperty;var Zx=(Vt,Wa,Ya)=>Wa in Vt?Qx(Vt,Wa,{enumerable:!0,configurable:!0,writable:!0,value:Ya}):Vt[Wa]=Ya;var $=(Vt,Wa,Ya)=>Zx(Vt,typeof Wa!="symbol"?Wa+"":Wa,Ya);(function(Vt){"use strict";function Wa(o,e){for(var a=0;a<e.length;a++){const r=e[a];if(typeof r!="string"&&!Array.isArray(r)){for(const i in r)if(i!=="default"&&!(i in o)){const l=Object.getOwnPropertyDescriptor(r,i);l&&Object.defineProperty(o,i,l.get?l:{enumerable:!0,get:()=>r[i]})}}}return Object.freeze(Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}))}function Ya(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var Gu={exports:{}},Ke={};var Lg;function OS(){if(Lg)return Ke;Lg=1;var o=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),u=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),v=Symbol.iterator;function S(x){return x===null||typeof x!="object"?null:(x=v&&x[v]||x["@@iterator"],typeof x=="function"?x:null)}var E={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,T={};function L(x,K,re){this.props=x,this.context=K,this.refs=T,this.updater=re||E}L.prototype.isReactComponent={},L.prototype.setState=function(x,K){if(typeof x!="object"&&typeof x!="function"&&x!=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,x,K,"setState")},L.prototype.forceUpdate=function(x){this.updater.enqueueForceUpdate(this,x,"forceUpdate")};function M(){}M.prototype=L.prototype;function R(x,K,re){this.props=x,this.context=K,this.refs=T,this.updater=re||E}var D=R.prototype=new M;D.constructor=R,A(D,L.prototype),D.isPureReactComponent=!0;var U=Array.isArray;function j(){}var P={H:null,A:null,T:null,S:null},X=Object.prototype.hasOwnProperty;function ge(x,K,re){var de=re.ref;return{$$typeof:o,type:x,key:K,ref:de!==void 0?de:null,props:re}}function Ee(x,K){return ge(x.type,K,x.props)}function Ce(x){return typeof x=="object"&&x!==null&&x.$$typeof===o}function $e(x){var K={"=":"=0",":":"=2"};return"$"+x.replace(/[=:]/g,function(re){return K[re]})}var Pe=/\/+/g;function Xe(x,K){return typeof x=="object"&&x!==null&&x.key!=null?$e(""+x.key):K.toString(36)}function ht(x){switch(x.status){case"fulfilled":return x.value;case"rejected":throw x.reason;default:switch(typeof x.status=="string"?x.then(j,j):(x.status="pending",x.then(function(K){x.status==="pending"&&(x.status="fulfilled",x.value=K)},function(K){x.status==="pending"&&(x.status="rejected",x.reason=K)})),x.status){case"fulfilled":return x.value;case"rejected":throw x.reason}}throw x}function Q(x,K,re,de,se){var me=typeof x;(me==="undefined"||me==="boolean")&&(x=null);var Ne=!1;if(x===null)Ne=!0;else switch(me){case"bigint":case"string":case"number":Ne=!0;break;case"object":switch(x.$$typeof){case o:case e:Ne=!0;break;case m:return Ne=x._init,Q(Ne(x._payload),K,re,de,se)}}if(Ne)return se=se(x),Ne=de===""?"."+Xe(x,0):de,U(se)?(re="",Ne!=null&&(re=Ne.replace(Pe,"$&/")+"/"),Q(se,K,re,"",function(Ae){return Ae})):se!=null&&(Ce(se)&&(se=Ee(se,re+(se.key==null||x&&x.key===se.key?"":(""+se.key).replace(Pe,"$&/")+"/")+Ne)),K.push(se)),1;Ne=0;var ve=de===""?".":de+":";if(U(x))for(var He=0;He<x.length;He++)de=x[He],me=ve+Xe(de,He),Ne+=Q(de,K,re,me,se);else if(He=S(x),typeof He=="function")for(x=He.call(x),He=0;!(de=x.next()).done;)de=de.value,me=ve+Xe(de,He++),Ne+=Q(de,K,re,me,se);else if(me==="object"){if(typeof x.then=="function")return Q(ht(x),K,re,de,se);throw K=String(x),Error("Objects are not valid as a React child (found: "+(K==="[object Object]"?"object with keys {"+Object.keys(x).join(", ")+"}":K)+"). If you meant to render a collection of children, use an array instead.")}return Ne}function fe(x,K,re){if(x==null)return x;var de=[],se=0;return Q(x,de,"","",function(me){return K.call(re,me,se++)}),de}function ye(x){if(x._status===-1){var K=x._result;K=K(),K.then(function(re){(x._status===0||x._status===-1)&&(x._status=1,x._result=re)},function(re){(x._status===0||x._status===-1)&&(x._status=2,x._result=re)}),x._status===-1&&(x._status=0,x._result=K)}if(x._status===1)return x._result.default;throw x._result}var te=typeof reportError=="function"?reportError:function(x){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var K=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof x=="object"&&x!==null&&typeof x.message=="string"?String(x.message):String(x),error:x});if(!window.dispatchEvent(K))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",x);return}console.error(x)},he={map:fe,forEach:function(x,KLine truncated
at`)?" (<anonymous>)":-1<s.stack.indexOf("@")?"@unknown:0:0":""}return`
`+Rt+t+_t}var un=!1;function gt(t,n){if(!t||un)return"";un=!0;var s=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var c={DetermineComponentFrameRoot:function(){try{if(n){var ue=function(){throw Error()};if(Object.defineProperty(ue.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(ue,[])}catch(ee){var J=ee}Reflect.construct(t,[],ue)}else{try{ue.call()}catch(ee){J=ee}t.call(ue.prototype)}}else{try{throw Error()}catch(ee){J=ee}(ue=t())&&typeof ue.catch=="function"&&ue.catch(function(){})}}catch(ee){if(ee&&J&&typeof ee.stack=="string")return[ee.stack,J.stack]}return[null,null]}};c.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var h=Object.getOwnPropertyDescriptor(c.DetermineComponentFrameRoot,"name");h&&h.configurable&&Object.defineProperty(c.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var p=c.DetermineComponentFrameRoot(),w=p[0],k=p[1];if(w&&k){var z=w.split(`
`),Y=k.split(`
`);for(h=c=0;c<z.length&&!z[c].includes("DetermineComponentFrameRoot");)c++;for(;h<Y.length&&!Y[h].includes("DetermineComponentFrameRoot");)h++;if(c===z.length||h===Y.length)for(c=z.length-1,h=Y.length-1;1<=c&&0<=h&&z[c]!==Y[h];)h--;for(;1<=c&&0<=h;c--,h--)if(z[c]!==Y[h]){if(c!==1||h!==1)do if(c--,h--,0>h||z[c]!==Y[h]){var ae=`
`+z[c].replace(" at new "," at ");return t.displayName&&ae.includes("<anonymous>")&&(ae=ae.replace("<anonymous>",t.displayName)),ae}while(1<=c&&0<=h);break}}}finally{un=!1,Error.prepareStackTrace=s}return(s=t?t.displayName||t.name:"")?ft(s):""}function za(t,n){switch(t.tag){case 26:case 27:case 5:return ft(t.type);case 16:return ft("Lazy");case 13:return t.child!==n&&n!==null?ft("Suspense Fallback"):ft("Suspense");case 19:return ft("SuspenseList");case 0:case 15:return gt(t.type,!1);case 11:return gt(t.type.render,!1);case 1:return gt(t.type,!0);case 31:return ft("Activity");default:return""}}function kn(t){try{var n="",s=null;do n+=za(t,s),s=t,t=t.return;while(t);return n}catch(c){return`
`+z[c].replace(" at new "," at ");return t.displayName&&ae.includes("<anonymous>")&&(ae=ae.replace("<anonymous>",t.displayName)),ae}while(1<=c&&0<=h);break}}}finally{un=!1,Error.prepareStackTrace=s}return(s=t?t.displayName||t.name:"")?ft(s):""}function ja(t,n){switch(t.tag){case 26:case 27:case 5:return ft(t.type);case 16:return ft("Lazy");case 13:return t.child!==n&&n!==null?ft("Suspense Fallback"):ft("Suspense");case 19:return ft("SuspenseList");case 0:case 15:return gt(t.type,!1);case 11:return gt(t.type.render,!1);case 1:return gt(t.type,!0);case 31:return ft("Activity");default:return""}}function kn(t){try{var n="",s=null;do n+=ja(t,s),s=t,t=t.return;while(t);return n}catch(c){return`
Error generating stack: `+c.message+`
`+c.stack}}var Dn=Object.prototype.hasOwnProperty,jt=o.unstable_scheduleCallback,On=o.unstable_cancelCallback,ea=o.unstable_shouldYield,ja=o.unstable_requestPaint,Ft=o.unstable_now,Rr=o.unstable_getCurrentPriorityLevel,Za=o.unstable_ImmediatePriority,bn=o.unstable_UserBlockingPriority,en=o.unstable_NormalPriority,et=o.unstable_LowPriority,ta=o.unstable_IdlePriority,En=o.log,er=o.unstable_setDisableYieldValue,Ea=null,tn=null;function _n(t){if(typeof En=="function"&&er(t),tn&&typeof tn.setStrictMode=="function")try{tn.setStrictMode(Ea,t)}catch{}}var P=Math.clz32?Math.clz32:Z,ce=Math.log,le=Math.LN2;function Z(t){return t>>>=0,t===0?32:31-(ce(t)/le|0)|0}var be=256,Se=262144,Re=4194304;function xe(t){var n=t&42;if(n!==0)return n;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function je(t,n,s){var c=t.pendingLanes;if(c===0)return 0;var h=0,p=t.suspendedLanes,w=t.pingedLanes;t=t.warmLanes;var k=c&134217727;return k!==0?(c=k&~p,c!==0?h=xe(c):(w&=k,w!==0?h=xe(w):s||(s=k&~t,s!==0&&(h=xe(s))))):(k=c&~p,k!==0?h=xe(k):w!==0?h=xe(w):s||(s=c&~t,s!==0&&(h=xe(s)))),h===0?0:n!==0&&n!==h&&(n&p)===0&&(p=h&-h,s=n&-n,p>=s||p===32&&(s&4194048)!==0)?n:h}function ze(t,n){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&n)===0}function Le(t,n){switch(t){case 1:case 2:case 4:case 8:case 64:return n+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return n+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function ke(){var t=Re;return Re<<=1,(Re&62914560)===0&&(Re=4194304),t}function We(t){for(var n=[],s=0;31>s;s++)n.push(t);return n}function Fe(t,n){t.pendingLanes|=n,n!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function _e(t,n,s,c,h,p){var w=t.pendingLanes;t.pendingLanes=s,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=s,t.entangledLanes&=s,t.errorRecoveryDisabledLanes&=s,t.shellSuspendCounter=0;var k=t.entanglements,z=t.expirationTimes,Y=t.hiddenUpdates;for(s=w&~s;0<s;){var ae=31-P(s),ue=1<<ae;k[ae]=0,z[ae]=-1;var J=Y[ae];if(J!==null)for(Y[ae]=null,ae=0;ae<J.length;ae++){var ee=J[ae];ee!==null&&(ee.lane&=-536870913)}s&=~ue}c!==0&&Te(t,c,0),p!==0&&h===0&&t.tag!==0&&(t.suspendedLanes|=p&~(w&~n))}function Te(t,n,s){t.pendingLanes|=n,t.suspendedLanes&=~n;var c=31-P(n);t.entangledLanes|=n,t.entanglements[c]=t.entanglements[c]|1073741824|s&261930}function Ue(t,n){var s=t.entangledLanes|=n;for(t=t.entanglements;s;){var c=31-P(s),h=1<<c;h&n|t[c]&n&&(t[c]|=n),s&=~h}}function st(t,n){var s=n&-n;return s=(s&42)!==0?1:Dt(s),(s&(t.suspendedLanes|n))!==0?0:s}function Dt(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function nn(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function Kt(){var t=fe.p;return t!==0?t:(t=window.event,t===void 0?32:pS(t.type))}function Pn(t,n){var s=fe.p;try{return fe.p=t,n()}finally{fe.p=s}}var Ln=Math.random().toString(36).slice(2),an="__reactFiber$"+Ln,vn="__reactProps$"+Ln,kr="__reactContainer$"+Ln,_o="__reactEvents$"+Ln,df="__reactListeners$"+Ln,pc="__reactHandles$"+Ln,Ua="__reactResources$"+Ln,pi="__reactMarker$"+Ln;function Ao(t){delete t[an],delete t[vn],delete t[_o],delete t[df],delete t[pc]}function Dr(t){var n=t[an];if(n)return n;for(var s=t.parentNode;s;){if(n=s[kr]||s[an]){if(s=n.alternate,n.child!==null||s!==null&&s.child!==null)for(t=Zv(t);t!==null;){if(s=t[an])return s;t=Zv(t)}return n}t=s,s=t.parentNode}return null}function Ba(t){if(t=t[an]||t[kr]){var n=t.tag;if(n===5||n===6||n===13||n===31||n===26||n===27||n===3)return t}return null}function tr(t){var n=t.tag;if(n===5||n===26||n===27||n===6)return t.stateNode;throw Error(r(33))}function Or(t){var n=t[Ua];return n||(n=t[Ua]={hoistableStyles:new Map,hoistableScripts:new Map}),n}function Wt(t){t[pi]=!0}var mc=new Set,yc={};function _a(t,n){Lr(t,n),Lr(t+"Capture",n)}function Lr(t,n){for(yc[t]=n,t=0;t<n.length;t++)mc.add(n[t])}var ff=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D Line truncated
`).replace(dx,"")}function Pv(t,n){return n=qv(n),qv(t)===n}function xt(t,n,s,c,h,p){switch(s){case"children":typeof c=="string"?n==="body"||n==="textarea"&&c===""||Ir(t,c):(typeof c=="number"||typeof c=="bigint")&&n!=="body"&&Ir(t,""+c);break;case"className":mi(t,"class",c);break;case"tabIndex":mi(t,"tabindex",c);break;case"dir":case"role":case"viewBox":case"width":case"height":mi(t,s,c);break;case"style":_c(t,c,p);break;case"data":if(n!=="object"){mi(t,"data",c);break}case"src":case"href":if(c===""&&(n!=="a"||s!=="href")){t.removeAttribute(s);break}if(c==null||typeof c=="function"||typeof c=="symbol"||typeof c=="boolean"){t.removeAttribute(s);break}c=na(""+c),t.setAttribute(s,c);break;case"action":case"formAction":if(typeof c=="function"){t.setAttribute(s,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof p=="function"&&(s==="formAction"?(n!=="input"&&xt(t,n,"name",h.name,h,null),xt(t,n,"formEncType",h.formEncType,h,null),xt(t,n,"formMethod",h.formMethod,h,null),xt(t,n,"formTarget",h.formTarget,h,null)):(xt(t,n,"encType",h.encType,h,null),xt(t,n,"method",h.method,h,null),xt(t,n,"target",h.target,h,null)));if(c==null||typeof c=="symbol"||typeof c=="boolean"){t.removeAttribute(s);break}c=na(""+c),t.setAttribute(s,c);break;case"onClick":c!=null&&(t.onclick=Aa);break;case"onScroll":c!=null&&nt("scroll",t);break;case"onScrollEnd":c!=null&&nt("scrollend",t);break;case"dangerouslySetInnerHTML":if(c!=null){if(typeof c!="object"||!("__html"in c))throw Error(r(61));if(s=c.__html,s!=null){if(h.children!=null)throw Error(r(60));t.innerHTML=s}}break;case"multiple":t.multiple=c&&typeof c!="function"&&typeof c!="symbol";break;case"muted":t.muted=c&&typeof c!="function"&&typeof c!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(c==null||typeof c=="function"||typeof c=="boolean"||typeof c=="symbol"){t.removeAttribute("xlink:href");break}s=na(""+c),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",s);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":c!=null&&typeof c!="function"&&typeof c!="symbol"?t.setAttribute(s,""+c):t.removeAttribute(s);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":c&&typeof c!="function"&&typeof c!="symbol"?t.setAttribute(s,""):t.removeAttribute(s);break;case"capture":case"download":c===!0?t.setAttribute(s,""):c!==!1&&c!=null&&typeof c!="function"&&typeof c!="symbol"?t.setAttribute(s,c):t.removeAttribute(s);break;case"cols":case"rows":case"size":case"span":c!=null&&typeof c!="function"&&typeof c!="symbol"&&!isNaN(c)&&1<=c?t.setAttribute(s,c):t.removeAttribute(s);break;case"rowSpan":case"start":c==null||typeof c=="function"||typeof c=="symbol"||isNaN(c)?t.removeAttribute(s):t.setAttribute(s,c);break;case"popover":nt("beforetoggle",t),nt("toggle",t),Mn(t,"popover",c);break;case"xlinkActuate":dn(t,"http://www.w3.org/1999/xlink","xlink:actuate",c);break;case"xlinkArcrole":dn(t,"http://www.w3.org/1999/xlink","xlink:arcrole",c);break;case"xlinkRole":dn(t,"http://www.w3.org/1999/xlink","xlink:role",c);break;case"xlinkShow":dn(t,"http://www.w3.org/1999/xlink","xlink:show",c);break;case"xlinkTitle":dn(t,"http://www.w3.org/1999/xlink","xlink:title",c);break;case"xlinkType":dn(t,"http://www.w3.org/1999/xlink","xlink:type",c);break;case"xmlBase":dn(t,"http://www.w3.org/XML/1998/namespace","xml:base",c);break;case"xmlLang":dn(t,"http://www.w3.org/XML/1998/namespace","xml:lang",c);break;case"xmlSpace":dn(t,"http://www.w3.org/XML/1998/namespace","xml:space",c);break;case"is":Mn(t,"is",c);break;case"innerText":case"textContent":break;default:(!(2<s.length)||s[0]!=="o"&&s[0]!=="O"||s[1]!=="n"&&s[1]!=="N")&&(s=yf.get(s)||s,Mn(t,s,c))}}function dg(t,n,s,c,h,p){switch(s){case"style":_c(t,c,p);break;case"dangerouslySetInnerHTML":if(c!=null){if(typeof c!="object"||!("__html"in c))throw Error(r(61));if(s=c.__html,s!=null){if(h.children!=null)throw Error(r(60));t.innerHTML=s}}break;case"children":typeof c=="string"?Ir(t,c):(typeof c=="number"||typeof c=="bigint")&&Ir(t,""+c);break;case"onScroll":c!=null&&nt("scroll",t);break;case"onScrollEnd":c!=null&&nt("scrollend",t);break;case"onClick":c!=null&&(t.onclick=Aa);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne Line truncated
`).forEach(function(u){i=u.indexOf(":"),a=u.substring(0,i).trim().toLowerCase(),r=u.substring(i+1).trim(),!(!a||e[a]&&Uw[a])&&(a==="set-cookie"?e[a]?e[a].push(r):e[a]=[r]:e[a]=e[a]?e[a]+", "+r:r)}),e};function qw(o){let e=0,a=o.length;for(;e<a;){const r=o.charCodeAt(e);if(r!==9&&r!==32)break;e+=1}for(;a>e;){const r=o.charCodeAt(a-1);if(r!==9&&r!==32)break;a-=1}return e===0&&a===o.length?o:o.slice(e,a)}const Pw=new RegExp("[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+","g"),Gw=new RegExp("[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+","g");function od(o,e){return F.isArray(o)?o.map(a=>od(a,e)):qw(String(o).replace(e,""))}const Vw=o=>od(o,Pw),Fw=o=>od(o,Gw);function yp(o){const e=Object.create(null);return F.forEach(o.toJSON(),(a,r)=>{e[r]=Fw(a)}),e}const bp=Symbol("internals");function Zs(o){return o&&String(o).trim().toLowerCase()}function Nl(o){return o===!1||o==null?o:F.isArray(o)?o.map(Nl):Vw(String(o))}function Kw(o){const e=Object.create(null),a=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=a.exec(o);)e[r[1]]=r[2];return e}const Ww=o=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(o.trim());function ld(o,e,a,r,i){if(F.isFunction(r))return r.call(this,e,a);if(i&&(e=a),!!F.isString(e)){if(F.isString(r))return e.indexOf(r)!==-1;if(F.isRegExp(r))return r.test(e)}}function Yw(o){return o.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,a,r)=>a.toUpperCase()+r)}function Xw(o,e){const a=F.toCamelCase(" "+e);["get","set","has"].forEach(r=>{Object.defineProperty(o,r+a,{__proto__:null,value:function(i,l,u){return this[r].call(this,e,i,l,u)},configurable:!0})})}let Cn=class{constructor(e){e&&this.set(e)}set(e,a,r){const i=this;function l(d,f,g){const m=Zs(f);if(!m)throw new Error("header name must be a non-empty string");const b=F.findKey(i,m);(!b||i[b]===void 0||g===!0||g===void 0&&i[b]!==!1)&&(i[b||f]=Nl(d))}const u=(d,f)=>F.forEach(d,(g,m)=>l(g,m,f));if(F.isPlainObject(e)||e instanceof this.constructor)u(e,a);else if(F.isString(e)&&(e=e.trim())&&!Ww(e))u(Bw(e),a);else if(F.isObject(e)&&F.isIterable(e)){let d={},f,g;for(const m of e){if(!F.isArray(m))throw TypeError("Object iterator must return a key-value pair");d[g=m[0]]=(f=d[g])?F.isArray(f)?[...f,m[1]]:[f,m[1]]:m[1]}u(d,a)}else e!=null&&l(a,e,r);return this}get(e,a){if(e=Zs(e),e){const r=F.findKey(this,e);if(r){const i=this[r];if(!a)return i;if(a===!0)return Kw(i);if(F.isFunction(a))return a.call(this,i,r);if(F.isRegExp(a))return a.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,a){if(e=Zs(e),e){const r=F.findKey(this,e);return!!(r&&this[r]!==void 0&&(!a||ld(this,this[r],r,a)))}return!1}delete(e,a){const r=this;let i=!1;function l(u){if(u=Zs(u),u){const d=F.findKey(r,u);d&&(!a||ld(r,r[d],d,a))&&(delete r[d],i=!0)}}return F.isArray(e)?e.forEach(l):l(e),i}clear(e){const a=Object.keys(this);let r=a.length,i=!1;for(;r--;){const l=a[r];(!e||ld(this,this[l],l,e,!0))&&(delete this[l],i=!0)}return i}normalize(e){const a=this,r={};return F.forEach(this,(i,l)=>{const u=F.findKey(r,l);if(u){a[u]=Nl(i),delete a[l];return}const d=e?Yw(l):String(l).trim();d!==l&&delete a[l],a[d]=Nl(i),r[d]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const a=Object.create(null);return F.forEach(this,(r,i)=>{r!=null&&r!==!1&&(a[i]=e&&F.isArray(r)?r.join(", "):r)}),a}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,a])=>e+": "+a).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,...a){const r=new this(e);return a.forEach(i=>r.set(i)),r}static accessor(e){const r=(this[bp]=this[bp]={accessors:{}}).accessors,i=this.prototype;function l(u){const d=Zs(u);r[d]||(Xw(i,u),r[d]=!0)}return F.isArray(e)?e.forEach(l):l(e),this}};Cn.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),F.reduceDescriptors(Cn.prototype,({value:o},e)=>{let a=e[0].toUpperCase()+e.slice(1);return{get:()=>o,set(r){this[a]=r}}}),F.freezeMethods(Cn);const Jw="[REDACTED ****]";function Qw(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 Zw(o,e){const a=new Set(e.map(l=>String(l).toLowerCase())),r=[],i=l=>{if(l===null||typeof l!="object"||F.isBuffer(l))return l;if(r.indexOf(l)!==-1)return;l instanceof Cn&&(l=l.toJSON()),r.push(l);let u;if(F.isArray(l))u=[],l.forEach((d,f)=>{const g=i(d);F.isUndefined(g)||(u[f]=g)});else{if(!F.isPlainObject(l)&&Qw(l))return r.pop(),l;u=Object.create(null);for(const[d,f]of Object.entries(l)){const g=a.has(d.toLowerCase())?Jw:i(f);F.isUndefined(g)||(u[d]=g)}}return r.pop(),u};return i(o)}let Oe=class CS extends Error{static from(e,a,r,i,l,u){const d=new CS(e.message,a||e.code,r,i,l);return d.cause=e,d.name=e.name,e.status!=null&&d.status==null&&(d.status=e.status),u&&Object.assign(d,u),d}constructor(e,a,r,i,l){super(e),Object.defineProperty(this,"message",{__proto__:null,value:e,enumerable:!0,writable:!0,configurable:!0}),this.name="AxiosError",this.isAxiosError=!0,a&&(this.code=a),r&&(this.config=r),i&&(this.request=i),l&&(this.response=l,this.status=l.status)}toJSON(){const e=this.config,a=e&&F.hasOwnProp(e,"redact")?e.redact:void 0,r=F.isArray(a)&&a.length>0?Zw(e,a):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:r,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 e0=null;function cd(o){return F.isPlainObject(o)||F.isArray(o)}function vp(o){return F.endsWith(o,"[]")?o.slice(0,-2):o}function ud(o,e,a){return o?o.concat(e).map(function(i,l){return i=vp(i),!a&&l?"["+i+"]":i}).join(a?".":""):e}function t0(o){return F.isArray(o)&&!o.some(cd)}const n0=F.toFlatObject(F,{},null,function(e){return/^is[A-Z]/.test(e)});function Il(o,e,a){if(!F.isObject(o))throw new TypeError("target must be an object");e=e||new FormData,a=F.toFlatObject(a,{metaTokens:!0,dots:!1,indexes:!1},!1,function(T,L){return!F.isUndefined(L[T])});const r=a.metaTokens,i=a.visitor||b,l=a.dots,u=a.indexes,d=a.Blob||typeof Blob<"u"&&Blob,f=a.maxDepth===void 0?100:a.maxDepth,g=d&&F.isSpecCompliantForm(e);if(!F.isFunction(i))throw new TypeError("visitor must be a function");function m(A){if(A===null)return"";if(F.isDate(A))return A.toISOString();if(F.isBoolean(A))return A.toString();if(!g&&F.isBlob(A))throw new Oe("Blob is not supported. Use a Buffer instead.");return F.isArrayBuffer(A)||F.isTypedArray(A)?g&&typeof Blob=="function"?new Blob([A]):Buffer.from(A):A}function b(A,T,L){let M=A;if(F.isReactNative(e)&&F.isReactNativeBlob(A))return e.append(ud(L,T,l),m(A)),!1;if(A&&!L&&typeof A=="object"){if(F.endsWith(T,"{}"))T=r?T:T.slice(0,-2),A=JSON.stringify(A);else if(F.isArray(A)&&t0(A)||(F.isFileList(A)||F.endsWith(T,"[]"))&&(M=F.toArray(A)))return T=vp(T),M.forEach(function(D,U){!(F.isUndefined(D)||D===null)&&e.append(u===!0?ud([T],U,l):u===null?T:T+"[]",m(D))}),!1}return cd(A)?!0:(e.append(ud(L,T,l),m(A)),!1)}const v=[],S=Object.assign(n0,{defaultVisitor:b,convertValue:m,isVisitable:cd});function E(A,T,L=0){if(!F.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(v.indexOf(A)!==-1)throw Error("Circular reference detected in "+T.join("."));v.push(A),F.forEach(A,function(R,D){(!(F.isUndefined(R)||R===null)&&i.call(e,R,F.isString(D)?D.trim():D,T,S))===!0&&E(R,T?T.concat(D):[D],L+1)}),v.pop()}}if(!F.isObject(o))throw new TypeError("data must be an object");return E(o),e}function Sp(o){const e={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"};return encodeURIComponent(o).replace(/[!'()~]|%20/g,function(r){return e[r]})} Line truncated
`+u.map(Ip).join(`
`):" "+Ip(u[0]):"as no adapter specified";throw new Oe("There is no suitable adapter to dispatch the request "+d,"ERR_NOT_SUPPORT")}return i}const Hp={getAdapter:O0,adapters:yd};function bd(o){if(o.cancelToken&&o.cancelToken.throwIfRequested(),o.signal&&o.signal.aborted)throw new to(null,o)}function zp(o){return bd(o),o.headers=Cn.from(o.headers),o.data=pd.call(o,o.transformRequest),["post","put","patch"].indexOf(o.method)!==-1&&o.headers.setContentType("application/x-www-form-urlencoded",!1),Hp.getAdapter(o.adapter||eo.adapter,o)(o).then(function(r){bd(o),o.response=r;try{r.data=pd.call(o,o.transformResponse,r)}finally{delete o.response}return r.headers=Cn.from(r.headers),r},function(r){if(!Ap(r)&&(bd(o),r&&r.response)){o.response=r.response;try{r.response.data=pd.call(o,o.transformResponse,r.response)}finally{delete o.response}r.response.headers=Cn.from(r.response.headers)}return Promise.reject(r)})}const jl={};["object","boolean","number","function","string","symbol"].forEach((o,e)=>{jl[o]=function(r){return typeof r===o||"a"+(e<1?"n ":" ")+o}});const jp={};jl.transitional=function(e,a,r){function i(l,u){return"[Axios v"+md+"] Transitional option '"+l+"'"+u+(r?". "+r:"")}return(l,u,d)=>{if(e===!1)throw new Oe(i(u," has been removed"+(a?" in "+a:"")),Oe.ERR_DEPRECATED);return a&&!jp[u]&&(jp[u]=!0,console.warn(i(u," has been deprecated since v"+a+" and will be removed in the near future"))),e?e(l,u,d):!0}},jl.spelling=function(e){return(a,r)=>(console.warn(`${r} is likely a misspelling of ${e}`),!0)};function L0(o,e,a){if(typeof o!="object")throw new Oe("options must be an object",Oe.ERR_BAD_OPTION_VALUE);const r=Object.keys(o);let i=r.length;for(;i-- >0;){const l=r[i],u=Object.prototype.hasOwnProperty.call(e,l)?e[l]:void 0;if(u){const d=o[l],f=d===void 0||u(d,l,o);if(f!==!0)throw new Oe("option "+l+" must be "+f,Oe.ERR_BAD_OPTION_VALUE);continue}if(a!==!0)throw new Oe("Unknown option "+l,Oe.ERR_BAD_OPTION)}}const Ul={assertOptions:L0,validators:jl},Jn=Ul.validators;let ui=class{constructor(e){this.defaults=e||{},this.interceptors={request:new Ep,response:new Ep}}async request(e,a){try{return await this._request(e,a)}catch(r){if(r instanceof Error){let i={};Error.captureStackTrace?Error.captureStackTrace(i):i=new Error;const l=(()=>{if(!i.stack)return"";const u=i.stack.indexOf(`
`+c.stack}}var Dn=Object.prototype.hasOwnProperty,jt=o.unstable_scheduleCallback,On=o.unstable_cancelCallback,ea=o.unstable_shouldYield,Ua=o.unstable_requestPaint,Ft=o.unstable_now,Dr=o.unstable_getCurrentPriorityLevel,er=o.unstable_ImmediatePriority,bn=o.unstable_UserBlockingPriority,en=o.unstable_NormalPriority,et=o.unstable_LowPriority,ta=o.unstable_IdlePriority,En=o.log,tr=o.unstable_setDisableYieldValue,Ea=null,tn=null;function _n(t){if(typeof En=="function"&&tr(t),tn&&typeof tn.setStrictMode=="function")try{tn.setStrictMode(Ea,t)}catch{}}var q=Math.clz32?Math.clz32:Z,ce=Math.log,le=Math.LN2;function Z(t){return t>>>=0,t===0?32:31-(ce(t)/le|0)|0}var be=256,Se=262144,Re=4194304;function xe(t){var n=t&42;if(n!==0)return n;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function je(t,n,s){var c=t.pendingLanes;if(c===0)return 0;var h=0,p=t.suspendedLanes,w=t.pingedLanes;t=t.warmLanes;var k=c&134217727;return k!==0?(c=k&~p,c!==0?h=xe(c):(w&=k,w!==0?h=xe(w):s||(s=k&~t,s!==0&&(h=xe(s))))):(k=c&~p,k!==0?h=xe(k):w!==0?h=xe(w):s||(s=c&~t,s!==0&&(h=xe(s)))),h===0?0:n!==0&&n!==h&&(n&p)===0&&(p=h&-h,s=n&-n,p>=s||p===32&&(s&4194048)!==0)?n:h}function ze(t,n){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&n)===0}function Le(t,n){switch(t){case 1:case 2:case 4:case 8:case 64:return n+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return n+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function ke(){var t=Re;return Re<<=1,(Re&62914560)===0&&(Re=4194304),t}function We(t){for(var n=[],s=0;31>s;s++)n.push(t);return n}function Fe(t,n){t.pendingLanes|=n,n!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function _e(t,n,s,c,h,p){var w=t.pendingLanes;t.pendingLanes=s,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=s,t.entangledLanes&=s,t.errorRecoveryDisabledLanes&=s,t.shellSuspendCounter=0;var k=t.entanglements,z=t.expirationTimes,Y=t.hiddenUpdates;for(s=w&~s;0<s;){var ae=31-q(s),ue=1<<ae;k[ae]=0,z[ae]=-1;var J=Y[ae];if(J!==null)for(Y[ae]=null,ae=0;ae<J.length;ae++){var ee=J[ae];ee!==null&&(ee.lane&=-536870913)}s&=~ue}c!==0&&Te(t,c,0),p!==0&&h===0&&t.tag!==0&&(t.suspendedLanes|=p&~(w&~n))}function Te(t,n,s){t.pendingLanes|=n,t.suspendedLanes&=~n;var c=31-q(n);t.entangledLanes|=n,t.entanglements[c]=t.entanglements[c]|1073741824|s&261930}function Ue(t,n){var s=t.entangledLanes|=n;for(t=t.entanglements;s;){var c=31-q(s),h=1<<c;h&n|t[c]&n&&(t[c]|=n),s&=~h}}function st(t,n){var s=n&-n;return s=(s&42)!==0?1:Dt(s),(s&(t.suspendedLanes|n))!==0?0:s}function Dt(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function nn(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function Kt(){var t=fe.p;return t!==0?t:(t=window.event,t===void 0?32:wS(t.type))}function qn(t,n){var s=fe.p;try{return fe.p=t,n()}finally{fe.p=s}}var Ln=Math.random().toString(36).slice(2),an="__reactFiber$"+Ln,vn="__reactProps$"+Ln,Or="__reactContainer$"+Ln,To="__reactEvents$"+Ln,gf="__reactListeners$"+Ln,bc="__reactHandles$"+Ln,Ba="__reactResources$"+Ln,yi="__reactMarker$"+Ln;function Ro(t){delete t[an],delete t[vn],delete t[To],delete t[gf],delete t[bc]}function Lr(t){var n=t[an];if(n)return n;for(var s=t.parentNode;s;){if(n=s[Or]||s[an]){if(s=n.alternate,n.child!==null||s!==null&&s.child!==null)for(t=iS(t);t!==null;){if(s=t[an])return s;t=iS(t)}return n}t=s,s=t.parentNode}return null}function Pa(t){if(t=t[an]||t[Or]){var n=t.tag;if(n===5||n===6||n===13||n===31||n===26||n===27||n===3)return t}return null}function nr(t){var n=t.tag;if(n===5||n===26||n===27||n===6)return t.stateNode;throw Error(r(33))}function Mr(t){var n=t[Ba];return n||(n=t[Ba]={hoistableStyles:new Map,hoistableScripts:new Map}),n}function Wt(t){t[yi]=!0}var vc=new Set,Sc={};function _a(t,n){$r(t,n),$r(t+"Capture",n)}function $r(t,n){for(Sc[t]=n,t=0;t<n.length;t++)vc.add(n[t])}var pf=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D Line truncated
`).replace(Sx,"")}function Yv(t,n){return n=Wv(n),Wv(t)===n}function xt(t,n,s,c,h,p){switch(s){case"children":typeof c=="string"?n==="body"||n==="textarea"&&c===""||zr(t,c):(typeof c=="number"||typeof c=="bigint")&&n!=="body"&&zr(t,""+c);break;case"className":bi(t,"class",c);break;case"tabIndex":bi(t,"tabindex",c);break;case"dir":case"role":case"viewBox":case"width":case"height":bi(t,s,c);break;case"style":Tc(t,c,p);break;case"data":if(n!=="object"){bi(t,"data",c);break}case"src":case"href":if(c===""&&(n!=="a"||s!=="href")){t.removeAttribute(s);break}if(c==null||typeof c=="function"||typeof c=="symbol"||typeof c=="boolean"){t.removeAttribute(s);break}c=na(""+c),t.setAttribute(s,c);break;case"action":case"formAction":if(typeof c=="function"){t.setAttribute(s,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof p=="function"&&(s==="formAction"?(n!=="input"&&xt(t,n,"name",h.name,h,null),xt(t,n,"formEncType",h.formEncType,h,null),xt(t,n,"formMethod",h.formMethod,h,null),xt(t,n,"formTarget",h.formTarget,h,null)):(xt(t,n,"encType",h.encType,h,null),xt(t,n,"method",h.method,h,null),xt(t,n,"target",h.target,h,null)));if(c==null||typeof c=="symbol"||typeof c=="boolean"){t.removeAttribute(s);break}c=na(""+c),t.setAttribute(s,c);break;case"onClick":c!=null&&(t.onclick=Aa);break;case"onScroll":c!=null&&nt("scroll",t);break;case"onScrollEnd":c!=null&&nt("scrollend",t);break;case"dangerouslySetInnerHTML":if(c!=null){if(typeof c!="object"||!("__html"in c))throw Error(r(61));if(s=c.__html,s!=null){if(h.children!=null)throw Error(r(60));t.innerHTML=s}}break;case"multiple":t.multiple=c&&typeof c!="function"&&typeof c!="symbol";break;case"muted":t.muted=c&&typeof c!="function"&&typeof c!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(c==null||typeof c=="function"||typeof c=="boolean"||typeof c=="symbol"){t.removeAttribute("xlink:href");break}s=na(""+c),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",s);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":c!=null&&typeof c!="function"&&typeof c!="symbol"?t.setAttribute(s,""+c):t.removeAttribute(s);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":c&&typeof c!="function"&&typeof c!="symbol"?t.setAttribute(s,""):t.removeAttribute(s);break;case"capture":case"download":c===!0?t.setAttribute(s,""):c!==!1&&c!=null&&typeof c!="function"&&typeof c!="symbol"?t.setAttribute(s,c):t.removeAttribute(s);break;case"cols":case"rows":case"size":case"span":c!=null&&typeof c!="function"&&typeof c!="symbol"&&!isNaN(c)&&1<=c?t.setAttribute(s,c):t.removeAttribute(s);break;case"rowSpan":case"start":c==null||typeof c=="function"||typeof c=="symbol"||isNaN(c)?t.removeAttribute(s):t.setAttribute(s,c);break;case"popover":nt("beforetoggle",t),nt("toggle",t),Mn(t,"popover",c);break;case"xlinkActuate":dn(t,"http://www.w3.org/1999/xlink","xlink:actuate",c);break;case"xlinkArcrole":dn(t,"http://www.w3.org/1999/xlink","xlink:arcrole",c);break;case"xlinkRole":dn(t,"http://www.w3.org/1999/xlink","xlink:role",c);break;case"xlinkShow":dn(t,"http://www.w3.org/1999/xlink","xlink:show",c);break;case"xlinkTitle":dn(t,"http://www.w3.org/1999/xlink","xlink:title",c);break;case"xlinkType":dn(t,"http://www.w3.org/1999/xlink","xlink:type",c);break;case"xmlBase":dn(t,"http://www.w3.org/XML/1998/namespace","xml:base",c);break;case"xmlLang":dn(t,"http://www.w3.org/XML/1998/namespace","xml:lang",c);break;case"xmlSpace":dn(t,"http://www.w3.org/XML/1998/namespace","xml:space",c);break;case"is":Mn(t,"is",c);break;case"innerText":case"textContent":break;default:(!(2<s.length)||s[0]!=="o"&&s[0]!=="O"||s[1]!=="n"&&s[1]!=="N")&&(s=Sf.get(s)||s,Mn(t,s,c))}}function gg(t,n,s,c,h,p){switch(s){case"style":Tc(t,c,p);break;case"dangerouslySetInnerHTML":if(c!=null){if(typeof c!="object"||!("__html"in c))throw Error(r(61));if(s=c.__html,s!=null){if(h.children!=null)throw Error(r(60));t.innerHTML=s}}break;case"children":typeof c=="string"?zr(t,c):(typeof c=="number"||typeof c=="bigint")&&zr(t,""+c);break;case"onScroll":c!=null&&nt("scroll",t);break;case"onScrollEnd":c!=null&&nt("scrollend",t);break;case"onClick":c!=null&&(t.onclick=Aa);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne Line truncated
`).forEach(function(u){i=u.indexOf(":"),a=u.substring(0,i).trim().toLowerCase(),r=u.substring(i+1).trim(),!(!a||e[a]&&Yw[a])&&(a==="set-cookie"?e[a]?e[a].push(r):e[a]=[r]:e[a]=e[a]?e[a]+", "+r:r)}),e};function Jw(o){let e=0,a=o.length;for(;e<a;){const r=o.charCodeAt(e);if(r!==9&&r!==32)break;e+=1}for(;a>e;){const r=o.charCodeAt(a-1);if(r!==9&&r!==32)break;a-=1}return e===0&&a===o.length?o:o.slice(e,a)}const Qw=new RegExp("[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+","g"),Zw=new RegExp("[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+","g");function ud(o,e){return F.isArray(o)?o.map(a=>ud(a,e)):Jw(String(o).replace(e,""))}const e0=o=>ud(o,Qw),t0=o=>ud(o,Zw);function Ep(o){const e=Object.create(null);return F.forEach(o.toJSON(),(a,r)=>{e[r]=t0(a)}),e}const _p=Symbol("internals");function no(o){return o&&String(o).trim().toLowerCase()}function zl(o){return o===!1||o==null?o:F.isArray(o)?o.map(zl):e0(String(o))}function n0(o){const e=Object.create(null),a=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=a.exec(o);)e[r[1]]=r[2];return e}const a0=o=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(o.trim());function dd(o,e,a,r,i){if(F.isFunction(r))return r.call(this,e,a);if(i&&(e=a),!!F.isString(e)){if(F.isString(r))return e.indexOf(r)!==-1;if(F.isRegExp(r))return r.test(e)}}function r0(o){return o.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,a,r)=>a.toUpperCase()+r)}function i0(o,e){const a=F.toCamelCase(" "+e);["get","set","has"].forEach(r=>{Object.defineProperty(o,r+a,{__proto__:null,value:function(i,l,u){return this[r].call(this,e,i,l,u)},configurable:!0})})}let Cn=class{constructor(e){e&&this.set(e)}set(e,a,r){const i=this;function l(d,f,g){const m=no(f);if(!m)throw new Error("header name must be a non-empty string");const b=F.findKey(i,m);(!b||i[b]===void 0||g===!0||g===void 0&&i[b]!==!1)&&(i[b||f]=zl(d))}const u=(d,f)=>F.forEach(d,(g,m)=>l(g,m,f));if(F.isPlainObject(e)||e instanceof this.constructor)u(e,a);else if(F.isString(e)&&(e=e.trim())&&!a0(e))u(Xw(e),a);else if(F.isObject(e)&&F.isIterable(e)){let d={},f,g;for(const m of e){if(!F.isArray(m))throw TypeError("Object iterator must return a key-value pair");d[g=m[0]]=(f=d[g])?F.isArray(f)?[...f,m[1]]:[f,m[1]]:m[1]}u(d,a)}else e!=null&&l(a,e,r);return this}get(e,a){if(e=no(e),e){const r=F.findKey(this,e);if(r){const i=this[r];if(!a)return i;if(a===!0)return n0(i);if(F.isFunction(a))return a.call(this,i,r);if(F.isRegExp(a))return a.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,a){if(e=no(e),e){const r=F.findKey(this,e);return!!(r&&this[r]!==void 0&&(!a||dd(this,this[r],r,a)))}return!1}delete(e,a){const r=this;let i=!1;function l(u){if(u=no(u),u){const d=F.findKey(r,u);d&&(!a||dd(r,r[d],d,a))&&(delete r[d],i=!0)}}return F.isArray(e)?e.forEach(l):l(e),i}clear(e){const a=Object.keys(this);let r=a.length,i=!1;for(;r--;){const l=a[r];(!e||dd(this,this[l],l,e,!0))&&(delete this[l],i=!0)}return i}normalize(e){const a=this,r={};return F.forEach(this,(i,l)=>{const u=F.findKey(r,l);if(u){a[u]=zl(i),delete a[l];return}const d=e?r0(l):String(l).trim();d!==l&&delete a[l],a[d]=zl(i),r[d]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const a=Object.create(null);return F.forEach(this,(r,i)=>{r!=null&&r!==!1&&(a[i]=e&&F.isArray(r)?r.join(", "):r)}),a}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,a])=>e+": "+a).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,...a){const r=new this(e);return a.forEach(i=>r.set(i)),r}static accessor(e){const r=(this[_p]=this[_p]={accessors:{}}).accessors,i=this.prototype;function l(u){const d=no(u);r[d]||(i0(i,u),r[d]=!0)}return F.isArray(e)?e.forEach(l):l(e),this}};Cn.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),F.reduceDescriptors(Cn.prototype,({value:o},e)=>{let a=e[0].toUpperCase()+e.slice(1);return{get:()=>o,set(r){this[a]=r}}}),F.freezeMethods(Cn);const s0="[REDACTED ****]";function o0(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 l0(o,e){const a=new Set(e.map(l=>String(l).toLowerCase())),r=[],i=l=>{if(l===null||typeof l!="object"||F.isBuffer(l))return l;if(r.indexOf(l)!==-1)return;l instanceof Cn&&(l=l.toJSON()),r.push(l);let u;if(F.isArray(l))u=[],l.forEach((d,f)=>{const g=i(d);F.isUndefined(g)||(u[f]=g)});else{if(!F.isPlainObject(l)&&o0(l))return r.pop(),l;u=Object.create(null);for(const[d,f]of Object.entries(l)){const g=a.has(d.toLowerCase())?s0:i(f);F.isUndefined(g)||(u[d]=g)}}return r.pop(),u};return i(o)}let Oe=class RS extends Error{static from(e,a,r,i,l,u){const d=new RS(e.message,a||e.code,r,i,l);return d.cause=e,d.name=e.name,e.status!=null&&d.status==null&&(d.status=e.status),u&&Object.assign(d,u),d}constructor(e,a,r,i,l){super(e),Object.defineProperty(this,"message",{__proto__:null,value:e,enumerable:!0,writable:!0,configurable:!0}),this.name="AxiosError",this.isAxiosError=!0,a&&(this.code=a),r&&(this.config=r),i&&(this.request=i),l&&(this.response=l,this.status=l.status)}toJSON(){const e=this.config,a=e&&F.hasOwnProp(e,"redact")?e.redact:void 0,r=F.isArray(a)&&a.length>0?l0(e,a):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:r,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 c0=null;function fd(o){return F.isPlainObject(o)||F.isArray(o)}function Ap(o){return F.endsWith(o,"[]")?o.slice(0,-2):o}function hd(o,e,a){return o?o.concat(e).map(function(i,l){return i=Ap(i),!a&&l?"["+i+"]":i}).join(a?".":""):e}function u0(o){return F.isArray(o)&&!o.some(fd)}const d0=F.toFlatObject(F,{},null,function(e){return/^is[A-Z]/.test(e)});function jl(o,e,a){if(!F.isObject(o))throw new TypeError("target must be an object");e=e||new FormData,a=F.toFlatObject(a,{metaTokens:!0,dots:!1,indexes:!1},!1,function(T,L){return!F.isUndefined(L[T])});const r=a.metaTokens,i=a.visitor||b,l=a.dots,u=a.indexes,d=a.Blob||typeof Blob<"u"&&Blob,f=a.maxDepth===void 0?100:a.maxDepth,g=d&&F.isSpecCompliantForm(e);if(!F.isFunction(i))throw new TypeError("visitor must be a function");function m(A){if(A===null)return"";if(F.isDate(A))return A.toISOString();if(F.isBoolean(A))return A.toString();if(!g&&F.isBlob(A))throw new Oe("Blob is not supported. Use a Buffer instead.");return F.isArrayBuffer(A)||F.isTypedArray(A)?g&&typeof Blob=="function"?new Blob([A]):Buffer.from(A):A}function b(A,T,L){let M=A;if(F.isReactNative(e)&&F.isReactNativeBlob(A))return e.append(hd(L,T,l),m(A)),!1;if(A&&!L&&typeof A=="object"){if(F.endsWith(T,"{}"))T=r?T:T.slice(0,-2),A=JSON.stringify(A);else if(F.isArray(A)&&u0(A)||(F.isFileList(A)||F.endsWith(T,"[]"))&&(M=F.toArray(A)))return T=Ap(T),M.forEach(function(D,U){!(F.isUndefined(D)||D===null)&&e.append(u===!0?hd([T],U,l):u===null?T:T+"[]",m(D))}),!1}return fd(A)?!0:(e.append(hd(L,T,l),m(A)),!1)}const v=[],S=Object.assign(d0,{defaultVisitor:b,convertValue:m,isVisitable:fd});function E(A,T,L=0){if(!F.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(v.indexOf(A)!==-1)throw Error("Circular reference detected in "+T.join("."));v.push(A),F.forEach(A,function(R,D){(!(F.isUndefined(R)||R===null)&&i.call(e,R,F.isString(D)?D.trim():D,T,S))===!0&&E(R,T?T.concat(D):[D],L+1)}),v.pop()}}if(!F.isObject(o))throw new TypeError("data must be an object");return E(o),e}function xp(o){const e={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"};return encodeURIComponent(o).replace(/[!'()~]|%20/g,function(r){return e[r]})} Line truncated
`+u.map(Pp).join(`
`):" "+Pp(u[0]):"as no adapter specified";throw new Oe("There is no suitable adapter to dispatch the request "+d,"ERR_NOT_SUPPORT")}return i}const qp={getAdapter:U0,adapters:Sd};function wd(o){if(o.cancelToken&&o.cancelToken.throwIfRequested(),o.signal&&o.signal.aborted)throw new ro(null,o)}function Gp(o){return wd(o),o.headers=Cn.from(o.headers),o.data=bd.call(o,o.transformRequest),["post","put","patch"].indexOf(o.method)!==-1&&o.headers.setContentType("application/x-www-form-urlencoded",!1),qp.getAdapter(o.adapter||ao.adapter,o)(o).then(function(r){wd(o),o.response=r;try{r.data=bd.call(o,o.transformResponse,r)}finally{delete o.response}return r.headers=Cn.from(r.headers),r},function(r){if(!Op(r)&&(wd(o),r&&r.response)){o.response=r.response;try{r.response.data=bd.call(o,o.transformResponse,r.response)}finally{delete o.response}r.response.headers=Cn.from(r.response.headers)}return Promise.reject(r)})}const Pl={};["object","boolean","number","function","string","symbol"].forEach((o,e)=>{Pl[o]=function(r){return typeof r===o||"a"+(e<1?"n ":" ")+o}});const Vp={};Pl.transitional=function(e,a,r){function i(l,u){return"[Axios v"+vd+"] Transitional option '"+l+"'"+u+(r?". "+r:"")}return(l,u,d)=>{if(e===!1)throw new Oe(i(u," has been removed"+(a?" in "+a:"")),Oe.ERR_DEPRECATED);return a&&!Vp[u]&&(Vp[u]=!0,console.warn(i(u," has been deprecated since v"+a+" and will be removed in the near future"))),e?e(l,u,d):!0}},Pl.spelling=function(e){return(a,r)=>(console.warn(`${r} is likely a misspelling of ${e}`),!0)};function B0(o,e,a){if(typeof o!="object")throw new Oe("options must be an object",Oe.ERR_BAD_OPTION_VALUE);const r=Object.keys(o);let i=r.length;for(;i-- >0;){const l=r[i],u=Object.prototype.hasOwnProperty.call(e,l)?e[l]:void 0;if(u){const d=o[l],f=d===void 0||u(d,l,o);if(f!==!0)throw new Oe("option "+l+" must be "+f,Oe.ERR_BAD_OPTION_VALUE);continue}if(a!==!0)throw new Oe("Unknown option "+l,Oe.ERR_BAD_OPTION)}}const ql={assertOptions:B0,validators:Pl},Jn=ql.validators;let fi=class{constructor(e){this.defaults=e||{},this.interceptors={request:new kp,response:new kp}}async request(e,a){try{return await this._request(e,a)}catch(r){if(r instanceof Error){let i={};Error.captureStackTrace?Error.captureStackTrace(i):i=new Error;const l=(()=>{if(!i.stack)return"";const u=i.stack.indexOf(`
`);return u===-1?"":i.stack.slice(u+1)})();try{if(!r.stack)r.stack=l;else if(l){const u=l.indexOf(`
`),d=u===-1?-1:l.indexOf(`
`,u+1),f=d===-1?"":l.slice(d+1);String(r.stack).endsWith(f)||(r.stack+=`
`+l)}}catch{}}throw r}}_request(e,a){typeof e=="string"?(a=a||{},a.url=e):a=e||{},a=ci(this.defaults,a);const{transitional:r,paramsSerializer:i,headers:l}=a;r!==void 0&&Ul.assertOptions(r,{silentJSONParsing:Jn.transitional(Jn.boolean),forcedJSONParsing:Jn.transitional(Jn.boolean),clarifyTimeoutError:Jn.transitional(Jn.boolean),legacyInterceptorReqResOrdering:Jn.transitional(Jn.boolean)},!1),i!=null&&(F.isFunction(i)?a.paramsSerializer={serialize:i}:Ul.assertOptions(i,{encode:Jn.function,serialize:Jn.function},!0)),a.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?a.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:a.allowAbsoluteUrls=!0),Ul.assertOptions(a,{baseUrl:Jn.spelling("baseURL"),withXsrfToken:Jn.spelling("withXSRFToken")},!0),a.method=(a.method||this.defaults.method||"get").toLowerCase();let u=l&&F.merge(l.common,l[a.method]);l&&F.forEach(["delete","get","head","post","put","patch","query","common"],E=>{delete l[E]}),a.headers=Cn.concat(u,l);const d=[];let f=!0;this.interceptors.request.forEach(function(A){if(typeof A.runWhen=="function"&&A.runWhen(a)===!1)return;f=f&&A.synchronous;const T=a.transitional||fd;T&&T.legacyInterceptorReqResOrdering?d.unshift(A.fulfilled,A.rejected):d.push(A.fulfilled,A.rejected)});const g=[];this.interceptors.response.forEach(function(A){g.push(A.fulfilled,A.rejected)});let m,b=0,v;if(!f){const E=[zp.bind(this),void 0];for(E.unshift(...d),E.push(...g),v=E.length,m=Promise.resolve(a);b<v;)m=m.then(E[b++],E[b++]);return m}v=d.length;let S=a;for(;b<v;){const E=d[b++],A=d[b++];try{S=E(S)}catch(T){A.call(this,T);break}}try{m=zp.call(this,S)}catch(E){return Promise.reject(E)}for(b=0,v=g.length;b<v;)m=m.then(g[b++],g[b++]);return m}getUri(e){e=ci(this.defaults,e);const a=kp(e.baseURL,e.url,e.allowAbsoluteUrls);return Cp(a,e.params,e.paramsSerializer)}};F.forEach(["delete","get","head","options"],function(e){ui.prototype[e]=function(a,r){return this.request(ci(r||{},{method:e,url:a,data:(r||{}).data}))}}),F.forEach(["post","put","patch","query"],function(e){function a(r){return function(l,u,d){return this.request(ci(d||{},{method:e,headers:r?{"Content-Type":"multipart/form-data"}:{},url:l,data:u}))}}ui.prototype[e]=a(),e!=="query"&&(ui.prototype[e+"Form"]=a(!0))});let M0=class ES{constructor(e){if(typeof e!="function")throw new TypeError("executor must be a function.");let a;this.promise=new Promise(function(l){a=l});const r=this;this.promise.then(i=>{if(!r._listeners)return;let l=r._listeners.length;for(;l-- >0;)r._listeners[l](i);r._listeners=null}),this.promise.then=i=>{let l;const u=new Promise(d=>{r.subscribe(d),l=d}).then(i);return u.cancel=function(){r.unsubscribe(l)},u},e(function(l,u,d){r.reason||(r.reason=new to(l,u,d),a(r.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 a=this._listeners.indexOf(e);a!==-1&&this._listeners.splice(a,1)}toAbortSignal(){const e=new AbortController,a=r=>{e.abort(r)};return this.subscribe(a),e.signal.unsubscribe=()=>this.unsubscribe(a),e.signal}static source(){let e;return{token:new ES(function(i){e=i}),cancel:e}}};function $0(o){return function(a){return o.apply(null,a)}}function N0(o){return F.isObject(o)&&o.isAxiosError===!0}const vd={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(vd).forEach(([o,e])=>{vd[e]=o});function Up(o){const e=new ui(o),a=op(ui.prototype.request,e);return F.extend(a,ui.prototype,e,{allOwnKeys:!0}),F.extend(a,e,null,{allOwnKeys:!0}),a.create=function(i){return Up(ci(o,i))},a}const qt=Up(eo);qt.Axios=ui,qt.CanceledError=to,qt.CancelToken=M0,qt.isCa Line truncated
`+l)}}catch{}}throw r}}_request(e,a){typeof e=="string"?(a=a||{},a.url=e):a=e||{},a=di(this.defaults,a);const{transitional:r,paramsSerializer:i,headers:l}=a;r!==void 0&&ql.assertOptions(r,{silentJSONParsing:Jn.transitional(Jn.boolean),forcedJSONParsing:Jn.transitional(Jn.boolean),clarifyTimeoutError:Jn.transitional(Jn.boolean),legacyInterceptorReqResOrdering:Jn.transitional(Jn.boolean)},!1),i!=null&&(F.isFunction(i)?a.paramsSerializer={serialize:i}:ql.assertOptions(i,{encode:Jn.function,serialize:Jn.function},!0)),a.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?a.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:a.allowAbsoluteUrls=!0),ql.assertOptions(a,{baseUrl:Jn.spelling("baseURL"),withXsrfToken:Jn.spelling("withXSRFToken")},!0),a.method=(a.method||this.defaults.method||"get").toLowerCase();let u=l&&F.merge(l.common,l[a.method]);l&&F.forEach(["delete","get","head","post","put","patch","query","common"],E=>{delete l[E]}),a.headers=Cn.concat(u,l);const d=[];let f=!0;this.interceptors.request.forEach(function(A){if(typeof A.runWhen=="function"&&A.runWhen(a)===!1)return;f=f&&A.synchronous;const T=a.transitional||pd;T&&T.legacyInterceptorReqResOrdering?d.unshift(A.fulfilled,A.rejected):d.push(A.fulfilled,A.rejected)});const g=[];this.interceptors.response.forEach(function(A){g.push(A.fulfilled,A.rejected)});let m,b=0,v;if(!f){const E=[Gp.bind(this),void 0];for(E.unshift(...d),E.push(...g),v=E.length,m=Promise.resolve(a);b<v;)m=m.then(E[b++],E[b++]);return m}v=d.length;let S=a;for(;b<v;){const E=d[b++],A=d[b++];try{S=E(S)}catch(T){A.call(this,T);break}}try{m=Gp.call(this,S)}catch(E){return Promise.reject(E)}for(b=0,v=g.length;b<v;)m=m.then(g[b++],g[b++]);return m}getUri(e){e=di(this.defaults,e);const a=Np(e.baseURL,e.url,e.allowAbsoluteUrls);return Rp(a,e.params,e.paramsSerializer)}};F.forEach(["delete","get","head","options"],function(e){fi.prototype[e]=function(a,r){return this.request(di(r||{},{method:e,url:a,data:(r||{}).data}))}}),F.forEach(["post","put","patch","query"],function(e){function a(r){return function(l,u,d){return this.request(di(d||{},{method:e,headers:r?{"Content-Type":"multipart/form-data"}:{},url:l,data:u}))}}fi.prototype[e]=a(),e!=="query"&&(fi.prototype[e+"Form"]=a(!0))});let P0=class kS{constructor(e){if(typeof e!="function")throw new TypeError("executor must be a function.");let a;this.promise=new Promise(function(l){a=l});const r=this;this.promise.then(i=>{if(!r._listeners)return;let l=r._listeners.length;for(;l-- >0;)r._listeners[l](i);r._listeners=null}),this.promise.then=i=>{let l;const u=new Promise(d=>{r.subscribe(d),l=d}).then(i);return u.cancel=function(){r.unsubscribe(l)},u},e(function(l,u,d){r.reason||(r.reason=new ro(l,u,d),a(r.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 a=this._listeners.indexOf(e);a!==-1&&this._listeners.splice(a,1)}toAbortSignal(){const e=new AbortController,a=r=>{e.abort(r)};return this.subscribe(a),e.signal.unsubscribe=()=>this.unsubscribe(a),e.signal}static source(){let e;return{token:new kS(function(i){e=i}),cancel:e}}};function q0(o){return function(a){return o.apply(null,a)}}function G0(o){return F.isObject(o)&&o.isAxiosError===!0}const Cd={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(Cd).forEach(([o,e])=>{Cd[e]=o});function Fp(o){const e=new fi(o),a=hp(fi.prototype.request,e);return F.extend(a,fi.prototype,e,{allOwnKeys:!0}),F.extend(a,e,null,{allOwnKeys:!0}),a.create=function(i){return Fp(di(o,i))},a}const Pt=Fp(ao);Pt.Axios=fi,Pt.CanceledError=ro,Pt.CancelToken=P0,Pt.isCa Line truncated
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
@@ -47,11 +47,11 @@ Error generating stack: `+c.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=()=>{a.parentNode&&a.parentNode.removeChild(a)};i.onclick=l;const u=d=>{this.isImeComposing(d)||d.key==="Escape"&&(l(),document.removeEventListener("keydown",u))};return document.addEventListener("keydown",u),a.onclick=d=>{d.target===a&&l()},r.appendChild(i),a.appendChild(r),document.body.appendChild(a),{overlay:a,layer:r,closeLayer:l}}getNestedProperty(e,a){return a.split(".").reduce((r,i)=>r&&r[i]!==void 0?r[i]:void 0,e)}async handleSaveToLocalStorage(e,a){const{key:r,value:i}=e;if(!r)return H.error("saveToLocalStorage: key parameter is required"),!1;try{const l=typeof i=="string"?i:JSON.stringify(i);return localStorage.setItem(r,l),H.log(`saveToLocalStorage: saved ${r}`,i),!0}catch(l){return H.error("saveToLocalStorage: failed to save",{key:r,error:l}),!1}}async handleLoadFromLocalStorage(e,a){const{key:r,target:i,stateKey:l,defaultValue:u}=e;if(!r)return H.error("loadFromLocalStorage: key parameter is required"),u;try{const d=localStorage.getItem(r);if(d===null)return H.log(`loadFromLocalStorage: no value found for ${r}, using default`),l&&a.setState&&a.setState({[l]:u}),u;let f;try{f=JSON.parse(d)}catch{f=d}return H.log(`loadFromLocalStorage: loaded ${r}`,f),l&&a.setState&&a.setState({[l]:f}),f}catch(d){return H.error("loadFromLocalStorage: failed to load",{key:r,error:d}),u}}async handleCustomAction(e,a){const r=this.customHandlers.get(e.handler);if(!r){if(this.previewMode){H.warn(`[Preview] Unknown action handler "${e.handler}" — skipped (preview mode)`);return}const i=new It(`Unknown action handler: ${e.handler}`,e);throw i.unknownHandler=!0,i}return await r(e,a)}resolveParams(e,a){if(!e)return{};const r={};for(const[i,l]of Object.entries(e))i.includes("{{")&&H.warn(`[resolveParams] setState params의 키에 표현식이 포함되어 있습니다: "${i}". 키는 해석되지 않습니다. 배열 항목 수정은 .map()/.filter() 패턴을 사용하세요.`),typeof l=="string"&&l.includes("{{")?r[i]=this.evaluateExpression(l,a):typeof l=="string"?r[i]=l:Array.isArray(l)?r[i]=l.map(u=>typeof u=="string"&&u.includes("{{")?this.evaluateExpression(u,a):u):typeof l=="object"&&l!==null&&!Array.isArray(l)&&Object.getPrototypeOf(l)!==Object.prototype&&Object.getPrototypeOf(l)!==null?r[i]=l:typeof l=="object"&&l!==null?r[i]=this.resolveParams(l,a):r[i]=l;return r}resolveValue(e,a){return a?this.bindingEngine.resolveBindings(e,a,{skipCache:!0}):e}evaluateExpressions(e,a){let r={};for(const[i,l]of Object.entries(e))if(i==="..."){let u=l;typeof l=="string"&&l.includes("{{")&&(u=this.evaluateExpression(l,a)),u&&typeof u=="object"&&!Array.isArray(u)&&(r={...r,...u})}else typeof l=="string"&&l.includes("{{")?r[i]=this.evaluateExpression(l,a):typeof l=="object"&&l!==null&&!Array.isArray(l)?r[i]=this.evaluateExpressions(l,a):r[i]=l;return r}evaluateExpressionsIfNeeded(e,a){const r={};for(const[i,l]of Object.entries(e))typeof l=="string"&&l.includes("{{")?r[i]=this.evaluateExpression(l,a):r[i]=l;return r}evaluateExpression(e,a){if(!a)return e;const r=e.match(/^\{\{(.+)\}\}$/);if(!(r&&!r[1].includes("}}")&&!r[1].includes("{{"))){let d=a;if(e.includes("_global")){const g=window.G7Core?.state?.get()?._global;g&&(d={...a,_global:g})}if(e.includes("_computed")||e.includes("$computed")){const g=window.__g7ActionContext?.computedRef?.current;g&&Object.keys(g).length>0&&(d={...d,_computed:g,$computed:g})}return this.bindingEngine.resolveBindings(e,d,{skipCache:!0})}let l=r[1].trim();l=l.replace(/\$args\.(\d+)/g,"$args[$1]");let u=a;if(l.includes("_global")){const f=window.G7Core?.state?.get()?._global;f&&(u={...a,_global:f})}if(l.includes("_computed")||l.includes("$computed")){const f=window.__g7ActionContext?.computedRef?.current;f&&Object.keys(f).length>0&&(u={...u,_computed:f,$computed:f})}l.includes("_global.modules")&&(H.log("[evaluateExpression] expression:",l),H.log("[evaluateExpression] dataContext._global:",u._global),H.log("[evaluateExpression] dataContext._global?.modules:",u._global?.modules));try{const d=this.bindingEngine.evaluateExpression(l,u);return l.includes("_global.modules")&&H.log("[evaluateExpression] result:",d),typeof d=="string"&&d.startsWith("$t:")&&this.translationEngine&&this.translationContext?this.translationEngine.resolveTranslations(d,this.translationContext,a):d}catch(d){return H.error("Expression evaluation failed:",e,d),e}}setDefaultContext(e){this.defaultContext={...this.defaultContext,...e}}setGlobalStateUpdater(e){this.globalStateUpdater=e}getGlobalStateUpdater(){return this.globalStateUpdater}async dispatchAction(e,a){const r={...this.defaultContext,...a,data:{...this.defaultContext.data,...a?.data}};if(e.confirm){let i=this.resolveValue(e.confirm,r.data);if(this.translationEngine&&this.translationContext&&i.startsWith("$t:")&&(i=this.translationEngine.resolveTranslations(i,this.translationContext,r.data)),!confirm(i))return{success:!1}}try Line truncated
`,i.onmouseenter=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.2)"},i.onmouseleave=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.1)"};const l=()=>{a.parentNode&&a.parentNode.removeChild(a)};i.onclick=l;const u=d=>{this.isImeComposing(d)||d.key==="Escape"&&(l(),document.removeEventListener("keydown",u))};return document.addEventListener("keydown",u),a.onclick=d=>{d.target===a&&l()},r.appendChild(i),a.appendChild(r),document.body.appendChild(a),{overlay:a,layer:r,closeLayer:l}}getNestedProperty(e,a){return a.split(".").reduce((r,i)=>r&&r[i]!==void 0?r[i]:void 0,e)}async handleSaveToLocalStorage(e,a){const{key:r,value:i}=e;if(!r)return H.error("saveToLocalStorage: key parameter is required"),!1;try{const l=typeof i=="string"?i:JSON.stringify(i);return localStorage.setItem(r,l),H.log(`saveToLocalStorage: saved ${r}`,i),!0}catch(l){return H.error("saveToLocalStorage: failed to save",{key:r,error:l}),!1}}async handleLoadFromLocalStorage(e,a){const{key:r,target:i,stateKey:l,defaultValue:u}=e;if(!r)return H.error("loadFromLocalStorage: key parameter is required"),u;try{const d=localStorage.getItem(r);if(d===null)return H.log(`loadFromLocalStorage: no value found for ${r}, using default`),l&&a.setState&&a.setState({[l]:u}),u;let f;try{f=JSON.parse(d)}catch{f=d}return H.log(`loadFromLocalStorage: loaded ${r}`,f),l&&a.setState&&a.setState({[l]:f}),f}catch(d){return H.error("loadFromLocalStorage: failed to load",{key:r,error:d}),u}}async handleCustomAction(e,a){const r=this.customHandlers.get(e.handler);if(!r){if(this.previewMode){H.warn(`[Preview] Unknown action handler "${e.handler}" — skipped (preview mode)`);return}const i=new It(`Unknown action handler: ${e.handler}`,e);throw i.unknownHandler=!0,i}return await r(e,a)}resolveParams(e,a){if(!e)return{};const r={};for(const[i,l]of Object.entries(e))i.includes("{{")&&H.warn(`[resolveParams] setState params의 키에 표현식이 포함되어 있습니다: "${i}". 키는 해석되지 않습니다. 배열 항목 수정은 .map()/.filter() 패턴을 사용하세요.`),typeof l=="string"&&l.includes("{{")?r[i]=this.evaluateExpression(l,a):typeof l=="string"?r[i]=l:Array.isArray(l)?r[i]=l.map(u=>typeof u=="string"&&u.includes("{{")?this.evaluateExpression(u,a):u):typeof l=="object"&&l!==null&&!Array.isArray(l)&&Object.getPrototypeOf(l)!==Object.prototype&&Object.getPrototypeOf(l)!==null?r[i]=l:typeof l=="object"&&l!==null?r[i]=this.resolveParams(l,a):r[i]=l;return r}resolveValue(e,a){return a?this.bindingEngine.resolveBindings(e,a,{skipCache:!0}):e}evaluateExpressions(e,a){let r={};for(const[i,l]of Object.entries(e))if(i==="..."){let u=l;typeof l=="string"&&l.includes("{{")&&(u=this.evaluateExpression(l,a)),u&&typeof u=="object"&&!Array.isArray(u)&&(r={...r,...u})}else typeof l=="string"&&l.includes("{{")?r[i]=this.evaluateExpression(l,a):typeof l=="object"&&l!==null&&!Array.isArray(l)?r[i]=this.evaluateExpressions(l,a):r[i]=l;return r}evaluateExpressionsIfNeeded(e,a){const r={};for(const[i,l]of Object.entries(e))typeof l=="string"&&l.includes("{{")?r[i]=this.evaluateExpression(l,a):r[i]=l;return r}evaluateExpression(e,a){if(!a)return e;const r=e.match(/^\{\{(.+)\}\}$/);if(!(r&&!r[1].includes("}}")&&!r[1].includes("{{"))){let d=a;if(e.includes("_global")){const g=window.G7Core?.state?.get()?._global;g&&(d={...a,_global:g})}if(e.includes("_computed")||e.includes("$computed")){const g=window.__g7ActionContext?.computedRef?.current;g&&Object.keys(g).length>0&&(d={...d,_computed:g,$computed:g})}return this.bindingEngine.resolveBindings(e,d,{skipCache:!0})}let l=r[1].trim();l=l.replace(/\$args\.(\d+)/g,"$args[$1]");let u=a;if(l.includes("_global")){const f=window.G7Core?.state?.get()?._global;f&&(u={...a,_global:f})}if(l.includes("_computed")||l.includes("$computed")){const f=window.__g7ActionContext?.computedRef?.current;f&&Object.keys(f).length>0&&(u={...u,_computed:f,$computed:f})}l.includes("_global.modules")&&(H.log("[evaluateExpression] expression:",l),H.log("[evaluateExpression] dataContext._global:",u._global),H.log("[evaluateExpression] dataContext._global?.modules:",u._global?.modules));try{const d=this.bindingEngine.evaluateExpression(l,u);return l.includes("_global.modules")&&H.log("[evaluateExpression] result:",d),typeof d=="string"&&d.startsWith("$t:")&&this.translationEngine&&this.translationContext?this.translationEngine.resolveTranslations(d,this.translationContext,a):d}catch(d){return H.error("Expression evaluation failed:",e,d),e}}setDefaultContext(e){this.defaultContext={...this.defaultContext,...e}}setGlobalStateUpdater(e){this.globalStateUpdater=e}getGlobalStateUpdater(){return this.globalStateUpdater}async dispatchAction(e,a){const r={...this.defaultContext,...a,data:{...this.defaultContext.data,...a?.data}};if(e.confirm){let i=this.resolveValue(e.confirm,r.data);if(this.translationEngine&&this.translationContext&&i.startsWith("$t:")&&(i=this.translationEngine.resolveTranslations(i,this.translationContext,r.data)),!confirm(i))return{success:!1}}try Line truncated
To pick up a draggable item, press the space bar.
While dragging, use the arrow keys to move the item.
Press space again to drop the item in its new position, or press escape to cancel.
`},_C={onDragStart(o){let{active:e}=o;return"Picked up draggable item "+e.id+"."},onDragOver(o){let{active:e,over:a}=o;return a?"Draggable item "+e.id+" was moved over droppable area "+a.id+".":"Draggable item "+e.id+" is no longer over a droppable area."},onDragEnd(o){let{active:e,over:a}=o;return a?"Draggable item "+e.id+" was dropped over droppable area "+a.id:"Draggable item "+e.id+" was dropped."},onDragCancel(o){let{active:e}=o;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function AC(o){let{announcements:e=_C,container:a,hiddenTextDescribedById:r,screenReaderInstructions:i=EC}=o;const{announce:l,announcement:u}=SC(),d=lo("DndLiveRegion"),[f,g]=N.useState(!1);if(N.useEffect(()=>{g(!0)},[]),wC(N.useMemo(()=>({onDragStart(b){let{active:v}=b;l(e.onDragStart({active:v}))},onDragMove(b){let{active:v,over:S}=b;e.onDragMove&&l(e.onDragMove({active:v,over:S}))},onDragOver(b){let{active:v,over:S}=b;l(e.onDragOver({active:v,over:S}))},onDragEnd(b){let{active:v,over:S}=b;l(e.onDragEnd({active:v,over:S}))},onDragCancel(b){let{active:v,over:S}=b;l(e.onDragCancel({active:v,over:S}))}}),[l,e])),!f)return null;const m=Ye.createElement(Ye.Fragment,null,Ye.createElement(bC,{id:r,value:i.draggable}),Ye.createElement(vC,{id:d,announcement:u}));return a?Er.createPortal(m,a):m}var Zt;(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"})(Zt||(Zt={}));function Xl(){}function sm(o,e){return N.useMemo(()=>({sensor:o,options:e??{}}),[o,e])}function xC(){for(var o=arguments.length,e=new Array(o),a=0;a<o;a++)e[a]=arguments[a];return N.useMemo(()=>[...e].filter(r=>r!=null),[...e])}const ma=Object.freeze({x:0,y:0});function om(o,e){return Math.sqrt(Math.pow(o.x-e.x,2)+Math.pow(o.y-e.y,2))}function lm(o,e){let{data:{value:a}}=o,{data:{value:r}}=e;return a-r}function TC(o,e){let{data:{value:a}}=o,{data:{value:r}}=e;return r-a}function cm(o){let{left:e,top:a,height:r,width:i}=o;return[{x:e,y:a},{x:e+i,y:a},{x:e,y:a+r},{x:e+i,y:a+r}]}function um(o,e){if(!o||o.length===0)return null;const[a]=o;return a[e]}function dm(o,e,a){return e===void 0&&(e=o.left),a===void 0&&(a=o.top),{x:e+o.width*.5,y:a+o.height*.5}}const RC=o=>{let{collisionRect:e,droppableRects:a,droppableContainers:r}=o;const i=dm(e,e.left,e.top),l=[];for(const u of r){const{id:d}=u,f=a.get(d);if(f){const g=om(dm(f),i);l.push({id:d,data:{droppableContainer:u,value:g}})}}return l.sort(lm)},kC=o=>{let{collisionRect:e,droppableRects:a,droppableContainers:r}=o;const i=cm(e),l=[];for(const u of r){const{id:d}=u,f=a.get(d);if(f){const g=cm(f),m=i.reduce((v,S,E)=>v+om(g[E],S),0),b=Number((m/4).toFixed(4));l.push({id:d,data:{droppableContainer:u,value:b}})}}return l.sort(lm)};function DC(o,e){const a=Math.max(e.top,o.top),r=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),u=i-r,d=l-a;if(r<i&&a<l){const f=e.width*e.height,g=o.width*o.height,m=u*d,b=m/(f+g-m);return Number(b.toFixed(4))}return 0}const OC=o=>{let{collisionRect:e,droppableRects:a,droppableContainers:r}=o;const i=[];for(const l of r){const{id:u}=l,d=a.get(u);if(d){const f=DC(d,e);f>0&&i.push({id:u,data:{droppableContainer:l,value:f}})}}return i.sort(TC)};function LC(o,e,a){return{...o,scaleX:e&&a?e.width/a.width:1,scaleY:e&&a?e.height/a.height:1}}function fm(o,e){return o&&e?{x:o.left-e.left,y:o.top-e.top}:ma}function MC(o){return function(a){for(var r=arguments.length,i=new Array(r>1?r-1:0),l=1;l<r;l++)i[l-1]=arguments[l];return i.reduce((u,d)=>({...u,top:u.top+o*d.y,bottom:u.bottom+o*d.y,left:u.left+o*d.x,right:u.right+o*d.x}),{...a})}}const $C=MC(1);function NC(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 IC(o,e,a){const r=NC(e);if(!r)return o;const{scaleX:i,scaleY:l,x:u,y:d}=r,f=o.left-u-(1-i)*parseFloat(a),g=o.top-d-(1-l)*parseFloat(a.slice(a.indexOf(" ")+1)),m=i?o.width/i:o.width,b=l?o.height/l:o.height;return{width:m,height:b,top:g,right:f+m,bottom:g+b,left:f}}const HC={ignoreTransform:!1};function ns(o,e){e===void 0&&(e=HC);let a=o.getBoundingClientRect();if(e.ignoreTransform){const{transform:g,transformOrigin:m}=Tn(o).getComputedStyle(o);g&&(a=IC(a,g,m))}const{top:r,left:i,width:l,height:u,bottom:d,right:f}=a;return{top:r,left:i,width:l,height:u,bottom:d,right:f}}function hm(o){return ns(o,{ignoreTransform:!0})}function zC(o){const e=o.innerWidth,a=o.innerHeight;return{top:0,left:0,right:e,bottom:a,width:e,height:a}}function jC(o,e){return e===void 0&&(e=Tn(o).getComputedStyle(o)),e.position==="fixed"}function UC(o,e){e===void 0&&(e=Tn(o).getComputedStyle(o));const a=/(auto|scroll|overlay)/;return["overflow","overflowX Line truncated
`},MC={onDragStart(o){let{active:e}=o;return"Picked up draggable item "+e.id+"."},onDragOver(o){let{active:e,over:a}=o;return a?"Draggable item "+e.id+" was moved over droppable area "+a.id+".":"Draggable item "+e.id+" is no longer over a droppable area."},onDragEnd(o){let{active:e,over:a}=o;return a?"Draggable item "+e.id+" was dropped over droppable area "+a.id:"Draggable item "+e.id+" was dropped."},onDragCancel(o){let{active:e}=o;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function $C(o){let{announcements:e=MC,container:a,hiddenTextDescribedById:r,screenReaderInstructions:i=LC}=o;const{announce:l,announcement:u}=kC(),d=fo("DndLiveRegion"),[f,g]=N.useState(!1);if(N.useEffect(()=>{g(!0)},[]),DC(N.useMemo(()=>({onDragStart(b){let{active:v}=b;l(e.onDragStart({active:v}))},onDragMove(b){let{active:v,over:S}=b;e.onDragMove&&l(e.onDragMove({active:v,over:S}))},onDragOver(b){let{active:v,over:S}=b;l(e.onDragOver({active:v,over:S}))},onDragEnd(b){let{active:v,over:S}=b;l(e.onDragEnd({active:v,over:S}))},onDragCancel(b){let{active:v,over:S}=b;l(e.onDragCancel({active:v,over:S}))}}),[l,e])),!f)return null;const m=Ye.createElement(Ye.Fragment,null,Ye.createElement(TC,{id:r,value:i.draggable}),Ye.createElement(RC,{id:d,announcement:u}));return a?Ar.createPortal(m,a):m}var Zt;(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"})(Zt||(Zt={}));function Zl(){}function fm(o,e){return N.useMemo(()=>({sensor:o,options:e??{}}),[o,e])}function NC(){for(var o=arguments.length,e=new Array(o),a=0;a<o;a++)e[a]=arguments[a];return N.useMemo(()=>[...e].filter(r=>r!=null),[...e])}const ma=Object.freeze({x:0,y:0});function hm(o,e){return Math.sqrt(Math.pow(o.x-e.x,2)+Math.pow(o.y-e.y,2))}function gm(o,e){let{data:{value:a}}=o,{data:{value:r}}=e;return a-r}function IC(o,e){let{data:{value:a}}=o,{data:{value:r}}=e;return r-a}function pm(o){let{left:e,top:a,height:r,width:i}=o;return[{x:e,y:a},{x:e+i,y:a},{x:e,y:a+r},{x:e+i,y:a+r}]}function mm(o,e){if(!o||o.length===0)return null;const[a]=o;return a[e]}function ym(o,e,a){return e===void 0&&(e=o.left),a===void 0&&(a=o.top),{x:e+o.width*.5,y:a+o.height*.5}}const HC=o=>{let{collisionRect:e,droppableRects:a,droppableContainers:r}=o;const i=ym(e,e.left,e.top),l=[];for(const u of r){const{id:d}=u,f=a.get(d);if(f){const g=hm(ym(f),i);l.push({id:d,data:{droppableContainer:u,value:g}})}}return l.sort(gm)},zC=o=>{let{collisionRect:e,droppableRects:a,droppableContainers:r}=o;const i=pm(e),l=[];for(const u of r){const{id:d}=u,f=a.get(d);if(f){const g=pm(f),m=i.reduce((v,S,E)=>v+hm(g[E],S),0),b=Number((m/4).toFixed(4));l.push({id:d,data:{droppableContainer:u,value:b}})}}return l.sort(gm)};function jC(o,e){const a=Math.max(e.top,o.top),r=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),u=i-r,d=l-a;if(r<i&&a<l){const f=e.width*e.height,g=o.width*o.height,m=u*d,b=m/(f+g-m);return Number(b.toFixed(4))}return 0}const UC=o=>{let{collisionRect:e,droppableRects:a,droppableContainers:r}=o;const i=[];for(const l of r){const{id:u}=l,d=a.get(u);if(d){const f=jC(d,e);f>0&&i.push({id:u,data:{droppableContainer:l,value:f}})}}return i.sort(IC)};function BC(o,e,a){return{...o,scaleX:e&&a?e.width/a.width:1,scaleY:e&&a?e.height/a.height:1}}function bm(o,e){return o&&e?{x:o.left-e.left,y:o.top-e.top}:ma}function PC(o){return function(a){for(var r=arguments.length,i=new Array(r>1?r-1:0),l=1;l<r;l++)i[l-1]=arguments[l];return i.reduce((u,d)=>({...u,top:u.top+o*d.y,bottom:u.bottom+o*d.y,left:u.left+o*d.x,right:u.right+o*d.x}),{...a})}}const qC=PC(1);function GC(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 VC(o,e,a){const r=GC(e);if(!r)return o;const{scaleX:i,scaleY:l,x:u,y:d}=r,f=o.left-u-(1-i)*parseFloat(a),g=o.top-d-(1-l)*parseFloat(a.slice(a.indexOf(" ")+1)),m=i?o.width/i:o.width,b=l?o.height/l:o.height;return{width:m,height:b,top:g,right:f+m,bottom:g+b,left:f}}const FC={ignoreTransform:!1};function is(o,e){e===void 0&&(e=FC);let a=o.getBoundingClientRect();if(e.ignoreTransform){const{transform:g,transformOrigin:m}=Tn(o).getComputedStyle(o);g&&(a=VC(a,g,m))}const{top:r,left:i,width:l,height:u,bottom:d,right:f}=a;return{top:r,left:i,width:l,height:u,bottom:d,right:f}}function vm(o){return is(o,{ignoreTransform:!0})}function KC(o){const e=o.innerWidth,a=o.innerHeight;return{top:0,left:0,right:e,bottom:a,width:e,height:a}}function WC(o,e){return e===void 0&&(e=Tn(o).getComputedStyle(o)),e.position==="fixed"}function YC(o,e){e===void 0&&(e=Tn(o).getComputedStyle(o));const a=/(auto|scroll|overlay)/;return["overflow","overflowX Line truncated
<div style="padding: 1rem; border: 2px dashed #ccc; background: #f9f9f9; text-align: center;">
<p style="margin: 0; color: #666;">Component not found: <strong>${e}</strong></p>
</div>
@@ -102,8 +102,8 @@ Error generating stack: `+c.message+`
"
>페이지 새로고침</button>
</div>
`,Bn.error("Error state rendered:",i,r)}getCurrentLayout(){return this.currentLayout}clear(){this.currentLayout=null,this.layoutCache.clear(),Bn.log("Layout data and cache cleared")}}const d_=ct("TranslationContext"),Qd=N.createContext(null),sc=({children:o,translationEngine:e,translationContext:a})=>{const r=N.useMemo(()=>(l,u)=>{if(u){const d="|"+Object.entries(u).map(([f,g])=>`${f}=${g}`).join("|");return e.translate(l,a,d)}return e.translate(l,a)},[e,a]),i=N.useMemo(()=>({translationEngine:e,translationContext:a,t:r}),[e,a,r]);return ut.jsx(Qd.Provider,{value:i,children:o})},Um=()=>{const o=N.useContext(Qd);return o||(d_.warn("TranslationProvider 외부에서 호출되었습니다. 키를 그대로 반환합니다."),{t:e=>e,translationEngine:null,translationContext:null})},xr=ct("SlotContext"),f_={registerToSlot:()=>{xr.warn("SlotContext not available: registerToSlot called outside SlotProvider")},unregisterFromSlot:()=>{xr.warn("SlotContext not available: unregisterFromSlot called outside SlotProvider")},getSlotComponents:()=>[],subscribeToSlot:()=>()=>{},clearAllSlots:()=>{},isEnabled:!1},h_=N.createContext(f_),Zd=({children:o})=>{const e=N.useRef(new Map),a=N.useRef(new Map),r=N.useCallback(m=>{const b=a.current.get(m);b&&b.forEach(v=>{try{v()}catch(S){xr.error(`Slot subscriber error (slotId: ${m}):`,S)}})},[]),i=N.useCallback((m,b,v)=>{if(!m||!b){xr.warn("registerToSlot: slotId and componentId are required");return}e.current.has(m)||e.current.set(m,new Map);const S=e.current.get(m),E=S.get(b);E&&E.registrationKey===v.registrationKey||(S.set(b,v),xr.log(`Component registered to slot: ${b} -> ${m} (order: ${v.order})`),r(m))},[r]),l=N.useCallback((m,b)=>{const v=e.current.get(m);v&&v.has(b)&&(v.delete(b),xr.log(`Component unregistered from slot: ${b} <- ${m}`),v.size===0&&e.current.delete(m),r(m))},[r]),u=N.useCallback(m=>{const b=e.current.get(m);return b?Array.from(b.values()).sort((v,S)=>v.order-S.order):[]},[]),d=N.useCallback((m,b)=>{a.current.has(m)||a.current.set(m,new Set);const v=a.current.get(m);return v.add(b),()=>{v.delete(b),v.size===0&&a.current.delete(m)}},[]),f=N.useCallback(()=>{e.current.forEach((m,b)=>{r(b)}),e.current.clear(),a.current.clear(),xr.log("All slots cleared")},[r]),g=N.useMemo(()=>({registerToSlot:i,unregisterFromSlot:l,getSlotComponents:u,subscribeToSlot:d,clearAllSlots:f,isEnabled:!0}),[i,l,u,d,f]);return N.useLayoutEffect(()=>{typeof window<"u"&&(window.__slotContextValue=g,xr.log("SlotContext exposed to window.__slotContextValue"))},[g]),ut.jsx(h_.Provider,{value:g,children:o})};class ef{constructor(){this.notificationCreatedEvent=".Illuminate\\Notifications\\Events\\BroadcastNotificationCreated"}listenForWhisper(e,a){return this.listen(".client-"+e,a)}notification(e){return this.listen(this.notificationCreatedEvent,e)}stopListeningForNotification(e){return this.stopListening(this.notificationCreatedEvent,e)}stopListeningForWhisper(e,a){return this.stopListening(".client-"+e,a)}}class Bm{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 g_(o){try{return Reflect.construct(String,[],o),!0}catch{return!1}}class tf extends ef{constructor(e,a,r){super(),this.name=a,this.pusher=e,this.options=r,this.eventFormatter=new Bm(this.options.namespace),this.subscribe()}subscribe(){this.subscription=this.pusher.subscribe(this.name)}unsubscribe(){this.pusher.unsubscribe(this.name)}listen(e,a){return this.on(this.eventFormatter.format(e),a),this}listenToAll(e){return this.subscription.bind_global((a,r)=>{if(a.startsWith("pusher:"))return;let i=String(this.options.namespace??"").replace(/\./g,"\\"),l=a.startsWith(i)?a.substring(i.length+1):"."+a;e(l,r)}),this}stopListening(e,a){return a?this.subscription.unbind(this.eventFormatter.format(e),a):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",a=>{e(a)}),this}on(e,a){return this.subscription.bind(e,a),this}}class qm extends tf{whisper(e,a){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,a),this}}class p_ extends tf{whisper(e,a){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,a),this}}class m_ extends qm{here(e){return this.on("pusher:subscription_succeeded",a=>{e(Object.keys(a.members).map(r=>a.members[r]))}),this}joining(e){return this.on("pusher:member_added",a=>{e(a.info)}),this}whisper(e,a){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,a),this}leaving(e){return this.on("pusher:member_removed",a=>{e(a.info)}),this}}class Pm extends ef{constructor(e,a,r){super(),this.events={},this.listeners={},this.name=a,this.socket=e,this.options=r,this.eventFormatter=new Bm(this.op Line truncated
`);return O!==-1?(this.position+=O+1,C.slice(0,O)):null}isBufferTooLong(y){return this.position===y.length&&y.length>To}}var Ro;(function(_){_[_.CONNECTING=0]="CONNECTING",_[_.OPEN=1]="OPEN",_[_.CLOSED=3]="CLOSED"})(Ro||(Ro={}));const na=Ro;var Aa=1;class ko{constructor(y,C){this.hooks=y,this.session=ls(1e3)+"/"+xc(8),this.location=Do(C),this.readyState=na.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===na.OPEN)try{return Be.createSocketRequest("POST",ar(Hr(this.location,this.session))).start(y),!0}catch{return!1}else return!1}reconnect(){this.closeStream(),this.openStream()}onClose(y,C,O){this.closeStream(),this.readyState=na.CLOSED,this.onclose&&this.onclose({code:y,reason:C,wasClean:O})}onChunk(y){if(y.status===200){this.readyState===na.OPEN&&this.onActivity();var C,O=y.data.slice(0,1);switch(O){case"o":C=JSON.parse(y.data.slice(1)||"{}"),this.onOpen(C);break;case"a":C=JSON.parse(y.data.slice(1)||"[]");for(var I=0;I<C.length;I++)this.onEvent(C[I]);break;case"m":C=JSON.parse(y.data.slice(1)||"null"),this.onEvent(C);break;case"h":this.hooks.onHeartbeat(this);break;case"c":C=JSON.parse(y.data.slice(1)||"[]"),this.onClose(C[0],C[1],!0);break}}}onOpen(y){this.readyState===na.CONNECTING?(y&&y.hostname&&(this.location.base=Ac(this.location.base,y.hostname)),this.readyState=na.OPEN,this.onopen&&this.onopen()):this.onClose(1006,"Server lost session",!0)}onEvent(y){this.readyState===na.OPEN&&this.onmessage&&this.onmessage({data:y})}onActivity(){this.onactivity&&this.onactivity()}onError(y){this.onerror&&this.onerror(y)}openStream(){this.stream=Be.createSocketRequest("POST",ar(this.hooks.getReceiveURL(this.location,this.session))),this.stream.bind("chunk",y=>{this.onChunk(y)}),this.stream.bind("finished",y=>{this.hooks.onFinished(this,y)}),this.stream.bind("buffer_too_long",()=>{this.reconnect()});try{this.stream.start()}catch(y){He.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 Do(_){var y=/([^\?]*)\/*(\??.*)/.exec(_);return{base:y[1],queryString:y[2]}}function Hr(_,y){return _.base+"/"+y+"/xhr_send"}function ar(_){var y=_.indexOf("?")===-1?"?":"&";return _+y+"t="+ +new Date+"&n="+Aa++}function Ac(_,y){var C=/(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(_);return C[1]+y+C[3]}function ls(_){return Be.randomInt(_)}function xc(_){for(var y=[],C=0;C<_;C++)y.push(ls(32).toString(32));return y.join("")}const bi=ko;var xa={getReceiveURL:function(_,y){return _.base+"/"+y+"/xhr_streaming"+_.queryString},onHeartbeat:function(_){_.sendRaw("[]")},sendHeartbeat:function(_){_.sendRaw("[]")},onFinished:function(_,y){_.onClose(1006,"Connection interrupted ("+y+")",!1)}};const Oo=xa;var vi={getReceiveURL:function(_,y){return _.base+"/"+y+"/xhr"+_.queryString},onHeartbeat:function(){},sendHeartbeat:function(_){_.sendRaw("[]")},onFinished:function(_,y){y===200?_.reconnect():_.onClose(1006,"Connection interrupted ("+y+")",!1)}};const qa=vi;var Lo={getRequest:function(_){var y=Be.getXHRAPI(),C=new y;return C.onreadystatechange=C.onprogress=function(){switch(C.readyState){case 3:C.responseText&&C.responseText.length>0&&_.onChunk(C.status,C.responseText);break;case 4:C.responseText&&C.responseText.length>0&&_.onChunk(C.status,C.responseText),_.emit("finished",C.status),_.close();break}},C},abortRequest:function(_){_.onreadystatechange=null,_.abort()}};const cs=Lo;var Tc={createStreamingSocket(_){return this.createSocket(Oo,_)},createPollingSocket(_){return this.createSocket(qa,_)},createSocket(_,y){return new bi(_,y)},createXHR(_,y){return this.createRequest(cs,_,y)},createRequest(_,y,C){return new yf(_,y,C)}};const Si=Tc;Si.createXDR=function(_,y){return this.createRequest(_c,_,y)};var Rc={nextAuthCallbackID:1,auth_callbacks:{},ScriptReceivers:m,DependenciesReceivers:E,getDefaultStrategy:Ir,Transports:vn,transportConnectionInitializer:mf,HTTPFactory:Si,TimelineTransport:P,getXHRAPI(){return window.XMLHttpRequest},getWebSocketAPI(){return window.WebSocket||window.MozWebSocket},setup(_){window.Pusher=_;var y=()=>{this.onDocumentBody(_.ready)};window.JSON?y():A.load("json2",{},y)},getDocument(){return document},getProtocol(){return this.getDocument().location.protocol},getAuthorizers(){return{ajax:Xe,jsonp:En}},onDocumentBody(_){document.body?_():setTimeout(()=>{this.onDocumentBody(_)},0)},createJSONPRequest(_,y){return new Ea(_,y)},createScriptRequest(_){return new er(_)},getLocalStorage(){try{return window.localStorage}catch{return}},createXHR(){return this.getXHRAPI()?this.createXMLHttpRequest():this.createMicrosoftXHR()},createXMLHttpRequest(){var _=this.getXHRAPI();return new _},createMicrosoftXHR(){return new ActiveXObject("Microsoft.XMLHTTP")},getNetwork(){return _o},createWebSocket(_){var y=this.getWebSocketAPI();return new y(_)},createSocketRequest(_,y){if(this.isXHRSuppor Line truncated
`,Bn.error("Error state rendered:",i,r)}getCurrentLayout(){return this.currentLayout}clear(){this.currentLayout=null,this.layoutCache.clear(),Bn.log("Layout data and cache cleared")}}const S_=ct("TranslationContext"),tf=N.createContext(null),cc=({children:o,translationEngine:e,translationContext:a})=>{const r=N.useMemo(()=>(l,u)=>{if(u){const d="|"+Object.entries(u).map(([f,g])=>`${f}=${g}`).join("|");return e.translate(l,a,d)}return e.translate(l,a)},[e,a]),i=N.useMemo(()=>({translationEngine:e,translationContext:a,t:r}),[e,a,r]);return ut.jsx(tf.Provider,{value:i,children:o})},Fm=()=>{const o=N.useContext(tf);return o||(S_.warn("TranslationProvider 외부에서 호출되었습니다. 키를 그대로 반환합니다."),{t:e=>e,translationEngine:null,translationContext:null})},Rr=ct("SlotContext"),w_={registerToSlot:()=>{Rr.warn("SlotContext not available: registerToSlot called outside SlotProvider")},unregisterFromSlot:()=>{Rr.warn("SlotContext not available: unregisterFromSlot called outside SlotProvider")},getSlotComponents:()=>[],subscribeToSlot:()=>()=>{},clearAllSlots:()=>{},isEnabled:!1},C_=N.createContext(w_),nf=({children:o})=>{const e=N.useRef(new Map),a=N.useRef(new Map),r=N.useCallback(m=>{const b=a.current.get(m);b&&b.forEach(v=>{try{v()}catch(S){Rr.error(`Slot subscriber error (slotId: ${m}):`,S)}})},[]),i=N.useCallback((m,b,v)=>{if(!m||!b){Rr.warn("registerToSlot: slotId and componentId are required");return}e.current.has(m)||e.current.set(m,new Map);const S=e.current.get(m),E=S.get(b);E&&E.registrationKey===v.registrationKey||(S.set(b,v),Rr.log(`Component registered to slot: ${b} -> ${m} (order: ${v.order})`),r(m))},[r]),l=N.useCallback((m,b)=>{const v=e.current.get(m);v&&v.has(b)&&(v.delete(b),Rr.log(`Component unregistered from slot: ${b} <- ${m}`),v.size===0&&e.current.delete(m),r(m))},[r]),u=N.useCallback(m=>{const b=e.current.get(m);return b?Array.from(b.values()).sort((v,S)=>v.order-S.order):[]},[]),d=N.useCallback((m,b)=>{a.current.has(m)||a.current.set(m,new Set);const v=a.current.get(m);return v.add(b),()=>{v.delete(b),v.size===0&&a.current.delete(m)}},[]),f=N.useCallback(()=>{e.current.forEach((m,b)=>{r(b)}),e.current.clear(),a.current.clear(),Rr.log("All slots cleared")},[r]),g=N.useMemo(()=>({registerToSlot:i,unregisterFromSlot:l,getSlotComponents:u,subscribeToSlot:d,clearAllSlots:f,isEnabled:!0}),[i,l,u,d,f]);return N.useLayoutEffect(()=>{typeof window<"u"&&(window.__slotContextValue=g,Rr.log("SlotContext exposed to window.__slotContextValue"))},[g]),ut.jsx(C_.Provider,{value:g,children:o})};class af{constructor(){this.notificationCreatedEvent=".Illuminate\\Notifications\\Events\\BroadcastNotificationCreated"}listenForWhisper(e,a){return this.listen(".client-"+e,a)}notification(e){return this.listen(this.notificationCreatedEvent,e)}stopListeningForNotification(e){return this.stopListening(this.notificationCreatedEvent,e)}stopListeningForWhisper(e,a){return this.stopListening(".client-"+e,a)}}class Km{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 E_(o){try{return Reflect.construct(String,[],o),!0}catch{return!1}}class rf extends af{constructor(e,a,r){super(),this.name=a,this.pusher=e,this.options=r,this.eventFormatter=new Km(this.options.namespace),this.subscribe()}subscribe(){this.subscription=this.pusher.subscribe(this.name)}unsubscribe(){this.pusher.unsubscribe(this.name)}listen(e,a){return this.on(this.eventFormatter.format(e),a),this}listenToAll(e){return this.subscription.bind_global((a,r)=>{if(a.startsWith("pusher:"))return;let i=String(this.options.namespace??"").replace(/\./g,"\\"),l=a.startsWith(i)?a.substring(i.length+1):"."+a;e(l,r)}),this}stopListening(e,a){return a?this.subscription.unbind(this.eventFormatter.format(e),a):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",a=>{e(a)}),this}on(e,a){return this.subscription.bind(e,a),this}}class Wm extends rf{whisper(e,a){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,a),this}}class __ extends rf{whisper(e,a){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,a),this}}class A_ extends Wm{here(e){return this.on("pusher:subscription_succeeded",a=>{e(Object.keys(a.members).map(r=>a.members[r]))}),this}joining(e){return this.on("pusher:member_added",a=>{e(a.info)}),this}whisper(e,a){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,a),this}leaving(e){return this.on("pusher:member_removed",a=>{e(a.info)}),this}}class Ym extends af{constructor(e,a,r){super(),this.events={},this.listeners={},this.name=a,this.socket=e,this.options=r,this.eventFormatter=new Km(this.op Line truncated
`);return O!==-1?(this.position+=O+1,C.slice(0,O)):null}isBufferTooLong(y){return this.position===y.length&&y.length>Do}}var Oo;(function(_){_[_.CONNECTING=0]="CONNECTING",_[_.OPEN=1]="OPEN",_[_.CLOSED=3]="CLOSED"})(Oo||(Oo={}));const na=Oo;var Aa=1;class Lo{constructor(y,C){this.hooks=y,this.session=ds(1e3)+"/"+kc(8),this.location=Mo(C),this.readyState=na.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===na.OPEN)try{return Be.createSocketRequest("POST",rr(jr(this.location,this.session))).start(y),!0}catch{return!1}else return!1}reconnect(){this.closeStream(),this.openStream()}onClose(y,C,O){this.closeStream(),this.readyState=na.CLOSED,this.onclose&&this.onclose({code:y,reason:C,wasClean:O})}onChunk(y){if(y.status===200){this.readyState===na.OPEN&&this.onActivity();var C,O=y.data.slice(0,1);switch(O){case"o":C=JSON.parse(y.data.slice(1)||"{}"),this.onOpen(C);break;case"a":C=JSON.parse(y.data.slice(1)||"[]");for(var I=0;I<C.length;I++)this.onEvent(C[I]);break;case"m":C=JSON.parse(y.data.slice(1)||"null"),this.onEvent(C);break;case"h":this.hooks.onHeartbeat(this);break;case"c":C=JSON.parse(y.data.slice(1)||"[]"),this.onClose(C[0],C[1],!0);break}}}onOpen(y){this.readyState===na.CONNECTING?(y&&y.hostname&&(this.location.base=Rc(this.location.base,y.hostname)),this.readyState=na.OPEN,this.onopen&&this.onopen()):this.onClose(1006,"Server lost session",!0)}onEvent(y){this.readyState===na.OPEN&&this.onmessage&&this.onmessage({data:y})}onActivity(){this.onactivity&&this.onactivity()}onError(y){this.onerror&&this.onerror(y)}openStream(){this.stream=Be.createSocketRequest("POST",rr(this.hooks.getReceiveURL(this.location,this.session))),this.stream.bind("chunk",y=>{this.onChunk(y)}),this.stream.bind("finished",y=>{this.hooks.onFinished(this,y)}),this.stream.bind("buffer_too_long",()=>{this.reconnect()});try{this.stream.start()}catch(y){He.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 Mo(_){var y=/([^\?]*)\/*(\??.*)/.exec(_);return{base:y[1],queryString:y[2]}}function jr(_,y){return _.base+"/"+y+"/xhr_send"}function rr(_){var y=_.indexOf("?")===-1?"?":"&";return _+y+"t="+ +new Date+"&n="+Aa++}function Rc(_,y){var C=/(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(_);return C[1]+y+C[3]}function ds(_){return Be.randomInt(_)}function kc(_){for(var y=[],C=0;C<_;C++)y.push(ds(32).toString(32));return y.join("")}const Si=Lo;var xa={getReceiveURL:function(_,y){return _.base+"/"+y+"/xhr_streaming"+_.queryString},onHeartbeat:function(_){_.sendRaw("[]")},sendHeartbeat:function(_){_.sendRaw("[]")},onFinished:function(_,y){_.onClose(1006,"Connection interrupted ("+y+")",!1)}};const $o=xa;var wi={getReceiveURL:function(_,y){return _.base+"/"+y+"/xhr"+_.queryString},onHeartbeat:function(){},sendHeartbeat:function(_){_.sendRaw("[]")},onFinished:function(_,y){y===200?_.reconnect():_.onClose(1006,"Connection interrupted ("+y+")",!1)}};const qa=wi;var No={getRequest:function(_){var y=Be.getXHRAPI(),C=new y;return C.onreadystatechange=C.onprogress=function(){switch(C.readyState){case 3:C.responseText&&C.responseText.length>0&&_.onChunk(C.status,C.responseText);break;case 4:C.responseText&&C.responseText.length>0&&_.onChunk(C.status,C.responseText),_.emit("finished",C.status),_.close();break}},C},abortRequest:function(_){_.onreadystatechange=null,_.abort()}};const fs=No;var Dc={createStreamingSocket(_){return this.createSocket($o,_)},createPollingSocket(_){return this.createSocket(qa,_)},createSocket(_,y){return new Si(_,y)},createXHR(_,y){return this.createRequest(fs,_,y)},createRequest(_,y,C){return new Sf(_,y,C)}};const Ci=Dc;Ci.createXDR=function(_,y){return this.createRequest(Tc,_,y)};var Oc={nextAuthCallbackID:1,auth_callbacks:{},ScriptReceivers:m,DependenciesReceivers:E,getDefaultStrategy:zr,Transports:vn,transportConnectionInitializer:vf,HTTPFactory:Ci,TimelineTransport:q,getXHRAPI(){return window.XMLHttpRequest},getWebSocketAPI(){return window.WebSocket||window.MozWebSocket},setup(_){window.Pusher=_;var y=()=>{this.onDocumentBody(_.ready)};window.JSON?y():A.load("json2",{},y)},getDocument(){return document},getProtocol(){return this.getDocument().location.protocol},getAuthorizers(){return{ajax:Xe,jsonp:En}},onDocumentBody(_){document.body?_():setTimeout(()=>{this.onDocumentBody(_)},0)},createJSONPRequest(_,y){return new Ea(_,y)},createScriptRequest(_){return new tr(_)},getLocalStorage(){try{return window.localStorage}catch{return}},createXHR(){return this.getXHRAPI()?this.createXMLHttpRequest():this.createMicrosoftXHR()},createXMLHttpRequest(){var _=this.getXHRAPI();return new _},createMicrosoftXHR(){return new ActiveXObject("Microsoft.XMLHTTP")},getNetwork(){return To},createWebSocket(_){var y=this.getWebSocketAPI();return new y(_)},createSocketRequest(_,y){if(this.isXHRSuppor Line truncated
display: flex;
align-items: center;
justify-content: center;
@@ -224,7 +224,7 @@ Error generating stack: `+c.message+`
`:""}
</div>
</div>
`}static renderFromError(e,a,r,i=!1,l,u,d){let f,g;if(a.userMessageKey.startsWith("$t:"))if(u&&l)try{f=u.translate(a.userMessageKey,l)}catch{f=a.userMessageKey.replace("$t:","")}else f=a.userMessageKey.replace("$t:","");else f=a.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:r,message:f,detailMessage:g,icon:a.icon,showStack:a.showStack,stackTrace:a.stack,showReloadButton:a.recoverable,debug:i})}static escapeHtml(e){if(!e)return"";const a=document.createElement("div");return a.textContent=e,a.innerHTML}}const as=ct("Router");class O_{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,a){this.eventHandlers.has(e)||this.eventHandlers.set(e,[]),this.eventHandlers.get(e).push(a)}async emit(e,...a){const r=this.eventHandlers.get(e);r&&await Promise.all(r.map(i=>i(...a)))}async loadRoutes(e){try{const a=e!==void 0&&e>0?`?v=${e}`:"",r=await fetch(`/api/templates/${this.templateIdentifier}/routes.json${a}`);if(!r.ok)throw new Error(`Failed to load routes: ${r.statusText}`);const i=await r.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,as.log(`Loaded ${this.routes.length} routes${e?` (v=${e})`:""}`);else throw new Error("Invalid routes data format")}catch(a){throw as.error("Error loading routes:",a),a}}setRoutes(e){this.routes=e,as.log(`Set ${this.routes.length} routes`)}match(e){for(const a of this.routes){const r=this.matchPattern(a.path,e);if(r!==null)return{route:a,params:r}}return null}matchPattern(e,a){const r=[];let i=e.replace(/:([^/]+)/g,(f,g)=>(r.push(g),"([^/]+)"));i.startsWith("*/")&&(i="(?:/[^/]+)?"+i.slice(1)),i=i.replace(/\//g,"\\/");const l=new RegExp(`^${i}$`),u=a.match(l);if(!u)return null;const d={};return r.forEach((f,g)=>{d[f]=u[g+1]}),d}getRoutes(){return[...this.routes]}getAuthType(e,a){return e.auth_type?e.auth_type:a.startsWith("/admin")?"admin":"user"}async navigateToCurrentPath(){const e=window.location.pathname,a=window.location.search,r=this.match(e);if(!r){as.warn(`No route matched for path: ${e}`),this.emit("routeNotFound",e);return}if(r.route.redirect){as.log(`Redirecting from ${e} to ${r.route.redirect}`),this.navigate(r.route.redirect);return}if(r.route.auth_required){const u=this.getAuthType(r.route,e),d=ha.getInstance();let f=d.isAuthenticated()&&d.getAuthType()===u;if(f||(f=await d.checkAuth(u)),!f){const g=d.getLoginRedirectUrl(u,e+a);as.log(`Not authenticated, redirecting to: ${g}`),window.location.href=g;return}}const i=new URLSearchParams(a),l={};for(const u of i.keys()){if(u in l)continue;const d=i.getAll(u);d.length>1?l[u]=d:d.length===1&&(u.endsWith("[]")?l[u]=d:l[u]=d[0])}await this.emit("routeChange",{path:e,layout:r.route.layout,endpoint:r.route.endpoint,params:{...r.route.params||{},...r.params},query:l,auth_required:r.route.auth_required,auth_type:r.route.auth_type,meta:r.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 a=this.pendingNavigation;this.executeNavigation(a)}}}}const L_=/^\/admin\/layout-editor\/([^/?#]+)\/?$/;function Zm(o){const e=L_.exec(o);return e?{templateIdentifier:e[1]}:null}const zt=ct("ErrorPageHandler");class M_{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(`/api/templates/${this.templateId}/config.json`);if(!e.ok)return this.debug&&zt.warn("Failed to load template config:",e.statusText),this.configLoaded=!0,null;const a=await e.json();if(!a.success||!a.data)return this.debug&&zt.warn("Invalid template config response"),this.configLoaded=!0,null;const r=a.data;return!r.error_config||!r.error_config.layouts?(this.debug&&zt.warn("No error_config found in template.json"),this.configLoaded=!0,null):(this.errorConfig=r.error_config,this.configLoaded=!0,this.debug&&zt.log("Error config loaded:",this.errorConfig),this.errorConfig)}catch(e){return zt.error("Failed to load error config:",e),this.configLoaded=!0,null}}async rend Line truncated
`}static renderFromError(e,a,r,i=!1,l,u,d){let f,g;if(a.userMessageKey.startsWith("$t:"))if(u&&l)try{f=u.translate(a.userMessageKey,l)}catch{f=a.userMessageKey.replace("$t:","")}else f=a.userMessageKey.replace("$t:","");else f=a.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:r,message:f,detailMessage:g,icon:a.icon,showStack:a.showStack,stackTrace:a.stack,showReloadButton:a.recoverable,debug:i})}static escapeHtml(e){if(!e)return"";const a=document.createElement("div");return a.textContent=e,a.innerHTML}}const ss=ct("Router");class U_{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,a){this.eventHandlers.has(e)||this.eventHandlers.set(e,[]),this.eventHandlers.get(e).push(a)}async emit(e,...a){const r=this.eventHandlers.get(e);r&&await Promise.all(r.map(i=>i(...a)))}async loadRoutes(e){try{const a=e!==void 0&&e>0?e:null,r=await fetch(La(`/api/templates/${this.templateIdentifier}/routes`,"json",a));if(!r.ok)throw new Error(`Failed to load routes: ${r.statusText}`);const i=await r.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,ss.log(`Loaded ${this.routes.length} routes${e?` (v=${e})`:""}`);else throw new Error("Invalid routes data format")}catch(a){throw ss.error("Error loading routes:",a),a}}setRoutes(e){this.routes=e,ss.log(`Set ${this.routes.length} routes`)}match(e){const a=this.normalizePathname(e);for(const r of this.routes){const i=this.matchPattern(r.path,a);if(i!==null)return{route:r,params:i}}return null}normalizePathname(e){return e.length>1&&e.endsWith("/")?e.replace(/\/+$/,"")||"/":e}matchPattern(e,a){const r=[];let i=e.replace(/:([^/]+)/g,(f,g)=>(r.push(g),"([^/]+)"));i.startsWith("*/")&&(i="(?:/[^/]+)?"+i.slice(1)),i=i.replace(/\//g,"\\/");const l=new RegExp(`^${i}$`),u=a.match(l);if(!u)return null;const d={};return r.forEach((f,g)=>{d[f]=u[g+1]}),d}getRoutes(){return[...this.routes]}getAuthType(e,a){return e.auth_type?e.auth_type:a.startsWith("/admin")?"admin":"user"}async navigateToCurrentPath(){const e=window.location.pathname,a=window.location.search,r=this.match(e);if(!r){ss.warn(`No route matched for path: ${e}`),this.emit("routeNotFound",e);return}if(r.route.redirect){ss.log(`Redirecting from ${e} to ${r.route.redirect}`),this.navigate(r.route.redirect);return}if(r.route.auth_required){const u=this.getAuthType(r.route,e),d=ha.getInstance();let f=d.isAuthenticated()&&d.getAuthType()===u;if(f||(f=await d.checkAuth(u)),!f){const g=d.getLoginRedirectUrl(u,e+a);ss.log(`Not authenticated, redirecting to: ${g}`),window.location.href=g;return}}const i=new URLSearchParams(a),l={};for(const u of i.keys()){if(u in l)continue;const d=i.getAll(u);d.length>1?l[u]=d:d.length===1&&(u.endsWith("[]")?l[u]=d:l[u]=d[0])}await this.emit("routeChange",{path:e,layout:r.route.layout,endpoint:r.route.endpoint,params:{...r.route.params||{},...r.params},query:l,auth_required:r.route.auth_required,auth_type:r.route.auth_type,meta:r.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 a=this.pendingNavigation;this.executeNavigation(a)}}}}const B_=/^\/admin\/layout-editor\/([^/?#]+)\/?$/;function iy(o){const e=B_.exec(o);return e?{templateIdentifier:e[1]}:null}const zt=ct("ErrorPageHandler");class P_{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(La(`/api/templates/${this.templateId}/config`,"json"));if(!e.ok)return this.debug&&zt.warn("Failed to load template config:",e.statusText),this.configLoaded=!0,null;const a=await e.json();if(!a.success||!a.data)return this.debug&&zt.warn("Invalid template config response"),this.configLoaded=!0,null;const r=a.data;return!r.error_config||!r.error_config.layouts?(this.debug&&zt.warn("No error_config found in template.json"),this.configLoaded=!0,null):(this.errorConfig=r.error_config,this.configLoaded=!0,this.debug&&zt.log("Error config loaded:",this.error Line truncated
with(ctx) {
try {
return Boolean(${r});
@@ -232,7 +232,7 @@ Error generating stack: `+c.message+`
return false;
}
}
`)(a)}return!!e}catch(r){return B.warn(`Failed to evaluate script condition: ${e}`,r),!1}}showInitError(e){if(Ju()){B.warn("Init failed while document is unloading — skipping error screen",e);return}const a=Qm(e);rf.renderFromError("app",a,"초기화 실패",this.config.debug,{templateId:this.config.templateId,locale:this.config.locale},void 0,e)}showTransitionOverlay(e,a){const r=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(!r.enabled)return;if(this.hideTransitionOverlay(),r.style==="skeleton"&&r.skeleton?.component&&r.target&&a){this.renderSkeletonOverlay(r.target,r.skeleton,a,r.fallback_target);return}if(r.style==="spinner"&&r.target){this.renderSpinnerOverlay(r.target,r.spinner,r.fallback_target);return}const i=document.documentElement.classList.contains("dark");let l,u="";switch(r.style){case"blur":l=i?"rgba(17,24,39,0.3)":"rgba(255,255,255,0.3)",u="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(r.target){const d=`#${CSS.escape(r.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};${u}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 u.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,a,r,i){const u=da.getInstance().getComponent(a.component);if(!u){B.log(`Skeleton component "${a.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)",b=f==="fullpage"?"app":f==="fallback"?i:e,v=`#${CSS.escape(b)}`,S=document.createElement("style");S.id="g7-skeleton-overlay-style",S.textContent=`${v}{position:relative;z-index:0;}${v}::after{content:'';position:absolute;inset:0;background:${m};z-index:2147483646;pointer-events:none;}`,document.head.appendChild(S),this.transitionOverlayEl=S;const E=document.createElement("div");if(E.id="g7-skeleton-overlay",E.setAttribute("role","status"),E.setAttribute("aria-busy","true"),E.setAttribute("aria-label","Loading..."),f==="fullpage")E.style.cssText=["position:fixed","inset:0","z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";";else{const R=d.getBoundingClientRect(),D=window.scrollX||document.documentElement.scrollLeft,U=window.scrollY||document.documentElement.scrollTop;E.style.cssText=["position:absolute",`top:${R.top+U}px`,`left:${R.left+D}px`,`width:${R.width}px`,`height:${R.height}px`,"z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";"}document.body.appendChild(E),this.skeletonOverlayContainer=E;const A=r.components||[];let T;if(f==="fullpage")T=A;else{const R=f==="fallback"?i:e;T=this.findComponentChildrenById(A,R)}const L=Ku.createRoot(E);this.skeletonOverlayRoot=L,Er.flushSync(()=>{L.render(Ye.createElement(u,{components:T,options:{animation:a.animation||"pulse",iteration_count:a.iteration_count||5}}))});const M=f==="fullpage"?"fullpage (#app)":f==="fallback"?`fallback (#${i})`:`target (#${e})`;B.log(`Skeleton overlay rendered [${M}] with "${a.component}" (${T.length} components)`)}findComponentChildrenById(e,a){const r=i=>{for(const l of i){if(l.id===a)return l.children||[];if(l.children&&Array.isArray(l.children)){const u=r(l.children);if(u!==null)return u}}return null};return r(e)||e}renderSpinnerOverlay(e,a,r){let i=document.getElementById(e),l="target";if(!i&&r&&(i=document.getElementById(r),l="fallback"),i||(i=document.getElementById("app"),l="fullpage"),!i){B.log(`Spinner overlay: no target found (target="#${e}", fallback="${r||"none"}"), falling back to opaque overlay`),this. Line truncated
`)(a)}return!!e}catch(r){return B.warn(`Failed to evaluate script condition: ${e}`,r),!1}}showInitError(e){if(ed()){B.warn("Init failed while document is unloading — skipping error screen",e);return}const a=ry(e);lf.renderFromError("app",a,"초기화 실패",this.config.debug,{templateId:this.config.templateId,locale:this.config.locale},void 0,e)}showTransitionOverlay(e,a){const r=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(!r.enabled)return;if(this.hideTransitionOverlay(),r.style==="skeleton"&&r.skeleton?.component&&r.target&&a){this.renderSkeletonOverlay(r.target,r.skeleton,a,r.fallback_target);return}if(r.style==="spinner"&&r.target){this.renderSpinnerOverlay(r.target,r.spinner,r.fallback_target);return}const i=document.documentElement.classList.contains("dark");let l,u="";switch(r.style){case"blur":l=i?"rgba(17,24,39,0.3)":"rgba(255,255,255,0.3)",u="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(r.target){const d=`#${CSS.escape(r.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};${u}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 u.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,a,r,i){const u=da.getInstance().getComponent(a.component);if(!u){B.log(`Skeleton component "${a.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)",b=f==="fullpage"?"app":f==="fallback"?i:e,v=`#${CSS.escape(b)}`,S=document.createElement("style");S.id="g7-skeleton-overlay-style",S.textContent=`${v}{position:relative;z-index:0;}${v}::after{content:'';position:absolute;inset:0;background:${m};z-index:2147483646;pointer-events:none;}`,document.head.appendChild(S),this.transitionOverlayEl=S;const E=document.createElement("div");if(E.id="g7-skeleton-overlay",E.setAttribute("role","status"),E.setAttribute("aria-busy","true"),E.setAttribute("aria-label","Loading..."),f==="fullpage")E.style.cssText=["position:fixed","inset:0","z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";";else{const R=d.getBoundingClientRect(),D=window.scrollX||document.documentElement.scrollLeft,U=window.scrollY||document.documentElement.scrollTop;E.style.cssText=["position:absolute",`top:${R.top+U}px`,`left:${R.left+D}px`,`width:${R.width}px`,`height:${R.height}px`,"z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";"}document.body.appendChild(E),this.skeletonOverlayContainer=E;const A=r.components||[];let T;if(f==="fullpage")T=A;else{const R=f==="fallback"?i:e;T=this.findComponentChildrenById(A,R)}const L=Xu.createRoot(E);this.skeletonOverlayRoot=L,Ar.flushSync(()=>{L.render(Ye.createElement(u,{components:T,options:{animation:a.animation||"pulse",iteration_count:a.iteration_count||5}}))});const M=f==="fullpage"?"fullpage (#app)":f==="fallback"?`fallback (#${i})`:`target (#${e})`;B.log(`Skeleton overlay rendered [${M}] with "${a.component}" (${T.length} components)`)}findComponentChildrenById(e,a){const r=i=>{for(const l of i){if(l.id===a)return l.children||[];if(l.children&&Array.isArray(l.children)){const u=r(l.children);if(u!==null)return u}}return null};return r(e)||e}renderSpinnerOverlay(e,a,r){let i=document.getElementById(e),l="target";if(!i&&r&&(i=document.getElementById(r),l="fallback"),i||(i=document.getElementById("app"),l="fullpage"),!i){B.log(`Spinner overlay: no target found (target="#${e}", fallback="${r||"none"}"), falling back to opaque overlay`),this. Line truncated
with(ctx) {
try {
return ${e};
@@ -240,4 +240,5 @@ Error generating stack: `+c.message+`
return undefined;
}
}
`)(a)}catch(r){B.warn(`Expression evaluation failed: ${e}`,r);return}}async executeInitActions(e,a={}){const{getActionDispatcher:r}=await Promise.resolve().then(()=>Ha),i=r();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:",a),B.log("Init actions dataContext._global:",a._global);const l=e.map(d=>d.handler).filter(d=>d.includes("."));l.length>0&&await this.waitForHandlers(i,l);let u={...a};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,u),m=new Event("init");await g(m),B.log(`Init action executed: ${d.handler}`);const b=this.globalState;u={...u,_global:{...b},_local:b._local||u._local||{}},B.log(`Data context refreshed after ${d.handler}:`,{cartKey:u._global?.cartKey})}catch(f){B.error(`Failed to execute init action: ${d.handler}`,f)}}async waitForHandlers(e,a,r=5e3){const i=Date.now(),l=50,u=()=>a.every(f=>e.customHandlers?.has(f));if(u()){B.log("All module handlers already registered");return}const d=of().getFailedJsAssets();if(d.length>0){const f=d.some(b=>b==="module"||b==="plugin"),g=a.filter(b=>!e.customHandlers?.has(b)),m=g.length>0&&g.every(b=>d.some(v=>b.startsWith(`${v}.`)));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:",a),new Promise(f=>{const g=()=>{if(u()){B.log("All module handlers now registered"),f();return}if(Date.now()-i>=r){const m=a.filter(b=>!e.customHandlers?.has(b));B.warn("Timeout waiting for handlers:",m),f();return}setTimeout(g,l)};g()})}};$(Ca,"LOCALE_STORAGE_KEY","g7_locale"),$(Ca,"CACHE_VERSION_STORAGE_KEY","g7_cache_version");let mo=Ca;function cf(o){const e=new mo(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=cf,window.G7Core.dataSource={refetch:async(o,e)=>{const a=window.__templateApp;if(!a){B.warn("TemplateApp not initialized (G7Core.dataSource.refetch)");return}return a.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 z_(o={}){const{checked:e,value:a,name:r,type:i="checkbox"}=o,u=a!==void 0?a:i==="checkbox"||i==="radio"?e??!1:String(e??""),d={checked:e??!1,value:u,name:r??"",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 j_(o={}){const{button:e=0,clientX:a=0,clientY:r=0}=o;return{button:e,clientX:a,clientY:r,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 U_(){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 B_(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 q_={justify:/^justify-(start|end|center|between|around|evenly)$/,items:/^items-(start|end|center|baseline|stretch)$/,content:/^content-(start|end|center|between|around|evenly|stretch)$/,self:/^self-(auto|start|end|center|stretch|baseline)$/,flex:/^flex-(row|row-reverse|col|col-reverse|wrap|wrap-reverse|nowrap|1|auto|initial|none)$/,grow:/^(grow|grow-0)$/,shrink:/^(shrink|shrink-0)$/,basis:/^basis-/,order:/^(order-|-)order-/,gap:/^gap(-x|-y)?-/,gridCols:/^grid-cols-/,gridRows:/^grid-rows-/,colSpan:/^col-(span-|start-|end-)/,rowSpan:/^row-(span-|start-|end-)/,displa Line truncated
`)(a)}catch(r){B.warn(`Expression evaluation failed: ${e}`,r);return}}async executeInitActions(e,a={}){const{getActionDispatcher:r}=await Promise.resolve().then(()=>za),i=r();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:",a),B.log("Init actions dataContext._global:",a._global);const l=e.map(d=>d.handler).filter(d=>d.includes("."));l.length>0&&await this.waitForHandlers(i,l);let u={...a};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,u),m=new Event("init");await g(m),B.log(`Init action executed: ${d.handler}`);const b=this.globalState;u={...u,_global:{...b},_local:b._local||u._local||{}},B.log(`Data context refreshed after ${d.handler}:`,{cartKey:u._global?.cartKey})}catch(f){B.error(`Failed to execute init action: ${d.handler}`,f)}}async waitForHandlers(e,a,r=5e3){const i=Date.now(),l=50,u=()=>a.every(f=>e.customHandlers?.has(f));if(u()){B.log("All module handlers already registered");return}const d=uf().getFailedJsAssets();if(d.length>0){const f=d.some(b=>b==="module"||b==="plugin"),g=a.filter(b=>!e.customHandlers?.has(b)),m=g.length>0&&g.every(b=>d.some(v=>b.startsWith(`${v}.`)));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:",a),new Promise(f=>{const g=()=>{if(u()){B.log("All module handlers now registered"),f();return}if(Date.now()-i>=r){const m=a.filter(b=>!e.customHandlers?.has(b));B.warn("Timeout waiting for handlers:",m),f();return}setTimeout(g,l)};g()})}};$(Ca,"LOCALE_STORAGE_KEY","g7_locale"),$(Ca,"CACHE_VERSION_STORAGE_KEY","g7_cache_version");let vo=Ca;function ff(o){const e=new vo(o);return o.websocket&&(fc.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=ff,window.G7Core.dataSource={refetch:async(o,e)=>{const a=window.__templateApp;if(!a){B.warn("TemplateApp not initialized (G7Core.dataSource.refetch)");return}return a.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 K_(o={}){const{checked:e,value:a,name:r,type:i="checkbox"}=o,u=a!==void 0?a:i==="checkbox"||i==="radio"?e??!1:String(e??""),d={checked:e??!1,value:u,name:r??"",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 W_(o={}){const{button:e=0,clientX:a=0,clientY:r=0}=o;return{button:e,clientX:a,clientY:r,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 Y_(){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 X_(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 J_={justify:/^justify-(start|end|center|between|around|evenly)$/,items:/^items-(start|end|center|baseline|stretch)$/,content:/^content-(start|end|center|between|around|evenly|stretch)$/,self:/^self-(auto|start|end|center|stretch|baseline)$/,flex:/^flex-(row|row-reverse|col|col-reverse|wrap|wrap-reverse|nowrap|1|auto|initial|none)$/,grow:/^(grow|grow-0)$/,shrink:/^(shrink|shrink-0)$/,basis:/^basis-/,order:/^(order-|-)order-/,gap:/^gap(-x|-y)?-/,gridCols:/^grid-cols-/,gridRows:/^grid-rows-/,colSpan:/^col-(span-|start-|end-)/,rowSpan:/^row-(span-|start-|end-)/,displa Line truncated
//# sourceMappingURL=template-engine.min.js.map
+61 -30
View File
@@ -1,5 +1,8 @@
<?php
use App\Support\OpcacheStatus;
use App\Support\PrivilegedDatabaseAccounts;
/**
* G7 인스톨러 - 검증 통합 API
*
@@ -70,6 +73,7 @@ class ValidationApi
'required_files' => $this->checkRequiredFiles(),
'https' => $this->checkHttps(),
'opcache' => $this->checkOpcache(),
'asset_url_mode' => $this->checkAssetUrlMode(),
];
// 모든 필수 요구사항 통과 여부
@@ -141,7 +145,7 @@ class ValidationApi
// DB 최고권한 계정 차단 (Write/Read 공통).
// 최고권한 계정 자격증명이 유출되면 데이터베이스 전체가 위험해지므로
// 연결 시도 전에 막는다. 판정은 App\Support\PrivilegedDatabaseAccounts 가 SSoT.
if (\App\Support\PrivilegedDatabaseAccounts::isBlocked($username)) {
if (PrivilegedDatabaseAccounts::isBlocked($username)) {
throw new Exception(
lang('error_db_username_privileged', ['username' => $username])
);
@@ -190,7 +194,7 @@ class ValidationApi
// 기존 테이블 감지 (Write DB만 수행)
// 사용자가 입력한 db_prefix를 g7 시그니처에 적용하여 정확히 비교
$existingTables = null;
if (!$isReadDb) {
if (! $isReadDb) {
$detectPrefix = ($input['db_prefix'] ?? '') !== '' ? (string) $input['db_prefix'] : 'g7_';
$existingTables = checkExistingTables($pdo, $database, $detectPrefix);
}
@@ -526,7 +530,7 @@ class ValidationApi
*/
private function checkOpcache(): array
{
$status = \App\Support\OpcacheStatus::probe();
$status = OpcacheStatus::probe();
if ($status['enabled'] === null) {
$message = lang('opcache_unknown');
@@ -544,6 +548,27 @@ class ValidationApi
];
}
/**
* 자산 URL 방식 항목 정의 (권장 사항 — 설치를 차단하지 않음)
*
* 판정 자체는 **서버가 할 수 없다.** 서버에서 자기 APP_URL 로 curl 하면 loopback 이
* nginx vhost·SSL·프록시 체인을 우회하거나 다른 vhost 를 타서, 실제 방문자가 겪는
* 것과 다른 답을 낸다. 그래서 여기서는 프로브 경로만 내려주고 실제 판정은
* 브라우저가 수행한다 (`installer.js::probeAssetUrlMode`).
*
* `detected` 가 항상 null 인 것은 미구현이 아니라 위 이유에 따른 설계다.
* 프론트가 이 값을 받아 카드를 "확인 중" 상태로 먼저 그리고, 프로브가 끝나면 갱신한다.
*/
private function checkAssetUrlMode(): array
{
return [
'required' => false, // 어느 방식이든 정상 — 통과/실패 게이트가 아니다
'detected' => null, // 브라우저 프로브가 채운다 (서버 판정 불가)
'probe_extension' => '/api/system/asset-probe.js',
'probe_extensionless' => '/api/system/asset-probe',
];
}
/**
* 필수 PHP 함수(exec, proc_open) 비활성화 여부 검증
*/
@@ -601,7 +626,7 @@ class ValidationApi
$returnCode = -1;
exec('php --version 2>&1', $output, $returnCode);
if ($returnCode === 0 && !empty($output)) {
if ($returnCode === 0 && ! empty($output)) {
$outputStr = implode("\n", $output);
if (preg_match('/PHP\s+(\d+\.\d+\.\d+)/', $outputStr, $matches)) {
$cliVersion = $matches[1];
@@ -649,7 +674,7 @@ class ValidationApi
$minVersion = MIN_PHP_VERSION;
// PHP_BINARY 상수
if (defined('PHP_BINARY') && PHP_BINARY !== '' && !in_array(PHP_BINARY, $checkedPaths, true)) {
if (defined('PHP_BINARY') && PHP_BINARY !== '' && ! in_array(PHP_BINARY, $checkedPaths, true)) {
$checkedPaths[] = PHP_BINARY;
$result = $this->validatePhpPath(PHP_BINARY);
if ($result['valid']) {
@@ -663,7 +688,7 @@ class ValidationApi
continue;
}
$checkedPaths[] = $path;
if (!file_exists($path)) {
if (! file_exists($path)) {
continue;
}
$result = $this->validatePhpPath($path);
@@ -674,7 +699,7 @@ class ValidationApi
// 시스템 PATH의 'php'
$defaultPhpAvailable = false;
if (!in_array('php', $checkedPaths, true)) {
if (! in_array('php', $checkedPaths, true)) {
$result = $this->validatePhpPath('php');
if ($result['valid']) {
$found[] = ['path' => 'php', 'version' => $result['version']];
@@ -691,11 +716,11 @@ class ValidationApi
}
// Composer 자동 감지 (감지된 PHP 중 첫 번째로 검증)
$phpForComposer = !empty($found) ? $found[0]['path'] : 'php';
$phpForComposer = ! empty($found) ? $found[0]['path'] : 'php';
$composerResult = $this->detectComposerBinary($phpForComposer);
echo json_encode([
'success' => !empty($found),
'success' => ! empty($found),
'binaries' => $found,
'default_php_available' => $defaultPhpAvailable,
'composer' => $composerResult,
@@ -706,7 +731,7 @@ class ValidationApi
/**
* Composer 바이너리 자동 감지
*
* @param string $phpPath .phar 실행 시 사용할 PHP 경로
* @param string $phpPath .phar 실행 시 사용할 PHP 경로
* @return array{found: bool, path: string|null, version: string|null}
*/
private function detectComposerBinary(string $phpPath = 'php'): array
@@ -718,7 +743,7 @@ class ValidationApi
}
// 2. 프로젝트 루트의 composer.phar
$pharPath = realpath(__DIR__ . '/../../..') . '/composer.phar';
$pharPath = realpath(__DIR__.'/../../..').'/composer.phar';
if (file_exists($pharPath)) {
$result = $this->validateComposerPath($pharPath, $phpPath);
if ($result['valid']) {
@@ -727,7 +752,7 @@ class ValidationApi
}
// 3. 현재 디렉토리(public/install/api)의 composer.phar
$localPhar = realpath(__DIR__) . '/composer.phar';
$localPhar = realpath(__DIR__).'/composer.phar';
if (file_exists($localPhar)) {
$result = $this->validateComposerPath($localPhar, $phpPath);
if ($result['valid']) {
@@ -792,6 +817,7 @@ class ValidationApi
$path = $_GET['path'] ?? '';
if (empty($path)) {
echo json_encode(['success' => false, 'message' => lang('error_path_required')], JSON_UNESCAPED_UNICODE);
return;
}
@@ -804,22 +830,24 @@ class ValidationApi
'success' => false,
'message' => lang('error_core_pending_path_invalid'),
], JSON_UNESCAPED_UNICODE);
return;
}
$candidatePath = (str_starts_with($path, '/') || preg_match('#^[A-Za-z]:[/\\\\]#', $path) === 1)
? $path
: BASE_PATH . DIRECTORY_SEPARATOR . $path;
: BASE_PATH.DIRECTORY_SEPARATOR.$path;
$resolved = @realpath($candidatePath);
if ($resolved === false || !is_dir($resolved)) {
if ($resolved === false || ! is_dir($resolved)) {
// 존재 여부/타입 차이를 응답으로 분기하지 않고 단일 메시지로 통일하여
// 임의 경로 enumeration 신호 차단.
echo json_encode([
'success' => false,
'message' => lang('error_core_pending_path_invalid'),
], JSON_UNESCAPED_UNICODE);
return;
}
@@ -850,7 +878,7 @@ class ValidationApi
return false;
}
return !preg_match('/[\s;`$|<>"\'&\x00-\x1F]/', $path);
return ! preg_match('/[\s;`$|<>"\'&\x00-\x1F]/', $path);
}
/**
@@ -860,16 +888,16 @@ class ValidationApi
* `composer` 를 특정 PHP 로 실행하려는 운영 의도를 지원한다.
* 두 토큰 모두 isInstallerSafePathArg 통과해야 정상 입력으로 인정.
*
* @return array{php: string, composer: string}|null 분해 실패 시 null
* @return array{php: string, composer: string}|null 분해 실패 시 null
*/
private function splitPhpComposerTokens(string $path): ?array
{
if (!str_contains($path, ' ')) {
if (! str_contains($path, ' ')) {
return null;
}
$tokens = preg_split('/\s+/', trim($path), 2);
if (!is_array($tokens) || count($tokens) !== 2) {
if (! is_array($tokens) || count($tokens) !== 2) {
return null;
}
@@ -884,7 +912,7 @@ class ValidationApi
/**
* PHP 바이너리 경로 유효성 검증 헬퍼
*
* @param string $path PHP 바이너리 경로
* @param string $path PHP 바이너리 경로
* @return array{valid: bool, version: string|null, message: string}
*/
private function validatePhpPath(string $path): array
@@ -896,11 +924,11 @@ class ValidationApi
// 'php' 기본값이 아니면 셸 메타문자 차단. 파일 존재/실행 가능 검사는
// open_basedir 같은 PHP 런타임 제약 환경의 false negative 를 피하기 위해
// 생략하고, exec 결과로 최종 판정한다.
if ($path !== 'php' && !$this->isInstallerSafePathArg($path)) {
if ($path !== 'php' && ! $this->isInstallerSafePathArg($path)) {
return ['valid' => false, 'version' => null, 'message' => lang('error_php_exec_failed', ['path' => $path])];
}
$command = escapeshellarg($path) . ' --version 2>&1';
$command = escapeshellarg($path).' --version 2>&1';
$output = [];
$returnCode = -1;
exec($command, $output, $returnCode);
@@ -919,6 +947,7 @@ class ValidationApi
'message' => lang('success_php_binary_version', ['path' => $path, 'version' => $version]),
];
}
return [
'valid' => false,
'version' => $version,
@@ -932,8 +961,8 @@ class ValidationApi
/**
* Composer 바이너리 경로 유효성 검증 헬퍼
*
* @param string $composerPath Composer 바이너리 경로 (빈 문자열이면 시스템 'composer')
* @param string $phpPath PHP 바이너리 경로 (.phar 실행 시 사용)
* @param string $composerPath Composer 바이너리 경로 (빈 문자열이면 시스템 'composer')
* @param string $phpPath PHP 바이너리 경로 (.phar 실행 시 사용)
* @return array{valid: bool, version: string|null, message: string}
*/
private function validateComposerPath(string $composerPath, string $phpPath = 'php'): array
@@ -947,8 +976,8 @@ class ValidationApi
if ($effectivePath !== 'composer' && str_contains($effectivePath, ' ')) {
$tokens = $this->splitPhpComposerTokens($effectivePath);
if ($tokens === null
|| !$this->isInstallerSafePathArg($tokens['php'])
|| !$this->isInstallerSafePathArg($tokens['composer'])
|| ! $this->isInstallerSafePathArg($tokens['php'])
|| ! $this->isInstallerSafePathArg($tokens['composer'])
) {
return [
'valid' => false,
@@ -957,7 +986,7 @@ class ValidationApi
];
}
$command = escapeshellarg($tokens['php']) . ' ' . escapeshellarg($tokens['composer']) . ' --version 2>&1';
$command = escapeshellarg($tokens['php']).' '.escapeshellarg($tokens['composer']).' --version 2>&1';
$output = [];
$returnCode = -1;
@@ -971,6 +1000,7 @@ class ValidationApi
$outputStr = implode("\n", $output);
if (preg_match('/Composer\s+(?:version\s+)?(\d+\.\d+\.\d+)/', $outputStr, $matches)) {
$version = $matches[1];
return [
'valid' => true,
'version' => $version,
@@ -983,7 +1013,7 @@ class ValidationApi
// 단일 토큰 — 시스템 기본('composer') 가 아니면 셸 메타문자 차단.
// 파일 존재/실행 가능 검사는 open_basedir 환경의 false negative 회피를 위해 생략.
if ($effectivePath !== 'composer' && !$this->isInstallerSafePathArg($effectivePath)) {
if ($effectivePath !== 'composer' && ! $this->isInstallerSafePathArg($effectivePath)) {
return [
'valid' => false,
'version' => null,
@@ -994,16 +1024,16 @@ class ValidationApi
// .phar 파일이면 PHP 바이너리와 결합
if (str_ends_with($effectivePath, '.phar')) {
// phpPath 도 동일한 가드 — 'php' 기본값이 아니면 메타문자 없는 단일 토큰이어야 함
if ($phpPath !== 'php' && !$this->isInstallerSafePathArg($phpPath)) {
if ($phpPath !== 'php' && ! $this->isInstallerSafePathArg($phpPath)) {
return [
'valid' => false,
'version' => null,
'message' => lang('error_php_exec_failed', ['path' => $phpPath]),
];
}
$command = escapeshellarg($phpPath) . ' ' . escapeshellarg($effectivePath) . ' --version 2>&1';
$command = escapeshellarg($phpPath).' '.escapeshellarg($effectivePath).' --version 2>&1';
} else {
$command = escapeshellarg($effectivePath) . ' --version 2>&1';
$command = escapeshellarg($effectivePath).' --version 2>&1';
}
$output = [];
@@ -1020,6 +1050,7 @@ class ValidationApi
$outputStr = implode("\n", $output);
if (preg_match('/Composer\s+(?:version\s+)?(\d+\.\d+\.\d+)/', $outputStr, $matches)) {
$version = $matches[1];
return [
'valid' => true,
'version' => $version,
+161
View File
@@ -142,6 +142,10 @@
document.getElementById('requirements-result').innerHTML = resultHtml;
document.getElementById('navigation-buttons').classList.remove('hidden');
// 자산 URL 방식은 브라우저만 판정할 수 있어 카드 삽입 후 비동기로 채운다.
// await 하지 않는다 — 프로브가 늦거나 실패해도 요구사항 화면을 막지 않는다.
refreshAssetUrlModeCard();
// 버튼 표시 제어
const recheckBtn = document.getElementById('recheck-btn');
const nextBtn = document.getElementById('next-btn');
@@ -468,6 +472,20 @@
);
}
// 10. 자산 URL 방식 카드 (안내 항목, 필수 아님)
// 서버는 판정할 수 없으므로(loopback 이 vhost·프록시를 우회) "확인 중" 으로 먼저
// 그리고 refreshAssetUrlModeCard() 가 브라우저 프로브 결과로 갱신한다.
// failedRequirements.push() 를 하지 않는다 — 어느 방식이든 정상 동작이다.
if (data.asset_url_mode) {
html += `<div id="asset-url-mode-card">${renderSingleItemCard(
lang('asset_url_mode'),
'status-warning',
lang('asset_url_mode_checking'),
'',
false
)}</div>`;
}
html += '</div>';
// 필수 조건 미충족 시 에러 박스 표시
@@ -1554,11 +1572,154 @@
/**
* 실시간 검증 초기화
*/
/**
* 자산 URL 방식을 브라우저에서 판정한다 (이슈 #486 §6·§7).
*
* Step 2 요구사항 카드와 Step 3 설정 폼이 **같은 함수를 쓴다.** 판정 로직을 두 벌
* 두면 한쪽만 고쳐져 화면마다 다른 답을 내놓는다 (이 이슈에서 이미 URL 생성부가
* 그렇게 어긋났다).
*
* 정적 최적화 블록(`location ~* \.(js|css|json)$`)은 정규식 location 이라 프리픽스
* location 보다 먼저 매칭되고, 그 안에 PHP 핸들러가 없으면 확장자 붙은 동적 응답이
* PHP 에 도달하지 못한 채 404 가 된다. 설치 시점에 판정해 두지 않으면 설치 직후
* 첫 화면부터 백지가 된다.
*
* 판정은 **쌍으로** 던진다. 단일 프로브는 "PHP 자체가 죽음" 과 구분되지 않는다.
*
* probe.js 실패 + probe 성공 = 정적 블록 가로채기 확정 → extensionless
* 둘 다 성공 = extension
* 둘 다 실패 = PHP/라우팅 문제 (모드 문제 아님) → 판정 보류
*
* 성공 판정은 상태코드가 아니라 **본문의 매직 토큰**으로 한다. 상태코드만 보면
* "404 대신 200 + 에러 HTML" 이나 catch-all 200 페이지를 반환하는 설정에서
* 영원히 오판한다.
*/
async function probeAssetUrlMode(base) {
const TOKEN = 'G7_ASSET_PROBE_OK';
const root = (base || '').replace(/\/+$/, '');
/**
* 프로브 1건을 던져 매직 토큰 포함 여부를 반환한다.
*/
const probe = async (path) => {
try {
const res = await fetch(`${root}${path}`, { cache: 'no-store', credentials: 'omit' });
if (!res.ok) return false;
// Content-Type 도 함께 본다 (L6). 토큰만 검사해도 200+HTML 오판은
// 걸러지지만, 계획서는 두 신호를 모두 요구한다.
const contentType = res.headers.get('content-type') || '';
if (!/javascript|ecmascript/i.test(contentType)) return false;
return (await res.text()).includes(TOKEN);
} catch (e) {
return false;
}
};
const [withExt, withoutExt] = await Promise.all([
probe('/api/system/asset-probe.js'),
probe('/api/system/asset-probe'),
]);
if (withExt && withoutExt) {
return 'extension';
}
if (!withExt && withoutExt) {
return 'extensionless';
}
// 둘 다 실패 = 모드 문제가 아니다(PHP/라우팅 장애). 빈 문자열로 판정 보류.
return '';
}
/**
* Laravel 앱 루트 경로를 반환한다 (`INSTALLER_BASE_URL` 은 항상 `/install` 로 끝난다).
*
* Step 2 에는 `app_url` 입력이 아직 없으므로 현재 origin 기준으로 조립한다.
*/
function getAppRootUrl() {
const base = (window.INSTALLER_BASE_URL || '').replace(/\/install$/, '');
return `${window.location.origin}${base}`;
}
/**
* Step 2 요구사항 카드의 자산 URL 방식 항목을 프로브 결과로 갱신한다 (§7).
*
* 서버는 이 값을 판정할 수 없어(loopback 이 vhost·프록시를 우회) 카드가 먼저
* "확인 중" 으로 그려진 뒤 여기서 채워진다. 통과/실패 게이트가 아니라 안내 항목이라
* `failedRequirements` 에 넣지 않는다 — 어느 방식이든 정상 동작이다.
*/
async function refreshAssetUrlModeCard() {
const slot = document.getElementById('asset-url-mode-card');
if (!slot) return;
const detected = await probeAssetUrlMode(getAppRootUrl());
// 확장자 미사용은 "서버가 확장자 주소를 가로채고 있다" 는 신호이므로 눈에 띄게 둔다.
// 판정 불가도 마찬가지 — 설치 후 백지 화면의 예고일 수 있다.
const statusClass = detected === 'extension' ? 'status-pass' : 'status-warning';
const label = detected === 'extension'
? lang('asset_url_mode_extension')
: (detected === 'extensionless' ? lang('asset_url_mode_extensionless') : lang('asset_url_mode_unknown'));
// 경고 상태(확장자 미사용 자동 선택 / 확인 불가)는 "왜 그런지" 를 설명 문구로
// 함께 보인다 — 짧은 라벨("확인 불가")만으로는 어떤 상황인지 알 수 없다.
// "확인 불가" 는 두 프로브가 모두 실패한 상태라 에셋 방식 문제가 아니라
// 앱 미응답·프록시·CSP 등 다른 원인일 수 있으므로 그 내용을 상세히 안내한다.
let note = '';
if (detected === 'extensionless') {
note = lang('asset_url_mode_detected_extensionless');
} else if (detected === '') {
note = lang('asset_url_mode_detected_unavailable');
}
const noteHtml = note ? `<p class="fix-guide-hint">${note}</p>` : '';
// 카드 내부를 부분 수정하지 않고 통째로 다시 그린다 — 아이콘·수식자 클래스가
// renderSingleItemCard 안에서 statusClass 로 함께 결정되므로, 밖에서 일부만
// 건드리면 아이콘과 카드 색이 어긋난다.
slot.innerHTML = renderSingleItemCard(lang('asset_url_mode'), statusClass, label, '', false) + noteHtml;
}
async function detectAssetUrlMode() {
const field = document.getElementById('asset_url_mode');
if (!field) return;
const base = (document.querySelector('[name="app_url"]')?.value || '').replace(/\/+$/, '');
const detected = await probeAssetUrlMode(base);
field.value = detected;
// 수동 override — 감지 결과를 기본 선택으로 채우고 관리자가 바꿀 수 있게 한다.
// 프로브가 CSP/프록시 등으로 둘 다 실패하면 자동 판정이 불가하므로,
// 손댈 수단이 없으면 설치를 마친 뒤에야 문제를 알게 된다.
const select = document.getElementById('asset_url_mode_select');
const status = document.getElementById('asset_url_mode_status');
if (select) {
select.value = detected || 'extension';
select.addEventListener('change', function () {
field.value = this.value;
});
// 감지 실패 시에도 select 값이 hidden 에 반영되도록 초기 동기화
field.value = select.value;
}
if (status) {
status.textContent = status.getAttribute(
detected ? `data-msg-${detected}` : 'data-msg-unavailable'
) || '';
status.classList.remove('hidden');
}
}
function initRealTimeValidation() {
// Step 3 (config-form)이 있는 경우에만 실행
const configForm = document.getElementById('config-form');
if (!configForm) return;
// 자산 URL 방식 자동 감지 (비차단 — 실패해도 설치 진행에 지장 없음)
detectAssetUrlMode();
const fieldsToValidate = [
'app_name',
'app_url',
+7 -2
View File
@@ -218,9 +218,14 @@ if (!defined('DELETE_INSTALLER_AFTER_COMPLETE')) {
// 두 클래스는 프레임워크 의존성이 0 이라 이 방식이 성립하며, 인스톨러와 코어가
// 서로 다른 판정을 내리는 것을 구조적으로 차단한다.
// class_exists 가드: Laravel 부트가 선행된 테스트 환경에서 중복 로드 방지.
if (!class_exists('App\\Support\\PrivilegedDatabaseAccounts', false)) {
//
// 오토로드를 **막지 않는다**(2번째 인자 생략). `false` 를 주면 오토로드 가능한
// 클래스를 못 본 채 require 로 내려가는데, BASE_PATH 를 임시 디렉토리로 바꿔 두는
// 인스톨러 단위 테스트에서는 그 경로에 app/Support 가 없어 fatal 이 된다.
// 순수 인스톨러 실행 시에는 등록된 오토로더가 없어 false 를 반환하므로 require 가 그대로 돈다.
if (!class_exists('App\\Support\\PrivilegedDatabaseAccounts')) {
require_once BASE_PATH . '/app/Support/PrivilegedDatabaseAccounts.php';
}
if (!class_exists('App\\Support\\OpcacheStatus', false)) {
if (!class_exists('App\\Support\\OpcacheStatus')) {
require_once BASE_PATH . '/app/Support/OpcacheStatus.php';
}
+12 -1
View File
@@ -1,5 +1,7 @@
<?php
use App\Support\PrivilegedDatabaseAccounts;
/**
* 그누보드7 웹 인스톨러 요청 처리 핸들러
*
@@ -140,7 +142,7 @@ function validateDbUsername(string $username, string $field, array &$errors): vo
return;
}
if (\App\Support\PrivilegedDatabaseAccounts::isBlocked($username)) {
if (PrivilegedDatabaseAccounts::isBlocked($username)) {
$errors[$field] = lang('error_db_username_privileged', ['username' => $username]);
}
}
@@ -205,6 +207,15 @@ function handleStep3Post(string $currentLang, array &$formData, array &$errors):
}
$formData['vendor_mode'] = $vendorMode;
// 자산 URL 방식 (이슈 #486) — Step 3 의 브라우저 프로브가 채운 hidden 필드.
// 정적 최적화 블록이 있는 서버는 확장자 붙은 동적 응답이 PHP 에 도달하지 못하므로
// 설치 시점에 확장자 없는 형태로 확정해야 첫 화면부터 정상 동작한다.
// 판정 불가(프로브 실패·JS 미실행)면 키를 비워 defaults.json 기본값을 따르게 한다.
$assetUrlMode = trim($formData['asset_url_mode'] ?? '');
$formData['asset_url_mode'] = in_array($assetUrlMode, ['extension', 'extensionless'], true)
? $assetUrlMode
: '';
// 코어 업데이트 _pending 경로 검증 (입력된 경우만)
$corePendingPath = trim($formData['core_update_pending_path'] ?? '');
if ($corePendingPath !== '') {
+8
View File
@@ -1657,6 +1657,14 @@ if (! function_exists('createSettingsJsonSSE')) {
$defaults['general']['language'] = getCurrentLanguage();
// 자산 URL 방식 (이슈 #486) — 설치 화면이 브라우저에서 프로브를 던져 판정한 결과.
// 정적 최적화 블록(`location ~* \.(js|css|json)$`)이 있는 서버는 확장자 붙은
// 동적 응답이 PHP 에 도달하지 못하므로 확장자 없는 형태로 설치를 마쳐야 한다.
// 미판정(구버전 설치 화면·프로브 실패)이면 defaults.json 의 기본값을 그대로 둔다.
if (in_array($config['asset_url_mode'] ?? null, ['extension', 'extensionless'], true)) {
$defaults['general']['asset_url_mode'] = $config['asset_url_mode'];
}
foreach ($categories as $category) {
if (! isset($defaults[$category])) {
sendSSEEvent('log', ['message' => " - {$category}.json skipped (no defaults)"]);
+15 -4
View File
@@ -1,4 +1,5 @@
<?php
// /install/lang/en.php
return [
@@ -424,7 +425,7 @@ return [
'abort_rollback_success' => '[Aborted] Rollback completed: :message',
'abort_rollback_failed' => '[Aborted] Rollback failed: :message (continuing)',
'abort_no_rollback_needed' => '[Aborted] No rollback needed. (current_task is null or already completed)',
'abort_by_user' => "[Aborted] User aborted installation. (Current task: :task)",
'abort_by_user' => '[Aborted] User aborted installation. (Current task: :task)',
'abort_installation_stopped' => 'Installation aborted.',
// Worker Failed Task Rollback Messages
@@ -548,7 +549,7 @@ return [
'validation_incomplete_title' => 'Please complete the following items:',
'confirm_leave_page' => 'Settings have not been saved. Are you sure you want to leave this page?',
'installation_in_progress_alert' => 'Installation is in progress. Do you want to go back to the settings page?',
'confirm_go_to_settings' => "Do you want to go back to the settings page?",
'confirm_go_to_settings' => 'Do you want to go back to the settings page?',
'confirm_go_to_settings_simple' => "Do you want to go back to the settings page?\n\nInstallation state will be reset and all tasks will start from the beginning.\n\n⚠️ Database tables will NOT be deleted automatically.\nPlease clean up manually using phpMyAdmin if needed.",
'confirm_go_to_settings_title' => 'Go to Settings Page',
'confirm_go_to_settings_desc' => 'Installation state will be reset and all tasks will start from the beginning.\n\n⚠️ Database tables will NOT be deleted automatically. Please clean up manually using phpMyAdmin if needed.',
@@ -712,7 +713,7 @@ Firewalls or proxies may be blocking long-lived HTTP connections.',
'deselect_all' => 'Deselect All',
// install-worker.php i18n keys
'db_task_abort_detected_before_start' => '[DB Task] Abort detected before start - skipping task.',
'db_task_abort_detected_before_start' => '[DB Task] Abort detected before start - skipping task.',
'db_task_failed_rollback_start' => '[DB Task] :task failed - starting rollback.',
'db_task_abort_reason_connection' => 'Connection lost',
'db_task_abort_reason_user' => 'User requested',
@@ -831,6 +832,17 @@ Firewalls or proxies may be blocking long-lived HTTP connections.',
'core_update_settings' => 'Core Update Settings (Optional)',
'core_update_pending_path' => 'Update Pending Directory Path',
'core_update_pending_path_help' => 'Leave empty to use the default (storage/app/core_pending). Enter an absolute path or a path relative to the Gnuboard7 root to use a custom location.',
// 자산 URL 방식 (이슈 #486)
'asset_url_mode' => 'Asset file serving mode',
'asset_url_mode_extension' => 'Use file extensions (recommended)',
'asset_url_mode_extensionless' => 'No file extensions',
'asset_url_mode_help' => 'Detected automatically from your server setup. A static-file optimization rule (e.g. nginx location ~* \\.(js|css|json|png|jpg|jpeg|gif|ico|svg|woff2?)$) can intercept extension URLs and make pages fail to load; choose no extensions in that case.',
'asset_url_mode_detected_extension' => 'Detected: file extensions work fine on this server.',
'asset_url_mode_detected_extensionless' => 'Detected: a static-file optimization rule (e.g. nginx location ~* \\.(js|css|json|...)$) intercepts extension URLs, so no extensions was selected.',
'asset_url_mode_detected_unavailable' => 'Could not detect the serving mode — neither the extension nor the extensionless probe URL responded. This is usually not an asset-mode issue but a sign that the app is not yet responding at this URL (PHP/routing) or that a proxy or security rule (e.g. CSP) is blocking the probe requests. This item does not block installation, so you can continue; if pages look blank afterward, switch to no extensions under Admin > Settings > General.',
'asset_url_mode_checking' => 'Checking…',
'asset_url_mode_unknown' => 'Unknown',
'core_update_github_url' => 'GitHub Repository URL',
'core_update_github_url_help' => 'GitHub repository URL to check for core updates.',
'core_update_github_token' => 'GitHub Access Token',
@@ -915,4 +927,3 @@ Firewalls or proxies may be blocking long-lived HTTP connections.',
// Relative path alternative
'or_relative_path' => 'Or from the G7 root directory:',
];
?>
+15 -4
View File
@@ -1,4 +1,5 @@
<?php
// /install/lang/ko.php
return [
@@ -424,7 +425,7 @@ return [
'abort_rollback_success' => '[중단] 롤백 완료: :message',
'abort_rollback_failed' => '[중단] 롤백 실패: :message (계속 진행)',
'abort_no_rollback_needed' => '[중단] 롤백할 작업이 없습니다. (current_task가 null이거나 이미 완료됨)',
'abort_by_user' => "[중단] 사용자가 설치를 중단했습니다. (현재 작업: :task)",
'abort_by_user' => '[중단] 사용자가 설치를 중단했습니다. (현재 작업: :task)',
'abort_installation_stopped' => '설치가 중단되었습니다.',
// Worker 실패 시 롤백 관련 메시지
@@ -548,7 +549,7 @@ return [
'validation_incomplete_title' => '다음 항목을 완료해주세요:',
'confirm_leave_page' => '설정이 저장되지 않았습니다. 페이지를 나가시겠습니까?',
'installation_in_progress_alert' => '설치가 진행 중입니다. 설정 페이지로 돌아가시겠습니까?',
'confirm_go_to_settings' => "설정 페이지로 이동하시겠습니까?",
'confirm_go_to_settings' => '설정 페이지로 이동하시겠습니까?',
'confirm_go_to_settings_simple' => "설정 페이지로 이동하시겠습니까?\n\n설치 상태가 초기화되며, 모든 작업이 처음부터 다시 실행됩니다.\n\n⚠️ 데이터베이스에 생성된 테이블은 자동으로 삭제되지 않습니다.\n필요 시 phpMyAdmin 등을 통해 수동으로 정리해주세요.",
'confirm_go_to_settings_title' => '설정 페이지로 이동',
'confirm_go_to_settings_desc' => '설치 상태가 초기화되며, 모든 작업이 처음부터 다시 실행됩니다.\n\n⚠️ 데이터베이스에 생성된 테이블은 자동으로 삭제되지 않습니다. 필요 시 phpMyAdmin 등을 통해 수동으로 정리해주세요.',
@@ -712,7 +713,7 @@ ini_set(\'zlib.output_compression\', \'off\');
'deselect_all' => '전체 해제',
// install-worker.php 다국어 키
'db_task_abort_detected_before_start' => '[DB 작업] 시작 전 중단 상태 감지 - 작업을 건너뜁니다.',
'db_task_abort_detected_before_start' => '[DB 작업] 시작 전 중단 상태 감지 - 작업을 건너뜁니다.',
'db_task_failed_rollback_start' => '[DB 작업] :task 실패 - 롤백을 시작합니다.',
'db_task_abort_reason_connection' => '연결 끊김',
'db_task_abort_reason_user' => '사용자 요청',
@@ -831,6 +832,17 @@ ini_set(\'zlib.output_compression\', \'off\');
'core_update_settings' => '코어 업데이트 설정 (선택)',
'core_update_pending_path' => '업데이트 대기 디렉토리 경로',
'core_update_pending_path_help' => '비워두면 기본값(storage/app/core_pending)을 사용합니다. 외부 경로를 사용하려면 절대 경로 또는 그누보드7 루트 기준 상대 경로를 입력하세요.',
// 자산 URL 방식 (이슈 #486)
'asset_url_mode' => '에셋 파일 서빙 방식',
'asset_url_mode_extension' => '확장자 사용 (권장)',
'asset_url_mode_extensionless' => '확장자 미사용',
'asset_url_mode_help' => '서버 설정을 자동으로 감지해 선택합니다. 정적 파일 최적화 규칙(nginx의 location ~* \\.(js|css|json|png|jpg|jpeg|gif|ico|svg|woff2?)$ 블록 등)이 확장자 주소를 가로채면 화면이 뜨지 않는데, 그 경우 확장자 미사용을 선택하세요.',
'asset_url_mode_detected_extension' => '감지 결과: 확장자를 사용할 수 있는 환경입니다.',
'asset_url_mode_detected_extensionless' => '감지 결과: 정적 파일 최적화 규칙(nginx location ~* \\.(js|css|json|...)$ 블록 등)이 확장자 주소를 가로채고 있어 확장자 미사용을 선택했습니다.',
'asset_url_mode_detected_unavailable' => '확장자 있는 주소와 없는 주소 모두 응답하지 않아 서빙 방식을 감지하지 못했습니다. 에셋 방식 자체의 문제라기보다, 앱이 아직 이 주소에서 정상 응답하지 않거나(PHP·라우팅 설정) 프록시·보안 규칙(CSP 등)이 프로브 요청을 막고 있을 가능성이 큽니다. 이 항목은 설치를 막지 않으니 계속 진행할 수 있으며, 설치 후 화면이 비어 보이면 관리자 > 환경설정 > 일반에서 확장자 미사용으로 바꿔 보세요.',
'asset_url_mode_checking' => '확인 중…',
'asset_url_mode_unknown' => '확인 불가',
'core_update_github_url' => 'GitHub 저장소 URL',
'core_update_github_url_help' => '코어 업데이트를 확인할 GitHub 저장소 URL입니다.',
'core_update_github_token' => 'GitHub 액세스 토큰',
@@ -915,4 +927,3 @@ ini_set(\'zlib.output_compression\', \'off\');
// 상대경로 병기 안내
'or_relative_path' => '또는 그누보드7 루트 디렉토리에서:',
];
?>
+76 -56
View File
@@ -5,8 +5,7 @@
* 데이터베이스 설정, 사이트 설정, 관리자 계정을 입력받습니다.
* $formData는 index.php에서 준비됨
*/
if (!isset($errors)) {
if (! isset($errors)) {
$errors = [];
}
@@ -22,15 +21,15 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
<div class="installer-container installer-container-wide">
<h1 class="installer-title"><?= htmlspecialchars(lang('step_3_configuration')) ?></h1>
<?php if (!empty($errors)): ?>
<?php if (! empty($errors)) { ?>
<div class="alert alert-error">
<ul class="alert-list">
<?php foreach ($errors as $error): ?>
<?php foreach ($errors as $error) { ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
<?php } ?>
</ul>
</div>
<?php endif; ?>
<?php } ?>
<form method="POST" id="config-form" class="installer-form">
<!-- 데이터베이스 설정 (Write DB) -->
@@ -116,7 +115,7 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
<h3 class="requirement-card-title" style="margin: 0;"><?= htmlspecialchars(lang('use_read_db')) ?></h3>
</label>
<input type="checkbox" name="use_read_db" id="use-read-db" value="1"
<?= !empty($formData['use_read_db']) ? 'checked' : '' ?>
<?= ! empty($formData['use_read_db']) ? 'checked' : '' ?>
class="toggle-switch">
</div>
<div id="read-db-section" class="requirement-card-body <?= empty($formData['use_read_db']) ? 'hidden' : '' ?>" style="transition: all 0.3s ease-out;">
@@ -220,11 +219,11 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
<select name="admin_language" id="admin_language" class="form-select" required>
<?php
$selectedAdminLang = $formData['admin_language'] ?? getCurrentLanguage();
foreach (SUPPORTED_LANGUAGES as $code => $label): ?>
foreach (SUPPORTED_LANGUAGES as $code => $label) { ?>
<option value="<?= $code ?>" <?= $selectedAdminLang === $code ? 'selected' : '' ?>>
<?= htmlspecialchars($label) ?>
</option>
<?php endforeach; ?>
<?php } ?>
</select>
</div>
@@ -266,6 +265,21 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
</p>
</div>
<div class="form-group">
<label class="form-label"><?= htmlspecialchars(lang('asset_url_mode')) ?></label>
<select name="asset_url_mode_select" id="asset_url_mode_select" class="form-select">
<option value="extension"><?= htmlspecialchars(lang('asset_url_mode_extension')) ?></option>
<option value="extensionless"><?= htmlspecialchars(lang('asset_url_mode_extensionless')) ?></option>
</select>
<p id="asset_url_mode_status" class="form-help hidden"
data-msg-extension="<?= htmlspecialchars(lang('asset_url_mode_detected_extension')) ?>"
data-msg-extensionless="<?= htmlspecialchars(lang('asset_url_mode_detected_extensionless')) ?>"
data-msg-unavailable="<?= htmlspecialchars(lang('asset_url_mode_detected_unavailable')) ?>"></p>
<p class="form-help">
<?= htmlspecialchars(lang('asset_url_mode_help')) ?>
</p>
</div>
<div class="form-group">
<label class="form-label"><?= htmlspecialchars(lang('core_update_github_url')) ?></label>
<input type="url" name="core_update_github_url"
@@ -307,7 +321,7 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
<h3 class="requirement-card-title" style="margin: 0;" id="php-cli-title"><?= htmlspecialchars(lang('php_cli_settings')) ?></h3>
</label>
<input type="checkbox" id="show-php-cli-settings" value="1"
<?= (!empty($formData['php_binary']) && $formData['php_binary'] !== 'php') || !empty($formData['composer_binary']) ? 'checked' : '' ?>
<?= (! empty($formData['php_binary']) && $formData['php_binary'] !== 'php') || ! empty($formData['composer_binary']) ? 'checked' : '' ?>
class="toggle-switch">
</div>
<div id="php-cli-section" class="requirement-card-body <?= (empty($formData['php_binary']) || $formData['php_binary'] === 'php') && empty($formData['composer_binary']) ? 'hidden' : '' ?>" style="transition: all 0.3s ease-out;">
@@ -384,14 +398,14 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
<!-- ========== Vendor 설치 방식 ========== -->
<?php
$bundleZipExists = file_exists(BASE_PATH . '/vendor-bundle.zip');
$zipArchiveAvailable = class_exists('ZipArchive');
$procOpenAvailable = function_exists('proc_open')
&& !in_array('proc_open', array_map('trim', explode(',', (string) ini_get('disable_functions'))), true);
$bundleAvailable = $bundleZipExists && $zipArchiveAvailable;
$composerAvailable = $procOpenAvailable;
$currentVendorMode = $formData['vendor_mode'] ?? 'auto';
?>
$bundleZipExists = file_exists(BASE_PATH.'/vendor-bundle.zip');
$zipArchiveAvailable = class_exists('ZipArchive');
$procOpenAvailable = function_exists('proc_open')
&& ! in_array('proc_open', array_map('trim', explode(',', (string) ini_get('disable_functions'))), true);
$bundleAvailable = $bundleZipExists && $zipArchiveAvailable;
$composerAvailable = $procOpenAvailable;
$currentVendorMode = $formData['vendor_mode'] ?? 'auto';
?>
<div class="requirement-card">
<div class="requirement-card-header">
<h2 class="card-title"><?= htmlspecialchars(lang('vendor_mode_title')) ?></h2>
@@ -437,15 +451,15 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
<p class="vendor-mode-card-description">
<?= htmlspecialchars(lang('vendor_mode_composer_description')) ?>
</p>
<?php if (!$composerAvailable): ?>
<?php if (! $composerAvailable) { ?>
<p class="vendor-mode-card-status vendor-mode-card-status-error">
<?= getSvgIcon('warning') ?: '⚠' ?> <?= htmlspecialchars(lang('vendor_mode_composer_status_blocked')) ?>
</p>
<?php else: ?>
<?php } else { ?>
<p class="vendor-mode-card-status vendor-mode-card-status-ok">
<?= getSvgIcon('check') ?: '✓' ?> <?= htmlspecialchars(lang('vendor_mode_composer_status_ok')) ?>
</p>
<?php endif; ?>
<?php } ?>
</label>
<!-- 번들 Vendor 사용 -->
@@ -464,19 +478,19 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
<p class="vendor-mode-card-description">
<?= htmlspecialchars(lang('vendor_mode_bundled_description')) ?>
</p>
<?php if (!$zipArchiveAvailable): ?>
<?php if (! $zipArchiveAvailable) { ?>
<p class="vendor-mode-card-status vendor-mode-card-status-error">
<?= getSvgIcon('warning') ?: '⚠' ?> <?= htmlspecialchars(lang('vendor_mode_bundled_status_no_ziparchive')) ?>
</p>
<?php elseif (!$bundleZipExists): ?>
<?php } elseif (! $bundleZipExists) { ?>
<p class="vendor-mode-card-status vendor-mode-card-status-error">
<?= getSvgIcon('warning') ?: '⚠' ?> <?= htmlspecialchars(lang('vendor_mode_bundled_status_no_zip')) ?>
</p>
<?php else: ?>
<?php } else { ?>
<p class="vendor-mode-card-status vendor-mode-card-status-ok">
<?= getSvgIcon('check') ?: '✓' ?> <?= htmlspecialchars(lang('vendor_mode_bundled_status_ok')) ?>
</p>
<?php endif; ?>
<?php } ?>
</label>
</div>
<p class="form-help" style="margin-top: var(--spacing-md);">
@@ -502,6 +516,12 @@ $dbReadHash = getDatabaseFieldHash($formData, 'db_read');
</div>
<!-- 네비게이션 -->
<?php /* 자산 URL 방식 (이슈 #486) — 아래 스크립트가 브라우저에서 프로브를 던져 채운다.
서버측에서 자기 APP_URL 로 curl 하면 loopback 이 vhost·프록시 체인을 우회해
오판하므로 반드시 브라우저가 판정해야 한다. 판정 실패 시 빈 값으로 남고
설치는 defaults.json 기본값(extension)으로 진행된다. */ ?>
<input type="hidden" name="asset_url_mode" id="asset_url_mode" value="">
<div class="btn-group btn-group-spread">
<button type="button" onclick="goToStep(2)" class="btn btn-secondary">
<?= htmlspecialchars(lang('previous')) ?>
@@ -588,18 +608,18 @@ window.CliValidator = {
summary.style.display = '';
phpStatus.textContent = this.phpVerified
? '<?= lang("cli_status_verified") ?>'
: '<?= lang("cli_status_not_verified") ?>';
? '<?= lang('cli_status_verified') ?>'
: '<?= lang('cli_status_not_verified') ?>';
phpStatus.style.color = this.phpVerified ? 'var(--success-color)' : 'var(--error-color)';
const composerOptional = !this.isComposerRequired();
if (composerOptional) {
composerStatus.textContent = '<?= lang("cli_status_optional_bundled") ?>';
composerStatus.textContent = '<?= lang('cli_status_optional_bundled') ?>';
composerStatus.style.color = 'var(--text-muted-color, #888)';
} else {
composerStatus.textContent = this.composerVerified
? '<?= lang("cli_status_verified") ?>'
: '<?= lang("cli_status_not_verified") ?>';
? '<?= lang('cli_status_verified') ?>'
: '<?= lang('cli_status_not_verified') ?>';
composerStatus.style.color = this.composerVerified ? 'var(--success-color)' : 'var(--error-color)';
}
},
@@ -663,8 +683,8 @@ async function initCliDetection() {
const summary = document.getElementById('cli-status-summary');
if (summary) summary.style.display = '';
if (phpStatus) { phpStatus.textContent = '<?= lang("cli_status_checking") ?>'; phpStatus.style.color = ''; }
if (composerStatus) { composerStatus.textContent = '<?= lang("cli_status_checking") ?>'; composerStatus.style.color = ''; }
if (phpStatus) { phpStatus.textContent = '<?= lang('cli_status_checking') ?>'; phpStatus.style.color = ''; }
if (composerStatus) { composerStatus.textContent = '<?= lang('cli_status_checking') ?>'; composerStatus.style.color = ''; }
// PHP 감지 먼저 실행 (composer 감지 결과 포함) → Composer 검증
const composerData = await detectAndVerifyPhp();
@@ -736,10 +756,10 @@ function switchToRequiredMode() {
window.CliValidator.cliRequired = true;
const title = document.getElementById('php-cli-title');
if (title) title.textContent = '<?= lang("php_cli_settings_required") ?>';
if (title) title.textContent = '<?= lang('php_cli_settings_required') ?>';
const helpText = document.getElementById('php-cli-help-text');
if (helpText) helpText.textContent = '<?= lang("php_cli_settings_help_required") ?>';
if (helpText) helpText.textContent = '<?= lang('php_cli_settings_help_required') ?>';
// 섹션 강제 오픈 + 토글 비활성화
const section = document.getElementById('php-cli-section');
@@ -760,7 +780,7 @@ async function testPhpBinary() {
const resultDiv = document.getElementById('php-binary-test-result');
resultDiv.className = 'test-result';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang("checking") ?>';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang('checking') ?>';
try {
const response = await fetch('api/check-configuration.php?action=test-php-binary', {
@@ -781,7 +801,7 @@ async function testPhpBinary() {
}
} catch (e) {
resultDiv.className = 'test-result test-error';
resultDiv.innerHTML = '<?= lang("error_check_failed") ?>';
resultDiv.innerHTML = '<?= lang('error_check_failed') ?>';
window.CliValidator.setPhpVerified(false);
}
}
@@ -793,7 +813,7 @@ async function testComposer() {
const resultDiv = document.getElementById('composer-test-result');
resultDiv.className = 'test-result';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang("composer_checking") ?>';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang('composer_checking') ?>';
try {
const response = await fetch('api/check-configuration.php?action=test-composer', {
@@ -818,7 +838,7 @@ async function testComposer() {
}
} catch (e) {
resultDiv.className = 'test-result test-error';
resultDiv.innerHTML = '<?= lang("error_check_failed") ?>';
resultDiv.innerHTML = '<?= lang('error_check_failed') ?>';
window.CliValidator.setComposerVerified(false);
showComposerInstallGuide();
}
@@ -847,40 +867,40 @@ function showComposerInstallGuide() {
let html = '';
// 표준 설치 안내 (항상 표시)
html += '<p class="fix-guide-label"><?= lang("composer_install_guide_global") ?></p>';
html += '<p class="fix-guide-label"><?= lang('composer_install_guide_global') ?></p>';
html += '<div class="code-box">';
html += '<pre class="fix-command">curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer</pre>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang("copy_command") ?></button>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang('copy_command') ?></button>';
html += '</div>';
html += '<p class="fix-guide-label" style="margin-top: var(--spacing-sm);"><?= lang("composer_install_guide_local") ?></p>';
html += '<p class="fix-guide-label" style="margin-top: var(--spacing-sm);"><?= lang('composer_install_guide_local') ?></p>';
html += '<div class="code-box">';
html += '<pre class="fix-command">curl -sS https://getcomposer.org/installer | php</pre>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang("copy_command") ?></button>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang('copy_command') ?></button>';
html += '</div>';
// 호스팅 환경 안내 (항상 표시 — 표준 방법 실패 시 대안)
html += '<p class="fix-guide-label" style="margin-top: var(--spacing-md);"><?= lang("composer_install_guide_hosting") ?></p>';
html += '<p class="fix-guide-label" style="margin-top: var(--spacing-md);"><?= lang('composer_install_guide_hosting') ?></p>';
html += '<div class="code-box">';
html += '<pre class="fix-command">curl -o composer-setup.php https://getcomposer.org/installer</pre>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang("copy_command") ?></button>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang('copy_command') ?></button>';
html += '</div>';
html += '<div class="code-box" style="margin-top: var(--spacing-xs);">';
html += '<pre class="fix-command">' + escapeHtml(phpPath) + ' -d allow_url_fopen=On composer-setup.php</pre>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang("copy_command") ?></button>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang('copy_command') ?></button>';
html += '</div>';
html += '<p class="fix-guide-hint" style="margin-top: var(--spacing-sm);"><?= lang("composer_install_guide_pwd_hint") ?></p>';
html += '<p class="fix-guide-hint" style="margin-top: var(--spacing-sm);"><?= lang('composer_install_guide_pwd_hint') ?></p>';
html += '<div class="code-box">';
html += '<pre class="fix-command">pwd</pre>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang("copy_command") ?></button>';
html += '<button type="button" class="btn-copy" onclick="copyCliCommand(this)"><?= lang('copy_command') ?></button>';
html += '</div>';
html += '<p class="fix-guide-hint" style="margin-top: var(--spacing-sm);"><?= lang("composer_install_guide_phar_hint") ?></p>';
html += '<p class="fix-guide-hint" style="margin-top: var(--spacing-sm);"><?= lang('composer_install_guide_phar_hint') ?></p>';
html += '<p class="fix-guide-hint" style="margin-top: var(--spacing-sm);"><?= lang("composer_install_guide_link") ?></p>';
html += '<p class="fix-guide-hint" style="margin-top: var(--spacing-sm);"><?= lang('composer_install_guide_link') ?></p>';
commandsDiv.innerHTML = html;
guide.style.display = '';
@@ -903,7 +923,7 @@ function showDetectResult(data) {
if (!resultDiv) return;
if (data.success && data.binaries && data.binaries.length > 0) {
let html = '<strong>' + '<?= lang("detected_php_binaries") ?>' + '</strong><ul style="margin: var(--spacing-xs) 0; padding-left: var(--spacing-lg);">';
let html = '<strong>' + '<?= lang('detected_php_binaries') ?>' + '</strong><ul style="margin: var(--spacing-xs) 0; padding-left: var(--spacing-lg);">';
data.binaries.forEach(function(bin) {
html += '<li><a href="#" onclick="selectPhpBinary(\'' + bin.path.replace(/'/g, "\\'") + '\'); return false;" style="cursor: pointer;">'
+ bin.path + '</a> — PHP ' + bin.version + '</li>';
@@ -913,7 +933,7 @@ function showDetectResult(data) {
resultDiv.innerHTML = html;
} else {
resultDiv.className = 'test-result test-error';
resultDiv.innerHTML = data.message || '<?= lang("no_php_detected") ?>';
resultDiv.innerHTML = data.message || '<?= lang('no_php_detected') ?>';
}
}
@@ -935,7 +955,7 @@ async function detectPhpBinaries() {
const resultDiv = document.getElementById('php-detect-result');
resultDiv.className = 'test-result';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang("detecting_php") ?>';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang('detecting_php') ?>';
try {
const response = await fetch('api/check-configuration.php?action=detect-php');
@@ -947,7 +967,7 @@ async function detectPhpBinaries() {
}
} catch (e) {
resultDiv.className = 'test-result test-error';
resultDiv.innerHTML = '<?= lang("error_check_failed") ?>';
resultDiv.innerHTML = '<?= lang('error_check_failed') ?>';
}
}
@@ -958,7 +978,7 @@ function copyCliCommand(btn) {
const command = btn.previousElementSibling.textContent;
navigator.clipboard.writeText(command).then(function() {
const originalText = btn.textContent;
btn.textContent = '<?= lang("copied") ?>';
btn.textContent = '<?= lang('copied') ?>';
setTimeout(function() { btn.textContent = originalText; }, 2000);
});
}
@@ -990,7 +1010,7 @@ async function checkCorePendingPath() {
if (!path) return;
resultDiv.className = 'test-result';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang("checking") ?>';
resultDiv.innerHTML = '<span class="loading-spinner"></span> ' + '<?= lang('checking') ?>';
try {
const response = await fetch('api/check-configuration.php?action=check-core-pending-path&path=' + encodeURIComponent(path));
@@ -1005,7 +1025,7 @@ async function checkCorePendingPath() {
}
} catch (e) {
resultDiv.className = 'test-result test-error';
resultDiv.innerHTML = '<?= lang("error_check_failed") ?>';
resultDiv.innerHTML = '<?= lang('error_check_failed') ?>';
}
}
</script>
+5 -4
View File
@@ -29,6 +29,7 @@ import { webSocketManager } from './websocket/WebSocketManager';
import { getModuleAssetLoader, parseModuleAssetsFromConfig, parsePluginAssetsFromConfig, parseBundleUrlsFromConfig } from './modules';
import { SystemBannerManager } from './template-engine/SystemBannerManager';
import { fetchWithRetry, installUnloadGuard, isDocumentUnloading } from './template-engine/networkResilience';
import { suffixed } from './support/assetUrl';
import { resetLocalInitTracking } from './template-engine/localInitSlot';
/**
* DevTools 추적 - G7DevToolsCore.getInstance() 직접 호출 대신 G7Core.devTools를 사용합니다.
@@ -472,7 +473,7 @@ export class TemplateApp {
// routes.json 로딩 (저장된 캐시 버전 사용)
// 네트워크 일시 실패(응답 없음)에만 재시도한다. HTTP 에러는 아래 체인이 종전대로 throw.
fetchWithRetry(
`/api/templates/${this.config.templateId}/routes.json${storedCacheVersion > 0 ? `?v=${storedCacheVersion}` : ''}`,
suffixed(`/api/templates/${this.config.templateId}/routes`, 'json', storedCacheVersion > 0 ? storedCacheVersion : null),
{ label: 'routes.json' }
)
.then(response => {
@@ -494,7 +495,7 @@ export class TemplateApp {
// 사용자 정보 프리로드 (에러 발생 시 무시)
authManager.preloadAuth(this.config.templateType === 'admin' ? 'admin' : 'user'),
// 템플릿 config.json 로딩 (errorHandling 파싱)
fetch(`/api/templates/${this.config.templateId}/config.json`)
fetch(suffixed(`/api/templates/${this.config.templateId}/config`, 'json'))
.then(response => {
if (!response.ok) {
// config.json 로드 실패는 무시 (선택적)
@@ -532,7 +533,7 @@ export class TemplateApp {
logger.log('Cache version changed, reloading routes...');
// routes.json을 새 캐시 버전으로 다시 로드
const newRoutesData = await fetchWithRetry(
`/api/templates/${this.config.templateId}/routes.json?v=${this.extensionCacheVersion}`,
suffixed(`/api/templates/${this.config.templateId}/routes`, 'json', this.extensionCacheVersion),
{ label: 'routes.json (reload)' }
)
.then(response => {
@@ -2807,7 +2808,7 @@ export class TemplateApp {
let newVersion: number | undefined;
try {
const configResponse = await fetch(
`/api/templates/${this.config.templateId}/config.json?_=${Date.now()}`
suffixed(`/api/templates/${this.config.templateId}/config`, 'json', null, `_=${Date.now()}`)
);
if (configResponse.ok) {
const configResult = await configResponse.json();
+15 -5
View File
@@ -8,6 +8,7 @@
import { createLogger } from '../utils/Logger';
import { loadScriptWithRetry } from '../template-engine/networkResilience';
import { convertToCurrentMode } from '../support/assetUrl';
const logger = createLogger('ModuleAssetLoader');
@@ -452,8 +453,8 @@ export function parseModuleAssetsFromConfig(): ModuleAsset[] {
moduleAssets.push({
identifier,
js: typedAsset.js,
css: typedAsset.css,
js: typedAsset.js ? convertToCurrentMode(typedAsset.js) : typedAsset.js,
css: typedAsset.css ? convertToCurrentMode(typedAsset.css) : typedAsset.css,
priority: typedAsset.priority,
external: typedAsset.external,
});
@@ -495,7 +496,16 @@ export function parseBundleUrlsFromConfig(): ExtensionBundleUrls | null {
return null;
}
return g7Config.bundleUrls as ExtensionBundleUrls;
// 서버가 확장자 형태로 굳혀 내려준 URL 을 현재 모드로 변환한다.
// 부트스트랩 자가 복구가 런타임에 모드를 뒤집은 경우 원본은 이미 옛 형태다.
const urls = g7Config.bundleUrls as ExtensionBundleUrls;
return {
moduleJs: urls.moduleJs ? convertToCurrentMode(urls.moduleJs) : urls.moduleJs,
moduleCss: urls.moduleCss ? convertToCurrentMode(urls.moduleCss) : urls.moduleCss,
pluginJs: urls.pluginJs ? convertToCurrentMode(urls.pluginJs) : urls.pluginJs,
pluginCss: urls.pluginCss ? convertToCurrentMode(urls.pluginCss) : urls.pluginCss,
} as ExtensionBundleUrls;
}
/**
@@ -525,8 +535,8 @@ export function parsePluginAssetsFromConfig(): ModuleAsset[] {
pluginAssets.push({
identifier,
js: typedAsset.js,
css: typedAsset.css,
js: typedAsset.js ? convertToCurrentMode(typedAsset.js) : typedAsset.js,
css: typedAsset.css ? convertToCurrentMode(typedAsset.css) : typedAsset.css,
priority: typedAsset.priority,
external: typedAsset.external,
});
+29 -5
View File
@@ -1,5 +1,6 @@
import { AuthManager } from '../auth/AuthManager';
import { createLogger } from '../utils/Logger';
import { suffixed } from '../support/assetUrl';
const logger = createLogger('Router');
@@ -107,10 +108,10 @@ export class Router {
*/
async loadRoutes(cacheVersion?: number): Promise<void> {
try {
const versionQuery = cacheVersion !== undefined && cacheVersion > 0
? `?v=${cacheVersion}`
: '';
const response = await fetch(`/api/templates/${this.templateIdentifier}/routes.json${versionQuery}`);
const version = cacheVersion !== undefined && cacheVersion > 0 ? cacheVersion : null;
const response = await fetch(
suffixed(`/api/templates/${this.templateIdentifier}/routes`, 'json', version),
);
if (!response.ok) {
throw new Error(`Failed to load routes: ${response.statusText}`);
@@ -152,8 +153,10 @@ export class Router {
* @returns 매칭된 라우트와 파라미터, 없으면 null
*/
match(pathname: string): RouteMatch | null {
const normalizedPath = this.normalizePathname(pathname);
for (const route of this.routes) {
const params = this.matchPattern(route.path, pathname);
const params = this.matchPattern(route.path, normalizedPath);
if (params !== null) {
return {
@@ -166,6 +169,27 @@ export class Router {
return null;
}
/**
* URL 경로의 끝 슬래시를 정규화합니다.
*
* 브라우저가 `/admin/` 처럼 끝 슬래시가 붙은 경로로 진입해도 끝 슬래시 없는
* 라우트 패턴(언어 prefix 형태 포함)에 매칭되도록, 루트(`/`)를 제외한 나머지
* 경로의 끝 슬래시를 제거한다. 라우트 패턴은 끝 슬래시 없는 형태로만 정의되므로
* (routes.json 규약), 미정규화 시 `/admin/` 이 어떤 라우트에도 매칭되지 않아
* 404 로 떨어지고 — 미인증 진입 시 로그인 리다이렉트 대신 404 가 노출된다.
*
* @param pathname 원본 URL 경로
* @returns 끝 슬래시가 정규화된 경로 (루트는 `/` 유지)
* @since engine-v1.54.1
*/
private normalizePathname(pathname: string): string {
if (pathname.length > 1 && pathname.endsWith('/')) {
return pathname.replace(/\/+$/, '') || '/';
}
return pathname;
}
/**
* 패턴과 경로를 매칭하고 파라미터를 추출합니다.
*
@@ -192,6 +192,50 @@ describe('Router', () => {
expect(result?.route.layout).toBe('user-list');
expect(result?.params).toEqual({});
});
// 브라우저가 끝 슬래시가 붙은 경로(`/admin/`)로 진입해도, 끝 슬래시 없는
// 라우트 패턴(`/admin`)에 매칭되어야 한다. 미정규화 시 어떤 라우트에도
// 매칭되지 않아 404 로 떨어지고, 미인증 진입 시 로그인 리다이렉트 대신
// 404 가 노출되는 회귀가 발생한다.
it('끝 슬래시가 붙은 경로도 끝 슬래시 없는 라우트에 매칭해야 합니다', () => {
const result = router.match('/admin/');
expect(result).not.toBeNull();
expect(result?.route.layout).toBe('dashboard');
expect(result?.params).toEqual({});
});
it('끝 슬래시가 붙은 하위 경로도 매칭해야 합니다', () => {
const result = router.match('/admin/users/');
expect(result).not.toBeNull();
expect(result?.route.layout).toBe('user-list');
expect(result?.params).toEqual({});
});
it('끝 슬래시가 붙은 동적 파라미터 경로도 매칭하고 파라미터를 추출해야 합니다', () => {
const result = router.match('/admin/users/123/');
expect(result).not.toBeNull();
expect(result?.route.layout).toBe('user-detail');
expect(result?.params).toEqual({ id: '123' });
});
it('연속된 끝 슬래시도 정규화해야 합니다', () => {
const result = router.match('/admin//');
expect(result).not.toBeNull();
expect(result?.route.layout).toBe('dashboard');
});
it('루트 경로(/)는 끝 슬래시를 제거하지 않아야 합니다', () => {
// 루트만 있는 라우트를 임시로 주입해 루트 정규화 안전성을 검증한다.
router.setRoutes([{ path: '/', layout: 'home' }]);
const result = router.match('/');
expect(result).not.toBeNull();
expect(result?.route.layout).toBe('home');
});
});
describe('matchPattern (간접 테스트)', () => {
@@ -0,0 +1,223 @@
/**
* 자산 URL 빌더(프론트) 가드 — 이슈 #486 단위 C.
*
* 단위 C 의 회귀 조건은 "기본 모드에서 치환 이전 URL 문자열과 바이트 동일" 이다.
* 아래 확장자 모드 기대값은 치환 이전 소스에 하드코딩되어 있던 문자열을 그대로 옮긴 것으로,
* 빌더 도입이 URL 을 한 글자도 바꾸지 않았음을 고정한다.
*
* 서버측 `App\Support\AssetUrl` 와 규칙이 일치해야 한다 — 한쪽만 바뀌면 그 자산만 404 가 된다.
*/
// e2e:allow 순수 URL 문자열 생성 유닛. 브라우저 거동은 단위 D(자가 복구) spec 이 커버한다.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
MODE_EXTENSION,
MODE_EXTENSIONLESS,
getAssetUrlMode,
isExtensionless,
setAssetUrlMode,
restoreCachedMode,
templateAsset,
moduleAsset,
pluginAsset,
extensionBundle,
suffixed,
layoutUrl,
layoutPreviewUrl,
} from '../assetUrl';
/**
* G7Config 를 지정 모드로 초기화합니다.
*/
function setConfig(mode?: string, cacheVersion = 7): void {
// 자가 복구가 기록하는 독립 전역까지 초기화한다 — 남아 있으면 이후 케이스가
// 앞 케이스의 전환 결과를 물려받아 거짓 실패/거짓 통과가 난다.
delete (globalThis as any).__g7AssetUrlMode;
(globalThis as any).G7Config = {
cache_version: cacheVersion,
...(mode ? { assetUrlMode: mode } : {}),
};
}
describe('assetUrl (프론트 자산 URL 빌더)', () => {
beforeEach(() => {
globalThis.localStorage?.clear();
setConfig(MODE_EXTENSION);
});
afterEach(() => {
delete (globalThis as any).G7Config;
delete (globalThis as any).__g7AssetUrlMode;
globalThis.localStorage?.clear();
});
describe('모드 판정', () => {
it('G7Config 부재 시 기본 모드는 확장자 유지', () => {
delete (globalThis as any).G7Config;
expect(getAssetUrlMode()).toBe(MODE_EXTENSION);
expect(isExtensionless()).toBe(false);
});
it('런타임 키가 설정값보다 우선한다', () => {
(globalThis as any).G7Config = {
assetUrlMode: MODE_EXTENSIONLESS,
settings: { general: { asset_url_mode: MODE_EXTENSION } },
};
expect(getAssetUrlMode()).toBe(MODE_EXTENSIONLESS);
});
it('런타임 키가 없으면 설정값을 따른다', () => {
(globalThis as any).G7Config = {
settings: { general: { asset_url_mode: MODE_EXTENSIONLESS } },
};
expect(getAssetUrlMode()).toBe(MODE_EXTENSIONLESS);
});
it('알 수 없는 값은 기본 모드로 폴백한다', () => {
(globalThis as any).G7Config = { assetUrlMode: 'nonsense' };
expect(getAssetUrlMode()).toBe(MODE_EXTENSION);
});
});
describe('확장자 모드 — 치환 이전 문자열과 바이트 동일', () => {
it('Router.ts:113 routes.json (버전 있음/없음)', () => {
expect(suffixed('/api/templates/sirsoft-basic/routes', 'json', 7)).toBe(
'/api/templates/sirsoft-basic/routes.json?v=7',
);
expect(suffixed('/api/templates/sirsoft-basic/routes', 'json', null)).toBe(
'/api/templates/sirsoft-basic/routes.json',
);
});
it('ComponentRegistry.ts:250 components.json', () => {
expect(suffixed('/api/templates/sirsoft-basic/components', 'json')).toBe(
'/api/templates/sirsoft-basic/components.json',
);
});
it('TemplateApp.ts:2810 config.json + 캐시버스트 쿼리', () => {
expect(suffixed('/api/templates/sirsoft-basic/config', 'json', null, '_=1699999999')).toBe(
'/api/templates/sirsoft-basic/config.json?_=1699999999',
);
});
it('LayoutLoader.ts:750,752 레이아웃 / 미리보기', () => {
expect(layoutUrl('sirsoft-basic', 'home', 7)).toBe('/api/layouts/sirsoft-basic/home.json?v=7');
expect(layoutPreviewUrl('abc-def')).toBe('/api/layouts/preview/abc-def.json');
});
it('편집기 문서 로드 — with_source_meta 가 v 보다 앞 (캐시 키 보존)', () => {
expect(
suffixed('/api/layouts/sirsoft-basic/home', 'json', '7.3', 'with_source_meta=1'),
).toBe('/api/layouts/sirsoft-basic/home.json?with_source_meta=1&v=7.3');
});
it('자산 URL — 템플릿/모듈/플러그인', () => {
expect(templateAsset('sirsoft-basic', 'js/components.iife.js', 7)).toBe(
'/api/templates/assets/sirsoft-basic/js/components.iife.js?v=7',
);
expect(moduleAsset('sirsoft-ecommerce', 'dist/js/module.iife.js', 7)).toBe(
'/api/modules/assets/sirsoft-ecommerce/dist/js/module.iife.js?v=7',
);
expect(pluginAsset('sirsoft-gdpr', 'dist/css/plugin.css')).toBe(
'/api/plugins/assets/sirsoft-gdpr/dist/css/plugin.css',
);
});
it('병합 번들', () => {
expect(extensionBundle('modules', 'js', 7)).toBe('/api/modules/bundle.js?v=7');
expect(extensionBundle('plugins', 'css', 7)).toBe('/api/plugins/bundle.css?v=7');
});
});
describe('확장자 없는 모드', () => {
beforeEach(() => setConfig(MODE_EXTENSIONLESS));
it('고정 접미사를 제거한다', () => {
expect(suffixed('/api/templates/sirsoft-basic/routes', 'json', 7)).toBe(
'/api/templates/sirsoft-basic/routes?v=7',
);
expect(layoutUrl('sirsoft-basic', 'home', 7)).toBe('/api/layouts/sirsoft-basic/home?v=7');
});
it('자산 경로를 file 쿼리로 옮긴다', () => {
expect(templateAsset('sirsoft-basic', 'js/components.iife.js', 7)).toBe(
'/api/templates/assets/sirsoft-basic?file=js%2Fcomponents.iife.js&v=7',
);
});
it('번들 접미사를 경로 세그먼트로 내린다', () => {
expect(extensionBundle('modules', 'js', 7)).toBe('/api/modules/bundle/js?v=7');
expect(extensionBundle('plugins', 'css')).toBe('/api/plugins/bundle/css');
});
it('생성된 모든 URL 의 경로에 정적 확장자가 남지 않는다', () => {
const urls = [
suffixed('/api/templates/t/routes', 'json', 7),
layoutUrl('t', 'home', 7),
layoutPreviewUrl('abc'),
templateAsset('t', 'js/a.js', 7),
moduleAsset('m', 'dist/js/a.js'),
pluginAsset('p', 'dist/css/a.css'),
extensionBundle('modules', 'js', 7),
];
for (const url of urls) {
const path = url.split('?')[0];
expect(path, `경로에 정적 확장자 잔존: ${url}`).not.toMatch(/\.(js|mjs|css|json|map)$/i);
}
});
});
describe('런타임 전환 (§12 L1 — 단방향 1회)', () => {
it('extension → extensionless 전환은 허용된다', () => {
expect(setAssetUrlMode(MODE_EXTENSIONLESS)).toBe(true);
expect(getAssetUrlMode()).toBe(MODE_EXTENSIONLESS);
});
it('역방향 전환은 거부된다 (무한 왕복 차단)', () => {
setAssetUrlMode(MODE_EXTENSIONLESS);
expect(setAssetUrlMode(MODE_EXTENSION as any)).toBe(false);
expect(getAssetUrlMode()).toBe(MODE_EXTENSIONLESS);
});
it('이미 전환된 상태에서 재전환은 false 를 반환한다 (1회 제한)', () => {
expect(setAssetUrlMode(MODE_EXTENSIONLESS)).toBe(true);
expect(setAssetUrlMode(MODE_EXTENSIONLESS)).toBe(false);
});
});
describe('localStorage 캐시 (§12 L7)', () => {
it('전환 결과가 캐시되어 재방문 시 복원된다', () => {
setAssetUrlMode(MODE_EXTENSIONLESS);
setConfig(MODE_EXTENSION);
expect(restoreCachedMode()).toBe(true);
expect(getAssetUrlMode()).toBe(MODE_EXTENSIONLESS);
});
it('cache_version 이 바뀌면 옛 캐시가 무시된다', () => {
setAssetUrlMode(MODE_EXTENSIONLESS);
setConfig(MODE_EXTENSION, 8);
expect(restoreCachedMode()).toBe(false);
expect(getAssetUrlMode()).toBe(MODE_EXTENSION);
});
it('TTL 을 넘긴 캐시는 무시되고 제거된다', () => {
const stale = JSON.stringify({ mode: MODE_EXTENSIONLESS, at: Date.now() - 25 * 60 * 60 * 1000 });
globalThis.localStorage.setItem('g7_asset_url_mode:7', stale);
setConfig(MODE_EXTENSION);
expect(restoreCachedMode()).toBe(false);
expect(globalThis.localStorage.getItem('g7_asset_url_mode:7')).toBeNull();
});
});
});
@@ -0,0 +1,302 @@
// e2e:allow 테스트 전용 수정 — 프로브 판정 함수 추출 리팩토링 후 stale 앵커(detect→probe) 정정. 런타임 동작 무변경이며 대상 기능 E2E 는 tests/Playwright/specs/asset-url-mode.spec.ts 로 이미 커버됨.
/**
* 자산 URL 자가 복구 불변식 가드 — 이슈 #486 단위 D (§12 L1~L9).
*
* 자가 복구는 "실패했으니 다시 시도한다" 는 구조라 루프 위험이 내재한다.
* 계획서 §12 는 이를 구현 불변식으로 못박았고, 각각에 대응 테스트를 요구한다.
* 여기서는 브라우저 없이 검증 가능한 불변식(L1·L5·L7)과, blade 인라인 복구기와
* TS 빌더의 **규칙 드리프트**를 다룬다. 요청 횟수·리로드·폴백 UI(L2·L3·L4·L8)는
* 브라우저 계층이므로 Playwright spec 이 담당한다.
*/
// e2e:allow 불변식의 순수 로직 단위. 브라우저 계층(L2·L3·L4·L8)은 asset-url-mode.spec.ts 가 커버한다.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { readFileSync, readdirSync } from 'node:fs';
import { resolve, join } from 'node:path';
import {
MODE_EXTENSION,
MODE_EXTENSIONLESS,
getAssetUrlMode,
setAssetUrlMode,
convertToCurrentMode,
} from '../assetUrl';
/**
* blade 파샬의 인라인 `toExtensionless` 를 추출해 실행 가능한 함수로 만든다.
*
* 부트스트랩은 코어 번들 로드 전에 동작해야 해서 import 를 쓸 수 없고, 그래서 변환 규칙이
* blade 인라인 JS 에 한 벌 더 존재한다. 두 구현이 갈라지면 서버가 만든 URL 과
* 클라이언트가 만든 URL 이 어긋나 그 자산만 404 가 되므로, 실제 blade 소스에서
* 함수를 뽑아 TS 구현과 대조한다.
*/
function loadInlineConverter(): (url: string) => string | null {
const bladePath = resolve(__dirname, '../../../../views/partials/asset-url-recovery.blade.php');
const source = readFileSync(bladePath, 'utf-8');
const start = source.indexOf('function toExtensionless(url) {');
expect(start, 'blade 파샬에서 toExtensionless 를 찾지 못했다').toBeGreaterThan(-1);
// 균형 중괄호로 함수 본문 끝을 찾는다
let depth = 0;
let end = start;
for (let i = source.indexOf('{', start); i < source.length; i += 1) {
if (source[i] === '{') depth += 1;
else if (source[i] === '}') {
depth -= 1;
if (depth === 0) {
end = i + 1;
break;
}
}
}
const fnSource = source.slice(start, end);
// 함수가 참조하는 파샬 상단 상수를 함께 주입한다 (blade 스코프 재현)
// eslint-disable-next-line no-new-func
return new Function(
`var FILE_QUERY_PARAM = 'file';\n${fnSource}\nreturn toExtensionless;`,
)() as (url: string) => string | null;
}
describe('자산 URL 자가 복구 불변식 (§12)', () => {
beforeEach(() => {
delete (globalThis as any).__g7AssetUrlMode;
(globalThis as any).G7Config = { cache_version: 7 };
globalThis.localStorage?.clear();
});
afterEach(() => {
delete (globalThis as any).__g7AssetUrlMode;
delete (globalThis as any).G7Config;
globalThis.localStorage?.clear();
});
describe('L1 — 전환은 단방향 1회', () => {
it('extension → extensionless 는 1회만 성공한다', () => {
expect(setAssetUrlMode(MODE_EXTENSIONLESS)).toBe(true);
expect(setAssetUrlMode(MODE_EXTENSIONLESS)).toBe(false);
});
it('역방향 전환 경로가 존재하지 않는다', () => {
setAssetUrlMode(MODE_EXTENSIONLESS);
expect(setAssetUrlMode(MODE_EXTENSION as any)).toBe(false);
expect(getAssetUrlMode()).toBe(MODE_EXTENSIONLESS);
});
it('G7Config 가 재대입되어도 전환 결과가 유지된다', () => {
setAssetUrlMode(MODE_EXTENSIONLESS);
// <body> 의 `window.G7Config = {...}` 재대입 모사
(globalThis as any).G7Config = { cache_version: 7, assetUrlMode: MODE_EXTENSION };
expect(getAssetUrlMode()).toBe(MODE_EXTENSIONLESS);
});
});
describe('L5 — 클라이언트는 서버 설정을 쓰지 않는다', () => {
it('전환이 settings 원본을 변경하지 않는다', () => {
(globalThis as any).G7Config = {
cache_version: 7,
settings: { general: { asset_url_mode: MODE_EXTENSION } },
};
setAssetUrlMode(MODE_EXTENSIONLESS);
expect((globalThis as any).G7Config.settings.general.asset_url_mode).toBe(MODE_EXTENSION);
});
});
describe('L7 — 캐시는 cache_version 을 포함하고 TTL 을 둔다', () => {
it('캐시 키에 cache_version 이 포함된다', () => {
setAssetUrlMode(MODE_EXTENSIONLESS);
expect(globalThis.localStorage.getItem('g7_asset_url_mode:7')).not.toBeNull();
});
});
describe('변환 규칙 — blade 인라인 복구기와 TS 빌더 동등성', () => {
const cases = [
'/api/templates/assets/sirsoft-basic/js/components.iife.js?v=7',
'/api/templates/assets/sirsoft-basic/css/components.css?v=7',
'/api/modules/assets/sirsoft-ecommerce/dist/js/module.iife.js?v=7',
'/api/plugins/assets/sirsoft-gdpr/dist/css/plugin.css',
'/api/modules/bundle.js?v=7',
'/api/plugins/bundle.css',
'/api/templates/sirsoft-basic/routes.json?v=7',
'/api/layouts/sirsoft-basic/home.json?with_source_meta=1&v=7',
];
it('모든 대표 URL 에서 두 구현의 결과가 동일하다', () => {
const inline = loadInlineConverter();
(globalThis as any).__g7AssetUrlMode = MODE_EXTENSIONLESS;
for (const url of cases) {
expect(inline(url), `blade 인라인 변환기가 ${url} 을 변환하지 못했다`).not.toBeNull();
expect(inline(url), `변환 규칙 드리프트: ${url}`).toBe(convertToCurrentMode(url));
}
});
it('코어 엔진 번들은 두 구현 모두 변환하지 않는다', () => {
const inline = loadInlineConverter();
(globalThis as any).__g7AssetUrlMode = MODE_EXTENSIONLESS;
const coreBundle = '/build/core/template-engine.min.js?v=123';
// public/ 의 실물 정적 파일 — 변환하면 오히려 깨진다
expect(inline(coreBundle)).toBeNull();
expect(convertToCurrentMode(coreBundle)).toBe(coreBundle);
});
it('확장자 모드에서는 TS 빌더가 원본을 그대로 돌려준다', () => {
(globalThis as any).__g7AssetUrlMode = undefined;
(globalThis as any).G7Config = { cache_version: 7, assetUrlMode: MODE_EXTENSION };
for (const url of cases) {
expect(convertToCurrentMode(url)).toBe(url);
}
});
});
describe('L6 — 인스톨러 프로브도 본문 토큰 + Content-Type 으로 판정', () => {
/**
* 프로브 판정 로직은 두 곳에 존재한다 — 관리자 핸들러(`detectAssetUrlModeHandler`)와
* 인스톨러(`installer.js`). 전자는 동작 테스트로 red 증명까지 마쳤지만,
* 인스톨러는 코어 번들 이전에 도는 순수 브라우저 스크립트라 vitest 하네스가 없다.
*
* 하네스를 새로 만드는 대신 소스 수준 드리프트 가드를 둔다 — 두 검사 중 하나라도
* 사라지면 이 테스트가 red 가 된다. `200 + 에러 HTML` 을 반환하는 서버에서
* 설치가 영원히 `extension` 으로 오판되는 것을 막는 검사다.
*/
it('인스톨러 프로브가 두 검사를 모두 수행한다', () => {
const installerPath = resolve(
__dirname,
'../../../../../public/install/assets/js/installer.js',
);
const source = readFileSync(installerPath, 'utf-8');
const start = source.indexOf('async function probeAssetUrlMode(');
expect(start, 'installer.js 에서 probeAssetUrlMode 를 찾지 못했다').toBeGreaterThan(-1);
// 주석은 제거하고 실행 코드만 본다 — 주석에 남은 "Content-Type" 문구가
// 검사 삭제를 가려주면 가드가 무력해진다(실제로 겪은 오탐).
const body = source
.slice(start, start + 2500)
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/[^\n]*/g, '');
expect(
/headers\s*\.\s*get\(\s*['"]content-type['"]/i.test(body),
'L6 위반: 인스톨러 프로브가 Content-Type 을 읽지 않는다 (200+HTML 오판)',
).toBe(true);
expect(
/javascript|ecmascript/i.test(body),
'L6 위반: 인스톨러 프로브에 스크립트 MIME 판정이 없다',
).toBe(true);
expect(
/includes\(\s*TOKEN\s*\)/.test(body),
'L6 위반: 인스톨러 프로브에 매직 토큰 검사가 없다 (무관한 200 응답 오판)',
).toBe(true);
});
});
describe('L9 — 감지 주체는 부트스트랩 하나', () => {
/**
* 엔진 레이어(Router/LayoutLoader/ComponentRegistry/TranslationEngine 등)는
* 확정된 모드를 **읽기만** 해야 한다. 계층마다 독립적으로 재감지하면
* 감지 캐스케이드가 중첩되어 요청이 폭증한다.
*
* 프로브 엔드포인트를 호출하는 코드가 엔진에 새로 들어오면 이 테스트가 잡는다.
*/
it('엔진 소스에 프로브 엔드포인트 호출이 없다', () => {
const engineRoot = resolve(__dirname, '../../');
const offenders: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === '__tests__' || entry.name === 'node_modules') continue;
walk(full);
continue;
}
if (!/\.(ts|tsx)$/.test(entry.name)) continue;
const src = readFileSync(full, 'utf-8');
if (src.includes('asset-probe')) {
offenders.push(full.replace(engineRoot, ''));
}
}
};
walk(engineRoot);
expect(
offenders,
`엔진 레이어가 프로브를 직접 호출한다 (L9 위반 — 감지는 부트스트랩 단독):\n${offenders.join('\n')}`,
).toEqual([]);
});
});
describe('convertToCurrentMode 변환 결과', () => {
beforeEach(() => {
(globalThis as any).__g7AssetUrlMode = MODE_EXTENSIONLESS;
});
it('자산은 file 쿼리로, 번들은 세그먼트로, 접미사는 제거로', () => {
expect(convertToCurrentMode('/api/templates/assets/t/js/a.js?v=7')).toBe(
'/api/templates/assets/t?file=js%2Fa.js&v=7',
);
expect(convertToCurrentMode('/api/modules/bundle.js?v=7')).toBe('/api/modules/bundle/js?v=7');
expect(convertToCurrentMode('/api/templates/t/routes.json')).toBe('/api/templates/t/routes');
});
it('변환 결과 경로에 정적 확장자가 남지 않는다', () => {
for (const url of [
'/api/templates/assets/t/js/a.js?v=7',
'/api/modules/bundle.js',
'/api/templates/t/routes.json',
'/api/layouts/t/home.json?v=7',
]) {
const path = convertToCurrentMode(url).split('?')[0];
expect(path, `경로에 확장자 잔존: ${url}`).not.toMatch(/\.(js|css|json)$/i);
}
});
it('외부 origin URL 은 건드리지 않는다', () => {
const external = 'https://cdn.example.com/lib.js';
expect(convertToCurrentMode(external)).toBe(external);
});
// 회귀: `<script>.src` / `<link>.href` 는 절대 URL 을 돌려준다. same-origin
// 접두사를 벗기지 않으면 부트스트랩 재시도에서 변환이 항상 null 이 되어
// 자가 복구가 통째로 죽는다 — 외형은 "3회 재시도 후 폴백 UI" 라 원인이 안 보인다.
it('same-origin 절대 URL 도 변환한다 (DOM 프로퍼티 형태)', () => {
const origin = globalThis.location.origin;
expect(convertToCurrentMode(`${origin}/api/templates/assets/t/js/a.js?v=7`)).toBe(
'/api/templates/assets/t?file=js%2Fa.js&v=7',
);
expect(convertToCurrentMode(`${origin}/api/modules/bundle.js?v=7`)).toBe(
'/api/modules/bundle/js?v=7',
);
});
it('blade 인라인 복구기도 same-origin 절대 URL 을 변환한다', () => {
const inline = loadInlineConverter();
const origin = globalThis.location.origin;
expect(inline(`${origin}/api/templates/assets/t/js/a.js?v=7`)).toBe(
'/api/templates/assets/t?file=js%2Fa.js&v=7',
);
});
});
});
+436
View File
@@ -0,0 +1,436 @@
/**
* 자산·동적 엔드포인트 URL 생성기 (프론트측 SSoT).
*
* 서버측 `App\Support\AssetUrl` 와 **동일한 규칙**을 구현한다. 한쪽만 바뀌면
* 서버가 만든 URL 과 클라이언트가 만든 URL 이 어긋나 그 자산만 404 가 되므로,
* 규칙을 바꿀 때는 반드시 양쪽을 함께 수정한다.
*
* ## 배경
*
* nginx/Apache 의 표준적 정적 최적화 블록은 URL 마지막 확장자로 분기한다.
*
* ```nginx
* location ~* \.(js|css|json)$ { expires max; access_log off; }
* ```
*
* nginx 에서 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로
* `try_files ... /index.php` 폴백이 실행될 기회가 없다. 그런 환경에서는
* 확장자 붙은 동적 응답이 PHP 에 도달하지 못하고 404 가 된다.
*
* ## 모드
*
* | 모드 | 의미 |
* |---|---|
* | `extension` (기본) | 확장자 유지 — 정상 환경 |
* | `extensionless` | 확장자 제거 — 정적 블록이 가로채는 환경 |
*
* 초기값은 서버가 `window.G7Config.assetUrlMode` 로 내려준다. 부트스트랩
* 자가 복구가 확장자 형태 실패를 감지하면 런타임에 `extensionless` 로 전환한다
* (단방향 1회 — 역방향 전환은 만들지 않는다. 무한 왕복 방지).
*
* @since engine-v1.54.0
*/
/** 확장자 유지 모드 식별자. */
export const MODE_EXTENSION = 'extension';
/** 확장자 제거 모드 식별자. */
export const MODE_EXTENSIONLESS = 'extensionless';
/** 자산 URL 모드 타입. */
export type AssetUrlMode = typeof MODE_EXTENSION | typeof MODE_EXTENSIONLESS;
/** 확장자 없는 자산 URL 에서 파일 경로를 담는 쿼리 파라미터명. */
export const FILE_QUERY_PARAM = 'file';
/** localStorage 캐시 키 접두사 (cache_version 을 포함해 서버 정상화 후 고착 방지). */
const STORAGE_KEY_PREFIX = 'g7_asset_url_mode';
/** localStorage 캐시 TTL (24시간). 서버가 정상화되면 자동으로 재판정된다. */
const STORAGE_TTL_MS = 24 * 60 * 60 * 1000;
/**
* 현재 자산 URL 모드를 반환합니다.
*
* 우선순위: 런타임 전환값(`G7Config.assetUrlMode`) → 설정값
* (`G7Config.settings.general.asset_url_mode`) → 기본값 `extension`.
*
* @returns 현재 모드
*/
export function getAssetUrlMode(): AssetUrlMode {
const config = (globalThis as any)?.G7Config;
// 부트스트랩 자가 복구가 기록하는 독립 전역이 최우선이다. `<head>` 파샬이 이 값을
// 확정한 뒤 `<body>` 에서 G7Config 객체가 통째로 재대입되는 순서라, G7Config 만
// 보면 전환 결과를 놓칠 수 있다.
const recovered = (globalThis as any)?.__g7AssetUrlMode;
if (recovered === MODE_EXTENSIONLESS) {
return MODE_EXTENSIONLESS;
}
const runtime = config?.assetUrlMode;
if (runtime === MODE_EXTENSIONLESS || runtime === MODE_EXTENSION) {
return runtime;
}
const configured = config?.settings?.general?.asset_url_mode;
if (configured === MODE_EXTENSIONLESS) {
return MODE_EXTENSIONLESS;
}
return MODE_EXTENSION;
}
/**
* 현재 모드가 확장자 없는 모드인지 여부를 반환합니다.
*
* @returns 확장자 없는 모드이면 true
*/
export function isExtensionless(): boolean {
return getAssetUrlMode() === MODE_EXTENSIONLESS;
}
/**
* 자산 URL 모드를 런타임에 전환합니다 (단방향).
*
* `extension → extensionless` 만 허용한다. 역방향을 허용하면 양쪽 형태가 모두
* 실패하는 상황(PHP 다운·WAF 차단)에서 `ext → extless → ext → …` 무한 왕복이 된다.
*
* 서버 설정은 바꾸지 않는다 — 미인증 클라이언트가 전역 설정을 뒤집을 수 있으면 안 된다.
*
* @param mode 전환할 모드
* @returns 실제로 전환되었으면 true
*/
export function setAssetUrlMode(mode: AssetUrlMode): boolean {
if (mode !== MODE_EXTENSIONLESS) {
return false;
}
if ((globalThis as any)?.__g7AssetUrlMode === MODE_EXTENSIONLESS) {
return false;
}
const config = (globalThis as any)?.G7Config;
if (config?.assetUrlMode === MODE_EXTENSIONLESS) {
return false;
}
(globalThis as any).__g7AssetUrlMode = MODE_EXTENSIONLESS;
if (config) {
config.assetUrlMode = MODE_EXTENSIONLESS;
}
persistMode(MODE_EXTENSIONLESS);
return true;
}
/**
* 캐시된 모드를 읽어 적용합니다 (같은 브라우저 재방문 시 첫 요청부터 정타).
*
* 캐시 키에 `cache_version` 을 포함하고 TTL 을 두어, 서버가 정상화된 뒤에도
* 클라이언트가 옛 모드에 영구 고착되지 않도록 한다.
*
* @returns 캐시가 적용되었으면 true
*/
export function restoreCachedMode(): boolean {
try {
const raw = globalThis.localStorage?.getItem(storageKey());
if (!raw) {
return false;
}
const parsed = JSON.parse(raw) as { mode?: string; at?: number };
if (parsed?.mode !== MODE_EXTENSIONLESS) {
return false;
}
if (typeof parsed.at !== 'number' || Date.now() - parsed.at > STORAGE_TTL_MS) {
globalThis.localStorage?.removeItem(storageKey());
return false;
}
(globalThis as any).__g7AssetUrlMode = MODE_EXTENSIONLESS;
const config = (globalThis as any)?.G7Config;
if (config) {
config.assetUrlMode = MODE_EXTENSIONLESS;
}
return true;
} catch {
// localStorage 접근 불가(사파리 프라이빗 등)는 치명적이지 않다 — 캐시 없이 진행
return false;
}
}
/**
* 템플릿 자산 URL 을 생성합니다.
*
* 템플릿은 서버가 `dist/` 를 자동 부가하므로 `path` 에 `dist/` 를 포함하지 않는다.
*
* @param identifier 템플릿 식별자
* @param path `dist/` 이하 파일 경로 (예: `js/components.iife.js`)
* @param version 캐시 무효화 버전 (없으면 미부착)
* @returns 생성된 URL
*/
export function templateAsset(identifier: string, path: string, version?: number | string | null): string {
return extensionAsset('templates', identifier, path, version);
}
/**
* 모듈 자산 URL 을 생성합니다.
*
* 모듈은 모듈 루트 기준이라 `path` 에 `dist/` 를 직접 포함한다 (템플릿과 비대칭).
*
* @param identifier 모듈 식별자
* @param path 모듈 루트 기준 파일 경로
* @param version 캐시 무효화 버전
* @returns 생성된 URL
*/
export function moduleAsset(identifier: string, path: string, version?: number | string | null): string {
return extensionAsset('modules', identifier, path, version);
}
/**
* 플러그인 자산 URL 을 생성합니다.
*
* @param identifier 플러그인 식별자
* @param path 플러그인 루트 기준 파일 경로
* @param version 캐시 무효화 버전
* @returns 생성된 URL
*/
export function pluginAsset(identifier: string, path: string, version?: number | string | null): string {
return extensionAsset('plugins', identifier, path, version);
}
/**
* 확장 타입을 인자로 받는 자산 URL 생성기.
*
* 확장자 없는 모드에서는 파일 경로를 `?file=` 쿼리로 옮긴다. 경로가 곧 파일명이라
* 접미사만 떼어낼 수 없기 때문이며, nginx 의 location 정규식이 쿼리스트링을 제외한
* 경로에만 매칭되므로 이 형태가 안전하다.
*
* @param type `templates` | `modules` | `plugins`
* @param identifier 확장 식별자
* @param path 파일 경로
* @param version 캐시 무효화 버전
* @returns 생성된 URL
*/
export function extensionAsset(
type: string,
identifier: string,
path: string,
version?: number | string | null,
): string {
const normalizedPath = path.replace(/^\/+/, '');
const id = encodeURIComponent(identifier);
if (!isExtensionless()) {
return `/api/${type}/assets/${id}/${normalizedPath}${versionQuery(version)}`;
}
let query = `${FILE_QUERY_PARAM}=${encodeURIComponent(normalizedPath)}`;
if (version !== undefined && version !== null && version !== '') {
query += `&v=${version}`;
}
return `/api/${type}/assets/${id}?${query}`;
}
/**
* 확장 병합 번들 URL 을 생성합니다.
*
* 접미사(js/css)가 번들 종류를 구분하므로 제거할 수 없다.
* 확장자 없는 모드에서는 경로 세그먼트로 내린다 (`bundle.js` → `bundle/js`).
*
* @param type `modules` | `plugins`
* @param kind `js` | `css`
* @param version 캐시 무효화 버전
* @returns 생성된 URL
*/
export function extensionBundle(type: string, kind: string, version?: number | string | null): string {
const base = isExtensionless() ? `/api/${type}/bundle/${kind}` : `/api/${type}/bundle.${kind}`;
return `${base}${versionQuery(version)}`;
}
/**
* 고정 접미사를 갖는 동적 엔드포인트 URL 을 생성합니다.
*
* 확장자 없는 모드에서는 접미사를 제거한다 (`routes.json` → `routes`).
* 추가 쿼리는 `extraQuery` 로 전달한다 (이미 `?` 가 붙은 뒤에 이어붙이지 않도록
* 결합을 이 함수가 책임진다).
*
* @param path 접미사를 제외한 경로 (예: `/api/templates/foo/routes`)
* @param suffix 접미사 (예: `json`)
* @param version 캐시 무효화 버전
* @param extraQuery 추가 쿼리스트링 (`a=1&b=2` 형태, `?` 없이)
* @returns 생성된 URL
*/
export function suffixed(
path: string,
suffix: string,
version?: number | string | null,
extraQuery?: string,
): string {
const base = path.replace(/\/+$/, '');
const normalized = suffix.replace(/^\.+/, '');
const url = isExtensionless() ? base : `${base}.${normalized}`;
// 순서 주의: extraQuery 를 v 보다 **앞**에 둔다. 치환 이전 호출부가
// `?with_source_meta=1&v=...` 순서로 만들고 있었고, 쿼리 순서가 바뀌면
// URL 문자열이 달라져 HTTP 캐시 키가 갈린다(의미는 같아도 캐시 미스).
const parts: string[] = [];
if (extraQuery) {
parts.push(extraQuery.replace(/^[?&]+/, ''));
}
if (version !== undefined && version !== null && version !== '') {
parts.push(`v=${version}`);
}
return parts.length > 0 ? `${url}?${parts.join('&')}` : url;
}
/**
* 레이아웃 서빙 URL 을 생성합니다.
*
* @param templateId 템플릿 식별자
* @param layoutPath 레이아웃 경로 (예: `home`, `board/list`)
* @param version 캐시 무효화 버전
* @param extraQuery 추가 쿼리스트링
* @returns 생성된 URL
*/
export function layoutUrl(
templateId: string,
layoutPath: string,
version?: number | string | null,
extraQuery?: string,
): string {
return suffixed(`/api/layouts/${encodeURIComponent(templateId)}/${layoutPath}`, 'json', version, extraQuery);
}
/**
* 레이아웃 미리보기 서빙 URL 을 생성합니다.
*
* @param token 미리보기 토큰 (UUID)
* @returns 생성된 URL
*/
export function layoutPreviewUrl(token: string): string {
return suffixed(`/api/layouts/preview/${encodeURIComponent(token)}`, 'json');
}
/**
* 서버가 확장자 형태로 만들어 내려준 URL 을 **현재 모드**에 맞게 변환합니다.
*
* `G7Config.bundleUrls` / `moduleAssets` / `pluginAssets` 는 서버 렌더 시점에
* 문자열로 굳어 내려온다. 부트스트랩 자가 복구가 런타임에 모드를 뒤집으면 이 값들이
* 옛 형태로 남으므로, 소비 지점에서 이 함수를 거쳐야 한다.
*
* 확장자 모드이거나 변환 대상이 아닌 URL(외부 origin, `/build/...` 정적 파일 등)은
* 원본을 그대로 돌려준다 — 코어 엔진 번들은 `public/` 의 실물 파일이라 변환하면 깨진다.
*
* @param url 서버가 생성한 URL
* @returns 현재 모드에 맞는 URL
*/
export function convertToCurrentMode(url: string): string {
if (!url || !isExtensionless()) {
return url;
}
// DOM 프로퍼티(`script.src` / `link.href`)는 절대 URL 을 돌려주므로
// same-origin 접두사를 먼저 벗긴다. 외부 origin 은 대상이 아니다.
const origin = (globalThis as any)?.location?.origin;
const relative = origin && url.startsWith(origin) ? url.slice(origin.length) : url;
if (!relative.startsWith('/api/')) {
return url;
}
const [path, query] = splitQuery(relative);
// 1) 병합 번들: /api/{type}/bundle.{js|css} → /api/{type}/bundle/{js|css}
const bundleMatch = path.match(/^\/api\/(modules|plugins)\/bundle\.(js|css)$/);
if (bundleMatch) {
return joinQuery(`/api/${bundleMatch[1]}/bundle/${bundleMatch[2]}`, query);
}
// 2) 자산: /api/{type}/assets/{id}/{path} → /api/{type}/assets/{id}?file={path}
const assetMatch = path.match(/^\/api\/(templates|modules|plugins)\/assets\/([^/]+)\/(.+)$/);
if (assetMatch) {
const [, type, identifier, filePath] = assetMatch;
const fileQuery = `${FILE_QUERY_PARAM}=${encodeURIComponent(decodeURIComponent(filePath))}`;
return `/api/${type}/assets/${identifier}?${fileQuery}${query ? `&${query}` : ''}`;
}
// 3) 고정 접미사: /api/.../x.json → /api/.../x
const suffixMatch = path.match(/^(\/api\/.+)\.(json|js|css)$/);
if (suffixMatch) {
return joinQuery(suffixMatch[1], query);
}
return url;
}
/**
* URL 을 경로와 쿼리로 분리합니다.
*
* @param url 대상 URL
* @returns [경로, 쿼리(`?` 제외)]
*/
function splitQuery(url: string): [string, string] {
const idx = url.indexOf('?');
return idx === -1 ? [url, ''] : [url.slice(0, idx), url.slice(idx + 1)];
}
/**
* 경로와 쿼리를 결합합니다.
*
* @param path 경로
* @param query 쿼리 (`?` 제외)
* @returns 결합된 URL
*/
function joinQuery(path: string, query: string): string {
return query ? `${path}?${query}` : path;
}
/**
* 캐시 무효화 쿼리스트링을 생성합니다.
*
* @param version 버전 값
* @returns `?v=...` 또는 빈 문자열
*/
function versionQuery(version?: number | string | null): string {
if (version === undefined || version === null || version === '') {
return '';
}
return `?v=${version}`;
}
/**
* localStorage 캐시 키를 생성합니다 (cache_version 포함).
*
* @returns 캐시 키
*/
function storageKey(): string {
const cacheVersion = (globalThis as any)?.G7Config?.cache_version ?? 0;
return `${STORAGE_KEY_PREFIX}:${cacheVersion}`;
}
/**
* 판정된 모드를 localStorage 에 저장합니다.
*
* @param mode 저장할 모드
*/
function persistMode(mode: AssetUrlMode): void {
try {
globalThis.localStorage?.setItem(storageKey(), JSON.stringify({ mode, at: Date.now() }));
} catch {
// 저장 실패는 치명적이지 않다 — 다음 방문에 다시 판정하면 된다
}
}
@@ -5,6 +5,33 @@
>
> 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)
## [engine-v1.54.1] - 2026-07-23
### Fixed
#### 끝 슬래시가 붙은 경로(`/admin/`)가 라우트에 매칭되지 않던 문제
- `routing/Router.ts` — `match()` 진입 시 경로의 끝 슬래시를 정규화(`normalizePathname`)한 뒤 패턴과 대조한다. 라우트 패턴은 끝 슬래시 없는 형태(`*/admin` 등)로만 정의되므로(routes.json 규약), 종전에는 브라우저가 `/admin/` 처럼 끝 슬래시가 붙은 경로로 진입하면 `matchPattern` 이 만드는 앵커 정규식(`^…/admin$`)에 걸리지 않아 어떤 라우트에도 매칭되지 않고 404 로 떨어졌다. 특히 미인증 상태로 `/admin/` 에 진입하면 대시보드 리다이렉트 → 로그인 화면 흐름 대신 404 가 노출됐다. 이제 루트(`/`)를 제외한 경로의 끝 슬래시(연속 슬래시 포함)를 제거해 정상 매칭한다.
## [engine-v1.54.0] - 2026-07-20
### Added
#### 자산 URL 이중 모드 — 정적 최적화 서버에서의 동작 보장
- `support/assetUrl.ts`(신규) — 동적 엔드포인트 URL 생성을 한 곳으로 모았다. `getAssetUrlMode()` 가 `window.G7Config.assetUrlMode`(서버가 내려주는 초기값) → `G7Config.settings.general.asset_url_mode` → 기본값 `extension` 순으로 판정하고, `suffixed()` / `templateAsset()` / `moduleAsset()` / `pluginAsset()` / `extensionBundle()` / `layoutUrl()` / `layoutPreviewUrl()` 가 그 모드에 맞는 URL 을 만든다. 서버측 `App\Support\AssetUrl` 와 **동일 규칙**이며, 한쪽만 바꾸면 서버가 만든 URL 과 클라이언트가 만든 URL 이 어긋나 그 자산만 404 가 된다.
- 배경: nginx 의 정규식 location(`location ~* \.(js|css|json)$`)은 프리픽스 location 보다 먼저 매칭되므로, 확장자 붙은 동적 엔드포인트는 `try_files ... /index.php` 폴백이 실행될 기회 없이 nginx 가 직접 파일시스템을 열려 시도해 404 가 된다. aaPanel/CyberPanel/Plesk 기본 템플릿에 들어있는 블록이다.
- 확장자 없는 모드의 변환 규칙 — 고정 접미사는 제거(`routes.json` → `routes`), 번들은 접미사가 종류를 구분하므로 세그먼트로 강등(`bundle.js` → `bundle/js`), 와일드카드 자산은 경로가 곧 파일명이라 쿼리로 이동(`assets/{id}/js/a.js` → `assets/{id}?file=js/a.js`). 마지막 형태가 안전한 이유는 nginx 의 location 정규식이 쿼리스트링을 제외한 경로에만 매칭되기 때문이다.
- `setAssetUrlMode()` 는 **단방향 1회**만 허용한다(`extension → extensionless`). 역방향을 허용하면 양쪽 형태가 모두 실패하는 상황(PHP 다운·WAF 차단)에서 무한 왕복이 된다. 서버 설정은 바꾸지 않는다 — 미인증 클라이언트가 전역 설정을 뒤집을 수 있으면 안 된다.
- `restoreCachedMode()` 의 localStorage 캐시는 키에 `cache_version` 을 포함하고 24시간 TTL 을 둔다. 서버가 정상화된 뒤에도 클라이언트가 옛 모드에 영구 고착되지 않도록 하기 위함이다.
### Changed
#### 동적 엔드포인트 URL 생성부 14지점을 빌더 경유로 전환
- `routing/Router.ts`, `TemplateApp.ts`(4), `ComponentRegistry.ts`, `ErrorPageHandler.ts`, `LayoutLoader.ts`(2), `layout-editor/LayoutEditorChrome.tsx`, `layout-editor/hooks/{useEditorTemplateAssets,useInlineEdit,useLayoutDocument,useExtensionDocument}.ts` — `routes.json`·`config.json`·`components.json`·레이아웃 JSON URL 을 모두 `suffixed()` 로 생성한다.
- 기본 모드(`extension`)에서 생성 결과는 치환 이전과 **문자열까지 동일**하다. `suffixed()` 는 추가 쿼리를 `v` 보다 앞에 놓는데(`?with_source_meta=1&v=...`), 이는 편집기 문서 로드 호출부의 기존 순서를 보존하기 위한 것이다 — 쿼리 순서가 바뀌면 의미는 같아도 URL 문자열이 달라져 HTTP 캐시 키가 갈린다.
## [engine-v1.53.1] - 2026-07-12
### Fixed
@@ -13,6 +13,7 @@
import React, { type ComponentType } from 'react';
import { createLogger } from '../utils/Logger';
import { fetchWithRetry } from './networkResilience';
import { suffixed } from '../support/assetUrl';
const logger = createLogger('ComponentRegistry');
@@ -247,7 +248,7 @@ export class ComponentRegistry {
// 캐시 미스 - API에서 로드
// 네트워크 일시 실패(응답 없음)에만 재시도. HTTP 에러는 아래 !ok 분기가 종전대로 처리.
// @since engine-v1.53.0
const manifestUrl = `/api/templates/${this.templateId}/components.json`;
const manifestUrl = suffixed(`/api/templates/${this.templateId}/components`, 'json');
const response = await fetchWithRetry(manifestUrl, { label: 'components.json' });
if (!response.ok) {
@@ -8,6 +8,7 @@
import type { LayoutLoader } from './LayoutLoader';
import type { DataSourceManager, DataSource } from './DataSourceManager';
import { createLogger } from '../utils/Logger';
import { suffixed } from '../support/assetUrl';
const logger = createLogger('ErrorPageHandler');
@@ -83,7 +84,7 @@ export class ErrorPageHandler {
try {
// template.json fetch
const response = await fetch(`/api/templates/${this.templateId}/config.json`);
const response = await fetch(suffixed(`/api/templates/${this.templateId}/config`, 'json'));
if (!response.ok) {
if (this.debug) {
@@ -17,6 +17,7 @@ import type { ComponentRegistry } from './ComponentRegistry';
import type { ErrorHandlingMap } from '../types/ErrorHandling';
import { createLogger } from '../utils/Logger';
import { fetchWithRetry } from './networkResilience';
import { suffixed } from '../support/assetUrl';
import { G7DevToolsCore } from '../devtools/G7DevToolsCore';
import { getApiClient } from '../api/ApiClient';
@@ -744,14 +745,14 @@ export class LayoutLoader {
try {
// API 엔드포인트 구성 (캐시 버전 쿼리 파라미터 추가)
// 시스템 레이아웃 분기: __preview__ 는 별도 API 엔드포인트 사용
let baseUrl: string;
const version = this.cacheVersion > 0 ? this.cacheVersion : null;
let apiUrl: string;
if (layoutPath.startsWith('__preview__/')) {
const token = layoutPath.replace('__preview__/', '');
baseUrl = `/api/layouts/preview/${token}.json`;
apiUrl = suffixed(`/api/layouts/preview/${token}`, 'json', version);
} else {
baseUrl = `/api/layouts/${templateId}/${layoutPath}.json`;
apiUrl = suffixed(`/api/layouts/${templateId}/${layoutPath}`, 'json', version);
}
const apiUrl = this.cacheVersion > 0 ? `${baseUrl}?v=${this.cacheVersion}` : baseUrl;
logger.log('Fetching layout from API:', apiUrl);
@@ -13,6 +13,7 @@
*/
import { createLogger } from '../utils/Logger';
import { suffixed } from '../support/assetUrl';
const logger = createLogger('TranslationEngine');
@@ -221,20 +222,22 @@ export class TranslationEngine {
try {
// API 호출 (캐시 버전 쿼리 파라미터 추가, bustCache가 true면 타임스탬프도 추가)
let url = `${apiBaseUrl}/templates/${templateId}/lang/${locale}.json`;
// 쿼리 파라미터 구성
const queryParams: string[] = [];
if (this.cacheVersion > 0) {
queryParams.push(`v=${this.cacheVersion}`);
}
// 자산 URL 모드에 따라 `.json` 접미사가 붙거나 빠진다.
// 이 경로는 편집기 전용이 아니라 **모든 페이지가 타는 런타임 공통 경로**라,
// 여기만 확장자를 직접 조립하면 extensionless 환경에서 다국어가 통째로 404 가 되어
// 이슈 #486 의 원래 증상(화면이 온전히 뜨지 않음)이 다국어 계층에서 재현된다.
const extraParams: string[] = [];
if (bustCache) {
queryParams.push(`_=${Date.now()}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join('&')}`;
extraParams.push(`_=${Date.now()}`);
}
const url = suffixed(
`${apiBaseUrl}/templates/${templateId}/lang/${locale}`,
'json',
this.cacheVersion > 0 ? this.cacheVersion : null,
extraParams.length > 0 ? extraParams.join('&') : undefined,
);
const response = await fetch(url);
if (!response.ok) {
@@ -1,3 +1,4 @@
// e2e:allow 테스트 전용 수정 — suffixed 헬퍼 일원화(#486)로 바뀐 쿼리 파라미터 순서를 순서 무관 단언으로 정정. 런타임 동작 무변경이며 대상 기능 E2E 는 tests/Playwright/specs/asset-url-mode.spec.ts 로 이미 커버됨.
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
TranslationEngine,
@@ -1211,9 +1212,13 @@ describe('TranslationEngine', () => {
await engine.loadTranslations('template-1', 'ko', '/api', true);
// bustCache가 true이면 타임스탬프 쿼리 파라미터도 추가됨
// bustCache가 true이면 타임스탬프 쿼리 파라미터도 추가됨.
// 두 파라미터의 순서는 기능상 무의미하다 — #486 에서 URL 조립이 suffixed 헬퍼로
// 일원화되며 v=/_= 순서가 바뀌었다. 순서에 의존하지 말고 둘 다 존재하는지만 검증한다.
expect(global.fetch).toHaveBeenCalledWith(
expect.stringMatching(/\/api\/templates\/template-1\/lang\/ko\.json\?v=1735000000&_=\d+/)
expect.stringMatching(
/\/api\/templates\/template-1\/lang\/ko\.json\?(?=.*\bv=1735000000\b)(?=.*\b_=\d+\b)/
)
);
});
@@ -75,6 +75,7 @@ import { buildCoreActionRecipeSeed } from './spec/coreActionRecipes';
import { registerCoreWidgets } from './spec/registerCoreWidgets';
import { registerCoreEditors } from './spec/registerCoreEditors';
import { exposeLayoutEditorGlobals } from './spec/exposeLayoutEditorGlobals';
import { suffixed } from '../../support/assetUrl';
/**
* 편집기 셸 최소 너비(px) — 이 아래로는 셸을 압착하지 않고 브라우저 가로 스크롤로 흡수한다.
@@ -119,7 +120,7 @@ async function fetchPermissionCandidates(
if (typeof fetch !== 'function') return [];
try {
const response = await fetch(
`/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/permission-candidates.json`,
suffixed(`/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/permission-candidates`, 'json'),
{ credentials: 'same-origin', headers: buildAuthHeaders() },
);
if (!response.ok) return [];
@@ -223,7 +224,7 @@ function LayoutEditorChromeBody({
(async () => {
try {
const response = await fetch(
`/api/templates/${encodeURIComponent(templateIdentifier)}/components.json`,
suffixed(`/api/templates/${encodeURIComponent(templateIdentifier)}/components`, 'json'),
{ credentials: 'same-origin', headers: { Accept: 'application/json' } }
);
if (!response.ok) return;
@@ -1,3 +1,8 @@
// e2e:allow 이슈 #486 단위 A 는 서버 라우트 이중화 전용. 이 파일 변경은 편집기 CSS
// 엔드포인트 개명(editor/components.css → component-styles.css)을 따라간 기대값 수정뿐이며
// 브라우저 거동은 불변(URL 은 서버가 생성해 내려주고 hook 은 그대로 fetch).
// 자산 URL 모드의 브라우저 시나리오 spec 은 자가 복구를 도입하는 단위 D 에서 추가한다.
/**
* useEditorTemplateAssets 회귀 테스트
*
@@ -6,6 +11,7 @@
* hook 의 동작을 가드.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useEditorTemplateAssets } from '../../hooks/useEditorTemplateAssets';
@@ -118,7 +124,7 @@ describe('useEditorTemplateAssets', () => {
// 못 실어 500(Route[login])으로 떨어지므로, Bearer fetch → `<style>` 로 주입해야 한다.
it('권한 가드 admin CSS 는 Bearer fetch → <style> 주입 (link 아님)', async () => {
window.localStorage.setItem('auth_token', 'tok-css');
const adminCssUrl = '/api/admin/templates/sirsoft-basic/editor/components.css?v=1';
const adminCssUrl = '/api/admin/templates/sirsoft-basic/editor/component-styles.css?v=1';
fetchSpy.mockImplementation(async (url: string) => {
if (url.includes('/editor-assets')) {
return {
@@ -128,7 +134,7 @@ describe('useEditorTemplateAssets', () => {
}),
};
}
if (url.includes('/editor/components.css')) {
if (url.includes('/editor/component-styles.css')) {
return { ok: true, text: async () => '.g7le-preview-dark .x{color:red}' };
}
if (url.includes('/components.json')) {
@@ -150,7 +156,7 @@ describe('useEditorTemplateAssets', () => {
expect(styleEl?.textContent).toContain('.g7le-preview-dark');
// CSS fetch 에 Bearer 헤더 부착
const cssCall = fetchSpy.mock.calls.find((c) => String(c[0]).includes('/editor/components.css'));
const cssCall = fetchSpy.mock.calls.find((c) => String(c[0]).includes('/editor/component-styles.css'));
expect(cssCall?.[1]?.headers?.Authorization).toBe('Bearer tok-css');
});
@@ -150,7 +150,7 @@ const NON_TARGET: Record<string, Set<string>> = {
'Card', 'Badge', 'StatusBadge', 'Alert', 'EmptyState', 'LoadingSpinner', 'FormField',
'Label', 'FileInput', 'FileUploader', 'HtmlContent', 'HtmlEditor', 'Accordion', 'IconButton',
'StatCard', 'ProductCard', 'TemplateCard', 'ExtensionBadge', 'Toggle', 'Checkbox',
'ChipCheckbox', 'MultilingualTagInput',
'ChipCheckbox', 'MultilingualTagInput', 'SectionHeader', 'ThemeToggle',
]),
basic: new Set([
'Div', 'Span', 'P', 'H1', 'H2', 'H3', 'H4', 'A', 'Button', 'Img', 'Icon', 'Hr',
@@ -59,6 +59,7 @@ import { ComputedForm } from './ComputedForm';
import { InitialStateForm } from './InitialStateForm';
import { ErrorHandlingForm } from './ErrorHandlingForm';
import { DataSourceTab } from './DataSourceTab';
import { suffixed } from '../../../../support/assetUrl';
/** 8탭 키 — 헤더 와이어프레임 순서 */
export type PageSettingsTabKey =
@@ -124,7 +125,7 @@ async function fetchSeoExtensions(templateIdentifier: string): Promise<SeoExtens
if (typeof fetch !== 'function' || !templateIdentifier) return [];
try {
const response = await fetch(
`/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/seo-candidates.json`,
suffixed(`/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/seo-candidates`, 'json'),
{ credentials: 'same-origin', headers: buildAuthHeaders() },
);
if (!response.ok) return [];
@@ -31,6 +31,7 @@ import { SeoBotPreviewPanel } from './SeoBotPreviewPanel';
import type { BindingCandidate } from '../../spec/bindingCandidates';
import type { DataSourceOption } from '../../spec/candidatePools';
import { DataSourceChipLabel } from './DataSourceChipLabel';
import { suffixed } from '../../../../support/assetUrl';
/** seo.extensions 칩 — `{type,id}` */
export interface SeoExtensionRef {
@@ -171,7 +172,7 @@ export function SeoForm({
}
const qs = params.toString() ? `?${params.toString()}` : '';
const res = await fetch(
`/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/seo-candidates.json${qs}`,
suffixed(`/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/seo-candidates`, 'json', null, qs.replace(/^[?&]+/, '')),
{ credentials: 'same-origin', headers: buildAuthHeaders() },
);
const body = await res.json().catch(() => null);
@@ -30,6 +30,7 @@ import {
type EditorAccessError,
} from '../types/editorErrors';
import { buildAuthHeaders } from '../utils/authToken';
import { suffixed } from '../../../support/assetUrl';
const logger = createLogger('useEditorRoutes');
@@ -246,7 +247,7 @@ export function useEditorRoutes(options: UseEditorRoutesOptions): void {
// 먼저 실행되는 진입 fetch 이므로, 이를 가드 엔드포인트 + Bearer 토큰으로 전환하면
// 미인증/권한부족이 진입 시점에 즉시 401/403 으로 감지되어 chrome(트리/캔버스)이
// 렌더되기 전 AccessErrorPanel → 로그인 리다이렉트로 분기된다.
const url = `/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/routes.json${versionQuery}`;
const url = suffixed(`/api/admin/templates/${encodeURIComponent(templateIdentifier)}/editor/routes`, 'json', null, versionQuery.replace(/^[?&]+/, ''));
// 편집 대상 템플릿의 lang dictionary 도 함께 적재 — 라우트 meta.title 에 있는
// `$t:user.*` 키가 부팅 템플릿(admin) lang 에는 없으므로 별도 로드 필요.
@@ -31,6 +31,7 @@ import {
type EditorAccessError,
} from '../types/editorErrors';
import { reseedPendingIntoEngine } from './pendingCustomTranslations';
import { suffixed } from '../../../support/assetUrl';
export interface EditorTemplateAssetsState {
componentRegistry: ComponentRegistry | null;
@@ -98,7 +99,7 @@ async function fetchLatestCacheVersion(identifier: string): Promise<number> {
try {
if (typeof fetch !== 'function') return 0;
const res = await fetch(
`/api/templates/${encodeURIComponent(identifier)}/config.json`,
suffixed(`/api/templates/${encodeURIComponent(identifier)}/config`, 'json'),
{ credentials: 'same-origin', headers: { Accept: 'application/json' } },
);
if (!res.ok) return 0;
@@ -46,6 +46,7 @@ import { trackEditorDocument } from '../devtools/editorTrackers';
import { readSanctumToken } from '../utils/authToken';
import { getCacheBustNonce, bumpCacheBustNonce } from '../utils/editorCacheBust';
import type { SaveResult } from './useLayoutDocument';
import { suffixed } from '../../../support/assetUrl';
/**
* 확장 편집 모드의 가상 path(`__extension__/{extensionId}`)에서 extensionId 를 추출한다.
@@ -420,9 +421,7 @@ export function useExtensionDocument(): UseExtensionDocumentResult {
const cacheVersion = (window as any).G7Config?.cache_version ?? 0;
// 클라이언트 캐시-버스트 nonce 합성 — useLayoutDocument 와 공용 카운터.
// 버전 복원/저장 후 reload 시 HTTP 캐시 stale 호스트 응답을 우회한다(확장 캔버스 미갱신 결함).
const url = `/api/layouts/${encodeURIComponent(
templateIdentifier,
)}/${hostLayoutName}.json?with_source_meta=1&v=${cacheVersion}.${getCacheBustNonce()}`;
const url = suffixed(`/api/layouts/${encodeURIComponent(templateIdentifier)}/${hostLayoutName}`, 'json', `${cacheVersion}.${getCacheBustNonce()}`, 'with_source_meta=1');
const token = readSanctumToken();
const headers: Record<string, string> = { Accept: 'application/json' };
if (token) headers.Authorization = `Bearer ${token}`;
@@ -35,6 +35,7 @@ import {
} from '../devtools/editorTrackers';
import { setPendingValue, setPendingValues } from './pendingCustomTranslations';
import { TranslationEngine } from '../../TranslationEngine';
import { suffixed } from '../../../support/assetUrl';
import {
stripBindingTokens,
buildParamizedKeyText,
@@ -739,7 +740,7 @@ export async function bustTranslationCache(
// 최신 cache_version 을 노출하므로 그것을 읽어 엔진 버전을 갱신한 뒤 재로드한다.
try {
const cfg = await fetch(
`/api/templates/${encodeURIComponent(templateIdentifier)}/config.json`,
suffixed(`/api/templates/${encodeURIComponent(templateIdentifier)}/config`, 'json'),
{ headers: { Accept: 'application/json' }, credentials: 'same-origin' },
).then((r) => (r.ok ? r.json() : null));
const v = cfg?.data?.cache_version ?? cfg?.cache_version;
@@ -32,6 +32,7 @@ import { parseEditorPath } from './useElementSelection';
import { readSanctumToken } from '../utils/authToken';
import { getCacheBustNonce, bumpCacheBustNonce } from '../utils/editorCacheBust';
import { hasPending, flushPending } from './pendingCustomTranslations';
import { suffixed } from '../../../support/assetUrl';
/**
* 로드된 레이아웃 문서 — 백엔드 응답의 data 부분 (병합 + 확장 + 메타 포함)
@@ -440,7 +441,7 @@ export function useLayoutDocument(): UseLayoutDocumentResult {
const cacheVersion = (window as any).G7Config?.cache_version ?? 0;
// 저장 후 클라이언트 캐시-버스트 nonce 합성 — 같은 세션 저장→재로드가
// 옛 cache_version 으로 stale 응답을 받지 않도록 단조 증가 nonce 를 함께 붙인다.
const url = `/api/layouts/${encodeURIComponent(templateIdentifier)}/${layoutName}.json?with_source_meta=1&v=${cacheVersion}.${getCacheBustNonce()}`;
const url = suffixed(`/api/layouts/${encodeURIComponent(templateIdentifier)}/${layoutName}`, 'json', `${cacheVersion}.${getCacheBustNonce()}`, 'with_source_meta=1');
const token = readSanctumToken();
const headers: Record<string, string> = { Accept: 'application/json' };
if (token) {
+21 -4
View File
@@ -16,9 +16,16 @@
@include('partials.error-fallback-styles')
@endif
{{-- 자산 URL 자가 복구 헬퍼 — CSS <link> 의 onerror 보다 먼저 정의되어야 한다 --}}
@include('partials.asset-url-recovery')
<!-- 템플릿 컴포넌트 스타일 -->
@if(!empty($activeAdminTemplate))
<link rel="stylesheet" href="/api/templates/assets/{{ $activeAdminTemplate }}/css/components.css?v={{ $extensionCacheVersion }}">
{{-- onerror: 정적 최적화 서버에서 확장자 붙은 CSS 가 가로채였을 때
확장자 없는 형태로 1회 교체한다(무스타일 화면 방지). 링크당 1회. --}}
<link rel="stylesheet"
href="{{ \App\Support\AssetUrl::templateAsset($activeAdminTemplate, 'css/components.css', $extensionCacheVersion) }}"
onerror="window.__g7AssetUrl && window.__g7AssetUrl.recoverStylesheet(this);">
@endif
</head>
<body>
@@ -54,7 +61,14 @@
// 키가 새 버전으로 전환되어 routes.json/lang 변경이 즉시 가시화된다.
// 미주입 시 클라이언트가 항상 `v0` 으로 호출 → `template:cache-clear` 가 v 와일드카드를
// 처리하지 못해 캐시가 영구 stale 되는 결함이 발생.
cache_version: {{ (int) ($extensionCacheVersion ?? 0) }}
cache_version: {{ (int) ($extensionCacheVersion ?? 0) }},
// 자산 URL 모드 — 'extension'(기본) | 'extensionless'.
// 정적 최적화 블록이 동적 응답을 가로채는 서버에서 확장자 없는 형태로 전환.
// 부트스트랩 자가 복구가 런타임에 뒤집으므로 최상위 키로 노출한다.
// 자가 복구가 <head> 에서 이미 전환을 확정했다면 그 값을 잇는다.
// 이 대입은 G7Config 객체를 통째로 교체하므로, 독립 전역을 읽지 않으면
// 복원/전환 결과가 여기서 덮여 사라진다.
assetUrlMode: window.__g7AssetUrlMode || '{{ \App\Support\AssetUrl::mode() }}'
};
@if(isset($errorCode) && isset($errorLayout))
// 에러 상태 정보 (503 의존성 미충족 등)
@@ -71,8 +85,11 @@
{{-- 코어 엔진 + 템플릿 컴포넌트 번들 로드 → 초기화 (재시도 + 폴백 UI) --}}
@include('partials.bootstrap-scripts', [
'templateType' => 'admin',
'coreEngineSrc' => asset('build/core/template-engine.min.js') . '?v=' . filemtime(public_path('build/core/template-engine.min.js')),
'componentsSrc' => '/api/templates/assets/' . $activeAdminTemplate . '/js/components.iife.js?v=' . $extensionCacheVersion,
{{-- 코어 엔진 번들은 public/ 의 실물 정적 파일이라 자산 URL 이중 모드 대상이 아니다.
미빌드 상태에서 filemtime() 이 warning 을 내지 않도록 file_exists 가드
(coreEditorAsset/coreDevToolsAsset 와 동일 패턴). --}}
'coreEngineSrc' => asset('build/core/template-engine.min.js') . '?v=' . (file_exists(public_path('build/core/template-engine.min.js')) ? filemtime(public_path('build/core/template-engine.min.js')) : 0),
'componentsSrc' => \App\Support\AssetUrl::templateAsset($activeAdminTemplate, 'js/components.iife.js', $extensionCacheVersion),
'initConfig' => array_filter([
'templateId' => $activeAdminTemplate,
'templateType' => 'admin',
+23 -4
View File
@@ -16,9 +16,16 @@
@include('partials.error-fallback-styles')
@endif
{{-- 자산 URL 자가 복구 헬퍼 — CSS <link> 의 onerror 보다 먼저 정의되어야 한다 --}}
@include('partials.asset-url-recovery')
<!-- 템플릿 컴포넌트 스타일 -->
@if(!empty($activeUserTemplate))
<link rel="stylesheet" href="/api/templates/assets/{{ $activeUserTemplate }}/css/components.css?v={{ $extensionCacheVersion }}">
{{-- onerror: 정적 최적화 서버에서 확장자 붙은 CSS 가 가로채였을 때
확장자 없는 형태로 1회 교체한다(무스타일 화면 방지). 링크당 1회. --}}
<link rel="stylesheet"
href="{{ \App\Support\AssetUrl::templateAsset($activeUserTemplate, 'css/components.css', $extensionCacheVersion) }}"
onerror="window.__g7AssetUrl && window.__g7AssetUrl.recoverStylesheet(this);">
@endif
</head>
<body>
@@ -51,7 +58,16 @@
coreDevToolsAsset: '{{ asset('build/core/devtools.min.js') }}?v={{ file_exists(public_path('build/core/devtools.min.js')) ? filemtime(public_path('build/core/devtools.min.js')) : 0 }}',
// 확장 캐시 버전 SSoT — 클라이언트 fetch (`?v=`) 동반 필수.
// 자세한 설명은 admin.blade.php 참조.
cache_version: {{ (int) ($extensionCacheVersion ?? 0) }}
cache_version: {{ (int) ($extensionCacheVersion ?? 0) }},
// 자산 URL 모드 — 'extension'(기본) | 'extensionless'.
// 정적 최적화 블록(location ~* \.(js|css|json)$)이 동적 응답을 가로채는
// 서버에서 확장자 없는 형태로 전환한다. 부트스트랩 자가 복구가 실패 시
// 이 값을 런타임에 뒤집으므로 최상위 키로 노출한다(설정값 SSoT 는
// G7Config.settings.general.asset_url_mode).
// 자가 복구가 <head> 에서 이미 전환을 확정했다면 그 값을 잇는다.
// 이 대입은 G7Config 객체를 통째로 교체하므로, 독립 전역을 읽지 않으면
// 복원/전환 결과가 여기서 덮여 사라진다.
assetUrlMode: window.__g7AssetUrlMode || '{{ \App\Support\AssetUrl::mode() }}'
};
@if(isset($errorCode) && isset($errorLayout))
// 에러 상태 정보 (503 의존성 미충족 등)
@@ -68,8 +84,11 @@
{{-- 코어 엔진 + 템플릿 컴포넌트 번들 로드 → 초기화 (재시도 + 폴백 UI) --}}
@include('partials.bootstrap-scripts', [
'templateType' => 'user',
'coreEngineSrc' => asset('build/core/template-engine.min.js') . '?v=' . filemtime(public_path('build/core/template-engine.min.js')),
'componentsSrc' => '/api/templates/assets/' . $activeUserTemplate . '/js/components.iife.js?v=' . $extensionCacheVersion,
{{-- 코어 엔진 번들은 public/ 의 실물 정적 파일이라 자산 URL 이중 모드 대상이 아니다.
미빌드 상태에서 filemtime() 이 warning 을 내지 않도록 file_exists 가드
(coreEditorAsset/coreDevToolsAsset 와 동일 패턴). --}}
'coreEngineSrc' => asset('build/core/template-engine.min.js') . '?v=' . (file_exists(public_path('build/core/template-engine.min.js')) ? filemtime(public_path('build/core/template-engine.min.js')) : 0),
'componentsSrc' => \App\Support\AssetUrl::templateAsset($activeUserTemplate, 'js/components.iife.js', $extensionCacheVersion),
'initConfig' => array_filter([
'templateId' => $activeUserTemplate,
'templateType' => 'user',
+4
View File
@@ -844,6 +844,10 @@ if (isset($_GET['ajax_action'])) {
<span>SEO 캐시 삭제</span>
<span class="text-[10px] opacity-60">(seo:clear)</span>
</button>
<button onclick="runCommand('g7:asset-url-mode')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-slate-600 hover:bg-slate-700 text-white text-xs font-medium rounded transition-colors">
<span>자산 URL 방식 확인</span>
<span class="text-[10px] opacity-60">(g7:asset-url-mode)</span>
</button>
<button onclick="runCommand('seo:stats')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded transition-colors">
<span>SEO 통계</span>
<span class="text-[10px] opacity-60">(seo:stats)</span>
@@ -0,0 +1,177 @@
{{--
자산 URL 모드 자가 복구 — 공용 헬퍼 (이슈 #486 §5)
`<head>` 의 CSS `<link>` 보다 **먼저** 로드되어야 한다. CSS 링크의 onerror 가
이 헬퍼를 호출하기 때문이며, 부트스트랩 스크립트(`<body>`)도 같은 헬퍼를 재사용한다.
코어 번들 로드 전에 실행되므로 외부 의존성 없는 순수 인라인 JS 여야 한다
(닭-달걀: 코어가 없으면 코어를 받아올 코드도 없다).
## 배경
nginx 의 정적 최적화 블록은 정규식 location 이라 프리픽스 location 보다 먼저
매칭된다. 그 안에 PHP 핸들러가 없으면 확장자 붙은 동적 엔드포인트가 PHP 에
도달하지 못한 채 404 가 된다. 관리자 화면조차 뜨지 않으므로 "설정을 바꾸세요"
안내는 순환 참조 — 브라우저가 스스로 복구해야 한다.
## 불변식 (계획서 §12)
L1 전환은 단방향(extension → extensionless) 1회. 역방향 폴백을 만들지 않는다.
L3 자동 location.reload() 금지.
L5 서버 설정은 건드리지 않는다 (미인증 클라이언트의 전역 설정 변경 금지).
L7 localStorage 캐시는 cache_version 을 키에 포함하고 TTL 을 둔다.
--}}
<script>
(function () {
'use strict';
var MODE_EXTENSIONLESS = 'extensionless';
var FILE_QUERY_PARAM = 'file';
var STORAGE_TTL_MS = 24 * 60 * 60 * 1000;
// cache_version 은 blade 에서 직접 주입한다. 이 파샬은 <head> 에서 실행되어
// window.G7Config(<body> 에서 정의) 가 아직 없기 때문 — 참조하면 항상 0 이 되어
// 캐시 키가 어긋난다.
var CACHE_VERSION = {{ (int) ($extensionCacheVersion ?? 0) }};
function storageKey() {
return 'g7_asset_url_mode:' + CACHE_VERSION;
}
/**
* 확장자 형태 API URL 을 확장자 없는 형태로 변환한다.
*
* `resources/js/core/support/assetUrl.ts::convertToCurrentMode` 와 **동일 규칙**이어야
* 한다. 여기는 코어 번들 로드 전이라 import 가 불가능해 인라인으로 둔다 — 드리프트는
* `assetUrl.test.ts` 의 대조 케이스가 잡는다.
*
* 변환 대상이 아니면 null. 코어 엔진 번들(`/build/...`)은 public/ 의 실물 정적
* 파일이라 변환하면 오히려 깨지므로 대상에서 제외된다.
*
* @param url 원본 URL
* @returns 변환된 URL 또는 null
*/
function toExtensionless(url) {
if (!url) return null;
// `<script>.src` / `<link>.href` 는 **절대 URL** 을 돌려준다
// (`https://host/api/...`). same-origin 이면 경로만 남긴다 — 이 정규화를
// 빠뜨리면 부트스트랩 재시도 경로에서 변환이 항상 null 이 되어 자가 복구가
// 통째로 죽는다(외형은 "3회 재시도 후 폴백 UI" 라 원인이 드러나지 않는다).
var origin = window.location && window.location.origin;
if (origin && url.indexOf(origin) === 0) {
url = url.slice(origin.length);
}
if (url.indexOf('/api/') !== 0) return null;
var qi = url.indexOf('?');
var path = qi === -1 ? url : url.slice(0, qi);
var query = qi === -1 ? '' : url.slice(qi + 1);
var bundle = path.match(/^\/api\/(modules|plugins)\/bundle\.(js|css)$/);
if (bundle) {
return '/api/' + bundle[1] + '/bundle/' + bundle[2] + (query ? '?' + query : '');
}
var asset = path.match(/^\/api\/(templates|modules|plugins)\/assets\/([^/]+)\/(.+)$/);
if (asset) {
return '/api/' + asset[1] + '/assets/' + asset[2] +
'?' + FILE_QUERY_PARAM + '=' + encodeURIComponent(decodeURIComponent(asset[3])) +
(query ? '&' + query : '');
}
var suffixed = path.match(/^(\/api\/.+)\.(json|js|css)$/);
if (suffixed) {
return suffixed[1] + (query ? '?' + query : '');
}
return null;
}
/**
* 모드 전환을 1회 확정한다 (L1 — 단방향 1회).
*
* 서버 설정은 바꾸지 않는다(L5). 전역 플래그와 localStorage 캐시만 갱신하며,
* 이후 엔진의 모든 fetch 가 이 값을 읽어 올바른 형태를 쓴다.
*
* @returns 이번 호출로 전환되었으면 true
*/
function switchToExtensionless() {
if (window.__g7AssetUrlMode === MODE_EXTENSIONLESS) return false;
// 독립 전역에 기록한다. <body> 의 `window.G7Config = {...}` 대입이 이 파샬보다
// 나중에 실행되어 객체를 통째로 교체하므로, G7Config 에만 쓰면 덮여 사라진다.
// G7Config 는 이 값을 초기값으로 읽어간다(app/admin blade 참조).
window.__g7AssetUrlMode = MODE_EXTENSIONLESS;
if (window.G7Config) window.G7Config.assetUrlMode = MODE_EXTENSIONLESS;
try {
window.localStorage.setItem(storageKey(), JSON.stringify({
mode: MODE_EXTENSIONLESS,
at: Date.now()
}));
} catch (e) { /* 저장 실패는 치명적이지 않다 */ }
return true;
}
/**
* 이전 방문에서 확정된 모드를 복원한다 (L7).
*
* cache_version 을 키에 포함하고 TTL 을 둬서 서버가 정상화된 뒤에도
* 클라이언트가 옛 모드에 영구 고착되지 않도록 한다.
*/
function restoreCachedMode() {
try {
var raw = window.localStorage.getItem(storageKey());
if (!raw) return false;
var parsed = JSON.parse(raw);
if (!parsed || parsed.mode !== MODE_EXTENSIONLESS) return false;
if (typeof parsed.at !== 'number' || Date.now() - parsed.at > STORAGE_TTL_MS) {
window.localStorage.removeItem(storageKey());
return false;
}
window.__g7AssetUrlMode = MODE_EXTENSIONLESS;
if (window.G7Config) window.G7Config.assetUrlMode = MODE_EXTENSIONLESS;
return true;
} catch (e) {
return false;
}
}
/**
* CSS `<link>` 로드 실패 시 확장자 없는 형태로 1회 교체한다.
*
* 스타일 부재는 앱을 죽이지 않으므로(엔진의 CSS 로더도 실패를 resolve 로 처리)
* 재교체가 실패해도 조용히 끝낸다 — 폴백 UI 를 띄우지 않는다(과잉 적용 경계).
* 교체는 링크당 1회만 (`data-g7-recovered` 마킹).
*
* @param link 실패한 <link> 요소
*/
function recoverStylesheet(link) {
if (!link || link.getAttribute('data-g7-recovered') === '1') return;
link.setAttribute('data-g7-recovered', '1');
var converted = toExtensionless(link.getAttribute('href') || '');
if (!converted) return;
switchToExtensionless();
link.href = converted;
}
restoreCachedMode();
window.__g7AssetUrl = {
MODE_EXTENSIONLESS: MODE_EXTENSIONLESS,
toExtensionless: toExtensionless,
switchToExtensionless: switchToExtensionless,
restoreCachedMode: restoreCachedMode,
recoverStylesheet: recoverStylesheet
};
})();
</script>
@@ -38,11 +38,37 @@
window.addEventListener('pagehide', function () { window.__g7Unloading = true; });
window.addEventListener('pageshow', function (e) { if (e.persisted) window.__g7Unloading = false; });
// ─────────────────────────────────────────────────────────────────────────
// 자산 URL 모드 자가 복구 (이슈 #486)
//
// nginx 의 정적 최적화 블록(`location ~* \.(js|css|json)$`)은 정규식 location 이라
// 프리픽스 location 보다 먼저 매칭되고, 그 안에 PHP 핸들러가 없으면 확장자 붙은
// 동적 엔드포인트가 PHP 에 도달하지 못한 채 404 가 된다. 이 경우 관리자 화면조차
// 뜨지 않으므로 "설정을 바꾸세요" 안내는 순환 참조다 — 브라우저가 스스로 복구한다.
//
// 불변식 (계획서 §12):
// L1 전환은 단방향(extension → extensionless) 1회. 역방향 폴백을 만들지 않는다.
// 양쪽 다 실패하는 상황(PHP 다운·WAF)에서 무한 왕복이 되기 때문.
// L2 전환 시도는 기존 MAX_ATTEMPTS 예산 안에서 소비한다(별도 예산 신설 금지).
// L3 자동 location.reload() 금지. 새로고침은 폴백 UI 의 사용자 클릭만.
// L4 모든 실패 경로는 renderFallback() 으로 수렴하고 종료한다.
// L5 서버 설정은 건드리지 않는다(미인증 클라이언트의 전역 설정 변경 금지).
// L8 pending 카운터 — 실패 element 제거 후 교체, onload 는 정확히 1회만 감소.
// ─────────────────────────────────────────────────────────────────────────
// 자산 URL 모드 자가 복구 헬퍼 — `partials/asset-url-recovery` 가 <head> 에서
// 미리 정의한다(CSS <link> 의 onerror 가 그것을 먼저 쓰기 때문). 여기서는 재사용만 한다.
// 헬퍼가 없으면(부분 include 등) 자가 복구 없이 기존 재시도 로직으로만 동작한다.
var assetUrl = window.__g7AssetUrl || null;
// 부트스트랩 상태 — 정적 <script> 의 onerror/onload 가 여기에 기록한다
var bootstrap = window.__g7Bootstrap = {
failed: false,
pending: 0,
/** 모드 전환을 이미 시도했는지 (L1 — 페이지 수명당 1회) */
modeSwitched: false,
/**
* 정적 <script> 로드 실패 시 동적 재시도를 시작한다.
*
@@ -64,6 +90,19 @@
return;
}
// 자산 URL 모드 전환 (L1·L2) — 확장자 형태가 실패했고 아직 전환 전이라면,
// 지연 재시도 대신 확장자 없는 형태로 **즉시 1회** 시도한다.
// 이 시도는 기존 예산(attempt)을 그대로 소비하므로 총 시도 횟수는 불변이다.
var converted = (bootstrap.modeSwitched || !assetUrl) ? null : assetUrl.toExtensionless(src);
if (converted && converted !== src) {
bootstrap.modeSwitched = true;
assetUrl.switchToExtensionless();
console.warn(LABEL + ' Retrying with extensionless URL: ' + converted);
bootstrap.replaceScript(converted, attempt);
return;
}
var delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
console.warn(LABEL + ' Script load failed (attempt ' + attempt + '/' + MAX_ATTEMPTS + '), retrying in ' + delay + 'ms: ' + src);
@@ -83,6 +122,33 @@
}, delay);
},
/**
* 스크립트를 지연 없이 즉시 교체 삽입한다 (모드 전환 재시도용).
*
* L8 — onload 는 pending 을 정확히 1회만 감소시키고, 실패한 element 는
* 제거한 뒤 다음 경로로 넘어간다. 실패 시에도 attempt 를 증가시켜
* MAX_ATTEMPTS 예산을 공유하므로 총 네트워크 시도 횟수가 늘지 않는다(L2).
*
* @param src 삽입할 스크립트 URL
* @param attempt 현재 시도 번호
*/
replaceScript: function (src, attempt) {
if (window.__g7Unloading) return;
var script = document.createElement('script');
script.src = src;
script.async = false; // 삽입 순서대로 실행 (코어 → 컴포넌트 순서 보장)
script.onload = function () {
bootstrap.pending -= 1;
bootstrap.tryInit();
};
script.onerror = function () {
if (script.parentNode) script.parentNode.removeChild(script);
bootstrap.retry(src, attempt + 1);
};
document.head.appendChild(script);
},
/**
* 부트스트랩 최종 실패 시 사용자에게 보이는 정적 폴백을 심는다.
*
+41 -27
View File
@@ -47,6 +47,7 @@ use App\Http\Controllers\Api\Auth\AuthController as UserAuthController;
use App\Http\Controllers\Api\Auth\NotificationController as UserNotificationController;
use App\Http\Controllers\Api\Auth\ProfileController as UserProfileController;
use App\Http\Controllers\Api\Identity\IdentityVerificationController;
use App\Http\Controllers\Api\Public\AssetProbeController;
use App\Http\Controllers\Api\Public\LayoutPreviewController;
use App\Http\Controllers\Api\Public\LocaleController as PublicLocaleController;
use App\Http\Controllers\Api\Public\PublicAttachmentController;
@@ -77,25 +78,29 @@ use Illuminate\Support\Facades\Route;
Route::group([], function () {
// 템플릿 라우트 정보 조회
Route::prefix('templates')->group(function () {
Route::get('{identifier}/routes.json', [PublicTemplateController::class, 'getRoutes'])->name('api.public.templates.routes');
// 아래 dual* 매크로는 확장자 형태와 확장자 없는 형태를 동시에 등록한다.
// 정적 최적화 블록(location ~* \.(js|css|json)$)이 있는 서버에서 동적 응답이
// nginx 에 가로채이지 않도록 하기 위함. 상세: App\Support\Routing\DualExtensionRoute
Route::dualSuffix('{identifier}/routes', 'json', [PublicTemplateController::class, 'getRoutes'])
->name('api.public.templates.routes');
// 템플릿 설정 파일 서빙 (error_config 등)
Route::get('{identifier}/config.json', [PublicTemplateController::class, 'serveConfig'])->name('api.public.templates.config');
Route::dualSuffix('{identifier}/config', 'json', [PublicTemplateController::class, 'serveConfig'])
->name('api.public.templates.config');
// 레이아웃 편집기 스펙 조회
Route::get('{identifier}/editor-spec', [PublicTemplateController::class, 'serveEditorSpec'])->name('api.public.templates.editor_spec');
// 템플릿 정적 파일 서빙
Route::get('assets/{identifier}/{path}', [PublicTemplateController::class, 'serveAsset'])
->where('path', '.*')
// 템플릿 정적 파일 서빙 (확장자 없는 형태는 ?file= 로 경로 수신)
Route::dualAsset('assets/{identifier}', [PublicTemplateController::class, 'serveAsset'])
->name('api.public.templates.assets');
// 컴포넌트 정의 파일 서빙
Route::get('{identifier}/components.json', [PublicTemplateController::class, 'serveComponents'])
Route::dualSuffix('{identifier}/components', 'json', [PublicTemplateController::class, 'serveComponents'])
->name('api.public.templates.components');
// 다국어 파일 서빙
Route::get('{identifier}/lang/{locale}.json', [PublicTemplateController::class, 'serveLanguage'])
Route::dualSuffix('{identifier}/lang/{locale}', 'json', [PublicTemplateController::class, 'serveLanguage'])
->where('locale', '[a-z]{2}(-[A-Z]{2})?')
->name('api.public.templates.language');
@@ -114,11 +119,13 @@ Route::group([], function () {
->group(function () {
// 레이아웃 미리보기 서빙 (토큰 기반, 인증 불필요)
// 주의: 일반 레이아웃 서빙보다 먼저 정의 (preview가 templateIdentifier로 매칭되는 것 방지)
Route::get('preview/{token}.json', [LayoutPreviewController::class, 'serve'])
Route::dualSuffix('preview/{token}', 'json', [LayoutPreviewController::class, 'serve'])
->where('token', '[a-f0-9\-]{36}')
->name('api.public.layouts.preview.serve');
Route::get('{templateIdentifier}/{layoutName}.json', [PublicLayoutController::class, 'serve'])
// 주의: dualSuffix 는 확장자 형태를 먼저 등록한다. layoutName 정규식이 `.` 를
// 포함해 greedy 하므로, 확장자 없는 형태가 먼저 등록되면 `.json` 요청까지 삼킨다.
Route::dualSuffix('{templateIdentifier}/{layoutName}', 'json', [PublicLayoutController::class, 'serve'])
->where('layoutName', '[a-zA-Z0-9_/\.-]+')
->name('api.public.layouts.serve');
});
@@ -126,13 +133,13 @@ Route::group([], function () {
// 모듈 에셋 서빙 API
Route::prefix('modules')->group(function () {
// 활성 모듈 프론트엔드 IIFE/CSS 병합 번들 (개별 assets 라우트보다 위에 명시 등록)
Route::get('bundle.js', [PublicModuleController::class, 'serveBundleJs'])
// 접미사가 js/css 를 구분하므로 제거 불가 — 세그먼트로 내린다 (bundle/js, bundle/css).
Route::dualSuffixSegment('bundle', 'js', [PublicModuleController::class, 'serveBundleJs'])
->name('api.public.modules.bundle.js');
Route::get('bundle.css', [PublicModuleController::class, 'serveBundleCss'])
Route::dualSuffixSegment('bundle', 'css', [PublicModuleController::class, 'serveBundleCss'])
->name('api.public.modules.bundle.css');
Route::get('assets/{identifier}/{path}', [PublicModuleController::class, 'serveAsset'])
->where('path', '.*')
Route::dualAsset('assets/{identifier}', [PublicModuleController::class, 'serveAsset'])
->name('api.public.modules.assets');
// 레이아웃 편집기 스펙 조회
@@ -140,20 +147,19 @@ Route::group([], function () {
->name('api.public.modules.editor_spec');
// 컴포넌트 정의 파일 서빙 (module:build 산출물)
Route::get('{identifier}/components.json', [PublicModuleController::class, 'serveComponents'])
Route::dualSuffix('{identifier}/components', 'json', [PublicModuleController::class, 'serveComponents'])
->name('api.public.modules.components');
});
// 플러그인 에셋 서빙 API
Route::prefix('plugins')->group(function () {
// 활성 플러그인 프론트엔드 IIFE/CSS 병합 번들 (개별 assets 라우트보다 위에 명시 등록)
Route::get('bundle.js', [PublicPluginController::class, 'serveBundleJs'])
Route::dualSuffixSegment('bundle', 'js', [PublicPluginController::class, 'serveBundleJs'])
->name('api.public.plugins.bundle.js');
Route::get('bundle.css', [PublicPluginController::class, 'serveBundleCss'])
Route::dualSuffixSegment('bundle', 'css', [PublicPluginController::class, 'serveBundleCss'])
->name('api.public.plugins.bundle.css');
Route::get('assets/{identifier}/{path}', [PublicPluginController::class, 'serveAsset'])
->where('path', '.*')
Route::dualAsset('assets/{identifier}', [PublicPluginController::class, 'serveAsset'])
->name('api.public.plugins.assets');
// 레이아웃 편집기 스펙 조회
@@ -161,7 +167,7 @@ Route::group([], function () {
->name('api.public.plugins.editor_spec');
// 컴포넌트 정의 파일 서빙 (plugin:build 산출물)
Route::get('{identifier}/components.json', [PublicPluginController::class, 'serveComponents'])
Route::dualSuffix('{identifier}/components', 'json', [PublicPluginController::class, 'serveComponents'])
->name('api.public.plugins.components');
});
@@ -177,6 +183,12 @@ Route::group([], function () {
// 활성 로케일 목록 — 언어팩 설치/활성화 직후 셀렉터 즉시 갱신용
Route::get('locales/active', [PublicLocaleController::class, 'active'])
->name('api.public.locales.active');
// 자산 URL 모드 감지 프로브 — 정적 최적화 블록이 확장자 붙은 동적 응답을
// 가로채는지 쌍으로 판정한다. 매크로 자기적용(확장자 형태 + 대조군).
// DB 미접근·무인증·no-store. 상세: App\Http\Controllers\Api\Public\AssetProbeController
Route::dualSuffix('system/asset-probe', 'js', [AssetProbeController::class, 'probe'])
->name('api.public.system.asset-probe');
});
// 브로드캐스팅 인증 (Sanctum 토큰 사용)
@@ -757,32 +769,34 @@ Route::prefix('admin')->middleware(['auth:sanctum', 'check.user_status', 'admin'
Route::get('{identifier}/editor-assets', [AdminTemplateAssetController::class, 'getEditorAssets'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-assets');
Route::get('{identifier}/editor/components.json', [AdminTemplateAssetController::class, 'serveComponents'])
Route::dualSuffix('{identifier}/editor/components', 'json', [AdminTemplateAssetController::class, 'serveComponents'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-components');
Route::get('{identifier}/editor/routes.json', [AdminTemplateAssetController::class, 'serveRoutes'])
Route::dualSuffix('{identifier}/editor/routes', 'json', [AdminTemplateAssetController::class, 'serveRoutes'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-routes');
Route::get('{identifier}/editor/editor-spec.json', [AdminTemplateAssetController::class, 'serveEditorSpec'])
Route::dualSuffix('{identifier}/editor/editor-spec', 'json', [AdminTemplateAssetController::class, 'serveEditorSpec'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-spec');
Route::get('{identifier}/editor/lang/{locale}.json', [AdminTemplateAssetController::class, 'serveLanguage'])
Route::dualSuffix('{identifier}/editor/lang/{locale}', 'json', [AdminTemplateAssetController::class, 'serveLanguage'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-lang');
// 표시 권한 후보 — 코어 + 활성 확장 권한 (속성 모달 표시 권한 TagInput).
// 편집 권한 가드 하에서만 노출(전역 G7Config 상시 노출 회피).
Route::get('{identifier}/editor/permission-candidates.json', [AdminTemplateAssetController::class, 'servePermissionCandidates'])
Route::dualSuffix('{identifier}/editor/permission-candidates', 'json', [AdminTemplateAssetController::class, 'servePermissionCandidates'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-permission-candidates');
// 편집기 프리뷰 전용 CSS — 다크 셀렉터를 프리뷰 마커(.g7le-preview-dark)로 치환해 서빙
// 관리자 admin 의 html.dark 조상과 독립적으로 프리뷰 라이트/다크 격리.
Route::get('{identifier}/editor/components.css', [AdminTemplateAssetController::class, 'serveEditorCss'])
// URI 가 `components` 가 아니라 `component-styles` 인 이유: 확장자를 떼면
// `editor/components.json` 의 확장자 없는 형태와 충돌한다.
Route::dualSuffix('{identifier}/editor/component-styles', 'css', [AdminTemplateAssetController::class, 'serveEditorCss'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-css');
// 페이지 설정 모달 — SEO 후보/미리보기 + 브로드캐스트 카탈로그.
// 전부 편집 권한 가드 하 편집기 전용. {templateName} 동적 라우트보다 위에 둠.
Route::get('{identifier}/editor/seo-candidates.json', [SeoCandidateController::class, 'index'])
Route::dualSuffix('{identifier}/editor/seo-candidates', 'json', [SeoCandidateController::class, 'index'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-seo-candidates');
Route::post('{identifier}/editor/seo-og-preview', [SeoOgPreviewController::class, 'show'])
@@ -791,7 +805,7 @@ Route::prefix('admin')->middleware(['auth:sanctum', 'check.user_status', 'admin'
Route::post('{identifier}/editor/seo-bot-preview', [SeoBotPreviewController::class, 'show'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-seo-bot-preview');
Route::get('{identifier}/editor/broadcast-catalog.json', [BroadcastCatalogController::class, 'index'])
Route::dualSuffix('{identifier}/editor/broadcast-catalog', 'json', [BroadcastCatalogController::class, 'index'])
->middleware('permission:admin,core.templates.layouts.edit')
->name('api.admin.templates.editor-broadcast-catalog');
@@ -8,6 +8,8 @@
### Added
- 일반 설정에 에셋 파일 서빙 방식 항목을 추가했습니다. 정적 파일 최적화 규칙(nginx `location ~* \.(js|css|json|...)$` 블록 등)이 확장자 주소를 가로채 화면이 뜨지 않을 때 확장자 없는 주소로 전환할 수 있습니다. "자동 감지" 버튼을 누르면 판정 결과가 항목 아래에 인라인으로 표시되며, 확장자를 가로채는 환경이면 원인이 되는 서버 규칙까지 안내합니다. 평소에는 자동으로 선택되므로 화면이 정상이면 바꿀 필요가 없습니다.
- 저장된 에셋 파일 서빙 방식이 실제 서버 환경과 다르면 대시보드에서 안내합니다. 브라우저는 방식이 어긋나도 스스로 보정해 화면을 띄우지만 검색엔진 봇에는 보정이 적용되지 않으므로, 설정을 맞춰 두어야 검색 노출이 정상 유지됩니다. 안내에서 바로 환경설정으로 이동할 수 있습니다.
- SEO 설정에 Sitemap 분할 기준(파일당 URL 수)과 Sitemap 압축 항목을 추가했습니다. URL 이 분할 기준을 넘으면 sitemap 이 여러 파일로 나뉘어 생성됩니다.
- SEO 설정에 다국어 대체 링크(hreflang) 항목을 추가했습니다. 다국어 사이트에서 sitemap 에 각 언어 버전을 가리키는 링크를 넣을지 켜고 끌 수 있으며, 끄면 sitemap 크기를 줄일 수 있습니다.
- 고급 설정의 캐시 항목에 Sitemap 캐시 유지 시간을 추가했습니다. SEO 설정의 Sitemap 캐시 항목을 비워두면 이 값을 따릅니다.
@@ -0,0 +1,437 @@
/**
* @file admin-asset-url-mode.test.tsx
* @description 자산 URL 이중 모드 관련 레이아웃 렌더링 테스트 (이슈 #486 §검증)
*
* 계획서 §검증 "회귀 테스트" 표의 레이아웃 렌더링 계층 — `createLayoutTest()` 로
* **두 모드 각각** 렌더한다.
*
* 이 계층이 잡는 것:
*
* 1. **모드가 렌더링에 새어나오지 않는다.** 자산 URL 모드는 URL 생성에만 관여하고
* 화면 구조를 바꾸지 않아야 한다. 누군가 컴포넌트를 모드에 따라 분기시키면
* 확장자 없는 환경에서만 화면이 달라지는데, 그 환경은 개발 중에 거의 재현되지 않아
* 발견이 늦다. 두 모드의 DOM 을 직접 대조해 잠근다.
* 2. **대시보드 드리프트 안내와 환경설정 감지 UI 가 실제로 렌더된다.** 두 화면 모두
* "설정이 어긋난 상태" 를 관리자에게 알리는 유일한 통로다.
*/
import React from 'react';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createLayoutTest, screen } from '@core/template-engine/__tests__/utils/layoutTestUtils';
import { ComponentRegistry } from '@core/template-engine/ComponentRegistry';
import { MODE_EXTENSION, MODE_EXTENSIONLESS } from '@core/support/assetUrl';
const TestDiv: React.FC<any> = ({ className, children, 'data-testid': testId }) => (
<div className={className} data-testid={testId}>{children}</div>
);
const TestSpan: React.FC<any> = ({ className, children, text }) => (
<span className={className}>{children || text}</span>
);
const TestP: React.FC<any> = ({ className, children, text }) => (
<p className={className}>{children || text}</p>
);
const TestButton: React.FC<any> = ({ className, type, children, text, onClick }) => (
<button className={className} type={type} onClick={onClick}>{children || text}</button>
);
const TestLabel: React.FC<any> = ({ className, children, text }) => (
<label className={className}>{children || text}</label>
);
const TestIcon: React.FC<any> = ({ icon, className }) => (
<i className={className} data-icon={icon} />
);
const TestSelect: React.FC<any> = ({ name, options, disabled }) => (
<select name={name} disabled={disabled} data-testid={`select-${name}`}>
{(options ?? []).map((o: any) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
);
const TestFragment: React.FC<any> = ({ children }) => <>{children}</>;
/**
* 레이아웃 테스트용 컴포넌트 레지스트리를 구성한다.
*
* Fragment 를 빠뜨리면 자식 트리가 통째로 렌더되지 않고 빈 컨테이너만 남아,
* 부정 단언("없어야 한다")이 거짓 통과한다.
*/
function setupTestRegistry(): ComponentRegistry {
const registry = ComponentRegistry.getInstance();
(registry as any).registry = {
Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } },
Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } },
P: { component: TestP, metadata: { name: 'P', type: 'basic' } },
Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } },
Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } },
Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } },
Select: { component: TestSelect, metadata: { name: 'Select', type: 'composite' } },
Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } },
};
return registry;
}
/**
* 서버가 내려주는 자산 URL 모드를 흉내낸다.
*
* @param mode 자산 URL 모드
*/
function setServerMode(mode: string): void {
(globalThis as any).G7Config = {
...((globalThis as any).G7Config ?? {}),
assetUrlMode: mode,
settings: { general: { asset_url_mode: mode } },
};
}
/** 환경설정 > 일반 탭의 자산 URL 방식 필드 (레이아웃 원본과 동일 구조) */
const settingsFieldLayout = {
components: [
{
id: 'field_asset_url_mode',
type: 'basic',
name: 'Div',
children: [
{
type: 'basic',
name: 'Label',
props: { className: 'form-label' },
children: [{ type: 'basic', name: 'Span', text: '에셋 파일 서빙 방식' }],
},
{
type: 'basic',
name: 'P',
props: { className: 'form-help' },
text: '서버 설정을 자동으로 감지합니다',
},
{
type: 'basic',
name: 'Div',
props: { className: 'flex gap-2 items-start' },
children: [
{
type: 'basic',
name: 'Div',
props: { className: 'flex-1' },
children: [
{
type: 'composite',
name: 'Select',
props: {
name: 'general.asset_url_mode',
className: 'w-full',
options: [
{ value: 'extension', label: '확장자 사용' },
{ value: 'extensionless', label: '확장자 미사용' },
],
},
},
],
},
{
type: 'basic',
name: 'Button',
props: { type: 'button', className: 'btn btn-secondary whitespace-nowrap' },
children: [{ type: 'basic', name: 'Span', text: '지금 감지' }],
actions: [{ event: 'onClick', handler: 'detectAssetUrlMode' }],
},
],
},
{
type: 'basic',
name: 'P',
if: "{{_local.asset_url_mode_detect_status === 'detecting'}}",
props: { className: 'mt-2 text-sm text-gray-500', 'data-testid': 'detect-msg-detecting' },
text: '감지 중',
},
{
type: 'basic',
name: 'P',
if: "{{_local.asset_url_mode_detect_status === 'extension'}}",
props: { className: 'mt-2 text-sm text-green-600', 'data-testid': 'detect-msg-extension' },
text: '확장자 사용 방식으로 감지되었습니다',
},
{
type: 'basic',
name: 'P',
if: "{{_local.asset_url_mode_detect_status === 'extensionless'}}",
props: { className: 'mt-2 text-sm text-amber-600', 'data-testid': 'detect-msg-extensionless' },
text: '확장자 미사용 방식으로 감지되었습니다',
},
{
type: 'basic',
name: 'P',
if: "{{_local.asset_url_mode_detect_status === 'unavailable'}}",
props: { className: 'mt-2 text-sm text-red-600', 'data-testid': 'detect-msg-unavailable' },
text: '감지할 수 없습니다',
},
],
},
],
};
/** 대시보드 드리프트 안내 (레이아웃 원본과 동일한 if 조건) */
const driftAlertLayout = {
components: [
{
id: 'asset_url_mode_drift_alert',
type: 'basic',
name: 'Div',
if: '{{_global.assetUrlModeDrift}}',
props: { className: 'flex items-start gap-3 p-4 mb-6 rounded-lg', 'data-testid': 'drift-alert' },
children: [
{
type: 'basic',
name: 'Icon',
props: { icon: 'fa-solid fa-triangle-exclamation', className: 'text-base' },
},
{
type: 'basic',
name: 'Div',
props: { className: 'flex-1 min-w-0' },
children: [
{
type: 'basic',
name: 'Div',
props: { className: 'text-sm font-semibold' },
text: '에셋 파일 서빙 방식이 서버 환경과 다릅니다',
},
{
type: 'basic',
name: 'Button',
props: { type: 'button', className: 'mt-2 underline' },
text: '환경설정에서 확인하기',
actions: [
{ type: 'click', handler: 'navigate', params: { path: '/admin/settings' } },
],
},
],
},
],
},
],
};
const BOTH_MODES = [MODE_EXTENSION, MODE_EXTENSIONLESS];
describe('자산 URL 이중 모드 — 레이아웃 렌더링 (§검증)', () => {
beforeEach(() => {
setupTestRegistry();
});
afterEach(() => {
delete (globalThis as any).G7Config;
});
describe('환경설정 > 일반 — 자산 URL 방식 필드', () => {
it.each(BOTH_MODES)('%s 모드에서 선택 컨트롤과 감지 버튼이 렌더된다', async (mode) => {
setServerMode(mode);
const t = createLayoutTest(settingsFieldLayout as any);
await t.render();
const select = screen.getByTestId('select-general.asset_url_mode');
expect(select, `${mode} 모드에서 자산 URL 방식 선택 컨트롤이 렌더되지 않았다`).toBeInTheDocument();
// 두 선택지는 모드와 무관하게 항상 제공되어야 한다 — 현재 모드만 보이면
// 잘못 판정된 환경에서 관리자가 되돌릴 방법이 사라진다.
expect(select.querySelectorAll('option')).toHaveLength(2);
expect(screen.getByText('지금 감지')).toBeInTheDocument();
t.cleanup();
});
it('두 모드의 렌더 결과가 완전히 동일하다 (모드는 URL 생성에만 관여)', async () => {
const html: string[] = [];
for (const mode of BOTH_MODES) {
setServerMode(mode);
const t = createLayoutTest(settingsFieldLayout as any);
const { container } = await t.render();
html.push(container.innerHTML);
t.cleanup();
}
// 렌더가 아예 안 되면 '' === '' 로 항상 통과한다. 비교 대상이 실재하는지 먼저 못박는다.
expect(html[0], '렌더 결과가 비어 있어 두 모드 비교가 무의미하다').toContain('general.asset_url_mode');
expect(
html[0],
'모드에 따라 레이아웃 렌더 결과가 달라졌다 — 자산 URL 모드가 화면 구조로 새어나오고 있다',
).toBe(html[1]);
});
it('감지 상태가 없으면 결과 안내가 렌더되지 않는다 (초기 상태는 조용하다)', async () => {
setServerMode('extension');
const t = createLayoutTest(settingsFieldLayout as any);
await t.render();
for (const status of ['detecting', 'extension', 'extensionless', 'unavailable']) {
expect(
screen.queryByTestId(`detect-msg-${status}`),
`감지 전인데 ${status} 안내가 떠 있다`,
).not.toBeInTheDocument();
}
t.cleanup();
});
it('에셋 서빙 방식 항목이 지역화가 아니라 전용 카드에 있다 (배치)', () => {
const fs = require('fs');
const path = require('path');
const json = JSON.parse(
fs.readFileSync(
path.resolve(
__dirname,
'../../layouts/partials/admin_settings/_tab_general.json',
),
'utf8',
),
);
const find = (node: any, id: string): any => {
if (!node || typeof node !== 'object') return null;
if (node.id === id) return node;
for (const k of Object.keys(node)) {
if (node[k] && typeof node[k] === 'object') {
const r = find(node[k], id);
if (r) return r;
}
}
return null;
};
const has = (node: any, id: string): boolean => !!find(node, id);
const serving = find(json, 'card_asset_serving');
expect(serving, '에셋 서빙 전용 카드가 없다').toBeTruthy();
expect(has(serving, 'field_asset_url_mode'), '전용 카드에 항목이 없다').toBe(true);
const localization = find(json, 'card_localization');
expect(localization, '지역화 카드가 없다').toBeTruthy();
expect(
has(localization, 'field_asset_url_mode'),
'에셋 서빙 방식이 여전히 지역화 카드에 있다 — 언어/시간대와 무관한 인프라 설정이다',
).toBe(false);
});
// 감지 결과 인라인 안내의 실제 렌더는 `_local` 조건부이며, 이 상태는 엔진의
// 로컬상태 provider 로 평가된다 — createLayoutTest 는 이를 시드하지 못하므로
// (dataContext._local 은 value 바인딩만 커버) 렌더 단언 대신 실제 레이아웃 JSON 의
// 배선을 구조로 검증한다. 상태 기록은 핸들러 단위테스트가, 실제 렌더는 E2E 가 커버.
it('감지 결과가 상태별 인라인 안내로 배선돼 있다 (토스트 아님) + 감지 버튼에 btn 베이스 클래스', () => {
const fs = require('fs');
const path = require('path');
const layoutPath = path.resolve(
__dirname,
'../../layouts/partials/admin_settings/_tab_general.json',
);
const json = JSON.parse(fs.readFileSync(layoutPath, 'utf8'));
const collect = (node: any, acc: any[] = []): any[] => {
if (!node || typeof node !== 'object') return acc;
if (Array.isArray(node)) {
node.forEach((n) => collect(n, acc));
return acc;
}
acc.push(node);
for (const k of Object.keys(node)) {
if (node[k] && typeof node[k] === 'object') collect(node[k], acc);
}
return acc;
};
// 자산 URL 방식 필드 서브트리로 한정한다 — 같은 탭의 다른 필드는 정당하게
// toast 를 쓰므로 전체 탭을 훑으면 무관한 toast 까지 잡힌다.
const findById = (node: any, id: string): any => {
if (!node || typeof node !== 'object') return null;
if (node.id === id) return node;
for (const k of Object.keys(node)) {
if (node[k] && typeof node[k] === 'object') {
const found = findById(node[k], id);
if (found) return found;
}
}
return null;
};
const field = findById(json, 'field_asset_url_mode');
expect(field, 'field_asset_url_mode 노드를 찾지 못했다').toBeTruthy();
const nodes = collect(field);
// 감지 버튼: 다른 버튼(모달 취소·푸터)과 동일한 btn 베이스 클래스여야 박스로 보인다.
const button = nodes.find(
(n) =>
n.name === 'Button' &&
(n.actions ?? []).some((a: any) => a.handler === 'detectAssetUrlMode'),
);
expect(button, '감지 버튼을 찾지 못했다').toBeTruthy();
expect(
button.props.className,
'감지 버튼에 btn 베이스 클래스가 없어 평문처럼 보인다',
).toContain('btn btn-secondary');
// 4개 상태 각각에 대응하는 인라인 안내 P (필드 하단, 토스트 아님).
for (const st of ['detecting', 'extension', 'extensionless', 'unavailable']) {
const p = nodes.find(
(n) =>
n.name === 'P' &&
typeof n.if === 'string' &&
n.if.includes('_local.asset_url_mode_detect_status') &&
n.if.includes(`'${st}'`),
);
expect(p, `${st} 상태 인라인 안내가 배선되지 않았다`).toBeTruthy();
expect(String(p.text), `${st} 안내 문구가 다국어 키가 아니다`).toContain('$t:');
}
// 감지 결과가 toast 로 발화되지 않는다 (인라인 안내로 대체된 것을 배선으로 못박음).
const toastActions = nodes
.flatMap((n) => n.actions ?? [])
.filter((a: any) => a.handler === 'toast');
expect(toastActions, '감지 결과가 toast 로 발화되도록 배선돼 있다').toHaveLength(0);
});
});
describe('대시보드 — 자산 URL 방식 드리프트 안내', () => {
it.each(BOTH_MODES)('%s 모드에서 드리프트가 없으면 안내가 렌더되지 않는다', async (mode) => {
setServerMode(mode);
const t = createLayoutTest(driftAlertLayout as any);
await t.render();
expect(
screen.queryByTestId('drift-alert'),
'드리프트가 없는데 경고가 떴다 — 상시 경고는 신호가 아니라 소음이 된다',
).not.toBeInTheDocument();
t.cleanup();
});
it.each(BOTH_MODES)('%s 모드에서 드리프트가 감지되면 안내와 이동 버튼이 렌더된다', async (mode) => {
setServerMode(mode);
const t = createLayoutTest(driftAlertLayout as any, {
initialState: {
_global: {
assetUrlModeDrift: { detected: 'extensionless', stored: 'extension' },
},
},
});
await t.render();
expect(screen.getByTestId('drift-alert')).toBeInTheDocument();
expect(screen.getByText('에셋 파일 서빙 방식이 서버 환경과 다릅니다')).toBeInTheDocument();
expect(screen.getByText('환경설정에서 확인하기')).toBeInTheDocument();
t.cleanup();
});
});
});
@@ -1,4 +1,4 @@
var SirsoftAdminBasic=(function(R,h,v,On){"use strict";var T5=Object.defineProperty;var A5=(R,h,v)=>h in R?T5(R,h,{enumerable:!0,configurable:!0,writable:!0,value:v}):R[h]=v;var Fe=(R,h,v)=>A5(R,typeof h!="symbol"?h+"":h,v);var Oa,ur;function Qm(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const n in t)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,r.get?r:{enumerable:!0,get:()=>t[n]})}}return e.default=t,Object.freeze(e)}const P=Qm(v),Jm=()=>window.G7Core,oe=v.forwardRef(({children:t,variant:e="primary",size:n="md",className:r="",...s},o)=>{const i="button",a=Jm()?.style?.mergeClasses?.(i,r)??`${i} ${r}`;return h.jsx("button",{ref:o,className:a,...s,children:t})});oe.displayName="Button";const Ed=v.forwardRef(({accept:t,maxSize:e,onChange:n,onError:r,buttonText:s="Browse",placeholder:o="No file selected",className:i="",disabled:a,...l},c)=>{const d=v.useRef(null),u=c||d,[f,g]=v.useState(""),m={},p={};Object.entries(l).forEach(([x,E])=>{x.startsWith("data-editor-")||x==="id"||x==="onMouseMove"||x==="onMouseLeave"?m[x]=E:p[x]=E});const y=v.useCallback(()=>{!a&&u.current&&u.current.click()},[a,u]),b=v.useCallback(x=>{const E=x.target.files?.[0]||null;if(!E){g(""),n?.({target:{value:null,name:""}});return}if(e&&E.size>e*1024*1024){const k=`File size exceeds ${e}MB limit`;r?.(k),u.current&&(u.current.value=""),g("");return}g(E.name),n?.({target:{value:E,name:E.name}})},[e,n,r,u]),S=v.useCallback(x=>{x.stopPropagation(),u.current&&(u.current.value=""),g(""),n?.({target:{value:null,name:""}})},[n,u]);return h.jsxs("div",{className:`flex items-center gap-2 ${i}`,...m,children:[h.jsx("input",{ref:u,type:"file",accept:t,onChange:b,disabled:a,className:"hidden",...p}),h.jsx("button",{type:"button",onClick:y,disabled:a,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:s}),h.jsx("span",{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:f||o}),f&&!a&&h.jsx("button",{type:"button",onClick:S,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:h.jsx("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});Ed.displayName="FileInput";const Rm=t=>t==="checkbox"?"checkbox":t==="radio"?"radio":"input",eb=(t,e)=>{const n=Rm(t);return e?e.split(/\s+/).filter(Boolean).includes(n)?e:`${n} ${e}`:n},wt=v.forwardRef(({label:t,error:e,className:n="",onChange:r,onKeyPress:s,value:o,defaultValue:i,type:a="text",checked:l,defaultChecked:c,...d},u)=>{const f=a==="radio"||a==="checkbox",g=eb(a,n),m=v.useRef(!1),p=v.useRef(null);v.useImperativeHandle(u,()=>p.current);const[y,b]=v.useState(o??i??"");v.useEffect(()=>{if(!f&&!m.current&&o!==void 0){if(p.current&&document.activeElement===p.current)return;b(o)}},[o,f]);const S=v.useCallback(()=>{m.current=!0},[]),x=v.useCallback(M=>{if(m.current=!1,r){const O=M.target.value;b(O);const L=new Event("change",{bubbles:!0});Object.defineProperty(L,"target",{writable:!1,value:{value:O}}),Object.defineProperty(L,"currentTarget",{writable:!1,value:{value:O}}),r(L)}},[r]),E=v.useCallback(M=>{const O=M.target.value;b(O),!m.current&&r&&r(M)},[r]),k=v.useCallback(M=>{m.current||s&&s(M)},[s]),{loadingactions:C,formdata:N,..._}=d;if(f)return h.jsx("input",{ref:p,type:a,className:g,onChange:r,checked:l,defaultChecked:c,value:o,..._});const w=o!==void 0?{value:y}:{defaultValue:i};return h.jsx("input",{ref:p,type:a,className:g,onChange:E,onKeyPress:k,onCompositionStart:S,onCompositionEnd:x,...w,..._})});wt.displayName="Input";const Zn=({label:t,error:e,className:n="",onChange:r,value:s,defaultValue:o,...i})=>{const a=v.useRef(!1),l=v.useRef(null),[c,d]=v.useState(s??o??"");v.useEffect(()=>{!a.current&&s!==void 0&&d(s)},[s]);const u=v.useCallback(()=>{a.current=!0},[]),f=v.useCallback(E=>{if(a.current=!1,r){const k=E.target.value;d(k);const C=new Event("change",{bubbles:!0});Object.defineProperty(C,"target",{writable:!1,value:{value:k}}),Object.defineProperty(C,"currentTarget",{writable:!1,value:{value:k}}),r(C)}},[r]),g=v.useCallback(E=>{const k=E.target.value;d(k),!a.current&&r&&r(E)},[r]),{loadingactions:m,formdata:p,...y}=i,b=s!==void 0?{value:c}:{defaultValue:o},x=(n??"").split(/\s+/).filter(Boolean).includes("textarea")?n??"":["textarea",n].filter(Boolean).join(" ");return h.jsx("textarea",{ref:l,className:x,onChange:g,onCompositionStart:u,onCompositionEnd:f,...b,...y})},rn=({children:t,className:e="",...n})=>h.jsx("label",{className:e,...n,children:t}),I=v.forwardRef(({children:t,className:e="",...n},r)=>h.jsx("div",{ref:r,classNameLine truncated
var SirsoftAdminBasic=(function(R,h,v,On){"use strict";var F5=Object.defineProperty;var z5=(R,h,v)=>h in R?F5(R,h,{enumerable:!0,configurable:!0,writable:!0,value:v}):R[h]=v;var Fe=(R,h,v)=>z5(R,typeof h!="symbol"?h+"":h,v);var Ta,ur;function eb(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const n in t)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,r.get?r:{enumerable:!0,get:()=>t[n]})}}return e.default=t,Object.freeze(e)}const P=eb(v),tb=()=>window.G7Core,oe=v.forwardRef(({children:t,variant:e="primary",size:n="md",className:r="",...s},o)=>{const i="button",a=tb()?.style?.mergeClasses?.(i,r)??`${i} ${r}`;return h.jsx("button",{ref:o,className:a,...s,children:t})});oe.displayName="Button";const Md=v.forwardRef(({accept:t,maxSize:e,onChange:n,onError:r,buttonText:s="Browse",placeholder:o="No file selected",className:i="",disabled:a,...l},c)=>{const d=v.useRef(null),u=c||d,[f,g]=v.useState(""),m={},p={};Object.entries(l).forEach(([x,E])=>{x.startsWith("data-editor-")||x==="id"||x==="onMouseMove"||x==="onMouseLeave"?m[x]=E:p[x]=E});const y=v.useCallback(()=>{!a&&u.current&&u.current.click()},[a,u]),b=v.useCallback(x=>{const E=x.target.files?.[0]||null;if(!E){g(""),n?.({target:{value:null,name:""}});return}if(e&&E.size>e*1024*1024){const k=`File size exceeds ${e}MB limit`;r?.(k),u.current&&(u.current.value=""),g("");return}g(E.name),n?.({target:{value:E,name:E.name}})},[e,n,r,u]),S=v.useCallback(x=>{x.stopPropagation(),u.current&&(u.current.value=""),g(""),n?.({target:{value:null,name:""}})},[n,u]);return h.jsxs("div",{className:`flex items-center gap-2 ${i}`,...m,children:[h.jsx("input",{ref:u,type:"file",accept:t,onChange:b,disabled:a,className:"hidden",...p}),h.jsx("button",{type:"button",onClick:y,disabled:a,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:s}),h.jsx("span",{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:f||o}),f&&!a&&h.jsx("button",{type:"button",onClick:S,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:h.jsx("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});Md.displayName="FileInput";const nb=t=>t==="checkbox"?"checkbox":t==="radio"?"radio":"input",rb=(t,e)=>{const n=nb(t);return e?e.split(/\s+/).filter(Boolean).includes(n)?e:`${n} ${e}`:n},wt=v.forwardRef(({label:t,error:e,className:n="",onChange:r,onKeyPress:s,value:o,defaultValue:i,type:a="text",checked:l,defaultChecked:c,...d},u)=>{const f=a==="radio"||a==="checkbox",g=rb(a,n),m=v.useRef(!1),p=v.useRef(null);v.useImperativeHandle(u,()=>p.current);const[y,b]=v.useState(o??i??"");v.useEffect(()=>{if(!f&&!m.current&&o!==void 0){if(p.current&&document.activeElement===p.current)return;b(o)}},[o,f]);const S=v.useCallback(()=>{m.current=!0},[]),x=v.useCallback(M=>{if(m.current=!1,r){const O=M.target.value;b(O);const L=new Event("change",{bubbles:!0});Object.defineProperty(L,"target",{writable:!1,value:{value:O}}),Object.defineProperty(L,"currentTarget",{writable:!1,value:{value:O}}),r(L)}},[r]),E=v.useCallback(M=>{const O=M.target.value;b(O),!m.current&&r&&r(M)},[r]),k=v.useCallback(M=>{m.current||s&&s(M)},[s]),{loadingactions:C,formdata:N,..._}=d;if(f)return h.jsx("input",{ref:p,type:a,className:g,onChange:r,checked:l,defaultChecked:c,value:o,..._});const w=o!==void 0?{value:y}:{defaultValue:i};return h.jsx("input",{ref:p,type:a,className:g,onChange:E,onKeyPress:k,onCompositionStart:S,onCompositionEnd:x,...w,..._})});wt.displayName="Input";const Zn=({label:t,error:e,className:n="",onChange:r,value:s,defaultValue:o,...i})=>{const a=v.useRef(!1),l=v.useRef(null),[c,d]=v.useState(s??o??"");v.useEffect(()=>{!a.current&&s!==void 0&&d(s)},[s]);const u=v.useCallback(()=>{a.current=!0},[]),f=v.useCallback(E=>{if(a.current=!1,r){const k=E.target.value;d(k);const C=new Event("change",{bubbles:!0});Object.defineProperty(C,"target",{writable:!1,value:{value:k}}),Object.defineProperty(C,"currentTarget",{writable:!1,value:{value:k}}),r(C)}},[r]),g=v.useCallback(E=>{const k=E.target.value;d(k),!a.current&&r&&r(E)},[r]),{loadingactions:m,formdata:p,...y}=i,b=s!==void 0?{value:c}:{defaultValue:o},x=(n??"").split(/\s+/).filter(Boolean).includes("textarea")?n??"":["textarea",n].filter(Boolean).join(" ");return h.jsx("textarea",{ref:l,className:x,onChange:g,onCompositionStart:u,onCompositionEnd:f,...b,...y})},rn=({children:t,className:e="",...n})=>h.jsx("label",{className:e,...n,children:t}),I=v.forwardRef(({children:t,className:e="",...n},r)=>h.jsx("div",{ref:r,classNameLine truncated
inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium
${he?"bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"}
`,children:[h.jsx(re,{name:"globe",className:"w-3 h-3"}),H(X),he&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),!he&&h.jsx(oe,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",onClick:()=>z(X),disabled:l,children:h.jsx(re,{name:"xmark",className:"w-3 h-3"})})]}),he&&Z&&j()]}),n==="textarea"?h.jsx(Zn,{name:`${f}[${X}]`,value:k[X]||"",onChange:ce=>A(X,ce.target.value),placeholder:J,required:a&&he,disabled:l,maxLength:c,rows:d,className:`${ne} resize-vertical`}):h.jsx(wt,{type:"text",name:`${f}[${X}]`,value:k[X]||"",onChange:ce=>A(X,ce.target.value),placeholder:J,required:a&&he,disabled:l,maxLength:c,className:ee})]},X)},[f,k,A,i,a,l,c,d,n,H,x,z,j,b]),B=v.useCallback(X=>{const Z=b?"input input-error":"input",ee=b?"textarea input-error":"textarea",ne=`${i}${i?" (":""}${H(X)}${i?")":""}`;return h.jsx(I,{children:n==="textarea"?h.jsx(Zn,{name:`${f}[${X}]`,value:k[X]||"",onChange:he=>A(X,he.target.value),placeholder:ne,required:a&&X===x,disabled:l,maxLength:c,rows:d,className:`${ee} resize-vertical`}):h.jsx(wt,{type:"text",name:`${f}[${X}]`,value:k[X]||"",onChange:he=>A(X,he.target.value),placeholder:ne,required:a&&X===x,disabled:l,maxLength:c,className:Z})},X)},[f,k,A,i,a,l,c,d,n,x,H,b]),G=()=>h.jsx(I,{className:u,children:h.jsx(I,{className:"space-y-4",children:w.map((X,Z)=>U(X,Z===0))})}),V=()=>h.jsxs(I,{className:u,children:[h.jsxs(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:[w.map(X=>h.jsxs(oe,{type:"button",className:`
@@ -7,13 +7,13 @@ var SirsoftAdminBasic=(function(R,h,v,On){"use strict";var T5=Object.definePrope
`,onClick:()=>L(X),children:[h.jsx(re,{name:"globe",className:"w-3 h-3"}),g?h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:H(X)}),h.jsx(Q,{className:"inline md:hidden",children:X.toUpperCase()})]}):H(X),X===x&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),W(X)&&O!==X&&h.jsx(re,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"}),X!==x&&h.jsx(oe,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors",onClick:Z=>{Z.stopPropagation(),z(X)},disabled:l,children:h.jsx(re,{name:"xmark",className:"w-3 h-3"})})]},X)),j()]}),w.map(X=>h.jsx(I,{className:O===X?"block":"hidden",children:B(X)},X))]}),K=()=>{const X=b?"input input-error":"input",Z=b?"textarea input-error":"textarea";return h.jsx(I,{className:u,children:h.jsxs(I,{className:"flex items-center gap-1",children:[h.jsx(I,{className:"flex items-center shrink-0",children:w.map(ee=>h.jsxs(oe,{type:"button",className:`
px-2 py-1.5 text-xs font-medium border-y border-l first:rounded-l last:rounded-r last:border-r transition-colors
${O===ee?"bg-blue-500 text-white border-blue-500 dark:bg-blue-600 dark:border-blue-600":"bg-white text-gray-600 border-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"}
`,onClick:()=>L(ee),title:H(ee),children:[ee.toUpperCase(),ee===x&&h.jsx(Q,{className:"text-red-400 ml-0.5",children:"*"})]},ee))}),h.jsx(I,{className:"flex-1 min-w-0",children:n==="textarea"?h.jsx(Zn,{name:`${f}[${O}]`,value:k[O]||"",onChange:ee=>A(O,ee.target.value),placeholder:i,required:a&&O===x,disabled:l,maxLength:c,rows:d,className:Z}):h.jsx(wt,{type:"text",name:`${f}[${O}]`,value:k[O]||"",onChange:ee=>A(O,ee.target.value),placeholder:i,required:a&&O===x,disabled:l,maxLength:c,className:X})})]})})};return h.jsxs(I,{className:l?"opacity-50 cursor-not-allowed":void 0,id:p,...y,children:[r==="compact"?K():r==="tabs"?V():G(),b&&m&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:m})]})};Va.displayName="MultilingualInput";const mb=()=>window.G7Core,Ha=({checked:t,value:e,onChange:n,disabled:r=!1,label:s,description:o,size:i="md",className:a="",name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(()=>t??e??!1);v.useEffect(()=>{f(t??e??!1)},[t,e]);const g=b=>{const S=b.target.checked;f(S),n&&!r&&n(b)},p={sm:{track:"w-9 h-5",thumb:"after:h-4 after:w-4"},md:{track:"w-11 h-6",thumb:"after:h-5 after:w-5"},lg:{track:"w-14 h-7",thumb:"after:h-6 after:w-6"}}[i],y=()=>{if(!r){const b=!u;if(f(b),n){const S=mb();S?.createChangeEvent&&n(S.createChangeEvent({checked:b,name:l}))}}};return h.jsxs("div",{className:`toggle-container ${a}`,id:c,...d,children:[h.jsxs("div",{className:`toggle-switch-wrapper ${r?"cursor-not-allowed opacity-50":"cursor-pointer"}`,onClick:b=>{b.preventDefault(),b.stopPropagation(),y()},children:[h.jsx("input",{type:"checkbox",id:l,name:l,checked:u,onChange:g,disabled:r,className:"sr-only peer",tabIndex:-1}),h.jsx("div",{className:`toggle-switch-track ${p.track} ${p.thumb}`})]}),s&&h.jsx("span",{className:`toggle-label ${r?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:y,children:s}),o&&h.jsx("p",{className:"toggle-description",children:o})]})},$o=({status:t,label:e,showIcon:n=!0,iconName:r,className:s="",style:o,id:i,editorAttrs:a})=>{const l={success:"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800",warning:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800",error:"bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800",pending:"bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600",default:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-600"},c={success:de.CheckCircle,warning:de.ExclamationTriangle,error:de.XCircle,info:de.InfoCircle,pending:de.Clock,default:de.QuestionCircle},d={success:"성공",warning:"경고",error:"오류",info:"정보",pending:"대기 중",default:"기본"},u=r||c[t],f=e||d[t],g=l[t];return h.jsxs(Q,{className:`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border ${g} ${s}`,style:o,id:i,...a,children:[n&&h.jsx(re,{name:u,className:"w-4 h-4"}),h.jsx(Q,{children:f})]})},Dd={blue:"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",green:"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300",red:"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",gray:"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",yellow:"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",purple:"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",orange:"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",teal:"bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300",cyan:"bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"},bb={success:"green",warning:"yellow",danger:"red",error:"red",info:"blue",primary:"blue",secondary:"gray",text:"gray",default:"gray"},Id={sm:"px-1.5 py-0.5 text-[10px]",md:"px-2 py-0.5 text-xs",lg:"px-2.5 py-1 text-sm"},Ld=({color:t,variant:e,text:n,size:r="md",className:s="",style:o,children:i,id:a,editorAttrs:l})=>{const c=t||(e?bb[e]:void 0)||"gray",d=Dd[c]||Dd.gray,u=Id[r]||Id.md;return h.jsxs(Q,{className:`inline-flex items-center rounded-full font-medium ${u} ${d} ${s}`,style:o,id:a,...l,children:[n,i]})};const{entries:Pd,setPrototypeOf:$d,isFrozen:yb,getPrototypeOf:vb,getOwnPropertyDescriptor:xb}=Object;let{freeze:jt,seal:cn,create:Fd}=Object,{apply:Ua,construct:Ba}=typeof Reflect<"u"&&Reflect;jt||(jt=function(e){return e}),cn||(cn=function(e){return e}),Ua||(Ua=function(e,n){for(var r=arguments.length,s=new Array(r>2?r-2:0),o=2;o<r;o++)s[o-2]=arguments[o];return e.apply(n,s)}),Ba||(Ba=function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return new e(...r)});const Fo=Gt(Array.prototype.forEach),wb=Gt(Array.prototype.lastIndexOf),zd=Gt(Array.prototype.p Line truncated
`+we),J&&Fo([L,F,$],Ke=>{we=vs(we,Ke," ")}),E&&ke?E.createHTML(we):we},e.setConfig=function(){let Ce=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};tn(Ce),ae=!0},e.clearConfig=function(){Ze=null,ae=!1},e.isValidAttribute=function(Ce,Y,ie){Ze||tn({});const xe=De(Ce),ft=De(Y);return Ao(xe,ft,ie)},e.addHook=function(Ce,Y){typeof Y=="function"&&ys(O[Ce],Y)},e.removeHook=function(Ce,Y){if(Y!==void 0){const ie=wb(O[Ce],Y);return ie===-1?void 0:kb(O[Ce],ie,1)[0]}return zd(O[Ce])},e.removeHooks=function(Ce){O[Ce]=[]},e.removeAllHooks=function(){O=Yd()},e}var Vb=Xd();const qa=({content:t,text:e,isHtml:n=!0,className:r="",purifyConfig:s,id:o,editorAttrs:i})=>{const a=e??t??"";if(!a||a.trim()==="")return null;if(!n){const u=`
`,onClick:()=>L(ee),title:H(ee),children:[ee.toUpperCase(),ee===x&&h.jsx(Q,{className:"text-red-400 ml-0.5",children:"*"})]},ee))}),h.jsx(I,{className:"flex-1 min-w-0",children:n==="textarea"?h.jsx(Zn,{name:`${f}[${O}]`,value:k[O]||"",onChange:ee=>A(O,ee.target.value),placeholder:i,required:a&&O===x,disabled:l,maxLength:c,rows:d,className:Z}):h.jsx(wt,{type:"text",name:`${f}[${O}]`,value:k[O]||"",onChange:ee=>A(O,ee.target.value),placeholder:i,required:a&&O===x,disabled:l,maxLength:c,className:X})})]})})};return h.jsxs(I,{className:l?"opacity-50 cursor-not-allowed":void 0,id:p,...y,children:[r==="compact"?K():r==="tabs"?V():G(),b&&m&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:m})]})};Ha.displayName="MultilingualInput";const vb=()=>window.G7Core,Ua=({checked:t,value:e,onChange:n,disabled:r=!1,label:s,description:o,size:i="md",className:a="",name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(()=>t??e??!1);v.useEffect(()=>{f(t??e??!1)},[t,e]);const g=b=>{const S=b.target.checked;f(S),n&&!r&&n(b)},p={sm:{track:"w-9 h-5",thumb:"after:h-4 after:w-4"},md:{track:"w-11 h-6",thumb:"after:h-5 after:w-5"},lg:{track:"w-14 h-7",thumb:"after:h-6 after:w-6"}}[i],y=()=>{if(!r){const b=!u;if(f(b),n){const S=vb();S?.createChangeEvent&&n(S.createChangeEvent({checked:b,name:l}))}}};return h.jsxs("div",{className:`toggle-container ${a}`,id:c,...d,children:[h.jsxs("div",{className:`toggle-switch-wrapper ${r?"cursor-not-allowed opacity-50":"cursor-pointer"}`,onClick:b=>{b.preventDefault(),b.stopPropagation(),y()},children:[h.jsx("input",{type:"checkbox",id:l,name:l,checked:u,onChange:g,disabled:r,className:"sr-only peer",tabIndex:-1}),h.jsx("div",{className:`toggle-switch-track ${p.track} ${p.thumb}`})]}),s&&h.jsx("span",{className:`toggle-label ${r?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:y,children:s}),o&&h.jsx("p",{className:"toggle-description",children:o})]})},$o=({status:t,label:e,showIcon:n=!0,iconName:r,className:s="",style:o,id:i,editorAttrs:a})=>{const l={success:"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800",warning:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800",error:"bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800",pending:"bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600",default:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-600"},c={success:de.CheckCircle,warning:de.ExclamationTriangle,error:de.XCircle,info:de.InfoCircle,pending:de.Clock,default:de.QuestionCircle},d={success:"성공",warning:"경고",error:"오류",info:"정보",pending:"대기 중",default:"기본"},u=r||c[t],f=e||d[t],g=l[t];return h.jsxs(Q,{className:`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border ${g} ${s}`,style:o,id:i,...a,children:[n&&h.jsx(re,{name:u,className:"w-4 h-4"}),h.jsx(Q,{children:f})]})},Id={blue:"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",green:"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300",red:"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",gray:"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",yellow:"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",purple:"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",orange:"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",teal:"bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300",cyan:"bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"},xb={success:"green",warning:"yellow",danger:"red",error:"red",info:"blue",primary:"blue",secondary:"gray",text:"gray",default:"gray"},Ld={sm:"px-1.5 py-0.5 text-[10px]",md:"px-2 py-0.5 text-xs",lg:"px-2.5 py-1 text-sm"},Pd=({color:t,variant:e,text:n,size:r="md",className:s="",style:o,children:i,id:a,editorAttrs:l})=>{const c=t||(e?xb[e]:void 0)||"gray",d=Id[c]||Id.gray,u=Ld[r]||Ld.md;return h.jsxs(Q,{className:`inline-flex items-center rounded-full font-medium ${u} ${d} ${s}`,style:o,id:a,...l,children:[n,i]})};const{entries:$d,setPrototypeOf:Fd,isFrozen:wb,getPrototypeOf:kb,getOwnPropertyDescriptor:Cb}=Object;let{freeze:jt,seal:cn,create:zd}=Object,{apply:Ba,construct:ja}=typeof Reflect<"u"&&Reflect;jt||(jt=function(e){return e}),cn||(cn=function(e){return e}),Ba||(Ba=function(e,n){for(var r=arguments.length,s=new Array(r>2?r-2:0),o=2;o<r;o++)s[o-2]=arguments[o];return e.apply(n,s)}),ja||(ja=function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return new e(...r)});const Fo=Gt(Array.prototype.forEach),Sb=Gt(Array.prototype.lastIndexOf),Vd=Gt(Array.prototype.p Line truncated
`+we),J&&Fo([L,F,$],Ke=>{we=vs(we,Ke," ")}),E&&ke?E.createHTML(we):we},e.setConfig=function(){let Ce=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};tn(Ce),ae=!0},e.clearConfig=function(){Ze=null,ae=!1},e.isValidAttribute=function(Ce,Y,ie){Ze||tn({});const xe=De(Ce),ft=De(Y);return Ao(xe,ft,ie)},e.addHook=function(Ce,Y){typeof Y=="function"&&ys(O[Ce],Y)},e.removeHook=function(Ce,Y){if(Y!==void 0){const ie=Sb(O[Ce],Y);return ie===-1?void 0:_b(O[Ce],ie,1)[0]}return Vd(O[Ce])},e.removeHooks=function(Ce){O[Ce]=[]},e.removeAllHooks=function(){O=Xd()},e}var Bb=Kd();const Za=({content:t,text:e,isHtml:n=!0,className:r="",purifyConfig:s,id:o,editorAttrs:i})=>{const a=e??t??"";if(!a||a.trim()==="")return null;if(!n){const u=`
whitespace-pre-wrap
text-gray-900 dark:text-gray-100
font-sans
${r}
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:u,id:o,...i,children:a})}const l={FORBID_TAGS:["script","noscript","iframe","frame","frameset","object","embed","applet","portal","form","input","textarea","select","button","style","link","meta","base","body","head","html","title","svg","math","audio","video","source","track","canvas","details","dialog","plaintext","xmp","listing","marquee","noframes","noembed","template","slot"],FORBID_ATTR:["onerror","onload","onclick","onmouseover","onfocus","onblur","ontoggle","oncanplay","onloadstart","formaction","xlink:href","action"],ALLOW_DATA_ATTR:!0,ADD_ATTR:["target"]},c=v.useMemo(()=>{const u=s?{...l,...s,FORBID_TAGS:[...l.FORBID_TAGS,...s.FORBID_TAGS||[]],FORBID_ATTR:[...l.FORBID_ATTR,...s.FORBID_ATTR||[]]}:l;return Vb.sanitize(a,u).replace(/<a\s+([^>]*?)href=["']([^"']+)["']([^>]*?)>/gi,(g,m,p,y)=>(p.startsWith("http://")||p.startsWith("https://"))&&!g.includes("rel=")?`<a ${m}href="${p}"${y} rel="noopener noreferrer">`:g)},[a,s]),d=`
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:u,id:o,...i,children:a})}const l={FORBID_TAGS:["script","noscript","iframe","frame","frameset","object","embed","applet","portal","form","input","textarea","select","button","style","link","meta","base","body","head","html","title","svg","math","audio","video","source","track","canvas","details","dialog","plaintext","xmp","listing","marquee","noframes","noembed","template","slot"],FORBID_ATTR:["onerror","onload","onclick","onmouseover","onfocus","onblur","ontoggle","oncanplay","onloadstart","formaction","xlink:href","action"],ALLOW_DATA_ATTR:!0,ADD_ATTR:["target"]},c=v.useMemo(()=>{const u=s?{...l,...s,FORBID_TAGS:[...l.FORBID_TAGS,...s.FORBID_TAGS||[]],FORBID_ATTR:[...l.FORBID_ATTR,...s.FORBID_ATTR||[]]}:l;return Bb.sanitize(a,u).replace(/<a\s+([^>]*?)href=["']([^"']+)["']([^>]*?)>/gi,(g,m,p,y)=>(p.startsWith("http://")||p.startsWith("https://"))&&!g.includes("rel=")?`<a ${m}href="${p}"${y} rel="noopener noreferrer">`:g)},[a,s]),d=`
prose dark:prose-invert max-w-none
prose-p:my-2
prose-headings:font-bold prose-headings:mt-6 prose-headings:mb-4
@@ -35,26 +35,26 @@ var SirsoftAdminBasic=(function(R,h,v,On){"use strict";var T5=Object.definePrope
[&_tr:hover]:bg-gray-50 dark:[&_tr:hover]:bg-gray-700/50
after:content-[''] after:block after:clear-both
${r}
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:d,dangerouslySetInnerHTML:{__html:c},id:o,...i})},Ho=window.G7Core?.createLogger?.("Comp:DataGrid")??{log:(...t)=>console.log("[Comp:DataGrid]",...t),warn:(...t)=>console.warn("[Comp:DataGrid]",...t),error:(...t)=>console.error("[Comp:DataGrid]",...t)},Cs=(t,e)=>window.G7Core?.t?.(t,e)??t,Za={Div:I,Span:Q,Button:oe,Icon:re,Checkbox:vn,P:bt,A:Lr,Label:rn,Input:wt,H1:Aa,H2:Qn,H3:Ir,H4:Md,Img:yn,Select:Pr,Textarea:Zn,Table:Pa,Thead:$a,Tbody:Fa,Tfoot:za,Tr:Jn,Th:$r,Td:Kt,MultilingualInput:Va,Toggle:Ha,StatusBadge:$o,Badge:Ld,HtmlContent:qa,ActionMenu:bs},Qa=t=>{if(t==null)return"";if(typeof t=="string"||typeof t=="number"||typeof t=="boolean")return String(t);if(Array.isArray(t))return t.map(e=>Qa(e)).join(", ");if(typeof t=="object"){if(v.isValidElement(t))return t;if(Object.keys(t).length===0)return"";try{return JSON.stringify(t)}catch{return Ho.warn("객체를 문자열로 변환할 수 없습니다:",t),"[Object]"}}return String(t)},Kd=(t,e)=>{if(!t)return!0;const n=t.match(/^\{\{(.+)\}\}$/);if(!n)return!0;const r=n[1].trim();try{return!!new Function("row",`return ${r}`)(e)}catch(s){return Ho.warn("subRow 조건 평가 실패:",t,s),!0}},Ss=(t,e,n,r="",s)=>{const o=window.G7Core;if(o?.renderItemChildren){const i={row:e,value:n,$value:n};return o.renderItemChildren(t,i,Za,r,s?{componentContext:s}:void 0)}return Ho.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},Ja=(t,e,n="",r)=>{const s=window.G7Core;if(s?.renderItemChildren){const o={row:e,$value:e};return s.renderItemChildren(t,o,Za,n,r?{componentContext:r}:void 0)}return Ho.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},Hb=({id:t,columns:e,data:n,sortable:r=!0,pagination:s=!1,pageSize:o=10,onRowClick:i,className:a="",style:l,sortField:c,sortDirection:d,onSortChange:u,selectable:f=!1,selectedIds:g=[],onSelectionChange:m,idField:p="id",showColumnSelector:y=!1,visibleColumns:b,onColumnVisibilityChange:S,responsiveBreakpoint:x=768,cardRenderer:E,rowActions:k,onRowAction:C,alwaysShowPagination:N=!1,serverSidePagination:_=!1,serverCurrentPage:w,serverTotalPages:M,onPageChange:O,prevText:L,nextText:F,columnSelectorText:$,emptyMessage:D,actionsColumnHeader:A,requiredText:T,selectedCountText:z,showFirstLast:H=!0,expandable:W=!1,expandedRowIds:j,onExpandChange:U,expandChildren:B,expandedRowRender:G,expandColumnWidth:V="48px",expandIconExpanded:K=de.ChevronDown,expandIconCollapsed:X=de.ChevronRight,expandConditionField:Z,dynamicColumns:ee,dynamicColumnsInsertAfter:ne,expandContext:he,__componentContext:J,subRowChildren:ce,subRowCondition:ue,subRowClassName:ae="px-6 py-2 bg-gray-50 dark:bg-gray-900",subRowRender:ve,footerCells:fe,footerClassName:ye="bg-gray-50 dark:bg-gray-800/50 font-medium",footerCardChildren:ke,editorAttrs:Ue})=>{const se=L||void 0,Oe=F||void 0,Pe=$??Cs("common.column_selector"),Ie=D??Cs("common.no_data"),je=A??Cs("common.actions"),st=T??Cs("common.required"),ut=z??Cs("common.selected_count"),We=window.G7Core,et=We?.useControllableState,Xe=We?.shallowArrayEqual,[te,ge]=et?et(c,null,q=>{q!==null&&u?.(q,pe)}):[c??null,()=>{}],[pe,me]=et?et(d,"asc",q=>{te!==null&&u?.(te,q)}):[d??"asc",()=>{}],[be,Be]=v.useState(1),[Se,ze]=et?et(j,[],U,{isEqual:Xe}):[j??[],()=>{}],[_e,Re]=v.useState(!1),ot=v.useRef(null),$e=We?.useResponsive,Te=$e?.(),He=Te?Te.width<x:typeof window<"u"&&window.innerWidth<x,De=v.useMemo(()=>{if(!ee||!Array.isArray(ee)||ee.length===0)return e;if(ne){const q=e.findIndex(le=>le.field===ne);if(q!==-1)return[...e.slice(0,q+1),...ee,...e.slice(q+1)]}return[...e,...ee]},[e,ee,ne]),[Ze,it]=v.useState(()=>b&&b.length>0?b:e.filter(q=>!q.hidden).map(q=>q.field));v.useEffect(()=>{b&&b.length>0&&it(b)},[b]);const gt=v.useRef("");v.useEffect(()=>{const q=e.map(le=>le.field).join(",");gt.current&&q!==gt.current&&(!b||b.length===0)&&it(e.filter(le=>!le.hidden).map(le=>le.field)),gt.current=q},[e,b]);const tn=v.useRef([]),mt=v.useRef(null);v.useEffect(()=>{if(ee&&ee.length>0){const q=ee.map(Ke=>Ke.field);if(mt.current===null&&(mt.current=b!==void 0&&b.length>0,mt.current)){tn.current=q;return}const le=tn.current,we=q.filter(Ke=>!le.includes(Ke));if(we.length>0){const Ke=we.filter(Le=>!Ze.includes(Le));if(Ke.length>0){const Le=[...Ze,...Ke];it(Le),S?.(Le)}}tn.current=q}},[ee]),v.useEffect(()=>{_||Be(1)},[n,_]),v.useEffect(()=>{const q=le=>{ot.current&&!ot.current.contains(le.target)&&Re(!1)};return _e&&document.addEventListener("mousedown",q),()=>document.removeEventListener("mousedown",q)},[_e]);const xt=v.useMemo(()=>!y&&(!b||b.length===0)?De.filter(q=>!q.hidden):De.filter(q=>Ze.includes(q.field)),[De,Ze,y,b]),Dt=v.useMemo(()=>!te||!r||!n?n||[]:u?n:[...n].sort((q,le)=>{const we=q[te],Ke=le[te];if(we===Ke)return 0;const Le=we>Ke?1:-1;return pe==="asc"?Le:-Le}),[n,te,pe,r,u]),Ve=v.useMemo(()=>{if(_||!s)return Dt;const q=(be-1)*o,le=q+o;return Dt.slice(q,le)},[Dt,be,o,s,_]),Bt=_?w||1:be,ln=_?M||1:Math.ceil((Dt?.length||0)/o),nn=v.useCallback(q=>{_&&O? Line truncated
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:d,dangerouslySetInnerHTML:{__html:c},id:o,...i})},Ho=window.G7Core?.createLogger?.("Comp:DataGrid")??{log:(...t)=>console.log("[Comp:DataGrid]",...t),warn:(...t)=>console.warn("[Comp:DataGrid]",...t),error:(...t)=>console.error("[Comp:DataGrid]",...t)},Cs=(t,e)=>window.G7Core?.t?.(t,e)??t,Qa={Div:I,Span:Q,Button:oe,Icon:re,Checkbox:vn,P:bt,A:Lr,Label:rn,Input:wt,H1:Da,H2:Qn,H3:Ir,H4:Nd,Img:yn,Select:Pr,Textarea:Zn,Table:$a,Thead:Fa,Tbody:za,Tfoot:Va,Tr:Jn,Th:$r,Td:Kt,MultilingualInput:Ha,Toggle:Ua,StatusBadge:$o,Badge:Pd,HtmlContent:Za,ActionMenu:bs},Ja=t=>{if(t==null)return"";if(typeof t=="string"||typeof t=="number"||typeof t=="boolean")return String(t);if(Array.isArray(t))return t.map(e=>Ja(e)).join(", ");if(typeof t=="object"){if(v.isValidElement(t))return t;if(Object.keys(t).length===0)return"";try{return JSON.stringify(t)}catch{return Ho.warn("객체를 문자열로 변환할 수 없습니다:",t),"[Object]"}}return String(t)},qd=(t,e)=>{if(!t)return!0;const n=t.match(/^\{\{(.+)\}\}$/);if(!n)return!0;const r=n[1].trim();try{return!!new Function("row",`return ${r}`)(e)}catch(s){return Ho.warn("subRow 조건 평가 실패:",t,s),!0}},Ss=(t,e,n,r="",s)=>{const o=window.G7Core;if(o?.renderItemChildren){const i={row:e,value:n,$value:n};return o.renderItemChildren(t,i,Qa,r,s?{componentContext:s}:void 0)}return Ho.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},Ra=(t,e,n="",r)=>{const s=window.G7Core;if(s?.renderItemChildren){const o={row:e,$value:e};return s.renderItemChildren(t,o,Qa,n,r?{componentContext:r}:void 0)}return Ho.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},jb=({id:t,columns:e,data:n,sortable:r=!0,pagination:s=!1,pageSize:o=10,onRowClick:i,className:a="",style:l,sortField:c,sortDirection:d,onSortChange:u,selectable:f=!1,selectedIds:g=[],onSelectionChange:m,idField:p="id",showColumnSelector:y=!1,visibleColumns:b,onColumnVisibilityChange:S,responsiveBreakpoint:x=768,cardRenderer:E,rowActions:k,onRowAction:C,alwaysShowPagination:N=!1,serverSidePagination:_=!1,serverCurrentPage:w,serverTotalPages:M,onPageChange:O,prevText:L,nextText:F,columnSelectorText:$,emptyMessage:D,actionsColumnHeader:A,requiredText:T,selectedCountText:z,showFirstLast:H=!0,expandable:W=!1,expandedRowIds:j,onExpandChange:U,expandChildren:B,expandedRowRender:G,expandColumnWidth:V="48px",expandIconExpanded:K=de.ChevronDown,expandIconCollapsed:X=de.ChevronRight,expandConditionField:Z,dynamicColumns:ee,dynamicColumnsInsertAfter:ne,expandContext:he,__componentContext:J,subRowChildren:ce,subRowCondition:ue,subRowClassName:ae="px-6 py-2 bg-gray-50 dark:bg-gray-900",subRowRender:ve,footerCells:fe,footerClassName:ye="bg-gray-50 dark:bg-gray-800/50 font-medium",footerCardChildren:ke,editorAttrs:Ue})=>{const se=L||void 0,Oe=F||void 0,Pe=$??Cs("common.column_selector"),Ie=D??Cs("common.no_data"),je=A??Cs("common.actions"),st=T??Cs("common.required"),ut=z??Cs("common.selected_count"),We=window.G7Core,et=We?.useControllableState,Xe=We?.shallowArrayEqual,[te,ge]=et?et(c,null,q=>{q!==null&&u?.(q,pe)}):[c??null,()=>{}],[pe,me]=et?et(d,"asc",q=>{te!==null&&u?.(te,q)}):[d??"asc",()=>{}],[be,Be]=v.useState(1),[Se,ze]=et?et(j,[],U,{isEqual:Xe}):[j??[],()=>{}],[_e,Re]=v.useState(!1),ot=v.useRef(null),$e=We?.useResponsive,Te=$e?.(),He=Te?Te.width<x:typeof window<"u"&&window.innerWidth<x,De=v.useMemo(()=>{if(!ee||!Array.isArray(ee)||ee.length===0)return e;if(ne){const q=e.findIndex(le=>le.field===ne);if(q!==-1)return[...e.slice(0,q+1),...ee,...e.slice(q+1)]}return[...e,...ee]},[e,ee,ne]),[Ze,it]=v.useState(()=>b&&b.length>0?b:e.filter(q=>!q.hidden).map(q=>q.field));v.useEffect(()=>{b&&b.length>0&&it(b)},[b]);const gt=v.useRef("");v.useEffect(()=>{const q=e.map(le=>le.field).join(",");gt.current&&q!==gt.current&&(!b||b.length===0)&&it(e.filter(le=>!le.hidden).map(le=>le.field)),gt.current=q},[e,b]);const tn=v.useRef([]),mt=v.useRef(null);v.useEffect(()=>{if(ee&&ee.length>0){const q=ee.map(Ke=>Ke.field);if(mt.current===null&&(mt.current=b!==void 0&&b.length>0,mt.current)){tn.current=q;return}const le=tn.current,we=q.filter(Ke=>!le.includes(Ke));if(we.length>0){const Ke=we.filter(Le=>!Ze.includes(Le));if(Ke.length>0){const Le=[...Ze,...Ke];it(Le),S?.(Le)}}tn.current=q}},[ee]),v.useEffect(()=>{_||Be(1)},[n,_]),v.useEffect(()=>{const q=le=>{ot.current&&!ot.current.contains(le.target)&&Re(!1)};return _e&&document.addEventListener("mousedown",q),()=>document.removeEventListener("mousedown",q)},[_e]);const xt=v.useMemo(()=>!y&&(!b||b.length===0)?De.filter(q=>!q.hidden):De.filter(q=>Ze.includes(q.field)),[De,Ze,y,b]),Dt=v.useMemo(()=>!te||!r||!n?n||[]:u?n:[...n].sort((q,le)=>{const we=q[te],Ke=le[te];if(we===Ke)return 0;const Le=we>Ke?1:-1;return pe==="asc"?Le:-Le}),[n,te,pe,r,u]),Ve=v.useMemo(()=>{if(_||!s)return Dt;const q=(be-1)*o,le=q+o;return Dt.slice(q,le)},[Dt,be,o,s,_]),Bt=_?w||1:be,ln=_?M||1:Math.ceil((Dt?.length||0)/o),nn=v.useCallback(q=>{_&&O? Line truncated
flex items-center gap-3 p-4 rounded-lg border shadow-lg
min-w-[300px] max-w-md
transition-all duration-300
${s.bg}
${n?"opacity-100 translate-y-0":`opacity-0 ${i}`}
`,role:"alert",children:[h.jsx(re,{name:o,className:`w-5 h-5 flex-shrink-0 ${s.text}`}),h.jsx(Q,{className:`flex-1 text-sm font-medium ${s.text}`,children:t.message}),h.jsx(oe,{onClick:r,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${s.text} focus:outline-none`,"aria-label":cy("common.close_notification"),children:h.jsx(re,{name:de.Times,className:"w-4 h-4"})})]})},su=({items:t,separator:e=h.jsx(re,{name:de.ChevronRight,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),showHome:n=!1,homeHref:r="/",maxItems:s,className:o="",id:i,editorAttrs:a})=>{let c=n?[{label:"Home",href:r},...t]:t,d=c,u=!1;if(s&&c.length>s){u=!0;const f=c.slice(0,1),g=c.slice(-(s-2));d=[...f,...g]}return h.jsx(Lo,{className:`flex items-center space-x-2 text-sm ${o}`,"aria-label":"Breadcrumb",id:i,...a,children:h.jsx(I,{className:"flex items-center space-x-2",children:d.map((f,g)=>{const m=g===d.length-1,p=g===0;return h.jsxs(v.Fragment,{children:[u&&p&&h.jsxs(h.Fragment,{children:[h.jsx(ou,{item:f,isLast:!1}),h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e}),h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:"..."}),d.length>1&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e})]}),(!u||!p)&&h.jsxs(h.Fragment,{children:[h.jsx(ou,{item:f,isLast:m}),!m&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e})]})]},g)})})})},ou=({item:t,isLast:e})=>e||!t.href&&!t.onClick?h.jsx(Q,{className:"font-medium text-gray-900 dark:text-white","aria-current":e?"page":void 0,children:t.label}):t.onClick?h.jsx(Lr,{href:"#",onClick:n=>{n.preventDefault(),t.onClick?.()},className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:t.label}):h.jsx(Lr,{href:t.href,className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:t.label}),gy=({title:t,description:e,iconName:n,illustrationSrc:r,illustrationAlt:s="Empty state illustration",actions:o=[],className:i="",id:a,editorAttrs:l})=>{const c={primary:"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 focus:ring-gray-500",danger:"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-gray-500"};return h.jsxs(I,{className:`flex flex-col items-center justify-center p-8 text-center ${i}`,role:"status","aria-live":"polite",id:a,...l,children:[r?h.jsx(yn,{src:r,alt:s,className:"w-48 h-48 mb-6 object-contain"}):n?h.jsx(I,{className:"mb-6 p-4 rounded-full bg-gray-100 dark:bg-gray-700",children:h.jsx(re,{name:n,className:"w-12 h-12 text-gray-400 dark:text-gray-500"})}):null,h.jsx(Qn,{className:"text-xl font-semibold text-gray-900 dark:text-white mb-2",children:t}),e&&h.jsx(bt,{className:"text-gray-600 dark:text-gray-400 mb-6 max-w-md",children:e}),o.length>0&&h.jsx(I,{className:"flex flex-wrap gap-3 justify-center",children:o.map((d,u)=>h.jsxs(oe,{onClick:d.onClick,className:`
`,role:"alert",children:[h.jsx(re,{name:o,className:`w-5 h-5 flex-shrink-0 ${s.text}`}),h.jsx(Q,{className:`flex-1 text-sm font-medium ${s.text}`,children:t.message}),h.jsx(oe,{onClick:r,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${s.text} focus:outline-none`,"aria-label":fy("common.close_notification"),children:h.jsx(re,{name:de.Times,className:"w-4 h-4"})})]})},ou=({items:t,separator:e=h.jsx(re,{name:de.ChevronRight,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),showHome:n=!1,homeHref:r="/",maxItems:s,className:o="",id:i,editorAttrs:a})=>{let c=n?[{label:"Home",href:r},...t]:t,d=c,u=!1;if(s&&c.length>s){u=!0;const f=c.slice(0,1),g=c.slice(-(s-2));d=[...f,...g]}return h.jsx(Lo,{className:`flex items-center space-x-2 text-sm ${o}`,"aria-label":"Breadcrumb",id:i,...a,children:h.jsx(I,{className:"flex items-center space-x-2",children:d.map((f,g)=>{const m=g===d.length-1,p=g===0;return h.jsxs(v.Fragment,{children:[u&&p&&h.jsxs(h.Fragment,{children:[h.jsx(iu,{item:f,isLast:!1}),h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e}),h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:"..."}),d.length>1&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e})]}),(!u||!p)&&h.jsxs(h.Fragment,{children:[h.jsx(iu,{item:f,isLast:m}),!m&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e})]})]},g)})})})},iu=({item:t,isLast:e})=>e||!t.href&&!t.onClick?h.jsx(Q,{className:"font-medium text-gray-900 dark:text-white","aria-current":e?"page":void 0,children:t.label}):t.onClick?h.jsx(Lr,{href:"#",onClick:n=>{n.preventDefault(),t.onClick?.()},className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:t.label}):h.jsx(Lr,{href:t.href,className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:t.label}),by=({title:t,description:e,iconName:n,illustrationSrc:r,illustrationAlt:s="Empty state illustration",actions:o=[],className:i="",id:a,editorAttrs:l})=>{const c={primary:"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 focus:ring-gray-500",danger:"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-gray-500"};return h.jsxs(I,{className:`flex flex-col items-center justify-center p-8 text-center ${i}`,role:"status","aria-live":"polite",id:a,...l,children:[r?h.jsx(yn,{src:r,alt:s,className:"w-48 h-48 mb-6 object-contain"}):n?h.jsx(I,{className:"mb-6 p-4 rounded-full bg-gray-100 dark:bg-gray-700",children:h.jsx(re,{name:n,className:"w-12 h-12 text-gray-400 dark:text-gray-500"})}):null,h.jsx(Qn,{className:"text-xl font-semibold text-gray-900 dark:text-white mb-2",children:t}),e&&h.jsx(bt,{className:"text-gray-600 dark:text-gray-400 mb-6 max-w-md",children:e}),o.length>0&&h.jsx(I,{className:"flex flex-wrap gap-3 justify-center",children:o.map((d,u)=>h.jsxs(oe,{onClick:d.onClick,className:`
px-4 py-2 rounded-md font-medium transition-colors
focus:outline-none focus:ring-2 focus:ring-offset-2
${c[d.variant||"primary"]}
`,children:[d.iconName&&h.jsx(re,{name:d.iconName,className:"w-4 h-4 mr-2 inline-block"}),d.label]},u))})]})},py=(t,e)=>window.G7Core?.t?.(t,e)??t,my=({id:t,size:e="md",color:n="text-blue-600",fullscreen:r=!1,text:s,className:o="",editorAttrs:i})=>{const a={sm:"w-4 h-4 border-2",md:"w-8 h-8 border-2",lg:"w-12 h-12 border-3",xl:"w-16 h-16 border-4"},l={sm:"text-xs",md:"text-sm",lg:"text-base",xl:"text-lg"},c=h.jsxs(I,{className:`flex flex-col items-center justify-center gap-3 ${o}`,children:[h.jsx(I,{className:`
`,children:[d.iconName&&h.jsx(re,{name:d.iconName,className:"w-4 h-4 mr-2 inline-block"}),d.label]},u))})]})},yy=(t,e)=>window.G7Core?.t?.(t,e)??t,vy=({id:t,size:e="md",color:n="text-blue-600",fullscreen:r=!1,text:s,className:o="",editorAttrs:i})=>{const a={sm:"w-4 h-4 border-2",md:"w-8 h-8 border-2",lg:"w-12 h-12 border-3",xl:"w-16 h-16 border-4"},l={sm:"text-xs",md:"text-sm",lg:"text-base",xl:"text-lg"},c=h.jsxs(I,{className:`flex flex-col items-center justify-center gap-3 ${o}`,children:[h.jsx(I,{className:`
${a[e]}
border-gray-200 dark:border-gray-700 border-t-current
rounded-full animate-spin
${n}
`,role:"status","aria-label":py("common.loading")}),s&&h.jsx(Q,{className:`${l[e]} ${n} font-medium`,children:s})]});return r?h.jsx(I,{id:t,className:"fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90","aria-live":"polite","aria-busy":"true",...i,children:c}):h.jsx(I,{id:t,className:"flex items-center justify-center p-4","aria-live":"polite","aria-busy":"true",...i,children:c})},zr=()=>window.G7Core,sl=(t,e)=>window.G7Core?.t?.(t,e)??t,by=({logo:t,logoAlt:e="Logo",menu:n=[],collapsed:r=!1,onToggleCollapse:s,className:o="",currentLocale:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(new Set),[u,f]=v.useState(i||zr()?.locale?.current?.()||"ko"),[g,m]=v.useState(window.location.pathname),p=v.useMemo(()=>{const w=[],M=O=>{for(const L of O)L.url&&w.push(L.url),L.children&&M(L.children)};return M(n),w},[n]),y=v.useCallback(w=>{let M=null;for(const O of p)(w===O||w.startsWith(O+"/"))&&(!M||O.length>M.length)&&(M=O);return M},[p]);v.useEffect(()=>{const w=zr();return w?.state?.subscribe?w.state.subscribe(O=>{const L=O?._global?.locale||w?.locale?.current?.()||"ko";f(L);const F=O?._global?.currentPath||window.location.pathname;m(F)}):void 0},[]),v.useEffect(()=>{const w=()=>{m(window.location.pathname)};return window.addEventListener("popstate",w),()=>window.removeEventListener("popstate",w)},[]),v.useEffect(()=>{i&&f(i)},[i]);const b=(w,M)=>!w||typeof M!="string"?!1:M===w?!0:M.startsWith(w+"/")?y(M)===w:!1,S=(w,M)=>{const O=new Set,L=F=>{for(const $ of F)if($.children&&$.children.length>0){if(L($.children)||b($.url,M))return O.add($.id),!0}else if(b($.url,M))return!0;return!1};return L(w),O};v.useEffect(()=>{if(n.length>0&&g){const w=S(n,g);w.size>0&&d(M=>new Set([...M,...w]))}},[g,n]);const x=w=>b(w,g),E=w=>w.children?w.children.some(M=>x(M.url)||E(M)):!1,k=w=>{d(M=>{const O=new Set(M);return O.has(w)?O.delete(w):O.add(w),O})},C=w=>typeof w=="string"?w:w[u]||w.en||Object.values(w)[0]||"",N=(w,M)=>{zr()?.dispatch?(w.preventDefault(),zr().navigation?.onComplete(()=>{zr().dispatch({handler:"setState",params:{target:"global",sidebarOpen:!1}})}),zr().dispatch({handler:"navigate",params:{path:M}})):window.location.href=M},_=(w,M=0,O=!1)=>{const L=w.children&&w.children.length>0,F=c.has(w.id),$=C(w.name),D=M>0,A=x(w.url),T=E(w),z=D?"admin-sidebar-subitem":"admin-sidebar-item",H="admin-sidebar-item-active",W="admin-sidebar-item-active-parent",j="admin-sidebar-item-default";return h.jsxs(I,{className:D?"relative group":"",children:[D&&h.jsxs(h.Fragment,{children:[!O&&h.jsx(I,{className:"admin-sidebar-tree-line-vertical"}),h.jsx(I,{className:"admin-sidebar-tree-line-curve"})]}),L&&w.url?h.jsxs(I,{className:`
`,role:"status","aria-label":yy("common.loading")}),s&&h.jsx(Q,{className:`${l[e]} ${n} font-medium`,children:s})]});return r?h.jsx(I,{id:t,className:"fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90","aria-live":"polite","aria-busy":"true",...i,children:c}):h.jsx(I,{id:t,className:"flex items-center justify-center p-4","aria-live":"polite","aria-busy":"true",...i,children:c})},zr=()=>window.G7Core,ol=(t,e)=>window.G7Core?.t?.(t,e)??t,xy=({logo:t,logoAlt:e="Logo",menu:n=[],collapsed:r=!1,onToggleCollapse:s,className:o="",currentLocale:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(new Set),[u,f]=v.useState(i||zr()?.locale?.current?.()||"ko"),[g,m]=v.useState(window.location.pathname),p=v.useMemo(()=>{const w=[],M=O=>{for(const L of O)L.url&&w.push(L.url),L.children&&M(L.children)};return M(n),w},[n]),y=v.useCallback(w=>{let M=null;for(const O of p)(w===O||w.startsWith(O+"/"))&&(!M||O.length>M.length)&&(M=O);return M},[p]);v.useEffect(()=>{const w=zr();return w?.state?.subscribe?w.state.subscribe(O=>{const L=O?._global?.locale||w?.locale?.current?.()||"ko";f(L);const F=O?._global?.currentPath||window.location.pathname;m(F)}):void 0},[]),v.useEffect(()=>{const w=()=>{m(window.location.pathname)};return window.addEventListener("popstate",w),()=>window.removeEventListener("popstate",w)},[]),v.useEffect(()=>{i&&f(i)},[i]);const b=(w,M)=>!w||typeof M!="string"?!1:M===w?!0:M.startsWith(w+"/")?y(M)===w:!1,S=(w,M)=>{const O=new Set,L=F=>{for(const $ of F)if($.children&&$.children.length>0){if(L($.children)||b($.url,M))return O.add($.id),!0}else if(b($.url,M))return!0;return!1};return L(w),O};v.useEffect(()=>{if(n.length>0&&g){const w=S(n,g);w.size>0&&d(M=>new Set([...M,...w]))}},[g,n]);const x=w=>b(w,g),E=w=>w.children?w.children.some(M=>x(M.url)||E(M)):!1,k=w=>{d(M=>{const O=new Set(M);return O.has(w)?O.delete(w):O.add(w),O})},C=w=>typeof w=="string"?w:w[u]||w.en||Object.values(w)[0]||"",N=(w,M)=>{zr()?.dispatch?(w.preventDefault(),zr().navigation?.onComplete(()=>{zr().dispatch({handler:"setState",params:{target:"global",sidebarOpen:!1}})}),zr().dispatch({handler:"navigate",params:{path:M}})):window.location.href=M},_=(w,M=0,O=!1)=>{const L=w.children&&w.children.length>0,F=c.has(w.id),$=C(w.name),D=M>0,A=x(w.url),T=E(w),z=D?"admin-sidebar-subitem":"admin-sidebar-item",H="admin-sidebar-item-active",W="admin-sidebar-item-active-parent",j="admin-sidebar-item-default";return h.jsxs(I,{className:D?"relative group":"",children:[D&&h.jsxs(h.Fragment,{children:[!O&&h.jsx(I,{className:"admin-sidebar-tree-line-vertical"}),h.jsx(I,{className:"admin-sidebar-tree-line-curve"})]}),L&&w.url?h.jsxs(I,{className:`
${z}
${A?H:T?W:j}
${r?"justify-center":""}
`,children:[h.jsxs(oe,{onClick:B=>N(B,w.url),className:"flex items-center justify-start flex-1 min-w-0",children:[w.icon&&h.jsx(pt,{className:`${w.icon} ${r?"":"mr-3"} w-5 h-5 flex items-center justify-center`}),!r&&h.jsx(Q,{className:"truncate",children:$})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[w.module_id&&h.jsx(pt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(oe,{onClick:()=>k(w.id),className:"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600","aria-label":sl(F?"common.collapse":"common.expand"),children:h.jsx(pt,{className:`fas fa-chevron-down w-4 h-4 transition-transform ${F?"rotate-180":""}`})})]})]}):L?h.jsxs(oe,{onClick:()=>k(w.id),className:`
`,children:[h.jsxs(oe,{onClick:B=>N(B,w.url),className:"flex items-center justify-start flex-1 min-w-0",children:[w.icon&&h.jsx(pt,{className:`${w.icon} ${r?"":"mr-3"} w-5 h-5 flex items-center justify-center`}),!r&&h.jsx(Q,{className:"truncate",children:$})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[w.module_id&&h.jsx(pt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(oe,{onClick:()=>k(w.id),className:"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600","aria-label":ol(F?"common.collapse":"common.expand"),children:h.jsx(pt,{className:`fas fa-chevron-down w-4 h-4 transition-transform ${F?"rotate-180":""}`})})]})]}):L?h.jsxs(oe,{onClick:()=>k(w.id),className:`
${z}
${T?W:j}
${r?"justify-center":""}
@@ -68,34 +68,34 @@ var SirsoftAdminBasic=(function(R,h,v,On){"use strict";var T5=Object.definePrope
admin-sidebar-item-disabled
${r?"justify-center":""}
text-left
`,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(pt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&w.module_id&&h.jsx(pt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}),L&&!r&&F&&h.jsx(I,{className:"relative ml-2",children:w.children.map((B,G)=>_(B,M+1,G===w.children.length-1))})]},w.id)};return h.jsxs(I,{className:o,id:a,...l,children:[(t||s)&&h.jsxs(I,{className:"admin-sidebar-header",children:[t&&!r&&h.jsx(yn,{src:t,alt:e,className:"h-8"}),s&&h.jsx(oe,{onClick:s,className:"admin-sidebar-collapse-btn","aria-label":sl(r?"common.expand_sidebar":"common.collapse_sidebar"),children:h.jsx(pt,{className:`${r?"fas fa-chevron-right":"fas fa-chevron-left"} w-5 h-5 text-slate-600 dark:text-slate-400`})})]}),h.jsx(I,{className:"admin-sidebar-menu-list",children:n.map((w,M)=>_(w,0,M===n.length-1))})]})},_s=(t,e)=>window.G7Core?.t?.(t,e)??t,yy=({user:t={name:"",email:""},notifications:e=[],onNotificationClick:n,onProfileClick:r,onLogoutClick:s,className:o=""})=>{const[i,a]=v.useState(!1),[l,c]=v.useState(!1),d=v.useRef(null),u=v.useRef(null),f=e.filter(g=>!g.read).length;return v.useEffect(()=>{const g=m=>{d.current&&!d.current.contains(m.target)&&a(!1),u.current&&!u.current.contains(m.target)&&c(!1)};return document.addEventListener("mousedown",g),()=>document.removeEventListener("mousedown",g)},[]),h.jsxs(I,{className:`
`,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(pt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&w.module_id&&h.jsx(pt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}),L&&!r&&F&&h.jsx(I,{className:"relative ml-2",children:w.children.map((B,G)=>_(B,M+1,G===w.children.length-1))})]},w.id)};return h.jsxs(I,{className:o,id:a,...l,children:[(t||s)&&h.jsxs(I,{className:"admin-sidebar-header",children:[t&&!r&&h.jsx(yn,{src:t,alt:e,className:"h-8"}),s&&h.jsx(oe,{onClick:s,className:"admin-sidebar-collapse-btn","aria-label":ol(r?"common.expand_sidebar":"common.collapse_sidebar"),children:h.jsx(pt,{className:`${r?"fas fa-chevron-right":"fas fa-chevron-left"} w-5 h-5 text-slate-600 dark:text-slate-400`})})]}),h.jsx(I,{className:"admin-sidebar-menu-list",children:n.map((w,M)=>_(w,0,M===n.length-1))})]})},_s=(t,e)=>window.G7Core?.t?.(t,e)??t,wy=({user:t={name:"",email:""},notifications:e=[],onNotificationClick:n,onProfileClick:r,onLogoutClick:s,className:o=""})=>{const[i,a]=v.useState(!1),[l,c]=v.useState(!1),d=v.useRef(null),u=v.useRef(null),f=e.filter(g=>!g.read).length;return v.useEffect(()=>{const g=m=>{d.current&&!d.current.contains(m.target)&&a(!1),u.current&&!u.current.contains(m.target)&&c(!1)};return document.addEventListener("mousedown",g),()=>document.removeEventListener("mousedown",g)},[]),h.jsxs(I,{className:`
bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-end gap-4
${o}
`,children:[h.jsxs(I,{ref:d,className:"relative",children:[h.jsxs(oe,{onClick:()=>a(!i),className:"relative p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors","aria-label":_s("common.notifications"),children:[h.jsx(re,{name:de.Bell,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),f>0&&h.jsx(Q,{className:"absolute top-0 right-0 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full",children:f>9?"9+":f})]}),i&&h.jsxs(I,{className:"absolute right-0 mt-2 w-80 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsx(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx(Q,{className:"font-semibold text-gray-900 dark:text-white",children:_s("common.notifications")})}),h.jsx(I,{className:"max-h-96 overflow-y-auto",children:e.length===0?h.jsx(I,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:h.jsx(Q,{children:_s("common.no_notifications")})}):e.map(g=>h.jsx(oe,{onClick:()=>{g.onClick?.(),n?.(g.id),a(!1)},className:`
w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 border-b border-gray-100 dark:border-gray-700 transition-colors
${g.read?"":"bg-blue-50 dark:bg-blue-900/20"}
`,children:h.jsxs(I,{className:"flex items-start gap-3",children:[g.iconName&&h.jsx(re,{name:g.iconName,className:"w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5"}),h.jsxs(I,{className:"flex-1",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:g.title}),h.jsx(I,{className:"text-gray-600 dark:text-gray-400 text-sm mt-1",children:g.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1",children:g.time})]}),!g.read&&h.jsx(Q,{className:"w-2 h-2 bg-blue-500 rounded-full mt-1.5"})]})},g.id))})]})]}),h.jsxs(I,{ref:u,className:"relative",children:[h.jsxs(oe,{onClick:()=>c(!l),className:"flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[t.avatar?h.jsx(yn,{src:t.avatar,alt:t.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"hidden md:block text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:t.name}),t.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:t.role})]}),h.jsx(re,{name:de.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]}),l&&h.jsxs(I,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:t.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:t.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(oe,{onClick:()=>{r?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors text-gray-900 dark:text-white",children:[h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{children:_s("common.profile_settings")})]}),h.jsxs(oe,{onClick:()=>{s?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 text-red-600 dark:text-red-400 transition-colors",children:[h.jsx(re,{name:de.ArrowRight,className:"w-5 h-5"}),h.jsx(Q,{children:_s("common.logout")})]})]})]})]})]})},vy=({notifications:t=[],unreadCount:e=0,hasMore:n=!1,loading:r=!1,titleText:s="Notifications",emptyText:o="No notifications",markAllReadText:i="Mark all as read",deleteAllText:a="Delete all",unreadOnlyText:l="Unread only",unreadOnly:c=!1,onNotificationClick:d,onClose:u,onLoadMore:f,onMarkAllRead:g,onDeleteAll:m,onDelete:p,onUnreadOnlyToggle:y,dropdownAlign:b="right",className:S="",id:x,editorAttrs:E})=>{const[k,C]=v.useState(!1),N=v.useRef(null),_=v.useRef(null),w=v.useRef(null),M=v.useRef(null),O=v.useRef(new Set),L=v.useRef(f),F=v.useRef(n),$=v.useRef(r),D=v.useRef(t.length),A=v.useRef(-1);v.useEffect(()=>{L.current=f,F.current=n,$.current=r,D.current=t.length}),v.useEffect(()=>{if(!M.current||!w.current||!k)return;A.current=-1;const W=new IntersectionObserver(([j])=>{j.isIntersecting&&($.current||!F.current||A.current!==D.current&&(A.current=D.current,L.current?.()))},{root:w.current,threshold:.1});return W.observe(M.current),()=>W.disconnect()},[k]),v.useEffect(()=>{if(!w.current||!k)return;const W=new IntersectionObserver(U=>{U.forEach(B=>{const G=B.target.getAttribute("data-notification-id"),V=B.target.getAttribute("data-unread")==="true";G&&V&&B.isIntersecting&&O.current.add(G)})},{root:w.current,threshold:.5});return w.current.querySelectorAll("[data-notification-id]").forEach(U=>W.observe(U)),()=>W.disconnect()},[k,t]);const T=e>0?e:t.filter(W=>!W.read).length,z=v.useCallback(()=>{if(k){if(T>0){const W=Array.from(O.current).filter(j=>{const U=t.find(B=>String(B.id)===String(j));return U&&!U.read});W.length>0&&u?.(W)}O.current.clear(),C(!1)}},[k,u,T,t]);v.useEffect(()=>{if(!k)return;const W=j=>{N.current&&!N.current.contains(j.target)&&z()};return document.addEventListener("mousedown",W),()=>document.removeEventListener("mousedown",W)},[k,z]);const H=v.useCallback(()=>{k?z():C(!0)},[k,z]);return v.useLayoutEffect(()=>{const W=_.current;if(!k||!W)return;W.style.transform="";const j=W.getBoundingClientRect(),U=typeof window<"u"?window.innerWidth:0;if(j.width===0&&j.height===0)return;const B=8;let G=0;j.right>U-B?G=-(j.right-(U-B)):j.left<B&&(G=B-j.left),G!==0&&(W.style.transform=`translateX(${G}px)`)},[k,b,t.length]),h.jsxs(I,{ref:N,className:`relative ${S}`,id:x,...E,children:[h.jsxs(oe,{onClick:H,className:"relative w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors cursor-pointer","aria-label":s,children:[h.jsx(re,{name:de.Bell,className:"text-sm text-slate-500 dark:text-slate-400"}),T>0&&h.jsx(Q,{clas Line truncated
`,children:h.jsxs(I,{className:"flex items-start gap-3",children:[g.iconName&&h.jsx(re,{name:g.iconName,className:"w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5"}),h.jsxs(I,{className:"flex-1",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:g.title}),h.jsx(I,{className:"text-gray-600 dark:text-gray-400 text-sm mt-1",children:g.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1",children:g.time})]}),!g.read&&h.jsx(Q,{className:"w-2 h-2 bg-blue-500 rounded-full mt-1.5"})]})},g.id))})]})]}),h.jsxs(I,{ref:u,className:"relative",children:[h.jsxs(oe,{onClick:()=>c(!l),className:"flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[t.avatar?h.jsx(yn,{src:t.avatar,alt:t.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"hidden md:block text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:t.name}),t.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:t.role})]}),h.jsx(re,{name:de.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]}),l&&h.jsxs(I,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:t.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:t.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(oe,{onClick:()=>{r?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors text-gray-900 dark:text-white",children:[h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{children:_s("common.profile_settings")})]}),h.jsxs(oe,{onClick:()=>{s?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 text-red-600 dark:text-red-400 transition-colors",children:[h.jsx(re,{name:de.ArrowRight,className:"w-5 h-5"}),h.jsx(Q,{children:_s("common.logout")})]})]})]})]})]})},ky=({notifications:t=[],unreadCount:e=0,hasMore:n=!1,loading:r=!1,titleText:s="Notifications",emptyText:o="No notifications",markAllReadText:i="Mark all as read",deleteAllText:a="Delete all",unreadOnlyText:l="Unread only",unreadOnly:c=!1,onNotificationClick:d,onClose:u,onLoadMore:f,onMarkAllRead:g,onDeleteAll:m,onDelete:p,onUnreadOnlyToggle:y,dropdownAlign:b="right",className:S="",id:x,editorAttrs:E})=>{const[k,C]=v.useState(!1),N=v.useRef(null),_=v.useRef(null),w=v.useRef(null),M=v.useRef(null),O=v.useRef(new Set),L=v.useRef(f),F=v.useRef(n),$=v.useRef(r),D=v.useRef(t.length),A=v.useRef(-1);v.useEffect(()=>{L.current=f,F.current=n,$.current=r,D.current=t.length}),v.useEffect(()=>{if(!M.current||!w.current||!k)return;A.current=-1;const W=new IntersectionObserver(([j])=>{j.isIntersecting&&($.current||!F.current||A.current!==D.current&&(A.current=D.current,L.current?.()))},{root:w.current,threshold:.1});return W.observe(M.current),()=>W.disconnect()},[k]),v.useEffect(()=>{if(!w.current||!k)return;const W=new IntersectionObserver(U=>{U.forEach(B=>{const G=B.target.getAttribute("data-notification-id"),V=B.target.getAttribute("data-unread")==="true";G&&V&&B.isIntersecting&&O.current.add(G)})},{root:w.current,threshold:.5});return w.current.querySelectorAll("[data-notification-id]").forEach(U=>W.observe(U)),()=>W.disconnect()},[k,t]);const T=e>0?e:t.filter(W=>!W.read).length,z=v.useCallback(()=>{if(k){if(T>0){const W=Array.from(O.current).filter(j=>{const U=t.find(B=>String(B.id)===String(j));return U&&!U.read});W.length>0&&u?.(W)}O.current.clear(),C(!1)}},[k,u,T,t]);v.useEffect(()=>{if(!k)return;const W=j=>{N.current&&!N.current.contains(j.target)&&z()};return document.addEventListener("mousedown",W),()=>document.removeEventListener("mousedown",W)},[k,z]);const H=v.useCallback(()=>{k?z():C(!0)},[k,z]);return v.useLayoutEffect(()=>{const W=_.current;if(!k||!W)return;W.style.transform="";const j=W.getBoundingClientRect(),U=typeof window<"u"?window.innerWidth:0;if(j.width===0&&j.height===0)return;const B=8;let G=0;j.right>U-B?G=-(j.right-(U-B)):j.left<B&&(G=B-j.left),G!==0&&(W.style.transform=`translateX(${G}px)`)},[k,b,t.length]),h.jsxs(I,{ref:N,className:`relative ${S}`,id:x,...E,children:[h.jsxs(oe,{onClick:H,className:"relative w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors cursor-pointer","aria-label":s,children:[h.jsx(re,{name:de.Bell,className:"text-sm text-slate-500 dark:text-slate-400"}),T>0&&h.jsx(Q,{clas Line truncated
px-3 py-1.5 text-sm font-medium rounded-md transition-colors
${g===x?"bg-blue-600 text-white":"bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"}
${u?"opacity-50 cursor-not-allowed":""}
`,children:au(x)},x))})]}):h.jsxs(I,{ref:p,className:`relative ${s}`,id:a,...l,children:[h.jsxs(oe,{onClick:()=>d(!c),className:i?"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer":"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors","aria-haspopup":"listbox","aria-expanded":c,"aria-label":e,children:[h.jsx(re,{name:de.Globe,className:i?"w-4 h-4":"w-5 h-5 text-gray-600 dark:text-gray-400"}),i&&h.jsx(Q,{className:"font-medium uppercase",children:g}),i&&h.jsx(re,{name:de.ChevronDown,className:"w-3 h-3"})]}),c&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsx(I,{className:"py-2",children:t.map(x=>h.jsxs(oe,{onClick:()=>S(x),disabled:u,className:`
`,children:lu(x)},x))})]}):h.jsxs(I,{ref:p,className:`relative ${s}`,id:a,...l,children:[h.jsxs(oe,{onClick:()=>d(!c),className:i?"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer":"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors","aria-haspopup":"listbox","aria-expanded":c,"aria-label":e,children:[h.jsx(re,{name:de.Globe,className:i?"w-4 h-4":"w-5 h-5 text-gray-600 dark:text-gray-400"}),i&&h.jsx(Q,{className:"font-medium uppercase",children:g}),i&&h.jsx(re,{name:de.ChevronDown,className:"w-3 h-3"})]}),c&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsx(I,{className:"py-2",children:t.map(x=>h.jsxs(oe,{onClick:()=>S(x),disabled:u,className:`
w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors
${g===x?"bg-gray-50 dark:bg-gray-700":""}
${u?"opacity-50 cursor-not-allowed":""}
`,children:[h.jsx(Q,{className:"text-gray-900 dark:text-white flex-1",children:au(x)}),g===x&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400"})]},x))})})]})},ky=({copyright:t="© 2026 G7",version:e,quickLinks:n=[],className:r="",changelogModalId:s,copyrightModalId:o,id:i,editorAttrs:a})=>{const l=v.useCallback(()=>{s&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/changelog",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreChangelogContent:"{{response.data.content}}"}},{handler:"openModal",target:s}],onError:[{handler:"openModal",target:s}]}]})},[s]),c=v.useCallback(()=>{o&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/license",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreLicenseContent:"{{response.data.content}}"}},{handler:"openModal",target:o}],onError:[{handler:"openModal",target:o}]}]})},[o]);return h.jsx(I,{className:`admin-footer ${r}`,id:i,...a,children:h.jsxs(I,{className:"admin-footer-row",children:[h.jsxs(I,{className:"admin-footer-group",children:[h.jsx(Q,{className:o?"admin-footer-link-hover":"",onClick:o?c:void 0,children:t}),e&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:"•"}),h.jsxs(Q,{className:`flex-center gap-2 ${s?"admin-footer-link-hover":""}`,onClick:s?l:void 0,children:[h.jsx(re,{name:de.Tag,className:"w-4 h-4"}),h.jsxs(Q,{children:["v",e]})]})]})]}),n.length>0&&h.jsx(I,{className:"flex-center gap-4",children:n.map(d=>h.jsxs(Lr,{href:d.url,className:"admin-footer-link",children:[d.iconName&&h.jsx(re,{name:d.iconName,className:"w-4 h-4"}),h.jsx(Q,{children:d.label})]},d.id))})]})})},Cy=({title:t,description:e,breadcrumbItems:n,tabs:r,onTabChange:s,actions:o,className:i="",children:a,id:l,editorAttrs:c})=>{const d=(u="secondary")=>({primary:"page-action-primary",secondary:"page-action-secondary",danger:"page-action-danger",ghost:"page-action-ghost"})[u];return h.jsxs(I,{className:i,id:l,...c,children:[n&&n.length>0&&h.jsx(I,{className:"mb-md",children:h.jsx(su,{items:n})}),h.jsxs(I,{className:"flex-between mb-lg",children:[h.jsxs(I,{children:[h.jsx(Aa,{className:"page-title",children:t}),e&&h.jsx(bt,{className:"page-description",children:e})]}),a?h.jsx(I,{className:"flex-center page-actions",children:a}):o&&o.length>0?h.jsx(I,{className:"page-actions",children:o.map(u=>h.jsxs(oe,{onClick:u.onClick,disabled:u.disabled,className:d(u.variant),children:[u.iconName&&h.jsx(re,{name:u.iconName,className:"icon-md"}),u.variant!=="ghost"&&u.label&&h.jsx(I,{children:u.label})]},u.id))}):null]}),r&&r.length>0&&h.jsx(I,{className:"tab-container",children:r.map(u=>h.jsxs(oe,{onClick:()=>s?.(u.value),className:u.active?"tab-btn-active":"tab-btn",children:[h.jsx(I,{children:u.label}),u.badge!==void 0&&h.jsx(I,{className:u.active?"tab-badge-active":"tab-badge",children:u.badge})]},u.id))})]})},Sy=()=>window.G7Core,_y=({title:t,description:e,className:n="",style:r,children:s,id:o,editorAttrs:i})=>{const a="flex items-center justify-between",l=Sy()?.style?.mergeClasses?.(a,n)??`${a} ${n}`;return h.jsxs(I,{className:l,style:r,id:o,...i,children:[h.jsxs(I,{children:[t&&h.jsx(Qn,{className:"text-base font-semibold text-gray-900 dark:text-white",children:t}),e&&h.jsx(bt,{className:"mt-1 text-sm text-gray-500 dark:text-gray-400",children:e})]}),s&&h.jsx(I,{className:"flex items-center gap-2",children:s})]})},Rn=(t,e)=>window.G7Core?.t?.(t,e)??t,Ey=({image:t,imageAlt:e,vendor:n,name:r,version:s,status:o,updateAvailable:i=!1,latestVersion:a,dependencies:l=[],showLayoutEditButton:c=!1,actions:d=[],onLayoutEditClick:u,className:f="",id:g,editorAttrs:m})=>{const p=b=>({active:"success",inactive:"default",pending:"pending",error:"error"})[b],y=b=>({active:Rn("common.status_active"),inactive:Rn("common.status_inactive"),pending:Rn("common.status_pending"),error:Rn("common.status_error")})[b];return h.jsxs(I,{className:`
`,children:[h.jsx(Q,{className:"text-gray-900 dark:text-white flex-1",children:lu(x)}),g===x&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400"})]},x))})})]})},_y=({copyright:t="© 2026 G7",version:e,quickLinks:n=[],className:r="",changelogModalId:s,copyrightModalId:o,id:i,editorAttrs:a})=>{const l=v.useCallback(()=>{s&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/changelog",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreChangelogContent:"{{response.data.content}}"}},{handler:"openModal",target:s}],onError:[{handler:"openModal",target:s}]}]})},[s]),c=v.useCallback(()=>{o&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/license",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreLicenseContent:"{{response.data.content}}"}},{handler:"openModal",target:o}],onError:[{handler:"openModal",target:o}]}]})},[o]);return h.jsx(I,{className:`admin-footer ${r}`,id:i,...a,children:h.jsxs(I,{className:"admin-footer-row",children:[h.jsxs(I,{className:"admin-footer-group",children:[h.jsx(Q,{className:o?"admin-footer-link-hover":"",onClick:o?c:void 0,children:t}),e&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:"•"}),h.jsxs(Q,{className:`flex-center gap-2 ${s?"admin-footer-link-hover":""}`,onClick:s?l:void 0,children:[h.jsx(re,{name:de.Tag,className:"w-4 h-4"}),h.jsxs(Q,{children:["v",e]})]})]})]}),n.length>0&&h.jsx(I,{className:"flex-center gap-4",children:n.map(d=>h.jsxs(Lr,{href:d.url,className:"admin-footer-link",children:[d.iconName&&h.jsx(re,{name:d.iconName,className:"w-4 h-4"}),h.jsx(Q,{children:d.label})]},d.id))})]})})},Ey=({title:t,description:e,breadcrumbItems:n,tabs:r,onTabChange:s,actions:o,className:i="",children:a,id:l,editorAttrs:c})=>{const d=(u="secondary")=>({primary:"page-action-primary",secondary:"page-action-secondary",danger:"page-action-danger",ghost:"page-action-ghost"})[u];return h.jsxs(I,{className:i,id:l,...c,children:[n&&n.length>0&&h.jsx(I,{className:"mb-md",children:h.jsx(ou,{items:n})}),h.jsxs(I,{className:"flex-between mb-lg",children:[h.jsxs(I,{children:[h.jsx(Da,{className:"page-title",children:t}),e&&h.jsx(bt,{className:"page-description",children:e})]}),a?h.jsx(I,{className:"flex-center page-actions",children:a}):o&&o.length>0?h.jsx(I,{className:"page-actions",children:o.map(u=>h.jsxs(oe,{onClick:u.onClick,disabled:u.disabled,className:d(u.variant),children:[u.iconName&&h.jsx(re,{name:u.iconName,className:"icon-md"}),u.variant!=="ghost"&&u.label&&h.jsx(I,{children:u.label})]},u.id))}):null]}),r&&r.length>0&&h.jsx(I,{className:"tab-container",children:r.map(u=>h.jsxs(oe,{onClick:()=>s?.(u.value),className:u.active?"tab-btn-active":"tab-btn",children:[h.jsx(I,{children:u.label}),u.badge!==void 0&&h.jsx(I,{className:u.active?"tab-badge-active":"tab-badge",children:u.badge})]},u.id))})]})},My=()=>window.G7Core,Ny=({title:t,description:e,className:n="",style:r,children:s,id:o,editorAttrs:i})=>{const a="flex items-center justify-between",l=My()?.style?.mergeClasses?.(a,n)??`${a} ${n}`;return h.jsxs(I,{className:l,style:r,id:o,...i,children:[h.jsxs(I,{children:[t&&h.jsx(Qn,{className:"text-base font-semibold text-gray-900 dark:text-white",children:t}),e&&h.jsx(bt,{className:"mt-1 text-sm text-gray-500 dark:text-gray-400",children:e})]}),s&&h.jsx(I,{className:"flex items-center gap-2",children:s})]})},Rn=(t,e)=>window.G7Core?.t?.(t,e)??t,Oy=({image:t,imageAlt:e,vendor:n,name:r,version:s,status:o,updateAvailable:i=!1,latestVersion:a,dependencies:l=[],showLayoutEditButton:c=!1,actions:d=[],onLayoutEditClick:u,className:f="",id:g,editorAttrs:m})=>{const p=b=>({active:"success",inactive:"default",pending:"pending",error:"error"})[b],y=b=>({active:Rn("common.status_active"),inactive:Rn("common.status_inactive"),pending:Rn("common.status_pending"),error:Rn("common.status_error")})[b];return h.jsxs(I,{className:`
bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-lg transition-shadow
${f}
`,id:g,...m,children:[t&&h.jsxs(I,{className:"relative",children:[h.jsx(yn,{src:t,alt:e||Rn("common.template_preview",{vendor:n,name:r}),className:"w-full h-48 object-cover"}),h.jsx(I,{className:"absolute top-3 right-3",children:h.jsx($o,{status:p(o),label:y(o),showIcon:!0})}),i&&h.jsx(I,{className:"absolute top-3 left-3",children:h.jsxs(Q,{className:"flex items-center gap-1 px-2 py-1 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800 rounded-md text-xs font-semibold",children:[h.jsx(re,{name:de.ExclamationTriangle,className:"w-3 h-3"}),h.jsx(Q,{children:Rn("common.update_available")})]})})]}),h.jsxs(I,{className:"p-4",children:[h.jsxs(I,{className:"flex items-start justify-between mb-3",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Ir,{className:"text-lg font-bold text-gray-900 dark:text-white",children:[n,"/",r]}),h.jsxs(I,{className:"flex items-center gap-2 mt-1 text-sm text-gray-600 dark:text-gray-400",children:[h.jsxs(Q,{children:["v",s]}),i&&a&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{children:"→"}),h.jsxs(Q,{className:"text-yellow-600 dark:text-yellow-400 font-semibold",children:["v",a]})]})]}),!t&&h.jsx(I,{className:"mt-2",children:h.jsx($o,{status:p(o),label:y(o),showIcon:!0})})]}),d.length>0&&h.jsx(bs,{items:d,triggerIconName:de.EllipsisVertical,position:"left"})]}),l.length>0&&h.jsxs(I,{className:"mb-3",children:[h.jsxs(bt,{className:"text-xs text-gray-500 dark:text-gray-400 mb-1",children:[Rn("common.dependencies"),":"]}),h.jsx(I,{className:"flex flex-wrap gap-1",children:l.map((b,S)=>h.jsx(Q,{className:"px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded",children:b},S))})]}),c&&u&&h.jsxs(oe,{onClick:u,className:"w-full px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors flex items-center justify-center gap-2 font-medium",children:[h.jsx(re,{name:de.Edit,className:"w-4 h-4"}),h.jsx(Q,{children:Rn("common.edit_layout")})]})]})]})},lu=({type:t,message:e,dismissible:n=!1,onDismiss:r,className:s="",id:o,editorAttrs:i})=>{const a={info:{containerClass:"alert-info",iconClass:"alert-icon-info",textClass:"alert-text-info",icon:de.InfoCircle},success:{containerClass:"alert-success",iconClass:"alert-icon-success",textClass:"alert-text-success",icon:de.CheckCircle},warning:{containerClass:"alert-warning",iconClass:"alert-icon-warning",textClass:"alert-text-warning",icon:de.ExclamationTriangle},error:{containerClass:"alert-error",iconClass:"alert-icon-error",textClass:"alert-text-error",icon:de.TimesCircle}},l=a[t]??a.info;return h.jsxs(I,{className:`${l.containerClass} ${s}`,role:"alert","aria-live":"polite",id:o,...i,children:[h.jsx(re,{name:l.icon,className:l.iconClass,ariaLabel:`${t} icon`}),h.jsx(bt,{className:l.textClass,children:e}),n&&h.jsx(oe,{onClick:r,className:`alert-dismiss ${l.textClass}`,"aria-label":"Close alert",children:h.jsx(re,{name:de.Times,className:l.iconClass,ariaLabel:"Close"})})]})},My=({layoutName:t,onBack:e,onPreview:n,onSave:r,isSaving:s=!1,className:o=""})=>{const i=`
`,id:g,...m,children:[t&&h.jsxs(I,{className:"relative",children:[h.jsx(yn,{src:t,alt:e||Rn("common.template_preview",{vendor:n,name:r}),className:"w-full h-48 object-cover"}),h.jsx(I,{className:"absolute top-3 right-3",children:h.jsx($o,{status:p(o),label:y(o),showIcon:!0})}),i&&h.jsx(I,{className:"absolute top-3 left-3",children:h.jsxs(Q,{className:"flex items-center gap-1 px-2 py-1 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800 rounded-md text-xs font-semibold",children:[h.jsx(re,{name:de.ExclamationTriangle,className:"w-3 h-3"}),h.jsx(Q,{children:Rn("common.update_available")})]})})]}),h.jsxs(I,{className:"p-4",children:[h.jsxs(I,{className:"flex items-start justify-between mb-3",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Ir,{className:"text-lg font-bold text-gray-900 dark:text-white",children:[n,"/",r]}),h.jsxs(I,{className:"flex items-center gap-2 mt-1 text-sm text-gray-600 dark:text-gray-400",children:[h.jsxs(Q,{children:["v",s]}),i&&a&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{children:"→"}),h.jsxs(Q,{className:"text-yellow-600 dark:text-yellow-400 font-semibold",children:["v",a]})]})]}),!t&&h.jsx(I,{className:"mt-2",children:h.jsx($o,{status:p(o),label:y(o),showIcon:!0})})]}),d.length>0&&h.jsx(bs,{items:d,triggerIconName:de.EllipsisVertical,position:"left"})]}),l.length>0&&h.jsxs(I,{className:"mb-3",children:[h.jsxs(bt,{className:"text-xs text-gray-500 dark:text-gray-400 mb-1",children:[Rn("common.dependencies"),":"]}),h.jsx(I,{className:"flex flex-wrap gap-1",children:l.map((b,S)=>h.jsx(Q,{className:"px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded",children:b},S))})]}),c&&u&&h.jsxs(oe,{onClick:u,className:"w-full px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors flex items-center justify-center gap-2 font-medium",children:[h.jsx(re,{name:de.Edit,className:"w-4 h-4"}),h.jsx(Q,{children:Rn("common.edit_layout")})]})]})]})},cu=({type:t,message:e,dismissible:n=!1,onDismiss:r,className:s="",id:o,editorAttrs:i})=>{const a={info:{containerClass:"alert-info",iconClass:"alert-icon-info",textClass:"alert-text-info",icon:de.InfoCircle},success:{containerClass:"alert-success",iconClass:"alert-icon-success",textClass:"alert-text-success",icon:de.CheckCircle},warning:{containerClass:"alert-warning",iconClass:"alert-icon-warning",textClass:"alert-text-warning",icon:de.ExclamationTriangle},error:{containerClass:"alert-error",iconClass:"alert-icon-error",textClass:"alert-text-error",icon:de.TimesCircle}},l=a[t]??a.info;return h.jsxs(I,{className:`${l.containerClass} ${s}`,role:"alert","aria-live":"polite",id:o,...i,children:[h.jsx(re,{name:l.icon,className:l.iconClass,ariaLabel:`${t} icon`}),h.jsx(bt,{className:l.textClass,children:e}),n&&h.jsx(oe,{onClick:r,className:`alert-dismiss ${l.textClass}`,"aria-label":"Close alert",children:h.jsx(re,{name:de.Times,className:l.iconClass,ariaLabel:"Close"})})]})},Ty=({layoutName:t,onBack:e,onPreview:n,onSave:r,isSaving:s=!1,className:o=""})=>{const i=`
flex items-center justify-between gap-4 p-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700
${o}
`.trim().replace(/\s+/g," ");return h.jsxs(I,{className:i,children:[h.jsxs(I,{className:"flex items-center gap-3",children:[h.jsx(oe,{onClick:e,className:"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors bg-transparent border-0 cursor-pointer","aria-label":"뒤로가기",children:h.jsx(re,{name:de.ArrowLeft,className:"text-gray-700 dark:text-gray-300",ariaLabel:"뒤로가기"})}),h.jsx(Qn,{className:"text-xl font-semibold text-gray-900 dark:text-gray-100",children:t})]}),h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{onClick:n,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors border-0 cursor-pointer","aria-label":"미리보기",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(re,{name:de.Eye,className:"text-gray-700 dark:text-gray-300",ariaLabel:"미리보기 아이콘"}),h.jsx("span",{children:"미리보기"})]})}),h.jsx(oe,{onClick:r,disabled:s,className:`
px-4 py-2 rounded-lg transition-colors border-0 cursor-pointer
${s?"bg-blue-400 dark:bg-blue-600 cursor-not-allowed":"bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"}
text-white
`.trim().replace(/\s+/g," "),"aria-label":s?"저장 중...":"저장",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(re,{name:s?de.Spinner:de.Save,className:`text-white ${s?"animate-spin":""}`,ariaLabel:s?"저장 중 아이콘":"저장 아이콘"}),h.jsx("span",{children:s?"저장 중...":"저장"})]})})]})]})},Ny=({files:t,selectedId:e,onSelect:n,className:r=""})=>{const s=a=>{try{const l=new Date(a);if(isNaN(l.getTime()))return a;const c=l.getFullYear(),d=String(l.getMonth()+1).padStart(2,"0"),u=String(l.getDate()).padStart(2,"0"),f=String(l.getHours()).padStart(2,"0"),g=String(l.getMinutes()).padStart(2,"0");return`${c}-${d}-${u} ${f}:${g}`}catch{return a}},o=a=>{n(a)},i=`
`.trim().replace(/\s+/g," "),"aria-label":s?"저장 중...":"저장",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(re,{name:s?de.Spinner:de.Save,className:`text-white ${s?"animate-spin":""}`,ariaLabel:s?"저장 중 아이콘":"저장 아이콘"}),h.jsx("span",{children:s?"저장 중...":"저장"})]})})]})]})},Ay=({files:t,selectedId:e,onSelect:n,className:r=""})=>{const s=a=>{try{const l=new Date(a);if(isNaN(l.getTime()))return a;const c=l.getFullYear(),d=String(l.getMonth()+1).padStart(2,"0"),u=String(l.getDate()).padStart(2,"0"),f=String(l.getHours()).padStart(2,"0"),g=String(l.getMinutes()).padStart(2,"0");return`${c}-${d}-${u} ${f}:${g}`}catch{return a}},o=a=>{n(a)},i=`
w-full
${r}
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:i,children:t.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"파일이 없습니다."}):h.jsx(Da,{className:"divide-y divide-gray-200 dark:divide-gray-700",role:"listbox","aria-label":"레이아웃 파일 목록",children:t.map(a=>{const l=a.id===e,c=`
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:i,children:t.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"파일이 없습니다."}):h.jsx(Ia,{className:"divide-y divide-gray-200 dark:divide-gray-700",role:"listbox","aria-label":"레이아웃 파일 목록",children:t.map(a=>{const l=a.id===e,c=`
flex items-center justify-between p-4 cursor-pointer
transition-colors duration-150
${l?"bg-blue-100 dark:bg-blue-900 border-l-4 border-blue-500":"hover:bg-gray-100 dark:hover:bg-gray-800"}
@@ -105,8 +105,8 @@ var SirsoftAdminBasic=(function(R,h,v,On){"use strict";var T5=Object.definePrope
`.trim().replace(/\s+/g," "),u=`
text-sm
${l?"text-blue-700 dark:text-blue-300":"text-gray-500 dark:text-gray-400"}
`.trim().replace(/\s+/g," ");return h.jsx(Ia,{className:c,onClick:()=>o(a.id),role:"option","aria-selected":l,tabIndex:0,onKeyDown:f=>{(f.key==="Enter"||f.key===" ")&&(f.preventDefault(),o(a.id))},children:h.jsxs(I,{className:"flex flex-col gap-1",children:[h.jsx(Q,{className:d,children:a.name}),h.jsx(Q,{className:u,children:s(a.updated_at)})]})},a.id)})})})};function cu(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function Oy(t){if(Array.isArray(t))return t}function Ty(t,e,n){return(e=Fy(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function Ay(t,e){var n=t==null?null:typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(t)).next,e!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==e);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function Dy(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function du(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function uu(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?du(Object(n),!0).forEach(function(r){Ty(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):du(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function Iy(t,e){if(t==null)return{};var n,r,s=Ly(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r<o.length;r++)n=o[r],e.indexOf(n)===-1&&{}.propertyIsEnumerable.call(t,n)&&(s[n]=t[n])}return s}function Ly(t,e){if(t==null)return{};var n={};for(var r in t)if({}.hasOwnProperty.call(t,r)){if(e.indexOf(r)!==-1)continue;n[r]=t[r]}return n}function Py(t,e){return Oy(t)||Ay(t,e)||zy(t,e)||Dy()}function $y(t,e){if(typeof t!="object"||!t)return t;var n=t[Symbol.toPrimitive];if(n!==void 0){var r=n.call(t,e);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(t)}function Fy(t){var e=$y(t,"string");return typeof e=="symbol"?e:e+""}function zy(t,e){if(t){if(typeof t=="string")return cu(t,e);var n={}.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?cu(t,e):void 0}}function Vy(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function fu(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function hu(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?fu(Object(n),!0).forEach(function(r){Vy(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):fu(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function Hy(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return function(r){return e.reduceRight(function(s,o){return o(s)},r)}}function Es(t){return function e(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=t.length?t.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return e.apply(n,[].concat(s,a))}}}function jo(t){return{}.toString.call(t).includes("Object")}function Uy(t){return!Object.keys(t).length}function Ms(t){return typeof t=="function"}function By(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function jy(t,e){return jo(e)||er("changeType"),Object.keys(e).some(function(n){return!By(t,n)})&&er("changeField"),e}function Wy(t){Ms(t)||er("selectorType")}function Gy(t){Ms(t)||jo(t)||er("handlerType"),jo(t)&&Object.values(t).some(function(e){return!Ms(e)})&&er("handlersType")}function Yy(t){t||er("initialIsRequired"),jo(t)||er("initialType"),Uy(t)&&er("initialContent")}function Xy(t,e){throw new Error(t[e]||t.default)}var Ky={initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"},er=Es(Xy)(Ky),Wo={changes:jy,selector:Wy,handler:Gy,initial:Yy};function qy(t){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};Wo.initial(t),Wo.handler(e);var n={current:t},r=Es(Jy)(n,e),s=Es(Qy)(n),o=Es(Wo.changes)(t),i=Es(Zy)(n);function a(){var c=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(d){return d};return Wo.selector(c),c(n.current)}function l(c){Hy(r,s,o,i)(c)}return[a,l]}function Zy(t,e){return Ms(e)?e(t.current):e}function Qy(t,e){return t.current=hu(hu({},t.current),e),e}function Jy(t,e,n){return Ms(e)?e(t.current):Object.keys(n).forEach(function(r){var s;return(s=e[r])===null||s===void 0?void 0:s.call(e,t.current[r])}),n}var Ry={create:qy},ev={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function tv(t){return function e(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=t.length?t.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l]; Line truncated
`.trim().replace(/\s+/g," ");return h.jsx(La,{className:c,onClick:()=>o(a.id),role:"option","aria-selected":l,tabIndex:0,onKeyDown:f=>{(f.key==="Enter"||f.key===" ")&&(f.preventDefault(),o(a.id))},children:h.jsxs(I,{className:"flex flex-col gap-1",children:[h.jsx(Q,{className:d,children:a.name}),h.jsx(Q,{className:u,children:s(a.updated_at)})]})},a.id)})})})};function du(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function Dy(t){if(Array.isArray(t))return t}function Iy(t,e,n){return(e=Hy(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function Ly(t,e){var n=t==null?null:typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(t)).next,e!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==e);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function Py(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function uu(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function fu(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?uu(Object(n),!0).forEach(function(r){Iy(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):uu(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function $y(t,e){if(t==null)return{};var n,r,s=Fy(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r<o.length;r++)n=o[r],e.indexOf(n)===-1&&{}.propertyIsEnumerable.call(t,n)&&(s[n]=t[n])}return s}function Fy(t,e){if(t==null)return{};var n={};for(var r in t)if({}.hasOwnProperty.call(t,r)){if(e.indexOf(r)!==-1)continue;n[r]=t[r]}return n}function zy(t,e){return Dy(t)||Ly(t,e)||Uy(t,e)||Py()}function Vy(t,e){if(typeof t!="object"||!t)return t;var n=t[Symbol.toPrimitive];if(n!==void 0){var r=n.call(t,e);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(t)}function Hy(t){var e=Vy(t,"string");return typeof e=="symbol"?e:e+""}function Uy(t,e){if(t){if(typeof t=="string")return du(t,e);var n={}.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?du(t,e):void 0}}function By(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function hu(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function gu(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?hu(Object(n),!0).forEach(function(r){By(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):hu(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function jy(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return function(r){return e.reduceRight(function(s,o){return o(s)},r)}}function Es(t){return function e(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=t.length?t.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return e.apply(n,[].concat(s,a))}}}function jo(t){return{}.toString.call(t).includes("Object")}function Wy(t){return!Object.keys(t).length}function Ms(t){return typeof t=="function"}function Gy(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function Yy(t,e){return jo(e)||er("changeType"),Object.keys(e).some(function(n){return!Gy(t,n)})&&er("changeField"),e}function Xy(t){Ms(t)||er("selectorType")}function Ky(t){Ms(t)||jo(t)||er("handlerType"),jo(t)&&Object.values(t).some(function(e){return!Ms(e)})&&er("handlersType")}function qy(t){t||er("initialIsRequired"),jo(t)||er("initialType"),Wy(t)&&er("initialContent")}function Zy(t,e){throw new Error(t[e]||t.default)}var Qy={initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"},er=Es(Zy)(Qy),Wo={changes:Yy,selector:Xy,handler:Ky,initial:qy};function Jy(t){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};Wo.initial(t),Wo.handler(e);var n={current:t},r=Es(tv)(n,e),s=Es(ev)(n),o=Es(Wo.changes)(t),i=Es(Ry)(n);function a(){var c=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(d){return d};return Wo.selector(c),c(n.current)}function l(c){jy(r,s,o,i)(c)}return[a,l]}function Ry(t,e){return Ms(e)?e(t.current):e}function ev(t,e){return t.current=gu(gu({},t.current),e),e}function tv(t,e,n){return Ms(e)?e(t.current):Object.keys(n).forEach(function(r){var s;return(s=e[r])===null||s===void 0?void 0:s.call(e,t.current[r])}),n}var nv={create:Jy},rv={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function sv(t){return function e(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=t.length?t.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l]; Line truncated
You are using deprecated way of configuration.
Instead of using
@@ -115,10 +115,10 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
monaco.config({ paths: { vs: '...' } })
For more please check the link https://github.com/suren-atoyan/monaco-loader#config
`},pu=tv(ov)(gu),iv={config:rv},av=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return function(s){return n.reduceRight(function(o,i){return i(o)},s)}};function mu(t,e){return Object.keys(e).forEach(function(n){e[n]instanceof Object&&t[n]&&Object.assign(e[n],mu(t[n],e[n]))}),uu(uu({},t),e)}var lv={type:"cancelation",msg:"operation is manually canceled"};function ol(t){var e=!1,n=new Promise(function(r,s){t.then(function(o){return e?s(lv):r(o)}),t.catch(s)});return n.cancel=function(){return e=!0},n}var cv=["monaco"],dv=Ry.create({config:ev,isInitialized:!1,resolve:null,reject:null,monaco:null}),bu=Py(dv,2),Ns=bu[0],Go=bu[1];function uv(t){var e=iv.config(t),n=e.monaco,r=Iy(e,cv);Go(function(s){return{config:mu(s.config,r),monaco:n}})}function fv(){var t=Ns(function(e){var n=e.monaco,r=e.isInitialized,s=e.resolve;return{monaco:n,isInitialized:r,resolve:s}});if(!t.isInitialized){if(Go({isInitialized:!0}),t.monaco)return t.resolve(t.monaco),ol(il);if(window.monaco&&window.monaco.editor)return yu(window.monaco),t.resolve(window.monaco),ol(il);av(hv,pv)(mv)}return ol(il)}function hv(t){return document.body.appendChild(t)}function gv(t){var e=document.createElement("script");return t&&(e.src=t),e}function pv(t){var e=Ns(function(r){var s=r.config,o=r.reject;return{config:s,reject:o}}),n=gv("".concat(e.config.paths.vs,"/loader.js"));return n.onload=function(){return t()},n.onerror=e.reject,n}function mv(){var t=Ns(function(n){var r=n.config,s=n.resolve,o=n.reject;return{config:r,resolve:s,reject:o}}),e=window.require;e.config(t.config),e(["vs/editor/editor.main"],function(n){var r=n.m||n;yu(r),t.resolve(r)},function(n){t.reject(n)})}function yu(t){Ns().monaco||Go({monaco:t})}function bv(){return Ns(function(t){var e=t.monaco;return e})}var il=new Promise(function(t,e){return Go({resolve:t,reject:e})}),vu={config:uv,init:fv,__getMonacoInstance:bv},yv={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},al=yv,vv={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},xv=vv;function wv({children:t}){return v.createElement("div",{style:xv.container},t)}var kv=wv,Cv=kv;function Sv({width:t,height:e,isEditorReady:n,loading:r,_ref:s,className:o,wrapperProps:i}){return v.createElement("section",{style:{...al.wrapper,width:t,height:e},...i},!n&&v.createElement(Cv,null,r),v.createElement("div",{ref:s,style:{...al.fullWidth,...!n&&al.hide},className:o}))}var _v=Sv,xu=v.memo(_v);function Ev(t){v.useEffect(t,[])}var wu=Ev;function Mv(t,e,n=!0){let r=v.useRef(!0);v.useEffect(r.current||!n?()=>{r.current=!1}:t,e)}var sn=Mv;function Os(){}function Vr(t,e,n,r){return Nv(t,r)||Ov(t,e,n,r)}function Nv(t,e){return t.editor.getModel(ku(t,e))}function Ov(t,e,n,r){return t.editor.createModel(e,n,r?ku(t,r):void 0)}function ku(t,e){return t.Uri.parse(e)}function Tv({original:t,modified:e,language:n,originalLanguage:r,modifiedLanguage:s,originalModelPath:o,modifiedModelPath:i,keepCurrentOriginalModel:a=!1,keepCurrentModifiedModel:l=!1,theme:c="light",loading:d="Loading...",options:u={},height:f="100%",width:g="100%",className:m,wrapperProps:p={},beforeMount:y=Os,onMount:b=Os}){let[S,x]=v.useState(!1),[E,k]=v.useState(!0),C=v.useRef(null),N=v.useRef(null),_=v.useRef(null),w=v.useRef(b),M=v.useRef(y),O=v.useRef(!1);wu(()=>{let D=vu.init();return D.then(A=>(N.current=A)&&k(!1)).catch(A=>A?.type!=="cancelation"&&console.error("Monaco initialization: error:",A)),()=>C.current?$():D.cancel()}),sn(()=>{if(C.current&&N.current){let D=C.current.getOriginalEditor(),A=Vr(N.current,t||"",r||n||"text",o||"");A!==D.getModel()&&D.setModel(A)}},[o],S),sn(()=>{if(C.current&&N.current){let D=C.current.getModifiedEditor(),A=Vr(N.current,e||"",s||n||"text",i||"");A!==D.getModel()&&D.setModel(A)}},[i],S),sn(()=>{let D=C.current.getModifiedEditor();D.getOption(N.current.editor.EditorOption.readOnly)?D.setValue(e||""):e!==D.getValue()&&(D.executeEdits("",[{range:D.getModel().getFullModelRange(),text:e||"",forceMoveMarkers:!0}]),D.pushUndoStop())},[e],S),sn(()=>{C.current?.getModel()?.original.setValue(t||"")},[t],S),sn(()=>{let{original:D,modified:A}=C.current.getModel();N.current.editor.setModelLanguage(D,r||n||"text"),N.current.editor.setModelLanguage(A,s||n||"text")},[n,r,s],S),sn(()=>{N.current?.editor.setTheme(c)},[c],S),sn(()=>{C.current?.updateOptions(u)},[u],S);let L=v.useCallback(()=>{if(!N.current)return;M.current(N.current);let D=Vr(N.current,t||"",r||n||"text",o||""),A=Vr(N.current,e||"",s||n||"text",i||"");C.current?.setModel({original:D,modified:A})},[n,e,s,t,r,o,i]),F=v.useCallback(()=>{!O.current&&_.current&&(C.current=N.current.editor.createDiffEditor(_.current,{automaticLayout:!0,...u}),L(),N.current?.editor.setTheme(c),x(!0),O.current=!0)},[u,c,L]);v.useEffect(()=>{S&&w.current(C.current,N.current)},[S]),v.useEffect(()=>{!E&&!S&&F()},[E,S,F]);function $(){let D=C.current?.getModel();a||D?.original?.d Line truncated
`},mu=sv(lv)(pu),cv={config:iv},dv=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return function(s){return n.reduceRight(function(o,i){return i(o)},s)}};function bu(t,e){return Object.keys(e).forEach(function(n){e[n]instanceof Object&&t[n]&&Object.assign(e[n],bu(t[n],e[n]))}),fu(fu({},t),e)}var uv={type:"cancelation",msg:"operation is manually canceled"};function il(t){var e=!1,n=new Promise(function(r,s){t.then(function(o){return e?s(uv):r(o)}),t.catch(s)});return n.cancel=function(){return e=!0},n}var fv=["monaco"],hv=nv.create({config:rv,isInitialized:!1,resolve:null,reject:null,monaco:null}),yu=zy(hv,2),Ns=yu[0],Go=yu[1];function gv(t){var e=cv.config(t),n=e.monaco,r=$y(e,fv);Go(function(s){return{config:bu(s.config,r),monaco:n}})}function pv(){var t=Ns(function(e){var n=e.monaco,r=e.isInitialized,s=e.resolve;return{monaco:n,isInitialized:r,resolve:s}});if(!t.isInitialized){if(Go({isInitialized:!0}),t.monaco)return t.resolve(t.monaco),il(al);if(window.monaco&&window.monaco.editor)return vu(window.monaco),t.resolve(window.monaco),il(al);dv(mv,yv)(vv)}return il(al)}function mv(t){return document.body.appendChild(t)}function bv(t){var e=document.createElement("script");return t&&(e.src=t),e}function yv(t){var e=Ns(function(r){var s=r.config,o=r.reject;return{config:s,reject:o}}),n=bv("".concat(e.config.paths.vs,"/loader.js"));return n.onload=function(){return t()},n.onerror=e.reject,n}function vv(){var t=Ns(function(n){var r=n.config,s=n.resolve,o=n.reject;return{config:r,resolve:s,reject:o}}),e=window.require;e.config(t.config),e(["vs/editor/editor.main"],function(n){var r=n.m||n;vu(r),t.resolve(r)},function(n){t.reject(n)})}function vu(t){Ns().monaco||Go({monaco:t})}function xv(){return Ns(function(t){var e=t.monaco;return e})}var al=new Promise(function(t,e){return Go({resolve:t,reject:e})}),xu={config:gv,init:pv,__getMonacoInstance:xv},wv={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},ll=wv,kv={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},Cv=kv;function Sv({children:t}){return v.createElement("div",{style:Cv.container},t)}var _v=Sv,Ev=_v;function Mv({width:t,height:e,isEditorReady:n,loading:r,_ref:s,className:o,wrapperProps:i}){return v.createElement("section",{style:{...ll.wrapper,width:t,height:e},...i},!n&&v.createElement(Ev,null,r),v.createElement("div",{ref:s,style:{...ll.fullWidth,...!n&&ll.hide},className:o}))}var Nv=Mv,wu=v.memo(Nv);function Ov(t){v.useEffect(t,[])}var ku=Ov;function Tv(t,e,n=!0){let r=v.useRef(!0);v.useEffect(r.current||!n?()=>{r.current=!1}:t,e)}var sn=Tv;function Os(){}function Vr(t,e,n,r){return Av(t,r)||Dv(t,e,n,r)}function Av(t,e){return t.editor.getModel(Cu(t,e))}function Dv(t,e,n,r){return t.editor.createModel(e,n,r?Cu(t,r):void 0)}function Cu(t,e){return t.Uri.parse(e)}function Iv({original:t,modified:e,language:n,originalLanguage:r,modifiedLanguage:s,originalModelPath:o,modifiedModelPath:i,keepCurrentOriginalModel:a=!1,keepCurrentModifiedModel:l=!1,theme:c="light",loading:d="Loading...",options:u={},height:f="100%",width:g="100%",className:m,wrapperProps:p={},beforeMount:y=Os,onMount:b=Os}){let[S,x]=v.useState(!1),[E,k]=v.useState(!0),C=v.useRef(null),N=v.useRef(null),_=v.useRef(null),w=v.useRef(b),M=v.useRef(y),O=v.useRef(!1);ku(()=>{let D=xu.init();return D.then(A=>(N.current=A)&&k(!1)).catch(A=>A?.type!=="cancelation"&&console.error("Monaco initialization: error:",A)),()=>C.current?$():D.cancel()}),sn(()=>{if(C.current&&N.current){let D=C.current.getOriginalEditor(),A=Vr(N.current,t||"",r||n||"text",o||"");A!==D.getModel()&&D.setModel(A)}},[o],S),sn(()=>{if(C.current&&N.current){let D=C.current.getModifiedEditor(),A=Vr(N.current,e||"",s||n||"text",i||"");A!==D.getModel()&&D.setModel(A)}},[i],S),sn(()=>{let D=C.current.getModifiedEditor();D.getOption(N.current.editor.EditorOption.readOnly)?D.setValue(e||""):e!==D.getValue()&&(D.executeEdits("",[{range:D.getModel().getFullModelRange(),text:e||"",forceMoveMarkers:!0}]),D.pushUndoStop())},[e],S),sn(()=>{C.current?.getModel()?.original.setValue(t||"")},[t],S),sn(()=>{let{original:D,modified:A}=C.current.getModel();N.current.editor.setModelLanguage(D,r||n||"text"),N.current.editor.setModelLanguage(A,s||n||"text")},[n,r,s],S),sn(()=>{N.current?.editor.setTheme(c)},[c],S),sn(()=>{C.current?.updateOptions(u)},[u],S);let L=v.useCallback(()=>{if(!N.current)return;M.current(N.current);let D=Vr(N.current,t||"",r||n||"text",o||""),A=Vr(N.current,e||"",s||n||"text",i||"");C.current?.setModel({original:D,modified:A})},[n,e,s,t,r,o,i]),F=v.useCallback(()=>{!O.current&&_.current&&(C.current=N.current.editor.createDiffEditor(_.current,{automaticLayout:!0,...u}),L(),N.current?.editor.setTheme(c),x(!0),O.current=!0)},[u,c,L]);v.useEffect(()=>{S&&w.current(C.current,N.current)},[S]),v.useEffect(()=>{!E&&!S&&F()},[E,S,F]);function $(){let D=C.current?.getModel();a||D?.original?.d Line truncated
p-3 rounded-lg border cursor-pointer transition-colors
${e?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500"}
`,onClick:r,children:h.jsxs(I,{className:"flex justify-between items-start",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Q,{className:"font-semibold text-gray-900 dark:text-gray-100",children:["버전 ",t.version]}),h.jsx(Q,{className:"ml-2 text-sm text-gray-500 dark:text-gray-400",children:Vv(t.created_at)})]}),h.jsxs(I,{className:"flex gap-3 text-sm",children:[h.jsxs(Q,{className:"text-green-600 dark:text-green-400",children:["+",t.changes_summary.added]}),h.jsxs(Q,{className:"text-red-600 dark:text-red-400",children:["-",t.changes_summary.removed]})]})]})})});Cu.displayName="VersionListItem";const Su=({versions:t,selectedId:e,onSelect:n})=>t.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"버전이 없습니다."}):h.jsx(Da,{role:"list",className:"space-y-2",children:t.map(r=>h.jsx(Cu,{version:r,isSelected:e===r.id,onSelect:n},r.id))}),Hv=({layoutId:t,versions:e,onRestore:n})=>{const[r,s]=v.useState(),o=l=>{s(l)},i=()=>{r!==void 0&&n(r)},a=r===void 0;return h.jsxs(I,{className:"space-y-4",role:"region","aria-label":"레이아웃 히스토리 패널",children:[h.jsx(I,{className:"border border-gray-300 dark:border-gray-600 rounded-lg p-4",children:h.jsx(Su,{versions:e,selectedId:r,onSelect:o})}),h.jsx(I,{className:"flex justify-end",children:h.jsx(oe,{variant:"primary",disabled:a,onClick:i,"aria-label":"선택한 버전으로 복원",children:"복원"})})]})},Uv=({emailLabel:t="Email",passwordLabel:e="Password",submitButtonText:n="Sign In",processingText:r="Processing...",emailPlaceholder:s="Email",passwordPlaceholder:o="Password",forgotPasswordText:i="Forgot your password?",forgotPasswordUrl:a="/admin/password/reset",emailRequiredError:l="Please enter your email.",emailInvalidError:c="Please enter a valid email address.",passwordRequiredError:d="Please enter your password.",onSubmit:u,loadingActions:f={},isLoading:g=!1,disabled:m=!1,apiError:p="",className:y="",style:b})=>{const[S,x]=v.useState(""),[E,k]=v.useState(""),[C,N]=v.useState({}),_=p,w=Object.values(f).some($=>$===!0),M=g||w,O=m||M,L=()=>{const $={},D=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return!S||S.trim()===""?$.email=l:D.test(S)||($.email=c),(!E||E.trim()==="")&&($.password=d),N($),Object.keys($).length===0},F=async $=>{if(!L()){$.preventDefault();return}N({}),u?await u($):$.preventDefault()};return h.jsx(I,{className:`w-full ${y}`,style:b,children:h.jsxs(La,{onSubmit:F,className:"space-y-6",noValidate:!0,children:[_&&h.jsx(I,{className:"p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800",children:h.jsx(bt,{className:"text-sm text-red-600 dark:text-red-400",children:_})}),h.jsxs(I,{children:[h.jsx(rn,{htmlFor:"email",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:t}),h.jsx(wt,{id:"email",name:"email",type:"email",value:S,onChange:$=>x($.target.value),placeholder:s,disabled:O,className:`
`,onClick:r,children:h.jsxs(I,{className:"flex justify-between items-start",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Q,{className:"font-semibold text-gray-900 dark:text-gray-100",children:["버전 ",t.version]}),h.jsx(Q,{className:"ml-2 text-sm text-gray-500 dark:text-gray-400",children:Bv(t.created_at)})]}),h.jsxs(I,{className:"flex gap-3 text-sm",children:[h.jsxs(Q,{className:"text-green-600 dark:text-green-400",children:["+",t.changes_summary.added]}),h.jsxs(Q,{className:"text-red-600 dark:text-red-400",children:["-",t.changes_summary.removed]})]})]})})});Su.displayName="VersionListItem";const _u=({versions:t,selectedId:e,onSelect:n})=>t.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"버전이 없습니다."}):h.jsx(Ia,{role:"list",className:"space-y-2",children:t.map(r=>h.jsx(Su,{version:r,isSelected:e===r.id,onSelect:n},r.id))}),jv=({layoutId:t,versions:e,onRestore:n})=>{const[r,s]=v.useState(),o=l=>{s(l)},i=()=>{r!==void 0&&n(r)},a=r===void 0;return h.jsxs(I,{className:"space-y-4",role:"region","aria-label":"레이아웃 히스토리 패널",children:[h.jsx(I,{className:"border border-gray-300 dark:border-gray-600 rounded-lg p-4",children:h.jsx(_u,{versions:e,selectedId:r,onSelect:o})}),h.jsx(I,{className:"flex justify-end",children:h.jsx(oe,{variant:"primary",disabled:a,onClick:i,"aria-label":"선택한 버전으로 복원",children:"복원"})})]})},Wv=({emailLabel:t="Email",passwordLabel:e="Password",submitButtonText:n="Sign In",processingText:r="Processing...",emailPlaceholder:s="Email",passwordPlaceholder:o="Password",forgotPasswordText:i="Forgot your password?",forgotPasswordUrl:a="/admin/password/reset",emailRequiredError:l="Please enter your email.",emailInvalidError:c="Please enter a valid email address.",passwordRequiredError:d="Please enter your password.",onSubmit:u,loadingActions:f={},isLoading:g=!1,disabled:m=!1,apiError:p="",className:y="",style:b})=>{const[S,x]=v.useState(""),[E,k]=v.useState(""),[C,N]=v.useState({}),_=p,w=Object.values(f).some($=>$===!0),M=g||w,O=m||M,L=()=>{const $={},D=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return!S||S.trim()===""?$.email=l:D.test(S)||($.email=c),(!E||E.trim()==="")&&($.password=d),N($),Object.keys($).length===0},F=async $=>{if(!L()){$.preventDefault();return}N({}),u?await u($):$.preventDefault()};return h.jsx(I,{className:`w-full ${y}`,style:b,children:h.jsxs(Pa,{onSubmit:F,className:"space-y-6",noValidate:!0,children:[_&&h.jsx(I,{className:"p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800",children:h.jsx(bt,{className:"text-sm text-red-600 dark:text-red-400",children:_})}),h.jsxs(I,{children:[h.jsx(rn,{htmlFor:"email",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:t}),h.jsx(wt,{id:"email",name:"email",type:"email",value:S,onChange:$=>x($.target.value),placeholder:s,disabled:O,className:`
w-full px-4 py-3 rounded-lg border
${C.email?"border-red-500 focus:border-red-500 focus:ring-red-500":"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"}
bg-white dark:bg-gray-800
@@ -146,7 +146,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200
flex items-center justify-center
`,children:M?h.jsxs(h.Fragment,{children:[h.jsxs(Hn,{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[h.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),h.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),r]}):n})]})})},_u=t=>t==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":t,ll=t=>{const e=_u(t);document.documentElement.setAttribute("data-theme",e),e==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},Bv=()=>{const t=localStorage.getItem("g7_color_scheme");return t&&["auto","light","dark"].includes(t)?t:"auto"},jv=({onThemeChange:t,className:e="",autoText:n="System",lightText:r="Light",darkText:s="Dark",id:o,editorAttrs:i})=>{const a=Bv(),[l,c]=v.useState(a),[d,u]=v.useState(!1),f=v.useRef(null);v.useEffect(()=>{ll(l)},[]),v.useEffect(()=>{if(l!=="auto")return;const p=window.matchMedia("(prefers-color-scheme: dark)"),y=()=>{ll("auto")};return p.addEventListener("change",y),()=>p.removeEventListener("change",y)},[l]),v.useEffect(()=>{const p=y=>{f.current&&!f.current.contains(y.target)&&u(!1)};return document.addEventListener("mousedown",p),()=>document.removeEventListener("mousedown",p)},[]);const g=p=>{c(p),localStorage.setItem("g7_color_scheme",p),ll(p),u(!1),t?.(p)},m=()=>_u(l)==="dark"?de.Moon:de.Sun;return h.jsxs(I,{ref:f,className:`relative ${e}`,id:o,...i,children:[h.jsx(oe,{onClick:()=>u(!d),className:"w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors","aria-label":"Toggle theme",children:h.jsx(re,{name:m(),className:"text-sm text-slate-500 dark:text-slate-400"})}),d&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsxs(I,{className:"py-2",children:[h.jsxs(oe,{onClick:()=>g("auto"),className:`
`,children:M?h.jsxs(h.Fragment,{children:[h.jsxs(Hn,{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[h.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),h.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),r]}):n})]})})},Eu=t=>t==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":t,cl=t=>{const e=Eu(t);document.documentElement.setAttribute("data-theme",e),e==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},Gv=()=>{const t=localStorage.getItem("g7_color_scheme");return t&&["auto","light","dark"].includes(t)?t:"auto"},Yv=({onThemeChange:t,className:e="",autoText:n="System",lightText:r="Light",darkText:s="Dark",id:o,editorAttrs:i})=>{const a=Gv(),[l,c]=v.useState(a),[d,u]=v.useState(!1),f=v.useRef(null);v.useEffect(()=>{cl(l)},[]),v.useEffect(()=>{if(l!=="auto")return;const p=window.matchMedia("(prefers-color-scheme: dark)"),y=()=>{cl("auto")};return p.addEventListener("change",y),()=>p.removeEventListener("change",y)},[l]),v.useEffect(()=>{const p=y=>{f.current&&!f.current.contains(y.target)&&u(!1)};return document.addEventListener("mousedown",p),()=>document.removeEventListener("mousedown",p)},[]);const g=p=>{c(p),localStorage.setItem("g7_color_scheme",p),cl(p),u(!1),t?.(p)},m=()=>Eu(l)==="dark"?de.Moon:de.Sun;return h.jsxs(I,{ref:f,className:`relative ${e}`,id:o,...i,children:[h.jsx(oe,{onClick:()=>u(!d),className:"w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors","aria-label":"Toggle theme",children:h.jsx(re,{name:m(),className:"text-sm text-slate-500 dark:text-slate-400"})}),d&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsxs(I,{className:"py-2",children:[h.jsxs(oe,{onClick:()=>g("auto"),className:`
w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors
${l==="auto"?"bg-gray-50 dark:bg-gray-700":""}
`,children:[h.jsx(re,{name:de.Settings,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:n}),l==="auto"&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(oe,{onClick:()=>g("light"),className:`
@@ -155,7 +155,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
`,children:[h.jsx(re,{name:de.Sun,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:r}),l==="light"&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(oe,{onClick:()=>g("dark"),className:`
w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors
${l==="dark"?"bg-gray-50 dark:bg-gray-700":""}
`,children:[h.jsx(re,{name:de.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:s}),l==="dark"&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Eu=window.G7Core?.createLogger?.("Comp:ColumnSelector")??{log:(...t)=>console.log("[Comp:ColumnSelector]",...t),warn:(...t)=>console.warn("[Comp:ColumnSelector]",...t),error:(...t)=>console.error("[Comp:ColumnSelector]",...t)},Mu=(t,e)=>window.G7Core?.t?.(t,e)??t,Wv=()=>window.G7Core,Gv=({id:t,columns:e,visibleColumns:n,onColumnVisibilityChange:r,triggerLabel:s,triggerIconName:o=de.Sliders,position:i="right",className:a="",style:l,requiredText:c,menuTitle:d,menuDescription:u,defaultColumns:f,triggerClassName:g,labelClassName:m,dynamicColumns:p,dynamicColumnsInsertAfter:y,editorAttrs:b})=>{const S=s??Mu("common.column_selector"),x=c??Mu("common.required"),E=v.useMemo(()=>{if(!p||!Array.isArray(p)||p.length===0)return e;if(y){const V=e.findIndex(K=>K.field===y);if(V!==-1)return[...e.slice(0,V+1),...p,...e.slice(V+1)]}return[...e,...p]},[e,p,y]),[k,C]=v.useState(!1),N=v.useRef(null),_=v.useRef(null),[w,M]=v.useState(!1),[O,L]=v.useState(i),F=v.useMemo(()=>{try{const V=window.G7Core?.AuthManager?.getInstance();return V?V.getUser()?.uuid:void 0}catch{return}},[]),$=t?`g7_columns_${t}${F!==void 0?`_${F}`:""}`:null,D=()=>{if(!$)return null;try{const V=localStorage.getItem($);if(V){const K=JSON.parse(V);if(Array.isArray(K)&&K.length>0)return K}}catch(V){Eu.warn("Failed to load from localStorage:",V)}return null},A=V=>{if($)try{localStorage.setItem($,JSON.stringify(V))}catch(K){Eu.warn("Failed to save to localStorage:",K)}},T=()=>{const V=D();return V||(n&&n.length>0?n:f&&f.length>0?f:E.filter(K=>K.visible!==!1).map(K=>K.field))},[z,H]=v.useState(T);v.useEffect(()=>{w||(r&&z.length>0&&r(z),M(!0))},[w,r,z]),v.useEffect(()=>{w&&n&&n.length>0&&(D()||H(n))},[n,w]);const W=v.useRef([]),j=v.useRef(null);v.useEffect(()=>{if(w&&p&&p.length>0){const V=p.map(Z=>Z.field);if(j.current===null){const Z=D();if(j.current=Z!==null,Z!==null){W.current=V;return}}const K=W.current,X=V.filter(Z=>!K.includes(Z));if(X.length>0){const Z=n&&n.length>0?n:z,ee=X.filter(ne=>!Z.includes(ne));if(ee.length>0){const ne=[...Z,...ee];H(ne),A(ne),r?.(ne)}}W.current=V}},[p,w]);const U=n&&n.length>0?n:z;v.useEffect(()=>{const V=K=>{N.current&&!N.current.contains(K.target)&&C(!1)};return k&&document.addEventListener("mousedown",V),()=>{document.removeEventListener("mousedown",V)}},[k]),v.useEffect(()=>{if(k&&_.current&&N.current){const V=_.current.getBoundingClientRect(),K=N.current.getBoundingClientRect(),X=window.innerWidth,Z=V.width||256;i==="right"?K.right-Z<0?L("left"):L("right"):K.left+Z>X?L("right"):L("left")}},[k,i]);const B=V=>{if(E.find(Z=>Z.field===V)?.required)return;const X=U.includes(V)?U.filter(Z=>Z!==V):[...U,V];H(X),A(X),r?.(X)},G=O==="left"?"left-0":"right-0";return h.jsxs(I,{ref:N,className:`relative inline-block ${a}`,style:l,id:t,...b,children:[h.jsxs(oe,{onClick:()=>C(!k),className:Wv()?.style?.mergeClasses?.("flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",g)??`flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors ${g??""}`,children:[h.jsx(re,{name:o,className:"w-5 h-5"}),S&&h.jsx(Q,{className:`${m||"hidden sm:inline"} text-sm`,children:S})]}),k&&h.jsxs(I,{ref:_,className:`absolute ${G} z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg`,children:[(d||u)&&h.jsxs(I,{className:"px-4 pt-3 pb-2 border-b border-gray-200 dark:border-gray-700",children:[d&&h.jsx(Q,{className:"block text-sm font-medium text-gray-900 dark:text-white",children:d}),u&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-1",children:u})]}),h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:E.map(V=>{const K=U.includes(V.field),X=V.required===!0;return h.jsxs(I,{onClick:()=>!X&&B(V.field),className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${X?"cursor-default":"cursor-pointer"}`,children:[h.jsx(vn,{checked:K,disabled:X,readOnly:!0,className:"h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 pointer-events-none"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:V.header}),X&&x&&h.jsx(Q,{className:"ml-auto text-xs text-gray-400 dark:text-gray-500",children:x})]},V.field)})})]})]})},Yv=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...t)=>console.log("[Comp:PageTransition]",...t),warn:(...t)=>console.warn("[Comp:PageTransition]",...t),error:(...t)=>console.error("[Comp:PageTransition]",...t)},Xv=({className:t="",style:e})=>{const[n,r]=v.useState(!1);return v.useEffect(()=>{c Line truncated
`,children:[h.jsx(re,{name:de.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:s}),l==="dark"&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Mu=window.G7Core?.createLogger?.("Comp:ColumnSelector")??{log:(...t)=>console.log("[Comp:ColumnSelector]",...t),warn:(...t)=>console.warn("[Comp:ColumnSelector]",...t),error:(...t)=>console.error("[Comp:ColumnSelector]",...t)},Nu=(t,e)=>window.G7Core?.t?.(t,e)??t,Xv=()=>window.G7Core,Kv=({id:t,columns:e,visibleColumns:n,onColumnVisibilityChange:r,triggerLabel:s,triggerIconName:o=de.Sliders,position:i="right",className:a="",style:l,requiredText:c,menuTitle:d,menuDescription:u,defaultColumns:f,triggerClassName:g,labelClassName:m,dynamicColumns:p,dynamicColumnsInsertAfter:y,editorAttrs:b})=>{const S=s??Nu("common.column_selector"),x=c??Nu("common.required"),E=v.useMemo(()=>{if(!p||!Array.isArray(p)||p.length===0)return e;if(y){const V=e.findIndex(K=>K.field===y);if(V!==-1)return[...e.slice(0,V+1),...p,...e.slice(V+1)]}return[...e,...p]},[e,p,y]),[k,C]=v.useState(!1),N=v.useRef(null),_=v.useRef(null),[w,M]=v.useState(!1),[O,L]=v.useState(i),F=v.useMemo(()=>{try{const V=window.G7Core?.AuthManager?.getInstance();return V?V.getUser()?.uuid:void 0}catch{return}},[]),$=t?`g7_columns_${t}${F!==void 0?`_${F}`:""}`:null,D=()=>{if(!$)return null;try{const V=localStorage.getItem($);if(V){const K=JSON.parse(V);if(Array.isArray(K)&&K.length>0)return K}}catch(V){Mu.warn("Failed to load from localStorage:",V)}return null},A=V=>{if($)try{localStorage.setItem($,JSON.stringify(V))}catch(K){Mu.warn("Failed to save to localStorage:",K)}},T=()=>{const V=D();return V||(n&&n.length>0?n:f&&f.length>0?f:E.filter(K=>K.visible!==!1).map(K=>K.field))},[z,H]=v.useState(T);v.useEffect(()=>{w||(r&&z.length>0&&r(z),M(!0))},[w,r,z]),v.useEffect(()=>{w&&n&&n.length>0&&(D()||H(n))},[n,w]);const W=v.useRef([]),j=v.useRef(null);v.useEffect(()=>{if(w&&p&&p.length>0){const V=p.map(Z=>Z.field);if(j.current===null){const Z=D();if(j.current=Z!==null,Z!==null){W.current=V;return}}const K=W.current,X=V.filter(Z=>!K.includes(Z));if(X.length>0){const Z=n&&n.length>0?n:z,ee=X.filter(ne=>!Z.includes(ne));if(ee.length>0){const ne=[...Z,...ee];H(ne),A(ne),r?.(ne)}}W.current=V}},[p,w]);const U=n&&n.length>0?n:z;v.useEffect(()=>{const V=K=>{N.current&&!N.current.contains(K.target)&&C(!1)};return k&&document.addEventListener("mousedown",V),()=>{document.removeEventListener("mousedown",V)}},[k]),v.useEffect(()=>{if(k&&_.current&&N.current){const V=_.current.getBoundingClientRect(),K=N.current.getBoundingClientRect(),X=window.innerWidth,Z=V.width||256;i==="right"?K.right-Z<0?L("left"):L("right"):K.left+Z>X?L("right"):L("left")}},[k,i]);const B=V=>{if(E.find(Z=>Z.field===V)?.required)return;const X=U.includes(V)?U.filter(Z=>Z!==V):[...U,V];H(X),A(X),r?.(X)},G=O==="left"?"left-0":"right-0";return h.jsxs(I,{ref:N,className:`relative inline-block ${a}`,style:l,id:t,...b,children:[h.jsxs(oe,{onClick:()=>C(!k),className:Xv()?.style?.mergeClasses?.("flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",g)??`flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors ${g??""}`,children:[h.jsx(re,{name:o,className:"w-5 h-5"}),S&&h.jsx(Q,{className:`${m||"hidden sm:inline"} text-sm`,children:S})]}),k&&h.jsxs(I,{ref:_,className:`absolute ${G} z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg`,children:[(d||u)&&h.jsxs(I,{className:"px-4 pt-3 pb-2 border-b border-gray-200 dark:border-gray-700",children:[d&&h.jsx(Q,{className:"block text-sm font-medium text-gray-900 dark:text-white",children:d}),u&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-1",children:u})]}),h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:E.map(V=>{const K=U.includes(V.field),X=V.required===!0;return h.jsxs(I,{onClick:()=>!X&&B(V.field),className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${X?"cursor-default":"cursor-pointer"}`,children:[h.jsx(vn,{checked:K,disabled:X,readOnly:!0,className:"h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 pointer-events-none"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:V.header}),X&&x&&h.jsx(Q,{className:"ml-auto text-xs text-gray-400 dark:text-gray-500",children:x})]},V.field)})})]})]})},qv=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...t)=>console.log("[Comp:PageTransition]",...t),warn:(...t)=>console.warn("[Comp:PageTransition]",...t),error:(...t)=>console.error("[Comp:PageTransition]",...t)},Zv=({className:t="",style:e})=>{const[n,r]=v.useState(!1);return v.useEffect(()=>{c Line truncated
@keyframes loading {
0% {
transform: translateX(-100%);
@@ -167,12 +167,12 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
transform: translateX(-100%);
}
}
`})]}):null},Kv=(t,e)=>window.G7Core?.t?.(t,e)??t,qv=({options:t})=>{const e=document.documentElement.classList.contains("dark"),n=e?"rgb(17,24,39)":"rgb(249,250,251)",r=e?"#6b7280":"#9ca3af",s=e?"#9ca3af":"#6b7280";return h.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:n,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[h.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${r}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),h.jsx("span",{style:{color:s,fontSize:"14px"},children:t?.text||Kv("nav.loading")})]})};function hr(t){"@babel/helpers - typeof";return hr=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},hr(t)}function Zv(t,e){if(hr(t)!="object"||!t)return t;var n=t[Symbol.toPrimitive];if(n!==void 0){var r=n.call(t,e);if(hr(r)!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(t)}function Nu(t){var e=Zv(t,"string");return hr(e)=="symbol"?e:e+""}function Ts(t,e,n){return(e=Nu(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function Ou(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function Me(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?Ou(Object(n),!0).forEach(function(r){Ts(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):Ou(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function Qv(t){if(Array.isArray(t))return t}function Jv(t,e){var n=t==null?null:typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(t)).next,e===0){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==e);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function cl(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function Tu(t,e){if(t){if(typeof t=="string")return cl(t,e);var n={}.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?cl(t,e):void 0}}function Rv(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Bn(t,e){return Qv(t)||Jv(t,e)||Tu(t,e)||Rv()}function ex(t,e){if(t==null)return{};var n={};for(var r in t)if({}.hasOwnProperty.call(t,r)){if(e.indexOf(r)!==-1)continue;n[r]=t[r]}return n}function Tn(t,e){if(t==null)return{};var n,r,s=ex(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r<o.length;r++)n=o[r],e.indexOf(n)===-1&&{}.propertyIsEnumerable.call(t,n)&&(s[n]=t[n])}return s}var tx=["defaultInputValue","defaultMenuIsOpen","defaultValue","inputValue","menuIsOpen","onChange","onInputChange","onMenuClose","onMenuOpen","value"];function Au(t){var e=t.defaultInputValue,n=e===void 0?"":e,r=t.defaultMenuIsOpen,s=r===void 0?!1:r,o=t.defaultValue,i=o===void 0?null:o,a=t.inputValue,l=t.menuIsOpen,c=t.onChange,d=t.onInputChange,u=t.onMenuClose,f=t.onMenuOpen,g=t.value,m=Tn(t,tx),p=v.useState(a!==void 0?a:n),y=Bn(p,2),b=y[0],S=y[1],x=v.useState(l!==void 0?l:s),E=Bn(x,2),k=E[0],C=E[1],N=v.useState(g!==void 0?g:i),_=Bn(N,2),w=_[0],M=_[1],O=v.useCallback(function(z,H){typeof c=="function"&&c(z,H),M(z)},[c]),L=v.useCallback(function(z,H){var W;typeof d=="function"&&(W=d(z,H)),S(W!==void 0?W:z)},[d]),F=v.useCallback(function(){typeof f=="function"&&f(),C(!0)},[f]),$=v.useCallback(function(){typeof u=="function"&&u(),C(!1)},[u]),D=a!==void 0?a:b,A=l!==void 0?l:k,T=g!==void 0?g:w;return Me(Me({},m),{},{inputValue:D,menuIsOpen:A,onChange:O,onInputChange:L,onMenuClose:$,onMenuOpen:F,value:T})}function Ae(){return Ae=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var r in n)({}).hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},Ae.apply(null,arguments)}function nx(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function Du(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,Nu(r.key),r)}}function rx(t,e,n){return e&&Du(t.prototype,e),n&&Du(t,n),Object.defineProperty(t,"prototype",{writable:!1}),t}function dl(t,e){return dl=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,r){return n.__proto__=r,n},dl(t,e)}function sx(t,e){if(typeof e!="function"&&e!==null)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&dl(t,e)}function Xo(t){return Xo=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},Xo(t)}function Iu(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(Iu=function(){return!!t})()}function ox(t){if(t===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function ix(t,e){if(e&&(hr(e)=="object"||typeof e=="function"))return e;if(e!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return ox(t)}function ax(t){var e=Iu();return function(){var n,r=Xo(t);if(e){var s=Xo(this).constructor;n=Reflect.construct(r,arguments,s)}else n=r.apply(this,arguments);return ix(this,n)}}function lx(t){if(Array.isArray(t))return cl(t)}function cx(t){if(typeof Symbol<"u"&&t[Symbol.iterator]!=null||t["@@iterator"]!=null)return Array.from(t)}function dx(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Hr(t){return lx(t)||cx(t)||Tu(t)||dx()}function ux(t){if(t.sheet)return t.sheet;for(var e=0;e<document.styleSheets.length;e++)if(document.styleSheets[e].ownerNode===t)return document.styleSheets[e]}function fx(t){var e=document.createElement("style");return e.setAttribute("data-emotion",t.key),t.nonce!==void 0&&e.setAttribute("nonce",t.nonce),e.appendChild(document.createTextNode("")),e.setAttribute("data-s",""),e}var hx=(function(){function t(n){var r=this;this._insertTag=function(s){var o;r.tags.length===0?r.insertionPoint?o=r.insertionPoint.nextSibling:r.prepend?o=r.container.firstChild:o=r.before:o=r.tags[r.tags.length-1].nextSibling,r.container.insertBefore(s,o),r.tags.push(s)},this.isSpeedy=n.speedy===void 0?!0:n.speedy,this.tags=[],this.ctr=0,this.nonce=n.nonce,this.key=n.key,this.container=n.container,this.prepend=n.prepend,this.insertionPoint=n.insertionPoint,this.before=null}var e=t.prototype;return e.hydrate=function(r){r.forEach(this._insertTag)},e.insert=function(r){this.ctr%(this.isSpeedy?65e3:1)===0&&this._insertTag(fx(this));var s=this.tags[this.tags.length-1];if(this.isSpeedy){var o=ux(s);try{o.insertRule(r,o.cssRules.length)}catch{}}else s.appendChild(document.createTextNode(r));this.ctr++},e.flush=function(){this.tags.forEach(function(r){var s;return(s=r.parentNode)==null?void 0:s.removeChild(r)}),this.tags=[],this.ctr=0},t})(),$t="-ms-",Ko="-moz-",tt="-webkit-",Lu="comm",ul="rule",fl="decl",gx="@import",Pu="@keyframes",px="@layer",mx=Math.abs,qo=String.fromCharCode,bx=Object.assign;function yx(t,e){return It(t,0)^45?(((e<<2^It(t,0))<<2^It(t,1))<<2^It(t,2))<<2^It(t,3):0}function $u(t){return t.trim()}function vx(t,e){return(t=e.exec(t))?t[0]:t}function nt(t,e,n){return t.replace(e,n)}function hl(t,e){return t.indexOf(e)}function It(t,e){return t.charCodeAt(e)|0}function As(t,e,n){return t.slice(e,n)}function An(t){return t.length}function gl(t){return t.length}function Zo(t,e){return e.push(t),t}function xx(t,e){return t.map(e).join("")}var Qo=1,Ur=1,Fu=0,qt=0,kt=0,Br="";function Jo(t,e,n,r,s,o,i){return{value:t,root:e,parent:n,type:r,props:s,children:o,line:Qo,column:Ur,length:i,return:""}}function Ds(t,e){return bx(Jo("",null,null,"",null,null,0),t,{length:-t.length},e)}function wx(){return kt}function kx(){return kt=qt>0?It(Br,--qt):0,Ur--,kt===10&&(Ur=1,Qo--),kt}function on(){return kt=qt<Fu?It(Br,qt++):0,Ur++,kt===10&&(Ur=1,Qo++),kt}function Dn(){return It(Br,qt)}function Ro(){return qt}function Is(t,e){return As(Br,t,e)}function Ls(t){switch(t){case 0:case 9:case 10:case 13:case 32:return 5;case 33:case 43:case 44:case 47:case 62:case 64:case 126:case 59:case 123:case 125:return 4;case 58:return 3;case 34:case 39:case 40:case 91:return 2;case 41:case 93:return 1}return 0}function zu(t){return Qo=Ur=1,Fu=An(Br=t),qt=0,[]}function Vu(t){return Br="",t}function ei(t){return $u(Is(qt-1,pl(t===91?t+2:t===40?t+1:t)))}function Cx(t){for(;(kt=Dn())&&kt<33;)on();return Ls(t)>2||Ls(kt)>3?"":" "}function Sx(t,e){for(;--e&&on()&&!(kt<48||kt>102||kt>57&&kt<65||kt>70&&kt<97););return Is(t,Ro()+(e<6&&Dn()==32&&on()==32))}function pl(t){for(;on();)switch(kt){case t:return qt;case 34:case 39:t!==34&&t!==39&&pl(kt);break;case 40:t===41&&pl(t);break;case 92:on();break}return qt}function _x(t,e){for(;on()&&t+kt!==57;)if(t+kt===84&&Dn()===47)break;return"/*"+Is(e,qt-1)+"*"+qo(t===47?t:on())}function Ex(t){for(;!Ls(Dn());)on();return Is(t,qt)}function Mx(t){return Vu(ti("",null,null,null,[""],t=zu(t),0,[0],t))}function ti(t,e,n,r,s,o,i,a,l){for(var c=0,d=0,u=i,f=0,g=0,m=0,p=1,y=1,b=1,S=0,x="",E=s,k=o,C=r,N=x;y;)switch(m=S,S=on()){case 40:if(m!=108&&It(N,u-1)==58){hl(N+=nt(ei(S),"&","&\f"),"&\f")!=-1&&(b=-1);break}case 34:case 39:case 91:N+=ei(S);break;case 9:case 10:case 13:case 32:N+=Cx(m);break;case 92:N+=Sx(Ro()-1,7);continue;case 47:switch(Dn()){case 42:case 47:Zo(Nx(_x(on(),Ro()),e,n),l);break;default:N+="/"}break;case 123*p:a[c++]=An(N)*b;case 125*p:case 59:case 0:switch(S){case 0:case 125:y=0;case 59+d:b==-1&&(N=nt(N,/\f/g,"")),g>0&&An(N)-u&&Zo(g>32?Uu(N+";",r,n,u-1):Uu(nt(N," ","")+";",r,n,u-2),l);break;case 59:N+=";";default:if(Zo(C=Hu(N,e,n,c,d,s,a,x,E=[],k=[],u),o),S===123)if(d===0)ti(N,e,C,C,E,o,u,a,k);else switch(f===99&&It(N,3)===110?100:f){case 100:case 108:case 109:case 115:ti(t,C,C,r&&Zo(Hu(t,C,C,0,0,s,a,x,s,E=[],u),k),s,k,u,a,r?E:k);break;default:ti(N,C,C,C,[""],k,0,a,k)}}c=d=g=0,p=b=1,x=N="",u=i;break;case 58:u=1+An(N),g=m;default:if(p<1){if(S==123)--p;else if(S==125&&p++==0&&kx()==125)continue}switch(N+=qo(S),S*p){case 38:b=d>0?1:(N+="\f",-1);break;case 44:a[c++]=(An(N)-1)*b,b=1;break;case 64:Dn()===45&&(N+=ei(on())),f=Dn(),d=u=An(x=N+=Ex(Ro())),S++;break;case 45:m===45&&An(N)==2&&(p=0)}}return o}function Hu(t,e,n,r,s,o,i,a,l,c,d){for(var u=s-1,f=s===0?o:[""],g=gl(f),m=0,p=0,y=0;m<r;++m)for(var b=0,S=As(t,u+1,u=mx(p=i[m])),x=t;b<g;++b)(x=$u(p>0?f[b]+" "+S:nt(S,/&\f/g,f[b])))&&(l[y++]=x);retu Line truncated
`})]}):null},Qv=(t,e)=>window.G7Core?.t?.(t,e)??t,Jv=({options:t})=>{const e=document.documentElement.classList.contains("dark"),n=e?"rgb(17,24,39)":"rgb(249,250,251)",r=e?"#6b7280":"#9ca3af",s=e?"#9ca3af":"#6b7280";return h.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:n,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[h.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${r}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),h.jsx("span",{style:{color:s,fontSize:"14px"},children:t?.text||Qv("nav.loading")})]})};function hr(t){"@babel/helpers - typeof";return hr=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},hr(t)}function Rv(t,e){if(hr(t)!="object"||!t)return t;var n=t[Symbol.toPrimitive];if(n!==void 0){var r=n.call(t,e);if(hr(r)!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(t)}function Ou(t){var e=Rv(t,"string");return hr(e)=="symbol"?e:e+""}function Ts(t,e,n){return(e=Ou(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function Tu(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function Me(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?Tu(Object(n),!0).forEach(function(r){Ts(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):Tu(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function ex(t){if(Array.isArray(t))return t}function tx(t,e){var n=t==null?null:typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(t)).next,e===0){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==e);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function dl(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function Au(t,e){if(t){if(typeof t=="string")return dl(t,e);var n={}.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?dl(t,e):void 0}}function nx(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Bn(t,e){return ex(t)||tx(t,e)||Au(t,e)||nx()}function rx(t,e){if(t==null)return{};var n={};for(var r in t)if({}.hasOwnProperty.call(t,r)){if(e.indexOf(r)!==-1)continue;n[r]=t[r]}return n}function Tn(t,e){if(t==null)return{};var n,r,s=rx(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r<o.length;r++)n=o[r],e.indexOf(n)===-1&&{}.propertyIsEnumerable.call(t,n)&&(s[n]=t[n])}return s}var sx=["defaultInputValue","defaultMenuIsOpen","defaultValue","inputValue","menuIsOpen","onChange","onInputChange","onMenuClose","onMenuOpen","value"];function Du(t){var e=t.defaultInputValue,n=e===void 0?"":e,r=t.defaultMenuIsOpen,s=r===void 0?!1:r,o=t.defaultValue,i=o===void 0?null:o,a=t.inputValue,l=t.menuIsOpen,c=t.onChange,d=t.onInputChange,u=t.onMenuClose,f=t.onMenuOpen,g=t.value,m=Tn(t,sx),p=v.useState(a!==void 0?a:n),y=Bn(p,2),b=y[0],S=y[1],x=v.useState(l!==void 0?l:s),E=Bn(x,2),k=E[0],C=E[1],N=v.useState(g!==void 0?g:i),_=Bn(N,2),w=_[0],M=_[1],O=v.useCallback(function(z,H){typeof c=="function"&&c(z,H),M(z)},[c]),L=v.useCallback(function(z,H){var W;typeof d=="function"&&(W=d(z,H)),S(W!==void 0?W:z)},[d]),F=v.useCallback(function(){typeof f=="function"&&f(),C(!0)},[f]),$=v.useCallback(function(){typeof u=="function"&&u(),C(!1)},[u]),D=a!==void 0?a:b,A=l!==void 0?l:k,T=g!==void 0?g:w;return Me(Me({},m),{},{inputValue:D,menuIsOpen:A,onChange:O,onInputChange:L,onMenuClose:$,onMenuOpen:F,value:T})}function Ae(){return Ae=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var r in n)({}).hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},Ae.apply(null,arguments)}function ox(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function Iu(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,Ou(r.key),r)}}function ix(t,e,n){return e&&Iu(t.prototype,e),n&&Iu(t,n),Object.defineProperty(t,"prototype",{writable:!1}),t}function ul(t,e){return ul=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,r){return n.__proto__=r,n},ul(t,e)}function ax(t,e){if(typeof e!="function"&&e!==null)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&ul(t,e)}function Xo(t){return Xo=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},Xo(t)}function Lu(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(Lu=function(){return!!t})()}function lx(t){if(t===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function cx(t,e){if(e&&(hr(e)=="object"||typeof e=="function"))return e;if(e!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return lx(t)}function dx(t){var e=Lu();return function(){var n,r=Xo(t);if(e){var s=Xo(this).constructor;n=Reflect.construct(r,arguments,s)}else n=r.apply(this,arguments);return cx(this,n)}}function ux(t){if(Array.isArray(t))return dl(t)}function fx(t){if(typeof Symbol<"u"&&t[Symbol.iterator]!=null||t["@@iterator"]!=null)return Array.from(t)}function hx(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Hr(t){return ux(t)||fx(t)||Au(t)||hx()}function gx(t){if(t.sheet)return t.sheet;for(var e=0;e<document.styleSheets.length;e++)if(document.styleSheets[e].ownerNode===t)return document.styleSheets[e]}function px(t){var e=document.createElement("style");return e.setAttribute("data-emotion",t.key),t.nonce!==void 0&&e.setAttribute("nonce",t.nonce),e.appendChild(document.createTextNode("")),e.setAttribute("data-s",""),e}var mx=(function(){function t(n){var r=this;this._insertTag=function(s){var o;r.tags.length===0?r.insertionPoint?o=r.insertionPoint.nextSibling:r.prepend?o=r.container.firstChild:o=r.before:o=r.tags[r.tags.length-1].nextSibling,r.container.insertBefore(s,o),r.tags.push(s)},this.isSpeedy=n.speedy===void 0?!0:n.speedy,this.tags=[],this.ctr=0,this.nonce=n.nonce,this.key=n.key,this.container=n.container,this.prepend=n.prepend,this.insertionPoint=n.insertionPoint,this.before=null}var e=t.prototype;return e.hydrate=function(r){r.forEach(this._insertTag)},e.insert=function(r){this.ctr%(this.isSpeedy?65e3:1)===0&&this._insertTag(px(this));var s=this.tags[this.tags.length-1];if(this.isSpeedy){var o=gx(s);try{o.insertRule(r,o.cssRules.length)}catch{}}else s.appendChild(document.createTextNode(r));this.ctr++},e.flush=function(){this.tags.forEach(function(r){var s;return(s=r.parentNode)==null?void 0:s.removeChild(r)}),this.tags=[],this.ctr=0},t})(),$t="-ms-",Ko="-moz-",tt="-webkit-",Pu="comm",fl="rule",hl="decl",bx="@import",$u="@keyframes",yx="@layer",vx=Math.abs,qo=String.fromCharCode,xx=Object.assign;function wx(t,e){return It(t,0)^45?(((e<<2^It(t,0))<<2^It(t,1))<<2^It(t,2))<<2^It(t,3):0}function Fu(t){return t.trim()}function kx(t,e){return(t=e.exec(t))?t[0]:t}function nt(t,e,n){return t.replace(e,n)}function gl(t,e){return t.indexOf(e)}function It(t,e){return t.charCodeAt(e)|0}function As(t,e,n){return t.slice(e,n)}function An(t){return t.length}function pl(t){return t.length}function Zo(t,e){return e.push(t),t}function Cx(t,e){return t.map(e).join("")}var Qo=1,Ur=1,zu=0,qt=0,kt=0,Br="";function Jo(t,e,n,r,s,o,i){return{value:t,root:e,parent:n,type:r,props:s,children:o,line:Qo,column:Ur,length:i,return:""}}function Ds(t,e){return xx(Jo("",null,null,"",null,null,0),t,{length:-t.length},e)}function Sx(){return kt}function _x(){return kt=qt>0?It(Br,--qt):0,Ur--,kt===10&&(Ur=1,Qo--),kt}function on(){return kt=qt<zu?It(Br,qt++):0,Ur++,kt===10&&(Ur=1,Qo++),kt}function Dn(){return It(Br,qt)}function Ro(){return qt}function Is(t,e){return As(Br,t,e)}function Ls(t){switch(t){case 0:case 9:case 10:case 13:case 32:return 5;case 33:case 43:case 44:case 47:case 62:case 64:case 126:case 59:case 123:case 125:return 4;case 58:return 3;case 34:case 39:case 40:case 91:return 2;case 41:case 93:return 1}return 0}function Vu(t){return Qo=Ur=1,zu=An(Br=t),qt=0,[]}function Hu(t){return Br="",t}function ei(t){return Fu(Is(qt-1,ml(t===91?t+2:t===40?t+1:t)))}function Ex(t){for(;(kt=Dn())&&kt<33;)on();return Ls(t)>2||Ls(kt)>3?"":" "}function Mx(t,e){for(;--e&&on()&&!(kt<48||kt>102||kt>57&&kt<65||kt>70&&kt<97););return Is(t,Ro()+(e<6&&Dn()==32&&on()==32))}function ml(t){for(;on();)switch(kt){case t:return qt;case 34:case 39:t!==34&&t!==39&&ml(kt);break;case 40:t===41&&ml(t);break;case 92:on();break}return qt}function Nx(t,e){for(;on()&&t+kt!==57;)if(t+kt===84&&Dn()===47)break;return"/*"+Is(e,qt-1)+"*"+qo(t===47?t:on())}function Ox(t){for(;!Ls(Dn());)on();return Is(t,qt)}function Tx(t){return Hu(ti("",null,null,null,[""],t=Vu(t),0,[0],t))}function ti(t,e,n,r,s,o,i,a,l){for(var c=0,d=0,u=i,f=0,g=0,m=0,p=1,y=1,b=1,S=0,x="",E=s,k=o,C=r,N=x;y;)switch(m=S,S=on()){case 40:if(m!=108&&It(N,u-1)==58){gl(N+=nt(ei(S),"&","&\f"),"&\f")!=-1&&(b=-1);break}case 34:case 39:case 91:N+=ei(S);break;case 9:case 10:case 13:case 32:N+=Ex(m);break;case 92:N+=Mx(Ro()-1,7);continue;case 47:switch(Dn()){case 42:case 47:Zo(Ax(Nx(on(),Ro()),e,n),l);break;default:N+="/"}break;case 123*p:a[c++]=An(N)*b;case 125*p:case 59:case 0:switch(S){case 0:case 125:y=0;case 59+d:b==-1&&(N=nt(N,/\f/g,"")),g>0&&An(N)-u&&Zo(g>32?Bu(N+";",r,n,u-1):Bu(nt(N," ","")+";",r,n,u-2),l);break;case 59:N+=";";default:if(Zo(C=Uu(N,e,n,c,d,s,a,x,E=[],k=[],u),o),S===123)if(d===0)ti(N,e,C,C,E,o,u,a,k);else switch(f===99&&It(N,3)===110?100:f){case 100:case 108:case 109:case 115:ti(t,C,C,r&&Zo(Uu(t,C,C,0,0,s,a,x,s,E=[],u),k),s,k,u,a,r?E:k);break;default:ti(N,C,C,C,[""],k,0,a,k)}}c=d=g=0,p=b=1,x=N="",u=i;break;case 58:u=1+An(N),g=m;default:if(p<1){if(S==123)--p;else if(S==125&&p++==0&&_x()==125)continue}switch(N+=qo(S),S*p){case 38:b=d>0?1:(N+="\f",-1);break;case 44:a[c++]=(An(N)-1)*b,b=1;break;case 64:Dn()===45&&(N+=ei(on())),f=Dn(),d=u=An(x=N+=Ox(Ro())),S++;break;case 45:m===45&&An(N)==2&&(p=0)}}return o}function Uu(t,e,n,r,s,o,i,a,l,c,d){for(var u=s-1,f=s===0?o:[""],g=pl(f),m=0,p=0,y=0;m<r;++m)for(var b=0,S=As(t,u+1,u=vx(p=i[m])),x=t;b<g;++b)(x=Fu(p>0?f[b]+" "+S:nt(S,/&\f/g,f[b])))&&(l[y++]=x);retu Line truncated
0%, 80%, 100% { opacity: 0; }
40% { opacity: 1; }
`]))),h1=function(e,n){var r=e.isFocused,s=e.size,o=e.theme,i=o.colors,a=o.spacing.baseUnit;return Me({label:"loadingIndicator",display:"flex",transition:"color 150ms",alignSelf:"center",fontSize:s,lineHeight:1,marginRight:s,textAlign:"center",verticalAlign:"middle"},n?{}:{color:r?i.neutral60:i.neutral20,padding:a*2})},Ol=function(e){var n=e.delay,r=e.offset;return Ee("span",{css:wl({animation:"".concat(f1," 1s ease-in-out ").concat(n,"ms infinite;"),backgroundColor:"currentColor",borderRadius:"1em",display:"inline-block",marginLeft:r?"1em":void 0,height:"1em",verticalAlign:"top",width:"1em"},"","")})},g1=function(e){var n=e.innerProps,r=e.isRtl,s=e.size,o=s===void 0?4:s,i=Tn(e,s1);return Ee("div",Ae({},yt(Me(Me({},i),{},{innerProps:n,isRtl:r,size:o}),"loadingIndicator",{indicator:!0,"loading-indicator":!0}),n),Ee(Ol,{delay:0,offset:r}),Ee(Ol,{delay:160,offset:!0}),Ee(Ol,{delay:320,offset:!r}))},p1=function(e,n){var r=e.isDisabled,s=e.isFocused,o=e.theme,i=o.colors,a=o.borderRadius,l=o.spacing;return Me({label:"control",alignItems:"center",cursor:"default",display:"flex",flexWrap:"wrap",justifyContent:"space-between",minHeight:l.controlHeight,outline:"0 !important",position:"relative",transition:"all 100ms"},n?{}:{backgroundColor:r?i.neutral5:i.neutral0,borderColor:r?i.neutral10:s?i.primary:i.neutral20,borderRadius:a,borderStyle:"solid",borderWidth:1,boxShadow:s?"0 0 0 1px ".concat(i.primary):void 0,"&:hover":{borderColor:s?i.primary:i.neutral30}})},m1=function(e){var n=e.children,r=e.isDisabled,s=e.isFocused,o=e.innerRef,i=e.innerProps,a=e.menuIsOpen;return Ee("div",Ae({ref:o},yt(e,"control",{control:!0,"control--is-disabled":r,"control--is-focused":s,"control--menu-is-open":a}),i,{"aria-disabled":r||void 0}),n)},b1=m1,y1=["data"],v1=function(e,n){var r=e.theme.spacing;return n?{}:{paddingBottom:r.baseUnit*2,paddingTop:r.baseUnit*2}},x1=function(e){var n=e.children,r=e.cx,s=e.getStyles,o=e.getClassNames,i=e.Heading,a=e.headingProps,l=e.innerProps,c=e.label,d=e.theme,u=e.selectProps;return Ee("div",Ae({},yt(e,"group",{group:!0}),l),Ee(i,Ae({},a,{selectProps:u,theme:d,getStyles:s,getClassNames:o,cx:r}),c),Ee("div",null,n))},w1=function(e,n){var r=e.theme,s=r.colors,o=r.spacing;return Me({label:"group",cursor:"default",display:"block"},n?{}:{color:s.neutral40,fontSize:"75%",fontWeight:500,marginBottom:"0.25em",paddingLeft:o.baseUnit*3,paddingRight:o.baseUnit*3,textTransform:"uppercase"})},k1=function(e){var n=df(e);n.data;var r=Tn(n,y1);return Ee("div",Ae({},yt(e,"groupHeading",{"group-heading":!0}),r))},C1=x1,S1=["innerRef","isDisabled","isHidden","inputClassName"],_1=function(e,n){var r=e.isDisabled,s=e.value,o=e.theme,i=o.spacing,a=o.colors;return Me(Me({visibility:r?"hidden":"visible",transform:s?"translateZ(0)":""},E1),n?{}:{margin:i.baseUnit/2,paddingBottom:i.baseUnit/2,paddingTop:i.baseUnit/2,color:a.neutral80})},kf={gridArea:"1 / 2",font:"inherit",minWidth:"2px",border:0,margin:0,outline:0,padding:0},E1={flex:"1 1 auto",display:"inline-grid",gridArea:"1 / 1 / 2 / 3",gridTemplateColumns:"0 min-content","&:after":Me({content:'attr(data-value) " "',visibility:"hidden",whiteSpace:"pre"},kf)},M1=function(e){return Me({label:"input",color:"inherit",background:0,opacity:e?0:1,width:"100%"},kf)},N1=function(e){var n=e.cx,r=e.value,s=df(e),o=s.innerRef,i=s.isDisabled,a=s.isHidden,l=s.inputClassName,c=Tn(s,S1);return Ee("div",Ae({},yt(e,"input",{"input-container":!0}),{"data-value":r||""}),Ee("input",Ae({className:n({input:!0},l),ref:o,style:M1(a),disabled:i},c)))},O1=N1,T1=function(e,n){var r=e.theme,s=r.spacing,o=r.borderRadius,i=r.colors;return Me({label:"multiValue",display:"flex",minWidth:0},n?{}:{backgroundColor:i.neutral10,borderRadius:o/2,margin:s.baseUnit/2})},A1=function(e,n){var r=e.theme,s=r.borderRadius,o=r.colors,i=e.cropWithEllipsis;return Me({overflow:"hidden",textOverflow:i||i===void 0?"ellipsis":void 0,whiteSpace:"nowrap"},n?{}:{borderRadius:s/2,color:o.neutral80,fontSize:"85%",padding:3,paddingLeft:6})},D1=function(e,n){var r=e.theme,s=r.spacing,o=r.borderRadius,i=r.colors,a=e.isFocused;return Me({alignItems:"center",display:"flex"},n?{}:{borderRadius:o/2,backgroundColor:a?i.dangerLight:void 0,paddingLeft:s.baseUnit,paddingRight:s.baseUnit,":hover":{backgroundColor:i.dangerLight,color:i.danger}})},Cf=function(e){var n=e.children,r=e.innerProps;return Ee("div",r,n)},I1=Cf,L1=Cf;function P1(t){var e=t.children,n=t.innerProps;return Ee("div",Ae({role:"button"},n),e||Ee(Nl,{size:14}))}var $1=function(e){var n=e.children,r=e.components,s=e.data,o=e.innerProps,i=e.isDisabled,a=e.removeProps,l=e.selectProps,c=r.Container,d=r.Label,u=r.Remove;return Ee(c,{data:s,innerProps:Me(Me({},yt(e,"multiValue",{"multi-value":!0,"multi-value--is-disabled":i})),o),selectProps:l},Ee(d,{data:s,innerProps:Me({},yt(e,"multiValueLabel",{"multi-value__label":!0})),selectProps:l},n),Ee(u,{data:s,innerProps:Me(Me({},yt(e,"multiValueRemove",{"multi-value__remove":!0})),{},{"aria-label":"Remove ".concat(n||"option")},a),s Line truncated
`]))),m1=function(e,n){var r=e.isFocused,s=e.size,o=e.theme,i=o.colors,a=o.spacing.baseUnit;return Me({label:"loadingIndicator",display:"flex",transition:"color 150ms",alignSelf:"center",fontSize:s,lineHeight:1,marginRight:s,textAlign:"center",verticalAlign:"middle"},n?{}:{color:r?i.neutral60:i.neutral20,padding:a*2})},Tl=function(e){var n=e.delay,r=e.offset;return Ee("span",{css:kl({animation:"".concat(p1," 1s ease-in-out ").concat(n,"ms infinite;"),backgroundColor:"currentColor",borderRadius:"1em",display:"inline-block",marginLeft:r?"1em":void 0,height:"1em",verticalAlign:"top",width:"1em"},"","")})},b1=function(e){var n=e.innerProps,r=e.isRtl,s=e.size,o=s===void 0?4:s,i=Tn(e,a1);return Ee("div",Ae({},yt(Me(Me({},i),{},{innerProps:n,isRtl:r,size:o}),"loadingIndicator",{indicator:!0,"loading-indicator":!0}),n),Ee(Tl,{delay:0,offset:r}),Ee(Tl,{delay:160,offset:!0}),Ee(Tl,{delay:320,offset:!r}))},y1=function(e,n){var r=e.isDisabled,s=e.isFocused,o=e.theme,i=o.colors,a=o.borderRadius,l=o.spacing;return Me({label:"control",alignItems:"center",cursor:"default",display:"flex",flexWrap:"wrap",justifyContent:"space-between",minHeight:l.controlHeight,outline:"0 !important",position:"relative",transition:"all 100ms"},n?{}:{backgroundColor:r?i.neutral5:i.neutral0,borderColor:r?i.neutral10:s?i.primary:i.neutral20,borderRadius:a,borderStyle:"solid",borderWidth:1,boxShadow:s?"0 0 0 1px ".concat(i.primary):void 0,"&:hover":{borderColor:s?i.primary:i.neutral30}})},v1=function(e){var n=e.children,r=e.isDisabled,s=e.isFocused,o=e.innerRef,i=e.innerProps,a=e.menuIsOpen;return Ee("div",Ae({ref:o},yt(e,"control",{control:!0,"control--is-disabled":r,"control--is-focused":s,"control--menu-is-open":a}),i,{"aria-disabled":r||void 0}),n)},x1=v1,w1=["data"],k1=function(e,n){var r=e.theme.spacing;return n?{}:{paddingBottom:r.baseUnit*2,paddingTop:r.baseUnit*2}},C1=function(e){var n=e.children,r=e.cx,s=e.getStyles,o=e.getClassNames,i=e.Heading,a=e.headingProps,l=e.innerProps,c=e.label,d=e.theme,u=e.selectProps;return Ee("div",Ae({},yt(e,"group",{group:!0}),l),Ee(i,Ae({},a,{selectProps:u,theme:d,getStyles:s,getClassNames:o,cx:r}),c),Ee("div",null,n))},S1=function(e,n){var r=e.theme,s=r.colors,o=r.spacing;return Me({label:"group",cursor:"default",display:"block"},n?{}:{color:s.neutral40,fontSize:"75%",fontWeight:500,marginBottom:"0.25em",paddingLeft:o.baseUnit*3,paddingRight:o.baseUnit*3,textTransform:"uppercase"})},_1=function(e){var n=uf(e);n.data;var r=Tn(n,w1);return Ee("div",Ae({},yt(e,"groupHeading",{"group-heading":!0}),r))},E1=C1,M1=["innerRef","isDisabled","isHidden","inputClassName"],N1=function(e,n){var r=e.isDisabled,s=e.value,o=e.theme,i=o.spacing,a=o.colors;return Me(Me({visibility:r?"hidden":"visible",transform:s?"translateZ(0)":""},O1),n?{}:{margin:i.baseUnit/2,paddingBottom:i.baseUnit/2,paddingTop:i.baseUnit/2,color:a.neutral80})},Cf={gridArea:"1 / 2",font:"inherit",minWidth:"2px",border:0,margin:0,outline:0,padding:0},O1={flex:"1 1 auto",display:"inline-grid",gridArea:"1 / 1 / 2 / 3",gridTemplateColumns:"0 min-content","&:after":Me({content:'attr(data-value) " "',visibility:"hidden",whiteSpace:"pre"},Cf)},T1=function(e){return Me({label:"input",color:"inherit",background:0,opacity:e?0:1,width:"100%"},Cf)},A1=function(e){var n=e.cx,r=e.value,s=uf(e),o=s.innerRef,i=s.isDisabled,a=s.isHidden,l=s.inputClassName,c=Tn(s,M1);return Ee("div",Ae({},yt(e,"input",{"input-container":!0}),{"data-value":r||""}),Ee("input",Ae({className:n({input:!0},l),ref:o,style:T1(a),disabled:i},c)))},D1=A1,I1=function(e,n){var r=e.theme,s=r.spacing,o=r.borderRadius,i=r.colors;return Me({label:"multiValue",display:"flex",minWidth:0},n?{}:{backgroundColor:i.neutral10,borderRadius:o/2,margin:s.baseUnit/2})},L1=function(e,n){var r=e.theme,s=r.borderRadius,o=r.colors,i=e.cropWithEllipsis;return Me({overflow:"hidden",textOverflow:i||i===void 0?"ellipsis":void 0,whiteSpace:"nowrap"},n?{}:{borderRadius:s/2,color:o.neutral80,fontSize:"85%",padding:3,paddingLeft:6})},P1=function(e,n){var r=e.theme,s=r.spacing,o=r.borderRadius,i=r.colors,a=e.isFocused;return Me({alignItems:"center",display:"flex"},n?{}:{borderRadius:o/2,backgroundColor:a?i.dangerLight:void 0,paddingLeft:s.baseUnit,paddingRight:s.baseUnit,":hover":{backgroundColor:i.dangerLight,color:i.danger}})},Sf=function(e){var n=e.children,r=e.innerProps;return Ee("div",r,n)},$1=Sf,F1=Sf;function z1(t){var e=t.children,n=t.innerProps;return Ee("div",Ae({role:"button"},n),e||Ee(Ol,{size:14}))}var V1=function(e){var n=e.children,r=e.components,s=e.data,o=e.innerProps,i=e.isDisabled,a=e.removeProps,l=e.selectProps,c=r.Container,d=r.Label,u=r.Remove;return Ee(c,{data:s,innerProps:Me(Me({},yt(e,"multiValue",{"multi-value":!0,"multi-value--is-disabled":i})),o),selectProps:l},Ee(d,{data:s,innerProps:Me({},yt(e,"multiValueLabel",{"multi-value__label":!0})),selectProps:l},n),Ee(u,{data:s,innerProps:Me(Me({},yt(e,"multiValueRemove",{"multi-value__remove":!0})),{},{"aria-label":"Remove ".concat(n||"option")},a),s Line truncated
px-4 py-2.5 bg-gray-100 dark:bg-gray-700
border-0 rounded-xl
text-gray-700 dark:text-gray-200 font-medium
@@ -187,11 +187,11 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
hover:bg-gray-100 dark:hover:bg-gray-700
transition-colors
${D.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}
`,role:"option","aria-selected":A,children:[h.jsx(I,{className:"w-5 h-5 flex-shrink-0 mt-0.5",children:A&&h.jsx(Hn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`font-medium ${A?"text-blue-600 dark:text-blue-400":"text-gray-900 dark:text-white"}`,children:D.label}),D.description&&h.jsx(I,{className:"text-sm text-gray-500 dark:text-gray-400 mt-0.5",children:D.description})]})]},D.value)})})]})]})]})},zs={container:{base:"tag-select",disabled:"opacity-50 cursor-not-allowed"},tag:"tag",tagRemove:"tag-remove",placeholder:"tag-select-placeholder"},Wk=({options:t=[],value:e=[],onChange:n,placeholder:r="No items selected",disabled:s=!1,className:o="",id:i,editorAttrs:a})=>{const l=v.useCallback(f=>t.find(m=>String(m.value)===String(f))?.label||String(f),[t]),c=v.useCallback(f=>{if(s)return;const g=e.filter(m=>String(m)!==String(f));n?.(g)},[e,n,s]),d=v.useMemo(()=>e.map(f=>({value:f,label:l(f)})),[e,l]),u=`${zs.container.base} ${s?zs.container.disabled:""} ${o}`.trim();return h.jsx(I,{className:u,id:i,...a,children:d.length===0?h.jsx(Q,{className:zs.placeholder,children:r}):d.map(f=>h.jsxs(Q,{className:zs.tag,children:[f.label,!s&&h.jsx(Q,{className:zs.tagRemove,onClick:()=>c(f.value),role:"button","aria-label":`Remove ${f.label}`,children:h.jsx(Hn,{className:"w-3 h-3",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]},f.value))})};function Gk(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return v.useMemo(()=>r=>{e.forEach(s=>s(r))},e)}const mi=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Yr(t){const e=Object.prototype.toString.call(t);return e==="[object Window]"||e==="[object global]"}function Hl(t){return"nodeType"in t}function Yt(t){var e,n;return t?Yr(t)?t:Hl(t)&&(e=(n=t.ownerDocument)==null?void 0:n.defaultView)!=null?e:window:window}function Ul(t){const{Document:e}=Yt(t);return t instanceof e}function Vs(t){return Yr(t)?!1:t instanceof Yt(t).HTMLElement}function nh(t){return t instanceof Yt(t).SVGElement}function Xr(t){return t?Yr(t)?t.document:Hl(t)?Ul(t)?t:Vs(t)||nh(t)?t.ownerDocument:document:document:document}const wn=mi?v.useLayoutEffect:v.useEffect;function bi(t){const e=v.useRef(t);return wn(()=>{e.current=t}),v.useCallback(function(){for(var n=arguments.length,r=new Array(n),s=0;s<n;s++)r[s]=arguments[s];return e.current==null?void 0:e.current(...r)},[])}function Yk(){const t=v.useRef(null),e=v.useCallback((r,s)=>{t.current=setInterval(r,s)},[]),n=v.useCallback(()=>{t.current!==null&&(clearInterval(t.current),t.current=null)},[]);return[e,n]}function Hs(t,e){e===void 0&&(e=[t]);const n=v.useRef(t);return wn(()=>{n.current!==t&&(n.current=t)},e),n}function Us(t,e){const n=v.useRef();return v.useMemo(()=>{const r=t(n.current);return n.current=r,r},[...e])}function yi(t){const e=bi(t),n=v.useRef(null),r=v.useCallback(s=>{s!==n.current&&e?.(s,n.current),n.current=s},[]);return[n,r]}function vi(t){const e=v.useRef();return v.useEffect(()=>{e.current=t},[t]),e.current}let Bl={};function Bs(t,e){return v.useMemo(()=>{if(e)return e;const n=Bl[t]==null?0:Bl[t]+1;return Bl[t]=n,t+"-"+n},[t,e])}function rh(t){return function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return r.reduce((o,i)=>{const a=Object.entries(i);for(const[l,c]of a){const d=o[l];d!=null&&(o[l]=d+t*c)}return o},{...e})}}const Kr=rh(1),js=rh(-1);function Xk(t){return"clientX"in t&&"clientY"in t}function xi(t){if(!t)return!1;const{KeyboardEvent:e}=Yt(t.target);return e&&t instanceof e}function Kk(t){if(!t)return!1;const{TouchEvent:e}=Yt(t.target);return e&&t instanceof e}function wi(t){if(Kk(t)){if(t.touches&&t.touches.length){const{clientX:e,clientY:n}=t.touches[0];return{x:e,y:n}}else if(t.changedTouches&&t.changedTouches.length){const{clientX:e,clientY:n}=t.changedTouches[0];return{x:e,y:n}}}return Xk(t)?{x:t.clientX,y:t.clientY}:null}const Ln=Object.freeze({Translate:{toString(t){if(!t)return;const{x:e,y:n}=t;return"translate3d("+(e?Math.round(e):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(t){if(!t)return;const{scaleX:e,scaleY:n}=t;return"scaleX("+e+") scaleY("+n+")"}},Transform:{toString(t){if(t)return[Ln.Translate.toString(t),Ln.Scale.toString(t)].join(" ")}},Transition:{toString(t){let{property:e,duration:n,easing:r}=t;return e+" "+n+"ms "+r}}}),sh="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function qk(t){return t.matches(sh)?t:t.querySelector(sh)}const Zk={display:"none"};function Qk(t){let{id:e,value:n}=t;return v.createElement("div",{id:e,s Line truncated
`,role:"option","aria-selected":A,children:[h.jsx(I,{className:"w-5 h-5 flex-shrink-0 mt-0.5",children:A&&h.jsx(Hn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`font-medium ${A?"text-blue-600 dark:text-blue-400":"text-gray-900 dark:text-white"}`,children:D.label}),D.description&&h.jsx(I,{className:"text-sm text-gray-500 dark:text-gray-400 mt-0.5",children:D.description})]})]},D.value)})})]})]})]})},zs={container:{base:"tag-select",disabled:"opacity-50 cursor-not-allowed"},tag:"tag",tagRemove:"tag-remove",placeholder:"tag-select-placeholder"},Xk=({options:t=[],value:e=[],onChange:n,placeholder:r="No items selected",disabled:s=!1,className:o="",id:i,editorAttrs:a})=>{const l=v.useCallback(f=>t.find(m=>String(m.value)===String(f))?.label||String(f),[t]),c=v.useCallback(f=>{if(s)return;const g=e.filter(m=>String(m)!==String(f));n?.(g)},[e,n,s]),d=v.useMemo(()=>e.map(f=>({value:f,label:l(f)})),[e,l]),u=`${zs.container.base} ${s?zs.container.disabled:""} ${o}`.trim();return h.jsx(I,{className:u,id:i,...a,children:d.length===0?h.jsx(Q,{className:zs.placeholder,children:r}):d.map(f=>h.jsxs(Q,{className:zs.tag,children:[f.label,!s&&h.jsx(Q,{className:zs.tagRemove,onClick:()=>c(f.value),role:"button","aria-label":`Remove ${f.label}`,children:h.jsx(Hn,{className:"w-3 h-3",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]},f.value))})};function Kk(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return v.useMemo(()=>r=>{e.forEach(s=>s(r))},e)}const mi=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Yr(t){const e=Object.prototype.toString.call(t);return e==="[object Window]"||e==="[object global]"}function Ul(t){return"nodeType"in t}function Yt(t){var e,n;return t?Yr(t)?t:Ul(t)&&(e=(n=t.ownerDocument)==null?void 0:n.defaultView)!=null?e:window:window}function Bl(t){const{Document:e}=Yt(t);return t instanceof e}function Vs(t){return Yr(t)?!1:t instanceof Yt(t).HTMLElement}function rh(t){return t instanceof Yt(t).SVGElement}function Xr(t){return t?Yr(t)?t.document:Ul(t)?Bl(t)?t:Vs(t)||rh(t)?t.ownerDocument:document:document:document}const wn=mi?v.useLayoutEffect:v.useEffect;function bi(t){const e=v.useRef(t);return wn(()=>{e.current=t}),v.useCallback(function(){for(var n=arguments.length,r=new Array(n),s=0;s<n;s++)r[s]=arguments[s];return e.current==null?void 0:e.current(...r)},[])}function qk(){const t=v.useRef(null),e=v.useCallback((r,s)=>{t.current=setInterval(r,s)},[]),n=v.useCallback(()=>{t.current!==null&&(clearInterval(t.current),t.current=null)},[]);return[e,n]}function Hs(t,e){e===void 0&&(e=[t]);const n=v.useRef(t);return wn(()=>{n.current!==t&&(n.current=t)},e),n}function Us(t,e){const n=v.useRef();return v.useMemo(()=>{const r=t(n.current);return n.current=r,r},[...e])}function yi(t){const e=bi(t),n=v.useRef(null),r=v.useCallback(s=>{s!==n.current&&e?.(s,n.current),n.current=s},[]);return[n,r]}function vi(t){const e=v.useRef();return v.useEffect(()=>{e.current=t},[t]),e.current}let jl={};function Bs(t,e){return v.useMemo(()=>{if(e)return e;const n=jl[t]==null?0:jl[t]+1;return jl[t]=n,t+"-"+n},[t,e])}function sh(t){return function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return r.reduce((o,i)=>{const a=Object.entries(i);for(const[l,c]of a){const d=o[l];d!=null&&(o[l]=d+t*c)}return o},{...e})}}const Kr=sh(1),js=sh(-1);function Zk(t){return"clientX"in t&&"clientY"in t}function xi(t){if(!t)return!1;const{KeyboardEvent:e}=Yt(t.target);return e&&t instanceof e}function Qk(t){if(!t)return!1;const{TouchEvent:e}=Yt(t.target);return e&&t instanceof e}function wi(t){if(Qk(t)){if(t.touches&&t.touches.length){const{clientX:e,clientY:n}=t.touches[0];return{x:e,y:n}}else if(t.changedTouches&&t.changedTouches.length){const{clientX:e,clientY:n}=t.changedTouches[0];return{x:e,y:n}}}return Zk(t)?{x:t.clientX,y:t.clientY}:null}const Ln=Object.freeze({Translate:{toString(t){if(!t)return;const{x:e,y:n}=t;return"translate3d("+(e?Math.round(e):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(t){if(!t)return;const{scaleX:e,scaleY:n}=t;return"scaleX("+e+") scaleY("+n+")"}},Transform:{toString(t){if(t)return[Ln.Translate.toString(t),Ln.Scale.toString(t)].join(" ")}},Transition:{toString(t){let{property:e,duration:n,easing:r}=t;return e+" "+n+"ms "+r}}}),oh="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function Jk(t){return t.matches(oh)?t:t.querySelector(oh)}const Rk={display:"none"};function eC(t){let{id:e,value:n}=t;return v.createElement("div",{id:e,s Line truncated
To pick up a draggable item, press the space bar.
While dragging, use the arrow keys to move the item.
Press space again to drop the item in its new position, or press escape to cancel.
`},rC={onDragStart(t){let{active:e}=t;return"Picked up draggable item "+e.id+"."},onDragOver(t){let{active:e,over:n}=t;return n?"Draggable item "+e.id+" was moved over droppable area "+n.id+".":"Draggable item "+e.id+" is no longer over a droppable area."},onDragEnd(t){let{active:e,over:n}=t;return n?"Draggable item "+e.id+" was dropped over droppable area "+n.id:"Draggable item "+e.id+" was dropped."},onDragCancel(t){let{active:e}=t;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function sC(t){let{announcements:e=rC,container:n,hiddenTextDescribedById:r,screenReaderInstructions:s=nC}=t;const{announce:o,announcement:i}=Rk(),a=Bs("DndLiveRegion"),[l,c]=v.useState(!1);if(v.useEffect(()=>{c(!0)},[]),eC(v.useMemo(()=>({onDragStart(u){let{active:f}=u;o(e.onDragStart({active:f}))},onDragMove(u){let{active:f,over:g}=u;e.onDragMove&&o(e.onDragMove({active:f,over:g}))},onDragOver(u){let{active:f,over:g}=u;o(e.onDragOver({active:f,over:g}))},onDragEnd(u){let{active:f,over:g}=u;o(e.onDragEnd({active:f,over:g}))},onDragCancel(u){let{active:f,over:g}=u;o(e.onDragCancel({active:f,over:g}))}}),[o,e])),!l)return null;const d=v.createElement(v.Fragment,null,v.createElement(Qk,{id:r,value:s.draggable}),v.createElement(Jk,{id:a,announcement:i}));return n?On.createPortal(d,n):d}var St;(function(t){t.DragStart="dragStart",t.DragMove="dragMove",t.DragEnd="dragEnd",t.DragCancel="dragCancel",t.DragOver="dragOver",t.RegisterDroppable="registerDroppable",t.SetDroppableDisabled="setDroppableDisabled",t.UnregisterDroppable="unregisterDroppable"})(St||(St={}));function ki(){}function qr(t,e){return v.useMemo(()=>({sensor:t,options:e??{}}),[t,e])}function jl(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return v.useMemo(()=>[...e].filter(r=>r!=null),[...e])}const kn=Object.freeze({x:0,y:0});function ih(t,e){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}function oC(t,e){const n=wi(t);if(!n)return"0 0";const r={x:(n.x-e.left)/e.width*100,y:(n.y-e.top)/e.height*100};return r.x+"% "+r.y+"%"}function ah(t,e){let{data:{value:n}}=t,{data:{value:r}}=e;return n-r}function iC(t,e){let{data:{value:n}}=t,{data:{value:r}}=e;return r-n}function lh(t){let{left:e,top:n,height:r,width:s}=t;return[{x:e,y:n},{x:e+s,y:n},{x:e,y:n+r},{x:e+s,y:n+r}]}function ch(t,e){if(!t||t.length===0)return null;const[n]=t;return n[e]}function dh(t,e,n){return e===void 0&&(e=t.left),n===void 0&&(n=t.top),{x:e+t.width*.5,y:n+t.height*.5}}const Wl=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=dh(e,e.left,e.top),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=ih(dh(l),s);o.push({id:a,data:{droppableContainer:i,value:c}})}}return o.sort(ah)},aC=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=lh(e),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=lh(l),d=s.reduce((f,g,m)=>f+ih(c[m],g),0),u=Number((d/4).toFixed(4));o.push({id:a,data:{droppableContainer:i,value:u}})}}return o.sort(ah)};function lC(t,e){const n=Math.max(e.top,t.top),r=Math.max(e.left,t.left),s=Math.min(e.left+e.width,t.left+t.width),o=Math.min(e.top+e.height,t.top+t.height),i=s-r,a=o-n;if(r<s&&n<o){const l=e.width*e.height,c=t.width*t.height,d=i*a,u=d/(l+c-d);return Number(u.toFixed(4))}return 0}const cC=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=[];for(const o of r){const{id:i}=o,a=n.get(i);if(a){const l=lC(a,e);l>0&&s.push({id:i,data:{droppableContainer:o,value:l}})}}return s.sort(iC)};function dC(t,e,n){return{...t,scaleX:e&&n?e.width/n.width:1,scaleY:e&&n?e.height/n.height:1}}function uh(t,e){return t&&e?{x:t.left-e.left,y:t.top-e.top}:kn}function uC(t){return function(n){for(var r=arguments.length,s=new Array(r>1?r-1:0),o=1;o<r;o++)s[o-1]=arguments[o];return s.reduce((i,a)=>({...i,top:i.top+t*a.y,bottom:i.bottom+t*a.y,left:i.left+t*a.x,right:i.right+t*a.x}),{...n})}}const fC=uC(1);function fh(t){if(t.startsWith("matrix3d(")){const e=t.slice(9,-1).split(/, /);return{x:+e[12],y:+e[13],scaleX:+e[0],scaleY:+e[5]}}else if(t.startsWith("matrix(")){const e=t.slice(7,-1).split(/, /);return{x:+e[4],y:+e[5],scaleX:+e[0],scaleY:+e[3]}}return null}function hC(t,e,n){const r=fh(e);if(!r)return t;const{scaleX:s,scaleY:o,x:i,y:a}=r,l=t.left-i-(1-s)*parseFloat(n),c=t.top-a-(1-o)*parseFloat(n.slice(n.indexOf(" ")+1)),d=s?t.width/s:t.width,u=o?t.height/o:t.height;return{width:d,height:u,top:c,right:l+d,bottom:c+u,left:l}}const gC={ignoreTransform:!1};function Zr(t,e){e===void 0&&(e=gC);let n=t.getBoundingClientRect();if(e.ignoreTransform){const{transform:c,transformOrigin:d}=Yt(t).getComputedStyle(t);c&&(n=hC(n,c,d))}const{top:r,left:s,width:o,height:i,bottom:a,right:l}=n;return{top:r,left:s,width:o,height:i,bottom:a,right:l}}function hh(t){return Zr(t,{ignoreTransform:!0})}function pC(t){const e=t.innerWidth,n=t.innerHeight;return{top:0,left:0,right:e,bottom:n,width:e,height:n}}function mC(t,e){return e===void 0&&(e=Yt(t).getComputedStyle(t)),e.posit Line truncated
`},iC={onDragStart(t){let{active:e}=t;return"Picked up draggable item "+e.id+"."},onDragOver(t){let{active:e,over:n}=t;return n?"Draggable item "+e.id+" was moved over droppable area "+n.id+".":"Draggable item "+e.id+" is no longer over a droppable area."},onDragEnd(t){let{active:e,over:n}=t;return n?"Draggable item "+e.id+" was dropped over droppable area "+n.id:"Draggable item "+e.id+" was dropped."},onDragCancel(t){let{active:e}=t;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function aC(t){let{announcements:e=iC,container:n,hiddenTextDescribedById:r,screenReaderInstructions:s=oC}=t;const{announce:o,announcement:i}=nC(),a=Bs("DndLiveRegion"),[l,c]=v.useState(!1);if(v.useEffect(()=>{c(!0)},[]),rC(v.useMemo(()=>({onDragStart(u){let{active:f}=u;o(e.onDragStart({active:f}))},onDragMove(u){let{active:f,over:g}=u;e.onDragMove&&o(e.onDragMove({active:f,over:g}))},onDragOver(u){let{active:f,over:g}=u;o(e.onDragOver({active:f,over:g}))},onDragEnd(u){let{active:f,over:g}=u;o(e.onDragEnd({active:f,over:g}))},onDragCancel(u){let{active:f,over:g}=u;o(e.onDragCancel({active:f,over:g}))}}),[o,e])),!l)return null;const d=v.createElement(v.Fragment,null,v.createElement(eC,{id:r,value:s.draggable}),v.createElement(tC,{id:a,announcement:i}));return n?On.createPortal(d,n):d}var St;(function(t){t.DragStart="dragStart",t.DragMove="dragMove",t.DragEnd="dragEnd",t.DragCancel="dragCancel",t.DragOver="dragOver",t.RegisterDroppable="registerDroppable",t.SetDroppableDisabled="setDroppableDisabled",t.UnregisterDroppable="unregisterDroppable"})(St||(St={}));function ki(){}function qr(t,e){return v.useMemo(()=>({sensor:t,options:e??{}}),[t,e])}function Wl(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return v.useMemo(()=>[...e].filter(r=>r!=null),[...e])}const kn=Object.freeze({x:0,y:0});function ah(t,e){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}function lC(t,e){const n=wi(t);if(!n)return"0 0";const r={x:(n.x-e.left)/e.width*100,y:(n.y-e.top)/e.height*100};return r.x+"% "+r.y+"%"}function lh(t,e){let{data:{value:n}}=t,{data:{value:r}}=e;return n-r}function cC(t,e){let{data:{value:n}}=t,{data:{value:r}}=e;return r-n}function ch(t){let{left:e,top:n,height:r,width:s}=t;return[{x:e,y:n},{x:e+s,y:n},{x:e,y:n+r},{x:e+s,y:n+r}]}function dh(t,e){if(!t||t.length===0)return null;const[n]=t;return n[e]}function uh(t,e,n){return e===void 0&&(e=t.left),n===void 0&&(n=t.top),{x:e+t.width*.5,y:n+t.height*.5}}const Gl=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=uh(e,e.left,e.top),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=ah(uh(l),s);o.push({id:a,data:{droppableContainer:i,value:c}})}}return o.sort(lh)},dC=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=ch(e),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=ch(l),d=s.reduce((f,g,m)=>f+ah(c[m],g),0),u=Number((d/4).toFixed(4));o.push({id:a,data:{droppableContainer:i,value:u}})}}return o.sort(lh)};function uC(t,e){const n=Math.max(e.top,t.top),r=Math.max(e.left,t.left),s=Math.min(e.left+e.width,t.left+t.width),o=Math.min(e.top+e.height,t.top+t.height),i=s-r,a=o-n;if(r<s&&n<o){const l=e.width*e.height,c=t.width*t.height,d=i*a,u=d/(l+c-d);return Number(u.toFixed(4))}return 0}const fC=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=[];for(const o of r){const{id:i}=o,a=n.get(i);if(a){const l=uC(a,e);l>0&&s.push({id:i,data:{droppableContainer:o,value:l}})}}return s.sort(cC)};function hC(t,e,n){return{...t,scaleX:e&&n?e.width/n.width:1,scaleY:e&&n?e.height/n.height:1}}function fh(t,e){return t&&e?{x:t.left-e.left,y:t.top-e.top}:kn}function gC(t){return function(n){for(var r=arguments.length,s=new Array(r>1?r-1:0),o=1;o<r;o++)s[o-1]=arguments[o];return s.reduce((i,a)=>({...i,top:i.top+t*a.y,bottom:i.bottom+t*a.y,left:i.left+t*a.x,right:i.right+t*a.x}),{...n})}}const pC=gC(1);function hh(t){if(t.startsWith("matrix3d(")){const e=t.slice(9,-1).split(/, /);return{x:+e[12],y:+e[13],scaleX:+e[0],scaleY:+e[5]}}else if(t.startsWith("matrix(")){const e=t.slice(7,-1).split(/, /);return{x:+e[4],y:+e[5],scaleX:+e[0],scaleY:+e[3]}}return null}function mC(t,e,n){const r=hh(e);if(!r)return t;const{scaleX:s,scaleY:o,x:i,y:a}=r,l=t.left-i-(1-s)*parseFloat(n),c=t.top-a-(1-o)*parseFloat(n.slice(n.indexOf(" ")+1)),d=s?t.width/s:t.width,u=o?t.height/o:t.height;return{width:d,height:u,top:c,right:l+d,bottom:c+u,left:l}}const bC={ignoreTransform:!1};function Zr(t,e){e===void 0&&(e=bC);let n=t.getBoundingClientRect();if(e.ignoreTransform){const{transform:c,transformOrigin:d}=Yt(t).getComputedStyle(t);c&&(n=mC(n,c,d))}const{top:r,left:s,width:o,height:i,bottom:a,right:l}=n;return{top:r,left:s,width:o,height:i,bottom:a,right:l}}function gh(t){return Zr(t,{ignoreTransform:!0})}function yC(t){const e=t.innerWidth,n=t.innerHeight;return{top:0,left:0,right:e,bottom:n,width:e,height:n}}function vC(t,e){return e===void 0&&(e=Yt(t).getComputedStyle(t)),e.posit Line truncated
w-full flex items-center justify-between px-3 py-2
bg-white dark:bg-gray-800
border border-gray-300 dark:border-gray-600 rounded-lg
@@ -201,7 +201,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
`,children:[h.jsx(I,{className:"flex items-center gap-2",children:y?h.jsxs(h.Fragment,{children:[h.jsx(re,{name:y.faIcon,className:"w-4 h-4 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white",children:y.label})]}):h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:r})}),h.jsx(re,{name:"chevron-down",className:`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${u?"rotate-180":""}`})]}),l&&h.jsx("input",{type:"hidden",name:l,value:t||""}),u&&h.jsxs(I,{className:"absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-72 overflow-hidden",children:[h.jsx(I,{className:"p-2 border-b border-gray-200 dark:border-gray-700",children:h.jsx(wt,{type:"text",value:g,onChange:x=>m(x.target.value),placeholder:s,className:"w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white",autoFocus:!0})}),h.jsx(I,{className:"max-h-56 overflow-y-auto",children:b.length>0?b.map(x=>h.jsxs(oe,{type:"button",onClick:()=>S(x),className:`
w-full flex items-center gap-3 px-3 py-2 text-left text-sm
${y?.faIcon===x.faIcon?"bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"}
`,children:[h.jsx(re,{name:x.faIcon,className:"w-4 h-4"}),h.jsx(Q,{children:x.label}),y?.faIcon===x.faIcon&&h.jsx(re,{name:"check",className:"w-4 h-4 ml-auto text-blue-600"})]},x.value)):h.jsx(I,{className:"px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center",children:o})})]})]})},rc=({defaultOpen:t=!1,isOpen:e,onToggle:n,className:r="",style:s,children:o,disabled:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(t),u=e!==void 0,f=u?e:c,g=v.useRef(null),[m,p]=v.useState(t?void 0:0),y=()=>{if(i)return;const S=!f;u||d(S),n?.(S)};v.useEffect(()=>{if(g.current)if(f){p(g.current.scrollHeight);const S=setTimeout(()=>{p(void 0)},300);return()=>clearTimeout(S)}else p(g.current.scrollHeight),requestAnimationFrame(()=>{p(0)})},[f]);const b={};if(v.Children.forEach(o,S=>{if(v.isValidElement(S)){const x=S.props?.slot;x==="trigger"?b.trigger=S:x==="content"&&(b.content=S)}}),!b.trigger&&!b.content){const S=v.Children.toArray(o);S.length>=2?(b.trigger=S[0],b.content=S.slice(1)):S.length===1&&(b.content=S[0])}return h.jsxs(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${r}`,style:s,id:a,...l,children:[h.jsxs(oe,{type:"button",onClick:y,disabled:i,className:`w-full flex items-center justify-between p-3 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${i?"opacity-50 cursor-not-allowed":""} ${f?"border-b border-gray-200 dark:border-gray-700":""}`,"aria-expanded":f,children:[h.jsx(I,{className:"flex-1",children:b.trigger}),!b.trigger&&h.jsx("svg",{className:`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),h.jsx(I,{ref:g,className:"overflow-hidden transition-all duration-300 ease-in-out",style:{height:m!==void 0?`${m}px`:"auto"},children:h.jsx(I,{className:"bg-white dark:bg-gray-800",children:b.content})})]})},Js=window.G7Core,Qh=Js?.useControllableState,Jh=(t,e)=>window.G7Core?.t?.(t,e)??t,PS={"":"common.scope_type_all_desc",self:"common.scope_type_self_desc",role:"common.scope_type_role_desc"},$S=({options:t,value:e,onChange:n,disabled:r=!1})=>{const[s,o]=v.useState(!1),[i,a]=v.useState({}),l=v.useRef(null),c=t.find(u=>u.value===e)??t[0];v.useEffect(()=>{if(!s)return;const u=g=>{l.current&&!l.current.contains(g.target)&&o(!1)},f=()=>{if(!l.current)return;const g=l.current.getBoundingClientRect();a({position:"fixed",top:g.bottom+4,right:window.innerWidth-g.right,zIndex:9999})};return document.addEventListener("mousedown",u),window.addEventListener("scroll",f,!0),window.addEventListener("resize",f),()=>{document.removeEventListener("mousedown",u),window.removeEventListener("scroll",f,!0),window.removeEventListener("resize",f)}},[s]);const d=v.useCallback(()=>{if(r||!l.current)return;const u=l.current.getBoundingClientRect();a({position:"fixed",top:u.bottom+4,right:window.innerWidth-u.right,zIndex:9999}),o(f=>!f)},[r]);return h.jsxs(I,{ref:l,className:"relative flex-shrink-0",children:[h.jsxs(I,{role:"button",tabIndex:0,onClick:u=>{u.stopPropagation(),u.preventDefault(),d()},onMouseDown:u=>u.stopPropagation(),onKeyDown:u=>{(u.key==="Enter"||u.key===" ")&&(u.stopPropagation(),u.preventDefault(),d()),u.key==="Escape"&&o(!1)},className:`flex items-center gap-1 text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 cursor-pointer select-none ${r?"opacity-50 cursor-not-allowed":"hover:border-blue-400 dark:hover:border-blue-500"}`,children:[h.jsx(Q,{children:c?.label??""}),h.jsx(re,{name:de.ChevronDown,className:"w-3 h-3 text-gray-400 dark:text-gray-500"})]}),s&&h.jsx(I,{style:i,className:"w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1",children:t.map(u=>{const f=u.value??"",g=PS[f],m=g?Jh(g):"",p=u.value===e;return h.jsxs(I,{role:"option","aria-selected":p,onClick:y=>{y.stopPropagation(),y.preventDefault(),n(u.value),o(!1)},onMouseDown:y=>y.stopPropagation(),className:`px-3 py-2 cursor-pointer transition-colors ${p?"bg-blue-50 dark:bg-blue-900/30":"hover:bg-gray-50 dark:hover:bg-gray-700/50"}`,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[h.jsx(Q,{className:`text-xs font-medium ${p?"text-blue-700 dark:text-blue-300":"text-gray-900 dark:text-white"}`,children:u.label}),p&&h.jsx(re,{name:de.Check,className:"w-3.5 h-3.5 text-blue-600 dark:text-blue-400"})]}),m&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:m})]},f||"__null__")})})]})},FS=(t,e)=>{if(t.length!==e.length)return!1;const n=[...t].sort((s,o)=>s.id-o.id),r=[...e].sort((s,o)=>s.id-o.id);return n.every((s,o)=>s.id===r[o].id&&s.scope_type===r[o].scope_type)},zS=({data:t=[],value:e=[],onChange:n,scopeOptions:r=[],className:s="",disabled:o=!1,desktopColumns:i=2,inst Line truncated
`,children:[h.jsx(re,{name:x.faIcon,className:"w-4 h-4"}),h.jsx(Q,{children:x.label}),y?.faIcon===x.faIcon&&h.jsx(re,{name:"check",className:"w-4 h-4 ml-auto text-blue-600"})]},x.value)):h.jsx(I,{className:"px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center",children:o})})]})]})},sc=({defaultOpen:t=!1,isOpen:e,onToggle:n,className:r="",style:s,children:o,disabled:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(t),u=e!==void 0,f=u?e:c,g=v.useRef(null),[m,p]=v.useState(t?void 0:0),y=()=>{if(i)return;const S=!f;u||d(S),n?.(S)};v.useEffect(()=>{if(g.current)if(f){p(g.current.scrollHeight);const S=setTimeout(()=>{p(void 0)},300);return()=>clearTimeout(S)}else p(g.current.scrollHeight),requestAnimationFrame(()=>{p(0)})},[f]);const b={};if(v.Children.forEach(o,S=>{if(v.isValidElement(S)){const x=S.props?.slot;x==="trigger"?b.trigger=S:x==="content"&&(b.content=S)}}),!b.trigger&&!b.content){const S=v.Children.toArray(o);S.length>=2?(b.trigger=S[0],b.content=S.slice(1)):S.length===1&&(b.content=S[0])}return h.jsxs(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${r}`,style:s,id:a,...l,children:[h.jsxs(oe,{type:"button",onClick:y,disabled:i,className:`w-full flex items-center justify-between p-3 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${i?"opacity-50 cursor-not-allowed":""} ${f?"border-b border-gray-200 dark:border-gray-700":""}`,"aria-expanded":f,children:[h.jsx(I,{className:"flex-1",children:b.trigger}),!b.trigger&&h.jsx("svg",{className:`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),h.jsx(I,{ref:g,className:"overflow-hidden transition-all duration-300 ease-in-out",style:{height:m!==void 0?`${m}px`:"auto"},children:h.jsx(I,{className:"bg-white dark:bg-gray-800",children:b.content})})]})},Js=window.G7Core,Jh=Js?.useControllableState,Rh=(t,e)=>window.G7Core?.t?.(t,e)??t,zS={"":"common.scope_type_all_desc",self:"common.scope_type_self_desc",role:"common.scope_type_role_desc"},VS=({options:t,value:e,onChange:n,disabled:r=!1})=>{const[s,o]=v.useState(!1),[i,a]=v.useState({}),l=v.useRef(null),c=t.find(u=>u.value===e)??t[0];v.useEffect(()=>{if(!s)return;const u=g=>{l.current&&!l.current.contains(g.target)&&o(!1)},f=()=>{if(!l.current)return;const g=l.current.getBoundingClientRect();a({position:"fixed",top:g.bottom+4,right:window.innerWidth-g.right,zIndex:9999})};return document.addEventListener("mousedown",u),window.addEventListener("scroll",f,!0),window.addEventListener("resize",f),()=>{document.removeEventListener("mousedown",u),window.removeEventListener("scroll",f,!0),window.removeEventListener("resize",f)}},[s]);const d=v.useCallback(()=>{if(r||!l.current)return;const u=l.current.getBoundingClientRect();a({position:"fixed",top:u.bottom+4,right:window.innerWidth-u.right,zIndex:9999}),o(f=>!f)},[r]);return h.jsxs(I,{ref:l,className:"relative flex-shrink-0",children:[h.jsxs(I,{role:"button",tabIndex:0,onClick:u=>{u.stopPropagation(),u.preventDefault(),d()},onMouseDown:u=>u.stopPropagation(),onKeyDown:u=>{(u.key==="Enter"||u.key===" ")&&(u.stopPropagation(),u.preventDefault(),d()),u.key==="Escape"&&o(!1)},className:`flex items-center gap-1 text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 cursor-pointer select-none ${r?"opacity-50 cursor-not-allowed":"hover:border-blue-400 dark:hover:border-blue-500"}`,children:[h.jsx(Q,{children:c?.label??""}),h.jsx(re,{name:de.ChevronDown,className:"w-3 h-3 text-gray-400 dark:text-gray-500"})]}),s&&h.jsx(I,{style:i,className:"w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1",children:t.map(u=>{const f=u.value??"",g=zS[f],m=g?Rh(g):"",p=u.value===e;return h.jsxs(I,{role:"option","aria-selected":p,onClick:y=>{y.stopPropagation(),y.preventDefault(),n(u.value),o(!1)},onMouseDown:y=>y.stopPropagation(),className:`px-3 py-2 cursor-pointer transition-colors ${p?"bg-blue-50 dark:bg-blue-900/30":"hover:bg-gray-50 dark:hover:bg-gray-700/50"}`,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[h.jsx(Q,{className:`text-xs font-medium ${p?"text-blue-700 dark:text-blue-300":"text-gray-900 dark:text-white"}`,children:u.label}),p&&h.jsx(re,{name:de.Check,className:"w-3.5 h-3.5 text-blue-600 dark:text-blue-400"})]}),m&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:m})]},f||"__null__")})})]})},HS=(t,e)=>{if(t.length!==e.length)return!1;const n=[...t].sort((s,o)=>s.id-o.id),r=[...e].sort((s,o)=>s.id-o.id);return n.every((s,o)=>s.id===r[o].id&&s.scope_type===r[o].scope_type)},US=({data:t=[],value:e=[],onChange:n,scopeOptions:r=[],className:s="",disabled:o=!1,desktopColumns:i=2,inst Line truncated
let scriptImported = false
self.addEventListener('message', async (e) => {
const { file, id, imageCompressionLibUrl, options } = e.data
@@ -220,11 +220,11 @@ self.addEventListener('message', async (e) => {
self.postMessage({ error: e.message + '\\n' + e.stack, id })
}
})
`;let Ic;function rE(t,e){return new Promise(((n,r)=>{Ic||(Ic=(function(i){const a=[];return a.push(i),URL.createObjectURL(new Blob(a))})(nE));const s=new Worker(Ic);s.addEventListener("message",(function(i){if(e.signal&&e.signal.aborted)s.terminate();else if(i.data.progress===void 0){if(i.data.error)return r(new Error(i.data.error)),void s.terminate();n(i.data.file),s.terminate()}else e.onProgress(i.data.progress)})),s.addEventListener("error",r),e.signal&&e.signal.addEventListener("abort",(()=>{r(e.signal.reason),s.terminate()})),s.postMessage({file:t,imageCompressionLibUrl:e.libURL,options:{...e,onProgress:void 0,signal:void 0}})}))}function Vt(t,e){return new Promise((function(n,r){let s,o,i,a,l,c;if(s={...e},i=0,{onProgress:a}=s,s.maxSizeMB=s.maxSizeMB||Number.POSITIVE_INFINITY,l=typeof s.useWebWorker!="boolean"||s.useWebWorker,delete s.useWebWorker,s.onProgress=g=>{i=g,typeof a=="function"&&a(i)},!(t instanceof Blob||t instanceof tE))return r(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(t.type))return r(new Error("The file given is not an image"));if(c=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!l||typeof Worker!="function"||c)return Cp(t,s).then(function(g){try{return o=g,f.call(this)}catch(m){return r(m)}}.bind(this),r);var d=function(){try{return f.call(this)}catch(g){return r(g)}}.bind(this),u=function(g){try{return Cp(t,s).then((function(m){try{return o=m,d()}catch(p){return r(p)}}),r)}catch(m){return r(m)}};try{return s.libURL=s.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",rE(t,s).then((function(g){try{return o=g,d()}catch{return u()}}),u)}catch{u()}function f(){try{o.name=t.name,o.lastModified=t.lastModified}catch{}try{s.preserveExif&&t.type==="image/jpeg"&&(!s.fileType||s.fileType&&s.fileType===t.type)&&(o=fp(t,o))}catch{}return n(o)}}))}Vt.getDataUrlFromFile=mp,Vt.getFilefromDataUrl=Dc,Vt.loadImage=bp,Vt.drawImageInCanvas=vp,Vt.drawFileInCanvas=Xi,Vt.canvasToFile=Ki,Vt.getExifOrientation=xp,Vt.handleMaxWidthOrHeight=wp,Vt.followExifOrientation=kp,Vt.cleanupCanvasMemory=Pn,Vt.isAutoOrientationInBrowser=is,Vt.approximateBelowMaximumCanvasSizeOfBrowser=yp,Vt.copyExifWithoutOrientation=fp,Vt.getBrowserName=wr,Vt.version="2.0.2";const sE=window.G7Core,Lc=t=>{const e=["B","KB","MB","GB"];let n=0,r=t;for(;r>=1024&&n<e.length-1;)r/=1024,n++;return`${r.toFixed(2)} ${e[n]}`},oE=t=>{if(t&&typeof t=="object"){const e=t;if(e.response?.data?.message)return e.response.data.message;if(e.response?.data?.errors){const n=Object.values(e.response.data.errors)[0];if(Array.isArray(n)&&n[0])return n[0]}if(e.message)return e.message}return t instanceof Error?t.message:"업로드 실패"},iE=t=>t?t.startsWith("image/")?"fa-file-image":t.startsWith("video/")?"fa-file-video":t.startsWith("audio/")?"fa-file-audio":t==="application/pdf"?"fa-file-pdf":t.includes("word")||t.includes("document")?"fa-file-word":t.includes("excel")||t.includes("spreadsheet")?"fa-file-excel":t.includes("zip")||t.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",At=(t,e)=>sE?.t?.(t,e)??t,Sp=window.G7Core?.createLogger?.("Comp:FileUploader")??{log:(...t)=>console.log("[Comp:FileUploader]",...t),warn:(...t)=>console.warn("[Comp:FileUploader]",...t),error:(...t)=>console.error("[Comp:FileUploader]",...t)},kr=window.G7Core;function aE(t){const{attachmentableType:e,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:u,endpoints:f,onFilesChange:g,onUploadComplete:m,onUploadError:p,onRemove:y,onReorder:b,uploadTriggerEvent:S,confirmBeforeRemove:x,uploadParams:E}=t,[k,C]=v.useState(u),[N,_]=v.useState([]),[w,M]=v.useState(!1),O=v.useRef(null),L=v.useRef(0),F=v.useRef(new Set),$=v.useRef(new Map),D=v.useRef(null),[A,T]=v.useState(!1),[z,H]=v.useState(null),[W,j]=v.useState(!1),U=v.useRef(!1),[B,G]=v.useState([]),[V,K]=v.useState(!1),[X,Z]=v.useState(0),ee=v.useRef(0),[ne,he]=v.useState(new Map),J=v.useRef(new Map),ce=v.useRef(g);ce.current=g,v.useEffect(()=>{ce.current?.(N)},[N]),v.useEffect(()=>{if(U.current)return;const te=u.filter(me=>!F.current.has(me.id)&&(!me.hash||!F.current.has(me.hash))),ge=new Set(te.map(me=>me.id)),pe=Array.from($.current.values()).filter(me=>!ge.has(me.id)&&!F.current.has(me.id));C([...te,...pe])},[u]);const ue=v.useCallback(async te=>{const ge={maxSizeMB:l?.maxSizeMB??1,maxWidthOrHeight:l?.maxWidthOrHeight??1920,useWebWorker:!0};return await Vt(te,ge)},[l]),ae=v.useCallback(async te=>{const ge=k.length+N.length,pe=Math.max(0,s-ge),me=Array.from(te).slice(0,pe);if(te.length>pe){const be=Array.from(te).slice(pe),Be=At("attachment.upload_limit_exceeded",{max:s});p?.(Be,be[0]??te[0])}for(const be of me){if(be.size>o*1024*1024){p?.(`[${be.name}] 파일 크기(${Lc(be.size)})가 최대 허용 크기(${o}MB)를 초과합니다.`,be);continue}if(a){const Se="."+be.name.split(".").pop()?.toLowerCase();if(!a.split(",").m Line truncated
`;let Lc;function iE(t,e){return new Promise(((n,r)=>{Lc||(Lc=(function(i){const a=[];return a.push(i),URL.createObjectURL(new Blob(a))})(oE));const s=new Worker(Lc);s.addEventListener("message",(function(i){if(e.signal&&e.signal.aborted)s.terminate();else if(i.data.progress===void 0){if(i.data.error)return r(new Error(i.data.error)),void s.terminate();n(i.data.file),s.terminate()}else e.onProgress(i.data.progress)})),s.addEventListener("error",r),e.signal&&e.signal.addEventListener("abort",(()=>{r(e.signal.reason),s.terminate()})),s.postMessage({file:t,imageCompressionLibUrl:e.libURL,options:{...e,onProgress:void 0,signal:void 0}})}))}function Vt(t,e){return new Promise((function(n,r){let s,o,i,a,l,c;if(s={...e},i=0,{onProgress:a}=s,s.maxSizeMB=s.maxSizeMB||Number.POSITIVE_INFINITY,l=typeof s.useWebWorker!="boolean"||s.useWebWorker,delete s.useWebWorker,s.onProgress=g=>{i=g,typeof a=="function"&&a(i)},!(t instanceof Blob||t instanceof sE))return r(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(t.type))return r(new Error("The file given is not an image"));if(c=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!l||typeof Worker!="function"||c)return Sp(t,s).then(function(g){try{return o=g,f.call(this)}catch(m){return r(m)}}.bind(this),r);var d=function(){try{return f.call(this)}catch(g){return r(g)}}.bind(this),u=function(g){try{return Sp(t,s).then((function(m){try{return o=m,d()}catch(p){return r(p)}}),r)}catch(m){return r(m)}};try{return s.libURL=s.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",iE(t,s).then((function(g){try{return o=g,d()}catch{return u()}}),u)}catch{u()}function f(){try{o.name=t.name,o.lastModified=t.lastModified}catch{}try{s.preserveExif&&t.type==="image/jpeg"&&(!s.fileType||s.fileType&&s.fileType===t.type)&&(o=hp(t,o))}catch{}return n(o)}}))}Vt.getDataUrlFromFile=bp,Vt.getFilefromDataUrl=Ic,Vt.loadImage=yp,Vt.drawImageInCanvas=xp,Vt.drawFileInCanvas=Xi,Vt.canvasToFile=Ki,Vt.getExifOrientation=wp,Vt.handleMaxWidthOrHeight=kp,Vt.followExifOrientation=Cp,Vt.cleanupCanvasMemory=Pn,Vt.isAutoOrientationInBrowser=is,Vt.approximateBelowMaximumCanvasSizeOfBrowser=vp,Vt.copyExifWithoutOrientation=hp,Vt.getBrowserName=wr,Vt.version="2.0.2";const aE=window.G7Core,Pc=t=>{const e=["B","KB","MB","GB"];let n=0,r=t;for(;r>=1024&&n<e.length-1;)r/=1024,n++;return`${r.toFixed(2)} ${e[n]}`},lE=t=>{if(t&&typeof t=="object"){const e=t;if(e.response?.data?.message)return e.response.data.message;if(e.response?.data?.errors){const n=Object.values(e.response.data.errors)[0];if(Array.isArray(n)&&n[0])return n[0]}if(e.message)return e.message}return t instanceof Error?t.message:"업로드 실패"},cE=t=>t?t.startsWith("image/")?"fa-file-image":t.startsWith("video/")?"fa-file-video":t.startsWith("audio/")?"fa-file-audio":t==="application/pdf"?"fa-file-pdf":t.includes("word")||t.includes("document")?"fa-file-word":t.includes("excel")||t.includes("spreadsheet")?"fa-file-excel":t.includes("zip")||t.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",At=(t,e)=>aE?.t?.(t,e)??t,_p=window.G7Core?.createLogger?.("Comp:FileUploader")??{log:(...t)=>console.log("[Comp:FileUploader]",...t),warn:(...t)=>console.warn("[Comp:FileUploader]",...t),error:(...t)=>console.error("[Comp:FileUploader]",...t)},kr=window.G7Core;function dE(t){const{attachmentableType:e,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:u,endpoints:f,onFilesChange:g,onUploadComplete:m,onUploadError:p,onRemove:y,onReorder:b,uploadTriggerEvent:S,confirmBeforeRemove:x,uploadParams:E}=t,[k,C]=v.useState(u),[N,_]=v.useState([]),[w,M]=v.useState(!1),O=v.useRef(null),L=v.useRef(0),F=v.useRef(new Set),$=v.useRef(new Map),D=v.useRef(null),[A,T]=v.useState(!1),[z,H]=v.useState(null),[W,j]=v.useState(!1),U=v.useRef(!1),[B,G]=v.useState([]),[V,K]=v.useState(!1),[X,Z]=v.useState(0),ee=v.useRef(0),[ne,he]=v.useState(new Map),J=v.useRef(new Map),ce=v.useRef(g);ce.current=g,v.useEffect(()=>{ce.current?.(N)},[N]),v.useEffect(()=>{if(U.current)return;const te=u.filter(me=>!F.current.has(me.id)&&(!me.hash||!F.current.has(me.hash))),ge=new Set(te.map(me=>me.id)),pe=Array.from($.current.values()).filter(me=>!ge.has(me.id)&&!F.current.has(me.id));C([...te,...pe])},[u]);const ue=v.useCallback(async te=>{const ge={maxSizeMB:l?.maxSizeMB??1,maxWidthOrHeight:l?.maxWidthOrHeight??1920,useWebWorker:!0};return await Vt(te,ge)},[l]),ae=v.useCallback(async te=>{const ge=k.length+N.length,pe=Math.max(0,s-ge),me=Array.from(te).slice(0,pe);if(te.length>pe){const be=Array.from(te).slice(pe),Be=At("attachment.upload_limit_exceeded",{max:s});p?.(Be,be[0]??te[0])}for(const be of me){if(be.size>o*1024*1024){p?.(`[${be.name}] 파일 크기(${Pc(be.size)})가 최대 허용 크기(${o}MB)를 초과합니다.`,be);continue}if(a){const Se="."+be.name.split(".").pop()?.toLowerCase();if(!a.split(",").m Line truncated
relative border-2 border-dashed rounded-lg transition-colors
${l?"opacity-60 cursor-not-allowed":""}
${t&&!l?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 dark:border-gray-600"}
`,onDragOver:u=>{u.preventDefault(),d&&r(!0)},onDragLeave:()=>r(!1),onDrop:u=>{u.preventDefault(),r(!1),d&&n(u.dataTransfer.files)},children:[h.jsx("input",{ref:s,type:"file",multiple:!0,accept:o,onChange:u=>u.target.files&&!l&&n(u.target.files),className:"hidden",disabled:l}),!c&&h.jsxs(I,{className:`p-8 text-center transition-colors ${l?"cursor-not-allowed":"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,onClick:()=>!l&&s.current?.click(),children:[h.jsx(pt,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),h.jsx(bt,{className:"text-sm text-gray-600 dark:text-gray-400",children:At("attachment.drop_or_click")}),h.jsxs(bt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[At("attachment.upload_limit",{maxFiles:i,maxSize:a}),o&&` (${o})`]})]}),c]})},cE=window.G7Core,dE=window.G7Core?.createLogger?.("Comp:SortableThumbnail")??{log:(...t)=>console.log("[Comp:SortableThumbnail]",...t),warn:(...t)=>console.warn("[Comp:SortableThumbnail]",...t),error:(...t)=>console.error("[Comp:SortableThumbnail]",...t)},uE=({file:t,onRemove:e,onRetry:n,onImageClick:r,onDownload:s,isPrimary:o,onPrimaryClick:i,disabled:a})=>{const{attributes:l,listeners:c,setNodeRef:d,transform:u,transition:f,isDragging:g}=Di({id:"hash"in t?t.hash:t.id}),m={transform:Ln.Transform.toString(u),transition:f,opacity:g?.5:1},p="status"in t&&t.status!==void 0,y=p?t.status:"done",b=y==="uploading",S=y==="compressing",x=y==="error",E=p?t.progress:100,k=p?t.preview:void 0,C=p?t.mime_type?.startsWith("image/")??!1:t.is_image,[N,_]=v.useState(void 0),w=!p&&C?t.download_url:void 0;v.useEffect(()=>{if(!w){_(void 0);return}let O=!0,L;return(async()=>{try{const $=await cE.api.get(w,{responseType:"blob"});O&&$&&(L=URL.createObjectURL($),_(L))}catch($){dE.error("Failed to load authenticated image:",$)}})(),()=>{O=!1,L&&URL.revokeObjectURL(L)}},[w]);const M=N;return h.jsxs(I,{ref:d,style:m,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[h.jsxs(I,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${C||!p?"cursor-pointer":""}`,onClick:()=>{b||S||(C&&r?r():!p&&s&&s())},children:[k||M?h.jsx(yn,{src:k||M,alt:t.original_filename,className:"w-full h-full object-cover"}):h.jsx(pt,{className:`fa-solid ${iE(t.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(b||S)&&h.jsxs(I,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[h.jsx(I,{className:"w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden",children:h.jsx(I,{className:"h-full bg-blue-500 transition-all",style:{width:`${E}%`}})}),h.jsxs(Q,{className:"text-white text-xs mt-1",children:[E,"%"]})]}),x&&h.jsx(I,{className:"absolute inset-0 bg-red-500/50 flex items-center justify-center",children:h.jsx(pt,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),i&&h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),i()},className:`absolute top-1.5 left-1.5 z-10 p-1 rounded-full transition-colors ${o?"bg-yellow-500 text-white":"bg-gray-800/50 text-gray-300 hover:bg-yellow-500 hover:text-white opacity-0 group-hover:opacity-100"}`,title:At(o?"attachment.primary_image":"attachment.set_as_primary"),children:h.jsx(pt,{className:"fa-solid fa-star text-xs"})}),h.jsxs(I,{className:`absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-start justify-end p-1 gap-1 opacity-0 group-hover:opacity-100 ${a?"hidden":""}`,children:[h.jsx(oe,{type:"button",...l,...c,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:At("attachment.drag_to_reorder"),onClick:O=>O.stopPropagation(),children:h.jsx(pt,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&s&&h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),s()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:At("common.download"),children:h.jsx(pt,{className:"fa-solid fa-download text-sm"})}),x&&n&&h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),n()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:At("common.retry"),children:h.jsx(pt,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),e()},disabled:b||S,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:At("common.delete"),children:h.jsx(pt,{className:"fa-solid fa-times text-sm"})})]})]}),h.jsxs(I,{className:`p-2 ${!p&&s?"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700":""}`,onClick:()=>{!p&&s&&s()},children:[h.jsx(bt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:t.original_filename,children:t.original_filename}),h.jsx(bt,{className:"text-xs text-gray-500 dark:text-gray-400",children:t.siz Line truncated
`,onDragOver:u=>{u.preventDefault(),d&&r(!0)},onDragLeave:()=>r(!1),onDrop:u=>{u.preventDefault(),r(!1),d&&n(u.dataTransfer.files)},children:[h.jsx("input",{ref:s,type:"file",multiple:!0,accept:o,onChange:u=>u.target.files&&!l&&n(u.target.files),className:"hidden",disabled:l}),!c&&h.jsxs(I,{className:`p-8 text-center transition-colors ${l?"cursor-not-allowed":"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,onClick:()=>!l&&s.current?.click(),children:[h.jsx(pt,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),h.jsx(bt,{className:"text-sm text-gray-600 dark:text-gray-400",children:At("attachment.drop_or_click")}),h.jsxs(bt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[At("attachment.upload_limit",{maxFiles:i,maxSize:a}),o&&` (${o})`]})]}),c]})},fE=window.G7Core,hE=window.G7Core?.createLogger?.("Comp:SortableThumbnail")??{log:(...t)=>console.log("[Comp:SortableThumbnail]",...t),warn:(...t)=>console.warn("[Comp:SortableThumbnail]",...t),error:(...t)=>console.error("[Comp:SortableThumbnail]",...t)},gE=({file:t,onRemove:e,onRetry:n,onImageClick:r,onDownload:s,isPrimary:o,onPrimaryClick:i,disabled:a})=>{const{attributes:l,listeners:c,setNodeRef:d,transform:u,transition:f,isDragging:g}=Di({id:"hash"in t?t.hash:t.id}),m={transform:Ln.Transform.toString(u),transition:f,opacity:g?.5:1},p="status"in t&&t.status!==void 0,y=p?t.status:"done",b=y==="uploading",S=y==="compressing",x=y==="error",E=p?t.progress:100,k=p?t.preview:void 0,C=p?t.mime_type?.startsWith("image/")??!1:t.is_image,[N,_]=v.useState(void 0),w=!p&&C?t.download_url:void 0;v.useEffect(()=>{if(!w){_(void 0);return}let O=!0,L;return(async()=>{try{const $=await fE.api.get(w,{responseType:"blob"});O&&$&&(L=URL.createObjectURL($),_(L))}catch($){hE.error("Failed to load authenticated image:",$)}})(),()=>{O=!1,L&&URL.revokeObjectURL(L)}},[w]);const M=N;return h.jsxs(I,{ref:d,style:m,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[h.jsxs(I,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${C||!p?"cursor-pointer":""}`,onClick:()=>{b||S||(C&&r?r():!p&&s&&s())},children:[k||M?h.jsx(yn,{src:k||M,alt:t.original_filename,className:"w-full h-full object-cover"}):h.jsx(pt,{className:`fa-solid ${cE(t.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(b||S)&&h.jsxs(I,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[h.jsx(I,{className:"w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden",children:h.jsx(I,{className:"h-full bg-blue-500 transition-all",style:{width:`${E}%`}})}),h.jsxs(Q,{className:"text-white text-xs mt-1",children:[E,"%"]})]}),x&&h.jsx(I,{className:"absolute inset-0 bg-red-500/50 flex items-center justify-center",children:h.jsx(pt,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),i&&h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),i()},className:`absolute top-1.5 left-1.5 z-10 p-1 rounded-full transition-colors ${o?"bg-yellow-500 text-white":"bg-gray-800/50 text-gray-300 hover:bg-yellow-500 hover:text-white opacity-0 group-hover:opacity-100"}`,title:At(o?"attachment.primary_image":"attachment.set_as_primary"),children:h.jsx(pt,{className:"fa-solid fa-star text-xs"})}),h.jsxs(I,{className:`absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-start justify-end p-1 gap-1 opacity-0 group-hover:opacity-100 ${a?"hidden":""}`,children:[h.jsx(oe,{type:"button",...l,...c,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:At("attachment.drag_to_reorder"),onClick:O=>O.stopPropagation(),children:h.jsx(pt,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&s&&h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),s()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:At("common.download"),children:h.jsx(pt,{className:"fa-solid fa-download text-sm"})}),x&&n&&h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),n()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:At("common.retry"),children:h.jsx(pt,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),h.jsx(oe,{type:"button",onClick:O=>{O.stopPropagation(),e()},disabled:b||S,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:At("common.delete"),children:h.jsx(pt,{className:"fa-solid fa-times text-sm"})})]})]}),h.jsxs(I,{className:`p-2 ${!p&&s?"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700":""}`,onClick:()=>{!p&&s&&s()},children:[h.jsx(bt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:t.original_filename,children:t.original_filename}),h.jsx(bt,{className:"text-xs text-gray-500 dark:text-gray-400",children:t.siz Line truncated
${y.radio}
text-blue-600
bg-gray-100 dark:bg-gray-700
@@ -237,10 +237,10 @@ self.addEventListener('message', async (e) => {
${y.label}
font-medium
text-gray-900 dark:text-gray-300
`,children:S.label})]},S.value)})}),c&&h.jsx(Q,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:c})]})},as=()=>window.G7Core,qi=(t,e)=>as()?.t?.(t,e)??t,gE=()=>as()?.locale?.supported?.()??["ko","en"],pE=()=>as()?.locale?.current?.()??"ko",Ep=t=>{const e=qi(`common.language_${t}`);return e===`common.language_${t}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[t]||t.toUpperCase():e},mE=({content:t="",value:e,onChange:n,multilingual:r=!1,isHtml:s=!1,onIsHtmlChange:o,rows:i=15,placeholder:a="",label:l,showHtmlModeToggle:c=!0,contentClassName:d="",purifyConfig:u,className:f="",name:g="content",htmlFieldName:m="content_mode",readOnly:p=!1,id:y,editorAttrs:b})=>{const S=v.useMemo(()=>gE(),[]),x=v.useMemo(()=>pE(),[]),[E,k]=v.useState(x),[C,N]=v.useState(s),[_,w]=v.useState(t),[M,O]=v.useState(e??{}),[L,F]=v.useState(!1);v.useEffect(()=>{N(s)},[s]),v.useEffect(()=>{r||w(t)},[t,r]),v.useEffect(()=>{r&&e&&O(e)},[e,r]);const $=v.useCallback(j=>{const U=j.target.value;if(w(U),n){const B=as()?.createChangeEvent?.({value:U,name:g,type:"textarea"})??{target:{name:g,value:U}};n(B)}},[n,g]),D=v.useCallback((j,U)=>{const B={...M,[j]:U};if(O(B),n){const G=as()?.createChangeEvent?.({value:B,name:g,type:"multilingual"})??{target:{name:g,value:B}};n(G)}},[n,g,M]),A=v.useCallback(j=>{const U=j.target.checked;if(N(U),U||F(!1),o){const B=as()?.createChangeEvent?.({checked:U,name:m,type:"checkbox"})??{target:{name:m,checked:U}};o(B)}},[o,m]),T=v.useCallback(()=>{F(j=>!j)},[]),z=r?M[E]??"":_,H=v.useCallback(j=>!!M[j]?.trim(),[M]),W=()=>h.jsx(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:S.map(j=>{const U=j===x,B=E===j;return h.jsxs(oe,{type:"button",className:`
`,children:S.label})]},S.value)})}),c&&h.jsx(Q,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:c})]})},as=()=>window.G7Core,qi=(t,e)=>as()?.t?.(t,e)??t,bE=()=>as()?.locale?.supported?.()??["ko","en"],yE=()=>as()?.locale?.current?.()??"ko",Mp=t=>{const e=qi(`common.language_${t}`);return e===`common.language_${t}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[t]||t.toUpperCase():e},vE=({content:t="",value:e,onChange:n,multilingual:r=!1,isHtml:s=!1,onIsHtmlChange:o,rows:i=15,placeholder:a="",label:l,showHtmlModeToggle:c=!0,contentClassName:d="",purifyConfig:u,className:f="",name:g="content",htmlFieldName:m="content_mode",readOnly:p=!1,id:y,editorAttrs:b})=>{const S=v.useMemo(()=>bE(),[]),x=v.useMemo(()=>yE(),[]),[E,k]=v.useState(x),[C,N]=v.useState(s),[_,w]=v.useState(t),[M,O]=v.useState(e??{}),[L,F]=v.useState(!1);v.useEffect(()=>{N(s)},[s]),v.useEffect(()=>{r||w(t)},[t,r]),v.useEffect(()=>{r&&e&&O(e)},[e,r]);const $=v.useCallback(j=>{const U=j.target.value;if(w(U),n){const B=as()?.createChangeEvent?.({value:U,name:g,type:"textarea"})??{target:{name:g,value:U}};n(B)}},[n,g]),D=v.useCallback((j,U)=>{const B={...M,[j]:U};if(O(B),n){const G=as()?.createChangeEvent?.({value:B,name:g,type:"multilingual"})??{target:{name:g,value:B}};n(G)}},[n,g,M]),A=v.useCallback(j=>{const U=j.target.checked;if(N(U),U||F(!1),o){const B=as()?.createChangeEvent?.({checked:U,name:m,type:"checkbox"})??{target:{name:m,checked:U}};o(B)}},[o,m]),T=v.useCallback(()=>{F(j=>!j)},[]),z=r?M[E]??"":_,H=v.useCallback(j=>!!M[j]?.trim(),[M]),W=()=>h.jsx(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:S.map(j=>{const U=j===x,B=E===j;return h.jsxs(oe,{type:"button",className:`
inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold transition-all
${B?U?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":U?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"}
`,onClick:()=>k(j),children:[h.jsx(re,{name:"globe",className:"w-3 h-3"}),h.jsx(Q,{children:Ep(j)}),U&&h.jsx(Q,{className:"text-red-500",children:"*"}),H(j)&&!B&&h.jsx(re,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"})]},j)})});return h.jsxs(I,{className:`space-y-2 ${f}`,id:y,...b,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[l&&h.jsx(rn,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:l}),h.jsxs(I,{className:"flex items-center gap-3",children:[C&&!p&&h.jsx(oe,{type:"button",onClick:T,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${L?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:qi(L?"common.preview_off":"common.preview")}),c&&h.jsxs(rn,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[h.jsx(wt,{type:"checkbox",name:m,checked:C,onChange:A,disabled:p,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(I,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:qi("common.html_mode")})]})]})]}),r&&W(),!L&&(r?S.map(j=>h.jsx(I,{className:E===j?"block":"hidden",children:h.jsx(Zn,{name:`${g}[${j}]`,value:M[j]??"",onChange:U=>D(j,U.target.value),placeholder:`${a} (${Ep(j)})`,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${C?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})},j)):h.jsx(Zn,{name:g,value:_,onChange:$,placeholder:a,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${C?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})),L&&h.jsx(I,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:h.jsx(qa,{content:z,isHtml:!0,className:d||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:u})})]})},Mp="g7_dismissed_warnings";function Np(){try{const t=sessionStorage.getItem(Mp);return t?JSON.parse(t):[]}catch{return[]}}function bE(t){try{const e=Np();e.includes(t)||(e.push(t),sessionStorage.setItem(Mp,JSON.stringify(e)))}catch{}}function yE(t){switch(t){case"error":return"error";case"warning":return"warning";default:return"info"}}const vE=({warnings:t,className:e=""})=>{const[n,r]=v.useState([]);v.useEffect(()=>{r(Np())},[]);const s=v.useCallback(i=>{bE(i),r(a=>[...a,i])},[]);if(!t||t.length===0)return null;const o=t.filter(i=>!n.includes(i.id));return o.length===0?null:h.jsx(I,{className:`space-y-2 ${e}`.trim(),children:o.map(i=>h.jsx(lu,{type:yE(i.level),message:i.message,dismissible:!0,onDismiss:()=>s(i.id)},i.id))})},Op=window.G7Core?.createLogger?.("Comp:FilterVisibility")??{log:(...t)=>console.log("[Comp:FilterVisibility]",...t),warn:(...t)=>console.warn("[Comp:FilterVisibility]",...t),error:(...t)=>console.error("[Comp:FilterVisibility]",...t)},xE=({id:t,visibleFilters:e,defaultFilters:n=[],onFilterVisibilityChange:r})=>{const[s,o]=v.useState(!1),i=v.useRef(!0),a=v.useMemo(()=>{try{const f=window.G7Core?.AuthManager?.getInstance();return f?f.getUser()?.uuid:void 0}catch{return}},[]),l=v.useMemo(()=>`g7_filters_${t}${a!==void 0?`_${a}`:""}`,[t,a]),c=v.useCallback(()=>{try{const f=localStorage.getItem(l);if(f){const g=JSON.parse(f);if(Array.isArray(g))return g}}catch(f){Op.warn("Failed to load from localStorage:",f)}return null},[l]),d=v.useCallback(f=>{try{localStorage.setItem(l,JSON.stringify(f))}catch(g){Op.warn("Failed to save to localStorage:",g)}},[l]),u=v.useCallback(()=>{const f=c();return f||(e&&e.length>0?e:n)},[c,e,n]);return v.useEffect(()=>{if(!s){const f=u();r&&f.length>0&&r(f),o(!0)}},[s,r,u]),v.useEffect(()=>{if(s&&e&&e.length>0){if(i.current){i.current=!1;return}d(e)}},[s,e,d]),null},wE=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...t)=>console.log( Line truncated
`,onClick:()=>k(j),children:[h.jsx(re,{name:"globe",className:"w-3 h-3"}),h.jsx(Q,{children:Mp(j)}),U&&h.jsx(Q,{className:"text-red-500",children:"*"}),H(j)&&!B&&h.jsx(re,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"})]},j)})});return h.jsxs(I,{className:`space-y-2 ${f}`,id:y,...b,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[l&&h.jsx(rn,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:l}),h.jsxs(I,{className:"flex items-center gap-3",children:[C&&!p&&h.jsx(oe,{type:"button",onClick:T,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${L?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:qi(L?"common.preview_off":"common.preview")}),c&&h.jsxs(rn,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[h.jsx(wt,{type:"checkbox",name:m,checked:C,onChange:A,disabled:p,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(I,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:qi("common.html_mode")})]})]})]}),r&&W(),!L&&(r?S.map(j=>h.jsx(I,{className:E===j?"block":"hidden",children:h.jsx(Zn,{name:`${g}[${j}]`,value:M[j]??"",onChange:U=>D(j,U.target.value),placeholder:`${a} (${Mp(j)})`,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${C?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})},j)):h.jsx(Zn,{name:g,value:_,onChange:$,placeholder:a,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${C?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})),L&&h.jsx(I,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:h.jsx(Za,{content:z,isHtml:!0,className:d||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:u})})]})},Np="g7_dismissed_warnings";function Op(){try{const t=sessionStorage.getItem(Np);return t?JSON.parse(t):[]}catch{return[]}}function xE(t){try{const e=Op();e.includes(t)||(e.push(t),sessionStorage.setItem(Np,JSON.stringify(e)))}catch{}}function wE(t){switch(t){case"error":return"error";case"warning":return"warning";default:return"info"}}const kE=({warnings:t,className:e=""})=>{const[n,r]=v.useState([]);v.useEffect(()=>{r(Op())},[]);const s=v.useCallback(i=>{xE(i),r(a=>[...a,i])},[]);if(!t||t.length===0)return null;const o=t.filter(i=>!n.includes(i.id));return o.length===0?null:h.jsx(I,{className:`space-y-2 ${e}`.trim(),children:o.map(i=>h.jsx(cu,{type:wE(i.level),message:i.message,dismissible:!0,onDismiss:()=>s(i.id)},i.id))})},Tp=window.G7Core?.createLogger?.("Comp:FilterVisibility")??{log:(...t)=>console.log("[Comp:FilterVisibility]",...t),warn:(...t)=>console.warn("[Comp:FilterVisibility]",...t),error:(...t)=>console.error("[Comp:FilterVisibility]",...t)},CE=({id:t,visibleFilters:e,defaultFilters:n=[],onFilterVisibilityChange:r})=>{const[s,o]=v.useState(!1),i=v.useRef(!0),a=v.useMemo(()=>{try{const f=window.G7Core?.AuthManager?.getInstance();return f?f.getUser()?.uuid:void 0}catch{return}},[]),l=v.useMemo(()=>`g7_filters_${t}${a!==void 0?`_${a}`:""}`,[t,a]),c=v.useCallback(()=>{try{const f=localStorage.getItem(l);if(f){const g=JSON.parse(f);if(Array.isArray(g))return g}}catch(f){Tp.warn("Failed to load from localStorage:",f)}return null},[l]),d=v.useCallback(f=>{try{localStorage.setItem(l,JSON.stringify(f))}catch(g){Tp.warn("Failed to save to localStorage:",g)}},[l]),u=v.useCallback(()=>{const f=c();return f||(e&&e.length>0?e:n)},[c,e,n]);return v.useEffect(()=>{if(!s){const f=u();r&&f.length>0&&r(f),o(!0)}},[s,r,u]),v.useEffect(()=>{if(s&&e&&e.length>0){if(i.current){i.current=!1;return}d(e)}},[s,e,d]),null},SE=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...t)=>console.log( Line truncated
w-full px-4 py-2.5
bg-gray-100 dark:bg-gray-700
border-0 rounded-xl
@@ -249,13 +249,13 @@ self.addEventListener('message', async (e) => {
flex items-center justify-between gap-2
text-left cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed
`,"aria-haspopup":"listbox","aria-expanded":f,children:[h.jsx(I,{className:"flex-1 min-w-0",children:E()}),h.jsx(Hn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&h.jsx(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",role:"listbox",children:h.jsx(I,{className:"overflow-auto",style:{maxHeight:i},children:t.length===0?h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:Tp("common.no_options")}):t.map((C,N)=>{const _=String(C.value)===String(e);return h.jsx(oe,{type:"button",onClick:()=>!C.disabled&&S(C.value),disabled:C.disabled,className:`
`,"aria-haspopup":"listbox","aria-expanded":f,children:[h.jsx(I,{className:"flex-1 min-w-0",children:E()}),h.jsx(Hn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&h.jsx(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",role:"listbox",children:h.jsx(I,{className:"overflow-auto",style:{maxHeight:i},children:t.length===0?h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:Ap("common.no_options")}):t.map((C,N)=>{const _=String(C.value)===String(e);return h.jsx(oe,{type:"button",onClick:()=>!C.disabled&&S(C.value),disabled:C.disabled,className:`
w-full px-4 py-3 text-left text-sm
hover:bg-gray-100 dark:hover:bg-gray-700
transition-colors
${_?"bg-blue-50 dark:bg-blue-900/20":""}
${C.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}
`,role:"option","aria-selected":_,"data-item-value":C.value,"data-item-index":N,"data-item-selected":_,children:c?.item?Ap(c.item,{item:C,index:N,isSelected:_},`item-${N}`):a||k(C,_)},C.value)})})})]})};function _E(t){const e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return e?{r:parseInt(e[1],16),g:parseInt(e[2],16),b:parseInt(e[3],16)}:null}const EE=({value:t,checked:e=!1,icon:n,label:r,variant:s="blue",customColor:o,className:i="",style:a,onChange:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),g="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium border cursor-pointer transition-all";if(o){const b=_E(o),S=document.documentElement.classList.contains("dark");let x;return e?x={backgroundColor:o,color:"#ffffff",borderColor:o,opacity:u?.85:1}:x=b?{backgroundColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${u?S?.25:.12:S?.15:.06})`,color:o,borderColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${S?.3:.2})`}:{},h.jsxs(rn,{className:`${g} ${i}`,style:{...x,...a},onMouseEnter:()=>f(!0),onMouseLeave:()=>f(!1),id:c,...d,children:[h.jsx(wt,{type:"checkbox",value:t,checked:e,onChange:E=>{l?.(E)},className:"hidden"}),n&&h.jsx(re,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})}const y=e?{green:"bg-green-600 dark:bg-green-600 text-white dark:text-white border-green-700 dark:border-green-500 hover:bg-green-700 dark:hover:bg-green-500",orange:"bg-orange-600 dark:bg-orange-600 text-white dark:text-white border-orange-700 dark:border-orange-500 hover:bg-orange-700 dark:hover:bg-orange-500",yellow:"bg-yellow-600 dark:bg-yellow-600 text-white dark:text-white border-yellow-700 dark:border-yellow-500 hover:bg-yellow-700 dark:hover:bg-yellow-500",gray:"bg-gray-600 dark:bg-gray-500 text-white dark:text-white border-gray-700 dark:border-gray-400 hover:bg-gray-700 dark:hover:bg-gray-400",blue:"bg-blue-600 dark:bg-blue-600 text-white dark:text-white border-blue-700 dark:border-blue-500 hover:bg-blue-700 dark:hover:bg-blue-500"}[s]:{green:"bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/30",orange:"bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800 hover:bg-orange-100 dark:hover:bg-orange-900/30",yellow:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 hover:bg-yellow-100 dark:hover:bg-yellow-900/30",gray:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600",blue:"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30"}[s];return h.jsxs(rn,{className:`${g} ${y} ${i}`,style:a,id:c,...d,children:[h.jsx(wt,{type:"checkbox",value:t,checked:e,onChange:b=>{l?.(b)},className:"hidden"}),n&&h.jsx(re,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})},ME=({label:t,icon:e,iconPosition:n="left",position:r="right",className:s="",style:o,children:i})=>{const[a,l]=v.useState(!1),[c,d]=v.useState({top:0,left:0}),u=v.useRef(null),f=v.useRef(null),g=v.useCallback(()=>{if(!u.current)return;const p=u.current.getBoundingClientRect(),y=224;let b=r==="right"?p.right-y:p.left;const S=p.bottom+8;b<8&&(b=8),b+y>window.innerWidth-8&&(b=window.innerWidth-y-8),d({top:S,left:b})},[r]);v.useEffect(()=>{a&&g()},[a,g]),v.useEffect(()=>{if(!a)return;const p=()=>{l(!1)};return window.addEventListener("scroll",p,!0),window.addEventListener("resize",p),()=>{window.removeEventListener("scroll",p,!0),window.removeEventListener("resize",p)}},[a]),v.useEffect(()=>{const p=y=>{const b=y.target,S=u.current?.contains(b),x=f.current?.contains(b);!S&&!x&&l(!1)};return a&&document.addEventListener("mousedown",p),()=>{document.removeEventListener("mousedown",p)}},[a]);const m=a?On.createPortal(h.jsx("div",{ref:f,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:c.top,left:c.left},onClick:()=>l(!1),children:i}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,children:[h.jsxs(oe,{ref:u,onClick:()=>l(!a),className:"flex items-center gap-2",children:[n==="left"&&e&&h.jsx(re,{name:e,className:"w-4 h-4"}),t&&h.jsx(Q,{className:"text-sm font-medium",children:t}),n==="right"&&e&&h.jsx(re,{name:e,className:"w-4 h-4"})]}),m]})},NE=({label:t,icon:e,variant:n="default",disabled:r=!1,divider:s=!1,className:o="",style:i,onClick:a})=>{if(s)return h.jsx(I,{className:"h-px bg-gray-200 dark:bg-gray-700 my-1",style:i});const l=n==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",c=r?"opacity-50 cursor-not-allowed":"cursor-pointer",d=()=>{!r&&a&&a()};return h.jsxs(I,{onClick:d,className:`flex items-center gap-3 px-4 py-3Line truncated
`,role:"option","aria-selected":_,"data-item-value":C.value,"data-item-index":N,"data-item-selected":_,children:c?.item?Dp(c.item,{item:C,index:N,isSelected:_},`item-${N}`):a||k(C,_)},C.value)})})})]})};function NE(t){const e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return e?{r:parseInt(e[1],16),g:parseInt(e[2],16),b:parseInt(e[3],16)}:null}const OE=({value:t,checked:e=!1,icon:n,label:r,variant:s="blue",customColor:o,className:i="",style:a,onChange:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),g="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium border cursor-pointer transition-all";if(o){const b=NE(o),S=document.documentElement.classList.contains("dark");let x;return e?x={backgroundColor:o,color:"#ffffff",borderColor:o,opacity:u?.85:1}:x=b?{backgroundColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${u?S?.25:.12:S?.15:.06})`,color:o,borderColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${S?.3:.2})`}:{},h.jsxs(rn,{className:`${g} ${i}`,style:{...x,...a},onMouseEnter:()=>f(!0),onMouseLeave:()=>f(!1),id:c,...d,children:[h.jsx(wt,{type:"checkbox",value:t,checked:e,onChange:E=>{l?.(E)},className:"hidden"}),n&&h.jsx(re,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})}const y=e?{green:"bg-green-600 dark:bg-green-600 text-white dark:text-white border-green-700 dark:border-green-500 hover:bg-green-700 dark:hover:bg-green-500",orange:"bg-orange-600 dark:bg-orange-600 text-white dark:text-white border-orange-700 dark:border-orange-500 hover:bg-orange-700 dark:hover:bg-orange-500",yellow:"bg-yellow-600 dark:bg-yellow-600 text-white dark:text-white border-yellow-700 dark:border-yellow-500 hover:bg-yellow-700 dark:hover:bg-yellow-500",gray:"bg-gray-600 dark:bg-gray-500 text-white dark:text-white border-gray-700 dark:border-gray-400 hover:bg-gray-700 dark:hover:bg-gray-400",blue:"bg-blue-600 dark:bg-blue-600 text-white dark:text-white border-blue-700 dark:border-blue-500 hover:bg-blue-700 dark:hover:bg-blue-500"}[s]:{green:"bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/30",orange:"bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800 hover:bg-orange-100 dark:hover:bg-orange-900/30",yellow:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 hover:bg-yellow-100 dark:hover:bg-yellow-900/30",gray:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600",blue:"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30"}[s];return h.jsxs(rn,{className:`${g} ${y} ${i}`,style:a,id:c,...d,children:[h.jsx(wt,{type:"checkbox",value:t,checked:e,onChange:b=>{l?.(b)},className:"hidden"}),n&&h.jsx(re,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})},TE=({label:t,icon:e,iconPosition:n="left",position:r="right",className:s="",style:o,children:i})=>{const[a,l]=v.useState(!1),[c,d]=v.useState({top:0,left:0}),u=v.useRef(null),f=v.useRef(null),g=v.useCallback(()=>{if(!u.current)return;const p=u.current.getBoundingClientRect(),y=224;let b=r==="right"?p.right-y:p.left;const S=p.bottom+8;b<8&&(b=8),b+y>window.innerWidth-8&&(b=window.innerWidth-y-8),d({top:S,left:b})},[r]);v.useEffect(()=>{a&&g()},[a,g]),v.useEffect(()=>{if(!a)return;const p=()=>{l(!1)};return window.addEventListener("scroll",p,!0),window.addEventListener("resize",p),()=>{window.removeEventListener("scroll",p,!0),window.removeEventListener("resize",p)}},[a]),v.useEffect(()=>{const p=y=>{const b=y.target,S=u.current?.contains(b),x=f.current?.contains(b);!S&&!x&&l(!1)};return a&&document.addEventListener("mousedown",p),()=>{document.removeEventListener("mousedown",p)}},[a]);const m=a?On.createPortal(h.jsx("div",{ref:f,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:c.top,left:c.left},onClick:()=>l(!1),children:i}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,children:[h.jsxs(oe,{ref:u,onClick:()=>l(!a),className:"flex items-center gap-2",children:[n==="left"&&e&&h.jsx(re,{name:e,className:"w-4 h-4"}),t&&h.jsx(Q,{className:"text-sm font-medium",children:t}),n==="right"&&e&&h.jsx(re,{name:e,className:"w-4 h-4"})]}),m]})},AE=({label:t,icon:e,variant:n="default",disabled:r=!1,divider:s=!1,className:o="",style:i,onClick:a})=>{if(s)return h.jsx(I,{className:"h-px bg-gray-200 dark:bg-gray-700 my-1",style:i});const l=n==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",c=r?"opacity-50 cursor-not-allowed":"cursor-pointer",d=()=>{!r&&a&&a()};return h.jsxs(I,{onClick:d,className:`flex items-center gap-3 px-4 py-3Line truncated
`)>-1?t.split(`
`):t}function gT(t,e){const{element:n,datasetIndex:r,index:s}=e,o=t.getDatasetMeta(r).controller,{label:i,value:a}=o.getLabelAndValue(s);return{chart:t,label:i,parsed:o.getParsed(s),raw:t.data.datasets[r].data[s],formattedValue:a,dataset:o.getDataset(),dataIndex:s,datasetIndex:r,element:n}}function mm(t,e){const n=t.chart.ctx,{body:r,footer:s,title:o}=t,{boxWidth:i,boxHeight:a}=e,l=Ut(e.bodyFont),c=Ut(e.titleFont),d=Ut(e.footerFont),u=o.length,f=s.length,g=r.length,m=pn(e.padding);let p=m.height,y=0,b=r.reduce((E,k)=>E+k.before.length+k.lines.length+k.after.length,0);if(b+=t.beforeBody.length+t.afterBody.length,u&&(p+=u*c.lineHeight+(u-1)*e.titleSpacing+e.titleMarginBottom),b){const E=e.displayColors?Math.max(a,l.lineHeight):l.lineHeight;p+=g*E+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(p+=e.footerMarginTop+f*d.lineHeight+(f-1)*e.footerSpacing);let S=0;const x=function(E){y=Math.max(y,n.measureText(E).width+S)};return n.save(),n.font=c.string,at(t.title,x),n.font=l.string,at(t.beforeBody.concat(t.afterBody),x),S=e.displayColors?i+2+e.boxPadding:0,at(r,E=>{at(E.before,x),at(E.lines,x),at(E.after,x)}),S=0,n.font=d.string,at(t.footer,x),n.restore(),y+=m.width,{width:y,height:p}}function pT(t,e){const{y:n,height:r}=e;return n<r/2?"top":n>t.height-r/2?"bottom":"center"}function mT(t,e,n,r){const{x:s,width:o}=r,i=n.caretSize+n.caretPadding;if(t==="left"&&s+o+i>e.width||t==="right"&&s-o-i<0)return!0}function bT(t,e,n,r){const{x:s,width:o}=n,{width:i,chartArea:{left:a,right:l}}=t;let c="center";return r==="center"?c=s<=(a+l)/2?"left":"right":s<=o/2?c="left":s>=i-o/2&&(c="right"),mT(c,t,e,n)&&(c="center"),c}function bm(t,e,n){const r=n.yAlign||e.yAlign||pT(t,n);return{xAlign:n.xAlign||e.xAlign||bT(t,e,n,r),yAlign:r}}function yT(t,e){let{x:n,width:r}=t;return e==="right"?n-=r:e==="center"&&(n-=r/2),n}function vT(t,e,n){let{y:r,height:s}=t;return e==="top"?r+=n:e==="bottom"?r-=s+n:r-=s/2,r}function ym(t,e,n,r){const{caretSize:s,caretPadding:o,cornerRadius:i}=t,{xAlign:a,yAlign:l}=n,c=s+o,{topLeft:d,topRight:u,bottomLeft:f,bottomRight:g}=cs(i);let m=yT(e,a);const p=vT(e,l,c);return l==="center"?a==="left"?m+=c:a==="right"&&(m-=c):a==="left"?m-=Math.max(d,f)+s:a==="right"&&(m+=Math.max(u,g)+s),{x:Jt(m,0,r.width-e.width),y:Jt(p,0,r.height-e.height)}}function ka(t,e,n){const r=pn(n.padding);return e==="center"?t.x+t.width/2:e==="right"?t.x+t.width-r.right:t.x+r.left}function vm(t){return Vn([],Kn(t))}function xT(t,e,n){return ds(t,{tooltip:e,tooltipItems:n,type:"tooltip"})}function xm(t,e){const n=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return n?t.override(n):t}const wm={beforeTitle:Gn,title(t){if(t.length>0){const e=t[0],n=e.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode==="dataset")return e.dataset.label||"";if(e.label)return e.label;if(r>0&&e.dataIndex<r)return n[e.dataIndex]}return""},afterTitle:Gn,beforeBody:Gn,beforeLabel:Gn,label(t){if(this&&this.options&&this.options.mode==="dataset")return t.label+": "+t.formattedValue||t.formattedValue;let e=t.dataset.label||"";e&&(e+=": ");const n=t.formattedValue;return lt(n)||(e+=n),e},labelColor(t){const n=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{borderColor:n.borderColor,backgroundColor:n.backgroundColor,borderWidth:n.borderWidth,borderDash:n.borderDash,borderDashOffset:n.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(t){const n=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{pointStyle:n.pointStyle,rotation:n.rotation}},afterLabel:Gn,afterBody:Gn,beforeFooter:Gn,footer:Gn,afterFooter:Gn};function Rt(t,e,n,r){const s=t[e].call(n,r);return typeof s>"u"?wm[e].call(n,r):s}class md extends Ar{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const e=this._cachedAnimations;if(e)return e;const n=this.chart,r=this.options.setContext(this.getContext()),s=r.enabled&&n.options.animation&&r.animations,o=new _0(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=xT(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,n){const{callbacks:r}=n,s=Rt(r,"beforeTitle",this,e),o=Rt(r,"title",this,e),i=Rt(r,"afterTitle",this,e);let a=[];return a=Vn(a,Kn(s)),a=Vn(a,Kn(o)),a=Vn(a,Kn(i)),a}getBeforeBody(e,n){return vm(Rt(n.call Line truncated
`):t}function bT(t,e){const{element:n,datasetIndex:r,index:s}=e,o=t.getDatasetMeta(r).controller,{label:i,value:a}=o.getLabelAndValue(s);return{chart:t,label:i,parsed:o.getParsed(s),raw:t.data.datasets[r].data[s],formattedValue:a,dataset:o.getDataset(),dataIndex:s,datasetIndex:r,element:n}}function bm(t,e){const n=t.chart.ctx,{body:r,footer:s,title:o}=t,{boxWidth:i,boxHeight:a}=e,l=Ut(e.bodyFont),c=Ut(e.titleFont),d=Ut(e.footerFont),u=o.length,f=s.length,g=r.length,m=pn(e.padding);let p=m.height,y=0,b=r.reduce((E,k)=>E+k.before.length+k.lines.length+k.after.length,0);if(b+=t.beforeBody.length+t.afterBody.length,u&&(p+=u*c.lineHeight+(u-1)*e.titleSpacing+e.titleMarginBottom),b){const E=e.displayColors?Math.max(a,l.lineHeight):l.lineHeight;p+=g*E+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(p+=e.footerMarginTop+f*d.lineHeight+(f-1)*e.footerSpacing);let S=0;const x=function(E){y=Math.max(y,n.measureText(E).width+S)};return n.save(),n.font=c.string,at(t.title,x),n.font=l.string,at(t.beforeBody.concat(t.afterBody),x),S=e.displayColors?i+2+e.boxPadding:0,at(r,E=>{at(E.before,x),at(E.lines,x),at(E.after,x)}),S=0,n.font=d.string,at(t.footer,x),n.restore(),y+=m.width,{width:y,height:p}}function yT(t,e){const{y:n,height:r}=e;return n<r/2?"top":n>t.height-r/2?"bottom":"center"}function vT(t,e,n,r){const{x:s,width:o}=r,i=n.caretSize+n.caretPadding;if(t==="left"&&s+o+i>e.width||t==="right"&&s-o-i<0)return!0}function xT(t,e,n,r){const{x:s,width:o}=n,{width:i,chartArea:{left:a,right:l}}=t;let c="center";return r==="center"?c=s<=(a+l)/2?"left":"right":s<=o/2?c="left":s>=i-o/2&&(c="right"),vT(c,t,e,n)&&(c="center"),c}function ym(t,e,n){const r=n.yAlign||e.yAlign||yT(t,n);return{xAlign:n.xAlign||e.xAlign||xT(t,e,n,r),yAlign:r}}function wT(t,e){let{x:n,width:r}=t;return e==="right"?n-=r:e==="center"&&(n-=r/2),n}function kT(t,e,n){let{y:r,height:s}=t;return e==="top"?r+=n:e==="bottom"?r-=s+n:r-=s/2,r}function vm(t,e,n,r){const{caretSize:s,caretPadding:o,cornerRadius:i}=t,{xAlign:a,yAlign:l}=n,c=s+o,{topLeft:d,topRight:u,bottomLeft:f,bottomRight:g}=cs(i);let m=wT(e,a);const p=kT(e,l,c);return l==="center"?a==="left"?m+=c:a==="right"&&(m-=c):a==="left"?m-=Math.max(d,f)+s:a==="right"&&(m+=Math.max(u,g)+s),{x:Jt(m,0,r.width-e.width),y:Jt(p,0,r.height-e.height)}}function ka(t,e,n){const r=pn(n.padding);return e==="center"?t.x+t.width/2:e==="right"?t.x+t.width-r.right:t.x+r.left}function xm(t){return Vn([],Kn(t))}function CT(t,e,n){return ds(t,{tooltip:e,tooltipItems:n,type:"tooltip"})}function wm(t,e){const n=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return n?t.override(n):t}const km={beforeTitle:Gn,title(t){if(t.length>0){const e=t[0],n=e.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode==="dataset")return e.dataset.label||"";if(e.label)return e.label;if(r>0&&e.dataIndex<r)return n[e.dataIndex]}return""},afterTitle:Gn,beforeBody:Gn,beforeLabel:Gn,label(t){if(this&&this.options&&this.options.mode==="dataset")return t.label+": "+t.formattedValue||t.formattedValue;let e=t.dataset.label||"";e&&(e+=": ");const n=t.formattedValue;return lt(n)||(e+=n),e},labelColor(t){const n=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{borderColor:n.borderColor,backgroundColor:n.backgroundColor,borderWidth:n.borderWidth,borderDash:n.borderDash,borderDashOffset:n.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(t){const n=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{pointStyle:n.pointStyle,rotation:n.rotation}},afterLabel:Gn,afterBody:Gn,beforeFooter:Gn,footer:Gn,afterFooter:Gn};function Rt(t,e,n,r){const s=t[e].call(n,r);return typeof s>"u"?km[e].call(n,r):s}class bd extends Ar{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const e=this._cachedAnimations;if(e)return e;const n=this.chart,r=this.options.setContext(this.getContext()),s=r.enabled&&n.options.animation&&r.animations,o=new E0(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=CT(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,n){const{callbacks:r}=n,s=Rt(r,"beforeTitle",this,e),o=Rt(r,"title",this,e),i=Rt(r,"afterTitle",this,e);let a=[];return a=Vn(a,Kn(s)),a=Vn(a,Kn(o)),a=Vn(a,Kn(i)),a}getBeforeBody(e,n){return xm(Rt(n.call Line truncated
//# sourceMappingURL=components.iife.js.map
@@ -0,0 +1,63 @@
/**
* detectAssetUrlMode 핸들러 (이슈 #486 §8)
*
* 관리자 환경설정에서 자산 URL 방식을 **브라우저에서** 재감지한다.
*
* 서버측에서 자기 `APP_URL` 로 curl 하면 loopback 이 nginx vhost·SSL·프록시 체인을
* 우회하거나 다른 vhost 를 타서 오판한다. 실제로 방문자가 겪는 경로를 재현하려면
* 브라우저가 던져야 한다.
*
* 감지 지점이 인스톨러 하나로 부족한 이유가 여기 있다 — 관리자가 설치 6개월 뒤
* 정적 최적화 블록을 추가하면 저장된 값은 "확장자 OK" 인 채 사이트가 다시 죽는다.
* 이 버튼이 그 상황의 재판정 수단이다.
*/
/** 판정 결과 */
export type DetectResult = 'extension' | 'extensionless' | 'unavailable';
/**
* 프로브 쌍을 던져 자산 URL 방식을 판정한다.
*
* 단일 프로브는 "PHP 자체가 죽음" 과 구분되지 않으므로 반드시 쌍으로 던진다.
*
* | probe.js | probe | 판정 |
* |---|---|---|
* | 성공 | 성공 | `extension` |
* | 실패 | 성공 | `extensionless` (정적 블록 가로채기 확정) |
* | 그 외 | | `unavailable` (모드 문제 아님 — PHP/라우팅 장애) |
*
* @returns 판정 결과
*/
export declare function detectAssetUrlMode(): Promise<DetectResult>;
/**
* 저장된 모드와 실제 환경을 대조해 불일치를 전역 상태에 실어준다 (§5).
*
* 관리자 화면 자체가 안 뜨는 결함이라 "브라우저로 설정을 바꾸세요" 는 순환 참조다.
* 반대로 자가 복구가 조용히 성공하면 관리자는 사이트가 정상이라 믿고 저장값을
* 영영 고치지 않는다. 그래서 **대시보드가 스스로 프로브를 던져 저장값과 대조**한다.
*
* L5 위반이 아니다 — 서버 설정을 쓰지 않고 관리자에게 보여주기만 한다.
* L9 위반도 아니다 — L9 가 금지하는 것은 엔진 레이어(Router/LayoutLoader/
* ComponentRegistry)의 자산 로딩 중 재감지 캐스케이드이고, 이것은 관리자 화면
* 1회 진단이다.
*
* 판정 불가(`unavailable`)면 아무것도 표시하지 않는다. 일시적 네트워크 장애로
* 대시보드에 경고가 뜨면 신호가 아니라 소음이 된다.
*
* @param _action 액션 정의 (미사용)
*/
export declare function checkAssetUrlModeDriftHandler(_action?: any): Promise<void>;
/**
* 자산 URL 방식 자동 감지 핸들러.
*
* 판정 결과를 폼 상태(`general.asset_url_mode`)에 반영하고, 감지 상태를
* `asset_url_mode_detect_status` 로 로컬 폼 상태에 기록한다 — 레이아웃이 이 값을 읽어
* 필드 하단에 결과 문구를 조건부로 렌더한다(토스트가 아니라 필드 옆 인라인 안내).
* **저장은 하지 않는다** — 관리자가 결과를 보고 저장 버튼을 누르는 흐름을 유지해,
* 감지가 오판했을 때 되돌릴 여지를 남긴다.
*
* 판정 불가(`unavailable`)면 값을 건드리지 않는다. 서버가 응답하지 않는 상황에서
* 임의 값을 넣으면 멀쩡한 설정을 덮어쓸 수 있다(상태만 `unavailable` 로 표시).
*
* @param _action 액션 정의 (미사용)
* @param context 액션 컨텍스트 (setState 등)
*/
export declare function detectAssetUrlModeHandler(_action: any, context?: any): Promise<void>;
@@ -6,6 +6,7 @@ import { saveMultilingualTagHandler, cancelMultilingualTagHandler, updateMultili
import { setDateRangeHandler } from './setDateRangeHandler';
import { toggleSidebarHandler, initSidebarHandler } from './sidebarHandler';
import { downloadAttachmentHandler } from './downloadAttachment';
import { checkAssetUrlModeDriftHandler, detectAssetUrlModeHandler } from './detectAssetUrlModeHandler';
/**
* 핸들러 맵
*
@@ -15,6 +16,8 @@ import { downloadAttachmentHandler } from './downloadAttachment';
* 새로운 핸들러 추가 시 여기에만 등록하면 자동으로 ActionDispatcher에 등록됩니다.
*/
export declare const handlerMap: {
readonly detectAssetUrlMode: typeof detectAssetUrlModeHandler;
readonly checkAssetUrlModeDrift: typeof checkAssetUrlModeDriftHandler;
readonly setTheme: typeof setThemeHandler;
readonly initTheme: typeof initThemeHandler;
readonly scrollToSection: typeof scrollToSectionHandler;
@@ -1,6 +1,30 @@
{
"comment": "admin 자체 + 코어 SSoT 도메인. locales/admin_menus/me/installed_modules/active_plugins/users 등. 실제 데이터소스 ID 는 착수 시점 admin 레이아웃 data_sources 로 재검증 — 본 블록은 코어 SSoT/공통 폴백.",
"byDataSourceId": {
"sitemap_status": {
"data": {
"last_updated_at": "2026-06-21T15:40:00+09:00",
"realtime_enabled": true,
"progress": {
"status": "completed",
"total": 128,
"written": 128,
"percent": 100
}
}
},
"sitemap_progress_ws": {
"data": {
"last_updated_at": "2026-06-21T15:40:00+09:00",
"realtime_enabled": true,
"progress": {
"status": "running",
"total": 128,
"written": 64,
"percent": 50
}
}
},
"locales": {
"data": [
{
@@ -135,7 +135,10 @@
"unanswered_inquiries": "Unanswered Inquiries",
"unanswered_inquiries_with_count": "Unanswered (3)",
"latest_reviews": "Latest Reviews"
}
},
"asset_url_mode_drift_title": "Asset file serving mode does not match this server",
"asset_url_mode_drift_description": "With the saved style, some URLs may be intercepted by the server and pages may not load. Visitors are corrected automatically in the browser, but search engine bots are not, so please save the detected style in Settings.",
"asset_url_mode_drift_action": "Review in Settings > General"
},
"users": {
"page_title": "User Management",
@@ -1307,7 +1310,16 @@
"default_language": "Default Language",
"select_language": "Select a language",
"timezone": "Timezone",
"select_timezone": "Select a timezone"
"select_timezone": "Select a timezone",
"asset_url_mode": "Asset file serving mode",
"asset_url_mode_help": "Chosen automatically based on your server setup. Leave it alone if pages load correctly.",
"asset_url_mode_extension": "Use file extensions (recommended)",
"asset_url_mode_extensionless": "No file extensions",
"asset_url_mode_detect": "Detect automatically",
"asset_url_mode_detecting": "Detecting asset file serving mode...",
"asset_url_mode_detect_extension": "File extensions work fine on this server.",
"asset_url_mode_detect_extensionless": "A static-file optimization rule (e.g. nginx location ~* \\.(js|css|json|png|jpg|jpeg|gif|ico|svg|woff2?)$) is intercepting extension URLs. Switch to \"No file extensions\" and save.",
"asset_url_mode_detect_unavailable": "Detection failed. The server did not respond, so the style could not be determined."
},
"mail": {
"smtp_settings": "SMTP Settings",
@@ -139,7 +139,10 @@
"unanswered_inquiries": "미답변 문의",
"unanswered_inquiries_with_count": "미답변문의(3)",
"latest_reviews": "최신 리뷰"
}
},
"asset_url_mode_drift_title": "에셋 파일 서빙 방식이 서버 환경과 다릅니다",
"asset_url_mode_drift_description": "저장된 방식으로는 일부 주소가 서버에 가로채여 화면이 뜨지 않을 수 있습니다. 방문자 화면은 브라우저가 자동으로 보정하지만 검색엔진 봇에는 보정이 적용되지 않으므로, 환경설정에서 감지 결과를 저장해 주세요.",
"asset_url_mode_drift_action": "환경설정 > 일반에서 확인하기"
},
"users": {
"page_title": "사용자 관리",
@@ -1311,7 +1314,16 @@
"default_language": "기본 언어",
"select_language": "언어를 선택하세요",
"timezone": "시간대",
"select_timezone": "시간대를 선택하세요"
"select_timezone": "시간대를 선택하세요",
"asset_url_mode": "에셋 파일 서빙 방식",
"asset_url_mode_help": "서버 설정에 따라 자동으로 선택됩니다. 화면이 정상적으로 보인다면 바꾸지 마세요.",
"asset_url_mode_extension": "확장자 사용 (권장)",
"asset_url_mode_extensionless": "확장자 미사용",
"asset_url_mode_detect": "자동 감지",
"asset_url_mode_detecting": "에셋 파일 서빙 방식을 감지하는 중입니다...",
"asset_url_mode_detect_extension": "확장자를 사용할 수 있는 환경입니다.",
"asset_url_mode_detect_extensionless": "정적 파일 최적화 규칙(nginx의 location ~* \\.(js|css|json|png|jpg|jpeg|gif|ico|svg|woff2?)$ 블록 등)이 확장자 주소를 가로채고 있습니다. '확장자 미사용'으로 바꾼 뒤 저장하세요.",
"asset_url_mode_detect_unavailable": "감지에 실패했습니다. 서버가 응답하지 않아 방식을 판정할 수 없습니다."
},
"mail": {
"smtp_settings": "SMTP 설정",
@@ -10,6 +10,11 @@
"description": "$t:admin.dashboard.description",
"auth_required": true
},
"init_actions": [
{
"handler": "checkAssetUrlModeDrift"
}
],
"data_sources": [
{
"id": "dashboard_stats",
@@ -271,6 +276,76 @@
}
]
},
{
"id": "asset_url_mode_drift_alert",
"type": "basic",
"name": "Div",
"if": "{{_global.assetUrlModeDrift}}",
"props": {
"className": "flex items-start gap-3 p-4 mb-6 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800"
},
"children": [
{
"id": "asset_url_mode_drift_icon",
"type": "basic",
"name": "Icon",
"props": {
"name": "triangle-exclamation",
"className": "text-base text-amber-600 dark:text-amber-400 mt-0.5"
}
},
{
"id": "asset_url_mode_drift_body",
"type": "basic",
"name": "Div",
"props": {
"className": "flex-1 min-w-0"
},
"children": [
{
"id": "asset_url_mode_drift_title",
"type": "basic",
"name": "Div",
"props": {
"className": "text-sm font-semibold text-amber-900 dark:text-amber-200"
},
"text": "$t:admin.dashboard.asset_url_mode_drift_title"
},
{
"id": "asset_url_mode_drift_desc",
"type": "basic",
"name": "P",
"props": {
"className": "mt-1 text-sm text-amber-800 dark:text-amber-300"
},
"text": "$t:admin.dashboard.asset_url_mode_drift_description"
},
{
"id": "asset_url_mode_drift_link",
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"className": "mt-2 inline-flex items-center gap-1.5 text-sm font-medium text-amber-900 dark:text-amber-200 underline hover:no-underline cursor-pointer"
},
"text": "$t:admin.dashboard.asset_url_mode_drift_action",
"actions": [
{
"type": "click",
"handler": "navigate",
"params": {
"path": "/admin/settings",
"query": {
"tab": "general"
}
}
}
]
}
]
}
]
},
{
"id": "quick_menu",
"type": "basic",
@@ -809,8 +884,7 @@
"id": "activity_list",
"type": "basic",
"name": "Div",
"props": {
},
"props": {},
"children": [
{
"id": "activity_item_{{$idx}}",
@@ -969,8 +1043,7 @@
"id": "module_list",
"type": "basic",
"name": "Div",
"props": {
},
"props": {},
"children": [
{
"id": "module_item_{{$idx}}",
@@ -1108,8 +1181,7 @@
"id": "plugin_list",
"type": "basic",
"name": "Div",
"props": {
},
"props": {},
"children": [
{
"id": "plugin_item_{{$idx}}",
@@ -1247,8 +1319,7 @@
"id": "template_list",
"type": "basic",
"name": "Div",
"props": {
},
"props": {},
"children": [
{
"id": "template_item_{{$idx}}",
@@ -1361,8 +1432,7 @@
"id": "alerts_list",
"type": "basic",
"name": "Div",
"props": {
},
"props": {},
"children": [
{
"id": "alert_item_{{$idx}}",
@@ -469,6 +469,139 @@
]
}
]
},
{
"id": "card_asset_serving",
"type": "basic",
"name": "Div",
"props": {
"className": "admin-card"
},
"children": [
{
"type": "basic",
"name": "H3",
"props": {
"className": "card-title"
},
"text": "$t:admin.settings.general.asset_url_mode"
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "card-description"
},
"text": "$t:admin.settings.general.asset_url_mode_help"
},
{
"id": "field_asset_url_mode",
"type": "basic",
"name": "Div",
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex gap-2 items-start"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex-1"
},
"children": [
{
"type": "composite",
"name": "Select",
"props": {
"name": "general.asset_url_mode",
"className": "w-full",
"disabled": "{{_computed.isReadOnly}}",
"options": [
{
"value": "extension",
"label": "$t:admin.settings.general.asset_url_mode_extension"
},
{
"value": "extensionless",
"label": "$t:admin.settings.general.asset_url_mode_extensionless"
}
]
}
}
]
},
{
"id": "asset_url_mode_detect_button",
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"className": "btn btn-secondary whitespace-nowrap",
"disabled": "{{_computed.isReadOnly}}"
},
"children": [
{
"type": "basic",
"name": "Span",
"text": "$t:admin.settings.general.asset_url_mode_detect"
}
],
"actions": [
{
"event": "onClick",
"handler": "detectAssetUrlMode"
}
]
}
]
},
{
"id": "asset_url_mode_msg_detecting",
"type": "basic",
"name": "P",
"if": "{{_local.asset_url_mode_detect_status === 'detecting'}}",
"props": {
"className": "mt-2 text-sm text-gray-500 dark:text-gray-400"
},
"text": "$t:admin.settings.general.asset_url_mode_detecting"
},
{
"id": "asset_url_mode_msg_extension",
"type": "basic",
"name": "P",
"if": "{{_local.asset_url_mode_detect_status === 'extension'}}",
"props": {
"className": "mt-2 text-sm text-green-600 dark:text-green-400"
},
"text": "$t:admin.settings.general.asset_url_mode_detect_extension"
},
{
"id": "asset_url_mode_msg_extensionless",
"type": "basic",
"name": "P",
"if": "{{_local.asset_url_mode_detect_status === 'extensionless'}}",
"props": {
"className": "mt-2 text-sm text-amber-600 dark:text-amber-400"
},
"text": "$t:admin.settings.general.asset_url_mode_detect_extensionless"
},
{
"id": "asset_url_mode_msg_unavailable",
"type": "basic",
"name": "P",
"if": "{{_local.asset_url_mode_detect_status === 'unavailable'}}",
"props": {
"className": "mt-2 text-sm text-red-600 dark:text-red-400"
},
"text": "$t:admin.settings.general.asset_url_mode_detect_unavailable"
}
]
}
]
}
]
}
@@ -0,0 +1,371 @@
/**
* detectAssetUrlMode 감지 로직 테스트 — 이슈 #486 §12 L6 전용 가드
*
* L6: "프로브 판정은 **본문 매직 토큰 + Content-Type** 기준"
*
* 이 불변식이 막는 사고는 구체적이다 — 일부 서버는 없는 경로에 404 대신
* `200 + 에러 HTML` 이나 catch-all 페이지를 반환한다. 상태코드만 보면 그 응답을
* "프로브 성공" 으로 읽어 영원히 `extension` 으로 오판하고, 관리자가 재감지를
* 몇 번 눌러도 같은 오답이 나온다.
*
* 로직·주석·구현은 실재하지만 **negative branch 회귀 가드가 없었다**. 즉 누군가
* Content-Type 검사나 토큰 검사를 지워도 아무 테스트도 red 가 되지 않는 상태였다.
* 본 파일이 그 구멍을 막는다.
*/
// e2e:allow 프로브 판정의 순수 분기 로직 단위. 브라우저 시나리오는 asset-url-mode.spec.ts 담당.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
checkAssetUrlModeDriftHandler,
detectAssetUrlMode,
detectAssetUrlModeHandler,
} from '../detectAssetUrlModeHandler';
/** 서버 AssetProbeController::PROBE_TOKEN 과 동일해야 하는 값 */
const TOKEN = 'G7_ASSET_PROBE_OK';
/** 정상 프로브 응답 본문 */
const VALID_BODY = `/* G7 asset URL mode probe */\nwindow.__g7AssetProbe = '${TOKEN}';\n`;
/**
* fetch 응답을 흉내낸다.
*
* @param init 상태·본문·Content-Type
*/
function mockResponse(init: { ok?: boolean; body?: string; contentType?: string }) {
return {
ok: init.ok ?? true,
headers: { get: () => init.contentType ?? 'application/javascript; charset=utf-8' },
text: async () => init.body ?? VALID_BODY,
};
}
describe('detectAssetUrlMode — 프로브 판정 (§12 L6)', () => {
let fetchSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
fetchSpy = vi.fn();
(globalThis as any).fetch = fetchSpy;
});
afterEach(() => {
vi.restoreAllMocks();
});
/**
* 경로별 응답을 지정한다.
*
* @param withExt `/api/system/asset-probe.js` 응답
* @param withoutExt `/api/system/asset-probe` 응답
*/
const respond = (withExt: any, withoutExt: any) => {
fetchSpy.mockImplementation(async (url: string) =>
url.endsWith('.js') ? withExt : withoutExt,
);
};
describe('정상 판정', () => {
it('둘 다 성공하면 extension', async () => {
respond(mockResponse({}), mockResponse({}));
await expect(detectAssetUrlMode()).resolves.toBe('extension');
});
it('확장자 형태만 실패하면 extensionless (정적 블록 가로채기 확정)', async () => {
respond(mockResponse({ ok: false }), mockResponse({}));
await expect(detectAssetUrlMode()).resolves.toBe('extensionless');
});
it('둘 다 실패하면 unavailable (모드 문제 아님 — PHP/라우팅 장애)', async () => {
respond(mockResponse({ ok: false }), mockResponse({ ok: false }));
await expect(detectAssetUrlMode()).resolves.toBe('unavailable');
});
it('확장자 없는 형태만 실패해도 unavailable (전환해도 소용없는 상태)', async () => {
respond(mockResponse({}), mockResponse({ ok: false }));
await expect(detectAssetUrlMode()).resolves.toBe('unavailable');
});
});
describe('L6 — 상태코드만으로 판정하지 않는다', () => {
// 계획서 §12 L6 이 명시한 시나리오: "200-but-wrong-body 환경에서
// 재감지가 영원히 같은 오답" 을 내면 안 된다.
it('200 + 에러 HTML 을 성공으로 오판하지 않는다', async () => {
const errorPage = mockResponse({
ok: true,
body: '<html><body>404 Not Found</body></html>',
contentType: 'text/html; charset=utf-8',
});
// 확장자 형태가 catch-all HTML 을 받고, 확장자 없는 형태는 정상
respond(errorPage, mockResponse({}));
await expect(
detectAssetUrlMode(),
'200+HTML 을 프로브 성공으로 오판했다 — 정적 블록 가로채기를 놓친다',
).resolves.toBe('extensionless');
});
// 계획서 §"루프 방지 전용 테스트" L6 행의 **문자 그대로의 시나리오**:
// "프로브 응답을 200 + <html>에러페이지</html> 로 가로챈다
// → 감지 결과가 extension 이 아니라 '판정 불가/PHP 문제' 로 분기"
//
// 위의 '둘 다 실패' 케이스(ok:false)와는 **다른 코드 경로**다. 404 는
// `!res.ok` 에서 조기 반환되지만, 200+본문오류는 Content-Type·토큰 검사를
// 통과해야 걸러진다. 두 검사가 사라지면 이 케이스만 조용히 extension 으로
// 오판되고, 관리자는 재감지를 몇 번 눌러도 같은 오답을 받는다.
it('둘 다 200 + 에러 HTML 이면 판정 불가로 분기한다 (catch-all 서버)', async () => {
const catchAllPage = () =>
mockResponse({
ok: true,
body: '<html><body>Page Not Found</body></html>',
contentType: 'text/html; charset=utf-8',
});
respond(catchAllPage(), catchAllPage());
await expect(
detectAssetUrlMode(),
'catch-all 200 페이지를 프로브 성공으로 읽어 extension 으로 오판했다',
).resolves.toBe('unavailable');
});
it('Content-Type 이 스크립트가 아니면 토큰이 있어도 실패로 본다', async () => {
// 토큰을 그대로 담았지만 text/html 로 응답하는 catch-all 페이지
const htmlWithToken = mockResponse({
ok: true,
body: `<html><body>${TOKEN}</body></html>`,
contentType: 'text/html; charset=utf-8',
});
respond(htmlWithToken, mockResponse({}));
await expect(
detectAssetUrlMode(),
'Content-Type 검사가 없어 HTML 응답을 프로브 성공으로 읽었다',
).resolves.toBe('extensionless');
});
it('매직 토큰이 없으면 Content-Type 이 맞아도 실패로 본다', async () => {
// JS 로 응답하지만 우리 프로브가 아닌 다른 스크립트 (CDN 폴백 등)
const otherScript = mockResponse({
ok: true,
body: 'console.log("some other script");',
contentType: 'application/javascript',
});
respond(otherScript, mockResponse({}));
await expect(
detectAssetUrlMode(),
'토큰 검사가 없어 무관한 JS 응답을 프로브 성공으로 읽었다',
).resolves.toBe('extensionless');
});
it('application/javascript 외 스크립트 MIME 도 허용한다', async () => {
// 일부 서버는 text/javascript / application/ecmascript 로 응답한다
for (const contentType of ['text/javascript', 'application/ecmascript']) {
respond(mockResponse({ contentType }), mockResponse({ contentType }));
await expect(
detectAssetUrlMode(),
`${contentType} 를 스크립트로 인정하지 않았다`,
).resolves.toBe('extension');
}
});
});
describe('네트워크 예외', () => {
it('fetch 가 throw 해도 판정이 죽지 않는다', async () => {
fetchSpy.mockRejectedValue(new TypeError('Failed to fetch'));
await expect(detectAssetUrlMode()).resolves.toBe('unavailable');
});
});
describe('대시보드 드리프트 대조 (§5)', () => {
let setGlobal: ReturnType<typeof vi.fn>;
/**
* 저장된 모드와 (선택적으로) 자가 복구된 런타임 모드를 심는다.
*
* @param stored 서버 설정에 저장된 모드 (undefined 면 미설정)
* @param recoveredRuntime 자가 복구가 전환해 둔 런타임 모드
*/
const setConfig = (stored?: string, recoveredRuntime?: string) => {
(window as any).G7Config = {
assetUrlMode: recoveredRuntime ?? stored,
settings: stored === undefined ? {} : { general: { asset_url_mode: stored } },
};
};
beforeEach(() => {
setGlobal = vi.fn();
(window as any).G7Core = { state: { setGlobal } };
});
afterEach(() => {
delete (window as any).G7Core;
delete (window as any).G7Config;
});
it('감지 결과가 저장값과 같으면 알리지 않는다', async () => {
setConfig('extension');
respond(mockResponse({}), mockResponse({}));
await checkAssetUrlModeDriftHandler();
expect(setGlobal, '드리프트가 없는데 대시보드에 경고를 띄웠다').not.toHaveBeenCalled();
});
it('감지 결과가 저장값과 다르면 양쪽 값을 함께 알린다', async () => {
setConfig('extension');
respond(mockResponse({ ok: false }), mockResponse({}));
await checkAssetUrlModeDriftHandler();
expect(setGlobal).toHaveBeenCalledWith({
assetUrlModeDrift: { detected: 'extensionless', stored: 'extension' },
});
});
it('판정 불가면 알리지 않는다 (일시 장애를 경고로 만들지 않는다)', async () => {
setConfig('extension');
respond(mockResponse({ ok: false }), mockResponse({ ok: false }));
await checkAssetUrlModeDriftHandler();
expect(setGlobal, '네트워크 장애를 설정 불일치로 오인해 경고했다').not.toHaveBeenCalled();
});
it('저장값이 없으면 기본값 extension 을 기준으로 대조한다', async () => {
setConfig(undefined);
respond(mockResponse({ ok: false }), mockResponse({}));
await checkAssetUrlModeDriftHandler();
expect(setGlobal).toHaveBeenCalledWith({
assetUrlModeDrift: { detected: 'extensionless', stored: 'extension' },
});
});
// 이 케이스가 이 기능의 존재 이유다. 자가 복구가 이미 런타임 모드를 바꿔 놓으면
// 화면은 멀쩡해 보이지만 저장값은 여전히 틀려 있다. 봇은 JavaScript 를 실행하지
// 않아 자가 복구가 닿지 않으므로, 저장값을 고치지 않으면 SEO 는 계속 깨진다.
it('자가 복구로 런타임 모드가 이미 바뀌었어도 저장값 기준으로 드리프트를 잡는다', async () => {
setConfig('extension', 'extensionless');
respond(mockResponse({ ok: false }), mockResponse({}));
await checkAssetUrlModeDriftHandler();
expect(
setGlobal,
'런타임 전환값을 기준으로 대조해 드리프트를 놓쳤다 — 저장값은 여전히 틀린 상태다',
).toHaveBeenCalledWith({
assetUrlModeDrift: { detected: 'extensionless', stored: 'extension' },
});
});
it('프로브가 throw 해도 대시보드를 죽이지 않는다', async () => {
setConfig('extension');
fetchSpy.mockRejectedValue(new TypeError('Failed to fetch'));
await expect(checkAssetUrlModeDriftHandler()).resolves.toBeUndefined();
expect(setGlobal).not.toHaveBeenCalled();
});
});
describe('프로브 요청 형태', () => {
it('두 경로를 쌍으로 던지고 캐시를 쓰지 않는다', async () => {
respond(mockResponse({}), mockResponse({}));
await detectAssetUrlMode();
const urls = fetchSpy.mock.calls.map((c) => c[0]);
expect(urls).toContain('/api/system/asset-probe.js');
expect(urls).toContain('/api/system/asset-probe');
for (const call of fetchSpy.mock.calls) {
expect(call[1]?.cache, '프로브가 캐시될 수 있는 요청으로 나갔다').toBe('no-store');
}
});
});
describe('detectAssetUrlModeHandler — 결과를 토스트가 아닌 폼 상태로 (인라인 안내)', () => {
let setState: ReturnType<typeof vi.fn>;
let dispatch: ReturnType<typeof vi.fn>;
beforeEach(() => {
setState = vi.fn();
dispatch = vi.fn();
(window as any).G7Core = { state: { setLocal: setState }, dispatch };
});
afterEach(() => {
delete (window as any).G7Core;
});
it('감지 시작 시 detecting 상태를 먼저 기록한다', async () => {
respond(mockResponse({}), mockResponse({}));
await detectAssetUrlModeHandler(null, { setState });
expect(setState).toHaveBeenCalledWith({ asset_url_mode_detect_status: 'detecting' });
});
it('extension 판정 시 폼 값과 상태를 함께 기록한다', async () => {
respond(mockResponse({}), mockResponse({}));
await detectAssetUrlModeHandler(null, { setState });
expect(setState).toHaveBeenCalledWith({
'general.asset_url_mode': 'extension',
asset_url_mode_detect_status: 'extension',
});
});
it('extensionless 판정도 폼 값과 상태를 함께 기록한다', async () => {
respond(mockResponse({ ok: false }), mockResponse({}));
await detectAssetUrlModeHandler(null, { setState });
expect(setState).toHaveBeenCalledWith({
'general.asset_url_mode': 'extensionless',
asset_url_mode_detect_status: 'extensionless',
});
});
it('판정 불가면 상태만 unavailable 로 두고 폼 값은 건드리지 않는다', async () => {
respond(mockResponse({ ok: false }), mockResponse({ ok: false }));
await detectAssetUrlModeHandler(null, { setState });
expect(setState).toHaveBeenCalledWith({ asset_url_mode_detect_status: 'unavailable' });
const valueWrites = setState.mock.calls.filter(
(c) => 'general.asset_url_mode' in (c[0] ?? {}),
);
expect(valueWrites, '판정 불가인데 폼 값을 덮어썼다').toHaveLength(0);
});
it('결과를 토스트로 띄우지 않는다 (인라인 안내로 대체)', async () => {
respond(mockResponse({}), mockResponse({}));
await detectAssetUrlModeHandler(null, { setState });
const toastCalls = dispatch.mock.calls.filter((c) => c[0]?.handler === 'toast');
expect(toastCalls, '감지 결과가 여전히 토스트로 발화된다').toHaveLength(0);
});
it('프로브가 throw 해도 unavailable 상태로 수렴한다', async () => {
fetchSpy.mockRejectedValue(new Error('network down'));
await detectAssetUrlModeHandler(null, { setState });
expect(setState).toHaveBeenCalledWith({ asset_url_mode_detect_status: 'unavailable' });
});
});
});
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more