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