/**
 * dashboard.js
 * 통합된 대시보드 관리 스크립트
 * (script.js + index.js + index_custom.js 통합)
 */

document.addEventListener('DOMContentLoaded', () => {
    // ─────────────────────────────────────────────────────────────
    // 1. 공통 유틸리티 & 설정
    // ─────────────────────────────────────────────────────────────
    const csrfToken = document.querySelector('input[name="csrf_token"]')?.value || '';

    // 진행바 업데이트 (전역 함수로 등록하여 다른 곳에서 호출 가능)
    window.updateProgress = function (val) {
        const bar = document.getElementById('progressBar');
        if (!bar) return;
        const v = Math.max(0, Math.min(100, Number(val) || 0));

        // 부모 컨테이너(섹션)가 숨겨져 있다면 표시
        const section = document.getElementById('progressSection');
        if (section && section.style.display === 'none') {
            section.style.display = 'block';
        }

        // (Legacy support) 부모 컨테이너가 숨겨져 있다면 표시
        const progressContainer = bar.closest('.progress');
        if (progressContainer && progressContainer.parentElement.classList.contains('d-none')) {
            progressContainer.parentElement.classList.remove('d-none');
        }

        bar.style.width = v + '%';
        bar.setAttribute('aria-valuenow', v);
        bar.innerHTML = `<span class="fw-semibold small">${v}%</span>`;
    };

    /**
     * 테이블 정렬 함수
     * @param {string} tableId - 테이블 ID
     * @param {number} colIdx - 정렬할 컬럼 인덱스 (0부터 시작)
     */
    window.sortTable = function (tableId, colIdx) {
        const table = document.getElementById(tableId);
        if (!table) return;
        const tbody = table.querySelector('tbody');
        const rows = Array.from(tbody.querySelectorAll('tr'));

        // 데이터가 없거나 1개 뿐이면 패스
        if (rows.length <= 1) return;
        if (rows[0].querySelector('td[colspan]')) return; // "파일이 없습니다" 행

        const th = table.querySelectorAll('th')[colIdx];
        let asc = !th.classList.contains('sorted-asc'); // 현재 상태의 반대로 토글

        // 모든 헤더 초기화
        table.querySelectorAll('th').forEach(h => {
            h.classList.remove('sorted-asc', 'sorted-desc');
            const icon = h.querySelector('i');
            if (icon) icon.className = 'bi bi-arrow-down-up small text-muted';
        });

        // 현재 헤더 상태 업데이트
        th.classList.add(asc ? 'sorted-asc' : 'sorted-desc');
        const icon = th.querySelector('i');
        if (icon) icon.className = asc ? 'bi bi-sort-down-alt text-primary' : 'bi bi-sort-up-alt text-primary';

        // 정렬
        rows.sort((rowA, rowB) => {
            const cellA = rowA.children[colIdx];
            const cellB = rowB.children[colIdx];

            // data-sort 속성이 있으면 그것을 우선 사용 (날짜/숫자 등)
            const valA = cellA.dataset.sort || cellA.textContent.trim();
            const valB = cellB.dataset.sort || cellB.textContent.trim();

            const isNum = !isNaN(parseFloat(valA)) && isFinite(valA) && !isNaN(parseFloat(valB)) && isFinite(valB);

            if (isNum) {
                return asc ? (parseFloat(valA) - parseFloat(valB)) : (parseFloat(valB) - parseFloat(valA));
            } else {
                return asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
            }
        });

        // 다시 추가 (재정렬)
        rows.forEach(row => tbody.appendChild(row));
    };

    /**
     * 테이블 필터링 함수
     * @param {string} tableId - 테이블 ID
     * @param {string} text - 검색어
     */
    window.filterTable = function (tableId, text) {
        const table = document.getElementById(tableId);
        if (!table) return 0;
        const tbody = table.querySelector('tbody');
        const rows = tbody.querySelectorAll('tr');
        const query = text.toLowerCase().trim();

        let visibleCount = 0;
        rows.forEach(row => {
            if (row.querySelector('td[colspan]')) return; // 메시지 행 제외

            // 첫 번째 셀(파일명)만 검색 대상이라고 가정하거나, 전체 텍스트 검색
            const rowText = row.textContent.toLowerCase();
            if (rowText.includes(query)) {
                row.style.display = '';
                visibleCount++;
            } else {
                row.style.display = 'none';
            }
        });

        // 검색 결과 없음 처리 (Optional)
        return visibleCount;
    };

    // 줄 수 카운터 (script.js에서 가져옴)
    function updateLineCount(textareaId, badgeId) {
        const textarea = document.getElementById(textareaId);
        const badge = document.getElementById(badgeId);
        if (!textarea || !badge) return;

        const updateCount = () => {
            const text = textarea.value.trim();
            if (text === '') {
                badge.textContent = '0';
                return;
            }
            // 빈 줄 제외하고 카운트
            const lines = text.split('\n').filter(line => line.trim().length > 0);
            badge.textContent = lines.length; // UI 간소화를 위해 '줄' 텍스트 제외하거나 포함 가능
        };

        updateCount();
        ['input', 'change', 'keyup', 'paste'].forEach(evt => {
            textarea.addEventListener(evt, () => setTimeout(updateCount, 10));
        });
    }

    // 초기화
    updateLineCount('ips', 'ipLineCount');
    updateLineCount('server_list_content', 'serverLineCount');

    // 알림 자동 닫기
    setTimeout(() => {
        document.querySelectorAll('.alert').forEach(alert => {
            const bsAlert = new bootstrap.Alert(alert);
            bsAlert.close();
        });
    }, 5000);


    // ─────────────────────────────────────────────────────────────
    // 2. IP 처리 및 스캔 로직
    // ─────────────────────────────────────────────────────────────

    // 2-1. 스크립트 선택 시 XML 드롭다운 토글
    const TARGET_SCRIPT = "02-set_config.py";
    const scriptSelect = document.getElementById('script');
    const xmlGroup = document.getElementById('xmlFileGroup');

    function toggleXml() {
        if (!scriptSelect || !xmlGroup) return;
        if (scriptSelect.value === TARGET_SCRIPT) {
            xmlGroup.style.display = 'block';
            xmlGroup.classList.add('fade-in');
        } else {
            xmlGroup.style.display = 'none';
        }
    }

    if (scriptSelect) {
        // TomSelect 적용 전/후 모두 대응하기 위해 이벤트 리스너 등록
        toggleXml();
        scriptSelect.addEventListener('change', toggleXml);

        // TomSelect 초기화
        new TomSelect("#script", {
            create: false,
            sortField: { field: "text", direction: "asc" },
            placeholder: "스크립트를 검색하거나 선택하세요...",
            plugins: ['clear_button'],
            allowEmptyOption: true,
            onChange: toggleXml // TomSelect 변경 시에도 호출
        });
    }

    // 2-2. IP 입력 데이터 보존 (Local Storage)
    const ipTextarea = document.getElementById('ips');
    const STORAGE_KEY_IP = 'ip_input_draft';

    if (ipTextarea) {
        const savedIps = localStorage.getItem(STORAGE_KEY_IP);
        if (savedIps) {
            ipTextarea.value = savedIps;
            // 강제 이벤트 트리거하여 줄 수 카운트 업데이트
            ipTextarea.dispatchEvent(new Event('input'));
        }

        ipTextarea.addEventListener('input', () => {
            localStorage.setItem(STORAGE_KEY_IP, ipTextarea.value);
        });
    }

    // 2-3. IP 스캔 (Modal / AJAX)
    const btnScan = document.getElementById('btnStartScan');
    if (btnScan) {
        btnScan.addEventListener('click', async () => {
            const startIp = '10.10.0.2';
            const endIp = '10.10.0.255';
            const ipsTextarea = document.getElementById('ips');
            const progressBar = document.getElementById('progressBar');

            // UI 잠금 및 로딩 표시
            const originalIcon = btnScan.innerHTML;
            btnScan.disabled = true;
            btnScan.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';

            if (progressBar) {
                // 진행바 표시 및 초기화
                const progressContainer = progressBar.closest('.progress');
                if (progressContainer && progressContainer.parentElement) {
                    // 이미 존재하는 row 등을 찾아서 보여주기 (필요시)
                }

                window.updateProgress(100); // 스캔 중임을 알리기 위해 꽉 채움 (애니메이션)
                progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
                progressBar.textContent = 'IP 스캔 중...';
            }

            try {
                const res = await fetch('/utils/scan_network', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
                    body: JSON.stringify({ start_ip: startIp, end_ip: endIp })
                });

                if (res.redirected) {
                    alert('세션이 만료되었습니다. 다시 로그인해주세요.');
                    window.location.reload();
                    return;
                }

                const contentType = res.headers.get("content-type");
                if (!contentType || !contentType.includes("application/json")) {
                    const text = await res.text();
                    throw new Error(`서버 응답 오류: ${text.substring(0, 100)}...`);
                }

                const data = await res.json();

                if (data.success) {
                    if (data.active_ips && data.active_ips.length > 0) {
                        ipsTextarea.value = data.active_ips.join('\n');
                        ipsTextarea.dispatchEvent(new Event('input')); // 저장 및 카운트 갱신
                        const btnCopy = document.getElementById('btnCopyIps');
                        if (btnCopy) btnCopy.disabled = false;
                        alert(`스캔 완료: ${data.active_ips.length}개의 활성 IP를 찾았습니다.`);
                    } else {
                        alert('활성 IP를 발견하지 못했습니다.');
                    }
                } else {
                    throw new Error(data.error || 'Unknown error');
                }
            } catch (err) {
                console.error(err);
                alert('오류가 발생했습니다: ' + (err.message || err));
            } finally {
                // 복구
                btnScan.disabled = false;
                btnScan.innerHTML = originalIcon;
                if (progressBar) {
                    window.updateProgress(0);
                    progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');

                    const section = document.getElementById('progressSection');
                    if (section) section.style.display = 'none';
                }
            }
        });
    }

    // 2-4. IP 복사 버튼
    const btnCopyIps = document.getElementById('btnCopyIps');
    if (btnCopyIps) {
        btnCopyIps.addEventListener('click', async () => {
            const ipsTextarea = document.getElementById('ips');
            if (!ipsTextarea || !ipsTextarea.value.trim()) return;
            try {
                await navigator.clipboard.writeText(ipsTextarea.value.trim());
                const original = btnCopyIps.innerHTML;
                btnCopyIps.innerHTML = '<i class="bi bi-clipboard-check me-1"></i>복사됨';
                btnCopyIps.classList.replace('btn-outline-secondary', 'btn-outline-success');
                setTimeout(() => {
                    btnCopyIps.innerHTML = original;
                    btnCopyIps.classList.replace('btn-outline-success', 'btn-outline-secondary');
                }, 1500);
            } catch {
                ipsTextarea.select();
                document.execCommand('copy');
            }
        });
    }

    // 2-5. IP 지우기 버튼
    const btnClear = document.getElementById('btnClearIps');
    if (btnClear) {
        btnClear.addEventListener('click', () => {
            const ipsTextarea = document.getElementById('ips');
            if (ipsTextarea && confirm('입력된 IP 목록을 모두 지우시겠습니까?')) {
                ipsTextarea.value = '';
                ipsTextarea.dispatchEvent(new Event('input'));
                const btnCopy = document.getElementById('btnCopyIps');
                if (btnCopy) btnCopy.disabled = true;
            }
        });
    }

    // 2-5. 메인 IP 폼 제출 및 진행률 폴링 (script.js 로직)
    const ipForm = document.getElementById("ipForm");
    if (ipForm) {
        ipForm.addEventListener("submit", async (ev) => {
            ev.preventDefault();

            const formData = new FormData(ipForm);

            // [NEW] GUID 작업 시 로컬 스토리지의 슬롯 우선순위 추가
            const jobType = formData.get('job_type');
            if (jobType === 'guid') {
                const savedPriority = localStorage.getItem('guidSlotNumbers');
                if (savedPriority) {
                    formData.append('slot_priority', savedPriority);
                }
            }
            const btn = ipForm.querySelector('button[type="submit"]');
            const originalHtml = btn.innerHTML;
            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';

            // 진행바 초기화
            window.updateProgress(0);

            try {
                const res = await fetch(ipForm.action, {
                    method: "POST",
                    body: formData
                });

                if (!res.ok) throw new Error("HTTP " + res.status);

                const data = await res.json();

                if (data.job_id) {
                    // 비동기 작업 시작됨 -> 폴링 시작
                    pollProgress(data.job_id);
                } else {
                    // 동기 처리 완료
                    window.updateProgress(100);
                    setTimeout(() => location.reload(), 1000);
                }
            } catch (err) {
                console.error("처리 중 오류:", err);
                alert("처리 중 오류 발생: " + err.message);
                btn.disabled = false;
                btn.innerHTML = originalHtml;
            }
        });
    }

    // 폴링 함수
    function pollProgress(jobId) {
        const interval = setInterval(async () => {
            try {
                const res = await fetch(`/progress_status/${jobId}`);
                if (!res.ok) {
                    clearInterval(interval);
                    return;
                }
                const data = await res.json();

                if (data.progress !== undefined) {
                    window.updateProgress(data.progress);
                }

                if (data.progress >= 100) {
                    clearInterval(interval);
                    window.updateProgress(100);
                    setTimeout(() => location.reload(), 1500);
                }
            } catch (err) {
                console.error('진행률 확인 중 오류:', err);
                clearInterval(interval);
            }
        }, 500);
    }


    // ─────────────────────────────────────────────────────────────
    // 3. 파일 관련 기능 (백업 이동, 파일 보기 등)
    // ─────────────────────────────────────────────────────────────

    // 3-1. 파일 보기 모달 (index.js)
    const modalEl = document.getElementById('fileViewModal');
    const titleEl = document.getElementById('fileViewModalLabel');
    const contentEl = document.getElementById('fileViewContent');

    if (modalEl) {
        modalEl.addEventListener('show.bs.modal', async (ev) => {
            const btn = ev.relatedTarget;
            const folder = btn?.getAttribute('data-folder') || '';
            const date = btn?.getAttribute('data-date') || '';
            const filename = btn?.getAttribute('data-filename') || '';

            titleEl.innerHTML = `<i class="bi bi-file-text me-2"></i>${filename || '파일'}`;
            contentEl.textContent = '불러오는 중...';

            const params = new URLSearchParams();
            if (folder) params.set('folder', folder);
            if (date) params.set('date', date);
            if (filename) params.set('filename', filename);

            try {
                const res = await fetch(`/view_file?${params.toString()}`, { cache: 'no-store' });
                if (!res.ok) throw new Error('HTTP ' + res.status);
                const data = await res.json();
                contentEl.textContent = data?.content ?? '(빈 파일)';
            } catch (e) {
                contentEl.textContent = '파일을 불러오지 못했습니다: ' + (e?.message || e);
            }
        });
    }

    // 3-2. 백업 파일 드래그 앤 드롭 이동 (index_custom.js)
    let selectedItems = new Set();
    const backupContainers = document.querySelectorAll('.backup-files-container');

    // 다중 선택 처리
    document.addEventListener('click', function (e) {
        const item = e.target.closest('.backup-file-item');
        if (item && !e.target.closest('a') && !e.target.closest('button')) {
            if (e.ctrlKey || e.metaKey) {
                toggleSelection(item);
            } else {
                const wasSelected = item.classList.contains('selected');
                clearSelection();
                if (!wasSelected) toggleSelection(item);
            }
        } else if (!e.target.closest('.backup-files-container')) {
            // 배경 클릭 시 선택 해제? (UX에 따라 결정, 여기선 일단 패스)
        }
    });

    // 빈 공간 클릭 시 선택 해제
    document.addEventListener('mousedown', function (e) {
        if (!e.target.closest('.backup-file-item') && !e.target.closest('.backup-files-container')) {
            clearSelection();
        }
    });

    function toggleSelection(item) {
        if (item.classList.contains('selected')) {
            item.classList.remove('selected');
            selectedItems.delete(item);
        } else {
            item.classList.add('selected');
            selectedItems.add(item);
        }
    }

    function clearSelection() {
        document.querySelectorAll('.backup-file-item.selected').forEach(el => el.classList.remove('selected'));
        selectedItems.clear();
    }

    function updateFolderCount(folderDate) {
        const container = document.querySelector(`.backup-files-container[data-folder="${folderDate}"]`);
        if (container) {
            const accordionItem = container.closest('.accordion-item');
            if (accordionItem) {
                const badge = accordionItem.querySelector('.accordion-button .badge');
                if (badge) {
                    const visibleRows = Array.from(container.children).filter(row => row.matches('.backup-file-item')).length;
                    badge.textContent = `${visibleRows} 파일`;
                }
            }
        }
    }

    // Sortable 초기화
    backupContainers.forEach(container => {
        if (typeof Sortable === 'undefined') return;

        new Sortable(container, {
            group: 'backup-files',
            animation: 150,
            ghostClass: 'sortable-ghost',
            dragClass: 'sortable-drag',
            handle: '.backup-drag-handle',
            delay: 100,
            delayOnTouchOnly: true,
            onStart: function (evt) {
                if (!evt.item.classList.contains('selected')) {
                    clearSelection();
                    toggleSelection(evt.item);
                }
            },
            onEnd: function (evt) {
                if (evt.to === evt.from) return;

                const sourceFolder = evt.from.getAttribute('data-folder');
                const targetFolder = evt.to.getAttribute('data-folder');

                if (!sourceFolder || !targetFolder) {
                    alert('잘못된 이동 요청입니다.');
                    location.reload();
                    return;
                }

                let itemsToMove = Array.from(selectedItems).filter(item => {
                    return item.closest('.backup-files-container')?.getAttribute('data-folder') === sourceFolder;
                });
                if (itemsToMove.length === 0) itemsToMove = [evt.item];
                else if (!itemsToMove.includes(evt.item)) itemsToMove.push(evt.item);

                // UI 상 이동 처리 (Sortable이 하나는 처리해주지만 다중 선택은 직접 옮겨야 함)
                itemsToMove.forEach(item => {
                    if (item !== evt.item) evt.to.appendChild(item);
                });

                // 서버 요청
                if (!window.APP_CONFIG) {
                    console.error("Window APP_CONFIG not found!");
                    return;
                }

                const promises = itemsToMove.map(item => {
                    const filename = item.getAttribute('data-filename');
                    if (!filename) return Promise.resolve();

                    return fetch(window.APP_CONFIG.moveBackupUrl, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-CSRFToken': window.APP_CONFIG.csrfToken
                        },
                        body: JSON.stringify({
                            filename: filename,
                            source_folder: sourceFolder,
                            target_folder: targetFolder
                        })
                    })
                        .then(r => r.json())
                        .then(data => {
                            if (data.success) {
                                // 속성 업데이트 (다운로드 링크 등)
                                const btn = item.querySelector('.btn-view-backup');
                                if (btn) btn.setAttribute('data-date', targetFolder);

                                const link = item.querySelector('a[download]');
                                if (link && window.APP_CONFIG.downloadBaseUrl) {
                                    const newHref = window.APP_CONFIG.downloadBaseUrl
                                        .replace('PLACEHOLDER_DATE', targetFolder)
                                        .replace('PLACEHOLDER_FILE', filename);
                                    link.setAttribute('href', newHref);
                                }
                            }
                            return data;
                        });
                });

                Promise.all(promises).then(results => {
                    updateFolderCount(sourceFolder);
                    updateFolderCount(targetFolder);
                    clearSelection();

                    const failed = results.filter(r => r && !r.success);
                    if (failed.length > 0) {
                        alert(failed.length + '개의 파일 이동 실패. 새로고침이 필요합니다.');
                        location.reload();
                    }
                }).catch(err => {
                    console.error(err);
                    alert('이동 중 통신 오류 발생');
                });
            }
        });
    });


    // ─────────────────────────────────────────────────────────────
    // 4. 슬롯 우선순위 설정 모달 (index_custom.js)
    // ─────────────────────────────────────────────────────────────
    const slotPriorityModal = document.getElementById('slotPriorityModal');
    if (slotPriorityModal) {
        const slotNumbersInput = document.getElementById('slotNumbersInput');
        const slotCountDisplay = document.getElementById('slotCountDisplay');
        const slotPreview = document.getElementById('slotPreview');
        const slotPriorityInput = document.getElementById('slot_priority_input');
        const modalServerListContent = document.getElementById('modal_server_list_content');
        const serverListTextarea = document.getElementById('server_list_content');
        const slotPriorityForm = document.getElementById('slotPriorityForm');
        const btnClearSlots = document.getElementById('btnClearSlots');
        const presetCountInput = document.getElementById('presetCountInput');
        const btnApplyPreset = document.getElementById('btnApplyPreset');

        const defaultPriority = ['38', '39', '37', '36', '32', '33', '34', '35', '31', '40'];

        function loadSlots() {
            const saved = localStorage.getItem('guidSlotNumbers');
            slotNumbersInput.value = saved ? saved : defaultPriority.join(', ');
            if (presetCountInput) presetCountInput.value = 10;
            updateSlotPreview();
        }

        function saveSlots() {
            localStorage.setItem('guidSlotNumbers', slotNumbersInput.value);
        }

        function parseSlots(input) {
            if (!input || !input.trim()) return [];
            return input.split(/[,\s\n]+/)
                .map(s => s.trim())
                .filter(s => s !== '' && /^\d+$/.test(s))
                .filter((v, i, a) => a.indexOf(v) === i); // Unique
        }

        function updateSlotPreview() {
            const slots = parseSlots(slotNumbersInput.value);
            const count = slots.length;

            slotCountDisplay.textContent = `${count}개`;
            slotCountDisplay.className = count > 0
                ? 'badge bg-primary text-white border border-primary rounded-pill px-3 py-1'
                : 'badge bg-white text-dark border rounded-pill px-3 py-1';

            if (count === 0) {
                slotPreview.style.display = 'flex';
                slotPreview.innerHTML = `
                    <div class="d-flex flex-column align-items-center justify-content-center w-100 h-100 text-muted opacity-50">
                        <i class="bi bi-layers fs-1 mb-2"></i>
                        <span class="small">프리셋을 선택하거나 번호를 입력하세요.</span>
                    </div>`;
            } else {
                slotPreview.style.display = 'grid';
                slotPreview.innerHTML = slots.map((slot, index) => `
                    <div class="slot-badge animate__animated animate__fadeIn" data-slot="${slot}" style="animation-delay: ${index * 0.02}s">
                        <span class="slot-index">${index + 1}</span>
                        <div>Slot ${slot}</div>
                    </div>
                `).join('');

                // 미리보기 Sortable
                new Sortable(slotPreview, {
                    animation: 150,
                    ghostClass: 'sortable-ghost',
                    dragClass: 'sortable-drag',
                    onEnd: function () {
                        const items = slotPreview.querySelectorAll('.slot-badge');
                        const newSlots = Array.from(items).map(item => item.getAttribute('data-slot'));
                        slotNumbersInput.value = newSlots.join(', ');
                        items.forEach((item, index) => {
                            const idx = item.querySelector('.slot-index');
                            if (idx) idx.textContent = index + 1;
                        });
                        saveSlots();
                    }
                });
            }
            saveSlots();
        }

        if (btnApplyPreset) {
            btnApplyPreset.addEventListener('click', () => {
                let count = parseInt(presetCountInput.value) || 10;
                count = Math.max(1, Math.min(10, count));
                presetCountInput.value = count;
                slotNumbersInput.value = defaultPriority.slice(0, count).join(', ');
                updateSlotPreview();
            });
        }

        slotNumbersInput.addEventListener('input', updateSlotPreview);

        btnClearSlots.addEventListener('click', () => {
            if (confirm('모두 지우시겠습니까?')) {
                slotNumbersInput.value = '';
                updateSlotPreview();
            }
        });

        slotPriorityModal.addEventListener('show.bs.modal', () => {
            if (serverListTextarea) modalServerListContent.value = serverListTextarea.value;
            loadSlots();
        });

        slotPriorityForm.addEventListener('submit', (e) => {
            const slots = parseSlots(slotNumbersInput.value);
            if (slots.length === 0) {
                e.preventDefault();
                alert('최소 1개 이상의 슬롯을 입력하세요.');
                return;
            }
            slotPriorityInput.value = slots.join(',');
            saveSlots();
        });
    }

    // ─────────────────────────────────────────────────────────────
    // 5. Quick Move (중복체크 포함) (index_custom.js)
    // ─────────────────────────────────────────────────────────────
    const quickMoveForms = ['macMoveForm', 'guidMoveForm', 'gpuMoveForm'];
    let pendingAction = null;

    const dupModalEl = document.getElementById('duplicateCheckModal');
    const dupModal = dupModalEl ? new bootstrap.Modal(dupModalEl) : null;
    const btnConfirmOverwrite = document.getElementById('btnConfirmOverwrite');

    if (btnConfirmOverwrite) {
        btnConfirmOverwrite.addEventListener('click', () => {
            if (pendingAction) {
                dupModal.hide();
                pendingAction(true); // overwrite=true
                pendingAction = null;
            }
        });
    }

    quickMoveForms.forEach(id => {
        const form = document.getElementById(id);
        if (form) {
            form.addEventListener('submit', (e) => {
                e.preventDefault();
                const btn = form.querySelector('button[type="submit"]');
                if (!btn || btn.disabled) return;

                const originalContent = btn.innerHTML;

                const executeMove = (overwrite = false) => {
                    btn.classList.add('disabled');
                    btn.disabled = true;
                    btn.innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"></div>';

                    fetch(form.action, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-CSRFToken': csrfToken // 전역 csrfToken 사용
                        },
                        body: JSON.stringify({ overwrite: overwrite })
                    })
                        .then(async response => {
                            const ct = response.headers.get("content-type");
                            if (!ct || !ct.includes("application/json")) {
                                // HTTP 200이지만 HTML이 올 경우 에러나 마찬가지 (로그인 리다이렉트 등)
                                if (response.ok && response.url.includes("login")) {
                                    window.location.reload();
                                    return;
                                }
                                // "MAC 이동 중 오류: HTTP 200" 등 텍스트일 수 있음
                                const txt = await response.text();
                                if (response.ok) {
                                    // 성공으로 간주하고 리로드 (가끔 백엔드가 JSON 대신 빈 성공 응답을 줄 때)
                                    window.location.reload();
                                    return;
                                }
                                throw new Error(txt);
                            }
                            return response.json();
                        })
                        .then(data => {
                            if (!data) return; // 위에서 처리됨
                            if (data.requires_confirmation) {
                                showDuplicateModal(data.duplicates, data.duplicate_count);
                                pendingAction = executeMove;
                                resetButton();
                            } else if (data.success) {
                                window.location.reload();
                            } else {
                                alert(data.error || '작업 실패');
                                resetButton();
                            }
                        })
                        .catch(err => {
                            console.error(err);
                            // HTTP 200 에러 억제 요청 반영
                            if (err.message && err.message.includes("200")) {
                                window.location.reload();
                                return;
                            }
                            alert('오류 발생: ' + err);
                            resetButton();
                        });
                };

                const resetButton = () => {
                    btn.classList.remove('disabled');
                    btn.disabled = false;
                    btn.innerHTML = originalContent;
                };

                executeMove(false);
            });
        }
    });

    function showDuplicateModal(duplicates, count) {
        const listEl = document.getElementById('dupList');
        const countEl = document.getElementById('dupCount');
        const moreEl = document.getElementById('dupMore');
        const moreCountEl = document.getElementById('dupMoreCount');

        if (countEl) countEl.textContent = count;
        if (listEl) {
            listEl.innerHTML = '';
            const limit = 10;
            duplicates.slice(0, limit).forEach(name => {
                const li = document.createElement('li');
                li.innerHTML = `<i class="bi bi-file-earmark text-secondary me-2"></i>${name}`;
                listEl.appendChild(li);
            });

            if (duplicates.length > limit) {
                if (moreEl) {
                    moreEl.style.display = 'block';
                    moreCountEl.textContent = duplicates.length - limit;
                }
            } else {
                if (moreEl) moreEl.style.display = 'none';
            }
        }
        if (dupModal) dupModal.show();
    }

    // ─────────────────────────────────────────────────────────────
    // 6. 작업 유형 및 프로파일 처리 (process_ips 관련)
    // ─────────────────────────────────────────────────────────────
    const jobTypeSelect = document.getElementById('job_type');
    const profileSelect = document.getElementById('profile');
    const profileGroup = document.getElementById('profileGroup');
    const profileDesc = document.getElementById('profileDesc');
    const legacyScriptGroup = document.getElementById('legacyScriptGroup');
    const scriptSelectLegacy = document.getElementById('script'); // 기존 스크립트 select

    if (jobTypeSelect) {
        jobTypeSelect.addEventListener('change', async () => {
            const jobType = jobTypeSelect.value;

            // 초기화
            if (profileSelect) {
                profileSelect.innerHTML = '<option value="">프로파일을 불러오는 중...</option>';
                profileDesc.textContent = '';
            }

            if (jobType === 'legacy') {
                // Legacy 모드
                if (profileGroup) profileGroup.style.display = 'none';
                if (legacyScriptGroup) legacyScriptGroup.style.display = 'block';
                if (scriptSelectLegacy) scriptSelectLegacy.required = true;
                if (profileSelect) profileSelect.required = false;
            } else if (['mac', 'server_info', 'guid'].includes(jobType)) {
                // 신규 모드
                if (legacyScriptGroup) legacyScriptGroup.style.display = 'none';
                if (profileGroup) profileGroup.style.display = 'block';
                if (scriptSelectLegacy) scriptSelectLegacy.required = false;
                if (profileSelect) profileSelect.required = true;

                // 프로파일 로드
                try {
                    const res = await fetch(`/api/profiles/${jobType}`);
                    if (!res.ok) throw new Error('프로파일 로드 실패');
                    const profiles = await res.json();

                    profileSelect.innerHTML = '<option value="">프로파일을 선택하세요</option>';
                    profiles.forEach(p => {
                        const opt = document.createElement('option');
                        opt.value = p.name;
                        opt.textContent = `${p.name} ${p.is_default ? '(기본)' : ''}`;
                        opt.dataset.desc = p.description;
                        profileSelect.appendChild(opt);
                    });

                } catch (err) {
                    console.error(err);
                    profileSelect.innerHTML = '<option value="">오류 발생</option>';
                }
            } else {
                // 선택 안함
                if (legacyScriptGroup) legacyScriptGroup.style.display = 'none';
                if (profileGroup) profileGroup.style.display = 'none';
            }
        });

        // 프로파일 설명 표시
        if (profileSelect) {
            profileSelect.addEventListener('change', () => {
                const selected = profileSelect.options[profileSelect.selectedIndex];
                if (selected && selected.dataset.desc) {
                    profileDesc.textContent = selected.dataset.desc;
                } else {
                    profileDesc.textContent = '';
                }
            });
        }
    }

    // ─────────────────────────────────────────────────────────────
    // 7. 탭 활성화 동기화 (외부 탭 활성화 시 내부 서브탭도 활성화)
    // ─────────────────────────────────────────────────────────────
    const repositoryTab = document.getElementById('repository-tab');
    if (repositoryTab) {
        repositoryTab.addEventListener('shown.bs.tab', function () {
            // repository 탭이 활성화되면 첫 번째 서브탭(pills-backup)도 활성화
            const firstPillTab = document.getElementById('pills-backup-tab');
            const firstPillPane = document.getElementById('pills-backup');

            if (firstPillTab && firstPillPane) {
                // 버튼 활성화
                document.querySelectorAll('#repoTabs .nav-link').forEach(btn => {
                    btn.classList.remove('active');
                    btn.setAttribute('aria-selected', 'false');
                });
                firstPillTab.classList.add('active');
                firstPillTab.setAttribute('aria-selected', 'true');

                // 패널 활성화
            }
        });
    }

    // ─────────────────────────────────────────────────────────────
    // 7. 커스텀 탭 전환 로직
    // ─────────────────────────────────────────────────────────────
    const globalFileSearch = document.getElementById('globalFileSearch');
    const repoTabs = document.getElementById('repoTabs');
    const activeFileModeBadge = document.getElementById('activeFileModeBadge');
    const fileToolbarHint = document.getElementById('fileToolbarHint');
    const fileSearchScopeText = document.getElementById('fileSearchScopeText');
    const clearFileSearchButton = document.getElementById('btnClearFileSearch');
    const kpiButtons = document.querySelectorAll('.file-kpi-button');

    function filterBackupAccordion(text) {
        const accordion = document.getElementById('backupAccordion');
        const backupSearchEmpty = document.getElementById('backupSearchEmpty');
        if (!accordion) return 0;

        const query = text.toLowerCase().trim();
        const items = accordion.querySelectorAll('.accordion-item');
        let visibleSections = 0;

        items.forEach(item => {
            const headerText = item.querySelector('.accordion-button')?.textContent.toLowerCase() || '';
            const rows = item.querySelectorAll('.backup-file-item');
            const headerMatched = query === '' || headerText.includes(query);
            let visibleRows = 0;

            rows.forEach(row => {
                const rowMatched = headerMatched || row.textContent.toLowerCase().includes(query);
                row.classList.toggle('match-hidden', !rowMatched);
                row.style.display = rowMatched ? '' : 'none';
                if (rowMatched) visibleRows++;
            });

            const isVisible = query === '' || headerMatched || visibleRows > 0;
            item.style.display = isVisible ? '' : 'none';
            if (isVisible) visibleSections++;
        });

        if (backupSearchEmpty) {
            backupSearchEmpty.classList.toggle('show', query !== '' && visibleSections === 0);
        }

        return visibleSections;
    }

    function getActiveSearchContext() {
        const repositoryTab = document.getElementById('repository-tab');
        const isRepositoryMode = repositoryTab?.classList.contains('active');

        if (!isRepositoryMode) {
            return {
                label: '처리된 파일',
                hint: '현재 스테이징 폴더에서 처리된 결과 파일을 확인합니다.',
                placeholder: '처리된 파일명을 검색하세요',
                scopeText: '처리된 파일',
                tableId: 'tableProcessed'
            };
        }

        const activeSubTab = document.querySelector('#repoTabs .nav-link.active');
        const activePanelId = activeSubTab?.getAttribute('data-bs-target');

        switch (activePanelId) {
            case '#subpanel-mac':
                return {
                    label: 'MAC',
                    hint: '정리된 MAC 파일 저장소를 검색합니다.',
                    placeholder: 'MAC 파일명을 검색하세요',
                    scopeText: 'MAC 저장소',
                    tableId: 'tableMac',
                    repo: 'mac'
                };
            case '#subpanel-guid':
                return {
                    label: 'GUID',
                    hint: '정리된 GUID 파일 저장소를 검색합니다.',
                    placeholder: 'GUID 파일명을 검색하세요',
                    scopeText: 'GUID 저장소',
                    tableId: 'tableGuid',
                    repo: 'guid'
                };
            case '#subpanel-gpu':
                return {
                    label: 'GPU',
                    hint: '정리된 GPU 파일 저장소를 검색합니다.',
                    placeholder: 'GPU 파일명을 검색하세요',
                    scopeText: 'GPU 저장소',
                    tableId: 'tableGpu',
                    repo: 'gpu'
                };
            default:
                return {
                    label: '서버 정보',
                    hint: '백업 폴더명과 내부 파일명을 함께 검색합니다.',
                    placeholder: '백업 폴더명 또는 서버 정보 파일명을 검색하세요',
                    scopeText: '서버 정보 백업',
                    backup: true
                };
        }
    }

    function applyGlobalFileSearch() {
        if (!globalFileSearch) return;

        const query = globalFileSearch.value || '';
        const context = getActiveSearchContext();

        if (context.backup) {
            filterBackupAccordion(query);
            return;
        }

        if (context.tableId) {
            window.filterTable(context.tableId, query);
            if (context.repo) {
                updateRepoSelectionState(context.repo);
            }
        }
    }

    function updateFileBrowserState() {
        const context = getActiveSearchContext();
        const activePanelId = document.querySelector('#repoTabs .nav-link.active')?.getAttribute('data-bs-target');

        if (repoTabs) {
            repoTabs.classList.toggle('d-none', context.tableId === 'tableProcessed');
        }

        if (activeFileModeBadge) {
            activeFileModeBadge.textContent = context.label;
        }

        if (fileToolbarHint) {
            fileToolbarHint.textContent = context.hint;
        }

        if (globalFileSearch) {
            globalFileSearch.placeholder = context.placeholder;
        }

        if (fileSearchScopeText) {
            fileSearchScopeText.textContent = context.scopeText || context.label;
        }

        kpiButtons.forEach(button => {
            const kpiTarget = button.getAttribute('data-kpi-target');
            const isActive = activePanelId === `#${kpiTarget?.replace('-tab', '')}` && context.tableId !== 'tableProcessed';
            button.classList.toggle('is-active', Boolean(isActive));
        });

        applyGlobalFileSearch();
    }

    if (globalFileSearch) {
        globalFileSearch.addEventListener('input', applyGlobalFileSearch);
    }

    if (clearFileSearchButton && globalFileSearch) {
        clearFileSearchButton.addEventListener('click', () => {
            globalFileSearch.value = '';
            applyGlobalFileSearch();
            globalFileSearch.focus();
        });
    }

    if (kpiButtons.length) {
        kpiButtons.forEach(button => {
            button.addEventListener('click', () => {
                const repositoryTab = document.getElementById('repository-tab');
                const targetTabId = button.getAttribute('data-kpi-target');
                const targetTab = targetTabId ? document.getElementById(targetTabId) : null;

                if (repositoryTab && !repositoryTab.classList.contains('active')) {
                    repositoryTab.click();
                }

                if (targetTab) {
                    targetTab.click();
                }
            });
        });
    }

    // 메인 탭 전환
    const mainTabButtons = document.querySelectorAll('#fileTabs button[data-custom-toggle="tab"]');
    mainTabButtons.forEach(btn => {
        btn.addEventListener('click', function (e) {
            e.preventDefault();

            // 모든 탭 버튼 비활성화
            mainTabButtons.forEach(b => {
                b.classList.remove('active');
                b.setAttribute('aria-selected', 'false');
            });
            // 클릭한 탭 버튼 활성화
            this.classList.add('active');
            this.setAttribute('aria-selected', 'true');

            // 모든 탭 패널 숨기기
            document.querySelectorAll('.custom-tab-panel').forEach(panel => {
                panel.classList.remove('custom-active');
                panel.style.display = 'none'; // 인라인 스타일로 강제 숨김
            });

            // 대상 탭 패널 표시
            const targetId = this.getAttribute('data-bs-target');
            const targetPanel = document.querySelector(targetId);
            if (targetPanel) {
                targetPanel.classList.add('custom-active');
                targetPanel.style.display = 'block'; // 인라인 스타일도 변경
            }

            updateFileBrowserState();
        });
    });


    // 서브 탭 전환 (Pills)
    const subTabButtons = document.querySelectorAll('#repoTabs button[data-custom-toggle="pill"]');
    subTabButtons.forEach(btn => {
        btn.addEventListener('click', function (e) {
            e.preventDefault();

            // 모든 서브 탭 버튼 비활성화
            subTabButtons.forEach(b => {
                b.classList.remove('active');
                b.setAttribute('aria-selected', 'false');
            });
            this.classList.add('active');
            this.setAttribute('aria-selected', 'true');
            // 모든 서브 탭 패널 숨기기
            document.querySelectorAll('.custom-sub-panel').forEach(panel => {
                panel.classList.remove('custom-sub-active');
                panel.classList.add('d-none');
                panel.style.display = 'none';
            });

            // subpanel-backup을 명시적으로 한 번 더 숨김
            const backupPanel = document.getElementById('subpanel-backup');
            if (backupPanel) {
                backupPanel.classList.add('d-none');
                backupPanel.style.setProperty('display', 'none', 'important');
            }

            // 대상 서브 탭 패널 표시
            const targetId = this.getAttribute('data-bs-target');
            const targetPanel = document.querySelector(targetId);
            if (targetPanel) {
                targetPanel.classList.add('custom-sub-active');
                targetPanel.classList.remove('d-none');
                targetPanel.classList.remove('display-none'); // 혹시 모를 클래스 제거
                targetPanel.style.setProperty('display', 'block', 'important');
            }

            updateFileBrowserState();
        });
    });

    updateFileBrowserState();

    // ─────────────────────────────────────────────────────────────
    // 8. 저장소 파일 선택 작업 (MAC/GUID/GPU)
    // ─────────────────────────────────────────────────────────────
    const repoCheckboxes = document.querySelectorAll('.repo-file-checkbox');
    const repoSelectAllButtons = document.querySelectorAll('.repo-select-all');
    const repoSelectedActionButtons = document.querySelectorAll('.repo-selected-action');

    function getRepoCheckedFiles(repo) {
        return Array.from(document.querySelectorAll(`.repo-file-checkbox[data-repo="${repo}"]:checked`))
            .map(input => input.value)
            .filter(Boolean);
    }

    function updateRepoSelectionState(repo) {
        const checkedFiles = getRepoCheckedFiles(repo);
        repoSelectedActionButtons.forEach(button => {
            if (button.getAttribute('data-repo') === repo) {
                button.disabled = checkedFiles.length === 0;
            }
        });

        repoSelectAllButtons.forEach(selectAll => {
            if (selectAll.getAttribute('data-repo') !== repo) return;

            const tableId = selectAll.getAttribute('data-table-id');
            const table = tableId ? document.getElementById(tableId) : null;
            const checkboxes = table
                ? Array.from(table.querySelectorAll('.repo-file-checkbox'))
                : [];
            const visibleCheckboxes = checkboxes.filter(input => {
                const row = input.closest('tr');
                return row && row.style.display !== 'none';
            });
            const visibleChecked = visibleCheckboxes.filter(input => input.checked);

            selectAll.checked = visibleCheckboxes.length > 0 && visibleChecked.length === visibleCheckboxes.length;
            selectAll.indeterminate = visibleChecked.length > 0 && visibleChecked.length < visibleCheckboxes.length;
        });
    }

    function submitRepoSelection(actionUrl, files) {
        const form = document.createElement('form');
        form.method = 'POST';
        form.action = actionUrl;
        form.className = 'd-none';

        const csrfInput = document.createElement('input');
        csrfInput.type = 'hidden';
        csrfInput.name = 'csrf_token';
        csrfInput.value = window.APP_CONFIG?.csrfToken || '';
        form.appendChild(csrfInput);

        files.forEach(filename => {
            const input = document.createElement('input');
            input.type = 'hidden';
            input.name = 'files';
            input.value = filename;
            form.appendChild(input);
        });

        document.body.appendChild(form);
        form.submit();
    }

    repoCheckboxes.forEach(input => {
        input.addEventListener('change', () => {
            updateRepoSelectionState(input.getAttribute('data-repo'));
        });
    });

    repoSelectAllButtons.forEach(selectAll => {
        selectAll.addEventListener('change', () => {
            const table = document.getElementById(selectAll.getAttribute('data-table-id'));
            const repo = selectAll.getAttribute('data-repo');
            if (!table || !repo) return;

            table.querySelectorAll('.repo-file-checkbox').forEach(input => {
                const row = input.closest('tr');
                if (row && row.style.display !== 'none') {
                    input.checked = selectAll.checked;
                }
            });
            updateRepoSelectionState(repo);
        });
    });

    repoSelectedActionButtons.forEach(button => {
        button.addEventListener('click', () => {
            const repo = button.getAttribute('data-repo');
            const actionUrl = button.getAttribute('data-action');
            const files = getRepoCheckedFiles(repo);
            const confirmMessage = button.getAttribute('data-confirm');

            if (!repo || !actionUrl || files.length === 0) return;
            if (confirmMessage && !confirm(confirmMessage)) return;

            submitRepoSelection(actionUrl, files);
        });
    });

    ['mac', 'guid', 'gpu'].forEach(updateRepoSelectionState);

    // ─────────────────────────────────────────────────────────────
    // 9. 워크스페이스 업로드 중복 처리
    // ─────────────────────────────────────────────────────────────
    document.querySelectorAll('.workspace-file-upload-input').forEach(input => {
        input.addEventListener('change', () => {
            const form = input.form;
            if (!form || !input.files || input.files.length === 0) return;

            let existingFiles = [];
            try {
                existingFiles = JSON.parse(input.getAttribute('data-existing-files') || '[]');
            } catch (_) {
                existingFiles = [];
            }

            const existingSet = new Set(existingFiles.map(name => String(name).toLowerCase()));
            const duplicates = Array.from(input.files)
                .map(file => file.name)
                .filter(name => existingSet.has(String(name).toLowerCase()));

            const policyInput = form.querySelector('input[name="duplicate_policy"]');
            if (policyInput) {
                policyInput.value = 'rename';
            }

            if (duplicates.length > 0) {
                const preview = duplicates.slice(0, 5).join('\n');
                const extra = duplicates.length > 5 ? `\n...외 ${duplicates.length - 5}개` : '';
                const overwrite = confirm(
                    `동일한 이름의 파일이 이미 있습니다.\n\n${preview}${extra}\n\n확인: 덮어쓰기\n취소: 다른 이름으로 저장`
                );

                if (policyInput) {
                    policyInput.value = overwrite ? 'overwrite' : 'rename';
                }
            }

            form.submit();
        });
    });

    // ─────────────────────────────────────────────────────────────
    // 10. URL 파라미터 기반 탭 활성화 (페이지네이션 지원)
    // ─────────────────────────────────────────────────────────────
    const urlParams = new URLSearchParams(window.location.search);

    // 'backup_page' 파라미터가 있으면 '백업 및 저장소' -> '서버 정보' 탭 활성화
    if (urlParams.has('backup_page')) {
        const repoTabBtn = document.getElementById('repository-tab');
        const backupSubTabBtn = document.getElementById('subpanel-backup-tab');

        if (repoTabBtn) {
            // 메인 탭 전환 트리거
            repoTabBtn.click();
        }

        if (backupSubTabBtn) {
            // 서브 탭 전환 트리거 (메인 탭 전환 후 실행)
            setTimeout(() => {
                backupSubTabBtn.click();
            }, 50);
        }
    }

    const workspaceParam = urlParams.get('workspace');
    if (workspaceParam) {
        const repoTabBtn = document.getElementById('repository-tab');
        const targetSubTabId = {
            server_info: 'subpanel-backup-tab',
            mac: 'subpanel-mac-tab',
            guid: 'subpanel-guid-tab',
            gpu: 'subpanel-gpu-tab'
        }[workspaceParam];
        const targetSubTabBtn = targetSubTabId ? document.getElementById(targetSubTabId) : null;

        if (repoTabBtn) {
            repoTabBtn.click();
        }

        if (targetSubTabBtn) {
            setTimeout(() => {
                targetSubTabBtn.click();
            }, 50);
        }
    }
    // ─────────────────────────────────────────────────────────────
    // 9. 시스템 제어 패널 토글 아이콘 회전
    // ─────────────────────────────────────────────────────────────
    const systemControlPanel = document.getElementById('systemControlPanel');
    const systemControlHint = document.getElementById('systemControlHint');
    const fileToolsPanel = document.getElementById('fileToolsPanel');
    const fileToolsHint = document.getElementById('fileToolsHint');

    if (systemControlPanel) {
        systemControlPanel.addEventListener('show.bs.collapse', () => {
            const icon = document.querySelector('button[data-bs-target="#systemControlPanel"] i');
            if (icon) {
                icon.classList.remove('bi-chevron-down');
                icon.classList.add('bi-chevron-up');
            }
            if (systemControlHint) {
                systemControlHint.style.display = 'none';
            }
        });
        systemControlPanel.addEventListener('hide.bs.collapse', () => {
            const icon = document.querySelector('button[data-bs-target="#systemControlPanel"] i');
            if (icon) {
                icon.classList.remove('bi-chevron-up');
                icon.classList.add('bi-chevron-down');
            }
            if (systemControlHint) {
                systemControlHint.style.display = 'inline';
            }
        });

        // 힌트 클릭 시 트리거
        if (systemControlHint) {
            systemControlHint.addEventListener('click', () => {
                const btn = document.querySelector('button[data-bs-target="#systemControlPanel"]');
                if (btn) btn.click();
            });
        }
    }

    if (fileToolsPanel) {
        fileToolsPanel.addEventListener('show.bs.collapse', () => {
            const icon = document.querySelector('button[data-bs-target="#fileToolsPanel"] i');
            if (icon) {
                icon.classList.remove('bi-chevron-down');
                icon.classList.add('bi-chevron-up');
            }
            if (fileToolsHint) {
                fileToolsHint.style.display = 'none';
            }
        });
        fileToolsPanel.addEventListener('hide.bs.collapse', () => {
            const icon = document.querySelector('button[data-bs-target="#fileToolsPanel"] i');
            if (icon) {
                icon.classList.remove('bi-chevron-up');
                icon.classList.add('bi-chevron-down');
            }
            if (fileToolsHint) {
                fileToolsHint.style.display = 'inline';
            }
        });

        if (fileToolsHint) {
            fileToolsHint.addEventListener('click', () => {
                const btn = document.querySelector('button[data-bs-target="#fileToolsPanel"]');
                if (btn) btn.click();
            });
        }
    }

});
