706 lines
30 KiB
JavaScript
706 lines
30 KiB
JavaScript
/**
|
|
* 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 = `<span class="fw-semibold small">${v}%</span>`;
|
|
|
|
// 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 = '<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')); // 저장 및 카운트 갱신
|
|
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 = '<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 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 = `
|
|
<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();
|
|
}
|
|
});
|