This commit is contained in:
unknown
2026-03-21 07:26:38 +09:00
parent 166d94fca4
commit 40150cd66d
406 changed files with 34179 additions and 55658 deletions
+392 -73
View File
@@ -15,8 +15,14 @@ document.addEventListener('DOMContentLoaded', () => {
const bar = document.getElementById('progressBar');
if (!bar) return;
const v = Math.max(0, Math.min(100, Number(val) || 0));
// 부모 컨테이너가 숨겨져 있다면 표시
// 부모 컨테이너(섹션)가 숨겨져 있다면 표시
const section = document.getElementById('progressSection');
if (section && section.style.display === 'none') {
section.style.display = 'block';
}
// (Legacy support) 부모 컨테이너가 숨겨져 있다면 표시
const progressContainer = bar.closest('.progress');
if (progressContainer && progressContainer.parentElement.classList.contains('d-none')) {
progressContainer.parentElement.classList.remove('d-none');
@@ -25,8 +31,87 @@ document.addEventListener('DOMContentLoaded', () => {
bar.style.width = v + '%';
bar.setAttribute('aria-valuenow', v);
bar.innerHTML = `<span class="fw-semibold small">${v}%</span>`;
// 100% 도달 시 애니메이션 효과 제어 등은 필요 시 추가
};
/**
* 테이블 정렬 함수
* @param {string} tableId - 테이블 ID
* @param {number} colIdx - 정렬할 컬럼 인덱스 (0부터 시작)
*/
window.sortTable = function (tableId, colIdx) {
const table = document.getElementById(tableId);
if (!table) return;
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// 데이터가 없거나 1개 뿐이면 패스
if (rows.length <= 1) return;
if (rows[0].querySelector('td[colspan]')) return; // "파일이 없습니다" 행
const th = table.querySelectorAll('th')[colIdx];
let asc = !th.classList.contains('sorted-asc'); // 현재 상태의 반대로 토글
// 모든 헤더 초기화
table.querySelectorAll('th').forEach(h => {
h.classList.remove('sorted-asc', 'sorted-desc');
const icon = h.querySelector('i');
if (icon) icon.className = 'bi bi-arrow-down-up small text-muted';
});
// 현재 헤더 상태 업데이트
th.classList.add(asc ? 'sorted-asc' : 'sorted-desc');
const icon = th.querySelector('i');
if (icon) icon.className = asc ? 'bi bi-sort-down-alt text-primary' : 'bi bi-sort-up-alt text-primary';
// 정렬
rows.sort((rowA, rowB) => {
const cellA = rowA.children[colIdx];
const cellB = rowB.children[colIdx];
// data-sort 속성이 있으면 그것을 우선 사용 (날짜/숫자 등)
const valA = cellA.dataset.sort || cellA.textContent.trim();
const valB = cellB.dataset.sort || cellB.textContent.trim();
const isNum = !isNaN(parseFloat(valA)) && isFinite(valA) && !isNaN(parseFloat(valB)) && isFinite(valB);
if (isNum) {
return asc ? (parseFloat(valA) - parseFloat(valB)) : (parseFloat(valB) - parseFloat(valA));
} else {
return asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
}
});
// 다시 추가 (재정렬)
rows.forEach(row => tbody.appendChild(row));
};
/**
* 테이블 필터링 함수
* @param {string} tableId - 테이블 ID
* @param {string} text - 검색어
*/
window.filterTable = function (tableId, text) {
const table = document.getElementById(tableId);
if (!table) return;
const tbody = table.querySelector('tbody');
const rows = tbody.querySelectorAll('tr');
const query = text.toLowerCase().trim();
let visibleCount = 0;
rows.forEach(row => {
if (row.querySelector('td[colspan]')) return; // 메시지 행 제외
// 첫 번째 셀(파일명)만 검색 대상이라고 가정하거나, 전체 텍스트 검색
const rowText = row.textContent.toLowerCase();
if (rowText.includes(query)) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
// 검색 결과 없음 처리 (Optional)
};
// 줄 수 카운터 (script.js에서 가져옴)
@@ -88,7 +173,7 @@ document.addEventListener('DOMContentLoaded', () => {
// TomSelect 적용 전/후 모두 대응하기 위해 이벤트 리스너 등록
toggleXml();
scriptSelect.addEventListener('change', toggleXml);
// TomSelect 초기화
new TomSelect("#script", {
create: false,
@@ -135,9 +220,9 @@ document.addEventListener('DOMContentLoaded', () => {
// 진행바 표시 및 초기화
const progressContainer = progressBar.closest('.progress');
if (progressContainer && progressContainer.parentElement) {
// 이미 존재하는 row 등을 찾아서 보여주기 (필요시)
// 이미 존재하는 row 등을 찾아서 보여주기 (필요시)
}
window.updateProgress(100); // 스캔 중임을 알리기 위해 꽉 채움 (애니메이션)
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressBar.textContent = 'IP 스캔 중...';
@@ -185,6 +270,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (progressBar) {
window.updateProgress(0);
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
const section = document.getElementById('progressSection');
if (section) section.style.display = 'none';
}
}
});
@@ -194,11 +282,11 @@ document.addEventListener('DOMContentLoaded', () => {
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'));
}
const ipsTextarea = document.getElementById('ips');
if (ipsTextarea && confirm('입력된 IP 목록을 모두 지우시겠습니까?')) {
ipsTextarea.value = '';
ipsTextarea.dispatchEvent(new Event('input'));
}
});
}
@@ -209,6 +297,15 @@ document.addEventListener('DOMContentLoaded', () => {
ev.preventDefault();
const formData = new FormData(ipForm);
// [NEW] GUID 작업 시 로컬 스토리지의 슬롯 우선순위 추가
const jobType = formData.get('job_type');
if (jobType === 'guid') {
const savedPriority = localStorage.getItem('guidSlotNumbers');
if (savedPriority) {
formData.append('slot_priority', savedPriority);
}
}
const btn = ipForm.querySelector('button[type="submit"]');
const originalHtml = btn.innerHTML;
btn.disabled = true;
@@ -226,7 +323,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
if (data.job_id) {
// 비동기 작업 시작됨 -> 폴링 시작
pollProgress(data.job_id);
@@ -323,7 +420,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (!wasSelected) toggleSelection(item);
}
} else if (!e.target.closest('.backup-files-container')) {
// 배경 클릭 시 선택 해제? (UX에 따라 결정, 여기선 일단 패스)
// 배경 클릭 시 선택 해제? (UX에 따라 결정, 여기선 일단 패스)
}
});
@@ -365,7 +462,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Sortable 초기화
backupContainers.forEach(container => {
if (typeof Sortable === 'undefined') return;
new Sortable(container, {
group: 'backup-files',
animation: 150,
@@ -421,23 +518,23 @@ document.addEventListener('DOMContentLoaded', () => {
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);
.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);
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;
});
return data;
});
});
Promise.all(promises).then(results => {
@@ -520,9 +617,9 @@ document.addEventListener('DOMContentLoaded', () => {
<div>Slot ${slot}</div>
</div>
`).join('');
// 미리보기 Sortable
new Sortable(slotPreview, {
new Sortable(slotPreview, {
animation: 150,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
@@ -552,7 +649,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
slotNumbersInput.addEventListener('input', updateSlotPreview);
btnClearSlots.addEventListener('click', () => {
if (confirm('모두 지우시겠습니까?')) {
slotNumbersInput.value = '';
@@ -604,7 +701,7 @@ document.addEventListener('DOMContentLoaded', () => {
e.preventDefault();
const btn = form.querySelector('button[type="submit"]');
if (!btn || btn.disabled) return;
const originalContent = btn.innerHTML;
const executeMove = (overwrite = false) => {
@@ -620,48 +717,48 @@ document.addEventListener('DOMContentLoaded', () => {
},
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")) {
.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;
}
// "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;
alert('오류 발생: ' + err);
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 = () => {
@@ -690,7 +787,7 @@ document.addEventListener('DOMContentLoaded', () => {
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';
@@ -702,4 +799,226 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (dupModal) dupModal.show();
}
// ─────────────────────────────────────────────────────────────
// 6. 작업 유형 및 프로파일 처리 (process_ips 관련)
// ─────────────────────────────────────────────────────────────
const jobTypeSelect = document.getElementById('job_type');
const profileSelect = document.getElementById('profile');
const profileGroup = document.getElementById('profileGroup');
const profileDesc = document.getElementById('profileDesc');
const legacyScriptGroup = document.getElementById('legacyScriptGroup');
const scriptSelectLegacy = document.getElementById('script'); // 기존 스크립트 select
if (jobTypeSelect) {
jobTypeSelect.addEventListener('change', async () => {
const jobType = jobTypeSelect.value;
// 초기화
if (profileSelect) {
profileSelect.innerHTML = '<option value="">프로파일을 불러오는 중...</option>';
profileDesc.textContent = '';
}
if (jobType === 'legacy') {
// Legacy 모드
if (profileGroup) profileGroup.style.display = 'none';
if (legacyScriptGroup) legacyScriptGroup.style.display = 'block';
if (scriptSelectLegacy) scriptSelectLegacy.required = true;
if (profileSelect) profileSelect.required = false;
} else if (['mac', 'server_info', 'guid'].includes(jobType)) {
// 신규 모드
if (legacyScriptGroup) legacyScriptGroup.style.display = 'none';
if (profileGroup) profileGroup.style.display = 'block';
if (scriptSelectLegacy) scriptSelectLegacy.required = false;
if (profileSelect) profileSelect.required = true;
// 프로파일 로드
try {
const res = await fetch(`/api/profiles/${jobType}`);
if (!res.ok) throw new Error('프로파일 로드 실패');
const profiles = await res.json();
profileSelect.innerHTML = '<option value="">프로파일을 선택하세요</option>';
profiles.forEach(p => {
const opt = document.createElement('option');
opt.value = p.name;
opt.textContent = `${p.name} ${p.is_default ? '(기본)' : ''}`;
opt.dataset.desc = p.description;
profileSelect.appendChild(opt);
});
} catch (err) {
console.error(err);
profileSelect.innerHTML = '<option value="">오류 발생</option>';
}
} else {
// 선택 안함
if (legacyScriptGroup) legacyScriptGroup.style.display = 'none';
if (profileGroup) profileGroup.style.display = 'none';
}
});
// 프로파일 설명 표시
if (profileSelect) {
profileSelect.addEventListener('change', () => {
const selected = profileSelect.options[profileSelect.selectedIndex];
if (selected && selected.dataset.desc) {
profileDesc.textContent = selected.dataset.desc;
} else {
profileDesc.textContent = '';
}
});
}
}
// ─────────────────────────────────────────────────────────────
// 7. 탭 활성화 동기화 (외부 탭 활성화 시 내부 서브탭도 활성화)
// ─────────────────────────────────────────────────────────────
const repositoryTab = document.getElementById('repository-tab');
if (repositoryTab) {
repositoryTab.addEventListener('shown.bs.tab', function () {
// repository 탭이 활성화되면 첫 번째 서브탭(pills-backup)도 활성화
const firstPillTab = document.getElementById('pills-backup-tab');
const firstPillPane = document.getElementById('pills-backup');
if (firstPillTab && firstPillPane) {
// 버튼 활성화
document.querySelectorAll('#repoTabs .nav-link').forEach(btn => {
btn.classList.remove('active');
btn.setAttribute('aria-selected', 'false');
});
firstPillTab.classList.add('active');
firstPillTab.setAttribute('aria-selected', 'true');
// 패널 활성화
}
});
}
// ─────────────────────────────────────────────────────────────
// 7. 커스텀 탭 전환 로직
// ─────────────────────────────────────────────────────────────
// 메인 탭 전환
const mainTabButtons = document.querySelectorAll('#fileTabs button[data-custom-toggle="tab"]');
mainTabButtons.forEach(btn => {
btn.addEventListener('click', function (e) {
e.preventDefault();
// 모든 탭 버튼 비활성화
mainTabButtons.forEach(b => b.classList.remove('active'));
// 클릭한 탭 버튼 활성화
this.classList.add('active');
// 모든 탭 패널 숨기기
document.querySelectorAll('.custom-tab-panel').forEach(panel => {
panel.classList.remove('custom-active');
panel.style.display = 'none'; // 인라인 스타일로 강제 숨김
});
// 대상 탭 패널 표시
const targetId = this.getAttribute('data-bs-target');
const targetPanel = document.querySelector(targetId);
if (targetPanel) {
targetPanel.classList.add('custom-active');
targetPanel.style.display = 'block'; // 인라인 스타일도 변경
}
});
});
// 서브 탭 전환 (Pills)
const subTabButtons = document.querySelectorAll('#repoTabs button[data-custom-toggle="pill"]');
subTabButtons.forEach(btn => {
btn.addEventListener('click', function (e) {
e.preventDefault();
// 모든 서브 탭 버튼 비활성화
subTabButtons.forEach(b => b.classList.remove('active'));
// 모든 서브 탭 패널 숨기기
document.querySelectorAll('.custom-sub-panel').forEach(panel => {
panel.classList.remove('custom-sub-active');
panel.classList.add('d-none');
panel.style.display = 'none';
});
// subpanel-backup을 명시적으로 한 번 더 숨김
const backupPanel = document.getElementById('subpanel-backup');
if (backupPanel) {
backupPanel.classList.add('d-none');
backupPanel.style.setProperty('display', 'none', 'important');
}
// 대상 서브 탭 패널 표시
const targetId = this.getAttribute('data-bs-target');
const targetPanel = document.querySelector(targetId);
if (targetPanel) {
targetPanel.classList.add('custom-sub-active');
targetPanel.classList.remove('d-none');
targetPanel.classList.remove('display-none'); // 혹시 모를 클래스 제거
targetPanel.style.setProperty('display', 'block', 'important');
}
});
});
// ─────────────────────────────────────────────────────────────
// 8. URL 파라미터 기반 탭 활성화 (페이지네이션 지원)
// ─────────────────────────────────────────────────────────────
const urlParams = new URLSearchParams(window.location.search);
// 'backup_page' 파라미터가 있으면 '백업 및 저장소' -> '서버 정보' 탭 활성화
if (urlParams.has('backup_page')) {
const repoTabBtn = document.getElementById('repository-tab');
const backupSubTabBtn = document.getElementById('subpanel-backup-tab');
if (repoTabBtn) {
// 메인 탭 전환 트리거
repoTabBtn.click();
}
if (backupSubTabBtn) {
// 서브 탭 전환 트리거 (메인 탭 전환 후 실행)
setTimeout(() => {
backupSubTabBtn.click();
}, 50);
}
}
// ─────────────────────────────────────────────────────────────
// 9. 시스템 제어 패널 토글 아이콘 회전
// ─────────────────────────────────────────────────────────────
const systemControlPanel = document.getElementById('systemControlPanel');
const systemControlHint = document.getElementById('systemControlHint');
if (systemControlPanel) {
systemControlPanel.addEventListener('show.bs.collapse', () => {
const icon = document.querySelector('button[data-bs-target="#systemControlPanel"] i');
if (icon) {
icon.classList.remove('bi-chevron-down');
icon.classList.add('bi-chevron-up');
}
if (systemControlHint) {
systemControlHint.style.display = 'none';
}
});
systemControlPanel.addEventListener('hide.bs.collapse', () => {
const icon = document.querySelector('button[data-bs-target="#systemControlPanel"] i');
if (icon) {
icon.classList.remove('bi-chevron-up');
icon.classList.add('bi-chevron-down');
}
if (systemControlHint) {
systemControlHint.style.display = 'inline';
}
});
// 힌트 클릭 시 트리거
if (systemControlHint) {
systemControlHint.addEventListener('click', () => {
const btn = document.querySelector('button[data-bs-target="#systemControlPanel"]');
if (btn) btn.click();
});
}
}
});