/** * 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 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 = `${v}%`; // 100% 도달 시 애니메이션 효과 제어 등은 필요 시 추가 }; // 줄 수 카운터 (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 = ''; 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')); // 저장 및 카운트 갱신 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'); } } }); } // 2-4. 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')); } }); } // 2-5. 메인 IP 폼 제출 및 진행률 폴링 (script.js 로직) const ipForm = document.getElementById("ipForm"); if (ipForm) { ipForm.addEventListener("submit", async (ev) => { ev.preventDefault(); const formData = new FormData(ipForm); const btn = ipForm.querySelector('button[type="submit"]'); const originalHtml = btn.innerHTML; btn.disabled = true; btn.innerHTML = '처리 중...'; // 진행바 초기화 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 = `${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 listItem = container.closest('.list-group-item'); if (listItem) { const badge = listItem.querySelector('.badge'); if (badge) { badge.textContent = `${container.children.length} 파일`; } } } } // Sortable 초기화 backupContainers.forEach(container => { if (typeof Sortable === 'undefined') return; new Sortable(container, { group: 'backup-files', animation: 150, ghostClass: 'sortable-ghost', 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); 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 = `