Update 2026-01-20 20:47:44
This commit is contained in:
255
backend/snapshots/20260120_consolidation/js/index.js
Normal file
255
backend/snapshots/20260120_consolidation/js/index.js
Normal file
@@ -0,0 +1,255 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 스크립트 선택 시 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) {
|
||||
toggleXml();
|
||||
scriptSelect.addEventListener('change', toggleXml);
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 파일 보기 모달
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 진행바 업데이트
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
window.updateProgress = function (val) {
|
||||
const bar = document.getElementById('progressBar');
|
||||
if (!bar) return;
|
||||
const v = Math.max(0, Math.min(100, Number(val) || 0));
|
||||
bar.style.width = v + '%';
|
||||
bar.setAttribute('aria-valuenow', v);
|
||||
bar.innerHTML = `<span class="fw-semibold">${v}%</span>`;
|
||||
};
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// CSRF 토큰
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value || '';
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// IP 입력 데이터 보존 (Local Storage)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const ipTextarea = document.getElementById('ips');
|
||||
const ipForm = document.getElementById('ipForm');
|
||||
const STORAGE_KEY_IP = 'ip_input_draft';
|
||||
|
||||
if (ipTextarea) {
|
||||
// 1. 페이지 로드 시 저장된 값 복원
|
||||
const savedIps = localStorage.getItem(STORAGE_KEY_IP);
|
||||
if (savedIps) {
|
||||
ipTextarea.value = savedIps;
|
||||
// 라인 수 업데이트 트리거
|
||||
if (window.updateIpCount) window.updateIpCount();
|
||||
}
|
||||
|
||||
// 2. 입력 시마다 저장
|
||||
ipTextarea.addEventListener('input', () => {
|
||||
localStorage.setItem(STORAGE_KEY_IP, ipTextarea.value);
|
||||
// script.js에 있는 updateIpCount 호출 (있다면)
|
||||
if (window.updateIpCount) window.updateIpCount();
|
||||
});
|
||||
|
||||
// 3. 폼 제출 성공 시 초기화?
|
||||
// 사용자의 의도에 따라 다름: "변경이 되지 않는 이상 계속 가지고 있게"
|
||||
// -> 제출 후에도 유지하는 것이 요청 사항에 부합함.
|
||||
// 만약 '성공적으로 작업이 끝나면 지워달라'는 요청이 있으면 여기를 수정.
|
||||
// 현재 요청: "페이지가 리셋이되도 변경이 되지 않는이상 계속 가지고있게" -> 유지.
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 공통 POST 함수
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
async function postFormAndHandle(url) {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Accept': 'application/json, text/html;q=0.9,*/*;q=0.8',
|
||||
},
|
||||
});
|
||||
|
||||
const ct = (res.headers.get('content-type') || '').toLowerCase();
|
||||
|
||||
if (ct.includes('application/json')) {
|
||||
const data = await res.json();
|
||||
if (data.success === false) {
|
||||
throw new Error(data.error || ('HTTP ' + res.status));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
return { success: true, html: true };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 알림 자동 닫기
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.alert').forEach(alert => {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// IP 스캔 로직 (Modal)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
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.classList.remove('d-none');
|
||||
}
|
||||
progressBar.style.width = '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 })
|
||||
});
|
||||
|
||||
// 1. 세션 만료로 인한 리다이렉트 감지
|
||||
if (res.redirected) {
|
||||
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. JSON 응답인지 확인
|
||||
const contentType = res.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
const text = await res.text();
|
||||
if (text.includes("CSRF")) {
|
||||
throw new Error("보안 토큰(CSRF)이 만료되었습니다. 페이지를 새로고침해주세요.");
|
||||
}
|
||||
throw new Error(`서버 응답 오류 (HTTP ${res.status}): ${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) {
|
||||
// 진행바 초기화
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.textContent = '0%';
|
||||
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// IP 입력 지우기 버튼
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const btnClear = document.getElementById('btnClearIps');
|
||||
if (btnClear) {
|
||||
btnClear.addEventListener('click', () => {
|
||||
const ipsTextarea = document.getElementById('ips');
|
||||
if (ipsTextarea) {
|
||||
ipsTextarea.value = '';
|
||||
ipsTextarea.dispatchEvent(new Event('input')); // 로컬 스토리지 업데이트 및 카운트 갱신 트리거
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
443
backend/snapshots/20260120_consolidation/js/index_custom.js
Normal file
443
backend/snapshots/20260120_consolidation/js/index_custom.js
Normal file
@@ -0,0 +1,443 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Tom Select 초기화
|
||||
if (document.getElementById('script')) {
|
||||
new TomSelect("#script", {
|
||||
create: false,
|
||||
sortField: {
|
||||
field: "text",
|
||||
direction: "asc"
|
||||
},
|
||||
placeholder: "스크립트를 검색하거나 선택하세요...",
|
||||
plugins: ['clear_button'],
|
||||
allowEmptyOption: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 슬롯 우선순위 로직
|
||||
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');
|
||||
|
||||
// 기본 우선순위 데이터 (최대 10개)
|
||||
const defaultPriority = ['38', '39', '37', '36', '32', '33', '34', '35', '31', '40'];
|
||||
|
||||
function loadFromStorage() {
|
||||
const saved = localStorage.getItem('guidSlotNumbers');
|
||||
if (saved) {
|
||||
slotNumbersInput.value = saved;
|
||||
} else {
|
||||
slotNumbersInput.value = defaultPriority.join(', ');
|
||||
}
|
||||
if (presetCountInput) presetCountInput.value = 10;
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function saveToStorage() {
|
||||
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 !== '')
|
||||
.filter(s => /^\d+$/.test(s))
|
||||
.filter((v, i, a) => a.indexOf(v) === i);
|
||||
}
|
||||
|
||||
let sortableInstance = null;
|
||||
|
||||
function updatePreview() {
|
||||
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';
|
||||
let html = '';
|
||||
slots.forEach((slot, index) => {
|
||||
html += `
|
||||
<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>
|
||||
`;
|
||||
});
|
||||
slotPreview.innerHTML = html;
|
||||
|
||||
if (!sortableInstance) {
|
||||
sortableInstance = new Sortable(slotPreview, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
dragClass: 'sortable-drag',
|
||||
onEnd: function (evt) {
|
||||
updateInputFromPreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
saveToStorage();
|
||||
}
|
||||
|
||||
function updateInputFromPreview() {
|
||||
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 idxSpan = item.querySelector('.slot-index');
|
||||
if (idxSpan) idxSpan.textContent = index + 1;
|
||||
});
|
||||
saveToStorage();
|
||||
}
|
||||
|
||||
if (btnApplyPreset) {
|
||||
btnApplyPreset.addEventListener('click', function () {
|
||||
let count = parseInt(presetCountInput.value);
|
||||
if (isNaN(count) || count < 1) count = 1;
|
||||
if (count > 10) count = 10;
|
||||
presetCountInput.value = count;
|
||||
const selected = defaultPriority.slice(0, count);
|
||||
slotNumbersInput.value = selected.join(', ');
|
||||
slotNumbersInput.style.transition = 'background-color 0.2s';
|
||||
slotNumbersInput.style.backgroundColor = '#f0f9ff';
|
||||
setTimeout(() => {
|
||||
slotNumbersInput.style.backgroundColor = '#f8f9fa';
|
||||
}, 300);
|
||||
updatePreview();
|
||||
});
|
||||
}
|
||||
|
||||
slotNumbersInput.addEventListener('input', updatePreview);
|
||||
btnClearSlots.addEventListener('click', function () {
|
||||
if (confirm('입력된 내용을 모두 지우시겠습니까?')) {
|
||||
slotNumbersInput.value = '';
|
||||
updatePreview();
|
||||
}
|
||||
});
|
||||
slotPriorityModal.addEventListener('show.bs.modal', function () {
|
||||
modalServerListContent.value = serverListTextarea.value;
|
||||
loadFromStorage();
|
||||
});
|
||||
slotPriorityForm.addEventListener('submit', function (e) {
|
||||
const slots = parseSlots(slotNumbersInput.value);
|
||||
if (slots.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('최소 1개 이상의 슬롯을 입력하세요.');
|
||||
slotNumbersInput.focus();
|
||||
slotNumbersInput.classList.add('is-invalid');
|
||||
setTimeout(() => slotNumbersInput.classList.remove('is-invalid'), 2000);
|
||||
return;
|
||||
}
|
||||
slotPriorityInput.value = slots.join(',');
|
||||
saveToStorage();
|
||||
});
|
||||
}
|
||||
|
||||
// 백업 파일 드래그 앤 드롭 이동 기능
|
||||
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')) {
|
||||
// Optional: click outside behavior
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
const count = container.children.length;
|
||||
badge.textContent = `${count} 파일`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backupContainers.forEach(container => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
itemsToMove.forEach(item => {
|
||||
if (item !== evt.item) {
|
||||
evt.to.appendChild(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Server Request
|
||||
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(response => response.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) {
|
||||
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('이동 중 통신 오류 발생');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Quick Move 버튼 중복 클릭 방지 (Race Condition 예방)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Quick Move: 중복 처리 및 AJAX (Race Condition + Confirmation)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
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', function () {
|
||||
if (pendingAction) {
|
||||
dupModal.hide();
|
||||
pendingAction(true); // overwrite=true로 재실행
|
||||
pendingAction = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
quickMoveForms.forEach(id => {
|
||||
const form = document.getElementById(id);
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault(); // 기본 제출 방지 (AJAX 사용)
|
||||
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
if (!btn || btn.disabled) return;
|
||||
|
||||
const originalContent = btn.innerHTML;
|
||||
|
||||
// 실행 함수 정의 (overwrite 여부 파라미터)
|
||||
const executeMove = (overwrite = false) => {
|
||||
// UI Lock
|
||||
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': window.APP_CONFIG.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
overwrite: overwrite
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok: ' + response.status);
|
||||
}
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
throw new TypeError("Oops, we haven't got JSON!");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
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 에러이거나 단순 JSON 파싱 문제지만 실제로는 성공했을 가능성 대비
|
||||
// (사용자 요청에 따라 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;
|
||||
};
|
||||
|
||||
// 최초 실행 (overwrite=false)
|
||||
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 = '';
|
||||
// 최대 10개만 표시
|
||||
const limit = 10;
|
||||
const showList = duplicates.slice(0, limit);
|
||||
|
||||
showList.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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user