Files
2026-01-20 20:47:45 +09:00

444 lines
19 KiB
JavaScript

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();
}
});