Merge pull request from gnuboard/chym1217/issue525

feat(board): 게시글 반응(추천/비추천) 기능 구현
This commit is contained in:
eunhye authored and GitHub committed 2026-08-07 18:06:56 +09:00
commit 7f4e665827
86 files changed
+4167 -270

No files matched your search

@@ -4,32 +4,23 @@
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
## [1.0.5] - 2026-08-06
### Added
- 관리자 게시글 작성·수정 화면의 첨부 안내 문구 일본어 번역 추가 — 게시판에 설정된 첨부 개수·용량이 일본어 로케일에서도 그대로 표시됩니다.
### Removed
- 화면에서 쓰이지 않던 첨부 안내(고정 5개·10MB)와 답변글 깊이 안내(1~5) 번역을 제거했습니다. 실제 상한과 다른 값이라 남겨 둘 이유가 없습니다.
## [1.0.4] - 2026-07-22
## [1.0.2] - 2026-08-06
### Added
- 게시글 반응(추천/비추천) 기능의 안내·검증·활동 로그·관리자 설정 문구 일본어 번역 추가 — 반응 등록/변경/취소 안내, 반응 비활성화·본인 글·비활성 유형·열람 권한 없는 비밀글 차단 안내, 반응 사용 토글·유형 선택 설정 라벨이 일본어 로케일에서 자연스럽게 표시됩니다.
- 게시판 환경설정 일괄 적용 화면의 "반응 사용" 항목 라벨 일본어 번역 추가.
- 첨부파일 개수 상한 초과 안내 문구 일본어 번역 추가.
- 게시판 대시보드 조회 안내 문구 일본어 번역 추가.
## [1.0.3] - 2026-07-22
### Added
- 관리자 게시글 작성·수정 화면의 첨부 안내 문구 일본어 번역 추가 — 게시판에 설정된 첨부 개수·용량이 일본어 로케일에서도 그대로 표시됩니다.
- 게시판 환경설정 일괄 적용 화면의 "선택한 게시판" 안내 문구 일본어 번역 추가.
- 게시판 환경설정의 조회·저장·일괄적용·캐시 초기화 안내 문구 일본어 번역 추가 (`messages.settings.*`) — 저장에 실패했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
- 댓글 비밀번호 확인, 댓글 목록 조회, 게시판 목록·폼 조회 실패, 비밀글 사용 불가 안내 문구의 일본어 번역 추가 — 지금까지 일본어 로케일에서 안내 문구 대신 내부 식별자가 그대로 보이던 화면들이 정상 표시됩니다.
- 게시판 설정 일괄 적용 화면의 입력 항목명과 검증 안내 문구, 신고 상태 변경 불가 안내, 권한 모드 선택 안내의 일본어 번역 추가.
## [1.0.2] - 2026-07-22
### Removed
- 화면에서 쓰이지 않던 첨부 안내(고정 5개·10MB)와 답변글 깊이 안내(1~5) 번역을 제거했습니다. 실제 상한과 다른 값이라 남겨 둘 이유가 없습니다.
### Fixed
@@ -18,6 +18,9 @@ return [
'update' => '編集',
'update_status' => 'ステータス変更',
'upload' => 'アップロード',
'add' => 'リアクション登録',
'change' => 'リアクション変更',
'remove' => 'リアクション取消',
],
'description' => [
'board_index' => '掲示板一覧閲覧',
@@ -57,6 +60,9 @@ return [
'board_settings_bulk_apply' => '掲示板設定一括適用',
'board_settings_bulk_apply_aborted' => '掲示板設定一括適用中止(全体ロールバック、失敗掲示板::failed_board_name、:failed_at/:total)',
'board_type_show' => '掲示板タイプ詳細閲覧 (:type_name)',
'reaction_add' => 'リアクション登録 (掲示板: :board_name, タイトル: :title, タイプ: :reaction_type)',
'reaction_change' => 'リアクション変更 (掲示板: :board_name, タイトル: :title, タイプ: :reaction_type)',
'reaction_remove' => 'リアクション取消 (掲示板: :board_name, タイトル: :title)',
],
'fields' => [
'name' => '掲示板名',
@@ -289,4 +289,16 @@ return [
'dashboard' => [
'fetch_success' => 'ダッシュボードデータを閲覧しました。',
],
'reaction' => [
'list_success' => 'リアクションタイプのリストを閲覧しました。',
'add_success' => 'リアクションを追加しました。',
'change_success' => 'リアクションを変更しました。',
'remove_success' => 'リアクションをキャンセルしました。',
'failed' => 'リアクション処理に失敗しました。',
'disabled' => 'この掲示板はリアクション機能が無効化されています。',
'inactive_type' => 'この掲示板では使用できないリアクションタイプです。',
'self_post' => '自分の投稿にはリアクションできません。',
'login_required' => 'ログインが必要な機能です。',
'secret_denied' => '閲覧権限がない秘密投稿には反応できません。',
],
];
@@ -286,6 +286,8 @@ return [
'spam_security.comment_cooldown_seconds' => 'コメント作成クールダウン(秒)',
'spam_security.report_cooldown_seconds' => '通報クールダウン(秒)',
'spam_security.view_count_cache_ttl' => '閲覧数キャッシュ有効期限(秒)',
'basic_defaults.use_reaction' => 'リアクション機能を使用',
'basic_defaults.active_reaction_types' => '使用するリアクションタイプ',
],
'post' => [
'title' => 'タイトル',
@@ -350,6 +352,9 @@ return [
'override_values.max_file_count' => '最大ファイル個数',
'override_values.new_display_hours' => '新規表示時間',
],
'reaction' => [
'reaction_type_id' => 'リアクションタイプ',
],
],
'blind' => [
'reason' => [
@@ -526,4 +531,10 @@ return [
'daily_limit_exceeded' => '本日の通報可能回数(:limit回)を超過しました。',
'rejection_limit_exceeded' => '最近 :days 日間の通報却下が :count 回累積され、通報が制限されました。',
],
'reaction' => [
'reaction_type_id' => [
'required' => 'リアクションタイプは必須です。',
'integer' => 'リアクションタイプは整数である必要があります。',
],
],
];
@@ -18,7 +18,8 @@
"reporters_list": "通報者一覧",
"reports": "通報一覧",
"roles": "ロール一覧",
"settings": "掲示板設定"
"settings": "掲示板設定",
"reactionTypes": "レスポンスタイプ"
}
},
"common": {
@@ -128,7 +128,9 @@
"channels_label": "通知チャネル",
"channel_mail": "メール",
"channel_database": "サイト通知"
}
},
"use_reaction": "リアクション使用",
"active_reaction_types": "使用するリアクションタイプを選択"
},
"placeholders": {
"name": "例:お知らせ",
@@ -146,7 +148,8 @@
"use_comment": "有効化するとコメント関連の詳細設定(長さ、深さ、並べ替え)を指定できます。",
"use_reply": "有効化すると回答投稿の最大深さを設定できます。",
"use_report": "不適切な投稿/コメントの通報機能を有効化します",
"type": "掲示板の種類は環境設定 > 基本設定で管理できます。"
"type": "掲示板の種類は環境設定 > 基本設定で管理できます。",
"use_reaction": "投稿に推薦/非推薦などのリアクションを残す機能を有効化します"
},
"options": {
"secret_mode": {
@@ -175,8 +175,11 @@
"use_file_upload": "ONに設定する必要があって、新しい掲示板にファイル添付機能が適用されます。",
"max_file_size": "アップロード可能な最大ファイルサイズです (MB単位、{{min}}~{{max}})",
"max_file_count": "投稿あたり添付可能な最大ファイル数です ({{min}}~{{max}})",
"allowed_extensions": "許可するファイル拡張子を最低1つ以上入力してください"
}
"allowed_extensions": "許可するファイル拡張子を最低1つ以上入力してください",
"use_reaction": "投稿に推薦/非推薦などのリアクションを残す機能を有効化します"
},
"use_reaction": "リアクション使用",
"active_reaction_types": "使用するリアクションタイプを選択"
},
"options": {
"date_display_format": {
@@ -298,8 +301,10 @@
"notify_author": "投稿者通知",
"new_display_hours": "新着表示時間",
"default_board_permissions": "デフォルト権限",
"manager": "掲示板管理"
}
"manager": "掲示板管理",
"use_reaction": "リアクション使用"
},
"select_boards_hint": "下記から一括適用する掲示板を検索して選択してください。"
},
"bulk_apply_modal": {
"title": "一括適用確認",
@@ -12,7 +12,7 @@
"en": "G7 module (sirsoft-board) Japanese language pack (bundled)",
"ja": "G7 モジュール (sirsoft-board) 日本語 言語パック(バンドル)"
},
"version": "1.0.5",
"version": "1.0.2",
"license": "MIT",
"scope": "module",
"target_identifier": "sirsoft-board",
@@ -20,10 +20,12 @@
### Added
- 게시글 반응(추천/비추천) 버튼의 로그인 필요 안내·처리 실패 안내 문구(`board.reaction.login_required`, `board.reaction.failed`) 일본어 번역 추가 — 반응 버튼 조작 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
- 회원가입 화면의 휴대폰번호·전화번호 입력란 라벨·안내 문구(`auth.mobile`, `auth.phone`) 일본어 번역 추가 — 회원가입 시 연락처 입력란이 일본어 로케일에서 자연스럽게 표시됩니다.
- 모달 닫기 버튼의 안내 라벨(`common.close_modal`) 일본어 번역 추가 — 화면 낭독기 사용자에게 버튼 용도가 일본어로 안내됩니다.
- 검색 결과 건수 표기와 검색어 구체화 안내(`search.result_count_suffix_at_least`, `search.refine_query_hint`) 일본어 번역 추가 — 총 건수를 상한까지만 센 경우의 "N건 이상" 표기가 일본어 로케일에서 자연스럽게 표시됩니다.
- 편집기 화면 문구(`editor.*`) 일본어 번역 보강.
- 본인 글에 반응 버튼을 눌렀을 때의 안내 문구(`board.reaction.self_post_denied`) 일본어 번역 추가 — 본인 글 반응 시도 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
## [1.0.1] - 2026-07-08
@@ -137,7 +137,10 @@
"laugh": "面白い",
"agree": "同意します",
"thanks": "ありがとうございます",
"wow": "驚きました"
"wow": "驚きました",
"login_required": "ログインが必要な機能です",
"failed": "応答処理に失敗しました。しばらく後にもう一度お試しください。",
"self_post_denied": "自分の投稿には反応できません。"
},
"attachments": {
"title": "添付ファイル",
@@ -13,6 +13,7 @@
### Added
- 게시글에 추천·비추천 반응을 남길 수 있는 기능을 추가했습니다. 로그인한 회원은 게시글 상세에서 반응을 남기거나 다른 반응으로 바꾸거나 취소할 수 있고, 각 반응의 개수가 항상 함께 표시됩니다. 본인이 쓴 글에는 반응할 수 없으며, 열람 권한이 없어 본문을 볼 수 없는 비밀글에는 반응 버튼이 표시되지 않고 반응도 남길 수 없습니다. 게시판 관리 설정에서 반응 사용 여부와 사용할 반응 유형(추천·비추천)을 게시판별로 켜고 끌 수 있으며, 새 게시판을 만들 때 적용될 기본값도 환경설정에서 지정합니다.
- 사이트맵에 담을 게시판·게시글 수의 안전 상한을 지원합니다. 상한을 설정하면 게시판 목록·게시판·게시글 어느 항목이든 그 수를 넘지 않으며, 상한에 걸려 일부가 빠진 경우 기록으로 남습니다.
- 게시글·게시판을 공개/비공개로 바꾸거나 삭제하면 사이트맵에 해당 항목만 자동으로 반영됩니다. 전체를 다시 만들지 않고 바뀐 부분만 갱신하므로 게시글이 많은 사이트에서도 빠르게 최신 상태가 유지됩니다.
- 게시판 알림 설정 화면에서도 코어와 동일하게 카카오 알림톡 템플릿을 연결할 수 있습니다(비즈뿌리오 플러그인 설치 시).
@@ -42,6 +43,7 @@
- 관리자 게시판 목록이 삭제글을 포함해 열릴 때마다 전체 건수를 다시 세던 것을 저장해 두고 재사용하도록 바꿨습니다.
- 답변글이 아주 많은 글을 열 때 답변 전체를 한꺼번에 읽던 것에 상한을 두고, 답변을 트리로 묶는 처리도 개선했습니다.
- 한 글의 댓글을 무제한으로 읽던 것에 상한을 두고, 같은 댓글을 두 번 가져오던 부분을 없앴습니다.
- 게시판 환경설정의 "기존 게시판에 일괄 적용" 화면을 정리했습니다. 대상 게시판(전체/선택)을 카드 형태로 눈에 띄게 표시하고, "선택한 게시판"을 고르면 검색 영역 위에 안내 문구가 함께 표시됩니다. 적용할 항목이나 대상 게시판을 고르지 않고 일괄 적용을 누르면 화면에 계속 남는 안내문 대신 토스트로 즉시 알려줍니다.
### Fixed
@@ -19,6 +19,8 @@
"comment_order": "ASC",
"show_view_count": true,
"use_report": false,
"use_reaction": true,
"active_reaction_types": ["like"],
"min_title_length": 2,
"max_title_length": 200,
"min_content_length": 2,
@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 게시판 반응 유형 테이블 생성
*
* 반응(추천/비추천 등) 유형을 DB로 관리한다 (Enum 코드 고정 아님).
* 향후 커뮤니티별 커스텀 명칭·관리자 CRUD 확장을 위한 구조 확보.
*/
public function up(): void
{
Schema::create('board_reaction_types', function (Blueprint $table) {
$table->id()->comment('반응 유형 ID');
$table->string('code', 50)->unique()->comment('내부 식별자 (예: like, dislike)');
$table->text('name')->comment('다국어 라벨 JSON ({"ko":"추천","en":"Recommend","ja":"おすすめ"})');
$table->string('icon')->nullable()->comment('Font Awesome 아이콘 클래스 (예: fas fa-thumbs-up)');
$table->unsignedInteger('display_order')->default(0)->comment('표시 순서');
$table->boolean('is_active')->default(true)->comment('활성 여부 (완전 삭제 미지원, 비활성화만 가능)');
$table->text('user_overrides')->nullable()->comment('사용자 수정 보존 필드 (언어팩 시드 머지 시 사용자 편집값 유지)');
$table->timestamps();
$table->index('is_active', 'idx_reaction_type_active');
$table->index('display_order', 'idx_reaction_type_order');
});
if (DB::getDriverName() == 'mysql') {
Schema::table('board_reaction_types', function (Blueprint $table) {
$table->comment('게시판 반응 유형 (추천/비추천 등)');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('board_reaction_types');
}
};
@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 게시판 반응 이력 테이블 생성
*
* 사용자당 대상(게시글)에 1행. 등록=INSERT, 전환=UPDATE, 해제=DELETE.
* 신고(boards_reports)처럼 target_type/target_id 폴리모픽 구조를 따르되,
* 케이스/로그 2테이블로 나누지 않고 1테이블로 처리한다 (반응엔 처리 상태 개념 없음).
*/
public function up(): void
{
Schema::create('board_reactions', function (Blueprint $table) {
$table->id()->comment('반응 이력 ID');
$table->unsignedBigInteger('user_id')->comment('반응한 사용자 ID');
$table->string('target_type', 20)->comment('반응 대상 타입 (현재 post, 향후 comment 확장 가능)');
$table->unsignedBigInteger('target_id')->comment('반응 대상 ID (동적 테이블 ID, FK 없이 앱 레벨 무결성 관리)');
$table->unsignedBigInteger('reaction_type_id')->comment('반응 유형 ID');
$table->unsignedBigInteger('board_id')->nullable()->comment('게시판 ID (게시판 삭제 시 NULL)');
$table->timestamps();
$table->unique(['user_id', 'target_type', 'target_id'], 'unique_user_target_reaction');
$table->index(['target_type', 'target_id'], 'idx_reaction_target');
$table->index('reaction_type_id', 'idx_reaction_type');
$table->index('board_id', 'idx_reaction_board');
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
$table->foreign('reaction_type_id')->references('id')->on('board_reaction_types')->restrictOnDelete();
$table->foreign('board_id')->references('id')->on('boards')->nullOnDelete();
});
if (DB::getDriverName() == 'mysql') {
Schema::table('board_reactions', function (Blueprint $table) {
$table->comment('게시판 반응 이력 (사용자+대상당 1행)');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('board_reactions');
}
};
@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* boards 테이블에 반응 사용 여부 컬럼 추가
*
* use_comment/use_report와 동일 패턴 — 게시판별 반응 기능 on/off.
*/
public function up(): void
{
Schema::table('boards', function (Blueprint $table) {
if (! Schema::hasColumn('boards', 'use_reaction')) {
$table->boolean('use_reaction')
->default(true)
->after('use_report')
->comment('반응(추천/비추천) 사용 여부');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasTable('boards')) {
$columns = Schema::getColumnListing('boards');
Schema::table('boards', function (Blueprint $table) use ($columns) {
if (in_array('use_reaction', $columns)) {
$table->dropColumn('use_reaction');
}
});
}
}
};
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* boards 테이블에 활성 반응 유형 목록 컬럼 추가
*
* 게시판별로 켠 반응 유형의 code(문자열) 목록을 JSON 배열로 저장한다.
* 예: ["like","dislike"]. ID가 아닌 code로 저장하는 이유는 시더 실행 순서에
* 따라 ID가 환경마다 달라질 수 있어서다 (설정값은 사람이 읽는 code로 통일).
*/
public function up(): void
{
Schema::table('boards', function (Blueprint $table) {
if (! Schema::hasColumn('boards', 'active_reaction_types')) {
$table->text('active_reaction_types')
->nullable()
->after('use_reaction')
->comment('활성화된 반응 유형 code 목록 JSON 배열 (예: ["like","dislike"])');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasTable('boards')) {
$columns = Schema::getColumnListing('boards');
Schema::table('boards', function (Blueprint $table) use ($columns) {
if (in_array('active_reaction_types', $columns)) {
$table->dropColumn('active_reaction_types');
}
});
}
}
};
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* board_posts 테이블에 반응 카운트 통합 컬럼 추가
*
* 유형별 반응 개수를 JSON 하나로 통합 저장한다. 키는 유형 ID 문자열.
* 예: {"1":18,"2":2}. 라벨/code가 바뀌어도 카운트가 끊기지 않도록 ID를 키로 쓴다.
* 2026_04_17_000002 패턴(hasColumn 가드 + after + 한국어 comment + down) 재사용.
*/
public function up(): void
{
Schema::table('board_posts', function (Blueprint $table) {
if (! Schema::hasColumn('board_posts', 'reaction_counts')) {
$table->text('reaction_counts')
->nullable()
->after('attachments_count')
->comment('반응 유형별 개수 JSON (키는 유형 ID, 예: {"1":18,"2":2})');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasTable('board_posts')) {
$columns = Schema::getColumnListing('board_posts');
Schema::table('board_posts', function (Blueprint $table) use ($columns) {
if (in_array('reaction_counts', $columns)) {
$table->dropColumn('reaction_counts');
}
});
}
}
};
@@ -0,0 +1,90 @@
<?php
namespace Modules\Sirsoft\Board\Database\Seeders;
use App\Concerns\Seeder\HasTranslatableSeeder;
use App\Contracts\Seeder\TranslatableSeederInterface;
use App\Extension\Helpers\GenericEntitySyncHelper;
use Illuminate\Database\Seeder;
use Modules\Sirsoft\Board\Models\ReactionType;
/**
* 게시판 반응 유형 초기 시더.
*
* 관리자 CRUD 화면이 없으므로(이슈 #525 확정 01) 이 시더가 유일한 유형 등록 경로다.
* 라벨 기본값은 ko/en/ja를 직접 하드코딩하되, 활성 언어팩의 seed/reaction_types.json
* 다국어 키를 trait 이 자동 머지한다 (BoardTypeSeeder 와 동일 패턴).
*
* code로 매칭하는 upsert(user_overrides 보존)라 재실행해도 중복 생성되지 않는다.
*/
class BoardReactionTypeSeeder extends Seeder implements TranslatableSeederInterface
{
use HasTranslatableSeeder;
public function getExtensionIdentifier(): string
{
return 'sirsoft-board';
}
public function getTranslatableEntity(): string
{
return 'reaction_types';
}
public function getMatchKey(): string
{
return 'code';
}
/**
* @return array<int, array<string, mixed>>
*/
public function getDefaults(): array
{
return [
[
'code' => 'like',
'name' => ['ko' => '추천', 'en' => 'Recommend', 'ja' => 'おすすめ'],
'icon' => 'fas fa-thumbs-up',
'display_order' => 1,
'is_active' => true,
],
[
'code' => 'dislike',
'name' => ['ko' => '비추천', 'en' => 'Not Recommend', 'ja' => 'ひどい'],
'icon' => 'fas fa-thumbs-down',
'display_order' => 2,
'is_active' => true,
],
];
}
/**
* 시더 실행
*/
public function run(): void
{
$helper = app(GenericEntitySyncHelper::class);
foreach ($this->resolveTranslatedDefaults() as $reactionType) {
$existing = ReactionType::where('code', $reactionType['code'])->exists();
$helper->sync(
ReactionType::class,
['code' => $reactionType['code']],
[
'name' => $reactionType['name'],
'icon' => $reactionType['icon'],
'display_order' => $reactionType['display_order'],
'is_active' => $reactionType['is_active'],
],
);
if ($existing) {
$this->command->info(" 반응 유형 '{$reactionType['code']}' 동기화 (사용자 수정 보존).");
} else {
$this->command->info(" 반응 유형 '{$reactionType['code']}' 생성 완료.");
}
}
}
}
@@ -44,9 +44,10 @@ class DatabaseSeeder extends Seeder
$this->command->info('');
// 설치 필수 시더 (항상 실행)
$this->command->info('[설치] 게시판 타입 생성');
$this->command->info('[설치] 게시판 타입 / 반응 유형 생성');
$this->call([
BoardTypeSeeder::class,
BoardReactionTypeSeeder::class,
]);
$this->command->info('');
@@ -15,6 +15,7 @@
| [boards.md](boards.md) | `boards` | 37 |
| [dashboard.md](dashboard.md) | `dashboard` | 4 |
| [my-comments.md](my-comments.md) | `my-comments` | 1 |
| [reaction.md](reaction.md) | `reaction` | 2 |
| [reports.md](reports.md) | `reports` | 7 |
| [settings.md](settings.md) | `settings` | 5 |
| [users.md](users.md) | `users` | 2 |
@@ -893,6 +893,8 @@ _단건 응답: `data` 객체의 필드._
| attachments | null | `null` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). attachments 관계가 로드된 경우에만 채워지며, 아니면 null(비밀글·삭제글은 권한에 따라 빈 배열 또는 연쇄 삭제분만 노출). |
| replies | null | `null` | 이 게시글에 달린 답변글 목록(PostResource 컬렉션, 재귀). replies 관계가 로드된 경우에만 채워지며, 아니면 null. |
| is_already_reported | boolean | `false` | already reported 여부 |
| reaction_counts | object | `{"1":18,"2":2}` | 게시판이 켠 활성 반응 유형별 개수 맵(JSON 객체). 키는 유형 ID 문자열, 개수 0인 유형도 포함됩니다. board 관계 미로드 시 빈 객체(`{}`). |
| my_reaction_type_id | integer\|null | `1` | 현재 로그인 사용자가 이 게시글에 남긴 반응 유형 ID. 반응이 없으면 null. |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
@@ -1055,6 +1057,8 @@ _단건 응답: `data` 객체의 필드._
| attachments | array | `[{"id":155,"hash":"apidocsmpl1","original_filename":"apid…` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). 비밀글은 열람 권한이 없으면 빈 배열, 삭제된 게시글은 관리 권한이 없으면 연쇄 삭제된 첨부만 노출됩니다. |
| replies | array | `[]` | 이 게시글에 달린 답변글 목록(PostResource 컬렉션, 재귀). replies 관계가 로드된 경우에만 채워지며, 아니면 null. |
| is_already_reported | boolean | `false` | already reported 여부 |
| reaction_counts | object | `{"1":18,"2":2}` | 게시판이 켠 활성 반응 유형별 개수 맵(JSON 객체). 키는 유형 ID 문자열, 개수 0인 유형도 포함됩니다. board 관계 미로드 시 빈 객체(`{}`). |
| my_reaction_type_id | integer\|null | `1` | 현재 로그인 사용자가 이 게시글에 남긴 반응 유형 ID. 반응이 없으면 null. |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
@@ -1312,6 +1316,8 @@ _단건 응답: `data` 객체의 필드._
| attachments | null | `null` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). attachments 관계가 로드된 경우에만 채워지며, 아니면 null(비밀글·삭제글은 권한에 따라 빈 배열 또는 연쇄 삭제분만 노출). |
| replies | null | `null` | 이 게시글에 달린 답변글 목록(PostResource 컬렉션, 재귀). replies 관계가 로드된 경우에만 채워지며, 아니면 null. |
| is_already_reported | boolean | `false` | already reported 여부 |
| reaction_counts | object | `{"1":18,"2":2}` | 게시판이 켠 활성 반응 유형별 개수 맵(JSON 객체). 키는 유형 ID 문자열, 개수 0인 유형도 포함됩니다. board 관계 미로드 시 빈 객체(`{}`). |
| my_reaction_type_id | integer\|null | `1` | 현재 로그인 사용자가 이 게시글에 남긴 반응 유형 ID. 반응이 없으면 null. |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
@@ -1471,6 +1477,8 @@ _단건 응답: `data` 객체의 필드._
| attachments | null | `null` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). attachments 관계가 로드된 경우에만 채워지며, 아니면 null(비밀글·삭제글은 권한에 따라 빈 배열 또는 연쇄 삭제분만 노출). |
| replies | null | `null` | 이 게시글에 달린 답변글 목록(PostResource 컬렉션, 재귀). replies 관계가 로드된 경우에만 채워지며, 아니면 null. |
| is_already_reported | boolean | `false` | already reported 여부 |
| reaction_counts | object | `{"1":18,"2":2}` | 게시판이 켠 활성 반응 유형별 개수 맵(JSON 객체). 키는 유형 ID 문자열, 개수 0인 유형도 포함됩니다. board 관계 미로드 시 빈 객체(`{}`). |
| my_reaction_type_id | integer\|null | `1` | 현재 로그인 사용자가 이 게시글에 남긴 반응 유형 ID. 반응이 없으면 null. |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
@@ -2078,6 +2086,8 @@ _단건 응답: `data` 객체의 필드._
| is_author | boolean | `true` | author 여부 |
| is_guest_comment | boolean | `false` | guest comment 여부 |
| is_already_reported | boolean | `false` | already reported 여부 |
| reaction_counts | object | `{"1":18,"2":2}` | 게시판이 켠 활성 반응 유형별 개수 맵(JSON 객체). 키는 유형 ID 문자열, 개수 0인 유형도 포함됩니다. board 관계 미로드 시 빈 객체(`{}`). |
| my_reaction_type_id | integer\|null | `1` | 현재 로그인 사용자가 이 게시글에 남긴 반응 유형 ID. 반응이 없으면 null. |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_write":true,"can_manage":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
@@ -134,6 +134,8 @@ HTTP/1.1 200
| use_comment | body | boolean | 예 | — | 댓글 기능 사용 여부 |
| use_reply | body | boolean | 예 | — | 게시글 답변(원글에 대한 답글) 기능 사용 여부 |
| use_report | body | boolean | 예 | — | 게시글/댓글 신고 기능 사용 여부 |
| use_reaction | body | boolean | 예 | — | 게시글 반응(추천/비추천) 기능 사용 여부 |
| active_reaction_types | body | array | 아니오 | — | 이 게시판에서 켠 반응 유형 code 목록 (문자열 배열, 예: `["like","dislike"]`) |
| comment_order | body | string | 예 | `ASC`, `DESC` | 댓글 정렬 순서 (ASC 오름차순 / DESC 내림차순) |
| new_display_hours | body | integer | 아니오 | min 0, max 720 | 신규(NEW) 표시를 유지할 기간 (시간 단위, 최대 720시간=30일). `0` 은 NEW 배지를 표시하지 않음 |
| min_title_length | body | integer | 아니오 | min 0, max 200 | 게시글 제목 최소 글자 수 |
@@ -394,6 +396,8 @@ _단건 응답: `data` 객체의 필드._
| comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) |
| show_view_count | boolean | `true` | 조회수 노출 |
| use_report | boolean | `false` | 게시글/댓글 신고 기능 사용 |
| use_reaction | boolean | `true` | 게시글 반응(추천/비추천) 기능 사용 |
| active_reaction_types | array | `["like","dislike"]` | 이 게시판에서 켠 반응 유형 code 목록. 생성 모드에서는 환경설정 기본값(`basic_defaults.active_reaction_types`)이 채워짐 |
| min_title_length | integer | `2` | 최소 제목 글자 수 |
| max_title_length | integer | `200` | 최대 제목 글자 수 |
| min_content_length | integer | `2` | 최소 게시글 글자 수 |
@@ -1251,6 +1255,8 @@ HTTP/1.1 200
| use_comment | body | boolean | 예 | — | 댓글 기능 사용 여부 |
| use_reply | body | boolean | 예 | — | 게시글 답변(원글에 대한 답글) 기능 사용 여부 |
| use_report | body | boolean | 예 | — | 게시글/댓글 신고 기능 사용 여부 |
| use_reaction | body | boolean | 예 | — | 게시글 반응(추천/비추천) 기능 사용 여부 |
| active_reaction_types | body | array | 아니오 | — | 이 게시판에서 켠 반응 유형 code 목록 (문자열 배열, 예: `["like","dislike"]`) |
| comment_order | body | string | 예 | `ASC`, `DESC` | 댓글 정렬 순서 (ASC 오름차순 / DESC 내림차순) |
| new_display_hours | body | integer | 아니오 | min 0, max 720 | 신규(NEW) 표시를 유지할 기간 (시간 단위, 최대 720시간=30일). `0` 은 NEW 배지를 표시하지 않음 |
| min_title_length | body | integer | 아니오 | min 0, max 200 | 게시글 제목 최소 글자 수 |
@@ -0,0 +1,135 @@
# Reaction API 레퍼런스
> **소유**: module `sirsoft-board` · 게시글 반응(추천/비추천) 관련 엔드포인트 레퍼런스입니다.
---
## TL;DR (5초 요약)
```text
1. 게시글 반응(추천/비추천) 등록·전환·해제를 단일 POST 엔드포인트로 처리합니다
2. 반응은 로그인 회원만 가능하며(auth:sanctum), 본인 글에는 반응할 수 없습니다
3. 글당 반응 1개 — 같은 유형 재요청은 해제, 다른 유형은 전환(이전 -1·신규 +1)
4. 관리자 반응 유형 목록 API는 게시판 설정 화면의 유형 체크박스 옵션 소스입니다
5. 유형 CRUD 는 이번 범위 밖 — 유형은 시더/스크립트로 관리합니다
```
---
## POST /api/modules/sirsoft-board/boards/{slug}/posts/{postId}/react
게시글에 반응(추천/비추천)을 남깁니다. 등록·전환·해제를 한 엔드포인트가 통합 처리합니다.
- **라우트명**: `api.modules.sirsoft-board.boards.posts.react`
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\ReactionController@react`
- **인증/권한**: `auth:sanctum` (로그인 회원 전용)
**경로 파라미터**
| 이름 | 위치 | 타입 | 필수 | 용도 |
| --- | --- | --- | --- | --- |
| slug | path | string | 예 | 게시판 slug |
| postId | path | integer | 예 | 반응 대상 게시글 ID (해당 게시판 소속이어야 함) |
**요청 파라미터**
| 이름 | 위치 | 타입 | 필수 | 용도 |
| --- | --- | --- | --- | --- |
| reaction_type_id | body | integer | 예 | 반응 유형 ID. 존재하는 활성 유형이면서 게시판이 켠(활성) 유형이어야 합니다. |
**요청 예시**
```http
POST /api/modules/sirsoft-board/boards/free/posts/42/react HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
Content-Type: application/json
{
"reaction_type_id": 1
}
```
**동작**
| 상황 | 결과 (`data.action`) | 카운트 변화 |
| --- | --- | --- |
| 기존 반응 없음 | `add` | 해당 유형 +1 |
| 기존 반응이 다른 유형 | `change` | 이전 유형 -1 · 신규 유형 +1 |
| 기존 반응이 같은 유형 | `remove` | 해당 유형 -1 (이력 행 삭제) |
**응답 필드** (`data` 내부)
| 필드 | 타입 | 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| action | string | `add` | 수행된 동작 (`add`/`change`/`remove`) |
| my_reaction_type_id | integer\|null | `1` | 처리 후 내가 누른 반응 유형 ID. 해제 시 `null`. |
| reaction_counts | object | `{"1":18,"2":2}` | 게시판이 켠 활성 유형 전체의 개수 맵. 키는 유형 ID 문자열, 개수 0인 유형도 포함됩니다. |
**응답 예시**
```json
{
"success": true,
"message": "반응을 남겼습니다.",
"data": {
"action": "add",
"my_reaction_type_id": 1,
"reaction_counts": { "1": 18, "2": 2 }
}
}
```
**오류**
| 상태 | 사유 |
| --- | --- |
| 401 | 비로그인 요청 |
| 404 | 게시글이 해당 게시판 소속이 아니거나 존재하지 않음 |
| 422 | 반응 기능이 꺼진 게시판 / 게시판이 켜지 않은 유형 / 본인 글 반응 / `reaction_type_id` 검증 실패 |
---
## GET /api/modules/sirsoft-board/admin/reaction-types
활성 반응 유형 전체를 `display_order` 순으로 반환합니다. 게시판 설정 화면의 "사용할 반응 유형" 체크박스 옵션 소스입니다. 유형 CRUD 는 제공하지 않습니다.
- **라우트명**: `api.modules.sirsoft-board.admin.reaction-types.index`
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReactionTypeController@index`
- **인증/권한**: `auth:sanctum` + `admin` + `permission:sirsoft-board.settings.read`
**요청 예시**
```http
GET /api/modules/sirsoft-board/admin/reaction-types HTTP/1.1
Host: api.example.com
Accept: application/json
Accept-Language: ko
Authorization: Bearer {YOUR_TOKEN}
```
**응답 필드** (`data.reaction_types[]` 배열 항목)
| 필드 | 타입 | 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 반응 유형 ID |
| code | string | `like` | 내부 식별자 (예: `like`/`dislike`) |
| name | string | `추천` | 현재 로케일 라벨. 미입력 언어는 사이트 기본 언어로 폴백됩니다. |
| icon | string\|null | `fas fa-thumbs-up` | 저장된 원본 Font Awesome 클래스 (스타일 접두사 포함) |
| icon_name | string\|null | `fa-thumbs-up` | Icon 컴포넌트 `name` prop 용 토큰 (스타일 접두사 제거). Icon 컴포넌트가 접두사를 자체 부착하므로 `icon` 을 그대로 넘기면 접두사가 중복됩니다. |
**응답 예시**
```json
{
"success": true,
"message": "반응 유형 목록을 조회했습니다.",
"data": {
"reaction_types": [
{ "id": 1, "code": "like", "name": "추천", "icon": "fas fa-thumbs-up", "icon_name": "fa-thumbs-up" },
{ "id": 2, "code": "dislike", "name": "비추천", "icon": "fas fa-thumbs-down", "icon_name": "fa-thumbs-down" }
]
}
}
```
@@ -361,7 +361,7 @@ HTTP/1.1 200
| --- | --- | --- | --- | --- | --- |
| _tab | body | string | 아니오 | `basic_defaults`, `report_policy`, `spam_security`, `general`, `seo`, `notifications`, `notification_definitions` | 현재 편집 중인 탭을 나타내는 메타 값. 탭 단위 부분 저장의 컨텍스트를 식별하는 용도이며 설정값으로는 저장되지 않습니다. |
| notifications | body | array | 아니오 | — | 알림 채널 설정. `channels` 배열의 각 항목에 채널 식별자(id), 활성화 여부(is_active), 정렬 순서(sort_order)를 담아 저장합니다. |
| basic_defaults | body | array | 아니오 | — | 기본 설정 카테고리 값. 게시판 타입·페이지당 글 수·정렬·댓글/답글·길이 제한·파일 업로드·기본 권한 등 basic_defaults 하위 키를 저장합니다. `basic_defaults.allowed_extensions` 는 `basic_defaults.use_file_upload` 가 `true` 일 때만 최소 1개가 필수이며, `false`/`null` 이면 검증에서 제외되어 빈 배열도 허용됩니다. `min_title_length`·`min_comment_length`·`new_display_hours` 의 하한은 `config('sirsoft-board.limits')` 선언을 따르며 `0` 을 허용합니다. |
| basic_defaults | body | array | 아니오 | — | 기본 설정 카테고리 값. 게시판 타입·페이지당 글 수·정렬·댓글/답글·길이 제한·파일 업로드·기본 권한 등 basic_defaults 하위 키를 저장합니다. 반응 기능 기본값으로 `basic_defaults.use_reaction`(boolean) 과 `basic_defaults.active_reaction_types`(반응 유형 code 문자열 배열)를 포함하며, 새 게시판 생성 시 이 값이 개별 게시판의 초기값으로 시드됩니다. `basic_defaults.allowed_extensions` 는 `basic_defaults.use_file_upload` 가 `true` 일 때만 최소 1개가 필수이며, `false`/`null` 이면 검증에서 제외되어 빈 배열도 허용됩니다. `min_title_length`·`min_comment_length`·`new_display_hours` 의 하한은 `config('sirsoft-board.limits')` 선언을 따르며 `0` 을 허용합니다. |
| report_policy | body | array | 아니오 | — | 신고 정책 카테고리 값. 자동 숨김 임계치/대상, 일일 신고 한도, 거부 누적 제한, 관리자·작성자 신고 알림 설정을 저장합니다. |
| report_policy.auto_hide_threshold | body | integer | 아니오 | min 0, max 100 | 자동 숨김 신고 수 (이 횟수 이상 신고되면 자동 숨김 처리) |
| report_policy.auto_hide_target | body | string | 아니오 | `post`, `comment`, `both` | 자동 숨김 대상 (게시글 / 댓글 / 둘 다) |
@@ -0,0 +1,70 @@
// e2e:allow 반응 사용 토글 + 유형 체크박스 저장은 본 레이아웃 회귀테스트(vitest) + MCP 실브라우저 검증(토글/체크/저장)으로 커버. 게시판 환경설정 화면 Playwright 인프라 부재
/**
* 게시판 반응(추천/비추천) 설정 UI 회귀 가드 (이슈 #525 확정 02·11·13)
*
* @description
* 고정 대상:
* 1) 환경설정 기본값 탭(_tab_board_settings_basic)에 use_reaction 토글이 있고
* basic_defaults.use_reaction 에 바인딩된다 (확정 13, 새 게시판 기본값).
* 2) 유형 체크박스 서브패널은 use_reaction on 일 때만 노출되며,
* reactionTypes 데이터소스를 iteration 해 basic_defaults.active_reaction_types(code 배열)를
* 토글한다 (확정 02·11, DB 유형 목록 동적 렌더).
* 3) 개별 게시판 편집 폼(admin_board_form/_tab_basic)에도 동일 구조가 use_reaction /
* active_reaction_types 에 바인딩되어 존재한다 (게시판별 override).
*
* @vitest-environment node
*/
import { describe, it, expect } from 'vitest';
import settingsTab from '../../../layouts/admin/partials/admin_board_settings/_tab_board_settings_basic.json';
import formTab from '../../../layouts/admin/partials/admin_board_form/_tab_basic.json';
const settingsJson = JSON.stringify(settingsTab);
const formJson = JSON.stringify(formTab);
describe('환경설정 기본값 탭 — 반응 사용 토글 + 유형 체크박스', () => {
// @scenario case=settings_toggle_and_checkbox_persist
// @effects settings_toggle_persists_use_reaction
it('use_reaction 토글이 basic_defaults.use_reaction 에 바인딩된다', () => {
expect(/"name":\s*"basic_defaults\.use_reaction"/.test(settingsJson)).toBe(true);
});
it('유형 체크박스 서브패널은 use_reaction on 일 때만 노출된다', () => {
expect(/_local\.form\?\.basic_defaults\?\.use_reaction === true/.test(settingsJson)).toBe(true);
});
it('reactionTypes 데이터소스를 iteration 해 유형을 동적 렌더한다', () => {
expect(/reactionTypes\?\.data\?\.reaction_types/.test(settingsJson)).toBe(true);
expect(/"item_var":\s*"reactionType"/.test(settingsJson)).toBe(true);
});
// @scenario case=settings_toggle_and_checkbox_persist
// @effects settings_checkbox_persists_active_reaction_types
it('체크박스가 active_reaction_types(code 배열) 를 include/exclude 로 토글한다', () => {
// 체크 상태: code 포함 여부
expect(
/\(_local\.form\?\.basic_defaults\?\.active_reaction_types \?\? \[\]\)\.includes\(reactionType\.code\)/.test(
settingsJson,
),
).toBe(true);
// 저장: 포함 시 filter 제거, 미포함 시 spread 추가 (upsert 토글)
expect(/"form\.basic_defaults\.active_reaction_types"/.test(settingsJson)).toBe(true);
expect(/filter\(c => c !== reactionType\.code\)/.test(settingsJson)).toBe(true);
});
});
describe('개별 게시판 편집 폼 — 반응 사용 토글 + 유형 체크박스 (override)', () => {
it('use_reaction 토글이 use_reaction 에 바인딩된다', () => {
expect(/"name":\s*"use_reaction"/.test(formJson)).toBe(true);
});
it('유형 체크박스는 use_reaction on 일 때만 노출되고 active_reaction_types 를 토글한다', () => {
expect(/_local\.form\?\.use_reaction === true/.test(formJson)).toBe(true);
expect(/reactionTypes\?\.data\?\.reaction_types/.test(formJson)).toBe(true);
expect(
/\(_local\.form\?\.active_reaction_types \?\? \[\]\)\.includes\(reactionType\.code\)/.test(formJson),
).toBe(true);
expect(/"form\.active_reaction_types"/.test(formJson)).toBe(true);
});
});
@@ -2,6 +2,7 @@
"editor": {
"data_source": {
"availableChannels": "Available Channels",
"reactionTypes": "Reaction Types",
"boardIdentityPolicies": "Board Identity Policies",
"boardIdentityPurposes": "Board Identity Purposes",
"boardNotificationDefinitions": "Board Notification Definitions",
@@ -2,6 +2,7 @@
"editor": {
"data_source": {
"availableChannels": "사용 가능 채널",
"reactionTypes": "반응 유형",
"boardIdentityPolicies": "게시판 본인인증 정책",
"boardIdentityPurposes": "게시판 본인인증 목적",
"boardNotificationDefinitions": "게시판 알림 정의",
@@ -23,6 +23,8 @@
"use_comment": "Enable Comments",
"use_reply": "Enable Reply Posts",
"use_report": "Enable Reports",
"use_reaction": "Enable Reactions",
"active_reaction_types": "Select Reaction Types",
"secret_mode": "Secret Mode",
"order_by": {
"label": "Sort By",
@@ -146,6 +148,7 @@
"use_comment": "When enabled, you can configure detailed comment settings (length, depth, order).",
"use_reply": "When enabled, you can set the maximum reply depth.",
"use_report": "Enable report feature for inappropriate posts/comments",
"use_reaction": "Enable reactions such as recommend/not recommend on posts",
"type": "Board types can be managed in Settings > Basic Settings."
},
"options": {
@@ -95,6 +95,8 @@
"notify_author": "Author Notification",
"notify_author_description": "Send email to the original author when a comment or reply is posted",
"use_report": "Enable Reports",
"use_reaction": "Enable Reactions",
"active_reaction_types": "Select Reaction Types",
"auto_hide_threshold": "Auto Blind Threshold",
"auto_hide_threshold_hint": "Automatically blind content when reports reach this count. (0=disabled, 1\u2013100)",
"auto_hide_target": "Blind Target",
@@ -155,6 +157,7 @@
"secret_mode": "Set the default behavior for the secret post feature",
"show_view_count": "Display view count on post list and detail pages",
"use_report": "Enable reporting of inappropriate posts and comments",
"use_reaction": "Enable reactions such as recommend\/not recommend on posts",
"new_display_hours": "Duration to display [New] badge after posting (in hours)",
"per_page": "Number of posts to display per page on desktop ({{min}}–{{max}})",
"per_page_mobile": "Number of posts to display per page on mobile ({{min}}–{{max}})",
@@ -264,6 +267,7 @@
"target_boards": "Target Boards",
"apply_all": "All Boards",
"apply_selected": "Selected Boards",
"select_boards_hint": "Search and select the boards to apply this to below.",
"select_boards_placeholder": "Select boards",
"apply_button": "Bulk Apply",
"no_fields_selected": "No items selected.",
@@ -279,6 +283,7 @@
"secret_mode": "Secret Mode",
"use_comment": "Enable Comments",
"use_reply": "Enable Replies",
"use_reaction": "Enable Reactions",
"max_reply_depth": "Max Reply Depth",
"max_comment_depth": "Max Comment Depth",
"comment_order": "Comment Sort Order",
@@ -23,6 +23,8 @@
"use_comment": "댓글 사용",
"use_reply": "답변글 사용",
"use_report": "신고 사용",
"use_reaction": "반응 사용",
"active_reaction_types": "사용할 반응 유형 선택",
"secret_mode": "비밀글 모드",
"order_by": {
"label": "정렬 기준",
@@ -146,6 +148,7 @@
"use_comment": "활성화하면 댓글 관련 세부 설정(길이, 깊이, 정렬)을 지정할 수 있습니다.",
"use_reply": "활성화하면 답변글 최대 깊이를 설정할 수 있습니다.",
"use_report": "부적절한 게시글/댓글 신고 기능을 활성화합니다",
"use_reaction": "게시글에 추천/비추천 등 반응을 남기는 기능을 활성화합니다",
"type": "게시판 유형은 환경설정 > 기본설정에서 관리할 수 있습니다."
},
"options": {
@@ -95,6 +95,8 @@
"notify_author": "작성자 알림",
"notify_author_description": "댓글, 답변글 작성 시 원글 작성자에게 이메일 발송",
"use_report": "신고 사용",
"use_reaction": "반응 사용",
"active_reaction_types": "사용할 반응 유형 선택",
"auto_hide_threshold": "자동 숨김 기준 (회)",
"auto_hide_threshold_hint": "이 횟수 이상 신고가 접수되면 자동으로 블라인드 처리됩니다. (0=비활성, 1~100)",
"auto_hide_target": "적용 대상",
@@ -155,6 +157,7 @@
"secret_mode": "비밀글 기능의 기본 동작을 설정합니다",
"show_view_count": "게시글 목록 및 상세에서 조회수를 표시합니다",
"use_report": "부적절한 게시글\/댓글 신고 기능을 활성화합니다",
"use_reaction": "게시글에 추천\/비추천 등 반응을 남기는 기능을 활성화합니다",
"new_display_hours": "게시글 작성 후 [New] 배지를 표시할 기간 (시간 단위)",
"per_page": "PC 환경에서 한 페이지에 표시할 게시글 수입니다 ({{min}}~{{max}})",
"per_page_mobile": "모바일 환경에서 한 페이지에 표시할 게시글 수입니다 ({{min}}~{{max}})",
@@ -264,6 +267,7 @@
"target_boards": "대상 게시판",
"apply_all": "전체 게시판",
"apply_selected": "선택한 게시판",
"select_boards_hint": "아래에서 일괄 적용할 게시판을 검색해 선택하세요.",
"select_boards_placeholder": "게시판을 선택하세요",
"apply_button": "일괄 적용",
"no_fields_selected": "적용할 항목이 선택되지 않았습니다.",
@@ -279,6 +283,7 @@
"secret_mode": "비밀글 모드",
"use_comment": "댓글 사용",
"use_reply": "답변글 사용",
"use_reaction": "반응 사용",
"max_reply_depth": "답변글 최대 깊이",
"max_comment_depth": "대댓글 최대 깊이",
"comment_order": "댓글 정렬 순서",
@@ -82,6 +82,20 @@
"channels": []
}
}
},
{
"id": "reactionTypes",
"label_key": "$t:sirsoft-board.editor.data_source.reactionTypes",
"type": "api",
"endpoint": "/api/modules/sirsoft-board/admin/reaction-types",
"method": "GET",
"auto_fetch": true,
"auth_required": true,
"fallback": {
"data": {
"reaction_types": []
}
}
}
],
"slots": {
@@ -158,6 +158,21 @@
}
}
},
{
"id": "reactionTypes",
"label_key": "$t:sirsoft-board.editor.data_source.reactionTypes",
"type": "api",
"endpoint": "/api/modules/sirsoft-board/admin/reaction-types",
"method": "GET",
"if": "{{(query.tab || _global.activeBoardSettingsTab || 'general') === 'basic_defaults'}}",
"auto_fetch": true,
"auth_required": true,
"fallback": {
"data": {
"reaction_types": []
}
}
},
{
"id": "boardIdentityPolicies",
"label_key": "$t:sirsoft-board.editor.data_source.boardIdentityPolicies",
@@ -1,7 +1,8 @@
{
"meta": {
"is_partial": true,
"description": "Board Form - Basic Tab Content"
"description": "Board Form - Basic Tab Content",
"_e2e_allow": "e2e:allow 반응 유형 체크박스 시각 스타일(배경 박스·들여쓰기·아이콘·정렬)만 변경, 클릭 동작(setState/hasChanges)은 기존과 동일. 렌더 분기·바인딩은 admin-board-reaction-settings.test.tsx(Vitest 레이아웃 테스트)로 커버, MCP 실브라우저로 시각 회귀 확인."
},
"id": "section_basic_card",
"type": "basic",
@@ -161,7 +162,7 @@
"name": "Icon",
"props": {
"name": "circle-info",
"className": "text-secondary w-4 h-4 mt-0.5 flex-shrink-0"
"className": "text-secondary text-base mt-0.5 flex-shrink-0"
}
},
{
@@ -673,6 +674,138 @@
}
}
]
},
{
"id": "field_use_reaction",
"type": "basic",
"name": "Div",
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex-start justify-between"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex-1"
},
"children": [
{
"type": "basic",
"name": "Label",
"props": {
"className": "text-heading"
},
"text": "$t:sirsoft-board.admin.form.fields.use_reaction"
},
{
"type": "basic",
"name": "P",
"props": {
"className": "form-hint"
},
"text": "$t:sirsoft-board.admin.form.descriptions.use_reaction"
}
]
},
{
"type": "composite",
"name": "Toggle",
"props": {
"name": "use_reaction",
"disabled": "{{_computed.isReadOnly}}",
"className": "flex-shrink-0 ml-4"
}
}
]
},
{
"comment": "사용할 반응 유형 선택 — 반응 사용 on 일 때만 노출. DB 등록 유형 전체(reactionTypes)를 순회해 체크박스로 active_reaction_types(code 배열) 를 토글. 게시판 환경설정 화면과 동일하게 배경 박스 + 좌측 들여쓰기로 시각적 구분 (PO 피드백).",
"type": "basic",
"name": "Div",
"if": "{{_local.form?.use_reaction === true}}",
"props": {
"className": "ml-6 mt-2 p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg"
},
"children": [
{
"type": "basic",
"name": "P",
"props": {
"className": "form-hint mb-2"
},
"text": "$t:sirsoft-board.admin.form.fields.active_reaction_types"
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex flex-col gap-2"
},
"children": [
{
"type": "basic",
"name": "Div",
"iteration": {
"source": "{{reactionTypes?.data?.reaction_types ?? []}}",
"item_var": "reactionType",
"index_var": "reactionIdx"
},
"children": [
{
"type": "basic",
"name": "Label",
"props": {
"className": "flex-center gap-2 cursor-pointer"
},
"children": [
{
"type": "basic",
"name": "Input",
"props": {
"type": "checkbox",
"className": "checkbox-aligned",
"disabled": "{{_computed.isReadOnly}}",
"checked": "{{(_local.form?.active_reaction_types ?? []).includes(reactionType.code)}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"form.active_reaction_types": "{{(_local.form?.active_reaction_types ?? []).includes(reactionType.code) ? (_local.form?.active_reaction_types ?? []).filter(c => c !== reactionType.code) : [...(_local.form?.active_reaction_types ?? []), reactionType.code]}}",
"hasChanges": true
}
}
]
},
{
"type": "basic",
"name": "Icon",
"props": {
"name": "{{reactionType.icon_name ?? 'fa-thumbs-up'}}",
"size": "sm"
}
},
{
"type": "basic",
"name": "Span",
"text": "{{reactionType.name ?? reactionType.code}}"
}
]
}
]
}
]
}
]
}
]
}
]
}
@@ -1,7 +1,8 @@
{
"meta": {
"is_partial": true,
"description": "게시판 설정 > 기본 탭 (기본 설정 + 일괄 적용)"
"description": "게시판 설정 > 기본 탭 (기본 설정 + 일괄 적용)",
"_e2e_allow": "e2e:allow 반응 유형 체크박스 시각 스타일(배경 박스·들여쓰기·아이콘·정렬)만 변경, 클릭 동작(setState/hasChanges)은 기존과 동일. 렌더 분기·바인딩은 admin-board-reaction-settings.test.tsx(Vitest 레이아웃 테스트)로 커버, MCP 실브라우저로 시각 회귀 확인."
},
"id": "basic",
"type": "basic",
@@ -470,6 +471,163 @@
]
}
]
},
{
"id": "field_use_reaction",
"type": "basic",
"name": "Div",
"props": {
"className": ""
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex-center gap-4"
},
"children": [
{
"type": "basic",
"name": "Label",
"props": {
"className": "flex-start gap-2 text-heading flex-1 cursor-pointer"
},
"children": [
{
"type": "basic",
"name": "Input",
"props": {
"type": "checkbox",
"className": "checkbox-aligned",
"checked": "{{(_local.bulkApplyFields ?? []).includes('use_reaction')}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('use_reaction') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'use_reaction') : [...(_local.bulkApplyFields ?? []), 'use_reaction']}}"
}
}
]
},
{
"type": "basic",
"name": "Div",
"children": [
{
"type": "basic",
"name": "Span",
"text": "$t:sirsoft-board.admin.settings.fields.use_reaction"
},
{
"type": "basic",
"name": "P",
"props": {
"className": "text-field-hint"
},
"text": "$t:sirsoft-board.admin.settings.fields.descriptions.use_reaction"
}
]
}
]
},
{
"type": "composite",
"name": "Toggle",
"props": {
"name": "basic_defaults.use_reaction",
"className": "flex-shrink-0",
"disabled": "{{_computed.isReadOnly}}"
}
}
]
},
{
"comment": "사용할 반응 유형 선택 — 반응 사용 on 일 때만 노출. DB 등록 유형 전체(reactionTypes)를 순회해 체크박스로 basic_defaults.active_reaction_types(code 배열) 를 토글. 좌측 일괄적용 체크박스(bulkApplyFields)와 형태가 같아 혼동되기 쉬우므로, 배경 박스 + 좌측 들여쓰기로 '설정값 입력 영역'임을 시각적으로 분리한다 (PO 피드백).",
"type": "basic",
"name": "Div",
"if": "{{_local.form?.basic_defaults?.use_reaction === true}}",
"props": {
"className": "row-content-indent ml-6 mt-2 p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg"
},
"children": [
{
"type": "basic",
"name": "P",
"props": {
"className": "text-field-hint mb-2"
},
"text": "$t:sirsoft-board.admin.settings.fields.active_reaction_types"
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex flex-col gap-2"
},
"children": [
{
"type": "basic",
"name": "Div",
"iteration": {
"source": "{{reactionTypes?.data?.reaction_types ?? []}}",
"item_var": "reactionType",
"index_var": "reactionIdx"
},
"children": [
{
"type": "basic",
"name": "Label",
"props": {
"className": "flex-center gap-2 cursor-pointer"
},
"children": [
{
"type": "basic",
"name": "Input",
"props": {
"type": "checkbox",
"className": "checkbox-aligned",
"disabled": "{{_computed.isReadOnly}}",
"checked": "{{(_local.form?.basic_defaults?.active_reaction_types ?? []).includes(reactionType.code)}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"form.basic_defaults.active_reaction_types": "{{(_local.form?.basic_defaults?.active_reaction_types ?? []).includes(reactionType.code) ? (_local.form?.basic_defaults?.active_reaction_types ?? []).filter(c => c !== reactionType.code) : [...(_local.form?.basic_defaults?.active_reaction_types ?? []), reactionType.code]}}",
"hasChanges": true
}
}
]
},
{
"type": "basic",
"name": "Icon",
"props": {
"name": "{{reactionType.icon_name ?? 'fa-thumbs-up'}}",
"size": "sm"
}
},
{
"type": "basic",
"name": "Span",
"text": "{{reactionType.name ?? reactionType.code}}"
}
]
}
]
}
]
}
]
}
]
}
]
}
@@ -35,93 +35,34 @@
{
"type": "basic",
"name": "Div",
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
"props": {
"className": "flex flex-col gap-3 mt-2"
},
"children": [
{
"type": "basic",
"name": "Div",
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
"props": {
"className": "alert-warning"
},
"children": [
{
"type": "basic",
"name": "Div",
"name": "Icon",
"props": {
"className": "alert-warning"
},
"children": [
{
"type": "basic",
"name": "Icon",
"props": {
"name": "triangle-exclamation",
"className": "text-amber-600 dark:text-amber-400 flex-shrink-0",
"size": "sm"
}
},
{
"type": "basic",
"name": "P",
"props": {
"className": "text-warning-soft"
},
"text": "$t:sirsoft-board.admin.settings.bulk_apply.checked_fields_notice"
}
]
"name": "triangle-exclamation",
"className": "text-amber-600 dark:text-amber-400 flex-shrink-0",
"size": "sm"
}
},
{
"type": "basic",
"name": "Div",
"if": "{{!(_local.bulkApplyFields ?? []).length}}",
"name": "P",
"props": {
"className": "flex-center gap-2 px-1"
"className": "text-warning-soft"
},
"children": [
{
"type": "basic",
"name": "Icon",
"props": {
"name": "circle-exclamation",
"className": "text-amber-600 dark:text-amber-400 flex-shrink-0",
"size": "sm"
}
},
{
"type": "basic",
"name": "P",
"props": {
"className": "text-warning-soft"
},
"text": "$t:sirsoft-board.admin.settings.bulk_apply.no_fields_selected"
}
]
},
{
"type": "basic",
"name": "Div",
"if": "{{(_local.bulkApplyFields ?? []).length > 0 && _local.bulkApplyAll === false && !(_local._bulkApplyBoardIds ?? []).length}}",
"props": {
"className": "flex-center gap-2 px-1"
},
"children": [
{
"type": "basic",
"name": "Icon",
"props": {
"name": "circle-exclamation",
"className": "text-red-500 dark:text-red-400 flex-shrink-0",
"size": "sm"
}
},
{
"type": "basic",
"name": "P",
"props": {
"className": "text-sm text-red-600 dark:text-red-400"
},
"text": "$t:sirsoft-board.admin.settings.bulk_apply.no_boards_selected"
}
]
"text": "$t:sirsoft-board.admin.settings.bulk_apply.checked_fields_notice"
}
]
}
@@ -156,125 +97,61 @@
]
},
{
"id": "bulk_apply_target",
"type": "basic",
"name": "Div",
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
"props": {
"className": "mt-6"
},
"children": [
{
"id": "bulk_apply_target",
"type": "basic",
"name": "Div",
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
"name": "Label",
"props": {
"className": "form-label"
},
"children": [
{
"type": "basic",
"name": "Label",
"props": {
"className": "form-label"
},
"children": [
{
"type": "basic",
"name": "Span",
"text": "$t:sirsoft-board.admin.settings.bulk_apply.target_boards"
}
]
},
"name": "Span",
"text": "$t:sirsoft-board.admin.settings.bulk_apply.target_boards"
}
]
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "grid grid-cols-1 sm:grid-cols-2 gap-3 mt-2"
},
"children": [
{
"id": "bulk_apply_target_all",
"type": "basic",
"name": "Div",
"props": {
"className": "flex-center gap-4"
"className": "{{_local.bulkApplyAll !== false ? 'flex-center gap-2 p-3 rounded-lg border-2 border-blue-500 bg-blue-50 dark:bg-blue-900/20 cursor-pointer' : 'flex-center gap-2 p-3 rounded-lg border-2 border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 cursor-pointer hover:border-gray-300 dark:hover:border-gray-600'}}"
},
"children": [
"actions": [
{
"type": "basic",
"name": "Label",
"props": {
"className": "flex-center gap-2 text-body cursor-pointer"
},
"children": [
{
"type": "basic",
"name": "Input",
"props": {
"type": "radio",
"name": "_bulkApplyAll",
"className": "radio",
"checked": "{{_local.bulkApplyAll !== false}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"bulkApplyAll": true
}
}
]
},
{
"type": "basic",
"name": "Span",
"text": "$t:sirsoft-board.admin.settings.bulk_apply.apply_all"
}
]
},
{
"type": "basic",
"name": "Label",
"props": {
"className": "flex-center gap-2 text-body cursor-pointer"
},
"children": [
{
"type": "basic",
"name": "Input",
"props": {
"type": "radio",
"name": "_bulkApplyAll",
"className": "radio",
"checked": "{{_local.bulkApplyAll === false}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"bulkApplyAll": false
}
}
]
},
{
"type": "basic",
"name": "Span",
"text": "$t:sirsoft-board.admin.settings.bulk_apply.apply_selected"
}
]
"type": "click",
"handler": "setState",
"params": {
"target": "local",
"bulkApplyAll": true
}
}
]
},
{
"type": "basic",
"name": "Div",
"if": "{{_local.bulkApplyAll === false}}",
"props": {
"className": "mt-3"
},
],
"children": [
{
"type": "composite",
"name": "TagInput",
"type": "basic",
"name": "Input",
"props": {
"value": "{{_local._bulkApplyBoardIds ?? []}}",
"options": "{{$computed.boardOptions ?? []}}",
"placeholder": "$t:sirsoft-board.admin.settings.bulk_apply.select_boards_placeholder",
"className": "w-full",
"disabled": "{{_computed.isReadOnly}}"
"type": "radio",
"name": "_bulkApplyAll",
"className": "radio",
"checked": "{{_local.bulkApplyAll !== false}}"
},
"actions": [
{
@@ -282,10 +159,105 @@
"handler": "setState",
"params": {
"target": "local",
"_bulkApplyBoardIds": "{{$event.target.value}}"
"bulkApplyAll": true
}
}
]
},
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-primary text-sm font-semibold"
},
"text": "$t:sirsoft-board.admin.settings.bulk_apply.apply_all"
}
]
},
{
"id": "bulk_apply_target_selected",
"type": "basic",
"name": "Div",
"props": {
"className": "{{_local.bulkApplyAll === false ? 'flex-center gap-2 p-3 rounded-lg border-2 border-blue-500 bg-blue-50 dark:bg-blue-900/20 cursor-pointer' : 'flex-center gap-2 p-3 rounded-lg border-2 border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 cursor-pointer hover:border-gray-300 dark:hover:border-gray-600'}}"
},
"actions": [
{
"type": "click",
"handler": "setState",
"params": {
"target": "local",
"bulkApplyAll": false
}
}
],
"children": [
{
"type": "basic",
"name": "Input",
"props": {
"type": "radio",
"name": "_bulkApplyAll",
"className": "radio",
"checked": "{{_local.bulkApplyAll === false}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"bulkApplyAll": false
}
}
]
},
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-primary text-sm font-semibold"
},
"text": "$t:sirsoft-board.admin.settings.bulk_apply.apply_selected"
}
]
}
]
},
{
"type": "basic",
"name": "Div",
"if": "{{_local.bulkApplyAll === false}}",
"props": {
"className": "mt-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/30 border border-gray-200 dark:border-gray-700"
},
"children": [
{
"type": "basic",
"name": "P",
"props": {
"className": "text-tertiary mb-2"
},
"text": "$t:sirsoft-board.admin.settings.bulk_apply.select_boards_hint"
},
{
"type": "composite",
"name": "TagInput",
"props": {
"value": "{{_local._bulkApplyBoardIds ?? []}}",
"options": "{{$computed.boardOptions ?? []}}",
"placeholder": "$t:sirsoft-board.admin.settings.bulk_apply.select_boards_placeholder",
"className": "w-full",
"disabled": "{{_computed.isReadOnly}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"_bulkApplyBoardIds": "{{$event.target.value}}"
}
}
]
}
@@ -298,7 +270,7 @@
"name": "Div",
"if": "{{(boards_list?.data?.data ?? []).length > 0}}",
"props": {
"className": "flex justify-end"
"className": "flex justify-end mt-6"
},
"children": [
{
@@ -306,7 +278,7 @@
"name": "Button",
"props": {
"type": "button",
"disabled": "{{_computed.isReadOnly || !(_local.bulkApplyFields ?? []).length || (_local.bulkApplyAll === false && !(_local._bulkApplyBoardIds ?? []).length)}}",
"disabled": "{{_computed.isReadOnly}}",
"className": "flex-center gap-2 px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 dark:bg-amber-500 dark:hover:bg-amber-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
},
"actions": [
@@ -314,9 +286,28 @@
"type": "click",
"handler": "sequence",
"actions": [
{
"comment": "검증 실패 안내 토스트 - 적용할 항목 미선택",
"handler": "toast",
"if": "{{!(_local.bulkApplyFields ?? []).length}}",
"params": {
"type": "error",
"message": "$t:sirsoft-board.admin.settings.bulk_apply.no_fields_selected"
}
},
{
"comment": "검증 실패 안내 토스트 - 선택한 게시판 미선택 (적용 항목은 선택된 경우에만 표시)",
"handler": "toast",
"if": "{{(_local.bulkApplyFields ?? []).length > 0 && _local.bulkApplyAll === false && !(_local._bulkApplyBoardIds ?? []).length}}",
"params": {
"type": "error",
"message": "$t:sirsoft-board.admin.settings.bulk_apply.no_boards_selected"
}
},
{
"comment": "모달 열기 전 현재 _local 상태를 _global 스냅샷에 저장 + 오류 상태 초기화",
"handler": "setState",
"if": "{{(_local.bulkApplyFields ?? []).length > 0 && (_local.bulkApplyAll !== false || (_local._bulkApplyBoardIds ?? []).length > 0)}}",
"params": {
"target": "global",
"bulkApplySnapshot": "{{(function() { const fields = _local.bulkApplyFields ?? []; const applyAll = _local.bulkApplyAll !== false; const boardIds = _local._bulkApplyBoardIds ?? []; const tab = _global.activeBoardSettingsTab || 'basic_defaults'; const form = _local.form || {}; return { fields: fields, applyAll: applyAll, boardIds: boardIds, tab: tab, form: form }; })()}}"
@@ -325,6 +316,7 @@
{
"comment": "이전 오류/롤백 안내 초기화 (모달 재오픈 시 직전 실패 안내 잔존 방지)",
"handler": "setState",
"if": "{{(_local.bulkApplyFields ?? []).length > 0 && (_local.bulkApplyAll !== false || (_local._bulkApplyBoardIds ?? []).length > 0)}}",
"params": {
"target": "local",
"bulkApplySaveError": false,
@@ -334,6 +326,7 @@
},
{
"handler": "openModal",
"if": "{{(_local.bulkApplyFields ?? []).length > 0 && (_local.bulkApplyAll !== false || (_local._bulkApplyBoardIds ?? []).length > 0)}}",
"target": "bulk_apply_confirm_modal"
}
]
@@ -0,0 +1,91 @@
<?php
namespace Modules\Sirsoft\Board\Exceptions;
use App\Helpers\ResponseHelper;
use Exception;
use Illuminate\Http\JsonResponse;
/**
* 반응 불가 예외
*
* 반응 기능이 꺼진 게시판, 게시판이 켜지 않은(비활성) 유형, 본인 글 반응 시도 등
* 반응이 허용되지 않는 상황에서 발생합니다 (이슈 #525 확정 07·08·11).
*
* `ReactPostRequest` + `AvailableReactionType` Rule 이 요청 단계에서 선차단하지만,
* 훅이나 Service 직접 호출처럼 FormRequest 를 거치지 않는 경로가 있으므로 최종
* 불변조건은 Service 가 보장합니다. 사용자가 고칠 수 있는 상태 문제이므로 422 로 매핑합니다.
*/
class ReactionNotAllowedException extends Exception
{
/**
* @param string $reason 거절 사유 (disabled | inactive_type | self_post | secret_denied)
* @param string $message 사용자에게 보일 메시지
*/
public function __construct(
private string $reason,
string $message,
) {
parent::__construct($message);
}
/**
* 반응 기능이 꺼진 게시판에 대한 예외를 생성합니다.
*
* @return self 반응 비활성화 사유 예외
*/
public static function disabled(): self
{
return new self('disabled', __('sirsoft-board::messages.reaction.disabled'));
}
/**
* 게시판이 켜지 않은(비활성) 유형에 대한 예외를 생성합니다.
*
* @return self 비활성 유형 사유 예외
*/
public static function inactiveType(): self
{
return new self('inactive_type', __('sirsoft-board::messages.reaction.inactive_type'));
}
/**
* 본인 글 반응 시도에 대한 예외를 생성합니다.
*
* @return self 본인 글 사유 예외
*/
public static function selfPost(): self
{
return new self('self_post', __('sirsoft-board::messages.reaction.self_post'));
}
/**
* 열람 권한이 없는 비밀글 반응 시도에 대한 예외를 생성합니다.
*
* @return self 비밀글 열람 권한 없음 사유 예외
*/
public static function secretDenied(): self
{
return new self('secret_denied', __('sirsoft-board::messages.reaction.secret_denied'));
}
/**
* 거절 사유를 반환합니다.
*
* @return string 거절 사유 (disabled | inactive_type | self_post)
*/
public function getReason(): string
{
return $this->reason;
}
/**
* 컨트롤러 catch 와 동일한 422 응답으로 렌더링합니다.
*
* @return JsonResponse 반응 불가 응답
*/
public function render(): JsonResponse
{
return ResponseHelper::error($this->getMessage(), 422, ['code' => 'reaction_not_allowed']);
}
}
@@ -288,6 +288,8 @@ class BoardController extends AdminBaseController
// TagInput 입력과 일관되게 배열로 주입 (categories 와 동일 패턴)
$data['blocked_keywords'] = array_values($spamSecurity['blocked_keywords'] ?? []);
$data['allowed_extensions'] = array_values($basicDefaults['allowed_extensions'] ?? []);
// reject(is_array) 로 제거된 배열 필드 — 체크박스 초기 선택 상태 렌더에 필요
$data['active_reaction_types'] = array_values($basicDefaults['active_reaction_types'] ?? []);
// depth 필드는 reject(is_array)로 걸러지지 않지만 basic_defaults가 비어있을 수 있으므로 명시적 기본값 보장
$limits = config('sirsoft-board.limits', []);
@@ -0,0 +1,50 @@
<?php
namespace Modules\Sirsoft\Board\Http\Controllers\Admin;
use App\Http\Controllers\Api\Base\AdminBaseController;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Modules\Sirsoft\Board\Http\Resources\ReactionTypeResource;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface;
/**
* 관리자용 반응 유형 컨트롤러
*
* 게시판 설정 화면의 "사용할 반응 유형" 체크박스 옵션 소스로 활성 유형 목록을 제공합니다.
* 유형 CRUD 는 이번 범위가 아니며(이슈 #525 확정 01), 목록 조회 전용 API 입니다.
*/
class ReactionTypeController extends AdminBaseController
{
/**
* ReactionTypeController 생성자
*
* @param ReactionTypeRepositoryInterface $reactionTypeRepository 반응 유형 Repository
*/
public function __construct(
private ReactionTypeRepositoryInterface $reactionTypeRepository,
) {
parent::__construct();
}
/**
* 활성 반응 유형 전체를 display_order 순으로 반환합니다.
*
* @return JsonResponse 반응 유형 목록 응답
*/
public function index(): JsonResponse
{
try {
$types = $this->reactionTypeRepository->getActive();
return $this->success(
'sirsoft-board::messages.reaction.list_success',
['reaction_types' => ReactionTypeResource::collection($types)]
);
} catch (\Exception $e) {
Log::error('반응 유형 목록 조회 실패', ['error' => $e->getMessage()]);
return $this->error('sirsoft-board::messages.reaction.failed', 500);
}
}
}
@@ -0,0 +1,77 @@
<?php
namespace Modules\Sirsoft\Board\Http\Controllers\User;
use App\Http\Controllers\Api\Base\AuthBaseController;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Modules\Sirsoft\Board\Exceptions\PostNotFoundException;
use Modules\Sirsoft\Board\Exceptions\ReactionNotAllowedException;
use Modules\Sirsoft\Board\Http\Requests\User\ReactPostRequest;
use Modules\Sirsoft\Board\Services\BoardService;
use Modules\Sirsoft\Board\Services\ReactionService;
/**
* 사용자용 반응 컨트롤러
*
* 게시글 반응(추천/비추천) 등록·전환·해제를 하나의 엔드포인트로 처리합니다.
*/
class ReactionController extends AuthBaseController
{
/**
* ReactionController 생성자
*
* @param ReactionService $reactionService 반응 서비스
* @param BoardService $boardService 게시판 서비스 (slug → 게시판 해석)
*/
public function __construct(
private ReactionService $reactionService,
private BoardService $boardService,
) {
parent::__construct();
}
/**
* 게시글에 반응합니다 (등록/전환/해제 통합).
*
* @param ReactPostRequest $request 반응 요청 (reaction_type_id 검증)
* @param string $slug 게시판 slug
* @param int $postId 게시글 ID
* @return JsonResponse 반응 결과 응답
*/
public function react(ReactPostRequest $request, string $slug, int $postId): JsonResponse
{
try {
$board = $this->boardService->getBoardBySlug($slug, checkScope: false);
if (! $board) {
return $this->notFound('sirsoft-board::messages.boards.error_404');
}
$result = $this->reactionService->react(
(int) Auth::id(),
$board,
$postId,
(int) $request->validated('reaction_type_id'),
);
$messageKey = match ($result['action']) {
'change' => 'sirsoft-board::messages.reaction.change_success',
'remove' => 'sirsoft-board::messages.reaction.remove_success',
default => 'sirsoft-board::messages.reaction.add_success',
};
return $this->success($messageKey, [
'action' => $result['action'],
'my_reaction_type_id' => $result['reaction_type_id'],
'reaction_counts' => $result['reaction_counts'],
]);
} catch (PostNotFoundException $e) {
return $this->notFound('sirsoft-board::messages.post.not_found');
} catch (ReactionNotAllowedException $e) {
return $this->error($e->getMessage(), 422, ['code' => 'reaction_not_allowed']);
} catch (\Exception $e) {
return $this->error('sirsoft-board::messages.reaction.failed', 500, $e->getMessage());
}
}
}
@@ -34,6 +34,8 @@ class BulkApplySettingsRequest extends FormRequest
'comment_order',
'show_view_count',
'use_report',
'use_reaction',
'active_reaction_types',
'min_title_length',
'max_title_length',
'min_content_length',
@@ -65,6 +65,7 @@ class StoreBoardSettingsRequest extends FormRequest
'basic_defaults.use_reply',
'basic_defaults.show_view_count',
'basic_defaults.use_report',
'basic_defaults.use_reaction',
'basic_defaults.use_file_upload',
'basic_defaults.notify_admin_on_post',
'basic_defaults.notify_author',
@@ -176,6 +177,9 @@ class StoreBoardSettingsRequest extends FormRequest
'basic_defaults.comment_order' => ['nullable', 'string', 'in:ASC,DESC'],
'basic_defaults.show_view_count' => ['nullable', 'boolean'],
'basic_defaults.use_report' => ['nullable', 'boolean'],
'basic_defaults.use_reaction' => ['nullable', 'boolean'],
'basic_defaults.active_reaction_types' => ['nullable', 'array'],
'basic_defaults.active_reaction_types.*' => ['string', 'max:50'],
'basic_defaults.min_title_length' => ['nullable', 'integer', "min:{$minTitleLengthMin}", "max:{$minTitleLengthMax}"],
'basic_defaults.max_title_length' => ['nullable', 'integer', "min:{$maxTitleLengthMin}", "max:{$maxTitleLengthMax}"],
'basic_defaults.min_content_length' => ['nullable', 'integer', "min:{$minContentLengthMin}", "max:{$minContentLengthMax}"],
@@ -53,6 +53,8 @@ class StoreBoardRequest extends FormRequest
'secret_mode' => $settings['secret_mode'] ?? 'disabled',
'use_comment' => $settings['use_comment'] ?? true,
'use_reply' => $settings['use_reply'] ?? true,
'use_reaction' => $settings['use_reaction'] ?? true,
'active_reaction_types' => $settings['active_reaction_types'] ?? [],
'max_reply_depth' => $settings['max_reply_depth'] ?? 5,
'max_comment_depth' => $settings['max_comment_depth'] ?? 10,
'use_file_upload' => $settings['use_file_upload'] ?? false,
@@ -86,7 +88,7 @@ class StoreBoardRequest extends FormRequest
// boolean 필드 캐스팅 (Toggle 컴포넌트가 "on"/"off" 문자열을 전송할 수 있음)
$booleanFields = [
'is_active', 'use_comment', 'use_reply', 'use_file_upload',
'use_report', 'show_view_count', 'is_notice',
'use_report', 'use_reaction', 'show_view_count', 'is_notice',
'notify_admin_on_post', 'notify_author',
];
@@ -169,6 +171,9 @@ class StoreBoardRequest extends FormRequest
'use_comment' => ['required', 'boolean'],
'use_reply' => ['required', 'boolean'],
'use_report' => ['required', 'boolean'],
'use_reaction' => ['required', 'boolean'],
'active_reaction_types' => ['sometimes', 'array'],
'active_reaction_types.*' => ['string'],
'comment_order' => ['required', 'in:ASC,DESC'],
'new_display_hours' => ['nullable', 'integer', "min:{$newDisplayHoursMin}", "max:{$newDisplayHoursMax}"],
@@ -75,7 +75,7 @@ class UpdateBoardRequest extends FormRequest
// boolean 필드 캐스팅 (Toggle 컴포넌트가 "on"/"off" 문자열을 전송할 수 있음)
$booleanFields = [
'is_active', 'use_comment', 'use_reply', 'use_file_upload',
'use_report', 'show_view_count', 'is_notice',
'use_report', 'use_reaction', 'show_view_count', 'is_notice',
'notify_admin_on_post', 'notify_author',
];
@@ -164,6 +164,9 @@ class UpdateBoardRequest extends FormRequest
'use_comment' => ['sometimes', 'required', 'boolean'],
'use_reply' => ['sometimes', 'required', 'boolean'],
'use_report' => ['sometimes', 'required', 'boolean'],
'use_reaction' => ['sometimes', 'required', 'boolean'],
'active_reaction_types' => ['sometimes', 'array'],
'active_reaction_types.*' => ['string'],
'comment_order' => ['sometimes', 'required', 'in:ASC,DESC'],
'new_display_hours' => ['sometimes', 'nullable', 'integer', "min:{$newDisplayHoursMin}", "max:{$newDisplayHoursMax}"],
@@ -261,6 +264,7 @@ class UpdateBoardRequest extends FormRequest
$settingFieldKeys = [
'per_page', 'per_page_mobile', 'order_by', 'order_direction',
'secret_mode', 'use_comment', 'use_reply', 'use_report',
'use_reaction', 'active_reaction_types',
'comment_order', 'show_view_count',
'max_reply_depth', 'max_comment_depth',
'min_title_length', 'max_title_length',
@@ -0,0 +1,68 @@
<?php
namespace Modules\Sirsoft\Board\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
use Modules\Sirsoft\Board\Repositories\Contracts\BoardRepositoryInterface;
use Modules\Sirsoft\Board\Rules\AvailableReactionType;
/**
* 게시글 반응 요청 폼 검증
*
* `reaction_type_id` 가 존재하는 활성 유형이면서 대상 게시판이 켠 유형인지
* 검증합니다. 게시판의 활성 유형 code 목록(`active_reaction_types`)을 조회해
* `AvailableReactionType` Rule 에 전달합니다.
*/
class ReactPostRequest extends FormRequest
{
/**
* 권한 체크는 라우트의 auth:sanctum 미들웨어에서 수행됩니다.
*
* @return bool 항상 true
*/
public function authorize(): bool
{
return true;
}
/**
* 요청에 적용할 검증 규칙
*
* @return array<string, mixed>
*/
public function rules(): array
{
$slug = (string) ($this->route('slug') ?? '');
$board = app(BoardRepositoryInterface::class)->findBySlug($slug);
$activeCodes = $board?->active_reaction_types ?? [];
return [
'reaction_type_id' => ['required', 'integer', new AvailableReactionType($activeCodes)],
];
}
/**
* 검증 오류 메시지 커스터마이징
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'reaction_type_id.required' => __('sirsoft-board::validation.reaction.reaction_type_id.required'),
'reaction_type_id.integer' => __('sirsoft-board::validation.reaction.reaction_type_id.integer'),
];
}
/**
* 검증할 필드의 이름을 커스터마이징
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'reaction_type_id' => __('sirsoft-board::validation.attributes.reaction.reaction_type_id'),
];
}
}
@@ -57,6 +57,9 @@ class BoardResource extends BaseApiResource
'use_reply' => $this->use_reply,
'max_reply_depth' => $this->max_reply_depth,
'use_report' => $this->use_report,
'use_reaction' => $this->use_reaction,
'active_reaction_types' => $this->active_reaction_types ?? [],
'reaction_type_options' => self::buildReactionTypeOptions($this->active_reaction_types ?? []),
'comment_order' => $this->comment_order,
'max_comment_depth' => $this->max_comment_depth,
@@ -266,6 +269,37 @@ class BoardResource extends BaseApiResource
return $this->blocked_keywords ?? [];
}
/**
* 게시판이 켠(활성) 반응 유형의 옵션 배열을 만듭니다.
*
* DB 컬럼 `active_reaction_types`(code 문자열 배열)와 `board_reaction_types` 를
* 조인해 프론트가 바로 쓸 수 있는 `{id, code, name, icon}` 배열로 변환합니다.
* 게시판이 켠 유형 중 시스템에서 살아있는(활성) 유형만, display_order 순으로 노출합니다
* (이슈 #525 확정 11 — 게시판 ∩ 시스템). 응답 필드명을 DB 컬럼과 의도적으로 달리해
* 동명이의어를 방지합니다.
*
* @param array<int, string> $activeCodes 게시판이 켠 반응 유형 code 목록
* @return array<int, array<string, mixed>> 반응 유형 옵션 배열
*/
public static function buildReactionTypeOptions(array $activeCodes): array
{
if (empty($activeCodes)) {
return [];
}
$types = app(\Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface::class)
->findByCodes($activeCodes);
return $types->map(fn ($type) => [
'id' => $type->id,
'code' => $type->code,
'name' => $type->getLocalizedName(),
'icon' => $type->icon,
// Icon 컴포넌트 name prop 용 — 스타일 접두사(fas/far 등)를 떼고 fa-* 토큰만 추출
'icon_name' => $type->getIconName(),
])->values()->all();
}
/**
* 허용 확장자를 배열로 반환합니다.
*
@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Auth;
use Modules\Sirsoft\Board\Enums\PostStatus;
use Modules\Sirsoft\Board\Enums\ReportReasonType;
use Modules\Sirsoft\Board\Enums\TriggerType;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReportRepositoryInterface;
use Modules\Sirsoft\Board\Support\BoardPermissionCacheKeys;
use Modules\Sirsoft\Board\Traits\ChecksBoardPermission;
@@ -77,6 +78,12 @@ class PostResource extends BaseApiResource
// 상세 전용: 신고 여부 (로그인 사용자 + board 관계 로드 시에만)
'is_already_reported' => $this->getIsAlreadyReported($request),
// 상세 전용: 반응 카운트 (활성 유형 전체 항상 포함 — 확정 09, 0도 노출)
'reaction_counts' => $this->getReactionCountsForResponse(),
// 상세 전용: 내가 누른 반응 유형 ID (로그인 사용자 + board 관계 로드 시에만, null 가능)
'my_reaction_type_id' => $this->getMyReactionTypeId($request),
// 권한 정보 (is_owner + abilities) — 상세 페이지에서만 포함
...$this->resourceMeta($request),
];
@@ -341,6 +348,8 @@ class PostResource extends BaseApiResource
'use_comment' => $this->board->use_comment,
'use_reply' => $this->board->use_reply,
'use_report' => $this->board->use_report,
'use_reaction' => $this->board->use_reaction,
'reaction_type_options' => BoardResource::buildReactionTypeOptions($this->board->active_reaction_types ?? []),
'show_view_count' => $this->board->show_view_count,
'max_reply_depth' => $this->board->max_reply_depth ?? g7_module_settings('sirsoft-board', 'basic_defaults.max_reply_depth', 5),
'max_comment_depth' => $this->board->max_comment_depth ?? g7_module_settings('sirsoft-board', 'basic_defaults.max_comment_depth', 10),
@@ -480,6 +489,58 @@ class PostResource extends BaseApiResource
->hasUserReported($user->id, $this->board->id, 'post', $this->id);
}
/**
* 반응 카운트를 응답용으로 반환합니다.
*
* 게시판이 켠(활성) 유형 전체를 키로 항상 포함하며, 저장된 카운트가 없는 유형은
* 0 으로 채웁니다 (확정 09 — 활성 유형은 개수 0이어도 항상 노출). 키는 유형 ID.
*
* 반드시 stdClass(객체)로 반환한다 — 키가 정수(유형 ID)뿐이면 배열이 list 로 오인되어
* JsonResource::resolve() 가 `[count1, count2]` 로 재인덱싱하고, 그러면 프론트가
* `reaction_counts[유형ID]` 로 읽을 때 엉뚱한 인덱스를 읽어 개수가 항상 0/어긋난다.
* stdClass 는 array 필터의 재인덱싱을 타지 않아 항상 `{"유형ID": 개수}` JSON 객체로 나간다.
*
* @return \stdClass 유형 ID => 개수 (JSON 객체)
*/
private function getReactionCountsForResponse(): \stdClass
{
if (! $this->relationLoaded('board') || ! $this->board) {
return new \stdClass;
}
$stored = $this->reaction_counts ?? [];
$options = BoardResource::buildReactionTypeOptions($this->board->active_reaction_types ?? []);
$counts = [];
foreach ($options as $option) {
$key = (string) $option['id'];
$counts[$key] = (int) ($stored[$key] ?? 0);
}
return (object) $counts;
}
/**
* 로그인 사용자가 이 게시글에 남긴 반응 유형 ID 를 반환합니다.
*
* 로그인 사용자 + board 관계 로드 시에만 조회하며, 반응이 없으면 null 을 반환합니다.
*
* @param Request $request HTTP 요청
* @return int|null 내가 누른 반응 유형 ID (없으면 null)
*/
private function getMyReactionTypeId(Request $request): ?int
{
$user = $request->user();
if (! $user || ! $this->relationLoaded('board') || ! $this->board) {
return null;
}
$reaction = app(ReactionRepositoryInterface::class)
->findByUserAndTarget($user->id, 'post', $this->id);
return $reaction?->reaction_type_id;
}
// =========================================================================
// 상세 페이지 전용: 조건부 필드 메서드
// =========================================================================
@@ -0,0 +1,30 @@
<?php
namespace Modules\Sirsoft\Board\Http\Resources;
use App\Http\Resources\BaseApiResource;
use Illuminate\Http\Request;
/**
* 반응 유형 API 리소스
*
* 반응 유형을 API 응답 형식으로 변환합니다. name 은 현재 로케일 문자열로
* 내려주며, 미입력 언어는 getLocalizedName() 이 폴백 처리합니다 (이슈 #525 확정 15).
*/
class ReactionTypeResource extends BaseApiResource
{
/**
* @param Request $request HTTP 요청
* @return array<string, mixed> 변환된 배열 데이터
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'code' => $this->code,
'name' => $this->getLocalizedName(),
'icon' => $this->icon,
'icon_name' => $this->getIconName(),
];
}
}
@@ -12,6 +12,7 @@ use Modules\Sirsoft\Board\Models\BoardType;
use Modules\Sirsoft\Board\Models\Comment;
use Modules\Sirsoft\Board\Models\Post;
use Modules\Sirsoft\Board\Models\Report;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReportRepositoryInterface;
/**
@@ -29,9 +30,11 @@ class BoardActivityLogListener implements HookListenerInterface
/**
* @param ReportRepositoryInterface $reportRepository 신고 bulk lookup
* @param ReactionTypeRepositoryInterface $reactionTypeRepository 반응 유형 라벨 조회
*/
public function __construct(
protected ReportRepositoryInterface $reportRepository,
protected ReactionTypeRepositoryInterface $reactionTypeRepository,
) {}
/**
@@ -83,6 +86,9 @@ class BoardActivityLogListener implements HookListenerInterface
'sirsoft-board.report.after_restore_content' => ['method' => 'handleReportAfterRestoreContent', 'priority' => 20],
'sirsoft-board.report.after_blind_content' => ['method' => 'handleReportAfterBlindContent', 'priority' => 20],
'sirsoft-board.report.after_delete_content' => ['method' => 'handleReportAfterDeleteContent', 'priority' => 20],
// ─── Reaction ───
'sirsoft-board.reaction.after_react' => ['method' => 'handleReactionAfterReact', 'priority' => 20],
];
}
@@ -715,4 +721,52 @@ class BoardActivityLogListener implements HookListenerInterface
'description_params' => ['report_id' => $report->id],
]);
}
// ═══════════════════════════════════════════
// Reaction 핸들러
// ═══════════════════════════════════════════
/**
* 반응 등록/전환/해제 후 로그 기록
*
* @param int $userId 반응한 사용자 ID
* @param Post $post 대상 게시글
* @param int $reactionTypeId 반응 유형 ID
* @param string $action 수행된 동작 (add | change | remove)
*/
public function handleReactionAfterReact(int $userId, Post $post, int $reactionTypeId, string $action): void
{
$post->loadMissing('board');
$reactionType = $this->reactionTypeRepository->findById($reactionTypeId);
$typeName = $reactionType?->getLocalizedName() ?? '';
$actionKey = match ($action) {
'change' => 'reaction.change',
'remove' => 'reaction.remove',
default => 'reaction.add',
};
$descriptionKey = match ($action) {
'change' => 'sirsoft-board::activity_log.description.reaction_change',
'remove' => 'sirsoft-board::activity_log.description.reaction_remove',
default => 'sirsoft-board::activity_log.description.reaction_add',
};
$this->logActivity($actionKey, [
'loggable' => $post,
'description_key' => $descriptionKey,
'description_params' => [
'title' => $post->title ?? '',
'reaction_type' => $typeName,
'board_name' => $post->board?->name ?? '',
],
'properties' => [
'title' => $post->title,
'reaction_type_id' => $reactionTypeId,
'reaction_type' => $typeName,
'board_name' => $post->board?->name ?? '',
],
]);
}
}
@@ -127,6 +127,8 @@ class Board extends Model
'use_reply',
'max_reply_depth',
'use_report',
'use_reaction',
'active_reaction_types',
'new_display_hours',
// 입력 제한 설정
@@ -183,6 +185,9 @@ class Board extends Model
// Post::isNew() 가 Carbon 시간 연산에 넘기는 값 — 조회 시점과 무관하게 정수를 보장한다.
'new_display_hours' => 'integer',
'use_report' => 'boolean',
'use_reaction' => 'boolean',
// 활성 반응 유형 code 목록 (문자열 배열, 예: ["like","dislike"])
'active_reaction_types' => 'array',
'use_file_upload' => 'boolean',
'notify_author' => 'boolean',
'notify_admin_on_post' => 'boolean',
@@ -79,6 +79,7 @@ class Post extends Model implements FulltextSearchable
'replies_count',
'comments_count',
'attachments_count',
'reaction_counts',
];
/**
@@ -99,6 +100,8 @@ class Post extends Model implements FulltextSearchable
'replies_count' => 'integer',
'comments_count' => 'integer',
'attachments_count' => 'integer',
// 반응 유형별 개수 (키는 유형 ID 문자열, 예: {"1":18,"2":2})
'reaction_counts' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
@@ -107,6 +110,8 @@ class Post extends Model implements FulltextSearchable
/**
* 게시판과의 관계를 정의합니다.
*
* @return BelongsTo<Board, Post>
*/
public function board(): BelongsTo
{
@@ -115,6 +120,8 @@ class Post extends Model implements FulltextSearchable
/**
* 작성자와의 관계를 정의합니다 (회원).
*
* @return BelongsTo<User, Post>
*/
public function user(): BelongsTo
{
@@ -123,6 +130,8 @@ class Post extends Model implements FulltextSearchable
/**
* 부모 게시글과의 관계를 정의합니다 (답글용).
*
* @return BelongsTo<Post, Post>
*/
public function parent(): BelongsTo
{
@@ -134,6 +143,8 @@ class Post extends Model implements FulltextSearchable
*
* board_id 조건은 Eager loading 호환을 위해 관계에서 제외하고
* Repository의 Eager loading 클로저에서 명시적으로 전달합니다.
*
* @return HasMany<Post>
*/
public function replies(): HasMany
{
@@ -145,6 +156,8 @@ class Post extends Model implements FulltextSearchable
*
* board_id 조건은 Eager loading 호환을 위해 관계에서 제외하고
* Repository의 Eager loading 클로저에서 명시적으로 전달합니다.
*
* @return HasMany<Comment>
*/
public function comments(): HasMany
{
@@ -157,6 +170,8 @@ class Post extends Model implements FulltextSearchable
*
* board_id 조건은 Eager loading 호환을 위해 관계에서 제외하고
* Repository의 Eager loading 클로저에서 명시적으로 전달합니다.
*
* @return HasMany<Attachment>
*/
public function attachments(): HasMany
{
@@ -0,0 +1,89 @@
<?php
namespace Modules\Sirsoft\Board\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 게시판 반응 이력 모델.
*
* 사용자당 대상(게시글)에 1행. 등록=INSERT, 전환=UPDATE, 해제=DELETE
* (이슈 #525 확정 03, 05). target_type/target_id 폴리모픽 구조 (확정 10).
*
* @property int $id
* @property int $user_id
* @property string $target_type
* @property int $target_id
* @property int $reaction_type_id
* @property int|null $board_id
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
*/
class Reaction extends Model
{
protected $table = 'board_reactions';
protected $fillable = [
'user_id',
'target_type',
'target_id',
'reaction_type_id',
'board_id',
];
protected function casts(): array
{
return [
'user_id' => 'integer',
'target_id' => 'integer',
'reaction_type_id' => 'integer',
'board_id' => 'integer',
];
}
/**
* 반응한 사용자와의 관계.
*
* @return BelongsTo<User, Reaction>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* 반응 유형과의 관계.
*
* @return BelongsTo<ReactionType, Reaction>
*/
public function reactionType(): BelongsTo
{
return $this->belongsTo(ReactionType::class, 'reaction_type_id');
}
/**
* 게시판과의 관계 (nullable).
*
* @return BelongsTo<Board, Reaction>
*/
public function board(): BelongsTo
{
return $this->belongsTo(Board::class, 'board_id');
}
/**
* 특정 대상(타입+ID)의 반응만 조회하는 스코프.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $targetType 대상 타입 (예: post)
* @param int $targetId 대상 ID
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeByTarget($query, string $targetType, int $targetId)
{
return $query->where('target_type', $targetType)
->where('target_id', $targetId);
}
}
@@ -0,0 +1,116 @@
<?php
namespace Modules\Sirsoft\Board\Models;
use App\Casts\AsUnicodeJson;
use App\Models\Concerns\HasUserOverrides;
use Illuminate\Database\Eloquent\Model;
/**
* 게시판 반응 유형 모델.
*
* 반응(추천/비추천 등) 유형을 DB로 관리한다. name은 다국어 JSON,
* 조회 시 getLocalizedName()이 로케일 폴백을 처리한다 (이슈 #525 확정 15).
*
* @property int $id
* @property string $code
* @property array $name
* @property string|null $icon
* @property int $display_order
* @property bool $is_active
* @property array|null $user_overrides
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
*/
class ReactionType extends Model
{
use HasUserOverrides;
protected $table = 'board_reaction_types';
protected $fillable = [
'code',
'name',
'icon',
'display_order',
'is_active',
'user_overrides',
];
/**
* 사용자 수정 보존 대상 필드.
*
* @var array<int, string>
*/
protected array $trackableFields = ['name', 'icon'];
/**
* 다국어 JSON 컬럼 — sub-key dot-path 단위 user_overrides 보존.
*
* @var array<int, string>
*/
protected array $translatableTrackableFields = ['name'];
protected function casts(): array
{
return [
'name' => AsUnicodeJson::class,
'display_order' => 'integer',
'is_active' => 'boolean',
'user_overrides' => 'array',
];
}
/**
* 지정된 로케일의 반응 유형명 반환 (미입력 언어는 폴백).
*
* @param string|null $locale 로케일 (null이면 현재 로케일)
* @return string 해당 로케일의 유형명
*/
public function getLocalizedName(?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
if (! is_array($this->name)) {
return (string) $this->name;
}
return $this->name[$locale]
?? $this->name[config('app.fallback_locale')]
?? (! empty($this->name) ? array_values($this->name)[0] : '')
?? '';
}
/**
* 활성 유형만 조회하는 스코프.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* 저장된 Font Awesome 클래스(`fas fa-thumbs-up`)에서 Icon 컴포넌트 name prop 용
* 아이콘 토큰(`fa-thumbs-up`)만 추출합니다. Icon 컴포넌트가 스타일 접두사를 자체 부착하므로
* 원본 클래스를 그대로 name 에 넘기면 접두사가 중복됩니다.
*
* @return string|null Icon name prop 값 (없으면 null)
*/
public function getIconName(): ?string
{
if ($this->icon === null || $this->icon === '') {
return null;
}
foreach (preg_split('/\s+/', trim($this->icon)) as $token) {
if (str_starts_with($token, 'fa-')) {
return $token;
}
}
return $this->icon;
}
}
@@ -16,9 +16,13 @@ use Modules\Sirsoft\Board\Repositories\Contracts\BoardStatRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\BoardTypeRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\CommentRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\PostRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReportRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\UserNotificationSettingRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\PostRepository;
use Modules\Sirsoft\Board\Repositories\ReactionRepository;
use Modules\Sirsoft\Board\Repositories\ReactionTypeRepository;
use Modules\Sirsoft\Board\Repositories\ReportRepository;
use Modules\Sirsoft\Board\Repositories\UserNotificationSettingRepository;
use Modules\Sirsoft\Board\Seo\BoardSitemapContributor;
@@ -77,6 +81,8 @@ class BoardServiceProvider extends BaseModuleServiceProvider
BoardTypeRepositoryInterface::class => BoardTypeRepository::class,
CommentRepositoryInterface::class => CommentRepository::class,
PostRepositoryInterface::class => PostRepository::class,
ReactionRepositoryInterface::class => ReactionRepository::class,
ReactionTypeRepositoryInterface::class => ReactionTypeRepository::class,
ReportRepositoryInterface::class => ReportRepository::class,
UserNotificationSettingRepositoryInterface::class => UserNotificationSettingRepository::class,
];
@@ -0,0 +1,67 @@
<?php
namespace Modules\Sirsoft\Board\Repositories\Contracts;
use Modules\Sirsoft\Board\Models\Reaction;
/**
* 반응 이력 Repository 인터페이스
*/
interface ReactionRepositoryInterface
{
/**
* 사용자+대상 기준 기존 반응을 조회합니다 (유일 제약과 동일 키).
*
* @param int $userId 사용자 ID
* @param string $targetType 대상 타입 (예: post)
* @param int $targetId 대상 ID
* @return Reaction|null 기존 반응 또는 null
*/
public function findByUserAndTarget(int $userId, string $targetType, int $targetId): ?Reaction;
/**
* 대상 게시글 행을 `lockForUpdate` 로 잠급니다.
*
* 짧은 시간에 연속 전송된 반응 요청이 서버에 도착 순서와 다르게 처리되어도
* (요청 A→B→C 전송, B→A→C 처리 등) `existing` 조회부터 upsert/delete·카운트
* 재집계까지 동일 게시글에 대한 처리 전체가 이 잠금 하나로 직렬화되도록,
* 반응 처리 트랜잭션 진입 직후 가장 먼저 호출해야 합니다.
*
* @param int $postId 게시글 ID
*/
public function lockPostForReaction(int $postId): void;
/**
* 반응을 등록하거나 전환합니다 (없으면 INSERT, 있으면 유형 UPDATE).
*
* @param int $userId 사용자 ID
* @param string $targetType 대상 타입
* @param int $targetId 대상 ID
* @param int $reactionTypeId 반응 유형 ID
* @param int|null $boardId 게시판 ID
* @return Reaction 등록/전환된 반응 모델
*/
public function upsert(int $userId, string $targetType, int $targetId, int $reactionTypeId, ?int $boardId): Reaction;
/**
* 반응을 해제합니다 (이력 행 삭제).
*
* @param Reaction $reaction 삭제할 반응 모델
* @return bool 삭제 성공 여부
*/
public function delete(Reaction $reaction): bool;
/**
* 게시글의 반응 카운트(JSON)를 board_reactions 실제 행 기준으로 재집계합니다.
*
* 반드시 트랜잭션 안에서 호출되어야 하며, 대상 게시글 행을 `lockForUpdate` 로 잠근 뒤
* 그 잠금 범위 안에서 COUNT 재집계까지 수행해 동시 요청의 처리 순서가 뒤바뀌어도
* (짧은 시간에 연속 전송된 반응 요청이 도착 순서와 다르게 처리되는 경우) 캐시된
* 카운트가 실제 반응 행 수와 항상 일치하도록 보장합니다. 델타 누적 방식은 요청
* 순서 역전 시 캐시가 실제 데이터와 어긋날 수 있어 사용하지 않습니다.
*
* @param int $postId 게시글 ID
* @return array<string, int> 갱신 후 reaction_counts (키는 유형 ID 문자열, 값은 실제 COUNT)
*/
public function recalculatePostReactionCounts(int $postId): array;
}
@@ -0,0 +1,43 @@
<?php
namespace Modules\Sirsoft\Board\Repositories\Contracts;
use Illuminate\Database\Eloquent\Collection;
use Modules\Sirsoft\Board\Models\ReactionType;
/**
* 반응 유형 Repository 인터페이스
*/
interface ReactionTypeRepositoryInterface
{
/**
* 활성 반응 유형 전체를 display_order 순으로 조회합니다.
*
* @return Collection<int, ReactionType>
*/
public function getActive(): Collection;
/**
* code 목록으로 활성 반응 유형을 조회합니다 (display_order 순).
*
* @param array<int, string> $codes 유형 code 목록
* @return Collection<int, ReactionType>
*/
public function findByCodes(array $codes): Collection;
/**
* ID로 반응 유형을 조회합니다.
*
* @param int $id 유형 ID
* @return ReactionType|null 유형 모델 또는 null
*/
public function findById(int $id): ?ReactionType;
/**
* ID 목록으로 반응 유형을 한 번에 조회합니다 (N+1 방지).
*
* @param array<int, int> $ids 유형 ID 목록
* @return Collection<int, ReactionType>
*/
public function findByIds(array $ids): Collection;
}
@@ -0,0 +1,92 @@
<?php
namespace Modules\Sirsoft\Board\Repositories;
use Modules\Sirsoft\Board\Models\Post;
use Modules\Sirsoft\Board\Models\Reaction;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
/**
* 반응 이력 Repository
*
* 반응 이력 데이터 접근 계층을 담당합니다.
*/
class ReactionRepository implements ReactionRepositoryInterface
{
/**
* {@inheritDoc}
*/
public function findByUserAndTarget(int $userId, string $targetType, int $targetId): ?Reaction
{
return Reaction::query()
->where('user_id', $userId)
->byTarget($targetType, $targetId)
->first();
}
/**
* {@inheritDoc}
*/
public function lockPostForReaction(int $postId): void
{
Post::query()->lockForUpdate()->findOrFail($postId);
}
/**
* {@inheritDoc}
*/
public function upsert(int $userId, string $targetType, int $targetId, int $reactionTypeId, ?int $boardId): Reaction
{
return Reaction::updateOrCreate(
[
'user_id' => $userId,
'target_type' => $targetType,
'target_id' => $targetId,
],
[
'reaction_type_id' => $reactionTypeId,
'board_id' => $boardId,
],
);
}
/**
* {@inheritDoc}
*/
public function delete(Reaction $reaction): bool
{
return (bool) $reaction->delete();
}
/**
* {@inheritDoc}
*/
public function recalculatePostReactionCounts(int $postId): array
{
/** @var Post $post */
$post = Post::query()->lockForUpdate()->findOrFail($postId);
// 이전에 캐시에 등장했던 유형은 반응이 0건이 되어도 키 자체는 유지한다
// (API 응답 계약 — 한 번 노출된 유형의 카운트는 0으로라도 항상 존재).
$zeroed = array_fill_keys(array_keys($post->reaction_counts ?? []), 0);
$actual = Reaction::query()
->byTarget('post', $postId)
->selectRaw('reaction_type_id, COUNT(*) as total')
->groupBy('reaction_type_id')
->pluck('total', 'reaction_type_id')
->mapWithKeys(fn ($total, $typeId) => [(string) $typeId => (int) $total])
->toArray();
// array_merge 는 숫자형 키(반응 유형 ID)를 재색인해 순서/키를 잃는다
// (예: [1=>1] 이 [0=>1] 로 바뀌어 JSON 직렬화 시 객체 대신 리스트가 됨).
// + 연산자는 키를 그대로 보존하면서 좌측을 우선하므로 실제 COUNT($actual)로
// zeroed 를 덮어쓰려면 우측에 두어야 한다.
$counts = $actual + $zeroed;
$post->reaction_counts = $counts;
$post->save();
return $counts;
}
}
@@ -0,0 +1,65 @@
<?php
namespace Modules\Sirsoft\Board\Repositories;
use Illuminate\Database\Eloquent\Collection;
use Modules\Sirsoft\Board\Models\ReactionType;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface;
/**
* 반응 유형 Repository
*
* 반응 유형 데이터 접근 계층을 담당합니다.
*/
class ReactionTypeRepository implements ReactionTypeRepositoryInterface
{
/**
* {@inheritDoc}
*/
public function getActive(): Collection
{
return ReactionType::query()
->where('is_active', true)
->orderBy('display_order')
->get();
}
/**
* {@inheritDoc}
*/
public function findByCodes(array $codes): Collection
{
if (empty($codes)) {
return ReactionType::query()->whereRaw('1 = 0')->get();
}
return ReactionType::query()
->where('is_active', true)
->whereIn('code', $codes)
->orderBy('display_order')
->get();
}
/**
* {@inheritDoc}
*/
public function findById(int $id): ?ReactionType
{
return ReactionType::query()->find($id);
}
/**
* {@inheritDoc}
*/
public function findByIds(array $ids): Collection
{
if (empty($ids)) {
return ReactionType::query()->whereIn('id', [])->get();
}
return ReactionType::query()
->whereIn('id', $ids)
->orderBy('display_order')
->get();
}
}
@@ -0,0 +1,49 @@
<?php
namespace Modules\Sirsoft\Board\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface;
/**
* 반응 유형 검증 규칙
*
* `reaction_type_id` 가 존재하는 활성 유형이면서 대상 게시판이 켠(활성) 유형인지
* 판정합니다. 게시판이 켠 유형 목록은 DB(`boards.active_reaction_types`, code 배열)에
* 저장되므로 허용 목록을 요청 클래스에 하드코딩하지 않습니다
* (`AvailableNotificationChannel` 과 동일한 서비스 기반 검증 구조).
*/
class AvailableReactionType implements ValidationRule
{
/**
* @param array<int, string> $activeCodes 대상 게시판이 켠 반응 유형 code 목록
*/
public function __construct(
private array $activeCodes = []
) {}
/**
* 검증 규칙 실행
*
* @param string $attribute 필드명
* @param mixed $value 검증 값 (reaction_type_id)
* @param Closure $fail 실패 콜백
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_numeric($value)) {
$fail(__('sirsoft-board::messages.reaction.inactive_type'));
return;
}
$type = app(ReactionTypeRepositoryInterface::class)->findById((int) $value);
if ($type === null
|| ! $type->is_active
|| ! in_array($type->code, $this->activeCodes, true)) {
$fail(__('sirsoft-board::messages.reaction.inactive_type'));
}
}
}
@@ -0,0 +1,190 @@
<?php
namespace Modules\Sirsoft\Board\Services;
use App\Enums\PermissionType;
use App\Extension\HookManager;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Exceptions\PostNotFoundException;
use Modules\Sirsoft\Board\Exceptions\ReactionNotAllowedException;
use Modules\Sirsoft\Board\Models\Board;
use Modules\Sirsoft\Board\Repositories\Contracts\PostRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface;
use Modules\Sirsoft\Board\Traits\ChecksBoardPermission;
/**
* 반응 서비스
*
* 게시글 반응의 등록/전환/해제를 단일 진입점(`react()`)으로 처리합니다.
* 확정 03(글당 1개, 전환 시 대체), 07(로그인 필수 — 컨트롤러 미들웨어),
* 08(본인 글 차단), 11(비활성 유형 차단)을 강제합니다.
*/
class ReactionService
{
use ChecksBoardPermission;
/**
* 현재 지원하는 반응 대상 타입 (확정 10 — 게시글만, 향후 comment 확장 가능).
*/
private const TARGET_POST = 'post';
/**
* @param ReactionRepositoryInterface $reactionRepository 반응 이력 Repository
* @param ReactionTypeRepositoryInterface $reactionTypeRepository 반응 유형 Repository
* @param PostRepositoryInterface $postRepository 게시글 스코프 조회 Repository
*/
public function __construct(
private readonly ReactionRepositoryInterface $reactionRepository,
private readonly ReactionTypeRepositoryInterface $reactionTypeRepository,
private readonly PostRepositoryInterface $postRepository,
) {}
/**
* 게시글에 반응을 남깁니다 (등록/전환/해제 통합).
*
* 게시글이 대상 게시판 소속인지 스코프 검증을 먼저 수행합니다 (교차 접근 차단).
*
* - 기존 반응 없음 → 신규(INSERT), 해당 유형 +1
* - 기존 반응이 같은 유형 → 해제(DELETE), 해당 유형 -1
* - 기존 반응이 다른 유형 → 전환(UPDATE), 이전 유형 -1 · 신규 유형 +1
*
* 카운트 증감과 이력 쓰기는 단일 트랜잭션으로 원자 처리합니다 (확정 04·동시성).
*
* @param int $userId 반응한 사용자 ID
* @param Board $board 대상 게시판 (컨트롤러가 slug 로 해석 후 전달)
* @param int $postId 대상 게시글 ID
* @param int $reactionTypeId 반응 유형 ID
* @return array{action: string, reaction_type_id: int|null, reaction_counts: array<string, int>}
*
* @throws PostNotFoundException 게시글이 이 게시판 소속이 아니거나 미존재
* @throws ReactionNotAllowedException 반응 비활성 게시판 / 비활성 유형 / 본인 글
*/
public function react(int $userId, Board $board, int $postId, int $reactionTypeId): array
{
// 게시글이 이 게시판 소속인지 스코프 검증 (교차 접근 차단)
$post = $this->postRepository->findByBoardId($board->id, $postId);
if ($post === null) {
throw new PostNotFoundException($postId);
}
// 반응 기능 off (확정 07 전제)
if (! $board->use_reaction) {
throw ReactionNotAllowedException::disabled();
}
// 본인 글 반응 차단 (확정 08)
if ((int) $post->user_id === $userId) {
throw ReactionNotAllowedException::selfPost();
}
// 비밀글 반응 차단 — 본문 열람 권한이 없는 사용자는 반응 불가.
// 신고(PostResource::canViewSecretContent)와 동일한 판정을 재사용해,
// 본문을 못 보는 사용자가 반응만 남기는 우회를 막는다.
// 작성자 본인은 위에서 이미 차단되므로 여기서는 게시판별 비밀글 열람 권한만 본다.
if ($post->is_secret && ! $this->canReactToSecretPost($board->slug)) {
throw ReactionNotAllowedException::secretDenied();
}
// 요청 유형이 존재하는 활성 유형이면서 게시판이 켠 유형인지 확인 (확정 11)
$requestedType = $this->reactionTypeRepository->findById($reactionTypeId);
$activeCodes = $board->active_reaction_types ?? [];
if ($requestedType === null
|| ! $requestedType->is_active
|| ! in_array($requestedType->code, $activeCodes, true)) {
throw ReactionNotAllowedException::inactiveType();
}
HookManager::doAction('sirsoft-board.reaction.before_react', $userId, $post, $reactionTypeId);
$result = DB::transaction(function () use ($userId, $board, $post, $reactionTypeId) {
// 이 게시글에 대한 반응 처리를 직렬화 — 짧은 시간에 연속 전송된 요청이
// 서버 도착 순서와 다르게 처리되어도(A→B→C 전송, B→A→C 처리 등) existing
// 조회부터 upsert/delete·카운트 재집계까지가 이 잠금 하나로 묶여, 이후
// 로직이 항상 "이 트랜잭션 차례가 됐을 때의 실제 최신 상태"를 기준으로
// 판단하도록 보장한다. 잠금 없이 existing 을 먼저 읽으면 다른 트랜잭션이
// 그 사이 상태를 바꿔도 반영되지 않아, 이미 지워진 반응을 다시 지우거나
// 캐시 카운트가 실제 반응 행 수와 어긋나는 결함으로 이어진다.
//
// 스코프 검증 통과 직후·락 획득 시점 사이에 게시글이 삭제되는 레이스에서는
// findOrFail() 이 ModelNotFoundException 을 던진다. 컨트롤러는 이 예외를
// 모르므로 그대로 두면 일반 500 으로 새어나가 사용자가 원인을 알 수 없다 —
// 이미 위에서 존재를 확인한 게시글이 사라진 것과 같은 의미이므로 동일하게
// PostNotFoundException 으로 변환한다.
try {
$this->reactionRepository->lockPostForReaction($post->id);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
throw new PostNotFoundException($post->id);
}
$existing = $this->reactionRepository->findByUserAndTarget(
$userId,
self::TARGET_POST,
$post->id,
);
// 같은 유형 재요청 → 해제
if ($existing !== null && (int) $existing->reaction_type_id === $reactionTypeId) {
$this->reactionRepository->delete($existing);
$counts = $this->reactionRepository->recalculatePostReactionCounts($post->id);
return ['action' => 'remove', 'reaction_type_id' => null, 'reaction_counts' => $counts];
}
// 다른 유형 → 전환 (이전 -1, 신규 +1)
if ($existing !== null) {
$this->reactionRepository->upsert(
$userId,
self::TARGET_POST,
$post->id,
$reactionTypeId,
$board->id,
);
$counts = $this->reactionRepository->recalculatePostReactionCounts($post->id);
return ['action' => 'change', 'reaction_type_id' => $reactionTypeId, 'reaction_counts' => $counts];
}
// 신규 등록
$this->reactionRepository->upsert(
$userId,
self::TARGET_POST,
$post->id,
$reactionTypeId,
$board->id,
);
$counts = $this->reactionRepository->recalculatePostReactionCounts($post->id);
return ['action' => 'add', 'reaction_type_id' => $reactionTypeId, 'reaction_counts' => $counts];
});
HookManager::doAction(
'sirsoft-board.reaction.after_react',
$userId,
$post,
$reactionTypeId,
$result['action'],
);
return $result;
}
/**
* 비밀글에 반응할 수 있는 열람 권한이 있는지 확인합니다.
*
* 반응은 사용자(User) 페이지 전용 기능이므로 게시판별 비밀글 읽기 권한
* (posts.read-secret) 또는 게시판 매니저 권한만 인정합니다. 작성자 본인은
* 호출 이전에 이미 selfPost 로 차단되므로 여기서는 고려하지 않습니다.
* `PostResource::canViewSecretContent` 의 게시판 권한 판정과 동일한 기준입니다.
*
* @param string $slug 대상 게시판 슬러그
* @return bool 비밀글 반응 가능 여부
*/
private function canReactToSecretPost(string $slug): bool
{
return $this->checkBoardPermission($slug, 'posts.read-secret', PermissionType::User)
|| $this->checkBoardPermission($slug, 'manager', PermissionType::User);
}
}
@@ -4,8 +4,10 @@ return [
// Action labels (last segment).
// ActivityLog::getActionLabelAttribute resolves module-origin labels from the module's lang first.
'action' => [
'add' => 'Reaction Added',
'add_to_menu' => 'Added to Menu',
'blind' => 'Blinded',
'change' => 'Reaction Changed',
'blind_content' => 'Content Blinded',
'bulk_apply' => 'Bulk Applied',
'bulk_apply_aborted' => 'Bulk Apply Aborted',
@@ -14,6 +16,7 @@ return [
'delete' => 'Deleted',
'delete_content' => 'Content Deleted',
'download' => 'Downloaded',
'remove' => 'Reaction Removed',
'remove_from_menu' => 'Removed from Menu',
'restore' => 'Restored',
'restore_content' => 'Content Restored',
@@ -69,6 +72,11 @@ return [
'report_blind_content' => 'Report content blinded (ID: :report_id)',
'report_delete_content' => 'Report content deleted (ID: :report_id)',
// Reaction (recommend/not recommend)
'reaction_add' => 'Reaction added (Board: :board_name, Title: :title, Type: :reaction_type)',
'reaction_change' => 'Reaction changed (Board: :board_name, Title: :title, Type: :reaction_type)',
'reaction_remove' => 'Reaction removed (Board: :board_name, Title: :title)',
// Board settings
'board_settings_index' => 'Board settings viewed',
'board_settings_bulk_apply' => 'Board settings bulk applied',
@@ -124,6 +124,20 @@ return [
'post_deleted' => 'You cannot comment on a deleted post.',
],
// Reaction (recommend/not recommend) messages
'reaction' => [
'list_success' => 'Reaction types have been retrieved.',
'add_success' => 'Your reaction has been added.',
'change_success' => 'Your reaction has been changed.',
'remove_success' => 'Your reaction has been removed.',
'failed' => 'Failed to process the reaction.',
'disabled' => 'Reactions are disabled for this board.',
'inactive_type' => 'This reaction type is not available on this board.',
'self_post' => 'You cannot react to your own post.',
'secret_denied' => 'You cannot react to a secret post you are not allowed to view.',
'login_required' => 'You need to sign in to use this feature.',
],
// Additional comment messages
'comments' => [
'comments_disabled' => 'Comments are disabled for this board.',
@@ -307,6 +307,8 @@ return [
'basic_defaults.comment_order' => 'Comment Order',
'basic_defaults.show_view_count' => 'Show View Count',
'basic_defaults.use_report' => 'Use Report',
'basic_defaults.use_reaction' => 'Use Reaction',
'basic_defaults.active_reaction_types' => 'Active Reaction Types',
'basic_defaults.min_title_length' => 'Min Title Length',
'basic_defaults.max_title_length' => 'Max Title Length',
'basic_defaults.min_content_length' => 'Min Content Length',
@@ -369,6 +371,9 @@ return [
'process_note' => 'Process Note',
'ids' => 'Report IDs',
],
'reaction' => [
'reaction_type_id' => 'Reaction Type',
],
'blind' => [
'reason' => 'Blind Reason',
],
@@ -580,6 +585,12 @@ return [
],
// Report validation messages
'reaction' => [
'reaction_type_id' => [
'required' => 'The reaction type is required.',
'integer' => 'The reaction type must be an integer.',
],
],
'report' => [
'invalid_status_transition' => 'The report cannot be changed to that status from its current status.',
'status' => [
@@ -4,8 +4,10 @@ return [
// 액션 라벨 (마지막 세그먼트 기준).
// ActivityLog::getActionLabelAttribute 가 모듈 origin 라벨을 자체 lang 에서 우선 조회.
'action' => [
'add' => '반응 등록',
'add_to_menu' => '메뉴 추가',
'blind' => '블라인드',
'change' => '반응 변경',
'blind_content' => '콘텐츠 블라인드',
'bulk_apply' => '일괄 적용',
'bulk_apply_aborted' => '일괄 적용 중단',
@@ -14,6 +16,7 @@ return [
'delete' => '삭제',
'delete_content' => '콘텐츠 삭제',
'download' => '다운로드',
'remove' => '반응 취소',
'remove_from_menu' => '메뉴 제거',
'restore' => '복원',
'restore_content' => '콘텐츠 복원',
@@ -69,6 +72,11 @@ return [
'report_blind_content' => '신고 콘텐츠 블라인드 (ID: :report_id)',
'report_delete_content' => '신고 콘텐츠 삭제 (ID: :report_id)',
// 반응 (추천/비추천)
'reaction_add' => '반응 등록 (게시판: :board_name, 제목: :title, 유형: :reaction_type)',
'reaction_change' => '반응 변경 (게시판: :board_name, 제목: :title, 유형: :reaction_type)',
'reaction_remove' => '반응 취소 (게시판: :board_name, 제목: :title)',
// 게시판 설정
'board_settings_index' => '게시판 설정 조회',
'board_settings_bulk_apply' => '게시판 설정 일괄 적용',
@@ -124,6 +124,20 @@ return [
'post_deleted' => '삭제된 게시글에는 댓글을 작성할 수 없습니다.',
],
// 반응(추천/비추천) 관련 메시지
'reaction' => [
'list_success' => '반응 유형 목록을 조회했습니다.',
'add_success' => '반응을 남겼습니다.',
'change_success' => '반응을 변경했습니다.',
'remove_success' => '반응을 취소했습니다.',
'failed' => '반응 처리에 실패했습니다.',
'disabled' => '이 게시판은 반응 기능이 비활성화되어 있습니다.',
'inactive_type' => '이 게시판에서 사용할 수 없는 반응 유형입니다.',
'self_post' => '본인 글에는 반응할 수 없습니다.',
'secret_denied' => '열람 권한이 없는 비밀글에는 반응할 수 없습니다.',
'login_required' => '로그인이 필요한 기능입니다.',
],
// 댓글 관련 추가 메시지
'comments' => [
'comments_disabled' => '이 게시판은 댓글 기능이 비활성화되어 있습니다.',
@@ -307,6 +307,8 @@ return [
'basic_defaults.comment_order' => '댓글 정렬',
'basic_defaults.show_view_count' => '조회수 표시',
'basic_defaults.use_report' => '신고 기능 사용',
'basic_defaults.use_reaction' => '반응 기능 사용',
'basic_defaults.active_reaction_types' => '사용할 반응 유형',
'basic_defaults.min_title_length' => '최소 제목 길이',
'basic_defaults.max_title_length' => '최대 제목 길이',
'basic_defaults.min_content_length' => '최소 내용 길이',
@@ -369,6 +371,9 @@ return [
'process_note' => '처리 메모',
'ids' => '신고 ID',
],
'reaction' => [
'reaction_type_id' => '반응 유형',
],
'blind' => [
'reason' => '블라인드 사유',
],
@@ -580,6 +585,12 @@ return [
],
// 신고 검증 메시지
'reaction' => [
'reaction_type_id' => [
'required' => '반응 유형은 필수입니다.',
'integer' => '반응 유형은 정수여야 합니다.',
],
],
'report' => [
'invalid_status_transition' => '현재 신고 상태에서는 해당 상태로 변경할 수 없습니다.',
'status' => [
@@ -8,11 +8,13 @@ use Modules\Sirsoft\Board\Http\Controllers\Admin\BoardTypeController;
use Modules\Sirsoft\Board\Http\Controllers\Admin\CommentController as AdminCommentController;
use Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController;
use Modules\Sirsoft\Board\Http\Controllers\Admin\PostController as AdminPostController;
use Modules\Sirsoft\Board\Http\Controllers\Admin\ReactionTypeController as AdminReactionTypeController;
use Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController as AdminReportController;
use Modules\Sirsoft\Board\Http\Controllers\User\AttachmentController as UserAttachmentController;
use Modules\Sirsoft\Board\Http\Controllers\User\BoardController as UserBoardController;
use Modules\Sirsoft\Board\Http\Controllers\User\CommentController as UserCommentController;
use Modules\Sirsoft\Board\Http\Controllers\User\PostController as UserPostController;
use Modules\Sirsoft\Board\Http\Controllers\User\ReactionController as UserReactionController;
use Modules\Sirsoft\Board\Http\Controllers\User\ReportController as UserReportController;
use Modules\Sirsoft\Board\Http\Controllers\User\UserActivityController;
@@ -126,6 +128,11 @@ Route::prefix('admin')->middleware(['auth:sanctum', 'admin'])->group(function ()
->middleware('permission:admin,sirsoft-board.settings.read')
->name('admin.settings.show');
// 반응 유형 목록 (게시판 설정 체크박스 옵션 소스, 확정 02·03)
Route::get('reaction-types', [AdminReactionTypeController::class, 'index'])
->middleware('permission:admin,sirsoft-board.settings.read')
->name('admin.reaction-types.index');
// 대시보드 - 오늘 새 글/댓글 현황 (진입 가드는 코어 core.dashboard.read + admin)
Route::get('dashboard/overview', [DashboardController::class, 'overview'])
->name('admin.dashboard.overview');
@@ -540,6 +547,10 @@ Route::prefix('boards/{slug}')->middleware(['throttle:600,1', 'auth:sanctum'])->
// 댓글 신고
Route::post('/comments/{commentId}/reports', [UserReportController::class, 'storeCommentReport'])
->name('comments.reports.store');
// 게시글 반응 (추천/비추천) — 등록/전환/해제 통합 (회원 전용, 확정 07)
Route::post('/posts/{postId}/react', [UserReactionController::class, 'react'])
->name('posts.react');
});
/*
@@ -146,6 +146,37 @@ class BoardManagementTest extends ModuleTestCase
$this->assertTrue($response->json('data.is_active'));
}
/**
* 게시판 생성 시 반응 기본값이 모듈 환경설정(basic_defaults)에서 시드된다 (이슈 #525 확정 13).
*
* @scenario case=new_board_seeds_reaction_defaults
* @effects new_board_seeds_reaction_defaults_from_settings
*/
public function test_board_created_seeds_reaction_defaults_from_settings(): void
{
// Given: 반응 관련 필드를 지정하지 않은 게시판 데이터 (기본값 시드 기대)
$slug = 'test-'.substr(md5(microtime()), 0, 8);
$data = [
'name' => ['ko' => '테스트 게시판', 'en' => 'Test Board'],
'slug' => $slug,
'type' => 'basic',
'show_view_count' => true,
'use_report' => false,
'board_manager_ids' => [$this->adminUser->uuid],
];
// When: 게시판 생성 API 호출
$response = $this->actingAs($this->adminUser)
->postJson('/api/modules/sirsoft-board/admin/boards', $data);
// Then: 반응 기본값(use_reaction=true, active_reaction_types=["like"])이 시드됨
$response->assertStatus(201);
$board = \Modules\Sirsoft\Board\Models\Board::where('slug', $slug)->firstOrFail();
$this->assertTrue($board->use_reaction);
$this->assertContains('like', $board->active_reaction_types ?? []);
}
/**
* is_active false로 게시판 생성 테스트
*/
@@ -0,0 +1,108 @@
<?php
namespace Modules\Sirsoft\Board\Tests\Feature\Admin;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Database\Seeders\BoardReactionTypeSeeder;
use Modules\Sirsoft\Board\Models\ReactionType;
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
/**
* 관리자 반응 유형 목록 API Feature 테스트.
*
* GET admin/reaction-types 목록 조회, 다국어 라벨 폴백(확정 15), 시더 등록 확인,
* 권한 경계를 검증한다 (이슈 #525 §10 테스트 범위).
*/
class ReactionTypeApiTest extends ModuleTestCase
{
private const ENDPOINT = '/api/modules/sirsoft-board/admin/reaction-types';
protected function setUp(): void
{
parent::setUp();
// FK(restrictOnDelete) 순서: 이력 먼저 정리 후 유형 삭제 (타 테스트 잔여 데이터 대비)
DB::table('board_reactions')->delete();
ReactionType::query()->delete();
$this->seed(BoardReactionTypeSeeder::class);
}
/**
* 권한 있는 관리자는 활성 유형 목록을 display_order 순으로 조회한다.
*/
public function test_admin_can_list_active_reaction_types(): void
{
$admin = $this->createAdminUser(['sirsoft-board.settings.read']);
$response = $this->actingAs($admin)
->withHeader('Accept-Language', 'ko')
->getJson(self::ENDPOINT);
$response->assertOk()
->assertJsonPath('data.reaction_types.0.code', 'like')
->assertJsonPath('data.reaction_types.0.name', '추천')
->assertJsonPath('data.reaction_types.0.icon', 'fas fa-thumbs-up')
->assertJsonPath('data.reaction_types.0.icon_name', 'fa-thumbs-up')
->assertJsonPath('data.reaction_types.1.code', 'dislike')
->assertJsonPath('data.reaction_types.1.name', '비추천')
->assertJsonPath('data.reaction_types.1.icon_name', 'fa-thumbs-down');
}
/**
* en 로케일에서는 영어 라벨을 반환한다.
*
* @scenario case=reaction_type_label_localized
* @effects reaction_type_name_localized
*/
public function test_reaction_type_name_localized_to_en(): void
{
$admin = $this->createAdminUser(['sirsoft-board.settings.read']);
$this->actingAs($admin)
->withHeader('Accept-Language', 'en')
->getJson(self::ENDPOINT)
->assertOk()
->assertJsonPath('data.reaction_types.0.name', 'Recommend');
}
/**
* 미입력 언어는 사이트 기본 언어(fallback)로 대체 노출된다 (확정 15).
*
* @scenario case=reaction_type_label_falls_back
* @effects missing_locale_falls_back_to_default
*/
public function test_missing_locale_falls_back(): void
{
// ja 미입력 유형을 하나 추가해 폴백 경로를 직접 검증
ReactionType::create([
'code' => 'love',
'name' => ['ko' => '사랑', 'en' => 'Love'],
'icon' => 'fas fa-heart',
'display_order' => 3,
'is_active' => true,
]);
$admin = $this->createAdminUser(['sirsoft-board.settings.read']);
$response = $this->actingAs($admin)
->withHeader('Accept-Language', 'ja')
->getJson(self::ENDPOINT);
$response->assertOk();
$love = collect($response->json('data.reaction_types'))->firstWhere('code', 'love');
$this->assertNotNull($love);
// ja 미입력 → fallback_locale(en) 또는 첫 값으로 폴백 (빈 문자열 아님)
$this->assertNotSame('', $love['name']);
}
/**
* 권한 없는 사용자는 403 으로 차단된다.
*/
public function test_user_without_permission_forbidden(): void
{
$user = $this->createUser();
$this->actingAs($user)->getJson(self::ENDPOINT)
->assertForbidden();
}
}
@@ -0,0 +1,325 @@
<?php
namespace Modules\Sirsoft\Board\Tests\Feature\User;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Database\Seeders\BoardReactionTypeSeeder;
use Modules\Sirsoft\Board\Models\ReactionType;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
/**
* 반응 API Feature 테스트.
*
* 등록/전환/해제, reaction_counts 증감 정합성(전환 포함), use_reaction off·비활성 유형
* 차단, 본인 글 차단, 비로그인 차단을 종단 검증한다 (이슈 #525 §10 테스트 범위).
*
* @scenario case=guest_react_blocked
*
* @effects guest_react_returns_401
*/
class ReactionApiTest extends BoardTestCase
{
private int $likeId;
private int $dislikeId;
protected function setUp(): void
{
parent::setUp();
// FK(restrictOnDelete) 순서: 이력 먼저 정리 후 유형 삭제 (타 테스트 잔여 데이터 대비)
DB::table('board_reactions')->delete();
ReactionType::query()->delete();
$this->seed(BoardReactionTypeSeeder::class);
$this->likeId = ReactionType::query()->where('code', 'like')->value('id');
$this->dislikeId = ReactionType::query()->where('code', 'dislike')->value('id');
$this->updateBoardSettings([
'use_reaction' => true,
'active_reaction_types' => ['like', 'dislike'],
]);
}
/**
* 반응 API 엔드포인트 URL 을 조립합니다.
*/
private function reactUrl(int $postId): string
{
return "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/react";
}
/**
* 반응 등록 → 카운트 +1, my_reaction_type_id 반영.
*/
public function test_register_reaction(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$response = $this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId]);
$response->assertOk()
->assertJsonPath('data.action', 'add')
->assertJsonPath('data.my_reaction_type_id', $this->likeId)
->assertJsonPath("data.reaction_counts.{$this->likeId}", 1);
$this->assertDatabaseHas('board_reactions', [
'user_id' => $reactor->id,
'target_id' => $postId,
'reaction_type_id' => $this->likeId,
]);
}
/**
* 다른 유형으로 전환 → 이전 -1·신규 +1 (증감 정합성).
*/
public function test_switch_reaction_adjusts_counts(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertOk();
$response = $this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->dislikeId]);
$response->assertOk()
->assertJsonPath('data.action', 'change')
->assertJsonPath('data.my_reaction_type_id', $this->dislikeId)
->assertJsonPath("data.reaction_counts.{$this->likeId}", 0)
->assertJsonPath("data.reaction_counts.{$this->dislikeId}", 1);
}
/**
* 같은 유형 재클릭 → 해제, 카운트 -1, my_reaction_type_id null.
*/
public function test_remove_reaction_on_same_type(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertOk();
$response = $this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId]);
$response->assertOk()
->assertJsonPath('data.action', 'remove')
->assertJsonPath('data.my_reaction_type_id', null)
->assertJsonPath("data.reaction_counts.{$this->likeId}", 0);
$this->assertDatabaseMissing('board_reactions', [
'user_id' => $reactor->id,
'target_id' => $postId,
]);
}
/**
* 비로그인 요청은 401 로 차단된다 (확정 07).
*/
public function test_guest_cannot_react(): void
{
$author = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertUnauthorized();
}
/**
* use_reaction 이 꺼진 게시판은 422 로 차단된다.
*/
public function test_react_blocked_when_use_reaction_off(): void
{
$this->updateBoardSettings(['use_reaction' => false]);
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertStatus(422);
}
/**
* 게시판이 켜지 않은(비활성) 유형은 검증 단계(422)에서 차단된다.
*/
public function test_react_blocked_for_inactive_type(): void
{
$this->updateBoardSettings(['active_reaction_types' => ['like']]);
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->dislikeId])
->assertStatus(422);
}
/**
* 본인 글에는 반응할 수 없다 (422, 확정 08).
*/
public function test_cannot_react_to_own_post(): void
{
$author = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->actingAs($author, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertStatus(422);
}
/**
* 다른 게시판 소속이 아닌(존재하지 않는) 게시글은 404 로 차단된다.
*/
public function test_react_to_missing_post_returns_404(): void
{
$reactor = $this->createUser();
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl(999999), ['reaction_type_id' => $this->likeId])
->assertNotFound();
}
/**
* 비밀글에 열람 권한이 없는 사용자는 반응할 수 없다 (422, secret_denied).
*
* 회귀: 반응 서비스가 비밀글 열람 권한을 검사하지 않으면, 본문(content=null)을
* 못 보는 사용자가 추천/비추천만 남기는 우회가 가능하다. 신고(canViewSecretContent)와
* 동일하게 열람 권한 없는 비밀글 반응을 서버가 최종 차단해야 한다.
*
* @scenario case=secret_post_react_blocked
*
* @effects secret_post_without_permission_returns_422
*/
public function test_react_blocked_on_secret_post_without_permission(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost([
'user_id' => $author->id,
'is_secret' => true,
]);
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertStatus(422)
->assertJsonPath('errors.code', 'reaction_not_allowed');
// 반응 이력이 남지 않아야 한다
$this->assertDatabaseMissing('board_reactions', [
'user_id' => $reactor->id,
'target_id' => $postId,
]);
}
/**
* 비밀글 열람 권한(posts.read-secret)이 있는 사용자는 비밀글에도 반응할 수 있다.
*
* 비밀글 가드가 열람 권한자까지 막지 않는지 확인한다 (신고 판정과 동일 기준).
*
* @scenario case=secret_post_react_allowed_with_permission
*
* @effects secret_post_with_read_secret_permission_allows_react
*/
public function test_react_allowed_on_secret_post_with_read_secret_permission(): void
{
$this->grantUserRolePermissions(['posts.read-secret']);
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost([
'user_id' => $author->id,
'is_secret' => true,
]);
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertOk()
->assertJsonPath('data.action', 'add');
$this->assertDatabaseHas('board_reactions', [
'user_id' => $reactor->id,
'target_id' => $postId,
'reaction_type_id' => $this->likeId,
]);
}
/**
* 게시글 상세 응답의 reaction_counts 가 유형 ID 키를 보존한다 (JSON 객체, 배열 재인덱싱 금지).
*
* 회귀: PostResource 의 reaction_counts 키가 정수 [1,2] 라 JsonResource::resolve() 가
* list 로 오인해 [count1, count2] 로 재인덱싱하면, 프론트가 reaction_counts[유형ID] 로
* 읽을 때 엉뚱한 인덱스를 읽어 개수가 항상 0/어긋나게 표시된다. 키는 항상 유형 ID 여야 한다.
*/
public function test_post_detail_reaction_counts_preserves_type_id_keys(): void
{
// 상세 조회는 posts.read 권한이 필요하다
$this->grantUserRolePermissions(['posts.read', 'posts.write']);
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $this->createUser()->id]);
// 추천 1회 등록
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertOk();
// 게시글 상세 조회 — reaction_counts 는 유형 ID 키로 개수를 담아야 한다
$detail = $this->actingAs($reactor, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
$detail->assertOk()
->assertJsonPath("data.reaction_counts.{$this->likeId}", 1)
->assertJsonPath("data.reaction_counts.{$this->dislikeId}", 0)
->assertJsonPath('data.my_reaction_type_id', $this->likeId);
// 키가 유형 ID 로 보존되는지 직접 확인 (0-기반 재인덱싱 아님)
$counts = $detail->json('data.reaction_counts');
$this->assertArrayHasKey((string) $this->likeId, $counts);
$this->assertArrayNotHasKey('0', $counts);
}
/**
* 반응 등록/전환/해제 시 활동 로그가 기록된다 (after_react 훅 → 리스너).
*
* @scenario case=react_logs_activity
*
* @effects react_logs_add_change_remove_activity
*/
public function test_react_writes_activity_log(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
// 등록 → reaction.add 로그
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId])
->assertOk();
$this->assertDatabaseHas('activity_logs', ['action' => 'reaction.add']);
// 전환 → reaction.change 로그
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->dislikeId])
->assertOk();
$this->assertDatabaseHas('activity_logs', ['action' => 'reaction.change']);
// 해제 → reaction.remove 로그
$this->actingAs($reactor, 'sanctum')
->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->dislikeId])
->assertOk();
$this->assertDatabaseHas('activity_logs', ['action' => 'reaction.remove']);
}
}
@@ -27,6 +27,8 @@ type BoardAuthFixtures = {
boardManageToken: string;
/** 게시판 조회 + 첨부 다운로드 권한 토큰 (#413-58b 행위자 기록 검증용) */
attachmentDownloadToken: string;
/** 게시판/게시글 조회 권한 토큰 (반응 등록/전환/해제 흐름 검증용, 반응은 로그인만 요구) */
reactionToken: string;
};
export const test = base.extend<BoardAuthFixtures>({
@@ -69,6 +71,15 @@ export const test = base.extend<BoardAuthFixtures>({
),
);
},
reactionToken: async ({}, use) => {
// 반응은 별도 권한 없이 로그인(auth:sanctum)만 요구. 게시글 조회 권한만 발급.
await use(
issueToken(
'sirsoft-board.boards.read',
'sirsoft-board.notice.posts.read',
),
);
},
});
export { authenticatePage };
@@ -0,0 +1,141 @@
/**
* 게시판 반응(추천/비추천) 사용자 흐름 — 등록 → 전환 → 해제 (이슈 #525).
*
* 게시글 상세에서 반응 버튼을 눌러 등록하고, 다른 유형으로 전환하고, 같은 유형을 재클릭해
* 해제하는 흐름을 브라우저에서 확인한다. 각 클릭 후 게시글 데이터소스를 재조회(refetchDataSource)
* 하므로, 서버 응답의 my_reaction_type_id·reaction_counts 가 버튼 하이라이트와 개수에 반영된다.
*
* 단위/레이아웃 테스트(ReactionServiceTest, ReactionApiTest, board-reaction-buttons.test.tsx)는
* 등록/전환/해제 판정·카운트 증감·렌더 분기를 검증하므로, 이 spec 은 실제 클릭 → API 호출 →
* 재조회 → 화면 갱신의 종단 흐름을 담당한다.
*
* @scenario surface=detail_register_switch_remove
* @effects detail_register_then_switch_then_remove_updates_ui,register_inserts_row_and_increments_count,switch_updates_row_and_adjusts_both_counts,remove_deletes_row_and_decrements_count,self_post_click_shows_denied_toast
*
* 활성화 절차: PlaywrightIssueToken 발급 + 반응 유형이 켜진 공개 게시판/게시글 시드가 가능한
* 환경에서 test.describe.skip → test.describe. SLUG/POST_ID 는 시드에 맞춰 조정.
*/
import { test, expect, authenticatePage } from '../../fixtures/board-auth';
const SLUG = 'free';
const POST_ID = 18;
const POST_PATH = `/board/${SLUG}/${POST_ID}`;
const REACT_API = `**/api/modules/sirsoft-board/boards/${SLUG}/posts/${POST_ID}/react`;
// 유형 ID (시드 순서상 like=1, dislike=2 가정 — 시드에 맞춰 조정)
const LIKE_ID = 1;
const DISLIKE_ID = 2;
// reactionToken 발급 계정이 작성자인 게시글 ID (본인 글 반응 차단 검증용) — 시드에 맞춰 조정
const OWN_POST_ID = 19;
const OWN_POST_PATH = `/board/${SLUG}/${OWN_POST_ID}`;
const OWN_POST_REACT_API = `**/api/modules/sirsoft-board/boards/${SLUG}/posts/${OWN_POST_ID}/react`;
test.describe('게시판 반응 등록→전환→해제 흐름 (#525)', () => {
// @scenario surface=detail_register_switch_remove
// @effects register_inserts_row_and_increments_count, switch_updates_row_and_adjusts_both_counts, remove_deletes_row_and_decrements_count
test('추천 등록 → 비추천 전환 → 비추천 해제 시 버튼 하이라이트·개수가 갱신된다', async ({
page,
reactionToken,
}) => {
await authenticatePage(page, reactionToken);
// react API 응답을 흐름 단계별로 제어 (등록/전환/해제)
const steps = [
{ action: 'add', my: LIKE_ID, counts: { [LIKE_ID]: 1, [DISLIKE_ID]: 0 } },
{ action: 'change', my: DISLIKE_ID, counts: { [LIKE_ID]: 0, [DISLIKE_ID]: 1 } },
{ action: 'remove', my: null, counts: { [LIKE_ID]: 0, [DISLIKE_ID]: 0 } },
];
let step = 0;
let reactBody: Record<string, unknown> | null = null;
await page.route(REACT_API, async (route) => {
reactBody = route.request().postDataJSON() as Record<string, unknown>;
const s = steps[Math.min(step, steps.length - 1)];
step += 1;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
success: true,
message: 'ok',
data: { action: s.action, my_reaction_type_id: s.my, reaction_counts: s.counts },
}),
});
});
await page.goto(POST_PATH);
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
const likeButton = page.getByRole('button', { name: /추천|Recommend/ }).first();
const dislikeButton = page.getByRole('button', { name: /비추천|Not Recommend/ }).first();
// 1) 추천 등록 → count +1, reaction_type_id = like
// 처리 중에는 버튼이 disabled(스피너) 되므로, 응답 도착 후 스피너가 완전히 걷히고
// 카운트 텍스트가 갱신될 때까지 기다린 다음 다음 클릭을 보낸다 — 그렇지 않으면 아직
// 리렌더 중인 버튼에 클릭이 씹혀 요청이 누락된다.
// step 자체를 폴링한다(2·3단계 모두 reaction_type_id=DISLIKE_ID 로 동일하므로,
// reactBody 값만 보면 세 번째 요청이 발생하지 않아도 이전 값과 우연히 일치해 통과해버린다).
await likeButton.click();
await expect.poll(() => step, { timeout: 5_000 }).toBe(1);
expect((reactBody as Record<string, unknown> | null)?.reaction_type_id).toBe(LIKE_ID);
await expect(likeButton).toHaveText(/추천\s*1/, { timeout: 5_000 });
await expect(dislikeButton).toBeEnabled({ timeout: 5_000 });
// 2) 비추천 전환 → like -1 · dislike +1
await dislikeButton.click();
await expect.poll(() => step, { timeout: 5_000 }).toBe(2);
expect((reactBody as Record<string, unknown> | null)?.reaction_type_id).toBe(DISLIKE_ID);
await expect(dislikeButton).toHaveText(/비추천\s*1/, { timeout: 5_000 });
await expect(dislikeButton).toBeEnabled({ timeout: 5_000 });
// 3) 비추천 재클릭 → 해제 (같은 유형 재요청)
await dislikeButton.click();
await expect.poll(() => step, { timeout: 5_000 }).toBe(3);
expect((reactBody as Record<string, unknown> | null)?.reaction_type_id).toBe(DISLIKE_ID);
});
// @scenario surface=detail_register_switch_remove
// @effects guest_react_returns_401
test('비로그인 상태에서 반응 클릭 시 로그인 안내 후 로그인 페이지로 이동한다', async ({ page }) => {
// 인증 없이 진입 (토큰 미주입)
await page.goto(POST_PATH);
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
const likeButton = page.getByRole('button', { name: /추천|Recommend/ }).first();
await likeButton.click();
// 비로그인 → 로그인 페이지로 이동 (redirect 파라미터로 원글 경로 전달)
await expect.poll(() => page.url(), { timeout: 5_000 }).toContain('/login');
expect(page.url()).toContain('redirect');
});
// @scenario surface=detail_register_switch_remove
// @effects self_post_click_shows_denied_toast
test('본인 글 반응 버튼 클릭 시 API 호출 없이 안내 토스트가 표시된다', async ({
page,
reactionToken,
}) => {
await authenticatePage(page, reactionToken);
// disabled 버튼은 클릭 이벤트 자체가 발생하지 않아 안내를 줄 수 없으므로, 본인 글에서는
// 버튼을 시각적으로만 비활성 톤으로 두고 클릭은 받아 switch 분기에서 토스트로 안내한다
// (트러블슈팅: PO 피드백 — disabled 버튼은 "왜 안 눌리는지" 사용자가 알 수 없음).
let reactCalled = false;
await page.route(OWN_POST_REACT_API, async (route) => {
reactCalled = true;
await route.abort();
});
await page.goto(OWN_POST_PATH);
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
const likeButton = page.getByRole('button', { name: /추천|Recommend/ }).first();
await likeButton.click();
await expect(page.getByText(/본인 글에는 반응할 수 없습니다|cannot react to your own post/i)).toBeVisible({
timeout: 5_000,
});
expect(reactCalled).toBe(false);
});
});
@@ -0,0 +1,90 @@
<?php
namespace Modules\Sirsoft\Board\Tests\Unit\Database\Seeders;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Database\Seeders\BoardReactionTypeSeeder;
use Modules\Sirsoft\Board\Models\ReactionType;
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
/**
* BoardReactionTypeSeeder 검증.
*
* 시더 실행 후 추천(like)/비추천(dislike) 2건이 정확한 code·아이콘·ko/en/ja
* 라벨로 등록되는지 확인한다 (이슈 #525 1단계 시더 작성 검증).
*/
class BoardReactionTypeSeederTest extends ModuleTestCase
{
protected function setUp(): void
{
parent::setUp();
// FK(restrictOnDelete) 순서: 이력 먼저 정리 (타 테스트 잔여 데이터 대비)
DB::table('board_reactions')->delete();
}
/**
* 시더가 추천/비추천 2건을 정확한 값으로 등록한다.
*
* @scenario case=reaction_type_label_localized
* @effects reaction_type_name_localized
*/
public function test_seeder_creates_like_and_dislike_types(): void
{
ReactionType::query()->delete();
$this->seed(BoardReactionTypeSeeder::class);
$this->assertSame(2, ReactionType::query()->count());
$like = ReactionType::query()->where('code', 'like')->first();
$this->assertNotNull($like);
$this->assertSame('fas fa-thumbs-up', $like->icon);
$this->assertSame(1, $like->display_order);
$this->assertTrue($like->is_active);
$this->assertSame(
['ko' => '추천', 'en' => 'Recommend', 'ja' => 'おすすめ'],
$like->name
);
$dislike = ReactionType::query()->where('code', 'dislike')->first();
$this->assertNotNull($dislike);
$this->assertSame('fas fa-thumbs-down', $dislike->icon);
$this->assertSame(2, $dislike->display_order);
$this->assertTrue($dislike->is_active);
$this->assertSame(
['ko' => '비추천', 'en' => 'Not Recommend', 'ja' => 'ひどい'],
$dislike->name
);
}
/**
* 시더는 code로 매칭하는 upsert라 재실행해도 중복 생성되지 않는다.
*/
public function test_seeder_is_idempotent(): void
{
ReactionType::query()->delete();
$this->seed(BoardReactionTypeSeeder::class);
$this->seed(BoardReactionTypeSeeder::class);
$this->assertSame(2, ReactionType::query()->count());
}
/**
* 한글이 유니코드 이스케이프 없이 저장된다 (AsUnicodeJson 캐스트).
*/
public function test_name_stored_as_unicode_json(): void
{
ReactionType::query()->delete();
$this->seed(BoardReactionTypeSeeder::class);
$raw = \Illuminate\Support\Facades\DB::table('board_reaction_types')
->where('code', 'like')
->value('name');
$this->assertStringContainsString('추천', $raw);
$this->assertStringNotContainsString('\\u', $raw);
}
}
@@ -0,0 +1,153 @@
<?php
namespace Modules\Sirsoft\Board\Tests\Unit\Repositories;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Database\Seeders\BoardReactionTypeSeeder;
use Modules\Sirsoft\Board\Models\Post;
use Modules\Sirsoft\Board\Models\ReactionType;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
/**
* ReactionRepository / ReactionTypeRepository 검증.
*
* upsert(등록/전환)·delete(해제)·recalculatePostReactionCounts(실제 행 기준 재집계) 및
* 유형 조회(getActive/findByCodes/findById/findByIds)를 검증한다.
*/
class ReactionRepositoryTest extends BoardTestCase
{
private ReactionRepositoryInterface $reactionRepository;
private ReactionTypeRepositoryInterface $reactionTypeRepository;
private int $likeId;
private int $dislikeId;
protected function setUp(): void
{
parent::setUp();
DB::table('board_reactions')->delete();
ReactionType::query()->delete();
$this->seed(BoardReactionTypeSeeder::class);
$this->likeId = ReactionType::query()->where('code', 'like')->value('id');
$this->dislikeId = ReactionType::query()->where('code', 'dislike')->value('id');
$this->reactionRepository = app(ReactionRepositoryInterface::class);
$this->reactionTypeRepository = app(ReactionTypeRepositoryInterface::class);
}
/**
* upsert 는 없으면 INSERT, 있으면 유형만 UPDATE 한다 (사용자당 1행 유지).
*
* @scenario case=register_first_like
* @effects register_inserts_row_and_increments_count
*/
public function test_upsert_inserts_then_updates_single_row(): void
{
$user = $this->createUser();
$postId = $this->createTestPost();
$first = $this->reactionRepository->upsert($user->id, 'post', $postId, $this->likeId, $this->board->id);
$this->assertSame($this->likeId, (int) $first->reaction_type_id);
$second = $this->reactionRepository->upsert($user->id, 'post', $postId, $this->dislikeId, $this->board->id);
$this->assertSame($first->id, $second->id);
$this->assertSame($this->dislikeId, (int) $second->reaction_type_id);
$this->assertSame(1, \Modules\Sirsoft\Board\Models\Reaction::query()
->where('user_id', $user->id)->where('target_id', $postId)->count());
}
/**
* findByUserAndTarget 은 사용자+대상 유일 키로 조회한다.
*/
public function test_find_by_user_and_target(): void
{
$user = $this->createUser();
$postId = $this->createTestPost();
$this->reactionRepository->upsert($user->id, 'post', $postId, $this->likeId, $this->board->id);
$found = $this->reactionRepository->findByUserAndTarget($user->id, 'post', $postId);
$this->assertNotNull($found);
$this->assertSame($this->likeId, (int) $found->reaction_type_id);
$this->assertNull($this->reactionRepository->findByUserAndTarget($user->id, 'post', $postId + 999));
}
/**
* delete 는 이력 행을 삭제한다.
*/
public function test_delete_removes_reaction(): void
{
$user = $this->createUser();
$postId = $this->createTestPost();
$reaction = $this->reactionRepository->upsert($user->id, 'post', $postId, $this->likeId, $this->board->id);
$this->assertTrue($this->reactionRepository->delete($reaction));
$this->assertNull($this->reactionRepository->findByUserAndTarget($user->id, 'post', $postId));
}
/**
* recalculatePostReactionCounts 는 board_reactions 실제 행 수를 그대로 반영한다
* (델타 누적이 아닌 COUNT 재집계 — 캐시가 실제 데이터와 항상 일치해야 함).
*/
public function test_recalculate_post_reaction_counts_reflects_actual_rows(): void
{
$postId = $this->createTestPost(['reaction_counts' => json_encode([(string) $this->likeId => 99])]);
// 실제 반응 행 없음 → 캐시에 남아있던 오염된 값(99)은 사라지고, 이전에
// 노출됐던 유형 키는 0으로 유지된다 (API 응답 계약 — 키 자체는 보존).
$counts = $this->reactionRepository->recalculatePostReactionCounts($postId);
$this->assertSame([(string) $this->likeId => 0], $counts);
$userA = $this->createUser();
$userB = $this->createUser();
$this->reactionRepository->upsert($userA->id, 'post', $postId, $this->likeId, $this->board->id);
$this->reactionRepository->upsert($userB->id, 'post', $postId, $this->dislikeId, $this->board->id);
$counts = $this->reactionRepository->recalculatePostReactionCounts($postId);
$this->assertSame(1, $counts[(string) $this->likeId]);
$this->assertSame(1, $counts[(string) $this->dislikeId]);
$post = Post::findOrFail($postId);
$this->assertSame($counts, $post->reaction_counts);
}
/**
* getActive 는 활성 유형을 display_order 순으로 반환한다.
*/
public function test_get_active_returns_ordered_active_types(): void
{
$active = $this->reactionTypeRepository->getActive();
$this->assertSame(['like', 'dislike'], $active->pluck('code')->all());
}
/**
* findByCodes 는 code 목록으로 활성 유형만 조회한다.
*/
public function test_find_by_codes(): void
{
$found = $this->reactionTypeRepository->findByCodes(['dislike']);
$this->assertSame(['dislike'], $found->pluck('code')->all());
$this->assertTrue($this->reactionTypeRepository->findByCodes([])->isEmpty());
}
/**
* findById / findByIds 로 유형을 조회한다.
*/
public function test_find_by_id_and_ids(): void
{
$this->assertSame('like', $this->reactionTypeRepository->findById($this->likeId)?->code);
$this->assertNull($this->reactionTypeRepository->findById(999999));
$byIds = $this->reactionTypeRepository->findByIds([$this->likeId, $this->dislikeId]);
$this->assertSame(['like', 'dislike'], $byIds->pluck('code')->all());
$this->assertTrue($this->reactionTypeRepository->findByIds([])->isEmpty());
}
}
@@ -0,0 +1,276 @@
<?php
namespace Modules\Sirsoft\Board\Tests\Unit\Services;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Database\Seeders\BoardReactionTypeSeeder;
use Modules\Sirsoft\Board\Exceptions\PostNotFoundException;
use Modules\Sirsoft\Board\Exceptions\ReactionNotAllowedException;
use Modules\Sirsoft\Board\Models\Reaction;
use Modules\Sirsoft\Board\Models\ReactionType;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
use Modules\Sirsoft\Board\Services\ReactionService;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
/**
* ReactionService 검증.
*
* 등록/전환/해제, reaction_counts 증감 정합성(전환 포함), use_reaction off,
* 비활성 유형, 본인 글 차단, 스코프 검증을 검증한다 (이슈 #525 §10 테스트 범위).
*/
class ReactionServiceTest extends BoardTestCase
{
private ReactionService $service;
private int $likeId;
private int $dislikeId;
protected function setUp(): void
{
parent::setUp();
DB::table('board_reactions')->delete();
ReactionType::query()->delete();
$this->seed(BoardReactionTypeSeeder::class);
$this->likeId = ReactionType::query()->where('code', 'like')->value('id');
$this->dislikeId = ReactionType::query()->where('code', 'dislike')->value('id');
$this->updateBoardSettings([
'use_reaction' => true,
'active_reaction_types' => ['like', 'dislike'],
]);
$this->service = app(ReactionService::class);
}
/**
* 반응이 없던 게시글에 반응하면 신규 등록되고 카운트가 +1 된다.
*
* @scenario case=register_first_like
* @effects register_inserts_row_and_increments_count
*/
public function test_react_adds_new_reaction_and_increments_count(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$result = $this->service->react($reactor->id, $this->board, $postId, $this->likeId);
$this->assertSame('add', $result['action']);
$this->assertSame($this->likeId, $result['reaction_type_id']);
$this->assertSame(1, $result['reaction_counts'][(string) $this->likeId]);
$this->assertDatabaseHas('board_reactions', [
'user_id' => $reactor->id,
'target_type' => 'post',
'target_id' => $postId,
'reaction_type_id' => $this->likeId,
]);
}
/**
* 다른 유형으로 전환하면 이전 유형 -1·신규 유형 +1 이 동시 반영된다 (단일 트랜잭션).
*
* @scenario case=switch_like_to_dislike
* @effects switch_updates_row_and_adjusts_both_counts, switch_count_atomic_in_single_transaction
*/
public function test_react_switches_type_and_adjusts_both_counts(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->service->react($reactor->id, $this->board, $postId, $this->likeId);
$result = $this->service->react($reactor->id, $this->board->fresh(), $postId, $this->dislikeId);
$this->assertSame('change', $result['action']);
$this->assertSame(0, $result['reaction_counts'][(string) $this->likeId]);
$this->assertSame(1, $result['reaction_counts'][(string) $this->dislikeId]);
// 사용자당 1행 유지 (전환은 UPDATE)
$this->assertSame(1, Reaction::query()->where('user_id', $reactor->id)->where('target_id', $postId)->count());
$this->assertDatabaseHas('board_reactions', [
'user_id' => $reactor->id,
'target_id' => $postId,
'reaction_type_id' => $this->dislikeId,
]);
}
/**
* 같은 유형을 재클릭하면 해제되어 이력 행이 삭제되고 카운트가 -1 된다.
*
* @scenario case=remove_same_type
* @effects remove_deletes_row_and_decrements_count
*/
public function test_react_same_type_removes_reaction_and_decrements_count(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->service->react($reactor->id, $this->board, $postId, $this->likeId);
$result = $this->service->react($reactor->id, $this->board->fresh(), $postId, $this->likeId);
$this->assertSame('remove', $result['action']);
$this->assertNull($result['reaction_type_id']);
$this->assertSame(0, $result['reaction_counts'][(string) $this->likeId]);
$this->assertDatabaseMissing('board_reactions', [
'user_id' => $reactor->id,
'target_id' => $postId,
]);
}
/**
* 카운트는 0 미만으로 내려가지 않는다 (해제 시 클램프).
*
* @scenario case=count_never_negative_on_over_remove
* @effects reaction_count_never_negative
*/
public function test_reaction_count_never_goes_below_zero(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id, 'reaction_counts' => json_encode([])]);
$this->service->react($reactor->id, $this->board, $postId, $this->likeId);
$result = $this->service->react($reactor->id, $this->board->fresh(), $postId, $this->likeId);
$this->assertGreaterThanOrEqual(0, $result['reaction_counts'][(string) $this->likeId]);
}
/**
* 캐시된 reaction_counts 가 실제 board_reactions 행 수와 어긋나 있어도(예: 짧은
* 시간에 연속 전송된 요청이 서버 도착 순서와 다르게 처리되어 캐시가 오염된 경우),
* 다음 반응 처리가 실제 행을 재집계해 캐시를 실제 데이터와 일치하는 값으로
* 정정한다. 델타(증감) 누적 방식은 이런 오염을 스스로 고치지 못하고 계속
* 누적시키므로, 실제 COUNT 재집계 방식임을 검증한다.
*
* @scenario case=polluted_cache_self_heals_on_next_reaction
* @effects reaction_counts_matches_actual_rows_after_recalculation
*/
public function test_react_recalculates_from_actual_rows_and_heals_polluted_cache(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
// 캐시에는 실제와 무관한 오염된 값(양쪽 다 1)을 미리 심어둔다.
$postId = $this->createTestPost([
'user_id' => $author->id,
'reaction_counts' => json_encode([(string) $this->likeId => 1, (string) $this->dislikeId => 1]),
]);
$result = $this->service->react($reactor->id, $this->board, $postId, $this->likeId);
// 실제 반응 행은 이번에 등록된 like 1건뿐 — 오염된 dislike=1 은 사라지고
// 캐시가 실제 상태(like=1, dislike=0)로 정정되어야 한다.
$this->assertSame(1, $result['reaction_counts'][(string) $this->likeId]);
$this->assertSame(0, $result['reaction_counts'][(string) $this->dislikeId]);
$this->assertSame(
1,
Reaction::query()->where('target_id', $postId)->where('target_type', 'post')->count(),
);
}
/**
* use_reaction 이 꺼진 게시판은 반응이 차단된다.
*
* @scenario case=use_reaction_off_react_blocked
* @effects use_reaction_off_react_returns_422
*/
public function test_react_blocked_when_use_reaction_off(): void
{
$this->updateBoardSettings(['use_reaction' => false]);
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->expectException(ReactionNotAllowedException::class);
$this->service->react($reactor->id, $this->board->fresh(), $postId, $this->likeId);
}
/**
* 게시판이 켜지 않은(비활성) 유형은 차단된다.
*
* @scenario case=inactive_type_react_blocked
* @effects inactive_type_react_returns_422
*/
public function test_react_blocked_for_inactive_type_on_board(): void
{
$this->updateBoardSettings(['active_reaction_types' => ['like']]);
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->expectException(ReactionNotAllowedException::class);
$this->service->react($reactor->id, $this->board->fresh(), $postId, $this->dislikeId);
}
/**
* 본인 글에는 반응할 수 없다.
*
* @scenario case=self_post_react_blocked
* @effects self_post_react_returns_422
*/
public function test_react_blocked_on_own_post(): void
{
$author = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$this->expectException(ReactionNotAllowedException::class);
$this->service->react($author->id, $this->board, $postId, $this->likeId);
}
/**
* 다른 게시판 소속 게시글 ID 로는 반응할 수 없다 (스코프 검증).
*
* @scenario case=post_not_in_board
* @effects post_not_in_board_returns_404
*/
public function test_react_blocked_for_post_not_in_board(): void
{
$reactor = $this->createUser();
$this->expectException(PostNotFoundException::class);
$this->service->react($reactor->id, $this->board, 999999, $this->likeId);
}
/**
* 스코프 검증 통과 직후·락 획득 시점 사이에 게시글이 삭제되는 레이스 상황에서도
* PostNotFoundException 으로 일관되게 처리된다.
*
* lockPostForReaction() 은 findOrFail() 을 사용해 락을 거는데, 이 시점에 게시글이
* 없으면 Eloquent 의 ModelNotFoundException 이 던져진다. 이를 감싸지 않으면 컨트롤러의
* catch 블록 어디에도 걸리지 않아 일반 \Exception 으로 떨어져 500 에러가 되고, 사용자는
* "반응 처리에 실패했습니다"만 보게 되어 게시글이 없다는 실제 원인을 알 수 없다.
*
* @scenario case=post_deleted_between_scope_check_and_lock
* @effects post_not_in_board_returns_404
*/
public function test_react_wraps_model_not_found_during_lock_as_post_not_found(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$repository = $this->mock(ReactionRepositoryInterface::class);
$repository->shouldReceive('lockPostForReaction')
->once()
->with($postId)
->andThrow(new ModelNotFoundException());
$service = new ReactionService(
$repository,
app(\Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface::class),
app(\Modules\Sirsoft\Board\Repositories\Contracts\PostRepositoryInterface::class),
);
$this->expectException(PostNotFoundException::class);
$service->react($reactor->id, $this->board, $postId, $this->likeId);
}
}
@@ -0,0 +1,105 @@
feature: 게시판 반응(추천/비추천)
description: |
이슈 #525 — 게시글에 반응(추천/비추천)을 남기는 기능. 로그인 회원이 게시글에 대해
게시판이 켠 반응 유형 중 하나를 선택해 남긴다. 공개 리액션이라 개수를 항상 노출하고,
본인 글 셀프 반응 제한·로그인 필수 등 어뷰징 방지 조건이 붙는다.
핵심 동작 (확정):
- 반응 유형은 DB 테이블(board_reaction_types)로 관리, 초기 시드 추천(like)/비추천(dislike)
- 글당 반응 1개 — 같은 유형 재클릭=해제(DELETE), 다른 유형=전환(UPDATE, 이전 -1·신규 +1)
- 카운트는 board_posts.reaction_counts JSON(키=유형 ID), 증감은 이력 쓰기와 단일 트랜잭션
- 게시판별 use_reaction on/off + active_reaction_types(code 배열) 로 노출 유형 결정
- 활성 유형이 0개면 반응 영역 자체가 미렌더 (use_reaction on 이어도)
- 비로그인 차단(401), 본인 글 차단(422), 비활성 유형 차단(422), 타 게시판 게시글 404
- 다국어 미입력 언어는 사이트 기본 언어로 폴백
# axis 의미:
# case 는 (기존 반응 상태 · 요청 유형 · 게시판 설정 · 사용자 상태) 의 의미 있는 조합을
# 한 축으로 열거한다. 독립 축을 곱하면 무의미한 조합(예: locale × guard)이 폭증하므로
# 기획서 확정 시나리오만 case 로 둔다 (board-conditional-validation.yaml 관례).
axes:
case:
- register_first_like
- switch_like_to_dislike
- remove_same_type
- count_never_negative_on_over_remove
- polluted_cache_self_heals_on_next_reaction
- guest_react_blocked
- self_post_react_blocked
- inactive_type_react_blocked
- use_reaction_off_react_blocked
- post_not_in_board
- post_deleted_between_scope_check_and_lock
- detail_shows_active_types_even_zero
- detail_hidden_when_no_active_types
- detail_hidden_when_use_reaction_off
- author_buttons_disabled
- settings_toggle_and_checkbox_persist
- new_board_seeds_reaction_defaults
- reaction_type_label_localized
- reaction_type_label_falls_back
- react_logs_activity
- react_failure_rolls_back
- react_pending_disables_button
- secret_post_react_blocked
- secret_post_react_allowed_with_permission
- reaction_area_hidden_on_inaccessible_secret_post
effects:
# 등록/전환/해제 + 카운트 증감 정합성
- register_inserts_row_and_increments_count
- switch_updates_row_and_adjusts_both_counts
- remove_deletes_row_and_decrements_count
- reaction_count_never_negative
- switch_count_atomic_in_single_transaction
- reaction_counts_matches_actual_rows_after_recalculation
# 어뷰징/상태 차단
- guest_react_returns_401
- self_post_react_returns_422
- inactive_type_react_returns_422
- use_reaction_off_react_returns_422
- post_not_in_board_returns_404
# 비밀글 열람 권한 가드 (신고와 동일 기준)
- secret_post_without_permission_returns_422
- secret_post_with_read_secret_permission_allows_react
- reaction_area_hidden_when_secret_content_inaccessible
# 화면 렌더 (확정 09·11)
- active_types_always_shown_even_zero_count
- reaction_area_hidden_when_no_active_types
- reaction_area_hidden_when_use_reaction_off
- author_sees_buttons_disabled
- my_reaction_type_highlighted
# 설정 (확정 02·13)
- settings_toggle_persists_use_reaction
- settings_checkbox_persists_active_reaction_types
- new_board_seeds_reaction_defaults_from_settings
# 다국어 (확정 15)
- reaction_type_name_localized
- missing_locale_falls_back_to_default
# 활동 로그
- react_logs_add_change_remove_activity
# 프론트 UX (낙관 하이라이트 + 응답값 개수 + 실패 롤백)
- reaction_failure_rolls_back_optimistic_state
- reaction_pending_disables_button
- self_post_click_shows_denied_toast
# E2E 흐름
- detail_register_then_switch_then_remove_updates_ui
test_files:
- modules/_bundled/sirsoft-board/tests/Unit/Services/ReactionServiceTest.php
- modules/_bundled/sirsoft-board/tests/Unit/Repositories/ReactionRepositoryTest.php
- modules/_bundled/sirsoft-board/tests/Unit/Database/Seeders/BoardReactionTypeSeederTest.php
- modules/_bundled/sirsoft-board/tests/Feature/User/ReactionApiTest.php
- modules/_bundled/sirsoft-board/tests/Feature/Admin/ReactionTypeApiTest.php
- modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardManagementTest.php
- templates/_bundled/sirsoft-basic/__tests__/layouts/board-reaction-buttons.test.tsx
- modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-reaction-settings.test.tsx
- modules/_bundled/sirsoft-board/tests/Playwright/specs/user/board-reaction-flow.spec.ts
validation:
audit_rule: frontend-change-requires-e2e
note: |
입력 조합 전수(case)는 백엔드/프론트/E2E 테스트가 @scenario/@effects 마킹으로 커버한다.
브라우저 수준의 등록→전환→해제 사용자 흐름은 board-reaction-flow.spec.ts 가 담당한다
(PlaywrightIssueToken 발급 가능 환경에서 describe.skip 해제).
@@ -70,6 +70,7 @@
- 템플릿 목록 화면의 탭 영역이 본문 여백과 어긋나 가로로 밀려 보일 수 있던 문제를 수정했습니다.
- 메일 템플릿·알림 정의 목록에서 각 행이 같은 식별자를 공유해 화면 읽기 도구가 항목을 구분하지 못하던 문제를 수정했습니다.
- 관리자 목록에서 검색·필터·정렬을 바꿀 때 표가 다시 그려지는 동안 로딩 표시가 나오지 않던 문제를 수정했습니다. 응답이 늦으면 아무 반응이 없는 것처럼 보여 같은 조작을 반복하게 되던 상황이 사라집니다.
- 태그 입력(다중 검색 선택) 컴포넌트의 드롭다운 목록이 화면 하단에 고정된 저장/취소 버튼 등에 가려 일부 항목을 선택하기 어렵던 문제를 수정했습니다.
### Added
@@ -1 +1 @@
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Pretendard", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .08Line truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Pretendard", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .08Line truncated
@@ -1,15 +1,15 @@
var SirsoftAdminBasic=(function(R,h,v,Ln){"use strict";var iA=Object.defineProperty;var aA=(R,h,v)=>h in R?iA(R,h,{enumerable:!0,configurable:!0,writable:!0,value:v}):R[h]=v;var Pe=(R,h,v)=>aA(R,typeof h!="symbol"?h+"":h,v);var za,vr;function fb(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n in e)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:()=>e[n]})}}return t.default=e,Object.freeze(t)}const F=fb(v),hb=()=>window.G7Core,ae=v.forwardRef(({children:e,variant:t="primary",size:n="md",className:r="",...s},o)=>{const i="button",a=hb()?.style?.mergeClasses?.(i,r)??`${i} ${r}`;return h.jsx("button",{ref:o,className:a,...s,children:e})});ae.displayName="Button";const Pd=v.forwardRef(({accept:e,maxSize:t,onChange:n,onError:r,buttonText:s="Browse",placeholder:o="No file selected",className:i="",disabled:a,...l},c)=>{const d=v.useRef(null),u=c||d,[f,g]=v.useState(""),m={},p={};Object.entries(l).forEach(([x,M])=>{x.startsWith("data-editor-")||x==="id"||x==="onMouseMove"||x==="onMouseLeave"?m[x]=M:p[x]=M});const y=v.useCallback(()=>{!a&&u.current&&u.current.click()},[a,u]),b=v.useCallback(x=>{const M=x.target.files?.[0]||null;if(!M){g(""),n?.({target:{value:null,name:""}});return}if(t&&M.size>t*1024*1024){const S=`File size exceeds ${t}MB limit`;r?.(S),u.current&&(u.current.value=""),g("");return}g(M.name),n?.({target:{value:M,name:M.name}})},[t,n,r,u]),_=v.useCallback(x=>{x.stopPropagation(),u.current&&(u.current.value=""),g(""),n?.({target:{value:null,name:""}})},[n,u]);return h.jsxs("div",{className:`flex items-center gap-2 ${i}`,...m,children:[h.jsx("input",{ref:u,type:"file",accept:e,onChange:b,disabled:a,className:"hidden",...p}),h.jsx("button",{type:"button",onClick:y,disabled:a,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:s}),h.jsx("span",{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:f||o}),f&&!a&&h.jsx("button",{type:"button",onClick:_,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:h.jsx("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});Pd.displayName="FileInput";const gb=e=>e==="checkbox"?"checkbox":e==="radio"?"radio":"input",pb=(e,t)=>{const n=gb(e);return t?t.split(/\s+/).filter(Boolean).includes(n)?t:`${n} ${t}`:n},kt=v.forwardRef(({label:e,error:t,className:n="",onChange:r,onKeyPress:s,value:o,defaultValue:i,type:a="text",checked:l,defaultChecked:c,...d},u)=>{const f=a==="radio"||a==="checkbox",g=pb(a,n),m=v.useRef(!1),p=v.useRef(null);v.useImperativeHandle(u,()=>p.current);const[y,b]=v.useState(o??i??"");v.useEffect(()=>{if(!f&&!m.current&&o!==void 0){if(p.current&&document.activeElement===p.current)return;b(o)}},[o,f]);const _=v.useCallback(()=>{m.current=!0},[]),x=v.useCallback(E=>{if(m.current=!1,r){const A=E.target.value;b(A);const P=new Event("change",{bubbles:!0});Object.defineProperty(P,"target",{writable:!1,value:{value:A}}),Object.defineProperty(P,"currentTarget",{writable:!1,value:{value:A}}),r(P)}},[r]),M=v.useCallback(E=>{const A=E.target.value;b(A),!m.current&&r&&r(E)},[r]),S=v.useCallback(E=>{m.current||s&&s(E)},[s]),{loadingactions:k,formdata:N,...C}=d;if(f)return h.jsx("input",{ref:p,type:a,className:g,onChange:r,checked:l,defaultChecked:c,value:o,...C});const w=o!==void 0?{value:y}:{defaultValue:i};return h.jsx("input",{ref:p,type:a,className:g,onChange:M,onKeyPress:S,onCompositionStart:_,onCompositionEnd:x,...w,...C})});kt.displayName="Input";const sr=({label:e,error:t,className:n="",onChange:r,value:s,defaultValue:o,...i})=>{const a=v.useRef(!1),l=v.useRef(null),[c,d]=v.useState(s??o??"");v.useEffect(()=>{!a.current&&s!==void 0&&d(s)},[s]);const u=v.useCallback(()=>{a.current=!0},[]),f=v.useCallback(M=>{if(a.current=!1,r){const S=M.target.value;d(S);const k=new Event("change",{bubbles:!0});Object.defineProperty(k,"target",{writable:!1,value:{value:S}}),Object.defineProperty(k,"currentTarget",{writable:!1,value:{value:S}}),r(k)}},[r]),g=v.useCallback(M=>{const S=M.target.value;d(S),!a.current&&r&&r(M)},[r]),{loadingactions:m,formdata:p,...y}=i,b=s!==void 0?{value:c}:{defaultValue:o},x=(n??"").split(/\s+/).filter(Boolean).includes("textarea")?n??"":["textarea",n].filter(Boolean).join(" ");return h.jsx("textarea",{ref:l,className:x,onChange:g,onCompositionStart:u,onCompositionEnd:f,...b,...y})},ln=({children:e,className:t="",...n})=>h.jsx("label",{className:t,...n,children:e}),I=v.forwardRef(({children:e,className:t="",...n},r)=>h.jsx("div",{ref:r,classNameLine truncated
var SirsoftAdminBasic=(function(R,h,v,$n){"use strict";var nA=Object.defineProperty;var rA=(R,h,v)=>h in R?nA(R,h,{enumerable:!0,configurable:!0,writable:!0,value:v}):R[h]=v;var Pe=(R,h,v)=>rA(R,typeof h!="symbol"?h+"":h,v);var Va,wr;function fb(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n in e)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:()=>e[n]})}}return t.default=e,Object.freeze(t)}const F=fb(v),hb=()=>window.G7Core,ae=v.forwardRef(({children:e,variant:t="primary",size:n="md",className:r="",...s},o)=>{const i="button",a=hb()?.style?.mergeClasses?.(i,r)??`${i} ${r}`;return h.jsx("button",{ref:o,className:a,...s,children:e})});ae.displayName="Button";const $d=v.forwardRef(({accept:e,maxSize:t,onChange:n,onError:r,buttonText:s="Browse",placeholder:o="No file selected",className:i="",disabled:a,...l},c)=>{const d=v.useRef(null),u=c||d,[f,g]=v.useState(""),m={},p={};Object.entries(l).forEach(([x,M])=>{x.startsWith("data-editor-")||x==="id"||x==="onMouseMove"||x==="onMouseLeave"?m[x]=M:p[x]=M});const y=v.useCallback(()=>{!a&&u.current&&u.current.click()},[a,u]),b=v.useCallback(x=>{const M=x.target.files?.[0]||null;if(!M){g(""),n?.({target:{value:null,name:""}});return}if(t&&M.size>t*1024*1024){const C=`File size exceeds ${t}MB limit`;r?.(C),u.current&&(u.current.value=""),g("");return}g(M.name),n?.({target:{value:M,name:M.name}})},[t,n,r,u]),_=v.useCallback(x=>{x.stopPropagation(),u.current&&(u.current.value=""),g(""),n?.({target:{value:null,name:""}})},[n,u]);return h.jsxs("div",{className:`flex items-center gap-2 ${i}`,...m,children:[h.jsx("input",{ref:u,type:"file",accept:e,onChange:b,disabled:a,className:"hidden",...p}),h.jsx("button",{type:"button",onClick:y,disabled:a,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:s}),h.jsx("span",{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:f||o}),f&&!a&&h.jsx("button",{type:"button",onClick:_,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:h.jsx("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});$d.displayName="FileInput";const gb=e=>e==="checkbox"?"checkbox":e==="radio"?"radio":"input",pb=(e,t)=>{const n=gb(e);return t?t.split(/\s+/).filter(Boolean).includes(n)?t:`${n} ${t}`:n},St=v.forwardRef(({label:e,error:t,className:n="",onChange:r,onKeyPress:s,value:o,defaultValue:i,type:a="text",checked:l,defaultChecked:c,...d},u)=>{const f=a==="radio"||a==="checkbox",g=pb(a,n),m=v.useRef(!1),p=v.useRef(null);v.useImperativeHandle(u,()=>p.current);const[y,b]=v.useState(o??i??"");v.useEffect(()=>{if(!f&&!m.current&&o!==void 0){if(p.current&&document.activeElement===p.current)return;b(o)}},[o,f]);const _=v.useCallback(()=>{m.current=!0},[]),x=v.useCallback(E=>{if(m.current=!1,r){const D=E.target.value;b(D);const P=new Event("change",{bubbles:!0});Object.defineProperty(P,"target",{writable:!1,value:{value:D}}),Object.defineProperty(P,"currentTarget",{writable:!1,value:{value:D}}),r(P)}},[r]),M=v.useCallback(E=>{const D=E.target.value;b(D),!m.current&&r&&r(E)},[r]),C=v.useCallback(E=>{m.current||s&&s(E)},[s]),{loadingactions:k,formdata:N,...S}=d;if(f)return h.jsx("input",{ref:p,type:a,className:g,onChange:r,checked:l,defaultChecked:c,value:o,...S});const w=o!==void 0?{value:y}:{defaultValue:i};return h.jsx("input",{ref:p,type:a,className:g,onChange:M,onKeyPress:C,onCompositionStart:_,onCompositionEnd:x,...w,...S})});St.displayName="Input";const ir=({label:e,error:t,className:n="",onChange:r,value:s,defaultValue:o,...i})=>{const a=v.useRef(!1),l=v.useRef(null),[c,d]=v.useState(s??o??"");v.useEffect(()=>{!a.current&&s!==void 0&&d(s)},[s]);const u=v.useCallback(()=>{a.current=!0},[]),f=v.useCallback(M=>{if(a.current=!1,r){const C=M.target.value;d(C);const k=new Event("change",{bubbles:!0});Object.defineProperty(k,"target",{writable:!1,value:{value:C}}),Object.defineProperty(k,"currentTarget",{writable:!1,value:{value:C}}),r(k)}},[r]),g=v.useCallback(M=>{const C=M.target.value;d(C),!a.current&&r&&r(M)},[r]),{loadingactions:m,formdata:p,...y}=i,b=s!==void 0?{value:c}:{defaultValue:o},x=(n??"").split(/\s+/).filter(Boolean).includes("textarea")?n??"":["textarea",n].filter(Boolean).join(" ");return h.jsx("textarea",{ref:l,className:x,onChange:g,onCompositionStart:u,onCompositionEnd:f,...b,...y})},ln=({children:e,className:t="",...n})=>h.jsx("label",{className:t,...n,children:e}),I=v.forwardRef(({children:e,className:t="",...n},r)=>h.jsx("div",{ref:r,classNameLine truncated
inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium
${fe?"bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"}
`,children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),U(K),fe&&h.jsx(J,{className:"text-red-500 dark:text-red-400",children:"*"}),!fe&&h.jsx(ae,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",onClick:()=>z(K),disabled:l,children:h.jsx(oe,{name:"xmark",className:"w-3 h-3"})})]}),fe&&q&&G()]}),n==="textarea"?h.jsx(sr,{name:`${f}[${K}]`,value:S[K]||"",onChange:le=>O(K,le.target.value),placeholder:te,required:a&&fe,disabled:l,maxLength:c,rows:d,className:`${ee} resize-vertical`}):h.jsx(kt,{type:"text",name:`${f}[${K}]`,value:S[K]||"",onChange:le=>O(K,le.target.value),placeholder:te,required:a&&fe,disabled:l,maxLength:c,className:ne})]},K)},[f,S,O,i,a,l,c,d,n,U,x,z,G,b]),j=v.useCallback(K=>{const q=b?"input input-error":"input",ne=b?"textarea input-error":"textarea",ee=`${i}${i?" (":""}${U(K)}${i?")":""}`;return h.jsx(I,{children:n==="textarea"?h.jsx(sr,{name:`${f}[${K}]`,value:S[K]||"",onChange:fe=>O(K,fe.target.value),placeholder:ee,required:a&&K===x,disabled:l,maxLength:c,rows:d,className:`${ne} resize-vertical`}):h.jsx(kt,{type:"text",name:`${f}[${K}]`,value:S[K]||"",onChange:fe=>O(K,fe.target.value),placeholder:ee,required:a&&K===x,disabled:l,maxLength:c,className:q})},K)},[f,S,O,i,a,l,c,d,n,x,U,b]),Y=()=>h.jsx(I,{className:u,children:h.jsx(I,{className:"space-y-4",children:w.map((K,q)=>B(K,q===0))})}),V=()=>h.jsxs(I,{className:u,children:[h.jsxs(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:[w.map(K=>h.jsxs(ae,{type:"button",className:`
${ue?"bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"}
`,children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),H(K),ue&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),!ue&&h.jsx(ae,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",onClick:()=>z(K),disabled:l,children:h.jsx(oe,{name:"xmark",className:"w-3 h-3"})})]}),ue&&q&&G()]}),n==="textarea"?h.jsx(ir,{name:`${f}[${K}]`,value:C[K]||"",onChange:ce=>O(K,ce.target.value),placeholder:J,required:a&&ue,disabled:l,maxLength:c,rows:d,className:`${ee} resize-vertical`}):h.jsx(St,{type:"text",name:`${f}[${K}]`,value:C[K]||"",onChange:ce=>O(K,ce.target.value),placeholder:J,required:a&&ue,disabled:l,maxLength:c,className:ne})]},K)},[f,C,O,i,a,l,c,d,n,H,x,z,G,b]),W=v.useCallback(K=>{const q=b?"input input-error":"input",ne=b?"textarea input-error":"textarea",ee=`${i}${i?" (":""}${H(K)}${i?")":""}`;return h.jsx(I,{children:n==="textarea"?h.jsx(ir,{name:`${f}[${K}]`,value:C[K]||"",onChange:ue=>O(K,ue.target.value),placeholder:ee,required:a&&K===x,disabled:l,maxLength:c,rows:d,className:`${ne} resize-vertical`}):h.jsx(St,{type:"text",name:`${f}[${K}]`,value:C[K]||"",onChange:ue=>O(K,ue.target.value),placeholder:ee,required:a&&K===x,disabled:l,maxLength:c,className:q})},K)},[f,C,O,i,a,l,c,d,n,x,H,b]),Y=()=>h.jsx(I,{className:u,children:h.jsx(I,{className:"space-y-4",children:w.map((K,q)=>U(K,q===0))})}),V=()=>h.jsxs(I,{className:u,children:[h.jsxs(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:[w.map(K=>h.jsxs(ae,{type:"button",className:`
inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold transition-all
${A===K?K===x?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":K===x?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"}
`,onClick:()=>P(K),children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),g?h.jsxs(h.Fragment,{children:[h.jsx(J,{className:"hidden md:inline",children:U(K)}),h.jsx(J,{className:"inline md:hidden",children:K.toUpperCase()})]}):U(K),K===x&&h.jsx(J,{className:"text-red-500 dark:text-red-400",children:"*"}),W(K)&&A!==K&&h.jsx(oe,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"}),K!==x&&h.jsx(ae,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors",onClick:q=>{q.stopPropagation(),z(K)},disabled:l,children:h.jsx(oe,{name:"xmark",className:"w-3 h-3"})})]},K)),G()]}),w.map(K=>h.jsx(I,{className:A===K?"block":"hidden",children:j(K)},K))]}),X=()=>{const K=b?"input input-error":"input",q=b?"textarea input-error":"textarea";return h.jsx(I,{className:u,children:h.jsxs(I,{className:"flex items-center gap-1",children:[h.jsx(I,{className:"flex items-center shrink-0",children:w.map(ne=>h.jsxs(ae,{type:"button",className:`
${D===K?K===x?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":K===x?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"}
`,onClick:()=>P(K),children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),g?h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:H(K)}),h.jsx(Q,{className:"inline md:hidden",children:K.toUpperCase()})]}):H(K),K===x&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),j(K)&&D!==K&&h.jsx(oe,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"}),K!==x&&h.jsx(ae,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors",onClick:q=>{q.stopPropagation(),z(K)},disabled:l,children:h.jsx(oe,{name:"xmark",className:"w-3 h-3"})})]},K)),G()]}),w.map(K=>h.jsx(I,{className:D===K?"block":"hidden",children:W(K)},K))]}),X=()=>{const K=b?"input input-error":"input",q=b?"textarea input-error":"textarea";return h.jsx(I,{className:u,children:h.jsxs(I,{className:"flex items-center gap-1",children:[h.jsx(I,{className:"flex items-center shrink-0",children:w.map(ne=>h.jsxs(ae,{type:"button",className:`
px-2 py-1.5 text-xs font-medium border-y border-l first:rounded-l last:rounded-r last:border-r transition-colors
${A===ne?"bg-blue-500 text-white border-blue-500 dark:bg-blue-600 dark:border-blue-600":"bg-white text-gray-600 border-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"}
`,onClick:()=>P(ne),title:U(ne),children:[ne.toUpperCase(),ne===x&&h.jsx(J,{className:"text-red-400 ml-0.5",children:"*"})]},ne))}),h.jsx(I,{className:"flex-1 min-w-0",children:n==="textarea"?h.jsx(sr,{name:`${f}[${A}]`,value:S[A]||"",onChange:ne=>O(A,ne.target.value),placeholder:i,required:a&&A===x,disabled:l,maxLength:c,rows:d,className:q}):h.jsx(kt,{type:"text",name:`${f}[${A}]`,value:S[A]||"",onChange:ne=>O(A,ne.target.value),placeholder:i,required:a&&A===x,disabled:l,maxLength:c,className:K})})]})})};return h.jsxs(I,{className:l?"opacity-50 cursor-not-allowed":void 0,id:p,...y,children:[r==="compact"?X():r==="tabs"?V():Y(),b&&m&&h.jsx(J,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:m})]})};Za.displayName="MultilingualInput";const Ab=()=>window.G7Core,Qa=({checked:e,value:t,onChange:n,disabled:r=!1,label:s,description:o,size:i="md",className:a="",name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(()=>e??t??!1);v.useEffect(()=>{f(e??t??!1)},[e,t]);const g=_=>{const x=_.target.checked;f(x),n&&!r&&n(_)},p={sm:{track:"w-9 h-5",thumb:"after:h-4 after:w-4"},md:{track:"w-11 h-6",thumb:"after:h-5 after:w-5"},lg:{track:"w-14 h-7",thumb:"after:h-6 after:w-6"}}[i],y=()=>{if(!r){const _=!u;if(f(_),n){const x=Ab();x?.createChangeEvent&&n(x.createChangeEvent({checked:_,name:l}))}}},b=_=>{r||_.key!==" "&&_.key!=="Enter"&&_.key!=="Spacebar"||(_.preventDefault(),y())};return h.jsxs("div",{className:`toggle-container ${a}`,id:c,...d,children:[h.jsxs("div",{className:`toggle-switch-wrapper ${r?"cursor-not-allowed opacity-50":"cursor-pointer"}`,role:"switch","aria-checked":u,"aria-disabled":r||void 0,"aria-label":s??l,tabIndex:r?-1:0,onKeyDown:b,onClick:_=>{_.preventDefault(),_.stopPropagation(),y()},children:[h.jsx("input",{type:"checkbox",id:l,name:l,checked:u,onChange:g,disabled:r,className:"sr-only peer",tabIndex:-1}),h.jsx("div",{className:`toggle-switch-track ${p.track} ${p.thumb}`})]}),s&&h.jsx("span",{className:`toggle-label ${r?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:y,children:s}),o&&h.jsx("p",{className:"toggle-description",children:o})]})},Xo=({status:e,label:t,showIcon:n=!0,iconName:r,className:s="",style:o,id:i,editorAttrs:a})=>{const l={success:"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800",warning:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800",error:"bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800",pending:"bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600",default:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-600"},c={success:ue.CheckCircle,warning:ue.ExclamationTriangle,error:ue.XCircle,info:ue.InfoCircle,pending:ue.Clock,default:ue.QuestionCircle},d={success:"성공",warning:"경고",error:"오류",info:"정보",pending:"대기 중",default:"기본"},u=r||c[e],f=t||d[e],g=l[e];return h.jsxs(J,{className:`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border ${g} ${s}`,style:o,id:i,...a,children:[n&&h.jsx(oe,{name:u,className:"w-4 h-4"}),h.jsx(J,{children:f})]})},Ud={blue:"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",green:"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300",red:"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",gray:"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",yellow:"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",purple:"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",orange:"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",teal:"bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300",cyan:"bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"},Db={success:"green",warning:"yellow",danger:"red",error:"red",info:"blue",primary:"blue",secondary:"gray",text:"gray",default:"gray"},Bd={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"},jd=({color:e,variant:t,text:n,size:r="md",className:s="",style:o,children:i,id:a,editorAttrs:l})=>{const c=e||(t?Db[t]:void 0)||"gray",d=Ud[c]||Ud.gray,u=Bd[r]||Bd.md;return h.jsxs(J,{className:`inline-flex items-center rounded-full font-medium ${u} ${d} ${s}`,style:o,id:a,...l,children:[n,i]})};function Wd(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function Ib(e){if(Array.isArray(e))return e}function Lb(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if Line truncated
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function $b(e,t){return Ib(e)||Lb(e,t)||Fb(e,t)||Pb()}function Fb(e,t){if(e){if(typeof e=="string")return Wd(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Wd(e,t):void 0}}const Gd=Object.entries,Yd=Object.setPrototypeOf,zb=Object.isFrozen,Vb=Object.getPrototypeOf,Hb=Object.getOwnPropertyDescriptor;let Yt=Object.freeze,fn=Object.seal,Kr=Object.create,Xd=typeof Reflect<"u"&&Reflect,Ja=Xd.apply,Ra=Xd.construct;Yt||(Yt=function(t){return t}),fn||(fn=function(t){return t}),Ja||(Ja=function(t,n){for(var r=arguments.length,s=new Array(r>2?r-2:0),o=2;o<r;o++)s[o-2]=arguments[o];return t.apply(n,s)}),Ra||(Ra=function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return new t(...r)});const wr=Ct(Array.prototype.forEach),Ub=Ct(Array.prototype.lastIndexOf),Kd=Ct(Array.prototype.pop),qr=Ct(Array.prototype.push),Bb=Ct(Array.prototype.splice),Xt=Array.isArray,Ps=Ct(String.prototype.toLowerCase),el=Ct(String.prototype.toString),qd=Ct(String.prototype.match),Zr=Ct(String.prototype.replace),Zd=Ct(String.prototype.indexOf),jb=Ct(String.prototype.trim),Wb=Ct(Number.prototype.toString),Gb=Ct(Boolean.prototype.toString),Qd=typeof BigInt>"u"?null:Ct(BigInt.prototype.toString),Jd=typeof Symbol>"u"?null:Ct(Symbol.prototype.toString),vt=Ct(Object.prototype.hasOwnProperty),$s=Ct(Object.prototype.toString),$t=Ct(RegExp.prototype.test),Qr=Yb(TypeError);function Ct(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return Ja(e,t,r)}}function Yb(e){return function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return Ra(e,n)}}function Ue(e,t){let n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:Ps;if(Yd&&Yd(e,null),!Xt(t))return e;let r=t.length;for(;r--;){let s=t[r];if(typeof s=="string"){const o=n(s);o!==s&&(zb(t)||(t[r]=o),s=o)}e[s]=!0}return e}function Xb(e){for(let t=0;t<e.length;t++)vt(e,t)||(e[t]=null);return e}function Vt(e){const t=Kr(null);for(const r of Gd(e)){var n=$b(r,2);const s=n[0],o=n[1];vt(e,s)&&(Xt(o)?t[s]=Xb(o):o&&typeof o=="object"&&o.constructor===Object?t[s]=Vt(o):t[s]=o)}return t}function Kb(e){switch(typeof e){case"string":return e;case"number":return Wb(e);case"boolean":return Gb(e);case"bigint":return Qd?Qd(e):"0";case"symbol":return Jd?Jd(e):"Symbol()";case"undefined":return $s(e);case"function":case"object":{if(e===null)return $s(e);const t=e,n=Pn(t,"toString");if(typeof n=="function"){const r=n(t);return typeof r=="string"?r:$s(r)}return $s(e)}default:return $s(e)}}function Pn(e,t){for(;e!==null;){const r=Hb(e,t);if(r){if(r.get)return Ct(r.get);if(typeof r.value=="function")return Ct(r.value)}e=Vb(e)}function n(){return null}return n}function qb(e){try{return $t(e,""),!0}catch{return!1}}const Rd=Yt(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),tl=Yt(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),nl=Yt(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),Zb=Yt(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","s Line truncated
`+Tt),pe&&wr([T,z,U],Cn=>{Tt=Zr(Tt,Cn," ")}),k&&Be?w(Tt):Tt},t.setConfig=function(){let se=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Je(se),ie=!0},t.clearConfig=function(){dt=null,ie=!1},t.isValidAttribute=function(se,H,Z){dt||Je({});const he=qe(se),tt=qe(H);return Wo(he,tt,Z)},t.addHook=function(se,H){typeof H=="function"&&qr(O[se],H)},t.removeHook=function(se,H){if(H!==void 0){const Z=Ub(O[se],H);return Z===-1?void 0:Bb(O[se],Z,1)[0]}return Kd(O[se])},t.removeHooks=function(se){O[se]=[]},t.removeAllHooks=function(){O=su()},t}var cy=ou();const ol=({content:e,text:t,isHtml:n=!0,className:r="",purifyConfig:s,id:o,editorAttrs:i})=>{const a=t??e??"";if(!a||a.trim()==="")return null;if(!n){const u=`
${D===ne?"bg-blue-500 text-white border-blue-500 dark:bg-blue-600 dark:border-blue-600":"bg-white text-gray-600 border-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"}
`,onClick:()=>P(ne),title:H(ne),children:[ne.toUpperCase(),ne===x&&h.jsx(Q,{className:"text-red-400 ml-0.5",children:"*"})]},ne))}),h.jsx(I,{className:"flex-1 min-w-0",children:n==="textarea"?h.jsx(ir,{name:`${f}[${D}]`,value:C[D]||"",onChange:ne=>O(D,ne.target.value),placeholder:i,required:a&&D===x,disabled:l,maxLength:c,rows:d,className:q}):h.jsx(St,{type:"text",name:`${f}[${D}]`,value:C[D]||"",onChange:ne=>O(D,ne.target.value),placeholder:i,required:a&&D===x,disabled:l,maxLength:c,className:K})})]})})};return h.jsxs(I,{className:l?"opacity-50 cursor-not-allowed":void 0,id:p,...y,children:[r==="compact"?X():r==="tabs"?V():Y(),b&&m&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:m})]})};Za.displayName="MultilingualInput";const Ab=()=>window.G7Core,Qa=({checked:e,value:t,onChange:n,disabled:r=!1,label:s,description:o,size:i="md",className:a="",name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(()=>e??t??!1);v.useEffect(()=>{f(e??t??!1)},[e,t]);const g=_=>{const x=_.target.checked;f(x),n&&!r&&n(_)},p={sm:{track:"w-9 h-5",thumb:"after:h-4 after:w-4"},md:{track:"w-11 h-6",thumb:"after:h-5 after:w-5"},lg:{track:"w-14 h-7",thumb:"after:h-6 after:w-6"}}[i],y=()=>{if(!r){const _=!u;if(f(_),n){const x=Ab();x?.createChangeEvent&&n(x.createChangeEvent({checked:_,name:l}))}}},b=_=>{r||_.key!==" "&&_.key!=="Enter"&&_.key!=="Spacebar"||(_.preventDefault(),y())};return h.jsxs("div",{className:`toggle-container ${a}`,id:c,...d,children:[h.jsxs("div",{className:`toggle-switch-wrapper ${r?"cursor-not-allowed opacity-50":"cursor-pointer"}`,role:"switch","aria-checked":u,"aria-disabled":r||void 0,"aria-label":s??l,tabIndex:r?-1:0,onKeyDown:b,onClick:_=>{_.preventDefault(),_.stopPropagation(),y()},children:[h.jsx("input",{type:"checkbox",id:l,name:l,checked:u,onChange:g,disabled:r,className:"sr-only peer",tabIndex:-1}),h.jsx("div",{className:`toggle-switch-track ${p.track} ${p.thumb}`})]}),s&&h.jsx("span",{className:`toggle-label ${r?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:y,children:s}),o&&h.jsx("p",{className:"toggle-description",children:o})]})},Ko=({status:e,label:t,showIcon:n=!0,iconName:r,className:s="",style:o,id:i,editorAttrs:a})=>{const l={success:"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800",warning:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800",error:"bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800",pending:"bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600",default:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-600"},c={success:de.CheckCircle,warning:de.ExclamationTriangle,error:de.XCircle,info:de.InfoCircle,pending:de.Clock,default:de.QuestionCircle},d={success:"성공",warning:"경고",error:"오류",info:"정보",pending:"대기 중",default:"기본"},u=r||c[e],f=t||d[e],g=l[e];return h.jsxs(Q,{className:`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border ${g} ${s}`,style:o,id:i,...a,children:[n&&h.jsx(oe,{name:u,className:"w-4 h-4"}),h.jsx(Q,{children:f})]})},Bd={blue:"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",green:"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300",red:"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",gray:"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",yellow:"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",purple:"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",orange:"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",teal:"bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300",cyan:"bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"},Db={success:"green",warning:"yellow",danger:"red",error:"red",info:"blue",primary:"blue",secondary:"gray",text:"gray",default:"gray"},jd={sm:"px-1.5 py-0.5 text-[10px]",md:"px-2 py-0.5 text-xs",lg:"px-2.5 py-1 text-sm"},Wd=({color:e,variant:t,text:n,size:r="md",className:s="",style:o,children:i,id:a,editorAttrs:l})=>{const c=e||(t?Db[t]:void 0)||"gray",d=Bd[c]||Bd.gray,u=jd[r]||jd.md;return h.jsxs(Q,{className:`inline-flex items-center rounded-full font-medium ${u} ${d} ${s}`,style:o,id:a,...l,children:[n,i]})};function Gd(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function Ib(e){if(Array.isArray(e))return e}function Lb(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if Line truncated
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function $b(e,t){return Ib(e)||Lb(e,t)||Fb(e,t)||Pb()}function Fb(e,t){if(e){if(typeof e=="string")return Gd(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Gd(e,t):void 0}}const Yd=Object.entries,Xd=Object.setPrototypeOf,zb=Object.isFrozen,Vb=Object.getPrototypeOf,Hb=Object.getOwnPropertyDescriptor;let Zt=Object.freeze,hn=Object.seal,Zr=Object.create,Kd=typeof Reflect<"u"&&Reflect,Ja=Kd.apply,Ra=Kd.construct;Zt||(Zt=function(t){return t}),hn||(hn=function(t){return t}),Ja||(Ja=function(t,n){for(var r=arguments.length,s=new Array(r>2?r-2:0),o=2;o<r;o++)s[o-2]=arguments[o];return t.apply(n,s)}),Ra||(Ra=function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return new t(...r)});const kr=_t(Array.prototype.forEach),Ub=_t(Array.prototype.lastIndexOf),qd=_t(Array.prototype.pop),Qr=_t(Array.prototype.push),Bb=_t(Array.prototype.splice),Qt=Array.isArray,Ps=_t(String.prototype.toLowerCase),el=_t(String.prototype.toString),Zd=_t(String.prototype.match),Jr=_t(String.prototype.replace),Qd=_t(String.prototype.indexOf),jb=_t(String.prototype.trim),Wb=_t(Number.prototype.toString),Gb=_t(Boolean.prototype.toString),Jd=typeof BigInt>"u"?null:_t(BigInt.prototype.toString),Rd=typeof Symbol>"u"?null:_t(Symbol.prototype.toString),xt=_t(Object.prototype.hasOwnProperty),$s=_t(Object.prototype.toString),zt=_t(RegExp.prototype.test),Rr=Yb(TypeError);function _t(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return Ja(e,t,r)}}function Yb(e){return function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return Ra(e,n)}}function Ve(e,t){let n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:Ps;if(Xd&&Xd(e,null),!Qt(t))return e;let r=t.length;for(;r--;){let s=t[r];if(typeof s=="string"){const o=n(s);o!==s&&(zb(t)||(t[r]=o),s=o)}e[s]=!0}return e}function Xb(e){for(let t=0;t<e.length;t++)xt(e,t)||(e[t]=null);return e}function Bt(e){const t=Zr(null);for(const r of Yd(e)){var n=$b(r,2);const s=n[0],o=n[1];xt(e,s)&&(Qt(o)?t[s]=Xb(o):o&&typeof o=="object"&&o.constructor===Object?t[s]=Bt(o):t[s]=o)}return t}function Kb(e){switch(typeof e){case"string":return e;case"number":return Wb(e);case"boolean":return Gb(e);case"bigint":return Jd?Jd(e):"0";case"symbol":return Rd?Rd(e):"Symbol()";case"undefined":return $s(e);case"function":case"object":{if(e===null)return $s(e);const t=e,n=Fn(t,"toString");if(typeof n=="function"){const r=n(t);return typeof r=="string"?r:$s(r)}return $s(e)}default:return $s(e)}}function Fn(e,t){for(;e!==null;){const r=Hb(e,t);if(r){if(r.get)return _t(r.get);if(typeof r.value=="function")return _t(r.value)}e=Vb(e)}function n(){return null}return n}function qb(e){try{return zt(e,""),!0}catch{return!1}}const eu=Zt(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),tl=Zt(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),nl=Zt(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),Zb=Zt(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","s Line truncated
`+At),pe&&kr([T,z,H],Sn=>{At=Jr(At,Sn," ")}),k&&Xe?w(At):At},t.setConfig=function(){let se=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};kn(se),ie=!0},t.clearConfig=function(){Ct=null,ie=!1},t.isValidAttribute=function(se,B,te){Ct||kn({});const he=Ue(se),ot=Ue(B);return Ua(he,ot,te)},t.addHook=function(se,B){typeof B=="function"&&Qr(O[se],B)},t.removeHook=function(se,B){if(B!==void 0){const te=Ub(O[se],B);return te===-1?void 0:Bb(O[se],te,1)[0]}return qd(O[se])},t.removeHooks=function(se){O[se]=[]},t.removeAllHooks=function(){O=ou()},t}var cy=iu();const ol=({content:e,text:t,isHtml:n=!0,className:r="",purifyConfig:s,id:o,editorAttrs:i})=>{const a=t??e??"";if(!a||a.trim()==="")return null;if(!n){const u=`
whitespace-pre-wrap
text-gray-900 dark:text-gray-100
font-sans
@@ -36,64 +36,64 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
[&_tr:hover]:bg-gray-50 dark:[&_tr:hover]:bg-gray-700/50
after:content-[''] after:block after:clear-both
${r}
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:d,dangerouslySetInnerHTML:{__html:c},id:o,...i})},qo=window.G7Core?.createLogger?.("Comp:DataGrid")??{log:(...e)=>console.log("[Comp:DataGrid]",...e),warn:(...e)=>console.warn("[Comp:DataGrid]",...e),error:(...e)=>console.error("[Comp:DataGrid]",...e)},Fs=(e,t)=>window.G7Core?.t?.(e,t)??e,dy={Div:I,Span:J,Button:ae,Icon:oe,Checkbox:_n,P:yt,A:Wr,Label:ln,Input:kt,H1:Ba,H2:or,H3:jr,H4:$d,Img:Sn,Select:Gr,Textarea:sr,Table:Ya,Thead:Xa,Tbody:Ka,Tfoot:qa,Tr:ir,Th:Yr,Td:Jt,MultilingualInput:Za,Toggle:Qa,StatusBadge:Xo,Badge:jd,HtmlContent:ol,ActionMenu:Ls},il=()=>{const e=window.G7Core?.getComponentMap?.();return e&&Object.keys(e).length>0?e:dy},al=e=>{if(e==null)return"";if(typeof e=="string"||typeof e=="number"||typeof e=="boolean")return String(e);if(Array.isArray(e))return e.map(t=>al(t)).join(", ");if(typeof e=="object"){if(v.isValidElement(e))return e;if(Object.keys(e).length===0)return"";try{return JSON.stringify(e)}catch{return qo.warn("객체를 문자열로 변환할 수 없습니다:",e),"[Object]"}}return String(e)},iu=(e,t)=>{if(!e)return!0;const n=window.G7Core;return typeof n?.evaluateCondition=="function"?n.evaluateCondition(e,{row:t,item:t,$item:t}):(qo.warn("G7Core.evaluateCondition 미노출 — subRow 조건을 평가할 수 없습니다:",e),!0)},Jr=(e,t,n,r="",s)=>{const o=window.G7Core;if(o?.renderItemChildren){const i={row:t,value:n,$value:n};return o.renderItemChildren(e,i,il(),r,s?{componentContext:s}:void 0)}return qo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},au=(e,t,n)=>e?!e.includes("{{")&&!e.includes("$t:")?e:Jr([{type:"basic",name:"Span",text:e}],{},null,t,n):"",ll=(e,t,n="",r)=>{const s=window.G7Core;if(s?.renderItemChildren){const o={row:t,$value:t};return s.renderItemChildren(e,o,il(),n,r?{componentContext:r}:void 0)}return qo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},uy=({id:e,columns:t,data:n,sortable:r=!0,pagination:s=!1,pageSize:o=10,onRowClick:i,className:a="",style:l,sortField:c,sortDirection:d,onSortChange:u,selectable:f=!1,selectedIds:g=[],onSelectionChange:m,idField:p="id",showColumnSelector:y=!1,visibleColumns:b,onColumnVisibilityChange:_,responsiveBreakpoint:x=768,cardRenderer:M,rowActions:S,onRowAction:k,alwaysShowPagination:N=!1,serverSidePagination:C=!1,serverCurrentPage:w,serverTotalPages:E,serverHasMorePages:A,onPageChange:P,prevText:L,nextText:$,columnSelectorText:D,emptyMessage:O,actionsColumnHeader:T,requiredText:z,selectedCountText:U,showFirstLast:W=!0,expandable:G=!1,expandedRowIds:B,onExpandChange:j,expandChildren:Y,expandedRowRender:V,expandColumnWidth:X="48px",expandIconExpanded:K=ue.ChevronDown,expandIconCollapsed:q=ue.ChevronRight,expandConditionField:ne,dynamicColumns:ee,dynamicColumnsInsertAfter:fe,expandContext:te,__componentContext:le,subRowChildren:ge,subRowCondition:ce,subRowClassName:ye="px-6 py-2 bg-gray-50 dark:bg-gray-900",subRowRender:me,footerCells:pe,footerClassName:ke="bg-gray-50 dark:bg-gray-800/50 font-medium",footerCardChildren:$e,editorAttrs:ie})=>{const _e=L||void 0,Ae=$||void 0,Ie=D??Fs("common.column_selector"),Be=O??Fs("common.no_data"),gt=T??Fs("common.actions"),mt=z??Fs("common.required"),Ge=U??Fs("common.selected_count"),ot=window.G7Core,Fe=ot?.useControllableState,re=ot?.shallowArrayEqual,[de,xe]=Fe?Fe(c,null):[c??null,()=>{}],[ve,we]=Fe?Fe(d,"asc"):[d??"asc",()=>{}],[Ve,De]=v.useState(1),[Ne,Te]=Fe?Fe(B,[],j,{isEqual:re}):[B??[],()=>{}],[He,Ke]=v.useState(!1),Le=v.useRef(null),Ee=ot?.useResponsive,ze=Ee?.(),rt=ze?ze.width<x:typeof window<"u"&&window.innerWidth<x,it=v.useMemo(()=>{if(!ee||!Array.isArray(ee)||ee.length===0)return t;if(fe){const Q=t.findIndex(be=>be.field===fe);if(Q!==-1)return[...t.slice(0,Q+1),...ee,...t.slice(Q+1)]}return[...t,...ee]},[t,ee,fe]),[Ye,at]=v.useState(()=>b&&b.length>0?b:t.filter(Q=>!Q.hidden).map(Q=>Q.field));v.useEffect(()=>{b&&b.length>0&&at(b)},[b]);const xn=v.useRef("");v.useEffect(()=>{const Q=t.map(be=>be.field).join(",");xn.current&&Q!==xn.current&&(!b||b.length===0)&&at(t.filter(be=>!be.hidden).map(be=>be.field)),xn.current=Q},[t,b]);const bt=v.useRef([]),qe=v.useRef(null);v.useEffect(()=>{if(ee&&ee.length>0){const Q=ee.map(H=>H.field);if(qe.current===null&&(qe.current=b!==void 0&&b.length>0,qe.current)){bt.current=Q;return}const be=bt.current,se=Q.filter(H=>!be.includes(H));if(se.length>0){const H=se.filter(Z=>!Ye.includes(Z));if(H.length>0){const Z=[...Ye,...H];at(Z),_?.(Z)}}bt.current=Q}},[ee]),v.useEffect(()=>{C||De(1)},[n,C]),v.useEffect(()=>{const Q=be=>{Le.current&&!Le.current.contains(be.target)&&Ke(!1)};return He&&document.addEventListener("mousedown",Q),()=>document.removeEventListener("mousedown",Q)},[He]);const dt=v.useMemo(()=>!y&&(!b||b.length===0)?it.filter(Q=>!Q.hidden):it.filter(Q=>Ye.includes(Q.field)),[it,Ye,y,b]),Zt=r&&!(C&&!u),on=v.useMemo(()=>!de||!r||!n?n||[]:u||C?n:[...n].sort((Q,be)=>{const se=Q[de],H=be[de];if(se===H)return 0;const Z=se>H?1:-1;return Line truncated
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:d,dangerouslySetInnerHTML:{__html:c},id:o,...i})},Zo=window.G7Core?.createLogger?.("Comp:DataGrid")??{log:(...e)=>console.log("[Comp:DataGrid]",...e),warn:(...e)=>console.warn("[Comp:DataGrid]",...e),error:(...e)=>console.error("[Comp:DataGrid]",...e)},Fs=(e,t)=>window.G7Core?.t?.(e,t)??e,il={Div:I,Span:Q,Button:ae,Icon:oe,Checkbox:En,P:vt,A:Yr,Label:ln,Input:St,H1:Ba,H2:ar,H3:Gr,H4:Fd,Img:_n,Select:Xr,Textarea:ir,Table:Ya,Thead:Xa,Tbody:Ka,Tfoot:qa,Tr:lr,Th:Kr,Td:en,MultilingualInput:Za,Toggle:Qa,StatusBadge:Ko,Badge:Wd,HtmlContent:ol,ActionMenu:Ls},al=e=>{if(e==null)return"";if(typeof e=="string"||typeof e=="number"||typeof e=="boolean")return String(e);if(Array.isArray(e))return e.map(t=>al(t)).join(", ");if(typeof e=="object"){if(v.isValidElement(e))return e;if(Object.keys(e).length===0)return"";try{return JSON.stringify(e)}catch{return Zo.warn("객체를 문자열로 변환할 수 없습니다:",e),"[Object]"}}return String(e)},au=(e,t)=>{if(!e)return!0;const n=e.match(/^\{\{(.+)\}\}$/);if(!n)return!0;const r=n[1].trim();try{return!!new Function("row",`return ${r}`)(t)}catch(s){return Zo.warn("subRow 조건 평가 실패:",e,s),!0}},zs=(e,t,n,r="",s)=>{const o=window.G7Core;if(o?.renderItemChildren){const i={row:t,value:n,$value:n};return o.renderItemChildren(e,i,il,r,s?{componentContext:s}:void 0)}return Zo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},ll=(e,t,n="",r)=>{const s=window.G7Core;if(s?.renderItemChildren){const o={row:t,$value:t};return s.renderItemChildren(e,o,il,n,r?{componentContext:r}:void 0)}return Zo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},dy=({id:e,columns:t,data:n,sortable:r=!0,pagination:s=!1,pageSize:o=10,onRowClick:i,className:a="",style:l,sortField:c,sortDirection:d,onSortChange:u,selectable:f=!1,selectedIds:g=[],onSelectionChange:m,idField:p="id",showColumnSelector:y=!1,visibleColumns:b,onColumnVisibilityChange:_,responsiveBreakpoint:x=768,cardRenderer:M,rowActions:C,onRowAction:k,alwaysShowPagination:N=!1,serverSidePagination:S=!1,serverCurrentPage:w,serverTotalPages:E,onPageChange:D,prevText:P,nextText:L,columnSelectorText:$,emptyMessage:A,actionsColumnHeader:O,requiredText:T,selectedCountText:z,showFirstLast:H=!0,expandable:j=!1,expandedRowIds:G,onExpandChange:U,expandChildren:W,expandedRowRender:Y,expandColumnWidth:V="48px",expandIconExpanded:X=de.ChevronDown,expandIconCollapsed:K=de.ChevronRight,expandConditionField:q,dynamicColumns:ne,dynamicColumnsInsertAfter:ee,expandContext:ue,__componentContext:J,subRowChildren:ce,subRowCondition:me,subRowClassName:le="px-6 py-2 bg-gray-50 dark:bg-gray-900",subRowRender:ve,footerCells:ge,footerClassName:pe="bg-gray-50 dark:bg-gray-800/50 font-medium",footerCardChildren:ke,editorAttrs:$e})=>{const ie=P||void 0,Me=L||void 0,Ae=$??Fs("common.column_selector"),Ie=A??Fs("common.no_data"),Xe=O??Fs("common.actions"),gt=T??Fs("common.required"),pt=z??Fs("common.selected_count"),Be=window.G7Core,et=Be?.useControllableState,je=Be?.shallowArrayEqual,[re,fe]=et?et(c,null):[c??null,()=>{}],[be,we]=et?et(d,"asc"):[d??"asc",()=>{}],[xe,He]=v.useState(1),[_e,Le]=et?et(G,[],U,{isEqual:je}):[G??[],()=>{}],[Ce,We]=v.useState(!1),Ze=v.useRef(null),Fe=Be?.useResponsive,Ne=Fe?.(),ze=Ne?Ne.width<x:typeof window<"u"&&window.innerWidth<x,Ke=v.useMemo(()=>{if(!ne||!Array.isArray(ne)||ne.length===0)return t;if(ee){const Z=t.findIndex(ye=>ye.field===ee);if(Z!==-1)return[...t.slice(0,Z+1),...ne,...t.slice(Z+1)]}return[...t,...ne]},[t,ne,ee]),[tt,at]=v.useState(()=>b&&b.length>0?b:t.filter(Z=>!Z.hidden).map(Z=>Z.field));v.useEffect(()=>{b&&b.length>0&&at(b)},[b]);const ct=v.useRef("");v.useEffect(()=>{const Z=t.map(ye=>ye.field).join(",");ct.current&&Z!==ct.current&&(!b||b.length===0)&&at(t.filter(ye=>!ye.hidden).map(ye=>ye.field)),ct.current=Z},[t,b]);const wn=v.useRef([]),yt=v.useRef(null);v.useEffect(()=>{if(ne&&ne.length>0){const Z=ne.map(Qe=>Qe.field);if(yt.current===null&&(yt.current=b!==void 0&&b.length>0,yt.current)){wn.current=Z;return}const ye=wn.current,De=Z.filter(Qe=>!ye.includes(Qe));if(De.length>0){const Qe=De.filter(se=>!tt.includes(se));if(Qe.length>0){const se=[...tt,...Qe];at(se),_?.(se)}}wn.current=Z}},[ne]),v.useEffect(()=>{S||He(1)},[n,S]),v.useEffect(()=>{const Z=ye=>{Ze.current&&!Ze.current.contains(ye.target)&&We(!1)};return Ce&&document.addEventListener("mousedown",Z),()=>document.removeEventListener("mousedown",Z)},[Ce]);const Ue=v.useMemo(()=>!y&&(!b||b.length===0)?Ke.filter(Z=>!Z.hidden):Ke.filter(Z=>tt.includes(Z.field)),[Ke,tt,y,b]),Ct=r&&!(S&&!u),Ut=v.useMemo(()=>!re||!r||!n?n||[]:u||S?n:[...n].sort((Z,ye)=>{const De=Z[re],Qe=ye[re];if(De===Qe)return 0;const se=De>Qe?1:-1;return be==="asc"?se:-se}),[n,re,be,r,u,S]),st=v.useMemo(()=>{if(S||!s)return Ut;const Z=(xe-1)*o,ye=Z+o;return Ut.slice(Z,ye)},[Ut,xe,o,s,S]),kn=S?w||1:xe,qt=S?E||1:Math.ceil((Ut?.length||0)/o),Ln=v.useCallback(Z=>{S&&D?D(Z):He(Z)},[S,D]),jr=Z=>{if(!C 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(oe,{name:o,className:`w-5 h-5 flex-shrink-0 ${s.text}`}),h.jsx(J,{className:`flex-1 text-sm font-medium ${s.text}`,children:e.message}),h.jsx(ae,{onClick:r,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${s.text} focus:outline-none`,"aria-label":Py("common.close_notification"),children:h.jsx(oe,{name:ue.Times,className:"w-4 h-4"})})]})},bu=({items:e,separator:t=h.jsx(oe,{name:ue.ChevronRight,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),showHome:n=!1,homeHref:r="/",maxItems:s,className:o="",id:i,editorAttrs:a})=>{let c=n?[{label:"Home",href:r},...e]:e,d=c,u=!1;if(s&&c.length>s){u=!0;const f=c.slice(0,1),g=c.slice(-(s-2));d=[...f,...g]}return h.jsx(Go,{className:`flex items-center space-x-2 text-sm ${o}`,"aria-label":"Breadcrumb",id:i,...a,children:h.jsx(I,{className:"flex items-center space-x-2",children:d.map((f,g)=>{const m=g===d.length-1,p=g===0;return h.jsxs(v.Fragment,{children:[u&&p&&h.jsxs(h.Fragment,{children:[h.jsx(yu,{item:f,isLast:!1}),h.jsx(J,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t}),h.jsx(J,{className:"text-gray-500 dark:text-gray-400",children:"..."}),d.length>1&&h.jsx(J,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t})]}),(!u||!p)&&h.jsxs(h.Fragment,{children:[h.jsx(yu,{item:f,isLast:m}),!m&&h.jsx(J,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t})]})]},g)})})})},yu=({item:e,isLast:t})=>t||!e.href&&!e.onClick?h.jsx(J,{className:"font-medium text-gray-900 dark:text-white","aria-current":t?"page":void 0,children:e.label}):e.onClick?h.jsx(Wr,{href:"#",onClick:n=>{n.preventDefault(),e.onClick?.()},className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:e.label}):h.jsx(Wr,{href:e.href,className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:e.label}),Hy=({title:e,description:t,iconName:n,illustrationSrc:r,illustrationAlt:s="Empty state illustration",actions:o=[],className:i="",id:a,editorAttrs:l})=>{const c={primary:"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 focus:ring-gray-500",danger:"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-gray-500"};return h.jsxs(I,{className:`flex flex-col items-center justify-center p-8 text-center ${i}`,role:"status","aria-live":"polite",id:a,...l,children:[r?h.jsx(Sn,{src:r,alt:s,className:"w-48 h-48 mb-6 object-contain"}):n?h.jsx(I,{className:"mb-6 p-4 rounded-full bg-gray-100 dark:bg-gray-700",children:h.jsx(oe,{name:n,className:"w-12 h-12 text-gray-400 dark:text-gray-500"})}):null,h.jsx(or,{className:"text-xl font-semibold text-gray-900 dark:text-white mb-2",children:e}),t&&h.jsx(yt,{className:"text-gray-600 dark:text-gray-400 mb-6 max-w-md",children:t}),o.length>0&&h.jsx(I,{className:"flex flex-wrap gap-3 justify-center",children:o.map((d,u)=>h.jsxs(ae,{onClick:d.onClick,className:`
`,role:"alert",children:[h.jsx(oe,{name:o,className:`w-5 h-5 flex-shrink-0 ${s.text}`}),h.jsx(Q,{className:`flex-1 text-sm font-medium ${s.text}`,children:e.message}),h.jsx(ae,{onClick:r,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${s.text} focus:outline-none`,"aria-label":Iy("common.close_notification"),children:h.jsx(oe,{name:de.Times,className:"w-4 h-4"})})]})},bu=({items:e,separator:t=h.jsx(oe,{name:de.ChevronRight,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),showHome:n=!1,homeHref:r="/",maxItems:s,className:o="",id:i,editorAttrs:a})=>{let c=n?[{label:"Home",href:r},...e]:e,d=c,u=!1;if(s&&c.length>s){u=!0;const f=c.slice(0,1),g=c.slice(-(s-2));d=[...f,...g]}return h.jsx(Yo,{className:`flex items-center space-x-2 text-sm ${o}`,"aria-label":"Breadcrumb",id:i,...a,children:h.jsx(I,{className:"flex items-center space-x-2",children:d.map((f,g)=>{const m=g===d.length-1,p=g===0;return h.jsxs(v.Fragment,{children:[u&&p&&h.jsxs(h.Fragment,{children:[h.jsx(yu,{item:f,isLast:!1}),h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t}),h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:"..."}),d.length>1&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t})]}),(!u||!p)&&h.jsxs(h.Fragment,{children:[h.jsx(yu,{item:f,isLast:m}),!m&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t})]})]},g)})})})},yu=({item:e,isLast:t})=>t||!e.href&&!e.onClick?h.jsx(Q,{className:"font-medium text-gray-900 dark:text-white","aria-current":t?"page":void 0,children:e.label}):e.onClick?h.jsx(Yr,{href:"#",onClick:n=>{n.preventDefault(),e.onClick?.()},className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:e.label}):h.jsx(Yr,{href:e.href,className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:e.label}),zy=({title:e,description:t,iconName:n,illustrationSrc:r,illustrationAlt:s="Empty state illustration",actions:o=[],className:i="",id:a,editorAttrs:l})=>{const c={primary:"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 focus:ring-gray-500",danger:"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-gray-500"};return h.jsxs(I,{className:`flex flex-col items-center justify-center p-8 text-center ${i}`,role:"status","aria-live":"polite",id:a,...l,children:[r?h.jsx(_n,{src:r,alt:s,className:"w-48 h-48 mb-6 object-contain"}):n?h.jsx(I,{className:"mb-6 p-4 rounded-full bg-gray-100 dark:bg-gray-700",children:h.jsx(oe,{name:n,className:"w-12 h-12 text-gray-400 dark:text-gray-500"})}):null,h.jsx(ar,{className:"text-xl font-semibold text-gray-900 dark:text-white mb-2",children:e}),t&&h.jsx(vt,{className:"text-gray-600 dark:text-gray-400 mb-6 max-w-md",children:t}),o.length>0&&h.jsx(I,{className:"flex flex-wrap gap-3 justify-center",children:o.map((d,u)=>h.jsxs(ae,{onClick:d.onClick,className:`
px-4 py-2 rounded-md font-medium transition-colors
focus:outline-none focus:ring-2 focus:ring-offset-2
${c[d.variant||"primary"]}
`,children:[d.iconName&&h.jsx(oe,{name:d.iconName,className:"w-4 h-4 mr-2 inline-block"}),d.label]},u))})]})},Uy=(e,t)=>window.G7Core?.t?.(e,t)??e,By=({id:e,size:t="md",color:n="text-blue-600",fullscreen:r=!1,text:s,className:o="",editorAttrs:i})=>{const a={sm:"w-4 h-4 border-2",md:"w-8 h-8 border-2",lg:"w-12 h-12 border-3",xl:"w-16 h-16 border-4"},l={sm:"text-xs",md:"text-sm",lg:"text-base",xl:"text-lg"},c=h.jsxs(I,{className:`flex flex-col items-center justify-center gap-3 ${o}`,children:[h.jsx(I,{className:`
`,children:[d.iconName&&h.jsx(oe,{name:d.iconName,className:"w-4 h-4 mr-2 inline-block"}),d.label]},u))})]})},Vy=(e,t)=>window.G7Core?.t?.(e,t)??e,Hy=({id:e,size:t="md",color:n="text-blue-600",fullscreen:r=!1,text:s,className:o="",editorAttrs:i})=>{const a={sm:"w-4 h-4 border-2",md:"w-8 h-8 border-2",lg:"w-12 h-12 border-3",xl:"w-16 h-16 border-4"},l={sm:"text-xs",md:"text-sm",lg:"text-base",xl:"text-lg"},c=h.jsxs(I,{className:`flex flex-col items-center justify-center gap-3 ${o}`,children:[h.jsx(I,{className:`
${a[t]}
border-gray-200 dark:border-gray-700 border-t-current
rounded-full animate-spin
${n}
`,role:"status","aria-label":Uy("common.loading")}),s&&h.jsx(J,{className:`${l[t]} ${n} font-medium`,children:s})]});return r?h.jsx(I,{id:e,className:"fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90","aria-live":"polite","aria-busy":"true",...i,children:c}):h.jsx(I,{id:e,className:"flex items-center justify-center p-4","aria-live":"polite","aria-busy":"true",...i,children:c})},Rr=()=>window.G7Core,hl=(e,t)=>window.G7Core?.t?.(e,t)??e,jy=({logo:e,logoAlt:t="Logo",menu:n=[],collapsed:r=!1,onToggleCollapse:s,className:o="",currentLocale:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(new Set),[u,f]=v.useState(i||Rr()?.locale?.current?.()||"ko"),[g,m]=v.useState(window.location.pathname),p=v.useMemo(()=>{const w=[],E=A=>{for(const P of A)P.url&&w.push(P.url),P.children&&E(P.children)};return E(n),w},[n]),y=v.useCallback(w=>{let E=null;for(const A of p)(w===A||w.startsWith(A+"/"))&&(!E||A.length>E.length)&&(E=A);return E},[p]);v.useEffect(()=>{const w=Rr();return w?.state?.subscribe?w.state.subscribe(A=>{const P=A?._global?.locale||w?.locale?.current?.()||"ko";f(P);const L=A?._global?.currentPath||window.location.pathname;m(L)}):void 0},[]),v.useEffect(()=>{const w=()=>{m(window.location.pathname)};return window.addEventListener("popstate",w),()=>window.removeEventListener("popstate",w)},[]),v.useEffect(()=>{i&&f(i)},[i]);const b=(w,E)=>!w||typeof E!="string"?!1:E===w?!0:E.startsWith(w+"/")?y(E)===w:!1,_=(w,E)=>{const A=new Set,P=L=>{for(const $ of L)if($.children&&$.children.length>0){if(P($.children)||b($.url,E))return A.add($.id),!0}else if(b($.url,E))return!0;return!1};return P(w),A};v.useEffect(()=>{if(n.length>0&&g){const w=_(n,g);w.size>0&&d(E=>new Set([...E,...w]))}},[g,n]);const x=w=>b(w,g),M=w=>w.children?w.children.some(E=>x(E.url)||M(E)):!1,S=w=>{d(E=>{const A=new Set(E);return A.has(w)?A.delete(w):A.add(w),A})},k=w=>typeof w=="string"?w:w[u]||w.en||Object.values(w)[0]||"",N=(w,E)=>{Rr()?.dispatch?(w.preventDefault(),Rr().navigation?.onComplete(()=>{Rr().dispatch({handler:"setState",params:{target:"global",sidebarOpen:!1}})}),Rr().dispatch({handler:"navigate",params:{path:E}})):window.location.href=E},C=(w,E=0,A=!1)=>{const P=w.children&&w.children.length>0,L=c.has(w.id),$=k(w.name),D=E>0,O=x(w.url),T=M(w),z=D?"admin-sidebar-subitem":"admin-sidebar-item",U="admin-sidebar-item-active",W="admin-sidebar-item-active-parent",G="admin-sidebar-item-default";return h.jsxs(I,{className:D?"relative group":"",children:[D&&h.jsxs(h.Fragment,{children:[!A&&h.jsx(I,{className:"admin-sidebar-tree-line-vertical"}),h.jsx(I,{className:"admin-sidebar-tree-line-curve"})]}),P&&w.url?h.jsxs(I,{className:`
`,role:"status","aria-label":Vy("common.loading")}),s&&h.jsx(Q,{className:`${l[t]} ${n} font-medium`,children:s})]});return r?h.jsx(I,{id:e,className:"fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90","aria-live":"polite","aria-busy":"true",...i,children:c}):h.jsx(I,{id:e,className:"flex items-center justify-center p-4","aria-live":"polite","aria-busy":"true",...i,children:c})},es=()=>window.G7Core,gl=(e,t)=>window.G7Core?.t?.(e,t)??e,Uy=({logo:e,logoAlt:t="Logo",menu:n=[],collapsed:r=!1,onToggleCollapse:s,className:o="",currentLocale:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(new Set),[u,f]=v.useState(i||es()?.locale?.current?.()||"ko"),[g,m]=v.useState(window.location.pathname),p=v.useMemo(()=>{const w=[],E=D=>{for(const P of D)P.url&&w.push(P.url),P.children&&E(P.children)};return E(n),w},[n]),y=v.useCallback(w=>{let E=null;for(const D of p)(w===D||w.startsWith(D+"/"))&&(!E||D.length>E.length)&&(E=D);return E},[p]);v.useEffect(()=>{const w=es();return w?.state?.subscribe?w.state.subscribe(D=>{const P=D?._global?.locale||w?.locale?.current?.()||"ko";f(P);const L=D?._global?.currentPath||window.location.pathname;m(L)}):void 0},[]),v.useEffect(()=>{const w=()=>{m(window.location.pathname)};return window.addEventListener("popstate",w),()=>window.removeEventListener("popstate",w)},[]),v.useEffect(()=>{i&&f(i)},[i]);const b=(w,E)=>!w||typeof E!="string"?!1:E===w?!0:E.startsWith(w+"/")?y(E)===w:!1,_=(w,E)=>{const D=new Set,P=L=>{for(const $ of L)if($.children&&$.children.length>0){if(P($.children)||b($.url,E))return D.add($.id),!0}else if(b($.url,E))return!0;return!1};return P(w),D};v.useEffect(()=>{if(n.length>0&&g){const w=_(n,g);w.size>0&&d(E=>new Set([...E,...w]))}},[g,n]);const x=w=>b(w,g),M=w=>w.children?w.children.some(E=>x(E.url)||M(E)):!1,C=w=>{d(E=>{const D=new Set(E);return D.has(w)?D.delete(w):D.add(w),D})},k=w=>typeof w=="string"?w:w[u]||w.en||Object.values(w)[0]||"",N=(w,E)=>{es()?.dispatch?(w.preventDefault(),es().navigation?.onComplete(()=>{es().dispatch({handler:"setState",params:{target:"global",sidebarOpen:!1}})}),es().dispatch({handler:"navigate",params:{path:E}})):window.location.href=E},S=(w,E=0,D=!1)=>{const P=w.children&&w.children.length>0,L=c.has(w.id),$=k(w.name),A=E>0,O=x(w.url),T=M(w),z=A?"admin-sidebar-subitem":"admin-sidebar-item",H="admin-sidebar-item-active",j="admin-sidebar-item-active-parent",G="admin-sidebar-item-default";return h.jsxs(I,{className:A?"relative group":"",children:[A&&h.jsxs(h.Fragment,{children:[!D&&h.jsx(I,{className:"admin-sidebar-tree-line-vertical"}),h.jsx(I,{className:"admin-sidebar-tree-line-curve"})]}),P&&w.url?h.jsxs(I,{className:`
${z}
${O?U:T?W:G}
${O?H:T?j:G}
${r?"justify-center":""}
`,children:[h.jsxs(ae,{onClick:j=>N(j,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(J,{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(ae,{onClick:()=>S(w.id),className:"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600","aria-label":hl(L?"common.collapse":"common.expand"),children:h.jsx(pt,{className:`fas fa-chevron-down w-4 h-4 transition-transform ${L?"rotate-180":""}`})})]})]}):P?h.jsxs(ae,{onClick:()=>S(w.id),className:`
`,children:[h.jsxs(ae,{onClick:W=>N(W,w.url),className:"flex items-center justify-start flex-1 min-w-0",children:[w.icon&&h.jsx(bt,{className:`${w.icon} ${r?"":"mr-3"} w-5 h-5 flex items-center justify-center`}),!r&&h.jsx(Q,{className:"truncate",children:$})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(ae,{onClick:()=>C(w.id),className:"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600","aria-label":gl(L?"common.collapse":"common.expand"),children:h.jsx(bt,{className:`fas fa-chevron-down w-4 h-4 transition-transform ${L?"rotate-180":""}`})})]})]}):P?h.jsxs(ae,{onClick:()=>C(w.id),className:`
${z}
${T?W:G}
${T?j:G}
${r?"justify-center":""}
`,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(pt,{className:w.icon})}),!r&&h.jsx(J,{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(pt,{className:`fas fa-chevron-right w-4 h-4 text-slate-400 transition-transform duration-200 ${L?"rotate-90":""}`})]})]}):w.url?h.jsxs(ae,{onClick:j=>N(j,w.url),className:`
`,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(bt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(bt,{className:`fas fa-chevron-right w-4 h-4 text-slate-400 transition-transform duration-200 ${L?"rotate-90":""}`})]})]}):w.url?h.jsxs(ae,{onClick:W=>N(W,w.url),className:`
${z}
${O?U:G}
${O?H:G}
${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(J,{children:$})]}),!r&&w.module_id&&h.jsx(pt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}):h.jsxs(ae,{className:`
`,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(bt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}):h.jsxs(ae,{className:`
${z}
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(J,{children:$})]}),!r&&w.module_id&&h.jsx(pt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}),P&&!r&&L&&h.jsx(I,{className:"relative ml-2",children:w.children.map((j,Y)=>C(j,E+1,Y===w.children.length-1))})]},w.id)};return h.jsxs(I,{className:o,id:a,...l,children:[(e||s)&&h.jsxs(I,{className:"admin-sidebar-header",children:[e&&!r&&h.jsx(Sn,{src:e,alt:t,className:"h-8"}),s&&h.jsx(ae,{onClick:s,className:"admin-sidebar-collapse-btn","aria-label":hl(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,E)=>C(w,0,E===n.length-1))})]})},zs=(e,t)=>window.G7Core?.t?.(e,t)??e,Wy=({user:e={name:"",email:""},notifications:t=[],onNotificationClick:n,onProfileClick:r,onLogoutClick:s,className:o=""})=>{const[i,a]=v.useState(!1),[l,c]=v.useState(!1),d=v.useRef(null),u=v.useRef(null),f=t.filter(g=>!g.read).length;return v.useEffect(()=>{const g=m=>{d.current&&!d.current.contains(m.target)&&a(!1),u.current&&!u.current.contains(m.target)&&c(!1)};return document.addEventListener("mousedown",g),()=>document.removeEventListener("mousedown",g)},[]),h.jsxs(I,{className:`
`,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(bt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}),P&&!r&&L&&h.jsx(I,{className:"relative ml-2",children:w.children.map((W,Y)=>S(W,E+1,Y===w.children.length-1))})]},w.id)};return h.jsxs(I,{className:o,id:a,...l,children:[(e||s)&&h.jsxs(I,{className:"admin-sidebar-header",children:[e&&!r&&h.jsx(_n,{src:e,alt:t,className:"h-8"}),s&&h.jsx(ae,{onClick:s,className:"admin-sidebar-collapse-btn","aria-label":gl(r?"common.expand_sidebar":"common.collapse_sidebar"),children:h.jsx(bt,{className:`${r?"fas fa-chevron-right":"fas fa-chevron-left"} w-5 h-5 text-slate-600 dark:text-slate-400`})})]}),h.jsx(I,{className:"admin-sidebar-menu-list",children:n.map((w,E)=>S(w,0,E===n.length-1))})]})},Vs=(e,t)=>window.G7Core?.t?.(e,t)??e,By=({user:e={name:"",email:""},notifications:t=[],onNotificationClick:n,onProfileClick:r,onLogoutClick:s,className:o=""})=>{const[i,a]=v.useState(!1),[l,c]=v.useState(!1),d=v.useRef(null),u=v.useRef(null),f=t.filter(g=>!g.read).length;return v.useEffect(()=>{const g=m=>{d.current&&!d.current.contains(m.target)&&a(!1),u.current&&!u.current.contains(m.target)&&c(!1)};return document.addEventListener("mousedown",g),()=>document.removeEventListener("mousedown",g)},[]),h.jsxs(I,{className:`
bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-end gap-4
${o}
`,children:[h.jsxs(I,{ref:d,className:"relative",children:[h.jsxs(ae,{onClick:()=>a(!i),className:"relative p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors","aria-label":zs("common.notifications"),children:[h.jsx(oe,{name:ue.Bell,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),f>0&&h.jsx(J,{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(J,{className:"font-semibold text-gray-900 dark:text-white",children:zs("common.notifications")})}),h.jsx(I,{className:"max-h-96 overflow-y-auto",children:t.length===0?h.jsx(I,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:h.jsx(J,{children:zs("common.no_notifications")})}):t.map(g=>h.jsx(ae,{onClick:()=>{g.onClick?.(),n?.(g.id),a(!1)},className:`
`,children:[h.jsxs(I,{ref:d,className:"relative",children:[h.jsxs(ae,{onClick:()=>a(!i),className:"relative p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors","aria-label":Vs("common.notifications"),children:[h.jsx(oe,{name:de.Bell,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),f>0&&h.jsx(Q,{className:"absolute top-0 right-0 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full",children:f>9?"9+":f})]}),i&&h.jsxs(I,{className:"absolute right-0 mt-2 w-80 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsx(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx(Q,{className:"font-semibold text-gray-900 dark:text-white",children:Vs("common.notifications")})}),h.jsx(I,{className:"max-h-96 overflow-y-auto",children:t.length===0?h.jsx(I,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:h.jsx(Q,{children:Vs("common.no_notifications")})}):t.map(g=>h.jsx(ae,{onClick:()=>{g.onClick?.(),n?.(g.id),a(!1)},className:`
w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 border-b border-gray-100 dark:border-gray-700 transition-colors
${g.read?"":"bg-blue-50 dark:bg-blue-900/20"}
`,children:h.jsxs(I,{className:"flex items-start gap-3",children:[g.iconName&&h.jsx(oe,{name:g.iconName,className:"w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5"}),h.jsxs(I,{className:"flex-1",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:g.title}),h.jsx(I,{className:"text-gray-600 dark:text-gray-400 text-sm mt-1",children:g.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1",children:g.time})]}),!g.read&&h.jsx(J,{className:"w-2 h-2 bg-blue-500 rounded-full mt-1.5"})]})},g.id))})]})]}),h.jsxs(I,{ref:u,className:"relative",children:[h.jsxs(ae,{onClick:()=>c(!l),className:"flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[e.avatar?h.jsx(Sn,{src:e.avatar,alt:e.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(oe,{name:ue.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"hidden md:block text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:e.name}),e.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:e.role})]}),h.jsx(oe,{name:ue.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]}),l&&h.jsxs(I,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:e.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:e.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(ae,{onClick:()=>{r?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors text-gray-900 dark:text-white",children:[h.jsx(oe,{name:ue.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(J,{children:zs("common.profile_settings")})]}),h.jsxs(ae,{onClick:()=>{s?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 text-red-600 dark:text-red-400 transition-colors",children:[h.jsx(oe,{name:ue.ArrowRight,className:"w-5 h-5"}),h.jsx(J,{children:zs("common.logout")})]})]})]})]})]})},Gy=({notifications:e=[],unreadCount:t=0,hasMore:n=!1,loading:r=!1,titleText:s="Notifications",emptyText:o="No notifications",markAllReadText:i="Mark all as read",deleteAllText:a="Delete all",unreadOnlyText:l="Unread only",unreadOnly:c=!1,onNotificationClick:d,onClose:u,onLoadMore:f,onMarkAllRead:g,onDeleteAll:m,onDelete:p,onUnreadOnlyToggle:y,dropdownAlign:b="right",className:_="",id:x,editorAttrs:M})=>{const[S,k]=v.useState(!1),N=v.useRef(null),C=v.useRef(null),w=v.useRef(null),E=v.useRef(null),A=v.useRef(new Set),P=v.useRef(f),L=v.useRef(n),$=v.useRef(r),D=v.useRef(e.length),O=v.useRef(-1);v.useEffect(()=>{P.current=f,L.current=n,$.current=r,D.current=e.length}),v.useEffect(()=>{if(!E.current||!w.current||!S)return;O.current=-1;const W=new IntersectionObserver(([G])=>{G.isIntersecting&&($.current||!L.current||O.current!==D.current&&(O.current=D.current,P.current?.()))},{root:w.current,threshold:.1});return W.observe(E.current),()=>W.disconnect()},[S]),v.useEffect(()=>{if(!w.current||!S)return;const W=new IntersectionObserver(B=>{B.forEach(j=>{const Y=j.target.getAttribute("data-notification-id"),V=j.target.getAttribute("data-unread")==="true";Y&&V&&j.isIntersecting&&A.current.add(Y)})},{root:w.current,threshold:.5});return w.current.querySelectorAll("[data-notification-id]").forEach(B=>W.observe(B)),()=>W.disconnect()},[S,e]);const T=t>0?t:e.filter(W=>!W.read).length,z=v.useCallback(()=>{if(S){if(T>0){const W=Array.from(A.current).filter(G=>{const B=e.find(j=>String(j.id)===String(G));return B&&!B.read});W.length>0&&u?.(W)}A.current.clear(),k(!1)}},[S,u,T,e]);v.useEffect(()=>{if(!S)return;const W=G=>{N.current&&!N.current.contains(G.target)&&z()};return document.addEventListener("mousedown",W),()=>document.removeEventListener("mousedown",W)},[S,z]);const U=v.useCallback(()=>{S?z():k(!0)},[S,z]);return v.useLayoutEffect(()=>{const W=C.current;if(!S||!W)return;W.style.transform="";const G=W.getBoundingClientRect(),B=typeof window<"u"?window.innerWidth:0;if(G.width===0&&G.height===0)return;const j=8;let Y=0;G.right>B-j?Y=-(G.right-(B-j)):G.left<j&&(Y=j-G.left),Y!==0&&(W.style.transform=`translateX(${Y}px)`)},[S,b,e.length]),h.jsxs(I,{ref:N,className:`relative ${_}`,id:x,...M,children:[h.jsxs(ae,{onClick:U,className:"relative w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors cursor-pointer","aria-label":s,children:[h.jsx(oe,{name:ue.Bell,className:"text-sm text-slate-500 dark:text-slate-400"}),T>0&&h.jsx(J,{clas Line truncated
`,children:h.jsxs(I,{className:"flex items-start gap-3",children:[g.iconName&&h.jsx(oe,{name:g.iconName,className:"w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5"}),h.jsxs(I,{className:"flex-1",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:g.title}),h.jsx(I,{className:"text-gray-600 dark:text-gray-400 text-sm mt-1",children:g.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1",children:g.time})]}),!g.read&&h.jsx(Q,{className:"w-2 h-2 bg-blue-500 rounded-full mt-1.5"})]})},g.id))})]})]}),h.jsxs(I,{ref:u,className:"relative",children:[h.jsxs(ae,{onClick:()=>c(!l),className:"flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[e.avatar?h.jsx(_n,{src:e.avatar,alt:e.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(oe,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"hidden md:block text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:e.name}),e.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:e.role})]}),h.jsx(oe,{name:de.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]}),l&&h.jsxs(I,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:e.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:e.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(ae,{onClick:()=>{r?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors text-gray-900 dark:text-white",children:[h.jsx(oe,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{children:Vs("common.profile_settings")})]}),h.jsxs(ae,{onClick:()=>{s?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 text-red-600 dark:text-red-400 transition-colors",children:[h.jsx(oe,{name:de.ArrowRight,className:"w-5 h-5"}),h.jsx(Q,{children:Vs("common.logout")})]})]})]})]})]})},jy=({notifications:e=[],unreadCount:t=0,hasMore:n=!1,loading:r=!1,titleText:s="Notifications",emptyText:o="No notifications",markAllReadText:i="Mark all as read",deleteAllText:a="Delete all",unreadOnlyText:l="Unread only",unreadOnly:c=!1,onNotificationClick:d,onClose:u,onLoadMore:f,onMarkAllRead:g,onDeleteAll:m,onDelete:p,onUnreadOnlyToggle:y,dropdownAlign:b="right",className:_="",id:x,editorAttrs:M})=>{const[C,k]=v.useState(!1),N=v.useRef(null),S=v.useRef(null),w=v.useRef(null),E=v.useRef(null),D=v.useRef(new Set),P=v.useRef(f),L=v.useRef(n),$=v.useRef(r),A=v.useRef(e.length),O=v.useRef(-1);v.useEffect(()=>{P.current=f,L.current=n,$.current=r,A.current=e.length}),v.useEffect(()=>{if(!E.current||!w.current||!C)return;O.current=-1;const j=new IntersectionObserver(([G])=>{G.isIntersecting&&($.current||!L.current||O.current!==A.current&&(O.current=A.current,P.current?.()))},{root:w.current,threshold:.1});return j.observe(E.current),()=>j.disconnect()},[C]),v.useEffect(()=>{if(!w.current||!C)return;const j=new IntersectionObserver(U=>{U.forEach(W=>{const Y=W.target.getAttribute("data-notification-id"),V=W.target.getAttribute("data-unread")==="true";Y&&V&&W.isIntersecting&&D.current.add(Y)})},{root:w.current,threshold:.5});return w.current.querySelectorAll("[data-notification-id]").forEach(U=>j.observe(U)),()=>j.disconnect()},[C,e]);const T=t>0?t:e.filter(j=>!j.read).length,z=v.useCallback(()=>{if(C){if(T>0){const j=Array.from(D.current).filter(G=>{const U=e.find(W=>String(W.id)===String(G));return U&&!U.read});j.length>0&&u?.(j)}D.current.clear(),k(!1)}},[C,u,T,e]);v.useEffect(()=>{if(!C)return;const j=G=>{N.current&&!N.current.contains(G.target)&&z()};return document.addEventListener("mousedown",j),()=>document.removeEventListener("mousedown",j)},[C,z]);const H=v.useCallback(()=>{C?z():k(!0)},[C,z]);return v.useLayoutEffect(()=>{const j=S.current;if(!C||!j)return;j.style.transform="";const G=j.getBoundingClientRect(),U=typeof window<"u"?window.innerWidth:0;if(G.width===0&&G.height===0)return;const W=8;let Y=0;G.right>U-W?Y=-(G.right-(U-W)):G.left<W&&(Y=W-G.left),Y!==0&&(j.style.transform=`translateX(${Y}px)`)},[C,b,e.length]),h.jsxs(I,{ref:N,className:`relative ${_}`,id:x,...M,children:[h.jsxs(ae,{onClick:H,className:"relative w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors cursor-pointer","aria-label":s,children:[h.jsx(oe,{name:de.Bell,className:"text-sm text-slate-500 dark:text-slate-400"}),T>0&&h.jsx(Q,{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:xu(x)},x))})]}):h.jsxs(I,{ref:p,className:`relative ${s}`,id:a,...l,children:[h.jsxs(ae,{onClick:()=>d(!c),className:i?"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer":"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors","aria-haspopup":"listbox","aria-expanded":c,"aria-label":t,children:[h.jsx(oe,{name:ue.Globe,className:i?"w-4 h-4":"w-5 h-5 text-gray-600 dark:text-gray-400"}),i&&h.jsx(J,{className:"font-medium uppercase",children:g}),i&&h.jsx(oe,{name:ue.ChevronDown,className:"w-3 h-3"})]}),c&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsx(I,{className:"py-2",children:e.map(x=>h.jsxs(ae,{onClick:()=>_(x),disabled:u,className:`
`,children:xu(x)},x))})]}):h.jsxs(I,{ref:p,className:`relative ${s}`,id:a,...l,children:[h.jsxs(ae,{onClick:()=>d(!c),className:i?"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer":"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors","aria-haspopup":"listbox","aria-expanded":c,"aria-label":t,children:[h.jsx(oe,{name:de.Globe,className:i?"w-4 h-4":"w-5 h-5 text-gray-600 dark:text-gray-400"}),i&&h.jsx(Q,{className:"font-medium uppercase",children:g}),i&&h.jsx(oe,{name:de.ChevronDown,className:"w-3 h-3"})]}),c&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsx(I,{className:"py-2",children:e.map(x=>h.jsxs(ae,{onClick:()=>_(x),disabled:u,className:`
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(J,{className:"text-gray-900 dark:text-white flex-1",children:xu(x)}),g===x&&h.jsx(oe,{name:ue.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400"})]},x))})})]})},Ky=({copyright:e="© 2026 G7",version:t,quickLinks:n=[],className:r="",changelogModalId:s,copyrightModalId:o,id:i,editorAttrs:a})=>{const l=v.useCallback(()=>{s&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/changelog",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreChangelogContent:"{{response.data.content}}"}},{handler:"openModal",target:s}],onError:[{handler:"openModal",target:s}]}]})},[s]),c=v.useCallback(()=>{o&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/license",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreLicenseContent:"{{response.data.content}}"}},{handler:"openModal",target:o}],onError:[{handler:"openModal",target:o}]}]})},[o]);return h.jsx(I,{className:`admin-footer ${r}`,id:i,...a,children:h.jsxs(I,{className:"admin-footer-row",children:[h.jsxs(I,{className:"admin-footer-group",children:[h.jsx(J,{className:o?"admin-footer-link-hover":"",onClick:o?c:void 0,children:e}),t&&h.jsxs(h.Fragment,{children:[h.jsx(J,{className:"hidden md:inline",children:"•"}),h.jsxs(J,{className:`flex-center gap-2 ${s?"admin-footer-link-hover":""}`,onClick:s?l:void 0,children:[h.jsx(oe,{name:ue.Tag,className:"w-4 h-4"}),h.jsxs(J,{children:["v",t]})]})]})]}),n.length>0&&h.jsx(I,{className:"flex-center gap-4",children:n.map(d=>h.jsxs(Wr,{href:d.url,className:"admin-footer-link",children:[d.iconName&&h.jsx(oe,{name:d.iconName,className:"w-4 h-4"}),h.jsx(J,{children:d.label})]},d.id))})]})})},qy=({title:e,description:t,breadcrumbItems:n,tabs:r,onTabChange:s,actions:o,className:i="",children:a,id:l,editorAttrs:c})=>{const d=(u="secondary")=>({primary:"page-action-primary",secondary:"page-action-secondary",danger:"page-action-danger",ghost:"page-action-ghost"})[u];return h.jsxs(I,{className:i,id:l,...c,children:[n&&n.length>0&&h.jsx(I,{className:"mb-md",children:h.jsx(bu,{items:n})}),h.jsxs(I,{className:"flex-between mb-lg",children:[h.jsxs(I,{children:[h.jsx(Ba,{className:"page-title",children:e}),t&&h.jsx(yt,{className:"page-description",children:t})]}),a?h.jsx(I,{className:"flex-center page-actions",children:a}):o&&o.length>0?h.jsx(I,{className:"page-actions",children:o.map(u=>h.jsxs(ae,{onClick:u.onClick,disabled:u.disabled,className:d(u.variant),children:[u.iconName&&h.jsx(oe,{name:u.iconName,className:"icon-md"}),u.variant!=="ghost"&&u.label&&h.jsx(I,{children:u.label})]},u.id))}):null]}),r&&r.length>0&&h.jsx(I,{className:"tab-container",children:r.map(u=>h.jsxs(ae,{onClick:()=>s?.(u.value),className:u.active?"tab-btn-active":"tab-btn",children:[h.jsx(I,{children:u.label}),u.badge!==void 0&&h.jsx(I,{className:u.active?"tab-badge-active":"tab-badge",children:u.badge})]},u.id))})]})},Zy=()=>window.G7Core,Qy=({title:e,description:t,className:n="",style:r,children:s,id:o,editorAttrs:i})=>{const a="flex items-center justify-between",l=Zy()?.style?.mergeClasses?.(a,n)??`${a} ${n}`;return h.jsxs(I,{className:l,style:r,id:o,...i,children:[h.jsxs(I,{children:[e&&h.jsx(or,{className:"text-base font-semibold text-gray-900 dark:text-white",children:e}),t&&h.jsx(yt,{className:"mt-1 text-sm text-gray-500 dark:text-gray-400",children:t})]}),s&&h.jsx(I,{className:"flex items-center gap-2",children:s})]})},ar=(e,t)=>window.G7Core?.t?.(e,t)??e,Jy=({image:e,imageAlt:t,vendor:n,name:r,version:s,status:o,updateAvailable:i=!1,latestVersion:a,dependencies:l=[],showLayoutEditButton:c=!1,actions:d=[],onLayoutEditClick:u,className:f="",id:g,editorAttrs:m})=>{const p=b=>({active:"success",inactive:"default",pending:"pending",error:"error"})[b],y=b=>({active:ar("common.status_active"),inactive:ar("common.status_inactive"),pending:ar("common.status_pending"),error:ar("common.status_error")})[b];return h.jsxs(I,{className:`
`,children:[h.jsx(Q,{className:"text-gray-900 dark:text-white flex-1",children:xu(x)}),g===x&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400"})]},x))})})]})},Yy=({copyright:e="© 2026 G7",version:t,quickLinks:n=[],className:r="",changelogModalId:s,copyrightModalId:o,id:i,editorAttrs:a})=>{const l=v.useCallback(()=>{s&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/changelog",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreChangelogContent:"{{response.data.content}}"}},{handler:"openModal",target:s}],onError:[{handler:"openModal",target:s}]}]})},[s]),c=v.useCallback(()=>{o&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/license",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreLicenseContent:"{{response.data.content}}"}},{handler:"openModal",target:o}],onError:[{handler:"openModal",target:o}]}]})},[o]);return h.jsx(I,{className:`admin-footer ${r}`,id:i,...a,children:h.jsxs(I,{className:"admin-footer-row",children:[h.jsxs(I,{className:"admin-footer-group",children:[h.jsx(Q,{className:o?"admin-footer-link-hover":"",onClick:o?c:void 0,children:e}),t&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:"•"}),h.jsxs(Q,{className:`flex-center gap-2 ${s?"admin-footer-link-hover":""}`,onClick:s?l:void 0,children:[h.jsx(oe,{name:de.Tag,className:"w-4 h-4"}),h.jsxs(Q,{children:["v",t]})]})]})]}),n.length>0&&h.jsx(I,{className:"flex-center gap-4",children:n.map(d=>h.jsxs(Yr,{href:d.url,className:"admin-footer-link",children:[d.iconName&&h.jsx(oe,{name:d.iconName,className:"w-4 h-4"}),h.jsx(Q,{children:d.label})]},d.id))})]})})},Xy=({title:e,description:t,breadcrumbItems:n,tabs:r,onTabChange:s,actions:o,className:i="",children:a,id:l,editorAttrs:c})=>{const d=(u="secondary")=>({primary:"page-action-primary",secondary:"page-action-secondary",danger:"page-action-danger",ghost:"page-action-ghost"})[u];return h.jsxs(I,{className:i,id:l,...c,children:[n&&n.length>0&&h.jsx(I,{className:"mb-md",children:h.jsx(bu,{items:n})}),h.jsxs(I,{className:"flex-between mb-lg",children:[h.jsxs(I,{children:[h.jsx(Ba,{className:"page-title",children:e}),t&&h.jsx(vt,{className:"page-description",children:t})]}),a?h.jsx(I,{className:"flex-center page-actions",children:a}):o&&o.length>0?h.jsx(I,{className:"page-actions",children:o.map(u=>h.jsxs(ae,{onClick:u.onClick,disabled:u.disabled,className:d(u.variant),children:[u.iconName&&h.jsx(oe,{name:u.iconName,className:"icon-md"}),u.variant!=="ghost"&&u.label&&h.jsx(I,{children:u.label})]},u.id))}):null]}),r&&r.length>0&&h.jsx(I,{className:"tab-container",children:r.map(u=>h.jsxs(ae,{onClick:()=>s?.(u.value),className:u.active?"tab-btn-active":"tab-btn",children:[h.jsx(I,{children:u.label}),u.badge!==void 0&&h.jsx(I,{className:u.active?"tab-badge-active":"tab-badge",children:u.badge})]},u.id))})]})},Ky=()=>window.G7Core,qy=({title:e,description:t,className:n="",style:r,children:s,id:o,editorAttrs:i})=>{const a="flex items-center justify-between",l=Ky()?.style?.mergeClasses?.(a,n)??`${a} ${n}`;return h.jsxs(I,{className:l,style:r,id:o,...i,children:[h.jsxs(I,{children:[e&&h.jsx(ar,{className:"text-base font-semibold text-gray-900 dark:text-white",children:e}),t&&h.jsx(vt,{className:"mt-1 text-sm text-gray-500 dark:text-gray-400",children:t})]}),s&&h.jsx(I,{className:"flex items-center gap-2",children:s})]})},cr=(e,t)=>window.G7Core?.t?.(e,t)??e,Zy=({image:e,imageAlt:t,vendor:n,name:r,version:s,status:o,updateAvailable:i=!1,latestVersion:a,dependencies:l=[],showLayoutEditButton:c=!1,actions:d=[],onLayoutEditClick:u,className:f="",id:g,editorAttrs:m})=>{const p=b=>({active:"success",inactive:"default",pending:"pending",error:"error"})[b],y=b=>({active:cr("common.status_active"),inactive:cr("common.status_inactive"),pending:cr("common.status_pending"),error:cr("common.status_error")})[b];return h.jsxs(I,{className:`
bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-lg transition-shadow
${f}
`,id:g,...m,children:[e&&h.jsxs(I,{className:"relative",children:[h.jsx(Sn,{src:e,alt:t||ar("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(Xo,{status:p(o),label:y(o),showIcon:!0})}),i&&h.jsx(I,{className:"absolute top-3 left-3",children:h.jsxs(J,{className:"flex items-center gap-1 px-2 py-1 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800 rounded-md text-xs font-semibold",children:[h.jsx(oe,{name:ue.ExclamationTriangle,className:"w-3 h-3"}),h.jsx(J,{children:ar("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(jr,{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(J,{children:["v",s]}),i&&a&&h.jsxs(h.Fragment,{children:[h.jsx(J,{children:"→"}),h.jsxs(J,{className:"text-yellow-600 dark:text-yellow-400 font-semibold",children:["v",a]})]})]}),!e&&h.jsx(I,{className:"mt-2",children:h.jsx(Xo,{status:p(o),label:y(o),showIcon:!0})})]}),d.length>0&&h.jsx(Ls,{items:d,triggerIconName:ue.EllipsisVertical,position:"left"})]}),l.length>0&&h.jsxs(I,{className:"mb-3",children:[h.jsxs(yt,{className:"text-xs text-gray-500 dark:text-gray-400 mb-1",children:[ar("common.dependencies"),":"]}),h.jsx(I,{className:"flex flex-wrap gap-1",children:l.map((b,_)=>h.jsx(J,{className:"px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded",children:b},_))})]}),c&&u&&h.jsxs(ae,{onClick:u,className:"w-full px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors flex items-center justify-center gap-2 font-medium",children:[h.jsx(oe,{name:ue.Edit,className:"w-4 h-4"}),h.jsx(J,{children:ar("common.edit_layout")})]})]})]})},wu=({type:e,message:t,dismissible:n=!1,onDismiss:r,className:s="",id:o,editorAttrs:i})=>{const a={info:{containerClass:"alert-info",iconClass:"alert-icon-info",textClass:"alert-text-info",icon:ue.InfoCircle},success:{containerClass:"alert-success",iconClass:"alert-icon-success",textClass:"alert-text-success",icon:ue.CheckCircle},warning:{containerClass:"alert-warning",iconClass:"alert-icon-warning",textClass:"alert-text-warning",icon:ue.ExclamationTriangle},error:{containerClass:"alert-error",iconClass:"alert-icon-error",textClass:"alert-text-error",icon:ue.TimesCircle}},l=a[e]??a.info;return h.jsxs(I,{className:`${l.containerClass} ${s}`,role:"alert","aria-live":"polite",id:o,...i,children:[h.jsx(oe,{name:l.icon,className:l.iconClass,ariaLabel:`${e} icon`}),h.jsx(yt,{className:l.textClass,children:t}),n&&h.jsx(ae,{onClick:r,className:`alert-dismiss ${l.textClass}`,"aria-label":"Close alert",children:h.jsx(oe,{name:ue.Times,className:l.iconClass,ariaLabel:"Close"})})]})},Ry=({layoutName:e,onBack:t,onPreview:n,onSave:r,isSaving:s=!1,className:o=""})=>{const i=`
`,id:g,...m,children:[e&&h.jsxs(I,{className:"relative",children:[h.jsx(_n,{src:e,alt:t||cr("common.template_preview",{vendor:n,name:r}),className:"w-full h-48 object-cover"}),h.jsx(I,{className:"absolute top-3 right-3",children:h.jsx(Ko,{status:p(o),label:y(o),showIcon:!0})}),i&&h.jsx(I,{className:"absolute top-3 left-3",children:h.jsxs(Q,{className:"flex items-center gap-1 px-2 py-1 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800 rounded-md text-xs font-semibold",children:[h.jsx(oe,{name:de.ExclamationTriangle,className:"w-3 h-3"}),h.jsx(Q,{children:cr("common.update_available")})]})})]}),h.jsxs(I,{className:"p-4",children:[h.jsxs(I,{className:"flex items-start justify-between mb-3",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Gr,{className:"text-lg font-bold text-gray-900 dark:text-white",children:[n,"/",r]}),h.jsxs(I,{className:"flex items-center gap-2 mt-1 text-sm text-gray-600 dark:text-gray-400",children:[h.jsxs(Q,{children:["v",s]}),i&&a&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{children:"→"}),h.jsxs(Q,{className:"text-yellow-600 dark:text-yellow-400 font-semibold",children:["v",a]})]})]}),!e&&h.jsx(I,{className:"mt-2",children:h.jsx(Ko,{status:p(o),label:y(o),showIcon:!0})})]}),d.length>0&&h.jsx(Ls,{items:d,triggerIconName:de.EllipsisVertical,position:"left"})]}),l.length>0&&h.jsxs(I,{className:"mb-3",children:[h.jsxs(vt,{className:"text-xs text-gray-500 dark:text-gray-400 mb-1",children:[cr("common.dependencies"),":"]}),h.jsx(I,{className:"flex flex-wrap gap-1",children:l.map((b,_)=>h.jsx(Q,{className:"px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded",children:b},_))})]}),c&&u&&h.jsxs(ae,{onClick:u,className:"w-full px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors flex items-center justify-center gap-2 font-medium",children:[h.jsx(oe,{name:de.Edit,className:"w-4 h-4"}),h.jsx(Q,{children:cr("common.edit_layout")})]})]})]})},wu=({type:e,message:t,dismissible:n=!1,onDismiss:r,className:s="",id:o,editorAttrs:i})=>{const a={info:{containerClass:"alert-info",iconClass:"alert-icon-info",textClass:"alert-text-info",icon:de.InfoCircle},success:{containerClass:"alert-success",iconClass:"alert-icon-success",textClass:"alert-text-success",icon:de.CheckCircle},warning:{containerClass:"alert-warning",iconClass:"alert-icon-warning",textClass:"alert-text-warning",icon:de.ExclamationTriangle},error:{containerClass:"alert-error",iconClass:"alert-icon-error",textClass:"alert-text-error",icon:de.TimesCircle}},l=a[e]??a.info;return h.jsxs(I,{className:`${l.containerClass} ${s}`,role:"alert","aria-live":"polite",id:o,...i,children:[h.jsx(oe,{name:l.icon,className:l.iconClass,ariaLabel:`${e} icon`}),h.jsx(vt,{className:l.textClass,children:t}),n&&h.jsx(ae,{onClick:r,className:`alert-dismiss ${l.textClass}`,"aria-label":"Close alert",children:h.jsx(oe,{name:de.Times,className:l.iconClass,ariaLabel:"Close"})})]})},Qy=({layoutName:e,onBack:t,onPreview:n,onSave:r,isSaving:s=!1,className:o=""})=>{const i=`
flex items-center justify-between gap-4 p-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700
${o}
`.trim().replace(/\s+/g," ");return h.jsxs(I,{className:i,children:[h.jsxs(I,{className:"flex items-center gap-3",children:[h.jsx(ae,{onClick:t,className:"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors bg-transparent border-0 cursor-pointer","aria-label":"뒤로가기",children:h.jsx(oe,{name:ue.ArrowLeft,className:"text-gray-700 dark:text-gray-300",ariaLabel:"뒤로가기"})}),h.jsx(or,{className:"text-xl font-semibold text-gray-900 dark:text-gray-100",children:e})]}),h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(ae,{onClick:n,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors border-0 cursor-pointer","aria-label":"미리보기",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{name:ue.Eye,className:"text-gray-700 dark:text-gray-300",ariaLabel:"미리보기 아이콘"}),h.jsx("span",{children:"미리보기"})]})}),h.jsx(ae,{onClick:r,disabled:s,className:`
`.trim().replace(/\s+/g," ");return h.jsxs(I,{className:i,children:[h.jsxs(I,{className:"flex items-center gap-3",children:[h.jsx(ae,{onClick:t,className:"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors bg-transparent border-0 cursor-pointer","aria-label":"뒤로가기",children:h.jsx(oe,{name:de.ArrowLeft,className:"text-gray-700 dark:text-gray-300",ariaLabel:"뒤로가기"})}),h.jsx(ar,{className:"text-xl font-semibold text-gray-900 dark:text-gray-100",children:e})]}),h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(ae,{onClick:n,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors border-0 cursor-pointer","aria-label":"미리보기",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{name:de.Eye,className:"text-gray-700 dark:text-gray-300",ariaLabel:"미리보기 아이콘"}),h.jsx("span",{children:"미리보기"})]})}),h.jsx(ae,{onClick:r,disabled:s,className:`
px-4 py-2 rounded-lg transition-colors border-0 cursor-pointer
${s?"bg-blue-400 dark:bg-blue-600 cursor-not-allowed":"bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"}
text-white
`.trim().replace(/\s+/g," "),"aria-label":s?"저장 중...":"저장",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{name:s?ue.Spinner:ue.Save,className:`text-white ${s?"animate-spin":""}`,ariaLabel:s?"저장 중 아이콘":"저장 아이콘"}),h.jsx("span",{children:s?"저장 중...":"저장"})]})})]})]})},ev=({files:e,selectedId:t,onSelect:n,className:r=""})=>{const s=a=>{try{const l=new Date(a);if(isNaN(l.getTime()))return a;const c=l.getFullYear(),d=String(l.getMonth()+1).padStart(2,"0"),u=String(l.getDate()).padStart(2,"0"),f=String(l.getHours()).padStart(2,"0"),g=String(l.getMinutes()).padStart(2,"0");return`${c}-${d}-${u} ${f}:${g}`}catch{return a}},o=a=>{n(a)},i=`
`.trim().replace(/\s+/g," "),"aria-label":s?"저장 중...":"저장",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{name:s?de.Spinner:de.Save,className:`text-white ${s?"animate-spin":""}`,ariaLabel:s?"저장 중 아이콘":"저장 아이콘"}),h.jsx("span",{children:s?"저장 중...":"저장"})]})})]})]})},Jy=({files:e,selectedId:t,onSelect:n,className:r=""})=>{const s=a=>{try{const l=new Date(a);if(isNaN(l.getTime()))return a;const c=l.getFullYear(),d=String(l.getMonth()+1).padStart(2,"0"),u=String(l.getDate()).padStart(2,"0"),f=String(l.getHours()).padStart(2,"0"),g=String(l.getMinutes()).padStart(2,"0");return`${c}-${d}-${u} ${f}:${g}`}catch{return a}},o=a=>{n(a)},i=`
w-full
${r}
`.trim().replace(/\s+/g," ");return h.jsx(I,{className:i,children:e.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"파일이 없습니다."}):h.jsx(ja,{className:"divide-y divide-gray-200 dark:divide-gray-700",role:"listbox","aria-label":"레이아웃 파일 목록",children:e.map(a=>{const l=a.id===t,c=`
@@ -106,8 +106,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
`.trim().replace(/\s+/g," "),u=`
text-sm
${l?"text-blue-700 dark:text-blue-300":"text-gray-500 dark:text-gray-400"}
`.trim().replace(/\s+/g," ");return h.jsx(Wa,{className:c,onClick:()=>o(a.id),role:"option","aria-selected":l,tabIndex:0,onKeyDown:f=>{(f.key==="Enter"||f.key===" ")&&(f.preventDefault(),o(a.id))},children:h.jsxs(I,{className:"flex flex-col gap-1",children:[h.jsx(J,{className:d,children:a.name}),h.jsx(J,{className:u,children:s(a.updated_at)})]})},a.id)})})})};function ku(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function tv(e){if(Array.isArray(e))return e}function nv(e,t,n){return(t=cv(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function rv(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function sv(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Cu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Su(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?Cu(Object(n),!0).forEach(function(r){nv(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Cu(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function ov(e,t){if(e==null)return{};var n,r,s=iv(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)===-1&&{}.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}function iv(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}function av(e,t){return tv(e)||rv(e,t)||dv(e,t)||sv()}function lv(e,t){if(typeof e!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function cv(e){var t=lv(e,"string");return typeof t=="symbol"?t:t+""}function dv(e,t){if(e){if(typeof e=="string")return ku(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?ku(e,t):void 0}}function uv(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function _u(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Eu(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?_u(Object(n),!0).forEach(function(r){uv(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):_u(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function fv(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return function(r){return t.reduceRight(function(s,o){return o(s)},r)}}function Vs(e){return function t(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=e.length?e.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return t.apply(n,[].concat(s,a))}}}function Jo(e){return{}.toString.call(e).includes("Object")}function hv(e){return!Object.keys(e).length}function Hs(e){return typeof e=="function"}function gv(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function pv(e,t){return Jo(t)||lr("changeType"),Object.keys(t).some(function(n){return!gv(e,n)})&&lr("changeField"),t}function mv(e){Hs(e)||lr("selectorType")}function bv(e){Hs(e)||Jo(e)||lr("handlerType"),Jo(e)&&Object.values(e).some(function(t){return!Hs(t)})&&lr("handlersType")}function yv(e){e||lr("initialIsRequired"),Jo(e)||lr("initialType"),hv(e)&&lr("initialContent")}function vv(e,t){throw new Error(e[t]||e.default)}var xv={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"},lr=Vs(vv)(xv),Ro={changes:pv,selector:mv,handler:bv,initial:yv};function wv(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};Ro.initial(e),Ro.handler(t);var n={current:e},r=Vs(Sv)(n,t),s=Vs(Cv)(n),o=Vs(Ro.changes)(e),i=Vs(kv)(n);function a(){var c=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(d){return d};return Ro.selector(c),c(n.current)}function l(c){fv(r,s,o,i)(c)}return[a,l]}function kv(e,t){return Hs(t)?t(e.current):t}function Cv(e,t){return e.current=Eu(Eu({},e.current),t),t}function Sv(e,t,n){return Hs(t)?t(e.current):Object.keys(n).forEach(function(r){var s;return(s=t[r])===null||s===void 0?void 0:s.call(t,e.current[r])}),n}var _v={create:wv},Ev={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function Mv(e){return function t(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=e.length?e.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l]; Line truncated
`.trim().replace(/\s+/g," ");return h.jsx(Wa,{className:c,onClick:()=>o(a.id),role:"option","aria-selected":l,tabIndex:0,onKeyDown:f=>{(f.key==="Enter"||f.key===" ")&&(f.preventDefault(),o(a.id))},children:h.jsxs(I,{className:"flex flex-col gap-1",children:[h.jsx(Q,{className:d,children:a.name}),h.jsx(Q,{className:u,children:s(a.updated_at)})]})},a.id)})})})};function ku(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function Ry(e){if(Array.isArray(e))return e}function ev(e,t,n){return(t=av(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function tv(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function nv(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Cu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Su(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?Cu(Object(n),!0).forEach(function(r){ev(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Cu(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function rv(e,t){if(e==null)return{};var n,r,s=sv(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)===-1&&{}.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}function sv(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}function ov(e,t){return Ry(e)||tv(e,t)||lv(e,t)||nv()}function iv(e,t){if(typeof e!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function av(e){var t=iv(e,"string");return typeof t=="symbol"?t:t+""}function lv(e,t){if(e){if(typeof e=="string")return ku(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?ku(e,t):void 0}}function cv(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function _u(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Eu(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?_u(Object(n),!0).forEach(function(r){cv(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):_u(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function dv(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return function(r){return t.reduceRight(function(s,o){return o(s)},r)}}function Hs(e){return function t(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=e.length?e.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return t.apply(n,[].concat(s,a))}}}function Ro(e){return{}.toString.call(e).includes("Object")}function uv(e){return!Object.keys(e).length}function Us(e){return typeof e=="function"}function fv(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function hv(e,t){return Ro(t)||dr("changeType"),Object.keys(t).some(function(n){return!fv(e,n)})&&dr("changeField"),t}function gv(e){Us(e)||dr("selectorType")}function pv(e){Us(e)||Ro(e)||dr("handlerType"),Ro(e)&&Object.values(e).some(function(t){return!Us(t)})&&dr("handlersType")}function mv(e){e||dr("initialIsRequired"),Ro(e)||dr("initialType"),uv(e)&&dr("initialContent")}function bv(e,t){throw new Error(e[t]||e.default)}var yv={initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"},dr=Hs(bv)(yv),ei={changes:hv,selector:gv,handler:pv,initial:mv};function vv(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};ei.initial(e),ei.handler(t);var n={current:e},r=Hs(kv)(n,t),s=Hs(wv)(n),o=Hs(ei.changes)(e),i=Hs(xv)(n);function a(){var c=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(d){return d};return ei.selector(c),c(n.current)}function l(c){dv(r,s,o,i)(c)}return[a,l]}function xv(e,t){return Us(t)?t(e.current):t}function wv(e,t){return e.current=Eu(Eu({},e.current),t),t}function kv(e,t,n){return Us(t)?t(e.current):Object.keys(n).forEach(function(r){var s;return(s=t[r])===null||s===void 0?void 0:s.call(t,e.current[r])}),n}var Cv={create:vv},Sv={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function _v(e){return function t(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=e.length?e.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l]; Line truncated
You are using deprecated way of configuration.
Instead of using
@@ -116,10 +116,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
`},Nu=Mv(Av)(Mu),Dv={config:Tv},Iv=function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return function(s){return n.reduceRight(function(o,i){return i(o)},s)}};function Tu(e,t){return Object.keys(t).forEach(function(n){t[n]instanceof Object&&e[n]&&Object.assign(t[n],Tu(e[n],t[n]))}),Su(Su({},e),t)}var Lv={type:"cancelation",msg:"operation is manually canceled"};function gl(e){var t=!1,n=new Promise(function(r,s){e.then(function(o){return t?s(Lv):r(o)}),e.catch(s)});return n.cancel=function(){return t=!0},n}var Pv=["monaco"],$v=_v.create({config:Ev,isInitialized:!1,resolve:null,reject:null,monaco:null}),Ou=av($v,2),Us=Ou[0],ei=Ou[1];function Fv(e){var t=Dv.config(e),n=t.monaco,r=ov(t,Pv);ei(function(s){return{config:Tu(s.config,r),monaco:n}})}function zv(){var e=Us(function(t){var n=t.monaco,r=t.isInitialized,s=t.resolve;return{monaco:n,isInitialized:r,resolve:s}});if(!e.isInitialized){if(ei({isInitialized:!0}),e.monaco)return e.resolve(e.monaco),gl(pl);if(window.monaco&&window.monaco.editor)return Au(window.monaco),e.resolve(window.monaco),gl(pl);Iv(Vv,Uv)(Bv)}return gl(pl)}function Vv(e){return document.body.appendChild(e)}function Hv(e){var t=document.createElement("script");return e&&(t.src=e),t}function Uv(e){var t=Us(function(r){var s=r.config,o=r.reject;return{config:s,reject:o}}),n=Hv("".concat(t.config.paths.vs,"/loader.js"));return n.onload=function(){return e()},n.onerror=t.reject,n}function Bv(){var e=Us(function(n){var r=n.config,s=n.resolve,o=n.reject;return{config:r,resolve:s,reject:o}}),t=window.require;t.config(e.config),t(["vs/editor/editor.main"],function(n){var r=n.m||n;Au(r),e.resolve(r)},function(n){e.reject(n)})}function Au(e){Us().monaco||ei({monaco:e})}function jv(){return Us(function(e){var t=e.monaco;return t})}var pl=new Promise(function(e,t){return ei({resolve:e,reject:t})}),Du={config:Fv,init:zv,__getMonacoInstance:jv},Wv={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},ml=Wv,Gv={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},Yv=Gv;function Xv({children:e}){return v.createElement("div",{style:Yv.container},e)}var Kv=Xv,qv=Kv;function Zv({width:e,height:t,isEditorReady:n,loading:r,_ref:s,className:o,wrapperProps:i}){return v.createElement("section",{style:{...ml.wrapper,width:e,height:t},...i},!n&&v.createElement(qv,null,r),v.createElement("div",{ref:s,style:{...ml.fullWidth,...!n&&ml.hide},className:o}))}var Qv=Zv,Iu=v.memo(Qv);function Jv(e){v.useEffect(e,[])}var Lu=Jv;function Rv(e,t,n=!0){let r=v.useRef(!0);v.useEffect(r.current||!n?()=>{r.current=!1}:e,t)}var cn=Rv;function Bs(){}function es(e,t,n,r){return ex(e,r)||tx(e,t,n,r)}function ex(e,t){return e.editor.getModel(Pu(e,t))}function tx(e,t,n,r){return e.editor.createModel(t,n,r?Pu(e,r):void 0)}function Pu(e,t){return e.Uri.parse(t)}function nx({original:e,modified:t,language:n,originalLanguage:r,modifiedLanguage:s,originalModelPath:o,modifiedModelPath:i,keepCurrentOriginalModel:a=!1,keepCurrentModifiedModel:l=!1,theme:c="light",loading:d="Loading...",options:u={},height:f="100%",width:g="100%",className:m,wrapperProps:p={},beforeMount:y=Bs,onMount:b=Bs}){let[_,x]=v.useState(!1),[M,S]=v.useState(!0),k=v.useRef(null),N=v.useRef(null),C=v.useRef(null),w=v.useRef(b),E=v.useRef(y),A=v.useRef(!1);Lu(()=>{let D=Du.init();return D.then(O=>(N.current=O)&&S(!1)).catch(O=>O?.type!=="cancelation"&&console.error("Monaco initialization: error:",O)),()=>k.current?$():D.cancel()}),cn(()=>{if(k.current&&N.current){let D=k.current.getOriginalEditor(),O=es(N.current,e||"",r||n||"text",o||"");O!==D.getModel()&&D.setModel(O)}},[o],_),cn(()=>{if(k.current&&N.current){let D=k.current.getModifiedEditor(),O=es(N.current,t||"",s||n||"text",i||"");O!==D.getModel()&&D.setModel(O)}},[i],_),cn(()=>{let D=k.current.getModifiedEditor();D.getOption(N.current.editor.EditorOption.readOnly)?D.setValue(t||""):t!==D.getValue()&&(D.executeEdits("",[{range:D.getModel().getFullModelRange(),text:t||"",forceMoveMarkers:!0}]),D.pushUndoStop())},[t],_),cn(()=>{k.current?.getModel()?.original.setValue(e||"")},[e],_),cn(()=>{let{original:D,modified:O}=k.current.getModel();N.current.editor.setModelLanguage(D,r||n||"text"),N.current.editor.setModelLanguage(O,s||n||"text")},[n,r,s],_),cn(()=>{N.current?.editor.setTheme(c)},[c],_),cn(()=>{k.current?.updateOptions(u)},[u],_);let P=v.useCallback(()=>{if(!N.current)return;E.current(N.current);let D=es(N.current,e||"",r||n||"text",o||""),O=es(N.current,t||"",s||n||"text",i||"");k.current?.setModel({original:D,modified:O})},[n,t,s,e,r,o,i]),L=v.useCallback(()=>{!A.current&&C.current&&(k.current=N.current.editor.createDiffEditor(C.current,{automaticLayout:!0,...u}),P(),N.current?.editor.setTheme(c),x(!0),A.current=!0)},[u,c,P]);v.useEffect(()=>{_&&w.current(k.current,N.current)},[_]),v.useEffect(()=>{!M&&!_&&L()},[M,_,L]);function $(){let D=k.current?.getModel();a||D?.original?.d Line truncated
`},Nu=_v(Tv)(Mu),Ov={config:Mv},Av=function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return function(s){return n.reduceRight(function(o,i){return i(o)},s)}};function Tu(e,t){return Object.keys(t).forEach(function(n){t[n]instanceof Object&&e[n]&&Object.assign(t[n],Tu(e[n],t[n]))}),Su(Su({},e),t)}var Dv={type:"cancelation",msg:"operation is manually canceled"};function pl(e){var t=!1,n=new Promise(function(r,s){e.then(function(o){return t?s(Dv):r(o)}),e.catch(s)});return n.cancel=function(){return t=!0},n}var Iv=["monaco"],Lv=Cv.create({config:Sv,isInitialized:!1,resolve:null,reject:null,monaco:null}),Ou=ov(Lv,2),Bs=Ou[0],ti=Ou[1];function Pv(e){var t=Ov.config(e),n=t.monaco,r=rv(t,Iv);ti(function(s){return{config:Tu(s.config,r),monaco:n}})}function $v(){var e=Bs(function(t){var n=t.monaco,r=t.isInitialized,s=t.resolve;return{monaco:n,isInitialized:r,resolve:s}});if(!e.isInitialized){if(ti({isInitialized:!0}),e.monaco)return e.resolve(e.monaco),pl(ml);if(window.monaco&&window.monaco.editor)return Au(window.monaco),e.resolve(window.monaco),pl(ml);Av(Fv,Vv)(Hv)}return pl(ml)}function Fv(e){return document.body.appendChild(e)}function zv(e){var t=document.createElement("script");return e&&(t.src=e),t}function Vv(e){var t=Bs(function(r){var s=r.config,o=r.reject;return{config:s,reject:o}}),n=zv("".concat(t.config.paths.vs,"/loader.js"));return n.onload=function(){return e()},n.onerror=t.reject,n}function Hv(){var e=Bs(function(n){var r=n.config,s=n.resolve,o=n.reject;return{config:r,resolve:s,reject:o}}),t=window.require;t.config(e.config),t(["vs/editor/editor.main"],function(n){var r=n.m||n;Au(r),e.resolve(r)},function(n){e.reject(n)})}function Au(e){Bs().monaco||ti({monaco:e})}function Uv(){return Bs(function(e){var t=e.monaco;return t})}var ml=new Promise(function(e,t){return ti({resolve:e,reject:t})}),Du={config:Pv,init:$v,__getMonacoInstance:Uv},Bv={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},bl=Bv,jv={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},Wv=jv;function Gv({children:e}){return v.createElement("div",{style:Wv.container},e)}var Yv=Gv,Xv=Yv;function Kv({width:e,height:t,isEditorReady:n,loading:r,_ref:s,className:o,wrapperProps:i}){return v.createElement("section",{style:{...bl.wrapper,width:e,height:t},...i},!n&&v.createElement(Xv,null,r),v.createElement("div",{ref:s,style:{...bl.fullWidth,...!n&&bl.hide},className:o}))}var qv=Kv,Iu=v.memo(qv);function Zv(e){v.useEffect(e,[])}var Lu=Zv;function Qv(e,t,n=!0){let r=v.useRef(!0);v.useEffect(r.current||!n?()=>{r.current=!1}:e,t)}var cn=Qv;function js(){}function ts(e,t,n,r){return Jv(e,r)||Rv(e,t,n,r)}function Jv(e,t){return e.editor.getModel(Pu(e,t))}function Rv(e,t,n,r){return e.editor.createModel(t,n,r?Pu(e,r):void 0)}function Pu(e,t){return e.Uri.parse(t)}function ex({original:e,modified:t,language:n,originalLanguage:r,modifiedLanguage:s,originalModelPath:o,modifiedModelPath:i,keepCurrentOriginalModel:a=!1,keepCurrentModifiedModel:l=!1,theme:c="light",loading:d="Loading...",options:u={},height:f="100%",width:g="100%",className:m,wrapperProps:p={},beforeMount:y=js,onMount:b=js}){let[_,x]=v.useState(!1),[M,C]=v.useState(!0),k=v.useRef(null),N=v.useRef(null),S=v.useRef(null),w=v.useRef(b),E=v.useRef(y),D=v.useRef(!1);Lu(()=>{let A=Du.init();return A.then(O=>(N.current=O)&&C(!1)).catch(O=>O?.type!=="cancelation"&&console.error("Monaco initialization: error:",O)),()=>k.current?$():A.cancel()}),cn(()=>{if(k.current&&N.current){let A=k.current.getOriginalEditor(),O=ts(N.current,e||"",r||n||"text",o||"");O!==A.getModel()&&A.setModel(O)}},[o],_),cn(()=>{if(k.current&&N.current){let A=k.current.getModifiedEditor(),O=ts(N.current,t||"",s||n||"text",i||"");O!==A.getModel()&&A.setModel(O)}},[i],_),cn(()=>{let A=k.current.getModifiedEditor();A.getOption(N.current.editor.EditorOption.readOnly)?A.setValue(t||""):t!==A.getValue()&&(A.executeEdits("",[{range:A.getModel().getFullModelRange(),text:t||"",forceMoveMarkers:!0}]),A.pushUndoStop())},[t],_),cn(()=>{k.current?.getModel()?.original.setValue(e||"")},[e],_),cn(()=>{let{original:A,modified:O}=k.current.getModel();N.current.editor.setModelLanguage(A,r||n||"text"),N.current.editor.setModelLanguage(O,s||n||"text")},[n,r,s],_),cn(()=>{N.current?.editor.setTheme(c)},[c],_),cn(()=>{k.current?.updateOptions(u)},[u],_);let P=v.useCallback(()=>{if(!N.current)return;E.current(N.current);let A=ts(N.current,e||"",r||n||"text",o||""),O=ts(N.current,t||"",s||n||"text",i||"");k.current?.setModel({original:A,modified:O})},[n,t,s,e,r,o,i]),L=v.useCallback(()=>{!D.current&&S.current&&(k.current=N.current.editor.createDiffEditor(S.current,{automaticLayout:!0,...u}),P(),N.current?.editor.setTheme(c),x(!0),D.current=!0)},[u,c,P]);v.useEffect(()=>{_&&w.current(k.current,N.current)},[_]),v.useEffect(()=>{!M&&!_&&L()},[M,_,L]);function $(){let A=k.current?.getModel();a||A?.original?.d Line truncated
p-3 rounded-lg border cursor-pointer transition-colors
${t?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500"}
`,onClick:r,children:h.jsxs(I,{className:"flex justify-between items-start",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(J,{className:"font-semibold text-gray-900 dark:text-gray-100",children:["버전 ",e.version]}),h.jsx(J,{className:"ml-2 text-sm text-gray-500 dark:text-gray-400",children:ux(e.created_at)})]}),h.jsxs(I,{className:"flex gap-3 text-sm",children:[h.jsxs(J,{className:"text-green-600 dark:text-green-400",children:["+",e.changes_summary.added]}),h.jsxs(J,{className:"text-red-600 dark:text-red-400",children:["-",e.changes_summary.removed]})]})]})})});$u.displayName="VersionListItem";const Fu=({versions:e,selectedId:t,onSelect:n})=>e.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"버전이 없습니다."}):h.jsx(ja,{role:"list",className:"space-y-2",children:e.map(r=>h.jsx($u,{version:r,isSelected:t===r.id,onSelect:n},r.id))}),fx=({layoutId:e,versions:t,onRestore:n})=>{const[r,s]=v.useState(),o=l=>{s(l)},i=()=>{r!==void 0&&n(r)},a=r===void 0;return h.jsxs(I,{className:"space-y-4",role:"region","aria-label":"레이아웃 히스토리 패널",children:[h.jsx(I,{className:"border border-gray-300 dark:border-gray-600 rounded-lg p-4",children:h.jsx(Fu,{versions:t,selectedId:r,onSelect:o})}),h.jsx(I,{className:"flex justify-end",children:h.jsx(ae,{variant:"primary",disabled:a,onClick:i,"aria-label":"선택한 버전으로 복원",children:"복원"})})]})},hx=({emailLabel:e="Email",passwordLabel:t="Password",submitButtonText:n="Sign In",processingText:r="Processing...",emailPlaceholder:s="Email",passwordPlaceholder:o="Password",forgotPasswordText:i="Forgot your password?",forgotPasswordUrl:a="/admin/password/reset",emailRequiredError:l="Please enter your email.",emailInvalidError:c="Please enter a valid email address.",passwordRequiredError:d="Please enter your password.",onSubmit:u,loadingActions:f={},isLoading:g=!1,disabled:m=!1,apiError:p="",className:y="",style:b})=>{const[_,x]=v.useState(""),[M,S]=v.useState(""),[k,N]=v.useState({}),C=p,w=Object.values(f).some($=>$===!0),E=g||w,A=m||E,P=()=>{const $={},D=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return!_||_.trim()===""?$.email=l:D.test(_)||($.email=c),(!M||M.trim()==="")&&($.password=d),N($),Object.keys($).length===0},L=async $=>{if(!P()){$.preventDefault();return}N({}),u?await u($):$.preventDefault()};return h.jsx(I,{className:`w-full ${y}`,style:b,children:h.jsxs(Ga,{onSubmit:L,className:"space-y-6",noValidate:!0,children:[C&&h.jsx(I,{className:"p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800",children:h.jsx(yt,{className:"text-sm text-red-600 dark:text-red-400",children:C})}),h.jsxs(I,{children:[h.jsx(ln,{htmlFor:"email",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:e}),h.jsx(kt,{id:"email",name:"email",type:"email",value:_,onChange:$=>x($.target.value),placeholder:s,disabled:A,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:["버전 ",e.version]}),h.jsx(Q,{className:"ml-2 text-sm text-gray-500 dark:text-gray-400",children:cx(e.created_at)})]}),h.jsxs(I,{className:"flex gap-3 text-sm",children:[h.jsxs(Q,{className:"text-green-600 dark:text-green-400",children:["+",e.changes_summary.added]}),h.jsxs(Q,{className:"text-red-600 dark:text-red-400",children:["-",e.changes_summary.removed]})]})]})})});$u.displayName="VersionListItem";const Fu=({versions:e,selectedId:t,onSelect:n})=>e.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"버전이 없습니다."}):h.jsx(ja,{role:"list",className:"space-y-2",children:e.map(r=>h.jsx($u,{version:r,isSelected:t===r.id,onSelect:n},r.id))}),dx=({layoutId:e,versions:t,onRestore:n})=>{const[r,s]=v.useState(),o=l=>{s(l)},i=()=>{r!==void 0&&n(r)},a=r===void 0;return h.jsxs(I,{className:"space-y-4",role:"region","aria-label":"레이아웃 히스토리 패널",children:[h.jsx(I,{className:"border border-gray-300 dark:border-gray-600 rounded-lg p-4",children:h.jsx(Fu,{versions:t,selectedId:r,onSelect:o})}),h.jsx(I,{className:"flex justify-end",children:h.jsx(ae,{variant:"primary",disabled:a,onClick:i,"aria-label":"선택한 버전으로 복원",children:"복원"})})]})},ux=({emailLabel:e="Email",passwordLabel:t="Password",submitButtonText:n="Sign In",processingText:r="Processing...",emailPlaceholder:s="Email",passwordPlaceholder:o="Password",forgotPasswordText:i="Forgot your password?",forgotPasswordUrl:a="/admin/password/reset",emailRequiredError:l="Please enter your email.",emailInvalidError:c="Please enter a valid email address.",passwordRequiredError:d="Please enter your password.",onSubmit:u,loadingActions:f={},isLoading:g=!1,disabled:m=!1,apiError:p="",className:y="",style:b})=>{const[_,x]=v.useState(""),[M,C]=v.useState(""),[k,N]=v.useState({}),S=p,w=Object.values(f).some($=>$===!0),E=g||w,D=m||E,P=()=>{const $={},A=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return!_||_.trim()===""?$.email=l:A.test(_)||($.email=c),(!M||M.trim()==="")&&($.password=d),N($),Object.keys($).length===0},L=async $=>{if(!P()){$.preventDefault();return}N({}),u?await u($):$.preventDefault()};return h.jsx(I,{className:`w-full ${y}`,style:b,children:h.jsxs(Ga,{onSubmit:L,className:"space-y-6",noValidate:!0,children:[S&&h.jsx(I,{className:"p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800",children:h.jsx(vt,{className:"text-sm text-red-600 dark:text-red-400",children:S})}),h.jsxs(I,{children:[h.jsx(ln,{htmlFor:"email",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:e}),h.jsx(St,{id:"email",name:"email",type:"email",value:_,onChange:$=>x($.target.value),placeholder:s,disabled:D,className:`
w-full px-4 py-3 rounded-lg border
${k.email?"border-red-500 focus:border-red-500 focus:ring-red-500":"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"}
bg-white dark:bg-gray-800
@@ -128,7 +128,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
focus:outline-none focus:ring-2
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200
`}),k.email&&h.jsx(yt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:k.email})]}),h.jsxs(I,{children:[h.jsx(ln,{htmlFor:"password",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:t}),h.jsx(kt,{id:"password",name:"password",type:"password",value:M,onChange:$=>S($.target.value),placeholder:o,disabled:A,className:`
`}),k.email&&h.jsx(vt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:k.email})]}),h.jsxs(I,{children:[h.jsx(ln,{htmlFor:"password",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:t}),h.jsx(St,{id:"password",name:"password",type:"password",value:M,onChange:$=>C($.target.value),placeholder:o,disabled:D,className:`
w-full px-4 py-3 rounded-lg border
${k.password?"border-red-500 focus:border-red-500 focus:ring-red-500":"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"}
bg-white dark:bg-gray-800
@@ -137,7 +137,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
focus:outline-none focus:ring-2
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200
`}),k.password&&h.jsx(yt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:k.password})]}),a&&h.jsx(I,{className:"flex items-center justify-end",children:h.jsx(Wr,{href:a,className:"text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 transition-colors duration-200",children:i})}),h.jsx(ae,{type:"submit",disabled:A,className:`
`}),k.password&&h.jsx(vt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:k.password})]}),a&&h.jsx(I,{className:"flex items-center justify-end",children:h.jsx(Yr,{href:a,className:"text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 transition-colors duration-200",children:i})}),h.jsx(ae,{type:"submit",disabled:D,className:`
w-full px-4 py-3 rounded-lg font-medium
bg-blue-600 hover:bg-blue-700
dark:bg-blue-500 dark:hover:bg-blue-600
@@ -147,16 +147,16 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200
flex items-center justify-center
`,children:E?h.jsxs(h.Fragment,{children:[h.jsxs(Xn,{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[h.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),h.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),r]}):n})]})})},zu=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,bl=e=>{const t=zu(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},gx=()=>{const e=localStorage.getItem("g7_color_scheme");return e&&["auto","light","dark"].includes(e)?e:"auto"},px=({onThemeChange:e,className:t="",autoText:n="System",lightText:r="Light",darkText:s="Dark",id:o,editorAttrs:i})=>{const a=gx(),[l,c]=v.useState(a),[d,u]=v.useState(!1),f=v.useRef(null);v.useEffect(()=>{bl(l)},[]),v.useEffect(()=>{if(l!=="auto")return;const p=window.matchMedia("(prefers-color-scheme: dark)"),y=()=>{bl("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),bl(p),u(!1),e?.(p)},m=()=>zu(l)==="dark"?ue.Moon:ue.Sun;return h.jsxs(I,{ref:f,className:`relative ${t}`,id:o,...i,children:[h.jsx(ae,{onClick:()=>u(!d),className:"w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors","aria-label":"Toggle theme",children:h.jsx(oe,{name:m(),className:"text-sm text-slate-500 dark:text-slate-400"})}),d&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsxs(I,{className:"py-2",children:[h.jsxs(ae,{onClick:()=>g("auto"),className:`
`,children:E?h.jsxs(h.Fragment,{children:[h.jsxs(Zn,{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[h.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),h.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),r]}):n})]})})},zu=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,yl=e=>{const t=zu(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},fx=()=>{const e=localStorage.getItem("g7_color_scheme");return e&&["auto","light","dark"].includes(e)?e:"auto"},hx=({onThemeChange:e,className:t="",autoText:n="System",lightText:r="Light",darkText:s="Dark",id:o,editorAttrs:i})=>{const a=fx(),[l,c]=v.useState(a),[d,u]=v.useState(!1),f=v.useRef(null);v.useEffect(()=>{yl(l)},[]),v.useEffect(()=>{if(l!=="auto")return;const p=window.matchMedia("(prefers-color-scheme: dark)"),y=()=>{yl("auto")};return p.addEventListener("change",y),()=>p.removeEventListener("change",y)},[l]),v.useEffect(()=>{const p=y=>{f.current&&!f.current.contains(y.target)&&u(!1)};return document.addEventListener("mousedown",p),()=>document.removeEventListener("mousedown",p)},[]);const g=p=>{c(p),localStorage.setItem("g7_color_scheme",p),yl(p),u(!1),e?.(p)},m=()=>zu(l)==="dark"?de.Moon:de.Sun;return h.jsxs(I,{ref:f,className:`relative ${t}`,id:o,...i,children:[h.jsx(ae,{onClick:()=>u(!d),className:"w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors","aria-label":"Toggle theme",children:h.jsx(oe,{name:m(),className:"text-sm text-slate-500 dark:text-slate-400"})}),d&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsxs(I,{className:"py-2",children:[h.jsxs(ae,{onClick:()=>g("auto"),className:`
w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors
${l==="auto"?"bg-gray-50 dark:bg-gray-700":""}
`,children:[h.jsx(oe,{name:ue.Settings,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(J,{className:"flex-1 text-left text-gray-900 dark:text-white",children:n}),l==="auto"&&h.jsx(oe,{name:ue.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(ae,{onClick:()=>g("light"),className:`
`,children:[h.jsx(oe,{name:de.Settings,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:n}),l==="auto"&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(ae,{onClick:()=>g("light"),className:`
w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors
${l==="light"?"bg-gray-50 dark:bg-gray-700":""}
`,children:[h.jsx(oe,{name:ue.Sun,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(J,{className:"flex-1 text-left text-gray-900 dark:text-white",children:r}),l==="light"&&h.jsx(oe,{name:ue.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(ae,{onClick:()=>g("dark"),className:`
`,children:[h.jsx(oe,{name:de.Sun,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:r}),l==="light"&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(ae,{onClick:()=>g("dark"),className:`
w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors
${l==="dark"?"bg-gray-50 dark:bg-gray-700":""}
`,children:[h.jsx(oe,{name:ue.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(J,{className:"flex-1 text-left text-gray-900 dark:text-white",children:s}),l==="dark"&&h.jsx(oe,{name:ue.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Vu=window.G7Core?.createLogger?.("Comp:ColumnSelector")??{log:(...e)=>console.log("[Comp:ColumnSelector]",...e),warn:(...e)=>console.warn("[Comp:ColumnSelector]",...e),error:(...e)=>console.error("[Comp:ColumnSelector]",...e)},Hu=(e,t)=>window.G7Core?.t?.(e,t)??e,mx=()=>window.G7Core,bx=({id:e,columns:t,visibleColumns:n,onColumnVisibilityChange:r,triggerLabel:s,triggerIconName:o=ue.Sliders,position:i="right",className:a="",style:l,requiredText:c,menuTitle:d,menuDescription:u,defaultColumns:f,triggerClassName:g,labelClassName:m,dynamicColumns:p,dynamicColumnsInsertAfter:y,editorAttrs:b})=>{const _=s??Hu("common.column_selector"),x=c??Hu("common.required"),M=v.useMemo(()=>{if(!p||!Array.isArray(p)||p.length===0)return t;if(y){const V=t.findIndex(X=>X.field===y);if(V!==-1)return[...t.slice(0,V+1),...p,...t.slice(V+1)]}return[...t,...p]},[t,p,y]),[S,k]=v.useState(!1),N=v.useRef(null),C=v.useRef(null),[w,E]=v.useState(!1),[A,P]=v.useState(i),L=v.useMemo(()=>{try{const V=window.G7Core?.AuthManager?.getInstance();return V?V.getUser()?.uuid:void 0}catch{return}},[]),$=e?`g7_columns_${e}${L!==void 0?`_${L}`:""}`:null,D=()=>{if(!$)return null;try{const V=localStorage.getItem($);if(V){const X=JSON.parse(V);if(Array.isArray(X)&&X.length>0)return X}}catch(V){Vu.warn("Failed to load from localStorage:",V)}return null},O=V=>{if($)try{localStorage.setItem($,JSON.stringify(V))}catch(X){Vu.warn("Failed to save to localStorage:",X)}},T=()=>{const V=D();return V||(n&&n.length>0?n:f&&f.length>0?f:M.filter(X=>X.visible!==!1).map(X=>X.field))},[z,U]=v.useState(T);v.useEffect(()=>{w||(r&&z.length>0&&r(z),E(!0))},[w,r,z]),v.useEffect(()=>{w&&n&&n.length>0&&(D()||U(n))},[n,w]);const W=v.useRef([]),G=v.useRef(null);v.useEffect(()=>{if(w&&p&&p.length>0){const V=p.map(q=>q.field);if(G.current===null){const q=D();if(G.current=q!==null,q!==null){W.current=V;return}}const X=W.current,K=V.filter(q=>!X.includes(q));if(K.length>0){const q=n&&n.length>0?n:z,ne=K.filter(ee=>!q.includes(ee));if(ne.length>0){const ee=[...q,...ne];U(ee),O(ee),r?.(ee)}}W.current=V}},[p,w]);const B=n&&n.length>0?n:z;v.useEffect(()=>{const V=X=>{N.current&&!N.current.contains(X.target)&&k(!1)};return S&&document.addEventListener("mousedown",V),()=>{document.removeEventListener("mousedown",V)}},[S]),v.useEffect(()=>{if(S&&C.current&&N.current){const V=C.current.getBoundingClientRect(),X=N.current.getBoundingClientRect(),K=window.innerWidth,q=V.width||256;i==="right"?X.right-q<0?P("left"):P("right"):X.left+q>K?P("right"):P("left")}},[S,i]);const j=V=>{if(M.find(q=>q.field===V)?.required)return;const K=B.includes(V)?B.filter(q=>q!==V):[...B,V];U(K),O(K),r?.(K)},Y=A==="left"?"left-0":"right-0";return h.jsxs(I,{ref:N,className:`relative inline-block ${a}`,style:l,id:e,...b,children:[h.jsxs(ae,{onClick:()=>k(!S),className:mx()?.style?.mergeClasses?.("flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",g)??`flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors ${g??""}`,children:[h.jsx(oe,{name:o,className:"w-5 h-5"}),_&&h.jsx(J,{className:`${m||"hidden sm:inline"} text-sm`,children:_})]}),S&&h.jsxs(I,{ref:C,className:`absolute ${Y} z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg`,children:[(d||u)&&h.jsxs(I,{className:"px-4 pt-3 pb-2 border-b border-gray-200 dark:border-gray-700",children:[d&&h.jsx(J,{className:"block text-sm font-medium text-gray-900 dark:text-white",children:d}),u&&h.jsx(J,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-1",children:u})]}),h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:M.map(V=>{const X=B.includes(V.field),K=V.required===!0;return h.jsxs(I,{onClick:()=>!K&&j(V.field),className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${K?"cursor-default":"cursor-pointer"}`,children:[h.jsx(_n,{checked:X,disabled:K,readOnly:!0,className:"h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 pointer-events-none"}),h.jsx(J,{className:"text-sm text-gray-700 dark:text-gray-300",children:V.header}),K&&x&&h.jsx(J,{className:"ml-auto text-xs text-gray-400 dark:text-gray-500",children:x})]},V.field)})})]})]})},yx=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...e)=>console.log("[Comp:PageTransition]",...e),warn:(...e)=>console.warn("[Comp:PageTransition]",...e),error:(...e)=>console.error("[Comp:PageTransition]",...e)},vx=({className:e="",style:t})=>{const[n,r]=v.useState(!1);return v.useEffect(()=>{c Line truncated
`,children:[h.jsx(oe,{name:de.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:s}),l==="dark"&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Vu=window.G7Core?.createLogger?.("Comp:ColumnSelector")??{log:(...e)=>console.log("[Comp:ColumnSelector]",...e),warn:(...e)=>console.warn("[Comp:ColumnSelector]",...e),error:(...e)=>console.error("[Comp:ColumnSelector]",...e)},Hu=(e,t)=>window.G7Core?.t?.(e,t)??e,gx=()=>window.G7Core,px=({id:e,columns:t,visibleColumns:n,onColumnVisibilityChange:r,triggerLabel:s,triggerIconName:o=de.Sliders,position:i="right",className:a="",style:l,requiredText:c,menuTitle:d,menuDescription:u,defaultColumns:f,triggerClassName:g,labelClassName:m,dynamicColumns:p,dynamicColumnsInsertAfter:y,editorAttrs:b})=>{const _=s??Hu("common.column_selector"),x=c??Hu("common.required"),M=v.useMemo(()=>{if(!p||!Array.isArray(p)||p.length===0)return t;if(y){const V=t.findIndex(X=>X.field===y);if(V!==-1)return[...t.slice(0,V+1),...p,...t.slice(V+1)]}return[...t,...p]},[t,p,y]),[C,k]=v.useState(!1),N=v.useRef(null),S=v.useRef(null),[w,E]=v.useState(!1),[D,P]=v.useState(i),L=v.useMemo(()=>{try{const V=window.G7Core?.AuthManager?.getInstance();return V?V.getUser()?.uuid:void 0}catch{return}},[]),$=e?`g7_columns_${e}${L!==void 0?`_${L}`:""}`:null,A=()=>{if(!$)return null;try{const V=localStorage.getItem($);if(V){const X=JSON.parse(V);if(Array.isArray(X)&&X.length>0)return X}}catch(V){Vu.warn("Failed to load from localStorage:",V)}return null},O=V=>{if($)try{localStorage.setItem($,JSON.stringify(V))}catch(X){Vu.warn("Failed to save to localStorage:",X)}},T=()=>{const V=A();return V||(n&&n.length>0?n:f&&f.length>0?f:M.filter(X=>X.visible!==!1).map(X=>X.field))},[z,H]=v.useState(T);v.useEffect(()=>{w||(r&&z.length>0&&r(z),E(!0))},[w,r,z]),v.useEffect(()=>{w&&n&&n.length>0&&(A()||H(n))},[n,w]);const j=v.useRef([]),G=v.useRef(null);v.useEffect(()=>{if(w&&p&&p.length>0){const V=p.map(q=>q.field);if(G.current===null){const q=A();if(G.current=q!==null,q!==null){j.current=V;return}}const X=j.current,K=V.filter(q=>!X.includes(q));if(K.length>0){const q=n&&n.length>0?n:z,ne=K.filter(ee=>!q.includes(ee));if(ne.length>0){const ee=[...q,...ne];H(ee),O(ee),r?.(ee)}}j.current=V}},[p,w]);const U=n&&n.length>0?n:z;v.useEffect(()=>{const V=X=>{N.current&&!N.current.contains(X.target)&&k(!1)};return C&&document.addEventListener("mousedown",V),()=>{document.removeEventListener("mousedown",V)}},[C]),v.useEffect(()=>{if(C&&S.current&&N.current){const V=S.current.getBoundingClientRect(),X=N.current.getBoundingClientRect(),K=window.innerWidth,q=V.width||256;i==="right"?X.right-q<0?P("left"):P("right"):X.left+q>K?P("right"):P("left")}},[C,i]);const W=V=>{if(M.find(q=>q.field===V)?.required)return;const K=U.includes(V)?U.filter(q=>q!==V):[...U,V];H(K),O(K),r?.(K)},Y=D==="left"?"left-0":"right-0";return h.jsxs(I,{ref:N,className:`relative inline-block ${a}`,style:l,id:e,...b,children:[h.jsxs(ae,{onClick:()=>k(!C),className:gx()?.style?.mergeClasses?.("flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",g)??`flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors ${g??""}`,children:[h.jsx(oe,{name:o,className:"w-5 h-5"}),_&&h.jsx(Q,{className:`${m||"hidden sm:inline"} text-sm`,children:_})]}),C&&h.jsxs(I,{ref:S,className:`absolute ${Y} z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg`,children:[(d||u)&&h.jsxs(I,{className:"px-4 pt-3 pb-2 border-b border-gray-200 dark:border-gray-700",children:[d&&h.jsx(Q,{className:"block text-sm font-medium text-gray-900 dark:text-white",children:d}),u&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-1",children:u})]}),h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:M.map(V=>{const X=U.includes(V.field),K=V.required===!0;return h.jsxs(I,{onClick:()=>!K&&W(V.field),className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${K?"cursor-default":"cursor-pointer"}`,children:[h.jsx(En,{checked:X,disabled:K,readOnly:!0,className:"h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 pointer-events-none"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:V.header}),K&&x&&h.jsx(Q,{className:"ml-auto text-xs text-gray-400 dark:text-gray-500",children:x})]},V.field)})})]})]})},mx=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...e)=>console.log("[Comp:PageTransition]",...e),warn:(...e)=>console.warn("[Comp:PageTransition]",...e),error:(...e)=>console.error("[Comp:PageTransition]",...e)},bx=({className:e="",style:t})=>{const[n,r]=v.useState(!1);return v.useEffect(()=>{c Line truncated
@keyframes loading {
0% {
transform: translateX(-100%);
@@ -168,12 +168,12 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
transform: translateX(-100%);
}
}
`})]}):null},xx=(e,t)=>window.G7Core?.t?.(e,t)??e,wx=({options:e})=>{const t=document.documentElement.classList.contains("dark"),n=t?"rgb(17,24,39)":"rgb(249,250,251)",r=t?"#6b7280":"#9ca3af",s=t?"#9ca3af":"#6b7280";return h.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:n,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[h.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${r}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),h.jsx("span",{style:{color:s,fontSize:"14px"},children:e?.text||xx("nav.loading")})]})};function kr(e){"@babel/helpers - typeof";return kr=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},kr(e)}function kx(e,t){if(kr(e)!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(kr(r)!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function Uu(e){var t=kx(e,"string");return kr(t)=="symbol"?t:t+""}function js(e,t,n){return(t=Uu(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function Bu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Se(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?Bu(Object(n),!0).forEach(function(r){js(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Bu(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function Cx(e){if(Array.isArray(e))return e}function Sx(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t===0){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function yl(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function ju(e,t){if(e){if(typeof e=="string")return yl(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?yl(e,t):void 0}}function _x(){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 Kn(e,t){return Cx(e)||Sx(e,t)||ju(e,t)||_x()}function Ex(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}function Fn(e,t){if(e==null)return{};var n,r,s=Ex(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)===-1&&{}.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var Mx=["defaultInputValue","defaultMenuIsOpen","defaultValue","inputValue","menuIsOpen","onChange","onInputChange","onMenuClose","onMenuOpen","value"];function Wu(e){var t=e.defaultInputValue,n=t===void 0?"":t,r=e.defaultMenuIsOpen,s=r===void 0?!1:r,o=e.defaultValue,i=o===void 0?null:o,a=e.inputValue,l=e.menuIsOpen,c=e.onChange,d=e.onInputChange,u=e.onMenuClose,f=e.onMenuOpen,g=e.value,m=Fn(e,Mx),p=v.useState(a!==void 0?a:n),y=Kn(p,2),b=y[0],_=y[1],x=v.useState(l!==void 0?l:s),M=Kn(x,2),S=M[0],k=M[1],N=v.useState(g!==void 0?g:i),C=Kn(N,2),w=C[0],E=C[1],A=v.useCallback(function(z,U){typeof c=="function"&&c(z,U),E(z)},[c]),P=v.useCallback(function(z,U){var W;typeof d=="function"&&(W=d(z,U)),_(W!==void 0?W:z)},[d]),L=v.useCallback(function(){typeof f=="function"&&f(),k(!0)},[f]),$=v.useCallback(function(){typeof u=="function"&&u(),k(!1)},[u]),D=a!==void 0?a:b,O=l!==void 0?l:S,T=g!==void 0?g:w;return Se(Se({},m),{},{inputValue:D,menuIsOpen:O,onChange:A,onInputChange:P,onMenuClose:$,onMenuOpen:L,value:T})}function Oe(){return Oe=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)({}).hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},Oe.apply(null,arguments)}function Nx(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Gu(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,Uu(r.key),r)}}function Tx(e,t,n){return t&&Gu(e.prototype,t),n&&Gu(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function vl(e,t){return vl=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,r){return n.__proto__=r,n},vl(e,t)}function Ox(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&vl(e,t)}function ni(e){return ni=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},ni(e)}function Yu(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(Yu=function(){return!!e})()}function Ax(e){if(e===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function Dx(e,t){if(t&&(kr(t)=="object"||typeof t=="function"))return t;if(t!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return Ax(e)}function Ix(e){var t=Yu();return function(){var n,r=ni(e);if(t){var s=ni(this).constructor;n=Reflect.construct(r,arguments,s)}else n=r.apply(this,arguments);return Dx(this,n)}}function Lx(e){if(Array.isArray(e))return yl(e)}function Px(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function $x(){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 ts(e){return Lx(e)||Px(e)||ju(e)||$x()}function Fx(e){if(e.sheet)return e.sheet;for(var t=0;t<document.styleSheets.length;t++)if(document.styleSheets[t].ownerNode===e)return document.styleSheets[t]}function zx(e){var t=document.createElement("style");return t.setAttribute("data-emotion",e.key),e.nonce!==void 0&&t.setAttribute("nonce",e.nonce),t.appendChild(document.createTextNode("")),t.setAttribute("data-s",""),t}var Vx=(function(){function e(n){var r=this;this._insertTag=function(s){var o;r.tags.length===0?r.insertionPoint?o=r.insertionPoint.nextSibling:r.prepend?o=r.container.firstChild:o=r.before:o=r.tags[r.tags.length-1].nextSibling,r.container.insertBefore(s,o),r.tags.push(s)},this.isSpeedy=n.speedy===void 0?!0:n.speedy,this.tags=[],this.ctr=0,this.nonce=n.nonce,this.key=n.key,this.container=n.container,this.prepend=n.prepend,this.insertionPoint=n.insertionPoint,this.before=null}var t=e.prototype;return t.hydrate=function(r){r.forEach(this._insertTag)},t.insert=function(r){this.ctr%(this.isSpeedy?65e3:1)===0&&this._insertTag(zx(this));var s=this.tags[this.tags.length-1];if(this.isSpeedy){var o=Fx(s);try{o.insertRule(r,o.cssRules.length)}catch{}}else s.appendChild(document.createTextNode(r));this.ctr++},t.flush=function(){this.tags.forEach(function(r){var s;return(s=r.parentNode)==null?void 0:s.removeChild(r)}),this.tags=[],this.ctr=0},e})(),Ht="-ms-",ri="-moz-",Re="-webkit-",Xu="comm",xl="rule",wl="decl",Hx="@import",Ku="@keyframes",Ux="@layer",Bx=Math.abs,si=String.fromCharCode,jx=Object.assign;function Wx(e,t){return Ft(e,0)^45?(((t<<2^Ft(e,0))<<2^Ft(e,1))<<2^Ft(e,2))<<2^Ft(e,3):0}function qu(e){return e.trim()}function Gx(e,t){return(e=t.exec(e))?e[0]:e}function et(e,t,n){return e.replace(t,n)}function kl(e,t){return e.indexOf(t)}function Ft(e,t){return e.charCodeAt(t)|0}function Ws(e,t,n){return e.slice(t,n)}function zn(e){return e.length}function Cl(e){return e.length}function oi(e,t){return t.push(e),e}function Yx(e,t){return e.map(t).join("")}var ii=1,ns=1,Zu=0,Rt=0,St=0,rs="";function ai(e,t,n,r,s,o,i){return{value:e,root:t,parent:n,type:r,props:s,children:o,line:ii,column:ns,length:i,return:""}}function Gs(e,t){return jx(ai("",null,null,"",null,null,0),e,{length:-e.length},t)}function Xx(){return St}function Kx(){return St=Rt>0?Ft(rs,--Rt):0,ns--,St===10&&(ns=1,ii--),St}function dn(){return St=Rt<Zu?Ft(rs,Rt++):0,ns++,St===10&&(ns=1,ii++),St}function Vn(){return Ft(rs,Rt)}function li(){return Rt}function Ys(e,t){return Ws(rs,e,t)}function Xs(e){switch(e){case 0:case 9:case 10:case 13:case 32:return 5;case 33:case 43:case 44:case 47:case 62:case 64:case 126:case 59:case 123:case 125:return 4;case 58:return 3;case 34:case 39:case 40:case 91:return 2;case 41:case 93:return 1}return 0}function Qu(e){return ii=ns=1,Zu=zn(rs=e),Rt=0,[]}function Ju(e){return rs="",e}function ci(e){return qu(Ys(Rt-1,Sl(e===91?e+2:e===40?e+1:e)))}function qx(e){for(;(St=Vn())&&St<33;)dn();return Xs(e)>2||Xs(St)>3?"":" "}function Zx(e,t){for(;--t&&dn()&&!(St<48||St>102||St>57&&St<65||St>70&&St<97););return Ys(e,li()+(t<6&&Vn()==32&&dn()==32))}function Sl(e){for(;dn();)switch(St){case e:return Rt;case 34:case 39:e!==34&&e!==39&&Sl(St);break;case 40:e===41&&Sl(e);break;case 92:dn();break}return Rt}function Qx(e,t){for(;dn()&&e+St!==57;)if(e+St===84&&Vn()===47)break;return"/*"+Ys(t,Rt-1)+"*"+si(e===47?e:dn())}function Jx(e){for(;!Xs(Vn());)dn();return Ys(e,Rt)}function Rx(e){return Ju(di("",null,null,null,[""],e=Qu(e),0,[0],e))}function di(e,t,n,r,s,o,i,a,l){for(var c=0,d=0,u=i,f=0,g=0,m=0,p=1,y=1,b=1,_=0,x="",M=s,S=o,k=r,N=x;y;)switch(m=_,_=dn()){case 40:if(m!=108&&Ft(N,u-1)==58){kl(N+=et(ci(_),"&","&\f"),"&\f")!=-1&&(b=-1);break}case 34:case 39:case 91:N+=ci(_);break;case 9:case 10:case 13:case 32:N+=qx(m);break;case 92:N+=Zx(li()-1,7);continue;case 47:switch(Vn()){case 42:case 47:oi(ew(Qx(dn(),li()),t,n),l);break;default:N+="/"}break;case 123*p:a[c++]=zn(N)*b;case 125*p:case 59:case 0:switch(_){case 0:case 125:y=0;case 59+d:b==-1&&(N=et(N,/\f/g,"")),g>0&&zn(N)-u&&oi(g>32?ef(N+";",r,n,u-1):ef(et(N," ","")+";",r,n,u-2),l);break;case 59:N+=";";default:if(oi(k=Ru(N,t,n,c,d,s,a,x,M=[],S=[],u),o),_===123)if(d===0)di(N,t,k,k,M,o,u,a,S);else switch(f===99&&Ft(N,3)===110?100:f){case 100:case 108:case 109:case 115:di(e,k,k,r&&oi(Ru(e,k,k,0,0,s,a,x,s,M=[],u),S),s,S,u,a,r?M:S);break;default:di(N,k,k,k,[""],S,0,a,S)}}c=d=g=0,p=b=1,x=N="",u=i;break;case 58:u=1+zn(N),g=m;default:if(p<1){if(_==123)--p;else if(_==125&&p++==0&&Kx()==125)continue}switch(N+=si(_),_*p){case 38:b=d>0?1:(N+="\f",-1);break;case 44:a[c++]=(zn(N)-1)*b,b=1;break;case 64:Vn()===45&&(N+=ci(dn())),f=Vn(),d=u=zn(x=N+=Jx(li())),_++;break;case 45:m===45&&zn(N)==2&&(p=0)}}return o}function Ru(e,t,n,r,s,o,i,a,l,c,d){for(var u=s-1,f=s===0?o:[""],g=Cl(f),m=0,p=0,y=0;m<r;++m)for(var b=0,_=Ws(e,u+1,u=Bx(p=i[m])),x=e;b<g;++b)(x=qu(p>0?f[b]+" "+_:et(_,/&\f/g,f[b])))&&(l[y++]=x);retu Line truncated
`})]}):null},yx=(e,t)=>window.G7Core?.t?.(e,t)??e,vx=({options:e})=>{const t=document.documentElement.classList.contains("dark"),n=t?"rgb(17,24,39)":"rgb(249,250,251)",r=t?"#6b7280":"#9ca3af",s=t?"#9ca3af":"#6b7280";return h.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:n,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[h.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${r}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),h.jsx("span",{style:{color:s,fontSize:"14px"},children:e?.text||yx("nav.loading")})]})};function Cr(e){"@babel/helpers - typeof";return Cr=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Cr(e)}function xx(e,t){if(Cr(e)!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(Cr(r)!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function Uu(e){var t=xx(e,"string");return Cr(t)=="symbol"?t:t+""}function Ws(e,t,n){return(t=Uu(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function Bu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Ee(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?Bu(Object(n),!0).forEach(function(r){Ws(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Bu(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function wx(e){if(Array.isArray(e))return e}function kx(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t===0){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function vl(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function ju(e,t){if(e){if(typeof e=="string")return vl(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?vl(e,t):void 0}}function Cx(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Qn(e,t){return wx(e)||kx(e,t)||ju(e,t)||Cx()}function Sx(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}function Vn(e,t){if(e==null)return{};var n,r,s=Sx(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)===-1&&{}.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var _x=["defaultInputValue","defaultMenuIsOpen","defaultValue","inputValue","menuIsOpen","onChange","onInputChange","onMenuClose","onMenuOpen","value"];function Wu(e){var t=e.defaultInputValue,n=t===void 0?"":t,r=e.defaultMenuIsOpen,s=r===void 0?!1:r,o=e.defaultValue,i=o===void 0?null:o,a=e.inputValue,l=e.menuIsOpen,c=e.onChange,d=e.onInputChange,u=e.onMenuClose,f=e.onMenuOpen,g=e.value,m=Vn(e,_x),p=v.useState(a!==void 0?a:n),y=Qn(p,2),b=y[0],_=y[1],x=v.useState(l!==void 0?l:s),M=Qn(x,2),C=M[0],k=M[1],N=v.useState(g!==void 0?g:i),S=Qn(N,2),w=S[0],E=S[1],D=v.useCallback(function(z,H){typeof c=="function"&&c(z,H),E(z)},[c]),P=v.useCallback(function(z,H){var j;typeof d=="function"&&(j=d(z,H)),_(j!==void 0?j:z)},[d]),L=v.useCallback(function(){typeof f=="function"&&f(),k(!0)},[f]),$=v.useCallback(function(){typeof u=="function"&&u(),k(!1)},[u]),A=a!==void 0?a:b,O=l!==void 0?l:C,T=g!==void 0?g:w;return Ee(Ee({},m),{},{inputValue:A,menuIsOpen:O,onChange:D,onInputChange:P,onMenuClose:$,onMenuOpen:L,value:T})}function Oe(){return Oe=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)({}).hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},Oe.apply(null,arguments)}function Ex(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Gu(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,Uu(r.key),r)}}function Mx(e,t,n){return t&&Gu(e.prototype,t),n&&Gu(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function xl(e,t){return xl=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,r){return n.__proto__=r,n},xl(e,t)}function Nx(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&xl(e,t)}function ri(e){return ri=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},ri(e)}function Yu(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(Yu=function(){return!!e})()}function Tx(e){if(e===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function Ox(e,t){if(t&&(Cr(t)=="object"||typeof t=="function"))return t;if(t!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return Tx(e)}function Ax(e){var t=Yu();return function(){var n,r=ri(e);if(t){var s=ri(this).constructor;n=Reflect.construct(r,arguments,s)}else n=r.apply(this,arguments);return Ox(this,n)}}function Dx(e){if(Array.isArray(e))return vl(e)}function Ix(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Lx(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function ns(e){return Dx(e)||Ix(e)||ju(e)||Lx()}function Px(e){if(e.sheet)return e.sheet;for(var t=0;t<document.styleSheets.length;t++)if(document.styleSheets[t].ownerNode===e)return document.styleSheets[t]}function $x(e){var t=document.createElement("style");return t.setAttribute("data-emotion",e.key),e.nonce!==void 0&&t.setAttribute("nonce",e.nonce),t.appendChild(document.createTextNode("")),t.setAttribute("data-s",""),t}var Fx=(function(){function e(n){var r=this;this._insertTag=function(s){var o;r.tags.length===0?r.insertionPoint?o=r.insertionPoint.nextSibling:r.prepend?o=r.container.firstChild:o=r.before:o=r.tags[r.tags.length-1].nextSibling,r.container.insertBefore(s,o),r.tags.push(s)},this.isSpeedy=n.speedy===void 0?!0:n.speedy,this.tags=[],this.ctr=0,this.nonce=n.nonce,this.key=n.key,this.container=n.container,this.prepend=n.prepend,this.insertionPoint=n.insertionPoint,this.before=null}var t=e.prototype;return t.hydrate=function(r){r.forEach(this._insertTag)},t.insert=function(r){this.ctr%(this.isSpeedy?65e3:1)===0&&this._insertTag($x(this));var s=this.tags[this.tags.length-1];if(this.isSpeedy){var o=Px(s);try{o.insertRule(r,o.cssRules.length)}catch{}}else s.appendChild(document.createTextNode(r));this.ctr++},t.flush=function(){this.tags.forEach(function(r){var s;return(s=r.parentNode)==null?void 0:s.removeChild(r)}),this.tags=[],this.ctr=0},e})(),jt="-ms-",si="-moz-",nt="-webkit-",Xu="comm",wl="rule",kl="decl",zx="@import",Ku="@keyframes",Vx="@layer",Hx=Math.abs,oi=String.fromCharCode,Ux=Object.assign;function Bx(e,t){return Vt(e,0)^45?(((t<<2^Vt(e,0))<<2^Vt(e,1))<<2^Vt(e,2))<<2^Vt(e,3):0}function qu(e){return e.trim()}function jx(e,t){return(e=t.exec(e))?e[0]:e}function rt(e,t,n){return e.replace(t,n)}function Cl(e,t){return e.indexOf(t)}function Vt(e,t){return e.charCodeAt(t)|0}function Gs(e,t,n){return e.slice(t,n)}function Hn(e){return e.length}function Sl(e){return e.length}function ii(e,t){return t.push(e),e}function Wx(e,t){return e.map(t).join("")}var ai=1,rs=1,Zu=0,tn=0,Et=0,ss="";function li(e,t,n,r,s,o,i){return{value:e,root:t,parent:n,type:r,props:s,children:o,line:ai,column:rs,length:i,return:""}}function Ys(e,t){return Ux(li("",null,null,"",null,null,0),e,{length:-e.length},t)}function Gx(){return Et}function Yx(){return Et=tn>0?Vt(ss,--tn):0,rs--,Et===10&&(rs=1,ai--),Et}function dn(){return Et=tn<Zu?Vt(ss,tn++):0,rs++,Et===10&&(rs=1,ai++),Et}function Un(){return Vt(ss,tn)}function ci(){return tn}function Xs(e,t){return Gs(ss,e,t)}function Ks(e){switch(e){case 0:case 9:case 10:case 13:case 32:return 5;case 33:case 43:case 44:case 47:case 62:case 64:case 126:case 59:case 123:case 125:return 4;case 58:return 3;case 34:case 39:case 40:case 91:return 2;case 41:case 93:return 1}return 0}function Qu(e){return ai=rs=1,Zu=Hn(ss=e),tn=0,[]}function Ju(e){return ss="",e}function di(e){return qu(Xs(tn-1,_l(e===91?e+2:e===40?e+1:e)))}function Xx(e){for(;(Et=Un())&&Et<33;)dn();return Ks(e)>2||Ks(Et)>3?"":" "}function Kx(e,t){for(;--t&&dn()&&!(Et<48||Et>102||Et>57&&Et<65||Et>70&&Et<97););return Xs(e,ci()+(t<6&&Un()==32&&dn()==32))}function _l(e){for(;dn();)switch(Et){case e:return tn;case 34:case 39:e!==34&&e!==39&&_l(Et);break;case 40:e===41&&_l(e);break;case 92:dn();break}return tn}function qx(e,t){for(;dn()&&e+Et!==57;)if(e+Et===84&&Un()===47)break;return"/*"+Xs(t,tn-1)+"*"+oi(e===47?e:dn())}function Zx(e){for(;!Ks(Un());)dn();return Xs(e,tn)}function Qx(e){return Ju(ui("",null,null,null,[""],e=Qu(e),0,[0],e))}function ui(e,t,n,r,s,o,i,a,l){for(var c=0,d=0,u=i,f=0,g=0,m=0,p=1,y=1,b=1,_=0,x="",M=s,C=o,k=r,N=x;y;)switch(m=_,_=dn()){case 40:if(m!=108&&Vt(N,u-1)==58){Cl(N+=rt(di(_),"&","&\f"),"&\f")!=-1&&(b=-1);break}case 34:case 39:case 91:N+=di(_);break;case 9:case 10:case 13:case 32:N+=Xx(m);break;case 92:N+=Kx(ci()-1,7);continue;case 47:switch(Un()){case 42:case 47:ii(Jx(qx(dn(),ci()),t,n),l);break;default:N+="/"}break;case 123*p:a[c++]=Hn(N)*b;case 125*p:case 59:case 0:switch(_){case 0:case 125:y=0;case 59+d:b==-1&&(N=rt(N,/\f/g,"")),g>0&&Hn(N)-u&&ii(g>32?ef(N+";",r,n,u-1):ef(rt(N," ","")+";",r,n,u-2),l);break;case 59:N+=";";default:if(ii(k=Ru(N,t,n,c,d,s,a,x,M=[],C=[],u),o),_===123)if(d===0)ui(N,t,k,k,M,o,u,a,C);else switch(f===99&&Vt(N,3)===110?100:f){case 100:case 108:case 109:case 115:ui(e,k,k,r&&ii(Ru(e,k,k,0,0,s,a,x,s,M=[],u),C),s,C,u,a,r?M:C);break;default:ui(N,k,k,k,[""],C,0,a,C)}}c=d=g=0,p=b=1,x=N="",u=i;break;case 58:u=1+Hn(N),g=m;default:if(p<1){if(_==123)--p;else if(_==125&&p++==0&&Yx()==125)continue}switch(N+=oi(_),_*p){case 38:b=d>0?1:(N+="\f",-1);break;case 44:a[c++]=(Hn(N)-1)*b,b=1;break;case 64:Un()===45&&(N+=di(dn())),f=Un(),d=u=Hn(x=N+=Zx(ci())),_++;break;case 45:m===45&&Hn(N)==2&&(p=0)}}return o}function Ru(e,t,n,r,s,o,i,a,l,c,d){for(var u=s-1,f=s===0?o:[""],g=Sl(f),m=0,p=0,y=0;m<r;++m)for(var b=0,_=Gs(e,u+1,u=Hx(p=i[m])),x=e;b<g;++b)(x=qu(p>0?f[b]+" "+_:rt(_,/&\f/g,f[b])))&&(l[y++]=x);retu Line truncated
0%, 80%, 100% { opacity: 0; }
40% { opacity: 1; }
`]))),F1=function(t,n){var r=t.isFocused,s=t.size,o=t.theme,i=o.colors,a=o.spacing.baseUnit;return Se({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})},$l=function(t){var n=t.delay,r=t.offset;return Ce("span",{css:Ol({animation:"".concat($1," 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"},"","")})},z1=function(t){var n=t.innerProps,r=t.isRtl,s=t.size,o=s===void 0?4:s,i=Fn(t,N1);return Ce("div",Oe({},xt(Se(Se({},i),{},{innerProps:n,isRtl:r,size:o}),"loadingIndicator",{indicator:!0,"loading-indicator":!0}),n),Ce($l,{delay:0,offset:r}),Ce($l,{delay:160,offset:!0}),Ce($l,{delay:320,offset:!r}))},V1=function(t,n){var r=t.isDisabled,s=t.isFocused,o=t.theme,i=o.colors,a=o.borderRadius,l=o.spacing;return Se({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}})},H1=function(t){var n=t.children,r=t.isDisabled,s=t.isFocused,o=t.innerRef,i=t.innerProps,a=t.menuIsOpen;return Ce("div",Oe({ref:o},xt(t,"control",{control:!0,"control--is-disabled":r,"control--is-focused":s,"control--menu-is-open":a}),i,{"aria-disabled":r||void 0}),n)},U1=H1,B1=["data"],j1=function(t,n){var r=t.theme.spacing;return n?{}:{paddingBottom:r.baseUnit*2,paddingTop:r.baseUnit*2}},W1=function(t){var n=t.children,r=t.cx,s=t.getStyles,o=t.getClassNames,i=t.Heading,a=t.headingProps,l=t.innerProps,c=t.label,d=t.theme,u=t.selectProps;return Ce("div",Oe({},xt(t,"group",{group:!0}),l),Ce(i,Oe({},a,{selectProps:u,theme:d,getStyles:s,getClassNames:o,cx:r}),c),Ce("div",null,n))},G1=function(t,n){var r=t.theme,s=r.colors,o=r.spacing;return Se({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"})},Y1=function(t){var n=Cf(t);n.data;var r=Fn(n,B1);return Ce("div",Oe({},xt(t,"groupHeading",{"group-heading":!0}),r))},X1=W1,K1=["innerRef","isDisabled","isHidden","inputClassName"],q1=function(t,n){var r=t.isDisabled,s=t.value,o=t.theme,i=o.spacing,a=o.colors;return Se(Se({visibility:r?"hidden":"visible",transform:s?"translateZ(0)":""},Z1),n?{}:{margin:i.baseUnit/2,paddingBottom:i.baseUnit/2,paddingTop:i.baseUnit/2,color:a.neutral80})},Pf={gridArea:"1 / 2",font:"inherit",minWidth:"2px",border:0,margin:0,outline:0,padding:0},Z1={flex:"1 1 auto",display:"inline-grid",gridArea:"1 / 1 / 2 / 3",gridTemplateColumns:"0 min-content","&:after":Se({content:'attr(data-value) " "',visibility:"hidden",whiteSpace:"pre"},Pf)},Q1=function(t){return Se({label:"input",color:"inherit",background:0,opacity:t?0:1,width:"100%"},Pf)},J1=function(t){var n=t.cx,r=t.value,s=Cf(t),o=s.innerRef,i=s.isDisabled,a=s.isHidden,l=s.inputClassName,c=Fn(s,K1);return Ce("div",Oe({},xt(t,"input",{"input-container":!0}),{"data-value":r||""}),Ce("input",Oe({className:n({input:!0},l),ref:o,style:Q1(a),disabled:i},c)))},R1=J1,ek=function(t,n){var r=t.theme,s=r.spacing,o=r.borderRadius,i=r.colors;return Se({label:"multiValue",display:"flex",minWidth:0},n?{}:{backgroundColor:i.neutral10,borderRadius:o/2,margin:s.baseUnit/2})},tk=function(t,n){var r=t.theme,s=r.borderRadius,o=r.colors,i=t.cropWithEllipsis;return Se({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})},nk=function(t,n){var r=t.theme,s=r.spacing,o=r.borderRadius,i=r.colors,a=t.isFocused;return Se({alignItems:"center",display:"flex"},n?{}:{borderRadius:o/2,backgroundColor:a?i.dangerLight:void 0,paddingLeft:s.baseUnit,paddingRight:s.baseUnit,":hover":{backgroundColor:i.dangerLight,color:i.danger}})},$f=function(t){var n=t.children,r=t.innerProps;return Ce("div",r,n)},rk=$f,sk=$f;function ok(e){var t=e.children,n=e.innerProps;return Ce("div",Oe({role:"button"},n),t||Ce(Pl,{size:14}))}var ik=function(t){var n=t.children,r=t.components,s=t.data,o=t.innerProps,i=t.isDisabled,a=t.removeProps,l=t.selectProps,c=r.Container,d=r.Label,u=r.Remove;return Ce(c,{data:s,innerProps:Se(Se({},xt(t,"multiValue",{"multi-value":!0,"multi-value--is-disabled":i})),o),selectProps:l},Ce(d,{data:s,innerProps:Se({},xt(t,"multiValueLabel",{"multi-value__label":!0})),selectProps:l},n),Ce(u,{data:s,innerProps:Se(Se({},xt(t,"multiValueRemove",{"multi-value__remove":!0})),{},{"aria-label":"Remove ".concat(n||"option")},a),s Line truncated
`]))),P1=function(t,n){var r=t.isFocused,s=t.size,o=t.theme,i=o.colors,a=o.spacing.baseUnit;return Ee({label:"loadingIndicator",display:"flex",transition:"color 150ms",alignSelf:"center",fontSize:s,lineHeight:1,marginRight:s,textAlign:"center",verticalAlign:"middle"},n?{}:{color:r?i.neutral60:i.neutral20,padding:a*2})},Fl=function(t){var n=t.delay,r=t.offset;return Se("span",{css:Al({animation:"".concat(L1," 1s ease-in-out ").concat(n,"ms infinite;"),backgroundColor:"currentColor",borderRadius:"1em",display:"inline-block",marginLeft:r?"1em":void 0,height:"1em",verticalAlign:"top",width:"1em"},"","")})},$1=function(t){var n=t.innerProps,r=t.isRtl,s=t.size,o=s===void 0?4:s,i=Vn(t,E1);return Se("div",Oe({},wt(Ee(Ee({},i),{},{innerProps:n,isRtl:r,size:o}),"loadingIndicator",{indicator:!0,"loading-indicator":!0}),n),Se(Fl,{delay:0,offset:r}),Se(Fl,{delay:160,offset:!0}),Se(Fl,{delay:320,offset:!r}))},F1=function(t,n){var r=t.isDisabled,s=t.isFocused,o=t.theme,i=o.colors,a=o.borderRadius,l=o.spacing;return Ee({label:"control",alignItems:"center",cursor:"default",display:"flex",flexWrap:"wrap",justifyContent:"space-between",minHeight:l.controlHeight,outline:"0 !important",position:"relative",transition:"all 100ms"},n?{}:{backgroundColor:r?i.neutral5:i.neutral0,borderColor:r?i.neutral10:s?i.primary:i.neutral20,borderRadius:a,borderStyle:"solid",borderWidth:1,boxShadow:s?"0 0 0 1px ".concat(i.primary):void 0,"&:hover":{borderColor:s?i.primary:i.neutral30}})},z1=function(t){var n=t.children,r=t.isDisabled,s=t.isFocused,o=t.innerRef,i=t.innerProps,a=t.menuIsOpen;return Se("div",Oe({ref:o},wt(t,"control",{control:!0,"control--is-disabled":r,"control--is-focused":s,"control--menu-is-open":a}),i,{"aria-disabled":r||void 0}),n)},V1=z1,H1=["data"],U1=function(t,n){var r=t.theme.spacing;return n?{}:{paddingBottom:r.baseUnit*2,paddingTop:r.baseUnit*2}},B1=function(t){var n=t.children,r=t.cx,s=t.getStyles,o=t.getClassNames,i=t.Heading,a=t.headingProps,l=t.innerProps,c=t.label,d=t.theme,u=t.selectProps;return Se("div",Oe({},wt(t,"group",{group:!0}),l),Se(i,Oe({},a,{selectProps:u,theme:d,getStyles:s,getClassNames:o,cx:r}),c),Se("div",null,n))},j1=function(t,n){var r=t.theme,s=r.colors,o=r.spacing;return Ee({label:"group",cursor:"default",display:"block"},n?{}:{color:s.neutral40,fontSize:"75%",fontWeight:500,marginBottom:"0.25em",paddingLeft:o.baseUnit*3,paddingRight:o.baseUnit*3,textTransform:"uppercase"})},W1=function(t){var n=Cf(t);n.data;var r=Vn(n,H1);return Se("div",Oe({},wt(t,"groupHeading",{"group-heading":!0}),r))},G1=B1,Y1=["innerRef","isDisabled","isHidden","inputClassName"],X1=function(t,n){var r=t.isDisabled,s=t.value,o=t.theme,i=o.spacing,a=o.colors;return Ee(Ee({visibility:r?"hidden":"visible",transform:s?"translateZ(0)":""},K1),n?{}:{margin:i.baseUnit/2,paddingBottom:i.baseUnit/2,paddingTop:i.baseUnit/2,color:a.neutral80})},Pf={gridArea:"1 / 2",font:"inherit",minWidth:"2px",border:0,margin:0,outline:0,padding:0},K1={flex:"1 1 auto",display:"inline-grid",gridArea:"1 / 1 / 2 / 3",gridTemplateColumns:"0 min-content","&:after":Ee({content:'attr(data-value) " "',visibility:"hidden",whiteSpace:"pre"},Pf)},q1=function(t){return Ee({label:"input",color:"inherit",background:0,opacity:t?0:1,width:"100%"},Pf)},Z1=function(t){var n=t.cx,r=t.value,s=Cf(t),o=s.innerRef,i=s.isDisabled,a=s.isHidden,l=s.inputClassName,c=Vn(s,Y1);return Se("div",Oe({},wt(t,"input",{"input-container":!0}),{"data-value":r||""}),Se("input",Oe({className:n({input:!0},l),ref:o,style:q1(a),disabled:i},c)))},Q1=Z1,J1=function(t,n){var r=t.theme,s=r.spacing,o=r.borderRadius,i=r.colors;return Ee({label:"multiValue",display:"flex",minWidth:0},n?{}:{backgroundColor:i.neutral10,borderRadius:o/2,margin:s.baseUnit/2})},R1=function(t,n){var r=t.theme,s=r.borderRadius,o=r.colors,i=t.cropWithEllipsis;return Ee({overflow:"hidden",textOverflow:i||i===void 0?"ellipsis":void 0,whiteSpace:"nowrap"},n?{}:{borderRadius:s/2,color:o.neutral80,fontSize:"85%",padding:3,paddingLeft:6})},ek=function(t,n){var r=t.theme,s=r.spacing,o=r.borderRadius,i=r.colors,a=t.isFocused;return Ee({alignItems:"center",display:"flex"},n?{}:{borderRadius:o/2,backgroundColor:a?i.dangerLight:void 0,paddingLeft:s.baseUnit,paddingRight:s.baseUnit,":hover":{backgroundColor:i.dangerLight,color:i.danger}})},$f=function(t){var n=t.children,r=t.innerProps;return Se("div",r,n)},tk=$f,nk=$f;function rk(e){var t=e.children,n=e.innerProps;return Se("div",Oe({role:"button"},n),t||Se($l,{size:14}))}var sk=function(t){var n=t.children,r=t.components,s=t.data,o=t.innerProps,i=t.isDisabled,a=t.removeProps,l=t.selectProps,c=r.Container,d=r.Label,u=r.Remove;return Se(c,{data:s,innerProps:Ee(Ee({},wt(t,"multiValue",{"multi-value":!0,"multi-value--is-disabled":i})),o),selectProps:l},Se(d,{data:s,innerProps:Ee({},wt(t,"multiValueLabel",{"multi-value__label":!0})),selectProps:l},n),Se(u,{data:s,innerProps:Ee(Ee({},wt(t,"multiValueRemove",{"multi-value__remove":!0})),{},{"aria-label":"Remove ".concat(n||"option")},a),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
@@ -182,27 +182,27 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
text-left cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed
min-w-[200px]
`,"aria-haspopup":"listbox","aria-expanded":m,children:[h.jsxs(I,{className:"flex items-center gap-2",children:[c,h.jsx(J,{className:"truncate",children:N})]}),h.jsx(Xn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${m?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),m&&h.jsxs(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden min-w-[280px]",role:"listbox",children:[h.jsx(I,{className:"p-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx("input",{id:M,type:"text",value:y,onChange:w,placeholder:s,className:"w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:outline-none"})}),h.jsxs(I,{className:"max-h-80 overflow-auto",children:[P&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:a}),L&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:i}),$&&h.jsx(I,{className:"py-2",children:k.map(D=>{const O=A(D.value);return h.jsxs(ae,{type:"button",onClick:()=>!D.disabled&&E(D.value),disabled:D.disabled,className:`
`,"aria-haspopup":"listbox","aria-expanded":m,children:[h.jsxs(I,{className:"flex items-center gap-2",children:[c,h.jsx(Q,{className:"truncate",children:N})]}),h.jsx(Zn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${m?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),m&&h.jsxs(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden min-w-[280px]",role:"listbox",children:[h.jsx(I,{className:"p-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx("input",{id:M,type:"text",value:y,onChange:w,placeholder:s,className:"w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:outline-none"})}),h.jsxs(I,{className:"max-h-80 overflow-auto",children:[P&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:a}),L&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:i}),$&&h.jsx(I,{className:"py-2",children:k.map(A=>{const O=D(A.value);return h.jsxs(ae,{type:"button",onClick:()=>!A.disabled&&E(A.value),disabled:A.disabled,className:`
w-full px-4 py-3 text-left
flex items-start gap-3
hover:bg-gray-100 dark:hover:bg-gray-700
transition-colors
${D.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}
`,role:"option","aria-selected":O,children:[h.jsx(I,{className:"w-5 h-5 flex-shrink-0 mt-0.5",children:O&&h.jsx(Xn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`font-medium ${O?"text-blue-600 dark:text-blue-400":"text-gray-900 dark:text-white"}`,children: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)})})]})]})]})},Qs={container:{base:"tag-select",disabled:"opacity-50 cursor-not-allowed"},tag:"tag",tagRemove:"tag-remove",placeholder:"tag-select-placeholder"},gC=({options:e=[],value:t=[],onChange:n,placeholder:r="No items selected",disabled:s=!1,className:o="",id:i,editorAttrs:a})=>{const l=v.useCallback(f=>e.find(m=>String(m.value)===String(f))?.label||String(f),[e]),c=v.useCallback(f=>{if(s)return;const g=t.filter(m=>String(m)!==String(f));n?.(g)},[t,n,s]),d=v.useMemo(()=>t.map(f=>({value:f,label:l(f)})),[t,l]),u=`${Qs.container.base} ${s?Qs.container.disabled:""} ${o}`.trim();return h.jsx(I,{className:u,id:i,...a,children:d.length===0?h.jsx(J,{className:Qs.placeholder,children:r}):d.map(f=>h.jsxs(J,{className:Qs.tag,children:[f.label,!s&&h.jsx(J,{className:Qs.tagRemove,onClick:()=>c(f.value),role:"button","aria-label":`Remove ${f.label}`,children:h.jsx(Xn,{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 pC(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return v.useMemo(()=>r=>{t.forEach(s=>s(r))},t)}const Si=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function as(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function Xl(e){return"nodeType"in e}function Kt(e){var t,n;return e?as(e)?e:Xl(e)&&(t=(n=e.ownerDocument)==null?void 0:n.defaultView)!=null?t:window:window}function Kl(e){const{Document:t}=Kt(e);return e instanceof t}function Js(e){return as(e)?!1:e instanceof Kt(e).HTMLElement}function ph(e){return e instanceof Kt(e).SVGElement}function ls(e){return e?as(e)?e.document:Xl(e)?Kl(e)?e:Js(e)||ph(e)?e.ownerDocument:document:document:document}const En=Si?v.useLayoutEffect:v.useEffect;function _i(e){const t=v.useRef(e);return En(()=>{t.current=e}),v.useCallback(function(){for(var n=arguments.length,r=new Array(n),s=0;s<n;s++)r[s]=arguments[s];return t.current==null?void 0:t.current(...r)},[])}function mC(){const e=v.useRef(null),t=v.useCallback((r,s)=>{e.current=setInterval(r,s)},[]),n=v.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,n]}function Rs(e,t){t===void 0&&(t=[e]);const n=v.useRef(e);return En(()=>{n.current!==e&&(n.current=e)},t),n}function eo(e,t){const n=v.useRef();return v.useMemo(()=>{const r=e(n.current);return n.current=r,r},[...t])}function Ei(e){const t=_i(e),n=v.useRef(null),r=v.useCallback(s=>{s!==n.current&&t?.(s,n.current),n.current=s},[]);return[n,r]}function Mi(e){const t=v.useRef();return v.useEffect(()=>{t.current=e},[e]),t.current}let ql={};function to(e,t){return v.useMemo(()=>{if(t)return t;const n=ql[e]==null?0:ql[e]+1;return ql[e]=n,e+"-"+n},[e,t])}function mh(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return r.reduce((o,i)=>{const a=Object.entries(i);for(const[l,c]of a){const d=o[l];d!=null&&(o[l]=d+e*c)}return o},{...t})}}const cs=mh(1),no=mh(-1);function bC(e){return"clientX"in e&&"clientY"in e}function Ni(e){if(!e)return!1;const{KeyboardEvent:t}=Kt(e.target);return t&&e instanceof t}function yC(e){if(!e)return!1;const{TouchEvent:t}=Kt(e.target);return t&&e instanceof t}function Ti(e){if(yC(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return bC(e)?{x:e.clientX,y:e.clientY}:null}const Un=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return"scaleX("+t+") scaleY("+n+")"}},Transform:{toString(e){if(e)return[Un.Translate.toString(e),Un.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:n,easing:r}=e;return t+" "+n+"ms "+r}}}),bh="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function vC(e){return e.matches(bh)?e:e.querySelector(bh)}const xC={display:"none"};function wC(e){let{id:t,value:n}=e;return v.createElement("div",{id:t,s Line truncated
${A.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}
`,role:"option","aria-selected":O,children:[h.jsx(I,{className:"w-5 h-5 flex-shrink-0 mt-0.5",children:O&&h.jsx(Zn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`font-medium ${O?"text-blue-600 dark:text-blue-400":"text-gray-900 dark:text-white"}`,children:A.label}),A.description&&h.jsx(I,{className:"text-sm text-gray-500 dark:text-gray-400 mt-0.5",children:A.description})]})]},A.value)})})]})]})]})},Js={container:{base:"tag-select",disabled:"opacity-50 cursor-not-allowed"},tag:"tag",tagRemove:"tag-remove",placeholder:"tag-select-placeholder"},fC=({options:e=[],value:t=[],onChange:n,placeholder:r="No items selected",disabled:s=!1,className:o="",id:i,editorAttrs:a})=>{const l=v.useCallback(f=>e.find(m=>String(m.value)===String(f))?.label||String(f),[e]),c=v.useCallback(f=>{if(s)return;const g=t.filter(m=>String(m)!==String(f));n?.(g)},[t,n,s]),d=v.useMemo(()=>t.map(f=>({value:f,label:l(f)})),[t,l]),u=`${Js.container.base} ${s?Js.container.disabled:""} ${o}`.trim();return h.jsx(I,{className:u,id:i,...a,children:d.length===0?h.jsx(Q,{className:Js.placeholder,children:r}):d.map(f=>h.jsxs(Q,{className:Js.tag,children:[f.label,!s&&h.jsx(Q,{className:Js.tagRemove,onClick:()=>c(f.value),role:"button","aria-label":`Remove ${f.label}`,children:h.jsx(Zn,{className:"w-3 h-3",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]},f.value))})};function hC(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return v.useMemo(()=>r=>{t.forEach(s=>s(r))},t)}const _i=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function ls(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function Kl(e){return"nodeType"in e}function Jt(e){var t,n;return e?ls(e)?e:Kl(e)&&(t=(n=e.ownerDocument)==null?void 0:n.defaultView)!=null?t:window:window}function ql(e){const{Document:t}=Jt(e);return e instanceof t}function Rs(e){return ls(e)?!1:e instanceof Jt(e).HTMLElement}function ph(e){return e instanceof Jt(e).SVGElement}function cs(e){return e?ls(e)?e.document:Kl(e)?ql(e)?e:Rs(e)||ph(e)?e.ownerDocument:document:document:document}const Mn=_i?v.useLayoutEffect:v.useEffect;function Ei(e){const t=v.useRef(e);return Mn(()=>{t.current=e}),v.useCallback(function(){for(var n=arguments.length,r=new Array(n),s=0;s<n;s++)r[s]=arguments[s];return t.current==null?void 0:t.current(...r)},[])}function gC(){const e=v.useRef(null),t=v.useCallback((r,s)=>{e.current=setInterval(r,s)},[]),n=v.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,n]}function eo(e,t){t===void 0&&(t=[e]);const n=v.useRef(e);return Mn(()=>{n.current!==e&&(n.current=e)},t),n}function to(e,t){const n=v.useRef();return v.useMemo(()=>{const r=e(n.current);return n.current=r,r},[...t])}function Mi(e){const t=Ei(e),n=v.useRef(null),r=v.useCallback(s=>{s!==n.current&&t?.(s,n.current),n.current=s},[]);return[n,r]}function Ni(e){const t=v.useRef();return v.useEffect(()=>{t.current=e},[e]),t.current}let Zl={};function no(e,t){return v.useMemo(()=>{if(t)return t;const n=Zl[e]==null?0:Zl[e]+1;return Zl[e]=n,e+"-"+n},[e,t])}function mh(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return r.reduce((o,i)=>{const a=Object.entries(i);for(const[l,c]of a){const d=o[l];d!=null&&(o[l]=d+e*c)}return o},{...t})}}const ds=mh(1),ro=mh(-1);function pC(e){return"clientX"in e&&"clientY"in e}function Ti(e){if(!e)return!1;const{KeyboardEvent:t}=Jt(e.target);return t&&e instanceof t}function mC(e){if(!e)return!1;const{TouchEvent:t}=Jt(e.target);return t&&e instanceof t}function Oi(e){if(mC(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return pC(e)?{x:e.clientX,y:e.clientY}:null}const jn=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return"scaleX("+t+") scaleY("+n+")"}},Transform:{toString(e){if(e)return[jn.Translate.toString(e),jn.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:n,easing:r}=e;return t+" "+n+"ms "+r}}}),bh="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function bC(e){return e.matches(bh)?e:e.querySelector(bh)}const yC={display:"none"};function vC(e){let{id:t,value:n}=e;return v.createElement("div",{id:t,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.
`},MC={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 NC(e){let{announcements:t=MC,container:n,hiddenTextDescribedById:r,screenReaderInstructions:s=EC}=e;const{announce:o,announcement:i}=CC(),a=to("DndLiveRegion"),[l,c]=v.useState(!1);if(v.useEffect(()=>{c(!0)},[]),SC(v.useMemo(()=>({onDragStart(u){let{active:f}=u;o(t.onDragStart({active:f}))},onDragMove(u){let{active:f,over:g}=u;t.onDragMove&&o(t.onDragMove({active:f,over:g}))},onDragOver(u){let{active:f,over:g}=u;o(t.onDragOver({active:f,over:g}))},onDragEnd(u){let{active:f,over:g}=u;o(t.onDragEnd({active:f,over:g}))},onDragCancel(u){let{active:f,over:g}=u;o(t.onDragCancel({active:f,over:g}))}}),[o,t])),!l)return null;const d=v.createElement(v.Fragment,null,v.createElement(wC,{id:r,value:s.draggable}),v.createElement(kC,{id:a,announcement:i}));return n?Ln.createPortal(d,n):d}var Et;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(Et||(Et={}));function Oi(){}function ds(e,t){return v.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function Zl(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return v.useMemo(()=>[...t].filter(r=>r!=null),[...t])}const Mn=Object.freeze({x:0,y:0});function vh(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function TC(e,t){const n=Ti(e);if(!n)return"0 0";const r={x:(n.x-t.left)/t.width*100,y:(n.y-t.top)/t.height*100};return r.x+"% "+r.y+"%"}function xh(e,t){let{data:{value:n}}=e,{data:{value:r}}=t;return n-r}function OC(e,t){let{data:{value:n}}=e,{data:{value:r}}=t;return r-n}function wh(e){let{left:t,top:n,height:r,width:s}=e;return[{x:t,y:n},{x:t+s,y:n},{x:t,y:n+r},{x:t+s,y:n+r}]}function kh(e,t){if(!e||e.length===0)return null;const[n]=e;return n[t]}function Ch(e,t,n){return t===void 0&&(t=e.left),n===void 0&&(n=e.top),{x:t+e.width*.5,y:n+e.height*.5}}const Ql=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=Ch(t,t.left,t.top),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=vh(Ch(l),s);o.push({id:a,data:{droppableContainer:i,value:c}})}}return o.sort(xh)},AC=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=wh(t),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=wh(l),d=s.reduce((f,g,m)=>f+vh(c[m],g),0),u=Number((d/4).toFixed(4));o.push({id:a,data:{droppableContainer:i,value:u}})}}return o.sort(xh)};function DC(e,t){const n=Math.max(t.top,e.top),r=Math.max(t.left,e.left),s=Math.min(t.left+t.width,e.left+e.width),o=Math.min(t.top+t.height,e.top+e.height),i=s-r,a=o-n;if(r<s&&n<o){const l=t.width*t.height,c=e.width*e.height,d=i*a,u=d/(l+c-d);return Number(u.toFixed(4))}return 0}const IC=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=[];for(const o of r){const{id:i}=o,a=n.get(i);if(a){const l=DC(a,t);l>0&&s.push({id:i,data:{droppableContainer:o,value:l}})}}return s.sort(OC)};function LC(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}function Sh(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Mn}function PC(e){return function(n){for(var r=arguments.length,s=new Array(r>1?r-1:0),o=1;o<r;o++)s[o-1]=arguments[o];return s.reduce((i,a)=>({...i,top:i.top+e*a.y,bottom:i.bottom+e*a.y,left:i.left+e*a.x,right:i.right+e*a.x}),{...n})}}const $C=PC(1);function _h(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function FC(e,t,n){const r=_h(t);if(!r)return e;const{scaleX:s,scaleY:o,x:i,y:a}=r,l=e.left-i-(1-s)*parseFloat(n),c=e.top-a-(1-o)*parseFloat(n.slice(n.indexOf(" ")+1)),d=s?e.width/s:e.width,u=o?e.height/o:e.height;return{width:d,height:u,top:c,right:l+d,bottom:c+u,left:l}}const zC={ignoreTransform:!1};function us(e,t){t===void 0&&(t=zC);let n=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:d}=Kt(e).getComputedStyle(e);c&&(n=FC(n,c,d))}const{top:r,left:s,width:o,height:i,bottom:a,right:l}=n;return{top:r,left:s,width:o,height:i,bottom:a,right:l}}function Eh(e){return us(e,{ignoreTransform:!0})}function VC(e){const t=e.innerWidth,n=e.innerHeight;return{top:0,left:0,right:t,bottom:n,width:t,height:n}}function HC(e,t){return t===void 0&&(t=Kt(e).getComputedStyle(e)),t.posit Line truncated
`},_C={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was moved over droppable area "+n.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was dropped over droppable area "+n.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function EC(e){let{announcements:t=_C,container:n,hiddenTextDescribedById:r,screenReaderInstructions:s=SC}=e;const{announce:o,announcement:i}=wC(),a=no("DndLiveRegion"),[l,c]=v.useState(!1);if(v.useEffect(()=>{c(!0)},[]),kC(v.useMemo(()=>({onDragStart(u){let{active:f}=u;o(t.onDragStart({active:f}))},onDragMove(u){let{active:f,over:g}=u;t.onDragMove&&o(t.onDragMove({active:f,over:g}))},onDragOver(u){let{active:f,over:g}=u;o(t.onDragOver({active:f,over:g}))},onDragEnd(u){let{active:f,over:g}=u;o(t.onDragEnd({active:f,over:g}))},onDragCancel(u){let{active:f,over:g}=u;o(t.onDragCancel({active:f,over:g}))}}),[o,t])),!l)return null;const d=v.createElement(v.Fragment,null,v.createElement(vC,{id:r,value:s.draggable}),v.createElement(xC,{id:a,announcement:i}));return n?$n.createPortal(d,n):d}var Nt;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(Nt||(Nt={}));function Ai(){}function us(e,t){return v.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function Ql(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return v.useMemo(()=>[...t].filter(r=>r!=null),[...t])}const Nn=Object.freeze({x:0,y:0});function vh(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function MC(e,t){const n=Oi(e);if(!n)return"0 0";const r={x:(n.x-t.left)/t.width*100,y:(n.y-t.top)/t.height*100};return r.x+"% "+r.y+"%"}function xh(e,t){let{data:{value:n}}=e,{data:{value:r}}=t;return n-r}function NC(e,t){let{data:{value:n}}=e,{data:{value:r}}=t;return r-n}function wh(e){let{left:t,top:n,height:r,width:s}=e;return[{x:t,y:n},{x:t+s,y:n},{x:t,y:n+r},{x:t+s,y:n+r}]}function kh(e,t){if(!e||e.length===0)return null;const[n]=e;return n[t]}function Ch(e,t,n){return t===void 0&&(t=e.left),n===void 0&&(n=e.top),{x:t+e.width*.5,y:n+e.height*.5}}const Jl=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=Ch(t,t.left,t.top),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=vh(Ch(l),s);o.push({id:a,data:{droppableContainer:i,value:c}})}}return o.sort(xh)},TC=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=wh(t),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=wh(l),d=s.reduce((f,g,m)=>f+vh(c[m],g),0),u=Number((d/4).toFixed(4));o.push({id:a,data:{droppableContainer:i,value:u}})}}return o.sort(xh)};function OC(e,t){const n=Math.max(t.top,e.top),r=Math.max(t.left,e.left),s=Math.min(t.left+t.width,e.left+e.width),o=Math.min(t.top+t.height,e.top+e.height),i=s-r,a=o-n;if(r<s&&n<o){const l=t.width*t.height,c=e.width*e.height,d=i*a,u=d/(l+c-d);return Number(u.toFixed(4))}return 0}const AC=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=[];for(const o of r){const{id:i}=o,a=n.get(i);if(a){const l=OC(a,t);l>0&&s.push({id:i,data:{droppableContainer:o,value:l}})}}return s.sort(NC)};function DC(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}function Sh(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Nn}function IC(e){return function(n){for(var r=arguments.length,s=new Array(r>1?r-1:0),o=1;o<r;o++)s[o-1]=arguments[o];return s.reduce((i,a)=>({...i,top:i.top+e*a.y,bottom:i.bottom+e*a.y,left:i.left+e*a.x,right:i.right+e*a.x}),{...n})}}const LC=IC(1);function _h(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function PC(e,t,n){const r=_h(t);if(!r)return e;const{scaleX:s,scaleY:o,x:i,y:a}=r,l=e.left-i-(1-s)*parseFloat(n),c=e.top-a-(1-o)*parseFloat(n.slice(n.indexOf(" ")+1)),d=s?e.width/s:e.width,u=o?e.height/o:e.height;return{width:d,height:u,top:c,right:l+d,bottom:c+u,left:l}}const $C={ignoreTransform:!1};function fs(e,t){t===void 0&&(t=$C);let n=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:d}=Jt(e).getComputedStyle(e);c&&(n=PC(n,c,d))}const{top:r,left:s,width:o,height:i,bottom:a,right:l}=n;return{top:r,left:s,width:o,height:i,bottom:a,right:l}}function Eh(e){return fs(e,{ignoreTransform:!0})}function FC(e){const t=e.innerWidth,n=e.innerHeight;return{top:0,left:0,right:t,bottom:n,width:t,height:n}}function zC(e,t){return t===void 0&&(t=Jt(e).getComputedStyle(e)),t.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
text-left text-sm
${a?"opacity-50 cursor-not-allowed":"hover:border-gray-400 dark:hover:border-gray-500"}
focus:outline-none focus:ring-2 focus:ring-blue-500
`,children:[h.jsx(I,{className:"flex items-center gap-2",children:y?h.jsxs(h.Fragment,{children:[h.jsx(oe,{name:y.faIcon,className:"w-4 h-4 text-gray-600 dark:text-gray-400"}),h.jsx(J,{className:"text-gray-900 dark:text-white",children:y.label})]}):h.jsx(J,{className:"text-gray-500 dark:text-gray-400",children:r})}),h.jsx(oe,{name:"chevron-down",className:`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${u?"rotate-180":""}`})]}),l&&h.jsx("input",{type:"hidden",name:l,value:e||""}),u&&h.jsxs(I,{className:"absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-72 overflow-hidden",children:[h.jsx(I,{className:"p-2 border-b border-gray-200 dark:border-gray-700",children:h.jsx(kt,{type:"text",value:g,onChange:x=>m(x.target.value),placeholder:s,className:"w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white",autoFocus:!0})}),h.jsx(I,{className:"max-h-56 overflow-y-auto",children:b.length>0?b.map(x=>h.jsxs(ae,{type:"button",onClick:()=>_(x),className:`
`,children:[h.jsx(I,{className:"flex items-center gap-2",children:y?h.jsxs(h.Fragment,{children:[h.jsx(oe,{name:y.faIcon,className:"w-4 h-4 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white",children:y.label})]}):h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:r})}),h.jsx(oe,{name:"chevron-down",className:`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${u?"rotate-180":""}`})]}),l&&h.jsx("input",{type:"hidden",name:l,value:e||""}),u&&h.jsxs(I,{className:"absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-72 overflow-hidden",children:[h.jsx(I,{className:"p-2 border-b border-gray-200 dark:border-gray-700",children:h.jsx(St,{type:"text",value:g,onChange:x=>m(x.target.value),placeholder:s,className:"w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white",autoFocus:!0})}),h.jsx(I,{className:"max-h-56 overflow-y-auto",children:b.length>0?b.map(x=>h.jsxs(ae,{type:"button",onClick:()=>_(x),className:`
w-full flex items-center gap-3 px-3 py-2 text-left text-sm
${y?.faIcon===x.faIcon?"bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"}
`,children:[h.jsx(oe,{name:x.faIcon,className:"w-4 h-4"}),h.jsx(J,{children:x.label}),y?.faIcon===x.faIcon&&h.jsx(oe,{name:"check",className:"w-4 h-4 ml-auto text-blue-600"})]},x.value)):h.jsx(I,{className:"px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center",children:o})})]})]})},dc=({defaultOpen:e=!1,isOpen:t,onToggle:n,className:r="",style:s,children:o,disabled:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(e),u=t!==void 0,f=u?t:c,g=v.useRef(null),[m,p]=v.useState(e?void 0:0),y=()=>{if(i)return;const _=!f;u||d(_),n?.(_)};v.useEffect(()=>{if(g.current)if(f){p(g.current.scrollHeight);const _=setTimeout(()=>{p(void 0)},300);return()=>clearTimeout(_)}else p(g.current.scrollHeight),requestAnimationFrame(()=>{p(0)})},[f]);const b={};if(v.Children.forEach(o,_=>{if(v.isValidElement(_)){const x=_.props?.slot;x==="trigger"?b.trigger=_:x==="content"&&(b.content=_)}}),!b.trigger&&!b.content){const _=v.Children.toArray(o);_.length>=2?(b.trigger=_[0],b.content=_.slice(1)):_.length===1&&(b.content=_[0])}return h.jsxs(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${r}`,style:s,id:a,...l,children:[h.jsxs(ae,{type:"button",onClick:y,disabled:i,className:`w-full flex items-center justify-between p-3 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${i?"opacity-50 cursor-not-allowed":""} ${f?"border-b border-gray-200 dark:border-gray-700":""}`,"aria-expanded":f,children:[h.jsx(I,{className:"flex-1",children:b.trigger}),!b.trigger&&h.jsx("svg",{className:`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),h.jsx(I,{ref:g,className:"overflow-hidden transition-all duration-300 ease-in-out",style:{height:m!==void 0?`${m}px`:"auto"},children:h.jsx(I,{className:"bg-white dark:bg-gray-800",children:b.content})})]})},fo=window.G7Core,dg=fo?.useControllableState,ug=(e,t)=>window.G7Core?.t?.(e,t)??e,o_={"":"common.scope_type_all_desc",self:"common.scope_type_self_desc",role:"common.scope_type_role_desc"},i_=({options:e,value:t,onChange:n,disabled:r=!1})=>{const[s,o]=v.useState(!1),[i,a]=v.useState({}),l=v.useRef(null),c=e.find(u=>u.value===t)??e[0];v.useEffect(()=>{if(!s)return;const u=g=>{l.current&&!l.current.contains(g.target)&&o(!1)},f=()=>{if(!l.current)return;const g=l.current.getBoundingClientRect();a({position:"fixed",top:g.bottom+4,right:window.innerWidth-g.right,zIndex:9999})};return document.addEventListener("mousedown",u),window.addEventListener("scroll",f,!0),window.addEventListener("resize",f),()=>{document.removeEventListener("mousedown",u),window.removeEventListener("scroll",f,!0),window.removeEventListener("resize",f)}},[s]);const d=v.useCallback(()=>{if(r||!l.current)return;const u=l.current.getBoundingClientRect();a({position:"fixed",top:u.bottom+4,right:window.innerWidth-u.right,zIndex:9999}),o(f=>!f)},[r]);return h.jsxs(I,{ref:l,className:"relative flex-shrink-0",children:[h.jsxs(I,{role:"button",tabIndex:0,onClick:u=>{u.stopPropagation(),u.preventDefault(),d()},onMouseDown:u=>u.stopPropagation(),onKeyDown:u=>{(u.key==="Enter"||u.key===" ")&&(u.stopPropagation(),u.preventDefault(),d()),u.key==="Escape"&&o(!1)},className:`flex items-center gap-1 text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 cursor-pointer select-none ${r?"opacity-50 cursor-not-allowed":"hover:border-blue-400 dark:hover:border-blue-500"}`,children:[h.jsx(J,{children:c?.label??""}),h.jsx(oe,{name:ue.ChevronDown,className:"w-3 h-3 text-gray-400 dark:text-gray-500"})]}),s&&h.jsx(I,{style:i,className:"w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1",children:e.map(u=>{const f=u.value??"",g=o_[f],m=g?ug(g):"",p=u.value===t;return h.jsxs(I,{role:"option","aria-selected":p,onClick:y=>{y.stopPropagation(),y.preventDefault(),n(u.value),o(!1)},onMouseDown:y=>y.stopPropagation(),className:`px-3 py-2 cursor-pointer transition-colors ${p?"bg-blue-50 dark:bg-blue-900/30":"hover:bg-gray-50 dark:hover:bg-gray-700/50"}`,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[h.jsx(J,{className:`text-xs font-medium ${p?"text-blue-700 dark:text-blue-300":"text-gray-900 dark:text-white"}`,children:u.label}),p&&h.jsx(oe,{name:ue.Check,className:"w-3.5 h-3.5 text-blue-600 dark:text-blue-400"})]}),m&&h.jsx(J,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:m})]},f||"__null__")})})]})},a_=(e,t)=>{if(e.length!==t.length)return!1;const n=[...e].sort((s,o)=>s.id-o.id),r=[...t].sort((s,o)=>s.id-o.id);return n.every((s,o)=>s.id===r[o].id&&s.scope_type===r[o].scope_type)},l_=({data:e=[],value:t=[],onChange:n,scopeOptions:r=[],className:s="",disabled:o=!1,desktopColumns:i=2,inst Line truncated
`,children:[h.jsx(oe,{name:x.faIcon,className:"w-4 h-4"}),h.jsx(Q,{children:x.label}),y?.faIcon===x.faIcon&&h.jsx(oe,{name:"check",className:"w-4 h-4 ml-auto text-blue-600"})]},x.value)):h.jsx(I,{className:"px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center",children:o})})]})]})},uc=({defaultOpen:e=!1,isOpen:t,onToggle:n,className:r="",style:s,children:o,disabled:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(e),u=t!==void 0,f=u?t:c,g=v.useRef(null),[m,p]=v.useState(e?void 0:0),y=()=>{if(i)return;const _=!f;u||d(_),n?.(_)};v.useEffect(()=>{if(g.current)if(f){p(g.current.scrollHeight);const _=setTimeout(()=>{p(void 0)},300);return()=>clearTimeout(_)}else p(g.current.scrollHeight),requestAnimationFrame(()=>{p(0)})},[f]);const b={};if(v.Children.forEach(o,_=>{if(v.isValidElement(_)){const x=_.props?.slot;x==="trigger"?b.trigger=_:x==="content"&&(b.content=_)}}),!b.trigger&&!b.content){const _=v.Children.toArray(o);_.length>=2?(b.trigger=_[0],b.content=_.slice(1)):_.length===1&&(b.content=_[0])}return h.jsxs(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${r}`,style:s,id:a,...l,children:[h.jsxs(ae,{type:"button",onClick:y,disabled:i,className:`w-full flex items-center justify-between p-3 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${i?"opacity-50 cursor-not-allowed":""} ${f?"border-b border-gray-200 dark:border-gray-700":""}`,"aria-expanded":f,children:[h.jsx(I,{className:"flex-1",children:b.trigger}),!b.trigger&&h.jsx("svg",{className:`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),h.jsx(I,{ref:g,className:"overflow-hidden transition-all duration-300 ease-in-out",style:{height:m!==void 0?`${m}px`:"auto"},children:h.jsx(I,{className:"bg-white dark:bg-gray-800",children:b.content})})]})},ho=window.G7Core,dg=ho?.useControllableState,ug=(e,t)=>window.G7Core?.t?.(e,t)??e,r_={"":"common.scope_type_all_desc",self:"common.scope_type_self_desc",role:"common.scope_type_role_desc"},s_=({options:e,value:t,onChange:n,disabled:r=!1})=>{const[s,o]=v.useState(!1),[i,a]=v.useState({}),l=v.useRef(null),c=e.find(u=>u.value===t)??e[0];v.useEffect(()=>{if(!s)return;const u=g=>{l.current&&!l.current.contains(g.target)&&o(!1)},f=()=>{if(!l.current)return;const g=l.current.getBoundingClientRect();a({position:"fixed",top:g.bottom+4,right:window.innerWidth-g.right,zIndex:9999})};return document.addEventListener("mousedown",u),window.addEventListener("scroll",f,!0),window.addEventListener("resize",f),()=>{document.removeEventListener("mousedown",u),window.removeEventListener("scroll",f,!0),window.removeEventListener("resize",f)}},[s]);const d=v.useCallback(()=>{if(r||!l.current)return;const u=l.current.getBoundingClientRect();a({position:"fixed",top:u.bottom+4,right:window.innerWidth-u.right,zIndex:9999}),o(f=>!f)},[r]);return h.jsxs(I,{ref:l,className:"relative flex-shrink-0",children:[h.jsxs(I,{role:"button",tabIndex:0,onClick:u=>{u.stopPropagation(),u.preventDefault(),d()},onMouseDown:u=>u.stopPropagation(),onKeyDown:u=>{(u.key==="Enter"||u.key===" ")&&(u.stopPropagation(),u.preventDefault(),d()),u.key==="Escape"&&o(!1)},className:`flex items-center gap-1 text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 cursor-pointer select-none ${r?"opacity-50 cursor-not-allowed":"hover:border-blue-400 dark:hover:border-blue-500"}`,children:[h.jsx(Q,{children:c?.label??""}),h.jsx(oe,{name:de.ChevronDown,className:"w-3 h-3 text-gray-400 dark:text-gray-500"})]}),s&&h.jsx(I,{style:i,className:"w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1",children:e.map(u=>{const f=u.value??"",g=r_[f],m=g?ug(g):"",p=u.value===t;return h.jsxs(I,{role:"option","aria-selected":p,onClick:y=>{y.stopPropagation(),y.preventDefault(),n(u.value),o(!1)},onMouseDown:y=>y.stopPropagation(),className:`px-3 py-2 cursor-pointer transition-colors ${p?"bg-blue-50 dark:bg-blue-900/30":"hover:bg-gray-50 dark:hover:bg-gray-700/50"}`,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[h.jsx(Q,{className:`text-xs font-medium ${p?"text-blue-700 dark:text-blue-300":"text-gray-900 dark:text-white"}`,children:u.label}),p&&h.jsx(oe,{name:de.Check,className:"w-3.5 h-3.5 text-blue-600 dark:text-blue-400"})]}),m&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:m})]},f||"__null__")})})]})},o_=(e,t)=>{if(e.length!==t.length)return!1;const n=[...e].sort((s,o)=>s.id-o.id),r=[...t].sort((s,o)=>s.id-o.id);return n.every((s,o)=>s.id===r[o].id&&s.scope_type===r[o].scope_type)},i_=({data:e=[],value:t=[],onChange:n,scopeOptions:r=[],className:s="",disabled:o=!1,desktopColumns:i=2,inst Line truncated
let scriptImported = false
self.addEventListener('message', async (e) => {
const { file, id, imageCompressionLibUrl, options } = e.data
@@ -221,11 +221,11 @@ self.addEventListener('message', async (e) => {
self.postMessage({ error: e.message + '\\n' + e.stack, id })
}
})
`;let Hc;function ME(e,t){return new Promise(((n,r)=>{Hc||(Hc=(function(i){const a=[];return a.push(i),URL.createObjectURL(new Blob(a))})(EE));const s=new Worker(Hc);s.addEventListener("message",(function(i){if(t.signal&&t.signal.aborted)s.terminate();else if(i.data.progress===void 0){if(i.data.error)return r(new Error(i.data.error)),void s.terminate();n(i.data.file),s.terminate()}else t.onProgress(i.data.progress)})),s.addEventListener("error",r),t.signal&&t.signal.addEventListener("abort",(()=>{r(t.signal.reason),s.terminate()})),s.postMessage({file:e,imageCompressionLibUrl:t.libURL,options:{...t,onProgress:void 0,signal:void 0}})}))}function jt(e,t){return new Promise((function(n,r){let s,o,i,a,l,c;if(s={...t},i=0,{onProgress:a}=s,s.maxSizeMB=s.maxSizeMB||Number.POSITIVE_INFINITY,l=typeof s.useWebWorker!="boolean"||s.useWebWorker,delete s.useWebWorker,s.onProgress=g=>{i=g,typeof a=="function"&&a(i)},!(e instanceof Blob||e instanceof _E))return r(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(e.type))return r(new Error("The file given is not an image"));if(c=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!l||typeof Worker!="function"||c)return $p(e,s).then(function(g){try{return o=g,f.call(this)}catch(m){return r(m)}}.bind(this),r);var d=function(){try{return f.call(this)}catch(g){return r(g)}}.bind(this),u=function(g){try{return $p(e,s).then((function(m){try{return o=m,d()}catch(p){return r(p)}}),r)}catch(m){return r(m)}};try{return s.libURL=s.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",ME(e,s).then((function(g){try{return o=g,d()}catch{return u()}}),u)}catch{u()}function f(){try{o.name=e.name,o.lastModified=e.lastModified}catch{}try{s.preserveExif&&e.type==="image/jpeg"&&(!s.fileType||s.fileType&&s.fileType===e.type)&&(o=_p(e,o))}catch{}return n(o)}}))}jt.getDataUrlFromFile=Tp,jt.getFilefromDataUrl=Vc,jt.loadImage=Op,jt.drawImageInCanvas=Dp,jt.drawFileInCanvas=ta,jt.canvasToFile=na,jt.getExifOrientation=Ip,jt.handleMaxWidthOrHeight=Lp,jt.followExifOrientation=Pp,jt.cleanupCanvasMemory=Bn,jt.isAutoOrientationInBrowser=ws,jt.approximateBelowMaximumCanvasSizeOfBrowser=Ap,jt.copyExifWithoutOrientation=_p,jt.getBrowserName=Ar,jt.version="2.0.2";const NE=window.G7Core,Uc=e=>{const t=["B","KB","MB","GB"];let n=0,r=e;for(;r>=1024&&n<t.length-1;)r/=1024,n++;return`${r.toFixed(2)} ${t[n]}`},TE=e=>{if(e&&typeof e=="object"){const t=e;if(t.response?.data?.message)return t.response.data.message;if(t.response?.data?.errors){const n=Object.values(t.response.data.errors)[0];if(Array.isArray(n)&&n[0])return n[0]}if(t.message)return t.message}return e instanceof Error?e.message:"업로드 실패"},OE=e=>e?e.startsWith("image/")?"fa-file-image":e.startsWith("video/")?"fa-file-video":e.startsWith("audio/")?"fa-file-audio":e==="application/pdf"?"fa-file-pdf":e.includes("word")||e.includes("document")?"fa-file-word":e.includes("excel")||e.includes("spreadsheet")?"fa-file-excel":e.includes("zip")||e.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",Lt=(e,t)=>NE?.t?.(e,t)??e,Fp=window.G7Core?.createLogger?.("Comp:FileUploader")??{log:(...e)=>console.log("[Comp:FileUploader]",...e),warn:(...e)=>console.warn("[Comp:FileUploader]",...e),error:(...e)=>console.error("[Comp:FileUploader]",...e)},Dr=window.G7Core;function AE(e){const{attachmentableType:t,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:u,endpoints:f,onFilesChange:g,onUploadComplete:m,onUploadError:p,onRemove:y,onReorder:b,uploadTriggerEvent:_,confirmBeforeRemove:x,uploadParams:M}=e,[S,k]=v.useState(u),[N,C]=v.useState([]),[w,E]=v.useState(!1),A=v.useRef(null),P=v.useRef(0),L=v.useRef(new Set),$=v.useRef(new Map),D=v.useRef(null),[O,T]=v.useState(!1),[z,U]=v.useState(null),[W,G]=v.useState(!1),B=v.useRef(!1),[j,Y]=v.useState([]),[V,X]=v.useState(!1),[K,q]=v.useState(0),ne=v.useRef(0),[ee,fe]=v.useState(new Map),te=v.useRef(new Map),le=v.useRef(g);le.current=g,v.useEffect(()=>{le.current?.(N)},[N]),v.useEffect(()=>{if(B.current)return;const re=u.filter(ve=>!L.current.has(ve.id)&&(!ve.hash||!L.current.has(ve.hash))),de=new Set(re.map(ve=>ve.id)),xe=Array.from($.current.values()).filter(ve=>!de.has(ve.id)&&!L.current.has(ve.id));k([...re,...xe])},[u]);const ge=v.useCallback(async re=>{const de={maxSizeMB:l?.maxSizeMB??1,maxWidthOrHeight:l?.maxWidthOrHeight??1920,useWebWorker:!0};return await jt(re,de)},[l]),ce=v.useCallback(async re=>{const de=S.length+N.length,xe=Math.max(0,s-de),ve=Array.from(re).slice(0,xe);if(re.length>xe){const we=Array.from(re).slice(xe),Ve=Lt("attachment.upload_limit_exceeded",{max:s});p?.(Ve,we[0]??re[0])}for(const we of ve){if(we.size>o*1024*1024){p?.(`[${we.name}] 파일 크기(${Uc(we.size)})가 최대 허용 크기(${o}MB)를 초과합니다.`,we);continue}if(a){const De="."+we.name.split(".").pop()?.toLowerCase();if(!a.split(","). Line truncated
`;let Uc;function _E(e,t){return new Promise(((n,r)=>{Uc||(Uc=(function(i){const a=[];return a.push(i),URL.createObjectURL(new Blob(a))})(SE));const s=new Worker(Uc);s.addEventListener("message",(function(i){if(t.signal&&t.signal.aborted)s.terminate();else if(i.data.progress===void 0){if(i.data.error)return r(new Error(i.data.error)),void s.terminate();n(i.data.file),s.terminate()}else t.onProgress(i.data.progress)})),s.addEventListener("error",r),t.signal&&t.signal.addEventListener("abort",(()=>{r(t.signal.reason),s.terminate()})),s.postMessage({file:e,imageCompressionLibUrl:t.libURL,options:{...t,onProgress:void 0,signal:void 0}})}))}function Yt(e,t){return new Promise((function(n,r){let s,o,i,a,l,c;if(s={...t},i=0,{onProgress:a}=s,s.maxSizeMB=s.maxSizeMB||Number.POSITIVE_INFINITY,l=typeof s.useWebWorker!="boolean"||s.useWebWorker,delete s.useWebWorker,s.onProgress=g=>{i=g,typeof a=="function"&&a(i)},!(e instanceof Blob||e instanceof CE))return r(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(e.type))return r(new Error("The file given is not an image"));if(c=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!l||typeof Worker!="function"||c)return $p(e,s).then(function(g){try{return o=g,f.call(this)}catch(m){return r(m)}}.bind(this),r);var d=function(){try{return f.call(this)}catch(g){return r(g)}}.bind(this),u=function(g){try{return $p(e,s).then((function(m){try{return o=m,d()}catch(p){return r(p)}}),r)}catch(m){return r(m)}};try{return s.libURL=s.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",_E(e,s).then((function(g){try{return o=g,d()}catch{return u()}}),u)}catch{u()}function f(){try{o.name=e.name,o.lastModified=e.lastModified}catch{}try{s.preserveExif&&e.type==="image/jpeg"&&(!s.fileType||s.fileType&&s.fileType===e.type)&&(o=_p(e,o))}catch{}return n(o)}}))}Yt.getDataUrlFromFile=Tp,Yt.getFilefromDataUrl=Hc,Yt.loadImage=Op,Yt.drawImageInCanvas=Dp,Yt.drawFileInCanvas=na,Yt.canvasToFile=ra,Yt.getExifOrientation=Ip,Yt.handleMaxWidthOrHeight=Lp,Yt.followExifOrientation=Pp,Yt.cleanupCanvasMemory=Wn,Yt.isAutoOrientationInBrowser=ks,Yt.approximateBelowMaximumCanvasSizeOfBrowser=Ap,Yt.copyExifWithoutOrientation=_p,Yt.getBrowserName=Dr,Yt.version="2.0.2";const EE=window.G7Core,Bc=e=>{const t=["B","KB","MB","GB"];let n=0,r=e;for(;r>=1024&&n<t.length-1;)r/=1024,n++;return`${r.toFixed(2)} ${t[n]}`},ME=e=>{if(e&&typeof e=="object"){const t=e;if(t.response?.data?.message)return t.response.data.message;if(t.response?.data?.errors){const n=Object.values(t.response.data.errors)[0];if(Array.isArray(n)&&n[0])return n[0]}if(t.message)return t.message}return e instanceof Error?e.message:"업로드 실패"},NE=e=>e?e.startsWith("image/")?"fa-file-image":e.startsWith("video/")?"fa-file-video":e.startsWith("audio/")?"fa-file-audio":e==="application/pdf"?"fa-file-pdf":e.includes("word")||e.includes("document")?"fa-file-word":e.includes("excel")||e.includes("spreadsheet")?"fa-file-excel":e.includes("zip")||e.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",$t=(e,t)=>EE?.t?.(e,t)??e,Fp=window.G7Core?.createLogger?.("Comp:FileUploader")??{log:(...e)=>console.log("[Comp:FileUploader]",...e),warn:(...e)=>console.warn("[Comp:FileUploader]",...e),error:(...e)=>console.error("[Comp:FileUploader]",...e)},Ir=window.G7Core;function TE(e){const{attachmentableType:t,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:u,endpoints:f,onFilesChange:g,onUploadComplete:m,onUploadError:p,onRemove:y,onReorder:b,uploadTriggerEvent:_,confirmBeforeRemove:x,uploadParams:M}=e,[C,k]=v.useState(u),[N,S]=v.useState([]),[w,E]=v.useState(!1),D=v.useRef(null),P=v.useRef(0),L=v.useRef(new Set),$=v.useRef(new Map),A=v.useRef(null),[O,T]=v.useState(!1),[z,H]=v.useState(null),[j,G]=v.useState(!1),U=v.useRef(!1),[W,Y]=v.useState([]),[V,X]=v.useState(!1),[K,q]=v.useState(0),ne=v.useRef(0),[ee,ue]=v.useState(new Map),J=v.useRef(new Map),ce=v.useRef(g);ce.current=g,v.useEffect(()=>{ce.current?.(N)},[N]),v.useEffect(()=>{if(U.current)return;const re=u.filter(we=>!L.current.has(we.id)&&(!we.hash||!L.current.has(we.hash))),fe=new Set(re.map(we=>we.id)),be=Array.from($.current.values()).filter(we=>!fe.has(we.id)&&!L.current.has(we.id));k([...re,...be])},[u]);const me=v.useCallback(async re=>{const fe={maxSizeMB:l?.maxSizeMB??1,maxWidthOrHeight:l?.maxWidthOrHeight??1920,useWebWorker:!0};return await Yt(re,fe)},[l]),le=v.useCallback(async re=>{const fe=C.length+N.length,be=Math.max(0,s-fe),we=Array.from(re).slice(0,be);if(re.length>be){const xe=Array.from(re).slice(be),He=$t("attachment.upload_limit_exceeded",{max:s});p?.(He,xe[0]??re[0])}for(const xe of we){if(xe.size>o*1024*1024){p?.(`[${xe.name}] 파일 크기(${Bc(xe.size)})가 최대 허용 크기(${o}MB)를 초과합니다.`,xe);continue}if(a){const _e="."+xe.name.split(".").pop()?.toLowerCase();if(!a.split(",").m Line truncated
relative border-2 border-dashed rounded-lg transition-colors
${l?"opacity-60 cursor-not-allowed":""}
${e&&!l?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 dark:border-gray-600"}
`,onDragOver:u=>{u.preventDefault(),d&&r(!0)},onDragLeave:()=>r(!1),onDrop:u=>{u.preventDefault(),r(!1),d&&n(u.dataTransfer.files)},children:[h.jsx("input",{ref:s,type:"file",multiple:!0,accept:o,onChange:u=>u.target.files&&!l&&n(u.target.files),className:"hidden",disabled:l}),!c&&h.jsxs(I,{className:`p-8 text-center transition-colors ${l?"cursor-not-allowed":"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,onClick:()=>!l&&s.current?.click(),children:[h.jsx(pt,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),h.jsx(yt,{className:"text-sm text-gray-600 dark:text-gray-400",children:Lt("attachment.drop_or_click")}),h.jsxs(yt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[Lt("attachment.upload_limit",{maxFiles:i,maxSize:a}),o&&` (${o})`]})]}),c]})},IE=window.G7Core,LE=window.G7Core?.createLogger?.("Comp:SortableThumbnail")??{log:(...e)=>console.log("[Comp:SortableThumbnail]",...e),warn:(...e)=>console.warn("[Comp:SortableThumbnail]",...e),error:(...e)=>console.error("[Comp:SortableThumbnail]",...e)},PE=({file:e,onRemove:t,onRetry:n,onImageClick:r,onDownload:s,isPrimary:o,onPrimaryClick:i,disabled:a})=>{const{attributes:l,listeners:c,setNodeRef:d,transform:u,transition:f,isDragging:g}=Hi({id:"hash"in e?e.hash:e.id}),m={transform:Un.Transform.toString(u),transition:f,opacity:g?.5:1},p="status"in e&&e.status!==void 0,y=p?e.status:"done",b=y==="uploading",_=y==="compressing",x=y==="error",M=p?e.progress:100,S=p?e.preview:void 0,k=p?e.mime_type?.startsWith("image/")??!1:e.is_image,[N,C]=v.useState(void 0),w=!p&&k?e.download_url:void 0;v.useEffect(()=>{if(!w){C(void 0);return}let A=!0,P;return(async()=>{try{const $=await IE.api.get(w,{responseType:"blob"});A&&$&&(P=URL.createObjectURL($),C(P))}catch($){LE.error("Failed to load authenticated image:",$)}})(),()=>{A=!1,P&&URL.revokeObjectURL(P)}},[w]);const E=N;return h.jsxs(I,{ref:d,style:m,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[h.jsxs(I,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${k||!p?"cursor-pointer":""}`,onClick:()=>{b||_||(k&&r?r():!p&&s&&s())},children:[S||E?h.jsx(Sn,{src:S||E,alt:e.original_filename,className:"w-full h-full object-cover"}):h.jsx(pt,{className:`fa-solid ${OE(e.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(b||_)&&h.jsxs(I,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[h.jsx(I,{className:"w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden",children:h.jsx(I,{className:"h-full bg-blue-500 transition-all",style:{width:`${M}%`}})}),h.jsxs(J,{className:"text-white text-xs mt-1",children:[M,"%"]})]}),x&&h.jsx(I,{className:"absolute inset-0 bg-red-500/50 flex items-center justify-center",children:h.jsx(pt,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),i&&h.jsx(ae,{type:"button",onClick:A=>{A.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:Lt(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(ae,{type:"button",...l,...c,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:Lt("attachment.drag_to_reorder"),onClick:A=>A.stopPropagation(),children:h.jsx(pt,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&s&&h.jsx(ae,{type:"button",onClick:A=>{A.stopPropagation(),s()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:Lt("common.download"),children:h.jsx(pt,{className:"fa-solid fa-download text-sm"})}),x&&n&&h.jsx(ae,{type:"button",onClick:A=>{A.stopPropagation(),n()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:Lt("common.retry"),children:h.jsx(pt,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),h.jsx(ae,{type:"button",onClick:A=>{A.stopPropagation(),t()},disabled:b||_,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:Lt("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(yt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:e.original_filename,children:e.original_filename}),h.jsx(yt,{className:"text-xs text-gray-500 dark:text-gray-400",children:e.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(bt,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),h.jsx(vt,{className:"text-sm text-gray-600 dark:text-gray-400",children:$t("attachment.drop_or_click")}),h.jsxs(vt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[$t("attachment.upload_limit",{maxFiles:i,maxSize:a}),o&&` (${o})`]})]}),c]})},AE=window.G7Core,DE=window.G7Core?.createLogger?.("Comp:SortableThumbnail")??{log:(...e)=>console.log("[Comp:SortableThumbnail]",...e),warn:(...e)=>console.warn("[Comp:SortableThumbnail]",...e),error:(...e)=>console.error("[Comp:SortableThumbnail]",...e)},IE=({file:e,onRemove:t,onRetry:n,onImageClick:r,onDownload:s,isPrimary:o,onPrimaryClick:i,disabled:a})=>{const{attributes:l,listeners:c,setNodeRef:d,transform:u,transition:f,isDragging:g}=Ui({id:"hash"in e?e.hash:e.id}),m={transform:jn.Transform.toString(u),transition:f,opacity:g?.5:1},p="status"in e&&e.status!==void 0,y=p?e.status:"done",b=y==="uploading",_=y==="compressing",x=y==="error",M=p?e.progress:100,C=p?e.preview:void 0,k=p?e.mime_type?.startsWith("image/")??!1:e.is_image,[N,S]=v.useState(void 0),w=!p&&k?e.download_url:void 0;v.useEffect(()=>{if(!w){S(void 0);return}let D=!0,P;return(async()=>{try{const $=await AE.api.get(w,{responseType:"blob"});D&&$&&(P=URL.createObjectURL($),S(P))}catch($){DE.error("Failed to load authenticated image:",$)}})(),()=>{D=!1,P&&URL.revokeObjectURL(P)}},[w]);const E=N;return h.jsxs(I,{ref:d,style:m,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[h.jsxs(I,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${k||!p?"cursor-pointer":""}`,onClick:()=>{b||_||(k&&r?r():!p&&s&&s())},children:[C||E?h.jsx(_n,{src:C||E,alt:e.original_filename,className:"w-full h-full object-cover"}):h.jsx(bt,{className:`fa-solid ${NE(e.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(b||_)&&h.jsxs(I,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[h.jsx(I,{className:"w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden",children:h.jsx(I,{className:"h-full bg-blue-500 transition-all",style:{width:`${M}%`}})}),h.jsxs(Q,{className:"text-white text-xs mt-1",children:[M,"%"]})]}),x&&h.jsx(I,{className:"absolute inset-0 bg-red-500/50 flex items-center justify-center",children:h.jsx(bt,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),i&&h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),i()},className:`absolute top-1.5 left-1.5 z-10 p-1 rounded-full transition-colors ${o?"bg-yellow-500 text-white":"bg-gray-800/50 text-gray-300 hover:bg-yellow-500 hover:text-white opacity-0 group-hover:opacity-100"}`,title:$t(o?"attachment.primary_image":"attachment.set_as_primary"),children:h.jsx(bt,{className:"fa-solid fa-star text-xs"})}),h.jsxs(I,{className:`absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-start justify-end p-1 gap-1 opacity-0 group-hover:opacity-100 ${a?"hidden":""}`,children:[h.jsx(ae,{type:"button",...l,...c,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:$t("attachment.drag_to_reorder"),onClick:D=>D.stopPropagation(),children:h.jsx(bt,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&s&&h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),s()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:$t("common.download"),children:h.jsx(bt,{className:"fa-solid fa-download text-sm"})}),x&&n&&h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),n()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:$t("common.retry"),children:h.jsx(bt,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),t()},disabled:b||_,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:$t("common.delete"),children:h.jsx(bt,{className:"fa-solid fa-times text-sm"})})]})]}),h.jsxs(I,{className:`p-2 ${!p&&s?"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700":""}`,onClick:()=>{!p&&s&&s()},children:[h.jsx(vt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:e.original_filename,children:e.original_filename}),h.jsx(vt,{className:"text-xs text-gray-500 dark:text-gray-400",children:e.siz Line truncated
${y.radio}
text-blue-600
bg-gray-100 dark:bg-gray-700
@@ -234,14 +234,14 @@ self.addEventListener('message', async (e) => {
focus:ring-2
dark:ring-offset-gray-800
${M?"cursor-not-allowed":"cursor-pointer"}
`}),h.jsx(J,{className:`
`}),h.jsx(Q,{className:`
${y.label}
font-medium
text-gray-900 dark:text-gray-300
`,children:_.label})]},_.value)})}),c&&h.jsx(J,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:c})]})},ks=()=>window.G7Core,ra=(e,t)=>ks()?.t?.(e,t)??e,zE=()=>ks()?.locale?.supported?.()??["ko","en"],VE=()=>ks()?.locale?.current?.()??"ko",Vp=e=>{const t=ra(`common.language_${e}`);return t===`common.language_${e}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[e]||e.toUpperCase():t},HE=({content:e="",value:t,onChange:n,multilingual:r=!1,isHtml:s=!1,onIsHtmlChange:o,rows:i=15,placeholder:a="",label:l,showHtmlModeToggle:c=!0,contentClassName:d="",purifyConfig:u,className:f="",name:g="content",htmlFieldName:m="content_mode",readOnly:p=!1,id:y,editorAttrs:b})=>{const _=v.useMemo(()=>zE(),[]),x=v.useMemo(()=>VE(),[]),[M,S]=v.useState(x),[k,N]=v.useState(s),[C,w]=v.useState(e),[E,A]=v.useState(t??{}),[P,L]=v.useState(!1);v.useEffect(()=>{N(s)},[s]),v.useEffect(()=>{r||w(e)},[e,r]),v.useEffect(()=>{r&&t&&A(t)},[t,r]);const $=v.useCallback(G=>{const B=G.target.value;if(w(B),n){const j=ks()?.createChangeEvent?.({value:B,name:g,type:"textarea"})??{target:{name:g,value:B}};n(j)}},[n,g]),D=v.useCallback((G,B)=>{const j={...E,[G]:B};if(A(j),n){const Y=ks()?.createChangeEvent?.({value:j,name:g,type:"multilingual"})??{target:{name:g,value:j}};n(Y)}},[n,g,E]),O=v.useCallback(G=>{const B=G.target.checked;if(N(B),B||L(!1),o){const j=ks()?.createChangeEvent?.({checked:B,name:m,type:"checkbox"})??{target:{name:m,checked:B}};o(j)}},[o,m]),T=v.useCallback(()=>{L(G=>!G)},[]),z=r?E[M]??"":C,U=v.useCallback(G=>!!E[G]?.trim(),[E]),W=()=>h.jsx(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:_.map(G=>{const B=G===x,j=M===G;return h.jsxs(ae,{type:"button",className:`
`,children:_.label})]},_.value)})}),c&&h.jsx(Q,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:c})]})},Cs=()=>window.G7Core,sa=(e,t)=>Cs()?.t?.(e,t)??e,$E=()=>Cs()?.locale?.supported?.()??["ko","en"],FE=()=>Cs()?.locale?.current?.()??"ko",Vp=e=>{const t=sa(`common.language_${e}`);return t===`common.language_${e}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[e]||e.toUpperCase():t},zE=({content:e="",value:t,onChange:n,multilingual:r=!1,isHtml:s=!1,onIsHtmlChange:o,rows:i=15,placeholder:a="",label:l,showHtmlModeToggle:c=!0,contentClassName:d="",purifyConfig:u,className:f="",name:g="content",htmlFieldName:m="content_mode",readOnly:p=!1,id:y,editorAttrs:b})=>{const _=v.useMemo(()=>$E(),[]),x=v.useMemo(()=>FE(),[]),[M,C]=v.useState(x),[k,N]=v.useState(s),[S,w]=v.useState(e),[E,D]=v.useState(t??{}),[P,L]=v.useState(!1);v.useEffect(()=>{N(s)},[s]),v.useEffect(()=>{r||w(e)},[e,r]),v.useEffect(()=>{r&&t&&D(t)},[t,r]);const $=v.useCallback(G=>{const U=G.target.value;if(w(U),n){const W=Cs()?.createChangeEvent?.({value:U,name:g,type:"textarea"})??{target:{name:g,value:U}};n(W)}},[n,g]),A=v.useCallback((G,U)=>{const W={...E,[G]:U};if(D(W),n){const Y=Cs()?.createChangeEvent?.({value:W,name:g,type:"multilingual"})??{target:{name:g,value:W}};n(Y)}},[n,g,E]),O=v.useCallback(G=>{const U=G.target.checked;if(N(U),U||L(!1),o){const W=Cs()?.createChangeEvent?.({checked:U,name:m,type:"checkbox"})??{target:{name:m,checked:U}};o(W)}},[o,m]),T=v.useCallback(()=>{L(G=>!G)},[]),z=r?E[M]??"":S,H=v.useCallback(G=>!!E[G]?.trim(),[E]),j=()=>h.jsx(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:_.map(G=>{const U=G===x,W=M===G;return h.jsxs(ae,{type:"button",className:`
inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold transition-all
${j?B?"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":B?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"}
`,onClick:()=>S(G),children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),h.jsx(J,{children:Vp(G)}),B&&h.jsx(J,{className:"text-red-500",children:"*"}),U(G)&&!j&&h.jsx(oe,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"})]},G)})});return h.jsxs(I,{className:`space-y-2 ${f}`,id:y,...b,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[l&&h.jsx(ln,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:l}),h.jsxs(I,{className:"flex items-center gap-3",children:[k&&!p&&h.jsx(ae,{type:"button",onClick:T,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${P?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:ra(P?"common.preview_off":"common.preview")}),c&&h.jsxs(ln,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[h.jsx(kt,{type:"checkbox",name:m,checked:k,onChange:O,disabled:p,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(I,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:ra("common.html_mode")})]})]})]}),r&&W(),!P&&(r?_.map(G=>h.jsx(I,{className:M===G?"block":"hidden",children:h.jsx(sr,{name:`${g}[${G}]`,value:E[G]??"",onChange:B=>D(G,B.target.value),placeholder:`${a} (${Vp(G)})`,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${k?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})},G)):h.jsx(sr,{name:g,value:C,onChange:$,placeholder:a,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${k?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})),P&&h.jsx(I,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:h.jsx(ol,{content:z,isHtml:!0,className:d||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:u})})]})},Hp="g7_dismissed_warnings";function Up(){try{const e=sessionStorage.getItem(Hp);return e?JSON.parse(e):[]}catch{return[]}}function UE(e){try{const t=Up();t.includes(e)||(t.push(e),sessionStorage.setItem(Hp,JSON.stringify(t)))}catch{}}function BE(e){switch(e){case"error":return"error";case"warning":return"warning";default:return"info"}}const jE=({warnings:e,className:t=""})=>{const[n,r]=v.useState([]);v.useEffect(()=>{r(Up())},[]);const s=v.useCallback(i=>{UE(i),r(a=>[...a,i])},[]);if(!e||e.length===0)return null;const o=e.filter(i=>!n.includes(i.id));return o.length===0?null:h.jsx(I,{className:`space-y-2 ${t}`.trim(),children:o.map(i=>h.jsx(wu,{type:BE(i.level),message:i.message,dismissible:!0,onDismiss:()=>s(i.id)},i.id))})},Bp=window.G7Core?.createLogger?.("Comp:FilterVisibility")??{log:(...e)=>console.log("[Comp:FilterVisibility]",...e),warn:(...e)=>console.warn("[Comp:FilterVisibility]",...e),error:(...e)=>console.error("[Comp:FilterVisibility]",...e)},WE=({id:e,visibleFilters:t,defaultFilters:n=[],onFilterVisibilityChange:r})=>{const[s,o]=v.useState(!1),i=v.useRef(!0),a=v.useMemo(()=>{try{const f=window.G7Core?.AuthManager?.getInstance();return f?f.getUser()?.uuid:void 0}catch{return}},[]),l=v.useMemo(()=>`g7_filters_${e}${a!==void 0?`_${a}`:""}`,[e,a]),c=v.useCallback(()=>{try{const f=localStorage.getItem(l);if(f){const g=JSON.parse(f);if(Array.isArray(g))return g}}catch(f){Bp.warn("Failed to load from localStorage:",f)}return null},[l]),d=v.useCallback(f=>{try{localStorage.setItem(l,JSON.stringify(f))}catch(g){Bp.warn("Failed to save to localStorage:",g)}},[l]),u=v.useCallback(()=>{const f=c();return f||(t&&t.length>0?t:n)},[c,t,n]);return v.useEffect(()=>{if(!s){const f=u();r&&f.length>0&&r(f),o(!0)}},[s,r,u]),v.useEffect(()=>{if(s&&t&&t.length>0){if(i.current){i.current=!1;return}d(t)}},[s,t,d]),null},GE=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...e)=>console.log( Line truncated
${W?U?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":U?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"}
`,onClick:()=>C(G),children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),h.jsx(Q,{children:Vp(G)}),U&&h.jsx(Q,{className:"text-red-500",children:"*"}),H(G)&&!W&&h.jsx(oe,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"})]},G)})});return h.jsxs(I,{className:`space-y-2 ${f}`,id:y,...b,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[l&&h.jsx(ln,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:l}),h.jsxs(I,{className:"flex items-center gap-3",children:[k&&!p&&h.jsx(ae,{type:"button",onClick:T,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${P?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:sa(P?"common.preview_off":"common.preview")}),c&&h.jsxs(ln,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[h.jsx(St,{type:"checkbox",name:m,checked:k,onChange:O,disabled:p,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(I,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:sa("common.html_mode")})]})]})]}),r&&j(),!P&&(r?_.map(G=>h.jsx(I,{className:M===G?"block":"hidden",children:h.jsx(ir,{name:`${g}[${G}]`,value:E[G]??"",onChange:U=>A(G,U.target.value),placeholder:`${a} (${Vp(G)})`,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${k?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})},G)):h.jsx(ir,{name:g,value:S,onChange:$,placeholder:a,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${k?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})),P&&h.jsx(I,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:h.jsx(ol,{content:z,isHtml:!0,className:d||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:u})})]})},Hp="g7_dismissed_warnings";function Up(){try{const e=sessionStorage.getItem(Hp);return e?JSON.parse(e):[]}catch{return[]}}function VE(e){try{const t=Up();t.includes(e)||(t.push(e),sessionStorage.setItem(Hp,JSON.stringify(t)))}catch{}}function HE(e){switch(e){case"error":return"error";case"warning":return"warning";default:return"info"}}const UE=({warnings:e,className:t=""})=>{const[n,r]=v.useState([]);v.useEffect(()=>{r(Up())},[]);const s=v.useCallback(i=>{VE(i),r(a=>[...a,i])},[]);if(!e||e.length===0)return null;const o=e.filter(i=>!n.includes(i.id));return o.length===0?null:h.jsx(I,{className:`space-y-2 ${t}`.trim(),children:o.map(i=>h.jsx(wu,{type:HE(i.level),message:i.message,dismissible:!0,onDismiss:()=>s(i.id)},i.id))})},Bp=window.G7Core?.createLogger?.("Comp:FilterVisibility")??{log:(...e)=>console.log("[Comp:FilterVisibility]",...e),warn:(...e)=>console.warn("[Comp:FilterVisibility]",...e),error:(...e)=>console.error("[Comp:FilterVisibility]",...e)},BE=({id:e,visibleFilters:t,defaultFilters:n=[],onFilterVisibilityChange:r})=>{const[s,o]=v.useState(!1),i=v.useRef(!0),a=v.useMemo(()=>{try{const f=window.G7Core?.AuthManager?.getInstance();return f?f.getUser()?.uuid:void 0}catch{return}},[]),l=v.useMemo(()=>`g7_filters_${e}${a!==void 0?`_${a}`:""}`,[e,a]),c=v.useCallback(()=>{try{const f=localStorage.getItem(l);if(f){const g=JSON.parse(f);if(Array.isArray(g))return g}}catch(f){Bp.warn("Failed to load from localStorage:",f)}return null},[l]),d=v.useCallback(f=>{try{localStorage.setItem(l,JSON.stringify(f))}catch(g){Bp.warn("Failed to save to localStorage:",g)}},[l]),u=v.useCallback(()=>{const f=c();return f||(t&&t.length>0?t:n)},[c,t,n]);return v.useEffect(()=>{if(!s){const f=u();r&&f.length>0&&r(f),o(!0)}},[s,r,u]),v.useEffect(()=>{if(s&&t&&t.length>0){if(i.current){i.current=!1;return}d(t)}},[s,t,d]),null},jE=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...e)=>console.log( Line truncated
w-full px-4 py-2.5
bg-gray-100 dark:bg-gray-700
border-0 rounded-xl
@@ -250,13 +250,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:M()}),h.jsx(Xn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&h.jsx(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",role:"listbox",children:h.jsx(I,{className:"overflow-auto",style:{maxHeight:i},children:e.length===0?h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:jp("common.no_options")}):e.map((k,N)=>{const C=String(k.value)===String(t);return h.jsx(ae,{type:"button",onClick:()=>!k.disabled&&_(k.value),disabled:k.disabled,className:`
`,"aria-haspopup":"listbox","aria-expanded":f,children:[h.jsx(I,{className:"flex-1 min-w-0",children:M()}),h.jsx(Zn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&h.jsx(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",role:"listbox",children:h.jsx(I,{className:"overflow-auto",style:{maxHeight:i},children:e.length===0?h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:jp("common.no_options")}):e.map((k,N)=>{const S=String(k.value)===String(t);return h.jsx(ae,{type:"button",onClick:()=>!k.disabled&&_(k.value),disabled:k.disabled,className:`
w-full px-4 py-3 text-left text-sm
hover:bg-gray-100 dark:hover:bg-gray-700
transition-colors
${C?"bg-blue-50 dark:bg-blue-900/20":""}
${S?"bg-blue-50 dark:bg-blue-900/20":""}
${k.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}
`,role:"option","aria-selected":C,"data-item-value":k.value,"data-item-index":N,"data-item-selected":C,children:c?.item?Wp(c.item,{item:k,index:N,isSelected:C},`item-${N}`):a||S(k,C)},k.value)})})})]})};function qE(e){const t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}const ZE=({value:e,checked:t=!1,icon:n,label:r,variant:s="blue",customColor:o,className:i="",style:a,onChange:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),g="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium border cursor-pointer transition-all";if(o){const b=qE(o),_=document.documentElement.classList.contains("dark");let x;return t?x={backgroundColor:o,color:"#ffffff",borderColor:o,opacity:u?.85:1}:x=b?{backgroundColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${u?_?.25:.12:_?.15:.06})`,color:o,borderColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${_?.3:.2})`}:{},h.jsxs(ln,{className:`${g} ${i}`,style:{...x,...a},onMouseEnter:()=>f(!0),onMouseLeave:()=>f(!1),id:c,...d,children:[h.jsx(kt,{type:"checkbox",value:e,checked:t,onChange:M=>{l?.(M)},className:"hidden"}),n&&h.jsx(oe,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})}const y=t?{green:"bg-green-600 dark:bg-green-600 text-white dark:text-white border-green-700 dark:border-green-500 hover:bg-green-700 dark:hover:bg-green-500",orange:"bg-orange-600 dark:bg-orange-600 text-white dark:text-white border-orange-700 dark:border-orange-500 hover:bg-orange-700 dark:hover:bg-orange-500",yellow:"bg-yellow-600 dark:bg-yellow-600 text-white dark:text-white border-yellow-700 dark:border-yellow-500 hover:bg-yellow-700 dark:hover:bg-yellow-500",gray:"bg-gray-600 dark:bg-gray-500 text-white dark:text-white border-gray-700 dark:border-gray-400 hover:bg-gray-700 dark:hover:bg-gray-400",blue:"bg-blue-600 dark:bg-blue-600 text-white dark:text-white border-blue-700 dark:border-blue-500 hover:bg-blue-700 dark:hover:bg-blue-500"}[s]:{green:"bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/30",orange:"bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800 hover:bg-orange-100 dark:hover:bg-orange-900/30",yellow:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 hover:bg-yellow-100 dark:hover:bg-yellow-900/30",gray:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600",blue:"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30"}[s];return h.jsxs(ln,{className:`${g} ${y} ${i}`,style:a,id:c,...d,children:[h.jsx(kt,{type:"checkbox",value:e,checked:t,onChange:b=>{l?.(b)},className:"hidden"}),n&&h.jsx(oe,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})},QE=({label:e,icon:t,iconPosition:n="left",position:r="right",className:s="",style:o,children:i})=>{const[a,l]=v.useState(!1),[c,d]=v.useState({top:0,left:0}),u=v.useRef(null),f=v.useRef(null),g=v.useCallback(()=>{if(!u.current)return;const p=u.current.getBoundingClientRect(),y=224;let b=r==="right"?p.right-y:p.left;const _=p.bottom+8;b<8&&(b=8),b+y>window.innerWidth-8&&(b=window.innerWidth-y-8),d({top:_,left:b})},[r]);v.useEffect(()=>{a&&g()},[a,g]),v.useEffect(()=>{if(!a)return;const p=()=>{l(!1)};return window.addEventListener("scroll",p,!0),window.addEventListener("resize",p),()=>{window.removeEventListener("scroll",p,!0),window.removeEventListener("resize",p)}},[a]),v.useEffect(()=>{const p=y=>{const b=y.target,_=u.current?.contains(b),x=f.current?.contains(b);!_&&!x&&l(!1)};return a&&document.addEventListener("mousedown",p),()=>{document.removeEventListener("mousedown",p)}},[a]);const m=a?Ln.createPortal(h.jsx("div",{ref:f,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:c.top,left:c.left},onClick:()=>l(!1),children:i}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,children:[h.jsxs(ae,{ref:u,onClick:()=>l(!a),className:"flex items-center gap-2",children:[n==="left"&&t&&h.jsx(oe,{name:t,className:"w-4 h-4"}),e&&h.jsx(J,{className:"text-sm font-medium",children:e}),n==="right"&&t&&h.jsx(oe,{name:t,className:"w-4 h-4"})]}),m]})},JE=({label:e,icon:t,variant:n="default",disabled:r=!1,divider:s=!1,className:o="",style:i,onClick:a})=>{if(s)return h.jsx(I,{className:"h-px bg-gray-200 dark:bg-gray-700 my-1",style:i});const l=n==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",c=r?"opacity-50 cursor-not-allowed":"cursor-pointer",d=()=>{!r&&a&&a()};return h.jsxs(I,{onClick:d,className:`flex items-center gap-3 px-4 py-3Line truncated
`,role:"option","aria-selected":S,"data-item-value":k.value,"data-item-index":N,"data-item-selected":S,children:c?.item?Wp(c.item,{item:k,index:N,isSelected:S},`item-${N}`):a||C(k,S)},k.value)})})})]})};function XE(e){const t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}const KE=({value:e,checked:t=!1,icon:n,label:r,variant:s="blue",customColor:o,className:i="",style:a,onChange:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),g="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium border cursor-pointer transition-all";if(o){const b=XE(o),_=document.documentElement.classList.contains("dark");let x;return t?x={backgroundColor:o,color:"#ffffff",borderColor:o,opacity:u?.85:1}:x=b?{backgroundColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${u?_?.25:.12:_?.15:.06})`,color:o,borderColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${_?.3:.2})`}:{},h.jsxs(ln,{className:`${g} ${i}`,style:{...x,...a},onMouseEnter:()=>f(!0),onMouseLeave:()=>f(!1),id:c,...d,children:[h.jsx(St,{type:"checkbox",value:e,checked:t,onChange:M=>{l?.(M)},className:"hidden"}),n&&h.jsx(oe,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})}const y=t?{green:"bg-green-600 dark:bg-green-600 text-white dark:text-white border-green-700 dark:border-green-500 hover:bg-green-700 dark:hover:bg-green-500",orange:"bg-orange-600 dark:bg-orange-600 text-white dark:text-white border-orange-700 dark:border-orange-500 hover:bg-orange-700 dark:hover:bg-orange-500",yellow:"bg-yellow-600 dark:bg-yellow-600 text-white dark:text-white border-yellow-700 dark:border-yellow-500 hover:bg-yellow-700 dark:hover:bg-yellow-500",gray:"bg-gray-600 dark:bg-gray-500 text-white dark:text-white border-gray-700 dark:border-gray-400 hover:bg-gray-700 dark:hover:bg-gray-400",blue:"bg-blue-600 dark:bg-blue-600 text-white dark:text-white border-blue-700 dark:border-blue-500 hover:bg-blue-700 dark:hover:bg-blue-500"}[s]:{green:"bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/30",orange:"bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800 hover:bg-orange-100 dark:hover:bg-orange-900/30",yellow:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 hover:bg-yellow-100 dark:hover:bg-yellow-900/30",gray:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600",blue:"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30"}[s];return h.jsxs(ln,{className:`${g} ${y} ${i}`,style:a,id:c,...d,children:[h.jsx(St,{type:"checkbox",value:e,checked:t,onChange:b=>{l?.(b)},className:"hidden"}),n&&h.jsx(oe,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})},qE=({label:e,icon:t,iconPosition:n="left",position:r="right",className:s="",style:o,children:i})=>{const[a,l]=v.useState(!1),[c,d]=v.useState({top:0,left:0}),u=v.useRef(null),f=v.useRef(null),g=v.useCallback(()=>{if(!u.current)return;const p=u.current.getBoundingClientRect(),y=224;let b=r==="right"?p.right-y:p.left;const _=p.bottom+8;b<8&&(b=8),b+y>window.innerWidth-8&&(b=window.innerWidth-y-8),d({top:_,left:b})},[r]);v.useEffect(()=>{a&&g()},[a,g]),v.useEffect(()=>{if(!a)return;const p=()=>{l(!1)};return window.addEventListener("scroll",p,!0),window.addEventListener("resize",p),()=>{window.removeEventListener("scroll",p,!0),window.removeEventListener("resize",p)}},[a]),v.useEffect(()=>{const p=y=>{const b=y.target,_=u.current?.contains(b),x=f.current?.contains(b);!_&&!x&&l(!1)};return a&&document.addEventListener("mousedown",p),()=>{document.removeEventListener("mousedown",p)}},[a]);const m=a?$n.createPortal(h.jsx("div",{ref:f,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:c.top,left:c.left},onClick:()=>l(!1),children:i}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,children:[h.jsxs(ae,{ref:u,onClick:()=>l(!a),className:"flex items-center gap-2",children:[n==="left"&&t&&h.jsx(oe,{name:t,className:"w-4 h-4"}),e&&h.jsx(Q,{className:"text-sm font-medium",children:e}),n==="right"&&t&&h.jsx(oe,{name:t,className:"w-4 h-4"})]}),m]})},ZE=({label:e,icon:t,variant:n="default",disabled:r=!1,divider:s=!1,className:o="",style:i,onClick:a})=>{if(s)return h.jsx(I,{className:"h-px bg-gray-200 dark:bg-gray-700 my-1",style:i});const l=n==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",c=r?"opacity-50 cursor-not-allowed":"cursor-pointer",d=()=>{!r&&a&&a()};return h.jsxs(I,{onClick:d,className:`flex items-center gap-3 px-4 py-3Line truncated
`)>-1?e.split(`
`):e}function zO(e,t){const{element:n,datasetIndex:r,index:s}=t,o=e.getDatasetMeta(r).controller,{label:i,value:a}=o.getLabelAndValue(s);return{chart:e,label:i,parsed:o.getParsed(s),raw:e.data.datasets[r].data[s],formattedValue:a,dataset:o.getDataset(),dataIndex:s,datasetIndex:r,element:n}}function Tm(e,t){const n=e.chart.ctx,{body:r,footer:s,title:o}=e,{boxWidth:i,boxHeight:a}=t,l=Gt(t.bodyFont),c=Gt(t.titleFont),d=Gt(t.footerFont),u=o.length,f=s.length,g=r.length,m=yn(t.padding);let p=m.height,y=0,b=r.reduce((M,S)=>M+S.before.length+S.lines.length+S.after.length,0);if(b+=e.beforeBody.length+e.afterBody.length,u&&(p+=u*c.lineHeight+(u-1)*t.titleSpacing+t.titleMarginBottom),b){const M=t.displayColors?Math.max(a,l.lineHeight):l.lineHeight;p+=g*M+(b-g)*l.lineHeight+(b-1)*t.bodySpacing}f&&(p+=t.footerMarginTop+f*d.lineHeight+(f-1)*t.footerSpacing);let _=0;const x=function(M){y=Math.max(y,n.measureText(M).width+_)};return n.save(),n.font=c.string,st(e.title,x),n.font=l.string,st(e.beforeBody.concat(e.afterBody),x),_=t.displayColors?i+2+t.boxPadding:0,st(r,M=>{st(M.before,x),st(M.lines,x),st(M.after,x)}),_=0,n.font=d.string,st(e.footer,x),n.restore(),y+=m.width,{width:y,height:p}}function VO(e,t){const{y:n,height:r}=t;return n<r/2?"top":n>e.height-r/2?"bottom":"center"}function HO(e,t,n,r){const{x:s,width:o}=r,i=n.caretSize+n.caretPadding;if(e==="left"&&s+o+i>t.width||e==="right"&&s-o-i<0)return!0}function UO(e,t,n,r){const{x:s,width:o}=n,{width:i,chartArea:{left:a,right:l}}=e;let c="center";return r==="center"?c=s<=(a+l)/2?"left":"right":s<=o/2?c="left":s>=i-o/2&&(c="right"),HO(c,e,t,n)&&(c="center"),c}function Om(e,t,n){const r=n.yAlign||t.yAlign||VO(e,n);return{xAlign:n.xAlign||t.xAlign||UO(e,t,n,r),yAlign:r}}function BO(e,t){let{x:n,width:r}=e;return t==="right"?n-=r:t==="center"&&(n-=r/2),n}function jO(e,t,n){let{y:r,height:s}=e;return t==="top"?r+=n:t==="bottom"?r-=s+n:r-=s/2,r}function Am(e,t,n,r){const{caretSize:s,caretPadding:o,cornerRadius:i}=e,{xAlign:a,yAlign:l}=n,c=s+o,{topLeft:d,topRight:u,bottomLeft:f,bottomRight:g}=Ss(i);let m=BO(t,a);const p=jO(t,l,c);return l==="center"?a==="left"?m+=c:a==="right"&&(m-=c):a==="left"?m-=Math.max(d,f)+s:a==="right"&&(m+=Math.max(u,g)+s),{x:nn(m,0,r.width-t.width),y:nn(p,0,r.height-t.height)}}function Oa(e,t,n){const r=yn(n.padding);return t==="center"?e.x+e.width/2:t==="right"?e.x+e.width-r.right:e.x+r.left}function Dm(e){return Yn([],Rn(e))}function WO(e,t,n){return _s(e,{tooltip:t,tooltipItems:n,type:"tooltip"})}function Im(e,t){const n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}const Lm={beforeTitle:Zn,title(e){if(e.length>0){const t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode==="dataset")return t.dataset.label||"";if(t.label)return t.label;if(r>0&&t.dataIndex<r)return n[t.dataIndex]}return""},afterTitle:Zn,beforeBody:Zn,beforeLabel:Zn,label(e){if(this&&this.options&&this.options.mode==="dataset")return e.label+": "+e.formattedValue||e.formattedValue;let t=e.dataset.label||"";t&&(t+=": ");const n=e.formattedValue;return lt(n)||(t+=n),t},labelColor(e){const n=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{borderColor:n.borderColor,backgroundColor:n.backgroundColor,borderWidth:n.borderWidth,borderDash:n.borderDash,borderDashOffset:n.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(e){const n=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{pointStyle:n.pointStyle,rotation:n.rotation}},afterLabel:Zn,afterBody:Zn,beforeFooter:Zn,footer:Zn,afterFooter:Zn};function rn(e,t,n,r){const s=e[t].call(n,r);return typeof s>"u"?Lm[t].call(n,r):s}class Cd extends Ur{constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this._cachedAnimations;if(t)return t;const n=this.chart,r=this.options.setContext(this.getContext()),s=r.enabled&&n.options.animation&&r.animations,o=new z0(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=WO(this.chart.getContext(),this,this._tooltipItems))}getTitle(t,n){const{callbacks:r}=n,s=rn(r,"beforeTitle",this,t),o=rn(r,"title",this,t),i=rn(r,"afterTitle",this,t);let a=[];return a=Yn(a,Rn(s)),a=Yn(a,Rn(o)),a=Yn(a,Rn(i)),a}getBeforeBody(t,n){return Dm(rn(n.call Line truncated
`):e}function $O(e,t){const{element:n,datasetIndex:r,index:s}=t,o=e.getDatasetMeta(r).controller,{label:i,value:a}=o.getLabelAndValue(s);return{chart:e,label:i,parsed:o.getParsed(s),raw:e.data.datasets[r].data[s],formattedValue:a,dataset:o.getDataset(),dataIndex:s,datasetIndex:r,element:n}}function Tm(e,t){const n=e.chart.ctx,{body:r,footer:s,title:o}=e,{boxWidth:i,boxHeight:a}=t,l=Kt(t.bodyFont),c=Kt(t.titleFont),d=Kt(t.footerFont),u=o.length,f=s.length,g=r.length,m=vn(t.padding);let p=m.height,y=0,b=r.reduce((M,C)=>M+C.before.length+C.lines.length+C.after.length,0);if(b+=e.beforeBody.length+e.afterBody.length,u&&(p+=u*c.lineHeight+(u-1)*t.titleSpacing+t.titleMarginBottom),b){const M=t.displayColors?Math.max(a,l.lineHeight):l.lineHeight;p+=g*M+(b-g)*l.lineHeight+(b-1)*t.bodySpacing}f&&(p+=t.footerMarginTop+f*d.lineHeight+(f-1)*t.footerSpacing);let _=0;const x=function(M){y=Math.max(y,n.measureText(M).width+_)};return n.save(),n.font=c.string,lt(e.title,x),n.font=l.string,lt(e.beforeBody.concat(e.afterBody),x),_=t.displayColors?i+2+t.boxPadding:0,lt(r,M=>{lt(M.before,x),lt(M.lines,x),lt(M.after,x)}),_=0,n.font=d.string,lt(e.footer,x),n.restore(),y+=m.width,{width:y,height:p}}function FO(e,t){const{y:n,height:r}=t;return n<r/2?"top":n>e.height-r/2?"bottom":"center"}function zO(e,t,n,r){const{x:s,width:o}=r,i=n.caretSize+n.caretPadding;if(e==="left"&&s+o+i>t.width||e==="right"&&s-o-i<0)return!0}function VO(e,t,n,r){const{x:s,width:o}=n,{width:i,chartArea:{left:a,right:l}}=e;let c="center";return r==="center"?c=s<=(a+l)/2?"left":"right":s<=o/2?c="left":s>=i-o/2&&(c="right"),zO(c,e,t,n)&&(c="center"),c}function Om(e,t,n){const r=n.yAlign||t.yAlign||FO(e,n);return{xAlign:n.xAlign||t.xAlign||VO(e,t,n,r),yAlign:r}}function HO(e,t){let{x:n,width:r}=e;return t==="right"?n-=r:t==="center"&&(n-=r/2),n}function UO(e,t,n){let{y:r,height:s}=e;return t==="top"?r+=n:t==="bottom"?r-=s+n:r-=s/2,r}function Am(e,t,n,r){const{caretSize:s,caretPadding:o,cornerRadius:i}=e,{xAlign:a,yAlign:l}=n,c=s+o,{topLeft:d,topRight:u,bottomLeft:f,bottomRight:g}=_s(i);let m=HO(t,a);const p=UO(t,l,c);return l==="center"?a==="left"?m+=c:a==="right"&&(m-=c):a==="left"?m-=Math.max(d,f)+s:a==="right"&&(m+=Math.max(u,g)+s),{x:sn(m,0,r.width-t.width),y:sn(p,0,r.height-t.height)}}function Aa(e,t,n){const r=vn(n.padding);return t==="center"?e.x+e.width/2:t==="right"?e.x+e.width-r.right:e.x+r.left}function Dm(e){return Kn([],nr(e))}function BO(e,t,n){return Es(e,{tooltip:t,tooltipItems:n,type:"tooltip"})}function Im(e,t){const n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}const Lm={beforeTitle:Rn,title(e){if(e.length>0){const t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode==="dataset")return t.dataset.label||"";if(t.label)return t.label;if(r>0&&t.dataIndex<r)return n[t.dataIndex]}return""},afterTitle:Rn,beforeBody:Rn,beforeLabel:Rn,label(e){if(this&&this.options&&this.options.mode==="dataset")return e.label+": "+e.formattedValue||e.formattedValue;let t=e.dataset.label||"";t&&(t+=": ");const n=e.formattedValue;return dt(n)||(t+=n),t},labelColor(e){const n=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{borderColor:n.borderColor,backgroundColor:n.backgroundColor,borderWidth:n.borderWidth,borderDash:n.borderDash,borderDashOffset:n.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(e){const n=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{pointStyle:n.pointStyle,rotation:n.rotation}},afterLabel:Rn,afterBody:Rn,beforeFooter:Rn,footer:Rn,afterFooter:Rn};function on(e,t,n,r){const s=e[t].call(n,r);return typeof s>"u"?Lm[t].call(n,r):s}class Sd extends Br{constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this._cachedAnimations;if(t)return t;const n=this.chart,r=this.options.setContext(this.getContext()),s=r.enabled&&n.options.animation&&r.animations,o=new z0(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=BO(this.chart.getContext(),this,this._tooltipItems))}getTitle(t,n){const{callbacks:r}=n,s=on(r,"beforeTitle",this,t),o=on(r,"title",this,t),i=on(r,"afterTitle",this,t);let a=[];return a=Kn(a,nr(s)),a=Kn(a,nr(o)),a=Kn(a,nr(i)),a}getBeforeBody(t,n){return Dm(on(n.call Line truncated
//# sourceMappingURL=components.iife.js.map
@@ -564,6 +564,12 @@ export const TagInput: React.FC<TagInputProps> = ({
classNamePrefix: 'tag-input',
unstyled: true,
classNames: commonClassNames,
// react-select 는 menuPortal 에 zIndex:1 을 인라인 스타일로 강제 주입한다.
// classNames.menuPortal(Tailwind 클래스)는 인라인 스타일에 밀려 무력화되므로
// styles.menuPortal 로 직접 오버라이드해야 sticky 푸터(z-10) 등에 가려지지 않는다.
styles: {
menuPortal: (base: any) => ({ ...base, zIndex: 9999 }),
},
// 구분자 기반 태그 분리 또는 외부 onInputChange 콜백이 있을 때 제어 컴포넌트 설정
...((creatable && isMulti) || onInputChangeProp ? {
inputValue,
@@ -112,6 +112,35 @@ describe('TagInput 컴포넌트', () => {
});
});
describe('메뉴 포탈 z-index', () => {
// 회귀(게시판 설정 일괄 적용 화면 결함): react-select 는 menuPortal 에
// zIndex:1 을 인라인 스타일로 강제 주입하며, 이는 Tailwind classNames
// 보다 우선 적용된다. styles.menuPortal 오버라이드가 없으면 드롭다운이
// sticky 하단 버튼바(z-10) 등 낮은 요소에도 가려질 수 있었다.
it('메뉴 포탈이 sticky 요소보다 높은 z-index를 인라인 스타일로 갖는다', async () => {
const user = userEvent.setup();
render(
<TagInput
value={[]}
options={mockOptions}
onChange={() => {}}
/>
);
const input = screen.getByRole('combobox');
await user.click(input);
await screen.findByText('A/S');
const portal = document.body.querySelector('.tag-input__menu-portal') as HTMLElement | null;
expect(portal).toBeTruthy();
// react-select(emotion)는 zIndex 를 style 속성이 아닌 생성된 CSS 클래스로 적용하므로
// getComputedStyle 로 확인해야 한다 (element.style.zIndex 는 항상 빈 값).
expect(Number(getComputedStyle(portal!).zIndex)).toBeGreaterThanOrEqual(9999);
});
});
describe('onBeforeRemove 콜백', () => {
it('onBeforeRemove가 true를 반환하면 삭제된다', async () => {
const handleChange = vi.fn();
@@ -12,6 +12,7 @@
- 주문서의 무통장입금 영역에 현금영수증 신청 폼이 표시될 수 있도록 확장 영역을 마련했습니다. 신청 폼 자체는 쇼핑몰 기능이 제공합니다.
- 주문 상세의 결제 정보 영역에 현금영수증 발급 내역이 표시될 수 있도록 확장 영역을 마련했습니다.
- 주문서에서 결제 플러그인이 등록한 결제수단(예: "가상계좌 (토스페이먼츠)")을 선택해 주문할 수 있습니다. 선택한 결제수단은 주문 시 대응하는 기본 결제수단으로 전송되어, 결제창 호출과 이후 주문 처리가 정상적으로 이어집니다.
- 게시글 상세 화면에 추천·비추천 반응 버튼을 추가했습니다. 로그인한 회원은 버튼을 눌러 반응을 남기거나, 다른 반응으로 바꾸거나, 같은 버튼을 다시 눌러 취소할 수 있으며 각 반응의 개수가 함께 표시됩니다. 버튼을 누르면 즉시 반응 표시가 반영되고, 처리 중에는 잠깐 로딩이 표시되며, 처리에 실패하면 원래 상태로 되돌리고 안내합니다. 반응 버튼은 게시글 하단의 수정·삭제 버튼과 같은 줄에 배치됩니다. 본인이 쓴 글에서 버튼을 누르면 반응할 수 없다는 안내가 표시되고, 로그인하지 않은 경우 안내 후 로그인 화면으로 이동합니다. (게시판에서 반응 기능을 켠 경우에만 표시됩니다.)
- 회원가입 화면에 휴대폰번호·전화번호 입력란을 추가했습니다. 두 항목 모두 선택 사항입니다.
### Changed
@@ -0,0 +1,243 @@
/**
* @file board-reaction-buttons.test.tsx
* @description 게시글 상세 반응(추천/비추천) 버튼 레이아웃 회귀 (이슈 #525)
*
* 검증 방식: 레이아웃 JSON 트리 직접 분석 (board 파티션 테스트 관례 — show.json 은 partial).
* 고정 대상:
* - 반응 영역은 use_reaction && reaction_type_options.length > 0 일 때만 노출 (확정 11)
* - reaction_type_options 를 iteration 으로 순회 (확정 02·유형 동적 렌더)
* - 유형별 개수는 reaction_counts[유형 ID] 를 바인딩, 없으면 0 (확정 09)
* - 본인 글이면 버튼 disabled (확정 08)
* - 클릭 시 비로그인은 안내+로그인 유도, 로그인 사용자는 react API 호출 후 post 재조회 (확정 03·07)
*/
import { describe, it, expect } from 'vitest';
import basicShow from '../../layouts/partials/board/types/basic/show.json';
type Node = Record<string, unknown> & { children?: unknown };
function collectNodes(node: unknown, predicate: (n: Node) => boolean): Node[] {
const result: Node[] = [];
const walk = (cur: unknown): void => {
if (Array.isArray(cur)) {
cur.forEach(walk);
return;
}
if (cur && typeof cur === 'object') {
const obj = cur as Node;
if (predicate(obj)) {
result.push(obj);
}
Object.values(obj).forEach(walk);
}
};
walk(node);
return result;
}
function findByComment(root: unknown, commentText: string): Node[] {
return collectNodes(root, (n) => typeof n.comment === 'string' && (n.comment as string).includes(commentText));
}
describe('이슈 #525 — 게시글 상세 반응 버튼 (basic/show.json)', () => {
// @scenario case=detail_hidden_when_no_active_types
// @effects reaction_area_hidden_when_no_active_types
it('반응 영역은 use_reaction + reaction_type_options 존재 시에만 노출된다 (확정 11)', () => {
// 이 if 는 use_reaction 과 reaction_type_options 둘 다에 의존하므로
// reaction off / 유형 0개 두 케이스 모두 이 조건으로 미노출된다.
// @scenario case=detail_hidden_when_use_reaction_off
// @effects reaction_area_hidden_when_use_reaction_off
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
expect(areas.length).toBe(1);
const area = areas[0];
expect(typeof area.if).toBe('string');
expect(area.if as string).toContain('use_reaction');
expect(area.if as string).toContain('reaction_type_options');
expect(area.if as string).toContain('length > 0');
});
// @scenario case=reaction_area_hidden_on_inaccessible_secret_post
// @effects reaction_area_hidden_when_secret_content_inaccessible
it('비밀글이면서 본문 열람 권한이 없으면(content=null) 반응 영역이 미노출된다 (신고와 동일 가드)', () => {
// 비밀글 본문을 못 보는 사용자가 반응만 남기는 우회를 막는다.
// 신고 버튼과 동일한 (!is_secret || content !== null) 가드를 반응 영역 if 에도 둔다.
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
expect(areas.length).toBe(1);
const cond = areas[0].if as string;
expect(cond).toContain('is_secret');
expect(cond).toContain('content !== null');
});
// @scenario case=detail_shows_active_types_even_zero
// @effects active_types_always_shown_even_zero_count
it('반응 유형을 reaction_type_options 로 iteration 한다', () => {
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
const iterations = collectNodes(areas[0], (n) => n.iteration !== undefined);
expect(iterations.length).toBeGreaterThan(0);
const iter = iterations[0].iteration as Record<string, unknown>;
expect(iter.source as string).toContain('reaction_type_options');
expect(iter.item_var).toBe('reactionType');
});
// @scenario case=author_buttons_disabled
// @effects author_sees_buttons_disabled, active_types_always_shown_even_zero_count, my_reaction_type_highlighted
it('유형별 버튼은 처리 중이면 disabled, 본인 글이면 시각적 비활성 톤 + 클릭 시 안내(disabled 미부여)로 개수를 바인딩한다 (확정 08·09)', () => {
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
const buttons = collectNodes(areas[0], (n) => n.name === 'Button');
expect(buttons.length).toBeGreaterThan(0);
const btn = buttons[0];
const props = btn.props as Record<string, unknown>;
expect(props.type).toBe('button');
// disabled 는 처리 중(reactingTypeId)에만 건다 — disabled 버튼은 클릭 이벤트가
// 발생하지 않아 본인 글 안내 토스트를 띄울 수 없으므로, 본인 글은 disabled 대신
// className 의 시각적 톤 + switch 핸들러의 self_post 분기로 처리한다.
expect(String(props.disabled)).toContain('reactingTypeId');
expect(String(props.disabled)).not.toContain('is_author');
expect(String(props.className)).toContain('is_author');
expect(String(props['aria-pressed'])).toBeTruthy();
expect(String(props['aria-disabled'])).toContain('reactingTypeId');
// 개수 바인딩 (reaction_counts[유형 ID] ?? 0)
const counts = collectNodes(btn, (n) =>
typeof n.text === 'string' && (n.text as string).includes('reaction_counts')
);
expect(counts.length).toBe(1);
expect(counts[0].text as string).toContain('reactionType.id');
expect(counts[0].text as string).toContain('?? 0');
});
it('클릭 액션은 비로그인/로그인 분기 — 로그인은 낙관 하이라이트+react API, 비로그인은 안내+로그인 (확정 03·07)', () => {
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
const buttons = collectNodes(areas[0], (n) => n.name === 'Button');
const actions = (buttons[0].actions as Node[]) ?? [];
expect(actions.length).toBeGreaterThan(0);
const action = actions[0];
expect(action.handler).toBe('switch');
const cases = action.cases as Record<string, Node>;
expect(cases.logged_in).toBeDefined();
expect(cases.not_logged_in).toBeDefined();
// 로그인 사용자: sequence(낙관 하이라이트 setState → react apiCall)
const loggedIn = cases.logged_in;
expect(loggedIn.handler).toBe('sequence');
const seq = loggedIn.actions as Node[];
// 첫 액션: 클릭 즉시 하이라이트 낙관 이동 + 처리 중 표시
const firstSetState = seq[0];
expect(firstSetState.handler).toBe('setState');
const firstParams = firstSetState.params as Record<string, unknown>;
expect(firstParams.reactingTypeId).toBeDefined();
expect(firstParams.myReactionTypeId).toBeDefined();
// 개수는 낙관적으로 건드리지 않는다 (reactionCounts 는 응답에서만)
expect(firstParams.reactionCounts).toBeUndefined();
// apiCall: react + 응답값으로 확정(refetch 없음)
const apiCall = seq.find((a) => a.handler === 'apiCall')!;
expect(apiCall).toBeDefined();
expect(apiCall.target as string).toContain('/react');
const onSuccess = apiCall.onSuccess as Node[];
// 성공: refetch 대신 응답값(reaction_counts/my_reaction_type_id)으로 setState
expect(onSuccess.some((a) => a.handler === 'refetchDataSource')).toBe(false);
const successSet = onSuccess.find((a) => a.handler === 'setState')!;
const successParams = successSet.params as Record<string, unknown>;
expect(String(successParams.reactionCounts)).toContain('response.data.reaction_counts');
expect(String(successParams.myReactionTypeId)).toContain('response.data.my_reaction_type_id');
// 비로그인: 안내 후 로그인 유도 (redirect 파라미터 전달)
const guest = cases.not_logged_in;
const guestActions = guest.actions as Node[];
expect(guestActions.some((a) => a.handler === 'toast')).toBe(true);
const navAction = guestActions.find((a) => a.handler === 'navigate');
expect(navAction).toBeDefined();
const navParams = navAction!.params as Record<string, unknown>;
expect(navParams.path).toBe('/login');
expect((navParams.query as Record<string, unknown>).redirect).toContain('/board/');
});
// @scenario case=react_failure_rolls_back
// @effects reaction_failure_rolls_back_optimistic_state
it('실패 시 낙관 상태를 롤백하고 안내 토스트를 발화한다 (onError)', () => {
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
const buttons = collectNodes(areas[0], (n) => n.name === 'Button');
const action = (buttons[0].actions as Node[])[0];
const apiCall = ((action.cases as Record<string, Node>).logged_in.actions as Node[])
.find((a) => a.handler === 'apiCall')!;
const onError = apiCall.onError as Node[];
expect(onError.length).toBeGreaterThan(0);
// 롤백: override 를 초기값으로 되돌리는 setState (reactionApplied=false, override 클리어)
const rollback = onError.find((a) => a.handler === 'setState')!;
expect(rollback).toBeDefined();
const rp = rollback.params as Record<string, unknown>;
expect(rp.reactionApplied).toBe(false);
expect(rp.myReactionTypeId).toBe(null);
expect(rp.reactionCounts).toBe(null);
expect(rp.reactingTypeId).toBe(null);
// 안내 토스트 (하드코딩 아닌 $t: 키)
const toast = onError.find((a) => a.handler === 'toast')!;
expect(toast).toBeDefined();
const tp = toast.params as Record<string, unknown>;
expect(tp.type).toBe('error');
expect(String(tp.message)).toContain('$t:');
});
// @scenario case=react_pending_disables_button
// @effects reaction_pending_disables_button
it('처리 중(reactingTypeId)에는 버튼이 비활성이고 스피너가 표시된다 (중복 클릭 차단)', () => {
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
const buttons = collectNodes(areas[0], (n) => n.name === 'Button');
const btn = buttons[0];
const props = btn.props as Record<string, unknown>;
// disabled 가 reactingTypeId 에 의존 (처리 중 비활성)
expect(String(props.disabled)).toContain('reactingTypeId');
// 스피너: 처리 중인 유형이면 fa-spinner + spin
const icon = collectNodes(btn, (n) => n.name === 'Icon')[0];
const iconProps = icon.props as Record<string, unknown>;
expect(String(iconProps.name)).toContain('fa-spinner');
expect(String(iconProps.spin)).toContain('reactingTypeId');
});
// @scenario case=reaction_active_button_color_only_highlight
// @effects reaction_active_button_color_highlight, reaction_click_moment_tactile_pulse
it('활성 상태는 배경·테두리 색상으로만 표시하고, 클릭 순간에는 두 분기 모두 active:scale-95 로 살짝 눌린다 (상시 확대 제거)', () => {
const areas = findByComment(basicShow, '반응(추천/비추천) 영역 — 반응 사용 on');
const buttons = collectNodes(areas[0], (n) => n.name === 'Button');
const btn = buttons[0];
const props = btn.props as Record<string, unknown>;
const className = String(props.className);
// 표현식 전체를 감싸는 최상위 삼항 " ? '...' : '...' " 만 분리 —
// 조건절 내 옵셔널 체이닝(?.)과 혼동하지 않도록 "? '" 패턴으로 삼항 시작점을 찾는다
const ternaryStart = className.indexOf("? '") + 1;
const ternaryBody = className.slice(ternaryStart + 1);
const [activeBranch, inactiveBranch] = ternaryBody.split(" : '");
// 활성(참) 분기: 색상만으로 하이라이트, 상시 확대(scale-105)·그림자(shadow-md) 없음
expect(activeBranch).toContain('bg-blue-50');
expect(activeBranch).toContain('border-blue-500');
expect(activeBranch).not.toContain('scale-105');
expect(activeBranch).not.toContain('shadow-md');
// 비활성(거짓) 분기도 상시 축소(scale-95 상태 유지)·불투명도 저하 없음
expect(inactiveBranch).not.toContain('opacity-90');
// 두 분기 모두 클릭 순간(:active 의사클래스)에만 살짝 눌리는 순수 CSS 펄스
expect(activeBranch).toContain('active:scale-95');
expect(inactiveBranch).toContain('active:scale-95');
// transform 전환은 부드럽게 (transition-transform + duration-150)
expect(activeBranch).toContain('transition-transform');
expect(activeBranch).toContain('duration-150');
expect(inactiveBranch).toContain('transition-transform');
expect(inactiveBranch).toContain('duration-150');
});
});
@@ -141,7 +141,10 @@
"laugh": "Funny",
"agree": "Agree",
"thanks": "Thanks",
"wow": "Wow"
"wow": "Wow",
"login_required": "You need to sign in to use this feature.",
"failed": "Failed to process the reaction. Please try again.",
"self_post_denied": "You cannot react to your own post."
},
"attachments": {
"title": "Attachments",
@@ -141,7 +141,10 @@
"laugh": "웃겨요",
"agree": "동의해요",
"thanks": "감사해요",
"wow": "놀라워요"
"wow": "놀라워요",
"login_required": "로그인이 필요한 기능입니다",
"failed": "반응 처리에 실패했습니다. 잠시 후 다시 시도해주세요.",
"self_post_denied": "본인 글에는 반응할 수 없습니다."
},
"attachments": {
"title": "첨부파일",
@@ -190,7 +190,7 @@
"name": "Span",
"if": "{{post?.data?.is_new}}",
"props": {
"className": "px-2 py-1 text-xs font-bold bg-red-500 text-white rounded flex-shrink-0"
"className": "px-2 py-1 text-xs font-bold bg-red-500 dark:bg-red-600 text-white rounded flex-shrink-0"
},
"text": "N"
}
@@ -928,12 +928,178 @@
]
},
{
"comment": "게시글 액션 버튼 영역 (답변, 신고, 수정, 삭제) - 비회원 글은 항상 표시 (수정/삭제용)",
"comment": "게시글 하단 바 — 반응(추천/비추천) 영역과 액션 버튼(답변/신고/수정/삭제)을 한 줄에 좌우로 배치. 반응은 왼쪽, 액션은 오른쪽. 둘 중 하나만 표시될 때도 justify-between 으로 정렬 유지. 둘 다 없으면 바 자체 미노출.",
"type": "basic",
"name": "Div",
"if": "{{(post?.data?.board?.use_reaction && (post?.data?.board?.reaction_type_options ?? []).length > 0) || post?.data?.is_guest_post || post?.data?.is_author || post?.data?.abilities?.can_manage || post?.data?.board?.use_reply || (_global.currentUser?.uuid && post?.data?.board?.use_report)}}",
"props": {
"className": "flex flex-wrap items-center justify-between gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-700"
},
"children": [
{
"comment": "반응(추천/비추천) 영역 — 반응 사용 on + 게시판이 켠 유형이 1개 이상일 때만 노출(확정 11). 활성 유형을 iteration 으로 순회해 유형별 버튼(아이콘+라벨+개수, 개수 0도 항상 노출, 확정 09). 본인 글은 버튼/개수는 노출하되 클릭 불가(확정 08). 비로그인은 안내 후 로그인 유도, 로그인 사용자는 같은 유형 재클릭=해제·다른 유형=전환.",
"type": "basic",
"name": "Div",
"if": "{{post?.data?.board?.use_reaction && (post?.data?.board?.reaction_type_options ?? []).length > 0 && (!post?.data?.is_secret || post?.data?.content !== null)}}",
"props": {
"className": "flex flex-wrap items-center gap-2"
},
"children": [
{
"type": "basic",
"name": "Div",
"iteration": {
"source": "{{post?.data?.board?.reaction_type_options ?? []}}",
"item_var": "reactionType",
"index_var": "reactionIdx"
},
"children": [
{
"comment": "반응 유형 버튼 — 하이라이트는 클릭 즉시 이동(_local.myReactionTypeId override, 없으면 서버값), 개수는 react API 응답의 reaction_counts 로 갱신(_local.reactionCounts override). 처리 중(_local.reactingTypeId)은 짧은 로딩(스피너+비활성, 실제 disabled). 본인 글은 시각적으로만 비활성 톤이며 disabled 는 걸지 않는다 — disabled 버튼은 클릭 이벤트 자체가 발생하지 않아 안내 토스트를 띄울 수 없으므로, 클릭은 받고 switch 분기에서 토스트로 안내한다. 실패 시 override 를 클릭 전 값으로 롤백. 활성 상태는 배경색+테두리+하단 밑줄로 표시(상시 확대는 제거 — 부자연스럽다는 PO 피드백). 클릭하는 순간에만 active:scale-95 로 살짝 눌리는 촉각 피드백을 주며, 손을 떼면 즉시 원래 크기로 복귀(순수 CSS :active 의사클래스, 상태 관리 불필요).",
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"disabled": "{{(_local?.reactingTypeId ?? null) !== null}}",
"aria-pressed": "{{(_local?.reactionApplied ? _local?.myReactionTypeId : post?.data?.my_reaction_type_id) === reactionType.id}}",
"aria-disabled": "{{(_local?.reactingTypeId ?? null) !== null}}",
"className": "{{(_local?.reactionApplied ? _local?.myReactionTypeId : post?.data?.my_reaction_type_id) === reactionType.id ? 'px-4 py-2 text-sm border border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-lg flex items-center gap-1.5 font-medium active:scale-95 transition-transform duration-150 ease-out' : 'px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-1.5 active:scale-95 transition-transform duration-150 ease-out'}}{{(post?.data?.is_author || (_local?.reactingTypeId ?? null) !== null) ? ' cursor-not-allowed opacity-60' : ' cursor-pointer'}}"
},
"actions": [
{
"type": "click",
"handler": "switch",
"params": {
"value": "{{post?.data?.is_author ? 'self_post' : (_global.currentUser?.uuid ? 'logged_in' : 'not_logged_in')}}"
},
"cases": {
"self_post": {
"handler": "toast",
"params": {
"type": "info",
"message": "$t:board.reaction.self_post_denied"
}
},
"logged_in": {
"handler": "sequence",
"actions": [
{
"comment": "클릭 즉시 — 하이라이트를 눌린 유형으로 낙관 이동(같은 유형 재클릭이면 해제 예측), 처리 중 표시. 개수는 건드리지 않고 응답값으로만 확정.",
"handler": "setState",
"params": {
"target": "local",
"reactingTypeId": "{{reactionType.id}}",
"reactionApplied": true,
"myReactionTypeId": "{{(_local?.reactionApplied ? _local?.myReactionTypeId : post?.data?.my_reaction_type_id) === reactionType.id ? null : reactionType.id}}"
}
},
{
"handler": "apiCall",
"target": "/api/modules/sirsoft-board/boards/{{route.slug}}/posts/{{route.id}}/react",
"auth_mode": "required",
"params": {
"method": "POST",
"body": {
"reaction_type_id": "{{reactionType.id}}"
}
},
"onSuccess": [
{
"comment": "서버 확정값으로 하이라이트·개수 갱신 (refetch 없이 응답값만). 응답만 오면 개수는 100% 정확.",
"handler": "setState",
"params": {
"target": "local",
"reactingTypeId": null,
"reactionApplied": true,
"myReactionTypeId": "{{response.data.my_reaction_type_id ?? null}}",
"reactionCounts": "{{response.data.reaction_counts ?? {}}}"
}
}
],
"onError": [
{
"comment": "실패 롤백 — override 를 서버 초기값(post.data)으로 되돌리고 로딩 해제",
"handler": "setState",
"params": {
"target": "local",
"reactingTypeId": null,
"reactionApplied": false,
"myReactionTypeId": null,
"reactionCounts": null
}
},
{
"handler": "toast",
"params": {
"type": "error",
"message": "$t:board.reaction.failed"
}
}
]
}
]
},
"not_logged_in": {
"handler": "sequence",
"actions": [
{
"handler": "toast",
"params": {
"type": "info",
"message": "$t:board.reaction.login_required"
}
},
{
"handler": "navigate",
"params": {
"path": "/login",
"query": {
"redirect": "/board/{{route.slug}}/{{route.id}}"
}
}
}
]
}
}
}
],
"children": [
{
"comment": "처리 중인 유형은 스피너, 그 외엔 유형 아이콘",
"type": "basic",
"name": "Icon",
"props": {
"name": "{{(_local?.reactingTypeId ?? null) === reactionType.id ? 'fa-spinner' : (reactionType.icon_name ?? 'fa-thumbs-up')}}",
"spin": "{{(_local?.reactingTypeId ?? null) === reactionType.id}}",
"size": "sm"
}
},
{
"type": "basic",
"name": "Span",
"text": "{{reactionType.name ?? ''}}"
},
{
"type": "basic",
"name": "Span",
"props": {
"className": "tabular-nums"
},
"text": "{{(_local?.reactionCounts ?? post?.data?.reaction_counts)?.[reactionType.id] ?? 0}}"
}
]
}
]
}
]
},
{
"comment": "액션 버튼 영역 (답변, 신고, 수정, 삭제) - 비회원 글은 항상 표시 (수정/삭제용). 하단 바 오른쪽에 배치. 반응 영역이 없으면 justify-between 이 이 그룹만 오른쪽으로 민다.",
"type": "basic",
"name": "Div",
"if": "{{post?.data?.is_guest_post || post?.data?.is_author || post?.data?.abilities?.can_manage || post?.data?.board?.use_reply || (_global.currentUser?.uuid && post?.data?.board?.use_report)}}",
"props": {
"className": "flex justify-end gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-700"
"className": "flex items-center justify-end gap-2 ml-auto"
},
"children": [
{
@@ -1143,6 +1309,8 @@
}
]
}
]
}
]
},
{