# GLOBEAU DAM(Digital Asset Management) 시스템 개발 및 리팩토링 종합 보고서

본 문서는 Windows 파일 탐색기 스타일의 독립형 이미지 자산 관리(DAM) 시스템 개발 과정에서 발생한 핵심 이슈, 기술적 병목의 원인 분석 및 해결 방안, 그리고 최종 확정된 컴포넌트별 소스코드를 정리한 종합 기록 문서입니다.

---

## 1. 프로젝트 개요 및 환경

1. 개발 환경
    - Framework: Laravel 13.23.0
    - Language: PHP 8.4.26
    - Database: MariaDB (globeau_db)
    - Frontend: Vanilla JavaScript, Bootstrap 5, FontAwesome 6, SweetAlert2
2. 주요 시스템 아키텍처
    - 파일/폴더 다대다 관계: `image_folder_pivot` 피벗 테이블 기반
    - 계층형 폴더 구조: `image_folders` 테이블 (`parent_id` 기반 셀프 조인 트리)
    - UI 레이아웃: 3열 분할 구조 (1열: 폴더 트리, 2열: 갤러리/그리드, 3열: 메타데이터 인스펙터)
    - 테마 표준: 관리자 공용 테마 6px 라운딩 (`var(--app-radius, 6px)`) 표준 준수

---

## 2. 발생한 주요 이슈 및 원인 분석과 해결 방안

### 2.1. 이미지 및 폴더 드래그 앤 드롭 미작동 현상
- 원인:
    1. 카드 내부 엘리먼트(`<img>`, 텍스트 등)의 브라우저 기본 드래그 동작 간섭으로 부모 `.win-thumb-card`의 커스텀 DragStart 이벤트가 씹힘.
    2. 드래그 고스트 요소(`setDragImage`)가 화면 밖 또는 렌더링되지 않는 위치에 있어 브라우저 엔진이 드래그를 취소함.
    3. 트리 요소가 `<button>`일 때 브라우저 엔진에 따라 드롭 거부 현상 발생.
- 해결:
    1. CSS에 `.win-thumb-card * { pointer-events: none; user-select: none; }`를 적용하여 모든 마우스 이벤트를 최상위 카드로 집중.
    2. 드래그 시작 시 선택된 ID들을 JS 메모리 전역 배열(`draggedImageIds`)에 직접 보관하여 크로스 브라우징 안정성 확보.
    3. `dragover` 시 `e.preventDefault()`와 `dropEffect = 'move'`를 선언하고, 드롭 타깃에 `.drag-hover` 시각적 피드백 제공.

### 2.2. 마우스 Marquee(러버밴드) 영역 다중 선택 및 유지 실패
- 원인:
    1. 2개 이상의 카드를 선택한 후 마우스를 뗄 때, 마우스업 직후 발생하는 브라우저의 전역 `click` 이벤트가 여백 클릭으로 인식되어 `clearAllSelection()`을 호출함.
    2. 선택 해제 시 상태바 뱃지와 인스펙터 폼의 잔여 데이터가 지워지지 않고 계속 노출됨.
- 해결:
    1. `dragPreventClick` 플래그를 도입하여 마키 드래그 영역이 일정 픽셀 이상 이동한 경우 마우스업 직후의 가짜 클릭 이벤트를 무시하도록 차단.
    2. `syncSelectionUI()` 함수에서 선택된 항목이 0개일 때 상태바 뱃지를 강제로 숨기고(`classList.add('d-none')`), 인스펙터 폼을 완전 초기화하는 `closeInspector()`를 반드시 호출하도록 보강.

### 2.3. 인스펙터 패널 DOM 파괴 현상
- 원인:
    - 다중 선택 시 `renderMultiSelectInspector()`가 실행되면서 `.win-inspector-scroll-area` 내부를 `innerHTML = ...`로 덮어써버려, 단일 수정 폼(`#inspectorForm`, `#insTitle` 등)이 DOM에서 영구적으로 소멸됨.
    - 이후 1개 항목을 다시 선택하더라도 주입 대상 엘리먼트가 없어 인스펙터가 이전 텍스트("몇 개 선택됨...") 상태로 멈춤.
- 해결:
    - 단일 선택 뷰(`#inspectorForm`)와 다중 선택 뷰(`#multiInspectorView`)를 HTML 상에 영구 유지하고, `d-none` / `d-flex` 클래스 토글 방식으로 상태만 전환하도록 리팩토링.
    - `closeInspector()` 호출 시 폼 리셋(`reset()`) 및 미리보기 이미지 소스를 비워 잔류 데이터 방지.

### 2.4. MariaDB 1062 복합 유니크 키 충돌 에러
- 에러 내용:
    - `Integrity constraint violation: 1062 Duplicate entry '3-28' for key 'image_folder_unique'`
- 원인:
    - 이미 대상 폴더(3번)에 매핑되어 있는 이미지(28번)를 다시 이동 처리하거나, 트랜잭션 도중 단순 `insert`를 수행할 때 유니크 제약 조건 위반 발생.
- 해결:
    - `ImageFolderController@moveOrCopyFiles`에서 단건 삽입 대신 `DB::table('image_folder_pivot')->insertOrIgnore($insertData);`를 적용하여 중복 키 충돌 시 MariaDB 차원에서 안전하게 통과하도록 처리.

### 2.5. 이미지와 폴더 동시 다중 이동 및 이동 후 폴더 자동 진입
- 요구사항:
    - 폴더와 이미지가 함께 선택되어 드래그될 때 둘 다 한 번에 이동되어야 하며, 이동 완료 후 사용자가 즉시 확인할 수 있도록 대상 폴더로 화면이 자동 전환되어야 함.
- 해결:
    - 드래그 페이로드를 `{ type: 'combined-move', image_ids: [...], folder_ids: [...] }` 형태로 통합.
    - 백엔드에서 폴더 계층 변경(`parent_id` 갱신)과 이미지 피벗 이동을 단일 트랜잭션으로 처리.
    - 프론트엔드 통신 완료 후 `selectFolder(targetFolderId, targetFolderName)`를 호출하여 대상 폴더로 즉시 자동 내비게이션.

### 2.6. 정렬 버튼 오름/내림차순 토글 및 Active/Hover 스타일 오류
- 원인:
    - 블레이드 파일의 버튼 클래스에 `.sort-toggle-btn` 대신 `.view-mode-btn`이 혼용되어 선언됨.
    - Bootstrap의 기본 `.btn-outline-secondary` 클래스의 `:focus`, `:active` 회색 오버라이드가 커스텀 테마 블루 색상을 덮어버림.
- 해결:
    - 블레이드의 클래스를 `.sort-toggle-btn`으로 표준화하고 ID를 일치시킴.
    - CSS에서 우선순위(`!important`)를 높여 `:hover`, `.active`, `:focus` 상태에서 배경 블루 + 글자/아이콘 순백색(`#ffffff`)이 적용되도록 강제 설정.
    - JS `updateMediaSortUI()`에서 `currentSortDirection`에 따라 아이콘 클래스를 `fa-arrow-up-short-wide`(▲)와 `fa-arrow-down-short-wide`(▼)로 정확하게 반전.

---

## 3. 백엔드 컨트롤러 소스코드

### 3.1. `app/Http/Controllers/Admin/ImageFolderController.php`

    <?php

    namespace App\Http\Controllers\Admin;

    use App\Http\Controllers\Controller;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Log;

    class ImageFolderController extends Controller
    {
        public function index()
        {
            try {
                $rawFolders = DB::table('image_folders')->orderBy('name', 'asc')->get();

                $countMap = DB::table('image_folder_pivot')
                    ->select('folder_id', DB::raw('count(*) as aggregate'))
                    ->groupBy('folder_id')
                    ->pluck('aggregate', 'folder_id')
                    ->toArray();

                $folderMap = [];
                foreach ($rawFolders as $f) {
                    $folderMap[$f->id] = [
                        'id'           => (int)$f->id,
                        'parent_id'    => $f->parent_id ? (int)$f->parent_id : null,
                        'name'         => (string)$f->name,
                        'images_count' => (int)($countMap[$f->id] ?? 0),
                        'has_children' => false,
                        'depth'        => 0,
                    ];
                }

                $childrenMap = [];
                foreach ($folderMap as $id => $item) {
                    $pid = $item['parent_id'] ?? 0;
                    $childrenMap[$pid][] = $id;
                    if ($pid && isset($folderMap[$pid])) {
                        $folderMap[$pid]['has_children'] = true;
                    }
                }

                $flatTree = [];
                $traverse = function ($parentId, $depth) use (&$traverse, &$flatTree, $childrenMap, $folderMap) {
                    if (!isset($childrenMap[$parentId])) return;
                    foreach ($childrenMap[$parentId] as $childId) {
                        $node = $folderMap[$childId];
                        $node['depth'] = $depth;
                        $flatTree[] = $node;
                        $traverse($childId, $depth + 1);
                    }
                };
                $traverse(0, 0);

                if (empty($flatTree) && !empty($folderMap)) {
                    $flatTree = array_values($folderMap);
                }

                return response()->json([
                    'success' => true,
                    'folders' => $flatTree
                ]);

            } catch (\Throwable $e) {
                Log::error('ImageFolder index error: ' . $e->getMessage());
                return response()->json([
                    'success' => false,
                    'message' => '폴더 목록 조회 중 오류: ' . $e->getMessage(),
                    'folders' => []
                ], 200);
            }
        }

        public function store(Request $request)
        {
            $request->validate([
                'name'      => 'required|string|max:100',
                'parent_id' => 'nullable|integer|exists:image_folders,id',
            ]);

            $name = trim($request->input('name'));
            $parentId = $request->input('parent_id') ? (int)$request->input('parent_id') : null;

            try {
                $exists = DB::table('image_folders')
                    ->where('name', $name)
                    ->where('parent_id', $parentId)
                    ->exists();

                if ($exists) {
                    return response()->json([
                        'success' => false,
                        'message' => '동일한 위치에 같은 이름의 폴더가 이미 존재합니다.'
                    ], 422);
                }

                $id = DB::table('image_folders')->insertGetId([
                    'name'       => $name,
                    'parent_id'  => $parentId,
                    'created_by' => auth()->id() ?? null,
                    'created_at' => now(),
                    'updated_at' => now(),
                ]);

                return response()->json([
                    'success' => true,
                    'message' => '폴더가 생성되었습니다.',
                    'folder'  => [
                        'id'           => $id,
                        'name'         => $name,
                        'parent_id'    => $parentId,
                        'images_count' => 0
                    ]
                ]);

            } catch (\Throwable $e) {
                return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
            }
        }

        public function rename(Request $request, $id)
        {
            $request->validate([
                'name' => 'required|string|max:50',
            ]);

            $folderId = (int)$id;
            $newName = trim($request->input('name'));

            $folder = DB::table('image_folders')->where('id', $folderId)->first();
            if (!$folder) {
                return response()->json([
                    'success' => false, 
                    'message' => '대상 폴더를 찾을 수 없습니다.'
                ], 404);
            }

            $isDuplicate = DB::table('image_folders')
                ->where('parent_id', $folder->parent_id)
                ->where('name', $newName)
                ->where('id', '!=', $folderId)
                ->exists();

            if ($isDuplicate) {
                return response()->json([
                    'success' => false, 
                    'message' => '같은 위치에 동일한 이름의 폴더가 이미 존재합니다.'
                ], 422);
            }

            DB::table('image_folders')
                ->where('id', $folderId)
                ->update([
                    'name'       => $newName,
                    'updated_at' => now(),
                ]);

            return response()->json([
                'success' => true,
                'message' => '폴더 이름이 성공적으로 변경되었습니다.',
                'name'    => $newName
            ]);
        }

        public function destroy($id)
        {
            $folderId = (int)$id;

            $targetFolder = DB::table('image_folders')->where('id', $folderId)->first();
            if (!$targetFolder) {
                return response()->json([
                    'success' => false,
                    'message' => '존재하지 않는 폴더입니다.'
                ], 404);
            }

            try {
                DB::beginTransaction();

                $allFolderIds = $this->getAllDescendantFolderIdsWithSelf($folderId);

                DB::table('image_folder_pivot')
                    ->whereIn('folder_id', $allFolderIds)
                    ->delete();

                DB::table('image_folders')
                    ->whereIn('id', $allFolderIds)
                    ->delete();

                DB::commit();

                return response()->json([
                    'success' => true,
                    'message' => "'{$targetFolder->name}' 폴더 및 하위 폴더가 모두 삭제되었습니다.",
                    'deleted_folder_ids' => $allFolderIds
                ]);

            } catch (\Throwable $e) {
                DB::rollBack();
                Log::error('Cascade delete folder error: ' . $e->getMessage());

                return response()->json([
                    'success' => false,
                    'message' => '폴더 삭제 중 오류: ' . $e->getMessage()
                ], 500);
            }
        }

        private function getAllDescendantFolderIdsWithSelf(int $folderId): array
        {
            $result = [$folderId];
            $childIds = DB::table('image_folders')
                ->where('parent_id', $folderId)
                ->pluck('id')
                ->toArray();

            foreach ($childIds as $childId) {
                $result = array_merge($result, $this->getAllDescendantFolderIdsWithSelf((int)$childId));
            }

            return array_values(array_unique($result));
        }

        public function moveOrCopyFiles(Request $request)
        {
            $request->validate([
                'image_ids'        => 'nullable|array',
                'folder_ids'       => 'nullable|array',
                'target_folder_id' => 'required',
            ]);

            $imageIds = array_map('intval', $request->input('image_ids', []));
            $folderIds = array_map('intval', $request->input('folder_ids', []));
            $targetFolderId = $request->input('target_folder_id');
            $action = $request->input('action', 'move');

            $destinationFolderId = (in_array((string)$targetFolderId, ['null', 'orphans', 'all', '', '0'], true)) 
                ? null 
                : (int)$targetFolderId;

            try {
                DB::beginTransaction();

                // 1. 하위 폴더 계층 이동
                if (!empty($folderIds)) {
                    foreach ($folderIds as $fId) {
                        if ($destinationFolderId !== null) {
                            $invalidTargets = $this->getAllDescendantFolderIds($fId);
                            if (in_array($destinationFolderId, $invalidTargets, true)) {
                                continue;
                            }
                        }

                        $curFolder = DB::table('image_folders')->where('id', $fId)->first();
                        if ($curFolder) {
                            $isDup = DB::table('image_folders')
                                ->where('parent_id', $destinationFolderId)
                                ->where('name', $curFolder->name)
                                ->where('id', '!=', $fId)
                                ->exists();

                            if (!$isDup) {
                                DB::table('image_folders')
                                    ->where('id', $fId)
                                    ->update([
                                        'parent_id'  => $destinationFolderId,
                                        'updated_at' => now(),
                                    ]);
                            }
                        }
                    }
                }

                // 2. 이미지 피벗 이동 (insertOrIgnore 방어)
                if (!empty($imageIds)) {
                    if ($action === 'move') {
                        $sourceFolderId = $request->input('source_folder_id');
                        
                        if ($sourceFolderId && !in_array((string)$sourceFolderId, ['orphans', 'all', 'null', ''], true)) {
                            DB::table('image_folder_pivot')
                                ->whereIn('image_id', $imageIds)
                                ->where('folder_id', (int)$sourceFolderId)
                                ->delete();
                        } else {
                            if ($destinationFolderId !== null) {
                                DB::table('image_folder_pivot')
                                    ->whereIn('image_id', $imageIds)
                                    ->where('folder_id', '!=', $destinationFolderId)
                                    ->delete();
                            } else {
                                DB::table('image_folder_pivot')
                                    ->whereIn('image_id', $imageIds)
                                    ->delete();
                            }
                        }
                    }

                    if ($destinationFolderId !== null) {
                        $insertData = [];
                        foreach ($imageIds as $imgId) {
                            $insertData[] = [
                                'folder_id'  => $destinationFolderId,
                                'image_id'   => $imgId,
                                'created_at' => now(),
                                'updated_at' => now(),
                            ];
                        }

                        DB::table('image_folder_pivot')->insertOrIgnore($insertData);

                        DB::table('image_folders')
                            ->where('id', $destinationFolderId)
                            ->update(['updated_at' => now()]);
                    }
                }

                DB::commit();

                $totalMoved = count($imageIds) + count($folderIds);

                return response()->json([
                    'success' => true,
                    'message' => "{$totalMoved}개 항목이 성공적으로 이동되었습니다.",
                    'moved_images_count' => count($imageIds),
                    'moved_folders_count' => count($folderIds),
                    'target_folder_id'   => $targetFolderId
                ]);

            } catch (\Throwable $e) {
                DB::rollBack();
                Log::error('Move files/folders combined error: ' . $e->getMessage());

                return response()->json([
                    'success' => false,
                    'message' => '이동 실패: ' . $e->getMessage()
                ], 500);
            }
        }

        public function moveFolder(Request $request)
        {
            $request->validate([
                'folder_id'        => 'required|integer|exists:image_folders,id',
                'target_parent_id' => 'nullable',
            ]);

            $folderId = (int)$request->input('folder_id');
            $rawTargetId = $request->input('target_parent_id');
            $targetParentId = ($rawTargetId === null || $rawTargetId === '0' || $rawTargetId === 0 || $rawTargetId === '') 
                ? null 
                : (int)$rawTargetId;

            if ($targetParentId !== null) {
                $invalidTargets = $this->getAllDescendantFolderIds($folderId);
                if (in_array($targetParentId, $invalidTargets, true)) {
                    return response()->json([
                        'success' => false,
                        'message' => '자기 자신 또는 하위 폴더 안으로는 이동할 수 없습니다.'
                    ], 422);
                }
            }

            $sourceFolder = DB::table('image_folders')->where('id', $folderId)->first();
            $isDuplicate = DB::table('image_folders')
                ->where('parent_id', $targetParentId)
                ->where('name', $sourceFolder->name)
                ->where('id', '!=', $folderId)
                ->exists();

            if ($isDuplicate) {
                return response()->json([
                    'success' => false,
                    'message' => '이동하려는 위치에 동일한 이름의 폴더가 이미 존재합니다.'
                ], 422);
            }

            DB::table('image_folders')
                ->where('id', $folderId)
                ->update([
                    'parent_id'  => $targetParentId,
                    'updated_at' => now(),
                ]);

            return response()->json([
                'success' => true,
                'message' => '폴더 위치가 성공적으로 이동되었습니다.'
            ]);
        }

        private function getAllDescendantFolderIds(int $folderId): array
        {
            $ids = [$folderId];
            $children = DB::table('image_folders')->where('parent_id', $folderId)->pluck('id')->toArray();
            foreach ($children as $cid) {
                $ids = array_merge($ids, $this->getAllDescendantFolderIds((int)$cid));
            }
            return array_unique($ids);
        }
    }

---

## 4. 프론트엔드 CSS 소스코드

### 4.1. `public/css/admin/admin_image.css`

    /**
     * GLOBEAU Admin - Windows Explorer 스타일 이미지 DAM 통합 스타일
     * /public/css/admin/admin_image.css
     */

    body:has(.win-explorer-root) .content-body {
        padding: 0 !important;
        overflow: hidden !important;
    }

    body:has(.win-explorer-root) .admin-footer {
        display: none !important;
    }

    /* 6px 테마 강제 선언 */
    .win-explorer-root {
        --app-radius: 6px;
        display: flex;
        flex-direction: column;
        height: calc(100vh - var(--topbar-height, 70px));
        height: calc(100dvh - var(--topbar-height, 70px));
        background-color: #ffffff;
        font-family: inherit;
        color: var(--color-text-main, #0f172a);
        overflow: hidden;
        position: relative;
    }

    /* 1. 상단 툴바 */
    .win-toolbar {
        height: 52px;
        background: #ffffff;
        border-bottom: 1px solid var(--border-color, #e2e8f0);
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0 16px;
        flex-shrink: 0;
        gap: 12px;
    }

    .win-toolbar-btn {
        border-radius: var(--app-radius, 6px) !important;
        font-size: 0.82rem;
        font-weight: 500;
        padding: 5px 10px;
        display: inline-flex;
        align-items: center;
        gap: 5px;
        transition: var(--transition-base, all 0.2s ease);
    }

    .win-nav-group {
        display: inline-flex;
        align-items: center;
        gap: 2px;
        background: #ffffff;
        border: 1px solid var(--border-color, #e2e8f0);
        border-radius: var(--app-radius, 6px);
        padding: 2px;
    }

    .win-nav-btn {
        border: none;
        background: transparent;
        color: #475569;
        width: 28px;
        height: 28px;
        border-radius: var(--app-radius, 6px);
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 0.75rem;
        cursor: pointer;
        transition: all 0.15s ease;
    }
    .win-nav-btn:hover:not(:disabled) {
        background: #f1f5f9;
        color: #0f172a;
    }
    .win-nav-btn:disabled {
        color: #cbd5e1;
        cursor: not-allowed;
    }

    .win-toolbar-path-badge {
        border-radius: var(--app-radius, 6px) !important;
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 5px 12px;
        background: #ffffff;
        border: 1px solid var(--border-color, #e2e8f0);
        font-size: 0.82rem;
        max-width: 260px;
    }

    /* 2. 워크스페이스 본문 */
    .win-workspace {
        flex: 1;
        display: flex;
        overflow: hidden;
        position: relative;
        background-color: #ffffff;
    }

    /* 1열: 좌측 트리 패널 */
    .win-tree-panel {
        width: 260px;
        min-width: 200px;
        max-width: 420px;
        background-color: #ffffff;
        border-right: 1px solid var(--border-color, #e2e8f0);
        overflow-y: auto;
        overflow-x: auto;
        flex-shrink: 0;
        padding: 10px 8px;
    }

    .win-tree-item {
        display: flex;
        align-items: center;
        padding: 6px 10px;
        border-radius: var(--app-radius, 6px) !important;
        font-size: 0.82rem;
        color: #334155;
        text-decoration: none;
        cursor: pointer;
        border: 1px solid transparent;
        background: transparent;
        width: max-content;
        min-width: 100%;
        text-align: left;
        white-space: nowrap;
        transition: background-color 0.15s ease, border-color 0.15s ease;
    }
    .win-tree-item:hover {
        background-color: #f8fafc;
        color: #0f172a;
    }
    .win-tree-item.active {
        background-color: #eff6ff;
        color: var(--primary-color, #2563eb);
        font-weight: 700;
    }
    .win-tree-item.drag-hover {
        background-color: #dbeafe !important;
        color: #2563eb !important;
        outline: 2px dashed #2563eb !important;
        outline-offset: -2px;
    }

    .tree-toggle-icon {
        width: 12px;
        font-size: 0.65rem;
        color: #94a3b8 !important;
        cursor: pointer;
        display: inline-block;
        transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1);
        text-align: center;
    }
    .tree-toggle-icon:hover {
        color: var(--primary-color, #2563eb) !important;
    }
    .tree-toggle-icon.expanded {
        transform: rotate(90deg);
    }

    /* 2열: 중앙 갤러리 패널 */
    .win-grid-panel {
        flex: 1;
        min-width: 360px;
        overflow-y: auto;
        padding: 12px 16px;
        background-color: #ffffff;
        position: relative;
        user-select: none;
        -webkit-user-select: none;
    }

    /* 3열: 우측 인스펙터 패널 */
    .win-inspector-panel {
        width: 380px;
        min-width: 300px;
        max-width: 700px;
        background-color: #ffffff;
        border-left: 1px solid var(--border-color, #e2e8f0);
        overflow: hidden;
        flex-shrink: 0;
        display: flex;
        flex-direction: column;
    }

    .win-resizer {
        width: 5px;
        background-color: #ffffff;
        border-left: 1px solid var(--border-color, #e2e8f0);
        border-right: 1px solid var(--border-color, #e2e8f0);
        cursor: col-resize;
        flex-shrink: 0;
        transition: background 0.15s;
        user-select: none;
        z-index: 10;
    }
    .win-resizer:hover, .win-resizer.resizing {
        background-color: var(--primary-color, #2563eb);
    }

    .win-statusbar {
        height: 30px;
        background-color: #ffffff;
        border-top: 1px solid var(--border-color, #e2e8f0);
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0 16px;
        font-size: 0.75rem;
        color: #64748b;
        flex-shrink: 0;
    }

    /* 그리드 및 썸네일 카드 */
    #damImageContainer {
        display: flex;
        flex-wrap: wrap;
        gap: 14px;
        align-content: flex-start;
        min-height: 100%;
    }

    .win-thumb-card, .win-folder-card {
        cursor: grab;
        border: 1px solid transparent;
        border-radius: var(--app-radius, 6px) !important;
        background: transparent !important;
        padding: 6px;
        display: flex;
        flex-direction: column;
        align-items: center;
        box-shadow: none !important;
        user-select: none;
        -webkit-user-select: none;
        transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
    }
    .win-thumb-card:active, .win-folder-card:active {
        cursor: grabbing;
    }
    .win-thumb-card *, .win-folder-card * {
        user-select: none;
        -webkit-user-select: none;
        pointer-events: none;
    }
    .win-thumb-card:hover, .win-folder-card:hover {
        transform: translateY(-1px);
        background-color: rgba(13, 110, 253, 0.05) !important;
    }
    .win-thumb-card.active, .win-folder-card.active {
        background-color: rgba(13, 110, 253, 0.12) !important;
        border-color: rgba(59, 130, 246, 0.35) !important;
    }
    .win-thumb-card.dragging, .win-folder-card.dragging {
        opacity: 0.4 !important;
    }
    .win-folder-card.drag-hover {
        background-color: rgba(13, 110, 253, 0.18) !important;
        outline: 2px dashed #0d6efd !important;
        outline-offset: -2px;
    }

    .win-thumb-box {
        background: transparent !important;
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        position: relative;
        border-radius: 4px;
    }
    .win-thumb-box img {
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
        filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08));
    }

    /* 뷰 모드별 크기 */
    .view-large .win-thumb-card, .view-large .win-folder-card { width: 190px; height: 240px; }
    .view-large .win-thumb-box { width: 100%; height: 180px; }
    .view-large .win-folder-card .win-thumb-box i { font-size: 4.8rem; color: #f59e0b; }

    .view-small .win-thumb-card, .view-small .win-folder-card { width: 110px; height: 150px; }
    .view-small .win-thumb-box { width: 100%; height: 100px; }
    .view-small .win-folder-card .win-thumb-box i { font-size: 2.8rem; color: #f59e0b; }

    .win-card-meta { width: 100%; margin-top: 6px; text-align: center; }
    .win-card-title {
        font-size: 0.78rem;
        line-height: 1.35;
        color: var(--color-text-main, #0f172a);
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
        word-break: break-all;
    }

    /* 리스트 뷰 */
    .dam-list-header {
        display: flex;
        align-items: center;
        padding: 6px 12px;
        border-bottom: 1px solid var(--border-color, #e2e8f0);
        background-color: #f8fafc;
        font-size: 0.75rem;
        font-weight: 600;
        color: #475569;
        user-select: none;
        margin-bottom: 4px;
    }
    .dam-col-header { cursor: pointer; display: flex; align-items: center; }
    .dam-col-header:hover { color: var(--primary-color, #2563eb); }
    .col-name { flex: 1; min-width: 180px; }
    .col-folder { width: 120px; }
    .col-date { width: 130px; }
    .col-type { width: 70px; }
    .col-size { width: 80px; }

    .view-list { display: flex !important; flex-direction: column !important; gap: 1px !important; width: 100% !important; }
    .view-list .win-thumb-card, .view-list .win-folder-card {
        width: 100% !important; height: 38px !important; flex-direction: row !important;
        justify-content: flex-start !important; padding: 3px 12px !important;
        border-bottom: 1px solid #f8fafc; align-items: center;
    }
    .view-list .win-thumb-box { width: 24px !important; height: 24px !important; margin-right: 10px; }
    .view-list .win-folder-card .win-thumb-box i { font-size: 1.15rem; color: #f59e0b; }
    .view-list .win-card-meta { margin-top: 0 !important; display: flex !important; align-items: center !important; text-align: left !important; flex: 1 !important; }
    .view-list .col-name-val { flex: 1; min-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.78rem; color: #1e293b; padding-right: 8px; }
    .view-list .col-folder-val { width: 120px; font-size: 0.72rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .view-list .col-date-val { width: 130px; font-size: 0.72rem; color: #64748b; }
    .view-list .col-type-val { width: 70px; font-size: 0.72rem; color: #64748b; }
    .view-list .col-size-val { width: 80px; font-size: 0.72rem; color: #64748b; text-align: right; }

    /* 마키 러버밴드 박스 & 드래그 고스트 */
    #damMarqueeBox {
        position: absolute;
        border: 1.5px solid #0d6efd;
        background-color: rgba(13, 110, 253, 0.15);
        pointer-events: none;
        z-index: 1000;
        display: none;
    }

    .win-drag-ghost-badge {
        position: fixed;
        top: -9999px;
        left: -9999px;
        background: #2563eb;
        color: #ffffff;
        font-size: 0.78rem;
        font-weight: 700;
        padding: 6px 14px;
        border-radius: 20px;
        box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
        display: inline-flex;
        align-items: center;
        gap: 6px;
        pointer-events: none;
        z-index: 99999;
    }

    /* 우측 인스펙터 미리보기 및 하단 고정 푸터 */
    .win-inspector-preview-box {
        padding: 0 !important;
        margin: 0 0 1rem 0 !important;
        background: transparent !important;
        border: none !important;
        width: 100% !important;
        max-height: 70vh !important;
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        position: relative;
        overflow: hidden;
    }
    .win-inspector-preview-box img {
        max-width: 100% !important;
        max-height: 70vh !important;
        object-fit: contain;
        border-radius: var(--app-radius, 6px);
        display: block;
    }

    .win-inspector-footer {
        border-top: 1px solid var(--border-color, #e2e8f0) !important;
        background-color: #ffffff !important;
        box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.03);
        z-index: 5;
    }

    /* ==========================================================
       정렬 & 뷰 모드 버튼 그룹 테마 강제 동기화 (우선순위 극대화)
       ========================================================== */
    .common-rounded-btn-group {
        display: inline-flex !important;
        border-radius: var(--app-radius, 6px) !important;
    }

    .common-rounded-btn-group .btn {
        border-radius: 0 !important;
        font-size: 0.78rem !important;
        font-weight: 500 !important;
        transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease !important;
    }

    .common-rounded-btn-group .btn:first-child {
        border-top-left-radius: var(--app-radius, 6px) !important;
        border-bottom-left-radius: var(--app-radius, 6px) !important;
    }

    .common-rounded-btn-group .btn:last-child {
        border-top-right-radius: var(--app-radius, 6px) !important;
        border-bottom-right-radius: var(--app-radius, 6px) !important;
    }

    .btn-outline-secondary.sort-toggle-btn,
    .btn-outline-secondary.view-mode-btn {
        background-color: #ffffff !important;
        border-color: #cbd5e1 !important;
        color: #475569 !important;
        box-shadow: none !important;
    }

    .btn-outline-secondary.sort-toggle-btn .sort-icon {
        color: #94a3b8 !important;
    }

    .btn-outline-secondary.sort-toggle-btn:hover,
    .btn-outline-secondary.view-mode-btn:hover {
        background-color: var(--primary-color, #2563eb) !important;
        border-color: var(--primary-color, #2563eb) !important;
        color: #ffffff !important;
    }

    .btn-outline-secondary.sort-toggle-btn:hover *,
    .btn-outline-secondary.view-mode-btn:hover * {
        color: #ffffff !important;
    }

    .btn-outline-secondary.sort-toggle-btn.active,
    .btn-outline-secondary.sort-toggle-btn:active,
    .btn-outline-secondary.view-mode-btn.active,
    .btn-outline-secondary.view-mode-btn:active,
    .btn-outline-secondary.sort-toggle-btn.active:focus,
    .btn-outline-secondary.view-mode-btn.active:focus {
        background-color: var(--primary-color, #2563eb) !important;
        border-color: var(--primary-color, #2563eb) !important;
        color: #ffffff !important;
        box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15) !important;
    }

    .btn-outline-secondary.sort-toggle-btn.active *,
    .btn-outline-secondary.sort-toggle-btn:active *,
    .btn-outline-secondary.view-mode-btn.active *,
    .btn-outline-secondary.view-mode-btn:active * {
        color: #ffffff !important;
    }

---

## 5. 프론트엔드 자바스크립트 소스코드

### 5.1. `public/js/admin/admin_image.js`

    /**
     * GLOBEAU Admin - 중앙 집중형 Windows Explorer 스타일 DAM 스크립트
     * /public/js/admin/admin_image.js
     */

    (function () {
        let currentFolderId = 'all';
        let currentFolderName = '전체 이미지';
        let currentSortBy = 'date';
        let currentSortDirection = 'desc';
        let currentViewMode = localStorage.getItem('dam_view_mode') || 'large';

        let currentPage = 1;
        const pageSize = 50;
        let hasMorePages = true;
        let isLoading = false;
        let scrollObserver = null;

        let selectedImagesMap = new Map();
        let selectedFoldersMap = new Map();
        let lastClickedImageId = null;

        let currentDragType = null;
        let draggedImageIds = [];
        let draggedFolderIds = [];

        let expandedFolderIds = new Set();
        let cachedFoldersList = [];
        let folderHistory = [{ id: 'all', name: '전체 이미지' }];
        let historyPointer = 0;

        let isMarqueeSelecting = false;
        let marqueeStartX = 0;
        let marqueeStartY = 0;
        let marqueeBoxEl = null;
        let dragPreventClick = false;

        let contextMenuTargetImage = null;
        let contextMenuTargetFolder = null;

        function getCsrfToken() {
            const meta = document.querySelector('meta[name="csrf-token"]');
            return meta ? meta.getAttribute('content') : '';
        }

        // =========================================================================
        // 1. 단축키 & 마우스 다중 선택
        // =========================================================================
        function handleCardClick(e, id, path, name) {
            e.stopPropagation();
            closeContextMenu();
            closeFolderContextMenu();

            id = parseInt(id, 10);
            const card = document.getElementById(`thumb-card-${id}`);
            const webp_url = card?.dataset?.webp || '';
            const thumb_url = card?.dataset?.thumb || '';

            const isCtrl = e.ctrlKey || e.metaKey;
            const isShift = e.shiftKey;
            const cardElements = Array.from(document.querySelectorAll('.win-thumb-card'));
            const allIds = cardElements.map(c => parseInt(c.getAttribute('data-id'), 10));

            if (isCtrl) {
                if (selectedImagesMap.has(id)) {
                    selectedImagesMap.delete(id);
                } else {
                    selectedImagesMap.set(id, { id, path, name, webp_url, thumb_url });
                }
                lastClickedImageId = id;
            } else if (isShift && lastClickedImageId !== null) {
                const startIdx = allIds.indexOf(lastClickedImageId);
                const endIdx = allIds.indexOf(id);

                if (startIdx !== -1 && endIdx !== -1) {
                    const [min, max] = [Math.min(startIdx, endIdx), Math.max(startIdx, endIdx)];
                    if (!isCtrl) {
                        selectedImagesMap.clear();
                        selectedFoldersMap.clear();
                    }
                    for (let i = min; i <= max; i++) {
                        const cId = allIds[i];
                        const cElem = cardElements[i];
                        selectedImagesMap.set(cId, {
                            id: cId,
                            path: cElem.dataset.path,
                            name: cElem.dataset.name,
                            webp_url: cElem.dataset.webp || '',
                            thumb_url: cElem.dataset.thumb || ''
                        });
                    }
                }
            } else {
                selectedImagesMap.clear();
                selectedFoldersMap.clear();
                selectedImagesMap.set(id, { id, path, name, webp_url, thumb_url });
                lastClickedImageId = id;
            }

            syncSelectionUI();
        }

        function handleFolderCardClick(e, folderId, folderName) {
            e.stopPropagation();
            closeContextMenu();
            closeFolderContextMenu();

            folderId = parseInt(folderId, 10);

            if (e.ctrlKey || e.metaKey) {
                if (selectedFoldersMap.has(folderId)) {
                    selectedFoldersMap.delete(folderId);
                } else {
                    selectedFoldersMap.set(folderId, { id: folderId, name: folderName });
                }
            } else {
                selectedImagesMap.clear();
                selectedFoldersMap.clear();
                selectedFoldersMap.set(folderId, { id: folderId, name: folderName });
            }

            syncSelectionUI();
        }

        function syncSelectionUI() {
            const selectedImgIds = new Set(selectedImagesMap.keys());
            const selectedFldIds = new Set(selectedFoldersMap.keys());
            const totalSelected = selectedImagesMap.size + selectedFoldersMap.size;

            document.querySelectorAll('.win-thumb-card').forEach(card => {
                const cardId = parseInt(card.getAttribute('data-id'), 10);
                card.classList.toggle('active', selectedImgIds.has(cardId));
            });

            document.querySelectorAll('.win-folder-card').forEach(fcard => {
                const folderId = parseInt(fcard.getAttribute('data-folder'), 10);
                fcard.classList.toggle('active', selectedFldIds.has(folderId));
            });

            const selBadge = document.getElementById('statSelectedBadge');
            if (selBadge) {
                if (totalSelected > 0) {
                    let parts = [];
                    if (selectedImagesMap.size > 0) parts.push(`이미지 ${selectedImagesMap.size}개`);
                    if (selectedFoldersMap.size > 0) parts.push(`폴더 ${selectedFoldersMap.size}개`);
                    selBadge.innerText = `${parts.join(', ')} 선택됨`;
                    selBadge.classList.remove('d-none');
                } else {
                    selBadge.innerText = '';
                    selBadge.classList.add('d-none');
                }
            }

            if (selectedImagesMap.size === 1 && selectedFoldersMap.size === 0) {
                const singleId = Array.from(selectedImagesMap.keys())[0];
                fetchImageDetail(singleId);
            } else if (totalSelected > 1 || selectedFoldersMap.size > 0) {
                renderMultiSelectInspector(selectedImagesMap.size, selectedFoldersMap.size);
            } else {
                closeInspector();
            }
        }

        function selectAllItems() {
            document.querySelectorAll('.win-thumb-card').forEach(card => {
                const id = parseInt(card.dataset.id, 10);
                selectedImagesMap.set(id, {
                    id,
                    path: card.dataset.path,
                    name: card.dataset.name,
                    webp_url: card.dataset.webp || '',
                    thumb_url: card.dataset.thumb || ''
                });
            });

            document.querySelectorAll('.win-folder-card').forEach(fcard => {
                const fid = parseInt(fcard.dataset.folder, 10);
                selectedFoldersMap.set(fid, { id: fid, name: fcard.dataset.name });
            });

            syncSelectionUI();
        }

        function clearAllSelection() {
            selectedImagesMap.clear();
            selectedFoldersMap.clear();
            lastClickedImageId = null;
            syncSelectionUI();
        }

        document.addEventListener('keydown', (e) => {
            if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName)) {
                if (document.activeElement.id === 'damSearchKeyword') return;
            }

            if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) {
                e.preventDefault();
                selectAllItems();
            }

            if (e.key === 'Escape') {
                clearAllSelection();
                closeContextMenu();
                closeFolderContextMenu();
            }
        });

        document.addEventListener('click', (e) => {
            if (dragPreventClick) return;

            const isInteractiveArea = e.target.closest('.win-thumb-card') || 
                                      e.target.closest('.win-folder-card') || 
                                      e.target.closest('.win-tree-panel') || 
                                      e.target.closest('.win-toolbar') || 
                                      e.target.closest('.win-inspector-panel') || 
                                      e.target.closest('.dropdown-menu') ||
                                      e.target.closest('.swal2-container');

            if (!isInteractiveArea) {
                clearAllSelection();
            }

            closeContextMenu();
            closeFolderContextMenu();
        });

        // =========================================================================
        // 2. 마키 러버밴드 드래그 선택
        // =========================================================================
        function initMarqueeSelection() {
            const gridPanel = document.querySelector('.win-grid-panel');
            if (!gridPanel) return;

            marqueeBoxEl = document.getElementById('damMarqueeBox');
            if (!marqueeBoxEl) {
                marqueeBoxEl = document.createElement('div');
                marqueeBoxEl.id = 'damMarqueeBox';
                gridPanel.appendChild(marqueeBoxEl);
            }

            gridPanel.addEventListener('mousedown', (e) => {
                if (e.target.closest('.win-thumb-card') || 
                    e.target.closest('.win-folder-card') || 
                    e.target.closest('button') || 
                    e.target.closest('input') || 
                    e.button !== 0) {
                    return;
                }

                isMarqueeSelecting = true;
                dragPreventClick = false;

                const rect = gridPanel.getBoundingClientRect();
                marqueeStartX = e.clientX - rect.left + gridPanel.scrollLeft;
                marqueeStartY = e.clientY - rect.top + gridPanel.scrollTop;

                marqueeBoxEl.style.left = `${marqueeStartX}px`;
                marqueeBoxEl.style.top = `${marqueeStartY}px`;
                marqueeBoxEl.style.width = '0px';
                marqueeBoxEl.style.height = '0px';
                marqueeBoxEl.style.display = 'block';

                if (!e.ctrlKey && !e.shiftKey && !e.metaKey) {
                    clearAllSelection();
                }

                e.preventDefault();
            });

            window.addEventListener('mousemove', (e) => {
                if (!isMarqueeSelecting || !marqueeBoxEl) return;

                const rect = gridPanel.getBoundingClientRect();
                const currentX = e.clientX - rect.left + gridPanel.scrollLeft;
                const currentY = e.clientY - rect.top + gridPanel.scrollTop;

                const boxLeft = Math.min(marqueeStartX, currentX);
                const boxTop = Math.min(marqueeStartY, currentY);
                const boxWidth = Math.abs(currentX - marqueeStartX);
                const boxHeight = Math.abs(currentY - marqueeStartY);

                if (boxWidth > 4 || boxHeight > 4) {
                    dragPreventClick = true;
                }

                marqueeBoxEl.style.left = `${boxLeft}px`;
                marqueeBoxEl.style.top = `${boxTop}px`;
                marqueeBoxEl.style.width = `${boxWidth}px`;
                marqueeBoxEl.style.height = `${boxHeight}px`;

                const boxRight = boxLeft + boxWidth;
                const boxBottom = boxTop + boxHeight;

                gridPanel.querySelectorAll('.win-thumb-card').forEach(card => {
                    const isOverlap = (
                        boxLeft < card.offsetLeft + card.offsetWidth &&
                        boxRight > card.offsetLeft &&
                        boxTop < card.offsetTop + card.offsetHeight &&
                        boxBottom > card.offsetTop
                    );

                    const cId = parseInt(card.dataset.id, 10);
                    if (isOverlap) {
                        selectedImagesMap.set(cId, {
                            id: cId,
                            path: card.dataset.path,
                            name: card.dataset.name,
                            webp_url: card.dataset.webp || '',
                            thumb_url: card.dataset.thumb || ''
                        });
                    } else if (!e.ctrlKey && !e.metaKey) {
                        selectedImagesMap.delete(cId);
                    }
                });

                gridPanel.querySelectorAll('.win-folder-card').forEach(fcard => {
                    const isOverlap = (
                        boxLeft < fcard.offsetLeft + fcard.offsetWidth &&
                        boxRight > fcard.offsetLeft &&
                        boxTop < fcard.offsetTop + fcard.offsetHeight &&
                        boxBottom > fcard.offsetTop
                    );

                    const fId = parseInt(fcard.dataset.folder, 10);
                    if (isOverlap) {
                        selectedFoldersMap.set(fId, { id: fId, name: fcard.dataset.name });
                    } else if (!e.ctrlKey && !e.metaKey) {
                        selectedFoldersMap.delete(fId);
                    }
                });

                syncSelectionUI();
            });

            window.addEventListener('mouseup', () => {
                if (isMarqueeSelecting) {
                    isMarqueeSelecting = false;
                    if (marqueeBoxEl) marqueeBoxEl.style.display = 'none';
                    setTimeout(() => { dragPreventClick = false; }, 80);
                }
            });
        }

        // =========================================================================
        // 3. 통합 드래그 앤 드롭 이동
        // =========================================================================
        function getCombinedDragPayload() {
            return {
                type: 'combined-move',
                image_ids: Array.from(selectedImagesMap.keys()),
                folder_ids: Array.from(selectedFoldersMap.keys())
            };
        }

        function initCardDragEvents(card, id) {
            card.setAttribute('draggable', 'true');

            card.addEventListener('dragstart', (e) => {
                currentDragType = 'combined';

                if (!selectedImagesMap.has(id)) {
                    selectedImagesMap.clear();
                    selectedFoldersMap.clear();
                    selectedImagesMap.set(id, {
                        id,
                        path: card.dataset.path,
                        name: card.dataset.name,
                        webp_url: card.dataset.webp || '',
                        thumb_url: card.dataset.thumb || ''
                    });
                    syncSelectionUI();
                }

                card.classList.add('dragging');

                const payload = getCombinedDragPayload();
                draggedImageIds = payload.image_ids;
                draggedFolderIds = payload.folder_ids;

                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', JSON.stringify(payload));

                const ghost = document.getElementById('dragGhostBadge');
                const ghostCount = document.getElementById('dragGhostCount');
                const totalCount = payload.image_ids.length + payload.folder_ids.length;
                if (ghost && ghostCount) {
                    ghostCount.innerText = totalCount;
                    try { e.dataTransfer.setDragImage(ghost, 15, 15); } catch (err) {}
                }
            });

            card.addEventListener('dragend', () => {
                card.classList.remove('dragging');
                currentDragType = null;
                draggedImageIds = [];
                draggedFolderIds = [];
                document.querySelectorAll('.win-tree-item, .win-folder-card').forEach(el => el.classList.remove('drag-hover'));
            });
        }

        function initFolderCardDragEvents(folderCard, folderId) {
            folderCard.setAttribute('draggable', 'true');

            folderCard.addEventListener('dragstart', (e) => {
                currentDragType = 'combined';
                folderId = parseInt(folderId, 10);

                if (!selectedFoldersMap.has(folderId)) {
                    selectedImagesMap.clear();
                    selectedFoldersMap.clear();
                    selectedFoldersMap.set(folderId, { id: folderId, name: folderCard.dataset.name });
                    syncSelectionUI();
                }

                folderCard.classList.add('dragging');

                const payload = getCombinedDragPayload();
                draggedImageIds = payload.image_ids;
                draggedFolderIds = payload.folder_ids;

                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', JSON.stringify(payload));

                const ghost = document.getElementById('dragGhostBadge');
                const ghostCount = document.getElementById('dragGhostCount');
                const totalCount = payload.image_ids.length + payload.folder_ids.length;
                if (ghost && ghostCount) {
                    ghostCount.innerText = totalCount;
                    try { e.dataTransfer.setDragImage(ghost, 15, 15); } catch (err) {}
                }
            });

            folderCard.addEventListener('dragend', () => {
                folderCard.classList.remove('dragging');
                currentDragType = null;
                draggedImageIds = [];
                draggedFolderIds = [];
                document.querySelectorAll('.win-tree-item, .win-folder-card').forEach(el => el.classList.remove('drag-hover'));
            });
        }

        function setupDropTargets() {
            const dropTargets = document.querySelectorAll('.win-tree-item, .win-folder-card');

            dropTargets.forEach(targetElem => {
                const targetFolderId = targetElem.getAttribute('data-folder');

                targetElem.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    if (selectedFoldersMap.has(parseInt(targetFolderId, 10))) {
                        return;
                    }

                    targetElem.classList.add('drag-hover');
                    e.dataTransfer.dropEffect = 'move';
                });

                targetElem.addEventListener('dragleave', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    targetElem.classList.remove('drag-hover');
                });

                targetElem.addEventListener('drop', async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    targetElem.classList.remove('drag-hover');

                    if (selectedFoldersMap.has(parseInt(targetFolderId, 10))) {
                        return;
                    }

                    const rawData = e.dataTransfer.getData('text/plain');
                    let imageIds = Array.from(selectedImagesMap.keys());
                    let folderIds = Array.from(selectedFoldersMap.keys());

                    if (rawData) {
                        try {
                            const parsed = JSON.parse(rawData);
                            if (parsed.type === 'combined-move') {
                                imageIds = parsed.image_ids || imageIds;
                                folderIds = parsed.folder_ids || folderIds;
                            }
                        } catch (err) {}
                    }

                    if (imageIds.length === 0 && folderIds.length === 0) return;

                    const folderName = targetElem.innerText.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
                    await executeMoveCombined(imageIds, folderIds, targetFolderId, folderName);
                });
            });
        }

        async function executeMoveCombined(imageIds, folderIds, targetFolderId, targetFolderName) {
            if (String(targetFolderId) === String(currentFolderId)) {
                if (typeof Swal !== 'undefined') {
                    Swal.fire({ icon: 'info', title: '알림', text: '이미 현재 폴더에 있는 항목입니다.', timer: 1200, showConfirmButton: false });
                }
                return;
            }

            try {
                const res = await fetch('/admin/image-folders/move-copy', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': getCsrfToken(),
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({
                        image_ids: imageIds,
                        folder_ids: folderIds,
                        source_folder_id: (currentFolderId && currentFolderId !== 'all') ? currentFolderId : null,
                        target_folder_id: targetFolderId,
                        action: 'move'
                    })
                });

                const json = await res.json();
                if (json.success) {
                    clearAllSelection();

                    if (typeof Swal !== 'undefined') {
                        Swal.fire({
                            icon: 'success',
                            title: '이동 완료',
                            text: `[${targetFolderName}] 폴더로 이동되었습니다. 해당 폴더로 진입합니다.`,
                            timer: 1100,
                            showConfirmButton: false
                        });
                    }

                    await fetchFolders();
                    selectFolder(targetFolderId, targetFolderName);
                } else {
                    throw new Error(json.message);
                }
            } catch (e) {
                if (typeof Swal !== 'undefined') {
                    Swal.fire('이동 실패', e.message || '항목 이동에 실패했습니다.', 'error');
                } else {
                    alert(e.message || '항목 이동 실패');
                }
            }
        }

        // =========================================================================
        // 4. 무한 스크롤 및 이미지 목록 로드
        // =========================================================================
        async function loadImages(page = 1, append = false) {
            if (isLoading) return;
            if (!append) {
                currentPage = 1;
                hasMorePages = true;
            }
            if (!hasMorePages && append) return;

            isLoading = true;
            currentPage = page;

            const spinner = document.getElementById('damLoadingSpinner');
            const container = document.getElementById('damImageContainer');
            const emptyState = document.getElementById('damEmptyState');
            const keyword = document.getElementById('damSearchKeyword')?.value.trim() || '';

            if (spinner) spinner.classList.remove('d-none');
            if (!append && container) {
                container.innerHTML = '';
                if (emptyState) emptyState.classList.add('d-none');
            }

            const existingSentinel = document.getElementById('damScrollSentinel');
            if (existingSentinel) existingSentinel.remove();

            try {
                const url = `/admin/images/list?page=${page}&per_page=${pageSize}&folder_id=${currentFolderId}&keyword=${encodeURIComponent(keyword)}&sort_by=${currentSortBy}&sort_direction=${currentSortDirection}`;
                const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
                const json = await res.json();

                if (json.success) {
                    const paginator = json.images || {};
                    const items = paginator.data || [];
                    const subFolders = json.sub_folders || [];

                    if (paginator.current_page && paginator.last_page) {
                        hasMorePages = paginator.current_page < paginator.last_page;
                    } else {
                        hasMorePages = items.length >= pageSize;
                    }

                    if (!append && items.length === 0 && subFolders.length === 0) {
                        if (emptyState) emptyState.classList.remove('d-none');
                    } else if (container) {
                        if (!append && subFolders.length > 0) {
                            subFolders.forEach(sub => container.appendChild(createFolderCard(sub)));
                        }
                        items.forEach(item => container.appendChild(createThumbCard(item)));
                    }

                    setupDropTargets();

                    const totalText = document.getElementById('statTotalText');
                    if (totalText && container) {
                        const loadedCount = container.querySelectorAll('.win-thumb-card').length;
                        const folderText = subFolders.length > 0 ? ` (폴더 ${subFolders.length}개)` : '';
                        totalText.innerText = `총 ${paginator.total || loadedCount}장 중 ${loadedCount}장 로드됨${folderText}`;
                    }

                    if (hasMorePages) {
                        setupScrollSentinel();
                    }
                }
            } catch (e) {
                console.error('목록 로드 실패:', e);
            } finally {
                if (spinner) spinner.classList.add('d-none');
                isLoading = false;
                const syncText = document.getElementById('statLastSyncTime');
                if (syncText) syncText.innerText = `동기화 완료: ${new Date().toLocaleTimeString()}`;
            }
        }

        function setupScrollSentinel() {
            const container = document.getElementById('damImageContainer');
            if (!container) return;

            if (scrollObserver) scrollObserver.disconnect();

            const sentinel = document.createElement('div');
            sentinel.id = 'damScrollSentinel';
            sentinel.style.height = '20px';
            sentinel.style.width = '100%';
            sentinel.style.pointerEvents = 'none';
            container.appendChild(sentinel);

            scrollObserver = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting && hasMorePages && !isLoading) {
                        loadImages(currentPage + 1, true);
                    }
                });
            }, {
                root: document.querySelector('.win-grid-panel'),
                rootMargin: '150px'
            });

            scrollObserver.observe(sentinel);
        }

        function createFolderCard(folder) {
            const card = document.createElement('div');
            card.id = `folder-card-${folder.id}`;
            card.setAttribute('data-folder', folder.id);
            card.setAttribute('data-name', folder.name);
            card.className = `win-folder-card ${selectedFoldersMap.has(folder.id) ? 'active' : ''}`;

            card.addEventListener('click', (e) => handleFolderCardClick(e, folder.id, folder.name));
            card.addEventListener('dblclick', () => selectFolder(folder.id, folder.name));
            card.addEventListener('contextmenu', (e) => handleFolderContextMenu(e, folder.id, folder.name));
            initFolderCardDragEvents(card, folder.id);

            if (currentViewMode === 'list') {
                card.innerHTML = `
                    <div class="win-thumb-box"><i class="fa-solid fa-folder"></i></div>
                    <div class="win-card-meta">
                        <span class="col-name-val" title="${folder.name}">${folder.name}</span>
                        <span class="col-folder-val">폴더</span>
                        <span class="col-date-val">-</span>
                        <span class="col-type-val">DIR</span>
                        <span class="col-size-val">${folder.images_count || 0}개 파일</span>
                    </div>
                `;
            } else {
                card.innerHTML = `
                    <div class="win-thumb-box"><i class="fa-solid fa-folder"></i></div>
                    <div class="win-card-meta">
                        <span class="win-card-title" title="${folder.name}">${folder.name}</span>
                    </div>
                `;
            }

            return card;
        }

        function createThumbCard(item) {
            const card = document.createElement('div');
            card.id = `thumb-card-${item.id}`;
            card.setAttribute('data-id', item.id);
            card.setAttribute('data-path', item.path);
            card.setAttribute('data-name', item.original_name);
            card.setAttribute('data-webp', item.webp_url || '');
            card.setAttribute('data-thumb', item.thumb_url || '');
            card.setAttribute('data-is-used', item.is_used ? '1' : '0');
            card.setAttribute('data-is-protected', item.is_protected ? '1' : '0');

            const isActive = selectedImagesMap.has(item.id) ? 'active' : '';
            const imgSrc = item.thumb_url || `/storage/${item.path}`;
            const displayName = item.title || item.original_name;
            const folderName = item.folder_name || '일반';

            card.className = `win-thumb-card ${isActive}`;

            card.addEventListener('click', (e) => handleCardClick(e, item.id, item.path, item.original_name));
            card.addEventListener('dblclick', () => fetchImageDetail(item.id));
            card.addEventListener('contextmenu', (e) => handleCardContextMenu(e, item));
            initCardDragEvents(card, item.id);

            if (currentViewMode === 'list') {
                card.innerHTML = `
                    <div class="win-thumb-box">
                        <img src="${imgSrc}" alt="${item.original_name}" loading="lazy" onerror="this.src='https://placehold.co/150x150?text=No+Img'">
                    </div>
                    <div class="win-card-meta">
                        <span class="col-name-val" title="${displayName}">${displayName}</span>
                        <span class="col-folder-val" title="${folderName}">${folderName}</span>
                        <span class="col-date-val">${item.formatted_date || '-'}</span>
                        <span class="col-type-val">${item.extension}</span>
                        <span class="col-size-val">${item.formatted_size}</span>
                    </div>
                `;
            } else {
                card.innerHTML = `
                    <div class="win-thumb-box">
                        <img src="${imgSrc}" alt="${item.original_name}" loading="lazy" onerror="this.src='https://placehold.co/150x150?text=No+Img'">
                    </div>
                    <div class="win-card-meta">
                        <span class="win-card-title" title="${displayName}">${displayName}</span>
                    </div>
                `;
            }

            return card;
        }

        // =========================================================================
        // 5. 폴더 트리 비동기 로드 및 아코디언 토글
        // =========================================================================
        async function fetchFolders() {
            const treeRoot = document.getElementById('treeRootContainer');
            if (!treeRoot) return;

            try {
                const res = await fetch('/admin/image-folders', {
                    headers: { 'X-Requested-With': 'XMLHttpRequest' }
                });
                const data = await res.json();
                if (data.success && data.folders) {
                    cachedFoldersList = data.folders;
                    renderFolderTreeUI();
                }
            } catch (e) {
                console.error('폴더 트리 로드 실패:', e);
            }
        }

        function renderFolderTreeUI() {
            const treeRoot = document.getElementById('treeRootContainer');
            if (!treeRoot) return;

            const isAllActive = (currentFolderId === 'all');
            const isOrphanActive = (currentFolderId === 'orphans');

            let html = `
                <button type="button" class="win-tree-item ${isAllActive ? 'active' : ''}" data-folder="all" onclick="selectFolder('all', '전체 이미지')">
                    <i class="fa-solid fa-layer-group text-primary me-2"></i>전체 이미지
                </button>
                <button type="button" class="win-tree-item ${isOrphanActive ? 'active' : ''}" data-folder="orphans" onclick="selectFolder('orphans', '임시 파일')">
                    <i class="fa-solid fa-folder text-warning me-2"></i>임시 파일
                </button>
                <hr class="my-1 border-slate-200">
                <div class="d-flex align-items-center justify-content-between px-2 py-1">
                    <span class="fw-bold small text-muted text-uppercase" style="font-size: 0.68rem; letter-spacing: 0.5px;">카테고리 폴더</span>
                    <button type="button" class="btn btn-link btn-sm p-0 text-primary" onclick="promptCreateSubFolder(null, '루트')" title="새 폴더 생성">
                        <i class="fa-solid fa-plus"></i>
                    </button>
                </div>
            `;

            const parentMap = new Map();
            cachedFoldersList.forEach(f => parentMap.set(f.id, f.parent_id));

            function isVisible(f) {
                let pid = f.parent_id;
                while (pid) {
                    if (!expandedFolderIds.has(pid)) return false;
                    pid = parentMap.get(pid);
                }
                return true;
            }

            if (cachedFoldersList.length > 0) {
                cachedFoldersList.forEach(f => {
                    if (!isVisible(f)) return;

                    const isActive = (String(currentFolderId) === String(f.id));
                    const depth = f.depth ?? 0;
                    const hasChildren = Boolean(f.has_children);
                    const isExpanded = expandedFolderIds.has(f.id);
                    const indentPx = depth * 14;

                    let caretHtml = '';
                    if (hasChildren) {
                        const chevronClass = isExpanded ? 'fa-chevron-down expanded' : 'fa-chevron-right';
                        caretHtml = `<i class="fa-solid ${chevronClass} me-2 tree-toggle-icon" onclick="event.stopPropagation(); toggleTreeExpand(${f.id});"></i>`;
                    } else {
                        caretHtml = `<span style="display:inline-block; width:12px; margin-right:0.5rem;"></span>`;
                    }

                    html += `
                        <div class="win-tree-branch">
                            <button type="button" class="win-tree-item ${isActive ? 'active' : ''}" 
                                    data-folder="${f.id}" 
                                    data-parent="${f.parent_id || ''}" 
                                    style="padding-left: ${10 + indentPx}px;"
                                    onclick="selectFolder('${f.id}', '${f.name}')"
                                    oncontextmenu="handleFolderContextMenu(event, ${f.id}, '${f.name}')">
                                ${caretHtml}
                                <i class="fa-solid fa-folder text-warning me-2"></i>${f.name}
                            </button>
                        </div>
                    `;
                });
            } else {
                html += `<div class="text-muted small px-2 py-1">등록된 폴더 없음</div>`;
            }

            treeRoot.innerHTML = html;
            setupDropTargets();
        }

        function toggleTreeExpand(folderId) {
            if (expandedFolderIds.has(folderId)) {
                expandedFolderIds.delete(folderId);
            } else {
                expandedFolderIds.add(folderId);
            }
            renderFolderTreeUI();
        }

        function selectFolder(folderId, folderName) {
            const strId = String(folderId);

            if (currentFolderId === strId) {
                const intId = parseInt(folderId, 10);
                if (!isNaN(intId)) toggleTreeExpand(intId);
                return;
            }

            if (folderHistory[historyPointer]?.id !== strId) {
                folderHistory = folderHistory.slice(0, historyPointer + 1);
                folderHistory.push({ id: strId, name: folderName });
                historyPointer = folderHistory.length - 1;
            }

            applyFolderChange(strId, folderName);
        }

        function applyFolderChange(folderId, folderName) {
            currentFolderId = String(folderId);
            currentFolderName = folderName;

            const intId = parseInt(folderId, 10);
            if (!isNaN(intId)) {
                expandedFolderIds.add(intId);
                let pid = cachedFoldersList.find(f => f.id === intId)?.parent_id;
                while (pid) {
                    expandedFolderIds.add(pid);
                    pid = cachedFoldersList.find(f => f.id === pid)?.parent_id;
                }
            }

            renderFolderTreeUI();

            const breadcrumb = document.getElementById('currentFolderBreadcrumb');
            const statFolder = document.getElementById('statSelectedFolder');
            if (breadcrumb) breadcrumb.innerText = folderName;
            if (statFolder) statFolder.innerText = `폴더: ${folderName}`;

            updateNavButtons();
            clearAllSelection();

            const searchInput = document.getElementById('damSearchKeyword');
            if (searchInput) searchInput.value = '';

            loadImages(1, false);
        }

        function updateNavButtons() {
            const btnBack = document.getElementById('btnNavBack');
            const btnForward = document.getElementById('btnNavForward');
            const dropdown = document.getElementById('folderHistoryDropdown');

            if (btnBack) btnBack.disabled = (historyPointer <= 0);
            if (btnForward) btnForward.disabled = (historyPointer >= folderHistory.length - 1);

            if (dropdown) {
                dropdown.innerHTML = '';
                folderHistory.forEach((item, idx) => {
                    const li = document.createElement('li');
                    const isCurrent = (idx === historyPointer) ? 'fw-bold text-primary' : '';
                    li.innerHTML = `<a class="dropdown-item ${isCurrent}" href="javascript:void(0)" onclick="navGoHistoryIndex(${idx})">${item.name}</a>`;
                    dropdown.appendChild(li);
                });
            }
        }

        function navHistoryBack() {
            if (historyPointer > 0) {
                historyPointer--;
                const target = folderHistory[historyPointer];
                applyFolderChange(target.id, target.name);
            }
        }

        function navHistoryForward() {
            if (historyPointer < folderHistory.length - 1) {
                historyPointer++;
                const target = folderHistory[historyPointer];
                applyFolderChange(target.id, target.name);
            }
        }

        function navGoParent() {
            if (currentFolderId === 'all') return;
            const current = cachedFoldersList.find(f => String(f.id) === String(currentFolderId));
            if (current && current.parent_id) {
                const parent = cachedFoldersList.find(f => f.id === current.parent_id);
                if (parent) {
                    selectFolder(parent.id, parent.name);
                    return;
                }
            }
            selectFolder('all', '전체 이미지');
        }

        function navGoHistoryIndex(idx) {
            if (idx >= 0 && idx < folderHistory.length) {
                historyPointer = idx;
                const target = folderHistory[idx];
                applyFolderChange(target.id, target.name);
            }
        }

        // =========================================================================
        // 6. 마우스 우클릭 컨텍스트 메뉴
        // =========================================================================
        function getOrCreateFolderContextMenuEl() {
            let menu = document.getElementById('mediaFolderContextMenu');
            if (!menu) {
                menu = document.createElement('div');
                menu.id = 'mediaFolderContextMenu';
                menu.className = 'dropdown-menu shadow-lg py-1 border-0 common-rounded-card';
                menu.style.position = 'fixed';
                menu.style.zIndex = '10070';
                menu.style.display = 'none';
                menu.innerHTML = `
                    <a class="dropdown-item py-1 px-3 small d-flex align-items-center" href="javascript:void(0)" onclick="folderContextMenuAction('createSub')">
                        <i class="fa-solid fa-folder-plus me-2"></i> 하위 폴더 만들기
                    </a>
                    <a class="dropdown-item py-1 px-3 small d-flex align-items-center" href="javascript:void(0)" onclick="folderContextMenuAction('rename')">
                        <i class="fa-solid fa-pen me-2"></i> 이름 바꾸기
                    </a>
                    <div class="dropdown-divider my-1"></div>
                    <a class="dropdown-item py-1 px-3 small text-danger d-flex align-items-center" href="javascript:void(0)" onclick="folderContextMenuAction('delete')">
                        <i class="fa-solid fa-trash me-2"></i> 폴더 삭제
                    </a>
                `;
                document.body.appendChild(menu);
            }
            return menu;
        }

        function handleFolderContextMenu(event, folderId, folderName) {
            event.preventDefault();
            event.stopPropagation();
            closeContextMenu();

            contextMenuTargetFolder = { id: parseInt(folderId, 10), name: folderName };

            const menu = getOrCreateFolderContextMenuEl();
            menu.style.display = 'block';

            let posX = event.clientX;
            let posY = event.clientY;
            const menuWidth = menu.offsetWidth || 160;
            const menuHeight = menu.offsetHeight || 130;

            if (posX + menuWidth > window.innerWidth) posX = window.innerWidth - menuWidth - 10;
            if (posY + menuHeight > window.innerHeight) posY = window.innerHeight - menuHeight - 10;

            menu.style.left = `${posX}px`;
            menu.style.top = `${posY}px`;
        }

        function closeFolderContextMenu() {
            const menu = document.getElementById('mediaFolderContextMenu');
            if (menu) menu.style.display = 'none';
        }

        function folderContextMenuAction(action) {
            if (!contextMenuTargetFolder) {
                closeFolderContextMenu();
                return;
            }

            const { id, name } = contextMenuTargetFolder;
            closeFolderContextMenu();

            switch (action) {
                case 'createSub':
                    promptCreateSubFolder(id, name);
                    break;
                case 'rename':
                    promptRenameFolder(id, name);
                    break;
                case 'delete':
                    deleteMediaFolderCascade(id, name);
                    break;
            }
        }

        function promptCreateSubFolder(parentId, parentName) {
            if (typeof Swal === 'undefined') {
                const newName = prompt(`'${parentName}' 폴더 내에 생성할 새 폴더명을 입력해 주세요:`);
                if (newName && newName.trim()) executeCreateSubFolder(parentId, newName.trim());
                return;
            }

            Swal.fire({
                title: '하위 폴더 만들기',
                html: `<div class="text-muted small mb-2 text-start"><strong>'${parentName}'</strong> 폴더 내에 새 폴더를 생성합니다.</div>`,
                input: 'text',
                inputPlaceholder: '새 폴더명을 입력하세요',
                showCancelButton: true,
                confirmButtonText: '생성',
                cancelButtonText: '취소',
                confirmButtonColor: '#0d6efd',
                inputValidator: (val) => (!val || !val.trim()) ? '폴더명을 입력해 주세요!' : null
            }).then((result) => {
                if (result.isConfirmed && result.value) {
                    executeCreateSubFolder(parentId, result.value.trim());
                }
            });
        }

        async function executeCreateSubFolder(parentId, folderName) {
            try {
                const res = await fetch('/admin/image-folders', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': getCsrfToken(),
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({ name: folderName, parent_id: parentId })
                });
                const data = await res.json();
                if (data.success) {
                    if (parentId) expandedFolderIds.add(parentId);
                    fetchFolders();
                    loadImages(1, false);
                } else {
                    alert(data.message || '폴더 생성 실패');
                }
            } catch (e) {
                console.error('Create Folder Error:', e);
            }
        }

        function promptRenameFolder(folderId, currentName) {
            if (typeof Swal === 'undefined') {
                const newName = prompt('새 폴더명을 입력해 주세요:', currentName);
                if (newName && newName.trim()) executeRenameFolder(folderId, newName.trim());
                return;
            }

            Swal.fire({
                title: '폴더 이름 바꾸기',
                input: 'text',
                inputValue: currentName,
                showCancelButton: true,
                confirmButtonText: '변경',
                cancelButtonText: '취소',
                confirmButtonColor: '#0d6efd',
                inputValidator: (val) => (!val || !val.trim()) ? '폴더명을 입력해 주세요!' : null
            }).then((result) => {
                if (result.isConfirmed && result.value) {
                    executeRenameFolder(folderId, result.value.trim());
                }
            });
        }

        async function executeRenameFolder(folderId, newName) {
            try {
                const res = await fetch(`/admin/image-folders/${folderId}/rename`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': getCsrfToken(),
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({ name: newName })
                });
                const data = await res.json();
                if (data.success) {
                    fetchFolders();
                    loadImages(1, false);
                } else {
                    alert(data.message || '폴더명 변경 실패');
                }
            } catch (e) {
                console.error('Rename Folder Error:', e);
            }
        }

        function deleteMediaFolderCascade(folderId, folderName) {
            const doDelete = async () => {
                try {
                    const res = await fetch(`/admin/image-folders/${folderId}`, {
                        method: 'DELETE',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-CSRF-TOKEN': getCsrfToken(),
                            'X-Requested-With': 'XMLHttpRequest'
                        }
                    });
                    const data = await res.json();
                    if (data.success) {
                        if (String(currentFolderId) === String(folderId)) {
                            currentFolderId = 'all';
                        }
                        fetchFolders();
                        loadImages(1, false);
                    } else {
                        alert(data.message || '폴더 삭제 실패');
                    }
                } catch (e) {
                    console.error('Delete Folder Error:', e);
                }
            };

            if (typeof Swal !== 'undefined') {
                Swal.fire({
                    title: `'${folderName}' 폴더 삭제`,
                    html: `이 폴더와 <strong>모든 하위 폴더가 함께 삭제</strong>됩니다.<br><small class="text-muted">(폴더 안의 이미지는 전체이미지에서 확인 가능)</small>`,
                    icon: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#dc3545',
                    confirmButtonText: '삭제하기',
                    cancelButtonText: '취소'
                }).then((res) => {
                    if (res.isConfirmed) doDelete();
                });
            } else {
                if (confirm(`'${folderName}' 폴더를 삭제하시겠습니까?`)) doDelete();
            }
        }

        function getOrCreateImageContextMenuEl() {
            let menu = document.getElementById('mediaFileContextMenu');
            if (!menu) {
                menu = document.createElement('div');
                menu.id = 'mediaFileContextMenu';
                menu.className = 'dropdown-menu shadow-lg py-1 border-0 common-rounded-card';
                menu.style.position = 'fixed';
                menu.style.zIndex = '10070';
                menu.style.display = 'none';
                menu.innerHTML = `
                    <a class="dropdown-item py-1 px-3 small d-flex align-items-center" href="javascript:void(0)" onclick="contextMenuAction('info')">
                        <i class="fa-solid fa-circle-info me-2 text-primary"></i> 상세정보 보기 & 수정
                    </a>
                    <a class="dropdown-item py-1 px-3 small d-flex align-items-center" href="javascript:void(0)" onclick="contextMenuAction('view')">
                        <i class="fa-solid fa-arrow-up-right-from-square me-2 text-secondary"></i> 원본 새 탭 보기
                    </a>
                    <a class="dropdown-item py-1 px-3 small d-flex align-items-center" href="javascript:void(0)" onclick="contextMenuAction('rename')">
                        <i class="fa-solid fa-pen me-2 text-secondary"></i> 이름 바꾸기
                    </a>
                    <a class="dropdown-item py-1 px-3 small d-flex align-items-center" id="contextMenuProtectBtn" href="javascript:void(0)" onclick="contextMenuAction('protect')">
                        <i class="fa-solid fa-lock me-2 text-warning"></i> 보호 잠금 설정/해제
                    </a>
                    <div class="dropdown-divider my-1" id="contextMenuDeleteDivider"></div>
                    <a class="dropdown-item py-1 px-3 small text-danger d-flex align-items-center" id="contextMenuDeleteBtn" href="javascript:void(0)" onclick="contextMenuAction('delete')">
                        <i class="fa-solid fa-trash me-2"></i> 삭제하기
                    </a>
                `;
                document.body.appendChild(menu);
            }
            return menu;
        }

        function handleCardContextMenu(event, item) {
            event.preventDefault();
            event.stopPropagation();
            closeFolderContextMenu();

            const id = parseInt(item.id, 10);
            contextMenuTargetImage = item;

            if (!selectedImagesMap.has(id)) {
                selectedImagesMap.clear();
                selectedFoldersMap.clear();
                selectedImagesMap.set(id, {
                    id,
                    path: item.path,
                    name: item.original_name,
                    webp_url: item.webp_url || '',
                    thumb_url: item.thumb_url || ''
                });
                syncSelectionUI();
            }

            const menu = getOrCreateImageContextMenuEl();
            const selectedCount = selectedImagesMap.size;

            const infoBtn = menu.querySelector('a[onclick*="\'info\'"]');
            const viewBtn = menu.querySelector('a[onclick*="\'view\'"]');
            const renameBtn = menu.querySelector('a[onclick*="\'rename\'"]');
            const protectBtn = document.getElementById('contextMenuProtectBtn');
            const deleteBtn = document.getElementById('contextMenuDeleteBtn');

            if (selectedCount > 1) {
                if (infoBtn) infoBtn.style.display = 'none';
                if (viewBtn) viewBtn.style.display = 'none';
                if (renameBtn) renameBtn.style.display = 'none';
                if (protectBtn) protectBtn.innerHTML = `<i class="fa-solid fa-shield-halved text-warning me-2"></i> 선택한 ${selectedCount}개 일괄 보호/해제`;
                if (deleteBtn) deleteBtn.innerHTML = `<i class="fa-solid fa-trash me-2"></i> 선택한 ${selectedCount}개 일괄 삭제`;
            } else {
                if (infoBtn) infoBtn.style.display = 'flex';
                if (viewBtn) viewBtn.style.display = 'flex';
                if (renameBtn) renameBtn.style.display = 'flex';
                if (protectBtn) protectBtn.innerHTML = `<i class="fa-solid fa-lock text-warning me-2"></i> 보호 잠금 설정/해제`;
                if (deleteBtn) deleteBtn.innerHTML = `<i class="fa-solid fa-trash me-2"></i> 삭제하기`;
            }

            menu.style.display = 'block';
            let posX = event.clientX;
            let posY = event.clientY;
            const menuWidth = menu.offsetWidth || 180;
            const menuHeight = menu.offsetHeight || 190;

            if (posX + menuWidth > window.innerWidth) posX = window.innerWidth - menuWidth - 10;
            if (posY + menuHeight > window.innerHeight) posY = window.innerHeight - menuHeight - 10;

            menu.style.left = `${posX}px`;
            menu.style.top = `${posY}px`;
        }

        function closeContextMenu() {
            const menu = document.getElementById('mediaFileContextMenu');
            if (menu) menu.style.display = 'none';
        }

        function contextMenuAction(action) {
            if (!contextMenuTargetImage) {
                closeContextMenu();
                return;
            }

            const img = contextMenuTargetImage;
            closeContextMenu();

            switch (action) {
                case 'info':
                    fetchImageDetail(img.id);
                    break;
                case 'view':
                    window.open(`/storage/${img.path}`, '_blank');
                    break;
                case 'rename':
                    promptRenameImage(img.id, img.original_name);
                    break;
                case 'protect':
                    if (selectedImagesMap.size > 1) {
                        bulkToggleProtection();
                    } else {
                        toggleProtectCurrent();
                    }
                    break;
                case 'delete':
                    if (selectedImagesMap.size > 1) {
                        deleteMultipleImages();
                    } else {
                        deleteCurrentImage();
                    }
                    break;
            }
        }

        function promptRenameImage(imageId, currentName) {
            if (typeof Swal === 'undefined') {
                const newName = prompt('새 파일명을 입력해 주세요:', currentName);
                if (newName && newName.trim()) executeRenameImage(imageId, newName.trim());
                return;
            }

            Swal.fire({
                title: '파일명 변경',
                input: 'text',
                inputValue: currentName,
                showCancelButton: true,
                confirmButtonText: '변경',
                cancelButtonText: '취소',
                confirmButtonColor: '#0d6efd',
                inputValidator: (val) => (!val || !val.trim()) ? '파일명을 입력해 주세요!' : null
            }).then((result) => {
                if (result.isConfirmed && result.value) {
                    executeRenameImage(imageId, result.value.trim());
                }
            });
        }

        async function executeRenameImage(imageId, newName) {
            try {
                const res = await fetch(`/admin/images/${imageId}/rename`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': getCsrfToken(),
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({ name: newName })
                });
                const data = await res.json();
                if (data.success) {
                    loadImages(currentPage, false);
                } else {
                    alert(data.message || '파일명 변경 실패');
                }
            } catch (e) {
                console.error('Rename Image Error:', e);
            }
        }

        async function bulkToggleProtection() {
            const ids = Array.from(selectedImagesMap.keys());
            if (ids.length === 0) return;

            try {
                const res = await fetch('/admin/images/bulk-toggle-protection', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': getCsrfToken(),
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({ ids: ids })
                });
                const json = await res.json();
                if (json.success) {
                    if (typeof Swal !== 'undefined') {
                        Swal.fire({ icon: 'info', title: '보호 설정 변경', text: json.message, timer: 1400, showConfirmButton: false });
                    }
                    loadImages(currentPage, false);
                }
            } catch (e) {
                console.error('Bulk Protect Toggle Error:', e);
            }
        }

        // =========================================================================
        // 7. 우측 인스펙터 패널 제어
        // =========================================================================
        async function fetchImageDetail(id) {
            try {
                const res = await fetch(`/admin/images/${id}`, {
                    headers: { 'X-Requested-With': 'XMLHttpRequest' }
                });
                const json = await res.json();
                if (json.success && json.image) {
                    renderSingleInspector(json.image);
                }
            } catch (e) {
                console.error('상세 정보 조회 실패:', e);
            }
        }

        function renderSingleInspector(img) {
            const emptyState = document.getElementById('inspectorEmptyState');
            const content = document.getElementById('inspectorContent');
            const singleForm = document.getElementById('inspectorForm');
            const multiView = document.getElementById('multiInspectorView');

            if (emptyState) emptyState.classList.add('d-none');
            if (content) {
                content.classList.remove('d-none');
                content.classList.add('d-flex');
            }

            if (multiView) multiView.classList.add('d-none');
            if (singleForm) {
                singleForm.classList.remove('d-none');
                singleForm.classList.add('d-flex');
            }

            const insImageId = document.getElementById('insImageId');
            const insPreviewImg = document.getElementById('insPreviewImg');
            const insDimensions = document.getElementById('insDimensions');
            const insSize = document.getElementById('insSize');
            const insExt = document.getElementById('insExt');
            const insUsedBadge = document.getElementById('insUsedBadge');
            const insTitle = document.getElementById('insTitle');
            const insAltText = document.getElementById('insAltText');
            const insTags = document.getElementById('insTags');
            const insOriginalName = document.getElementById('insOriginalName');
            const protectBadge = document.getElementById('insProtectedBadge');

            if (insImageId) insImageId.value = img.id;

            const highResUrl = img.webp_url 
                || img.avif_url 
                || img.original_url 
                || (img.path ? `/storage/${img.path}` : '');

            if (insPreviewImg) {
                insPreviewImg.src = '';
                insPreviewImg.src = highResUrl;
                insPreviewImg.alt = img.alt_text || img.title || img.original_name;

                insPreviewImg.onerror = function () {
                    if (img.original_url && this.src !== img.original_url) {
                        this.src = img.original_url;
                    } else if (img.thumb_url && this.src !== img.thumb_url) {
                        this.src = img.thumb_url;
                    }
                };
            }

            if (insDimensions) insDimensions.innerText = `${img.width || 0} × ${img.height || 0} px`;
            if (insSize) insSize.innerText = img.formatted_size || '-';
            if (insExt) insExt.innerText = (img.path ? img.path.split('.').pop() : 'IMG').toUpperCase();

            if (insUsedBadge) {
                insUsedBadge.innerText = img.is_used ? '실사용 중' : '미사용';
                insUsedBadge.className = img.is_used 
                    ? 'badge bg-success-subtle text-success border' 
                    : 'badge bg-light text-muted border';
            }

            if (insTitle) insTitle.value = img.title || '';
            if (insAltText) insAltText.value = img.alt_text || '';
            if (insTags) insTags.value = Array.isArray(img.tags) ? img.tags.join(', ') : (img.tags || '');
            if (insOriginalName) insOriginalName.value = img.original_name || '';

            if (protectBadge) {
                protectBadge.classList.toggle('d-none', !img.is_protected);
            }
        }

        function renderMultiSelectInspector(imageCount, folderCount) {
            const emptyState = document.getElementById('inspectorEmptyState');
            const content = document.getElementById('inspectorContent');
            const singleForm = document.getElementById('inspectorForm');
            const multiView = document.getElementById('multiInspectorView');

            if (emptyState) emptyState.classList.add('d-none');
            if (content) {
                content.classList.remove('d-none');
                content.classList.add('d-flex');
            }

            if (singleForm) {
                singleForm.classList.remove('d-flex');
                singleForm.classList.add('d-none');
            }
            if (multiView) {
                multiView.classList.remove('d-none');
                multiView.classList.add('d-flex');
            }

            const titleEl = document.getElementById('multiSelectTitle');
            const descEl = document.getElementById('multiSelectDesc');
            const btnProtect = document.getElementById('multiBtnProtect');
            const btnDelete = document.getElementById('multiBtnDelete');

            let textParts = [];
            if (imageCount > 0) textParts.push(`이미지 ${imageCount}개`);
            if (folderCount > 0) textParts.push(`폴더 ${folderCount}개`);

            if (titleEl) titleEl.innerText = `${textParts.join(', ')} 선택됨`;
            if (descEl) descEl.innerText = `선택한 항목들을 마우스로 잡고 좌측 폴더로 드래그하면 한 번에 이동할 수 있습니다.`;

            if (btnProtect) btnProtect.classList.toggle('d-none', imageCount === 0);
            if (btnDelete) btnDelete.classList.toggle('d-none', imageCount === 0);
        }

        function closeInspector() {
            const content = document.getElementById('inspectorContent');
            const emptyState = document.getElementById('inspectorEmptyState');
            const singleForm = document.getElementById('inspectorForm');
            const multiView = document.getElementById('multiInspectorView');

            if (content) {
                content.classList.remove('d-flex');
                content.classList.add('d-none');
            }
            if (emptyState) {
                emptyState.classList.remove('d-none');
                emptyState.classList.add('d-flex');
            }

            if (singleForm) {
                singleForm.classList.remove('d-flex');
                singleForm.classList.add('d-none');
                singleForm.reset();
            }
            if (multiView) {
                multiView.classList.remove('d-flex');
                multiView.classList.add('d-none');
            }

            const previewImg = document.getElementById('insPreviewImg');
            if (previewImg) previewImg.src = '';
        }

        async function saveImageMeta() {
            const singleId = Array.from(selectedImagesMap.keys())[0];
            if (!singleId) return;

            const payload = {
                title: document.getElementById('insTitle')?.value.trim() || '',
                alt_text: document.getElementById('insAltText')?.value.trim() || '',
                tags: document.getElementById('insTags')?.value.trim() || '',
                original_name: document.getElementById('insOriginalName')?.value.trim() || '',
            };

            try {
                const res = await fetch(`/admin/images/${singleId}`, {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': getCsrfToken(),
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify(payload)
                });

                const json = await res.json();
                if (json.success) {
                    if (typeof Swal !== 'undefined') {
                        Swal.fire({
                            icon: 'success',
                            title: '저장 완료',
                            text: '이미지 속성이 반영되었습니다.',
                            timer: 1300,
                            showConfirmButton: false
                        });
                    }
                    const cardTitle = document.querySelector(`#thumb-card-${singleId} .win-card-title`) ||
                                      document.querySelector(`#thumb-card-${singleId} .col-name-val`);
                    if (cardTitle) cardTitle.innerText = payload.title || payload.original_name;
                } else {
                    throw new Error(json.message);
                }
            } catch (e) {
                alert(e.message || '저장 실패');
            }
        }

        async function toggleProtectCurrent() {
            const singleId = Array.from(selectedImagesMap.keys())[0];
            if (!singleId) return;

            try {
                const res = await fetch(`/admin/images/${singleId}/toggle-protect`, {
                    method: 'POST',
                    headers: {
                        'X-CSRF-TOKEN': getCsrfToken(),
                        'X-Requested-With': 'XMLHttpRequest'
                    }
                });
                const json = await res.json();
                if (json.success) {
                    const badge = document.getElementById('insProtectedBadge');
                    if (badge) badge.classList.toggle('d-none', !json.is_protected);

                    if (typeof Swal !== 'undefined') {
                        Swal.fire({ icon: 'info', title: '보호 설정 변경', text: json.message, timer: 1200, showConfirmButton: false });
                    }
                } else {
                    alert(json.message);
                }
            } catch (e) {
                console.error('Protect Toggle Error:', e);
            }
        }

        async function deleteCurrentImage() {
            const singleId = Array.from(selectedImagesMap.keys())[0];
            if (!singleId) return;

            const doDelete = async () => {
                try {
                    const res = await fetch(`/admin/images/${singleId}`, {
                        method: 'DELETE',
                        headers: {
                            'X-CSRF-TOKEN': getCsrfToken(),
                            'X-Requested-With': 'XMLHttpRequest'
                        }
                    });
                    const json = await res.json();
                    if (json.success) {
                        document.getElementById(`thumb-card-${singleId}`)?.remove();
                        clearAllSelection();
                        if (typeof Swal !== 'undefined') {
                            Swal.fire('삭제 완료', json.message, 'success');
                        }
                    } else {
                        alert(json.message);
                    }
                } catch (e) {
                    alert('삭제 중 오류가 발생했습니다.');
                }
            };

            if (typeof Swal !== 'undefined') {
                Swal.fire({
                    title: '이미지 영구 삭제',
                    text: '이 이미지를 영구적으로 삭제하시겠습니까?',
                    icon: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#dc3545',
                    confirmButtonText: '삭제 실행',
                    cancelButtonText: '취소'
                }).then((res) => {
                    if (res.isConfirmed) doDelete();
                });
            } else {
                if (confirm('이 이미지를 삭제하시겠습니까?')) doDelete();
            }
        }

        async function deleteMultipleImages() {
            if (selectedImagesMap.size === 0) return;
            const ids = Array.from(selectedImagesMap.keys());

            const doBulkDelete = async () => {
                try {
                    const res = await fetch('/admin/images/bulk-delete', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-CSRF-TOKEN': getCsrfToken(),
                            'X-Requested-With': 'XMLHttpRequest'
                        },
                        body: JSON.stringify({ ids: ids })
                    });

                    const json = await res.json();
                    if (json.success) {
                        ids.forEach(id => document.getElementById(`thumb-card-${id}`)?.remove());
                        clearAllSelection();

                        if (typeof Swal !== 'undefined') {
                            Swal.fire('삭제 완료', json.message, 'success');
                        }
                    } else {
                        alert(json.message);
                    }
                } catch (e) {
                    alert('일괄 삭제 중 오류가 발생했습니다.');
                }
            };

            if (typeof Swal !== 'undefined') {
                Swal.fire({
                    title: '선택 이미지 일괄 삭제',
                    text: `선택하신 ${ids.length}개의 이미지를 모두 삭제하시겠습니까? (보호 중이거나 실사용 중인 이미지는 제외)`,
                    icon: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#dc3545',
                    confirmButtonText: '일괄 삭제 실행',
                    cancelButtonText: '취소'
                }).then((res) => {
                    if (res.isConfirmed) doBulkDelete();
                });
            } else {
                if (confirm(`선택하신 ${ids.length}개의 이미지를 모두 삭제하시겠습니까?`)) {
                    doBulkDelete();
                }
            }
        }

        // =========================================================================
        // 8. 뷰 모드 스위처 및 4대 항목 정렬 토글
        // =========================================================================
        function toggleMediaSort(type) {
            if (currentSortBy === type) {
                currentSortDirection = (currentSortDirection === 'asc') ? 'desc' : 'asc';
            } else {
                currentSortBy = type;
                currentSortDirection = (type === 'date') ? 'desc' : 'asc';
            }

            updateMediaSortUI();
            loadImages(1, false);
        }

        function updateMediaSortUI() {
            const sortTypes = ['date', 'name', 'size'];

            sortTypes.forEach(t => {
                const cap = t.charAt(0).toUpperCase() + t.slice(1);
                const btn = document.getElementById(`sortBtn${cap}`) || document.getElementById(`sort-btn-${t}`);
                if (!btn) return;

                const icon = btn.querySelector('.sort-icon') || btn.querySelector('i');
                const isCurrent = (t === currentSortBy);

                if (isCurrent) {
                    btn.classList.add('active');
                    if (icon) {
                        icon.classList.remove('text-muted');
                        if (currentSortDirection === 'asc') {
                            icon.className = 'fa-solid fa-arrow-up-short-wide ms-1 sort-icon';
                        } else {
                            icon.className = 'fa-solid fa-arrow-down-short-wide ms-1 sort-icon';
                        }
                    }
                } else {
                    btn.classList.remove('active');
                    if (icon) {
                        icon.className = 'fa-solid fa-sort ms-1 sort-icon text-muted';
                    }
                }
            });
        }

        function changeMediaViewMode(mode) {
            currentViewMode = mode;
            localStorage.setItem('dam_view_mode', mode);

            const container = document.getElementById('damImageContainer');
            const listHeader = document.getElementById('damListHeader');

            if (container) container.className = `view-${mode}`;
            if (listHeader) listHeader.classList.toggle('d-none', mode !== 'list');

            ['large', 'small', 'list'].forEach(m => {
                const cap = m.charAt(0).toUpperCase() + m.slice(1);
                const btn = document.getElementById(`viewBtn${cap}`) || document.getElementById(`view-mode-${m}`);
                if (btn) {
                    btn.classList.toggle('active', m === mode);
                }
            });

            loadImages(1, false);
        }

        function resetDamFilters() {
            const keywordInput = document.getElementById('damSearchKeyword');
            if (keywordInput) keywordInput.value = '';

            currentSortBy = 'date';
            currentSortDirection = 'desc';

            updateMediaSortUI();
            clearAllSelection();
            loadImages(1, false);
        }

        // =========================================================================
        // 9. 리사이저 및 업로드 이벤트
        // =========================================================================
        function initResizer() {
            const resizer = document.getElementById('winResizer');
            const panel = document.getElementById('winInspectorPanel');
            if (!resizer || !panel) return;

            let isResizing = false;
            const savedWidth = localStorage.getItem('dam_inspector_width');
            if (savedWidth) panel.style.width = `${savedWidth}px`;

            resizer.addEventListener('mousedown', () => {
                isResizing = true;
                resizer.classList.add('resizing');
                document.body.style.cursor = 'col-resize';
                document.body.style.userSelect = 'none';
            });

            document.addEventListener('mousemove', (e) => {
                if (!isResizing) return;
                const container = document.getElementById('winExplorer');
                if (!container) return;
                const containerRect = container.getBoundingClientRect();
                let newWidth = containerRect.right - e.clientX;
                if (newWidth >= 280 && newWidth <= 650) {
                    panel.style.width = `${newWidth}px`;
                    localStorage.setItem('dam_inspector_width', newWidth);
                }
            });

            document.addEventListener('mouseup', () => {
                if (isResizing) {
                    isResizing = false;
                    resizer.classList.remove('resizing');
                    document.body.style.cursor = '';
                    document.body.style.userSelect = '';
                }
            });
        }

        function initUpload() {
            const dropContainer = document.getElementById('winDropZoneContainer');
            const dropOverlay = document.getElementById('winDropOverlay');
            const directFileInput = document.getElementById('directFileInput');

            if (dropContainer && dropOverlay) {
                dropContainer.addEventListener('dragover', (e) => {
                    if (currentDragType) return;
                    e.preventDefault();
                    dropOverlay.classList.add('active');
                });

                dropContainer.addEventListener('dragleave', (e) => {
                    if (e.relatedTarget === null || !dropContainer.contains(e.relatedTarget)) {
                        dropOverlay.classList.remove('active');
                    }
                });

                dropContainer.addEventListener('drop', (e) => {
                    if (currentDragType) return;
                    e.preventDefault();
                    dropOverlay.classList.remove('active');
                    const files = e.dataTransfer.files;
                    if (files.length > 0) handleDirectUpload(files);
                });
            }

            if (directFileInput) {
                directFileInput.addEventListener('change', (e) => {
                    if (e.target.files.length > 0) handleDirectUpload(e.target.files);
                });
            }
        }

        async function handleDirectUpload(fileList) {
            let uploadedCount = 0;

            for (let file of fileList) {
                if (!file.type.startsWith('image/')) continue;

                const formData = new FormData();
                formData.append('image', file);
                if (currentFolderId !== 'all' && currentFolderId !== 'orphans') {
                    formData.append('folder_id', currentFolderId);
                }

                try {
                    const res = await fetch('/admin/images', {
                        method: 'POST',
                        headers: {
                            'X-CSRF-TOKEN': getCsrfToken(),
                            'X-Requested-With': 'XMLHttpRequest'
                        },
                        body: formData
                    });
                    const json = await res.json();
                    if (json.success && json.data) {
                        uploadedCount++;
                    }
                } catch (e) {
                    console.error('업로드 실패:', file.name, e);
                }
            }

            if (uploadedCount > 0) {
                loadImages(1, false);
                fetchFolders();
            }
        }

        // =========================================================================
        // 10. 전역 바인딩
        // =========================================================================
        window.selectFolder = selectFolder;
        window.toggleTreeExpand = toggleTreeExpand;
        window.navHistoryBack = navHistoryBack;
        window.navHistoryForward = navHistoryForward;
        window.navGoParent = navGoParent;
        window.navGoHistoryIndex = navGoHistoryIndex;
        window.toggleMediaSort = toggleMediaSort;
        window.toggleSort = toggleMediaSort;
        window.changeMediaViewMode = changeMediaViewMode;
        window.switchViewMode = changeMediaViewMode;
        window.loadImages = loadImages;
        window.resetDamFilters = resetDamFilters;
        window.closeInspector = closeInspector;
        window.saveImageMeta = saveImageMeta;
        window.toggleProtectCurrent = toggleProtectCurrent;
        window.deleteCurrentImage = deleteCurrentImage;
        window.deleteMultipleImages = deleteMultipleImages;
        window.bulkToggleProtection = bulkToggleProtection;
        window.promptCreateSubFolder = promptCreateSubFolder;
        window.promptRenameFolder = promptRenameFolder;
        window.deleteMediaFolderCascade = deleteMediaFolderCascade;
        window.folderContextMenuAction = folderContextMenuAction;
        window.contextMenuAction = contextMenuAction;

        document.addEventListener('DOMContentLoaded', () => {
            initResizer();
            initUpload();
            initMarqueeSelection();
            fetchFolders();
            changeMediaViewMode(currentViewMode);
            updateMediaSortUI();
            updateNavButtons();
        });
    })();

---

## 6. 블레이드 뷰 템플릿 소스코드

### 6.1. `resources/views/admin/images/list.blade.php`

    @extends('admin.layouts.master')

    @section('title', '이미지 자산 관리 (DAM)')

    @push('styles')
        <link rel="stylesheet" href="{{ asset('css/admin/admin_image.css') }}?v={{ time() }}">
    @endpush

    @section('content')
    <div class="win-explorer-root" id="winExplorer">
        <!-- 1. 상단 툴바 -->
        <div class="win-toolbar">
            <div class="d-flex align-items-center gap-2">
                <div class="win-nav-group">
                    <button type="button" class="win-nav-btn" id="btnNavBack" onclick="navHistoryBack()" title="뒤로" disabled>
                        <i class="fa-solid fa-arrow-left"></i>
                    </button>
                    <button type="button" class="win-nav-btn" id="btnNavForward" onclick="navHistoryForward()" title="앞으로" disabled>
                        <i class="fa-solid fa-arrow-right"></i>
                    </button>
                    <button type="button" class="win-nav-btn" id="btnNavUp" onclick="navGoParent()" title="상위 폴더">
                        <i class="fa-solid fa-arrow-up"></i>
                    </button>
                    <div class="dropdown d-inline-block">
                        <button type="button" class="win-nav-btn dropdown-toggle" data-bs-toggle="dropdown" title="방문 기록">
                            <i class="fa-solid fa-caret-down"></i>
                        </button>
                        <ul class="dropdown-menu shadow-sm small" id="folderHistoryDropdown" style="font-size: 0.8rem;">
                            <li><span class="dropdown-item-text text-muted">방문 기록 없음</span></li>
                        </ul>
                    </div>
                </div>

                <div class="win-toolbar-path-badge text-truncate">
                    <i class="fa-solid fa-folder-open text-warning"></i>
                    <span id="currentFolderBreadcrumb" class="fw-semibold text-truncate">전체 이미지</span>
                </div>
            </div>

            <!-- 중앙 정렬 및 뷰 모드 토글 -->
            <div class="d-flex align-items-center gap-2">
                <!-- 정렬 3칸 토글 그룹 -->
                <div class="btn-group-sm common-rounded-btn-group overflow-hidden" role="group" aria-label="정렬 토글">
                    <button type="button" class="btn btn-outline-secondary py-1 px-2 sort-toggle-btn active" id="sortBtnDate" onclick="toggleMediaSort('date')" title="날짜순 정렬 토글">
                        <span>날짜순</span> <i class="fa-solid fa-arrow-down-short-wide ms-1 sort-icon"></i>
                    </button>
                    <button type="button" class="btn btn-outline-secondary py-1 px-2 sort-toggle-btn" id="sortBtnName" onclick="toggleMediaSort('name')" title="파일이름 정렬 토글">
                        <span>파일이름</span> <i class="fa-solid fa-sort ms-1 sort-icon text-muted"></i>
                    </button>
                    <button type="button" class="btn btn-outline-secondary py-1 px-2 sort-toggle-btn" id="sortBtnSize" onclick="toggleMediaSort('size')" title="파일용량 정렬 토글">
                        <span>파일용량</span> <i class="fa-solid fa-sort ms-1 sort-icon text-muted"></i>
                    </button>
                </div>

                <!-- 보기 방식 전환 아이콘 그룹 -->
                <div class="btn-group-sm common-rounded-btn-group overflow-hidden" role="group" aria-label="보기 방식">
                    <button type="button" class="btn btn-outline-secondary py-1 px-2 view-mode-btn active" id="viewBtnLarge" onclick="changeMediaViewMode('large')" title="큰 아이콘 보기">
                        <i class="fa-solid fa-table-cells-large"></i>
                    </button>
                    <button type="button" class="btn btn-outline-secondary py-1 px-2 view-mode-btn" id="viewBtnSmall" onclick="changeMediaViewMode('small')" title="중간 아이콘 보기">
                        <i class="fa-solid fa-table-cells"></i>
                    </button>
                    <button type="button" class="btn btn-outline-secondary py-1 px-2 view-mode-btn" id="viewBtnList" onclick="changeMediaViewMode('list')" title="자세히 보기 (목록형)">
                        <i class="fa-solid fa-list"></i>
                    </button>
                </div>
            </div>

            <!-- 우측 검색 및 업로드 -->
            <div class="d-flex align-items-center gap-2">
                <div class="input-group input-group-sm" style="width: 220px;">
                    <span class="input-group-text bg-white border-end-0 text-muted"><i class="fa-solid fa-magnifying-glass"></i></span>
                    <input type="text" id="damSearchKeyword" class="form-control border-start-0" placeholder="검색어 입력..." onkeydown="if(event.key==='Enter') loadImages(1, false);">
                </div>
                <button type="button" class="btn btn-primary btn-sm win-toolbar-btn" onclick="document.getElementById('directFileInput').click()">
                    <i class="fa-solid fa-cloud-arrow-up"></i> 업로드
                </button>
                <input type="file" id="directFileInput" class="d-none" multiple accept="image/*">
            </div>
        </div>

        <!-- 2. 워크스페이스 본문 -->
        <div class="win-workspace">
            <!-- 1열: 좌측 트리 패널 -->
            <div class="win-tree-panel">
                <div class="d-flex flex-column gap-1" id="treeRootContainer">
                    <div class="text-center py-4 text-muted small">
                        <div class="spinner-border spinner-border-sm text-primary mb-2" role="status"></div>
                        <div style="font-size: 0.75rem;">폴더 목록 로딩 중...</div>
                    </div>
                </div>
            </div>

            <!-- 2열: 중앙 갤러리 패널 -->
            <div class="win-grid-panel" id="winDropZoneContainer">
                <div class="win-drop-overlay" id="winDropOverlay">
                    <div class="text-center text-primary">
                        <i class="fa-solid fa-cloud-arrow-up fa-3x mb-2"></i>
                        <h5 class="fw-bold">여기에 파일을 드롭하여 업로드</h5>
                    </div>
                </div>

                <div id="damMarqueeBox"></div>

                <div class="dam-list-header d-none" id="damListHeader">
                    <div class="dam-col-header col-name" onclick="toggleMediaSort('name')">이름</div>
                    <div class="dam-col-header col-folder">폴더</div>
                    <div class="dam-col-header col-date" onclick="toggleMediaSort('date')">날짜</div>
                    <div class="dam-col-header col-type">유형</div>
                    <div class="dam-col-header col-size text-end" onclick="toggleMediaSort('size')">크기</div>
                </div>

                <div id="damImageContainer" class="view-large"></div>

                <div id="damLoadingSpinner" class="text-center py-4 d-none">
                    <div class="spinner-border spinner-border-sm text-primary" role="status"></div>
                    <span class="small text-muted ms-2">이미지 로딩 중...</span>
                </div>

                <div id="damEmptyState" class="text-center py-5 text-muted small d-none">
                    <i class="fa-regular fa-image fa-3x mb-3 text-secondary opacity-50"></i>
                    <p>표시할 이미지가 없습니다.</p>
                </div>
            </div>

            <!-- 리사이저 바 -->
            <div class="win-resizer" id="winResizer"></div>

            <!-- 3열: 우측 인스펙터 패널 -->
            <div class="win-inspector-panel d-flex flex-column h-100" id="winInspectorPanel">
                <!-- 선택 없음 상태 -->
                <div id="inspectorEmptyState" class="h-100 d-flex flex-column align-items-center justify-content-center text-muted small p-4">
                    <i class="fa-solid fa-circle-info fa-2x mb-2 text-secondary opacity-50"></i>
                    <span>이미지를 선택하면 상세 정보가 표시됩니다.</span>
                </div>

                <!-- 인스펙터 활성 본문 -->
                <div id="inspectorContent" class="h-100 d-none flex-column overflow-hidden">
                    <!-- 단일 이미지 선택 폼 -->
                    <form id="inspectorForm" onsubmit="event.preventDefault(); saveImageMeta();" class="h-100 d-none flex-column overflow-hidden">
                        <input type="hidden" id="insImageId" name="id">

                        <div class="win-inspector-scroll-area flex-grow-1 overflow-y-auto p-3">
                            <div class="win-inspector-preview-box">
                                <img id="insPreviewImg" src="" alt="미리보기">
                                <span id="insProtectedBadge" class="position-absolute top-0 end-0 badge bg-warning text-dark m-2 shadow-sm d-none">
                                    <i class="fa-solid fa-lock"></i> 보호 중
                                </span>
                            </div>

                            <div class="d-flex flex-wrap gap-1 mb-3 small">
                                <span class="badge bg-light text-dark border" id="insDimensions">-</span>
                                <span class="badge bg-light text-dark border" id="insSize">-</span>
                                <span class="badge bg-light text-dark border" id="insExt">-</span>
                                <span class="badge bg-light text-dark border" id="insUsedBadge">-</span>
                            </div>

                            <div class="mb-2">
                                <label for="insTitle" class="form-label small fw-bold text-dark mb-1">이미지 타이틀</label>
                                <input type="text" id="insTitle" name="title" class="form-control form-control-sm" placeholder="타이틀 입력">
                            </div>

                            <div class="mb-2">
                                <label for="insAltText" class="form-label small fw-bold text-dark mb-1">대체 텍스트 (Alt)</label>
                                <input type="text" id="insAltText" name="alt_text" class="form-control form-control-sm" placeholder="Alt 텍스트 입력">
                            </div>

                            <div class="mb-2">
                                <label for="insTags" class="form-label small fw-bold text-dark mb-1">검색 태그 (쉼표 구분)</label>
                                <input type="text" id="insTags" name="tags" class="form-control form-control-sm" placeholder="예: 물광, 필링, 강남점">
                            </div>

                            <div class="mb-3">
                                <label for="insOriginalName" class="form-label small fw-bold text-dark mb-1">파일명</label>
                                <input type="text" id="insOriginalName" name="original_name" class="form-control form-control-sm">
                            </div>

                            <div class="mb-2">
                                <button type="button" class="btn btn-outline-secondary btn-sm w-100" id="insBtnToggleProtect" onclick="toggleProtectCurrent()">
                                    <i class="fa-solid fa-lock me-1"></i> 보호 잠금 설정/해제
                                </button>
                            </div>
                        </div>

                        <!-- 하단 고정 우측 정렬 버튼 푸터 -->
                        <div class="win-inspector-footer border-top px-3 py-2 bg-white d-flex align-items-center justify-content-end gap-2 flex-shrink-0">
                            <button type="button" class="btn btn-outline-danger btn-sm px-3 fw-medium d-inline-flex align-items-center gap-1" onclick="deleteCurrentImage()">
                                <i class="fa-solid fa-trash-can"></i> 삭제
                            </button>
                            <button type="submit" class="btn btn-primary btn-sm px-3 fw-bold d-inline-flex align-items-center gap-1">
                                <i class="fa-solid fa-floppy-disk"></i> 수정 저장
                            </button>
                        </div>
                    </form>

                    <!-- 다중 선택 안내 영역 -->
                    <div id="multiInspectorView" class="h-100 d-none flex-column justify-content-center p-3">
                        <div class="inspector-multi-info text-center py-4">
                            <i class="fa-solid fa-layer-group fa-3x text-primary mb-3"></i>
                            <h6 class="fw-bold text-dark" id="multiSelectTitle">0개 항목 선택됨</h6>
                            <p class="small text-muted mb-4" id="multiSelectDesc">선택한 항목들을 마우스로 잡고 좌측 폴더로 드래그하면 한 번에 이동할 수 있습니다.</p>
                            <div class="d-flex flex-column gap-2">
                                <button type="button" class="btn btn-outline-warning btn-sm" id="multiBtnProtect" onclick="bulkToggleProtection()">
                                    <i class="fa-solid fa-shield-halved me-1"></i>선택한 이미지 일괄 보호/해제
                                </button>
                                <button type="button" class="btn btn-outline-danger btn-sm" id="multiBtnDelete" onclick="deleteMultipleImages()">
                                    <i class="fa-solid fa-trash me-1"></i>선택한 이미지 일괄 삭제
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- 3. 하단 상태바 -->
        <div class="win-statusbar">
            <div class="d-flex align-items-center gap-2">
                <span id="statTotalText">준비 중...</span>
                <span class="badge bg-primary-subtle text-primary border border-primary-subtle d-none" id="statSelectedBadge">0개 선택됨</span>
            </div>
            <div>
                <span class="me-3" id="statSelectedFolder">폴더: 전체 이미지</span>
                <span id="statLastSyncTime">준비 완료</span>
            </div>
        </div>

        <!-- 다중 드래그 고스트 카운터 -->
        <div id="dragGhostBadge" class="win-drag-ghost-badge">
            <i class="fa-solid fa-clone"></i>
            <span id="dragGhostCount">1</span>개 항목 이동
        </div>
    </div>
    @endsection

    @push('scripts')
        <script src="{{ asset('js/admin/admin_image.js') }}?v={{ time() }}"></script>
    @endpush