/** * 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 = `
프리셋을 선택하거나 번호를 입력하세요.
`; } else { slotPreview.style.display = 'grid'; slotPreview.innerHTML = slots.map((slot, index) => `
${index + 1}
Slot ${slot}
`).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 = '
'; 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 = `${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(); } });