310 lines
13 KiB
TypeScript
310 lines
13 KiB
TypeScript
import { openModal, closeModals } from './BaseModal';
|
|
import { createIcons, X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } from 'lucide';
|
|
import { state, loadMasterDataFromDB } from '../../core/state';
|
|
import { TYPE_PREFIX_MAP } from './SharedData';
|
|
|
|
let parsedData: any = null;
|
|
let currentTab: string = '';
|
|
let onSuccessCallback: (() => void) | null = null;
|
|
|
|
const UPLOAD_PREVIEW_MODAL_HTML = `
|
|
<div id="upload-preview-modal" class="modal-overlay hidden">
|
|
<div class="modal-content wide" style="width: 90vw; max-width: 1400px; height: 85vh; display: flex; flex-direction: column;">
|
|
<div class="modal-header">
|
|
<div style="display:flex; align-items:center; gap:0.75rem;">
|
|
<div style="background:var(--primary-light); padding:0.5rem; border-radius:8px;">
|
|
<i data-lucide="file-spreadsheet" style="width:20px; height:20px; color:var(--primary-color);"></i>
|
|
</div>
|
|
<div>
|
|
<h2 id="upload-preview-title">데이터 업로드 검토</h2>
|
|
<p style="font-size:12px; color:var(--text-muted); margin-top:2px;">업로드 전 데이터를 확인하고 수정 사항이 있는지 검토하세요.</p>
|
|
</div>
|
|
</div>
|
|
<button id="btn-close-upload-preview" class="btn-icon"><i data-lucide="x"></i></button>
|
|
</div>
|
|
|
|
<div class="modal-body" style="display:flex; padding:0; overflow:hidden; flex: 1;">
|
|
<!-- Sidebar for Tabs -->
|
|
<div id="upload-tab-sidebar" style="width:240px; border-right:1px solid var(--border-color); background:#fafafa; padding:1.5rem 1rem; overflow-y:auto; flex-shrink: 0;">
|
|
<div style="font-size:11px; font-weight:700; color:var(--text-muted); text-transform:uppercase; margin-bottom:1rem; letter-spacing:0.05em;">데이터 카테고리</div>
|
|
<div id="upload-tabs-container" style="display:flex; flex-direction:column; gap:0.5rem;">
|
|
<!-- Tabs will be injected here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Area -->
|
|
<div style="flex:1; display:flex; flex-direction:column; background:white; overflow:hidden;">
|
|
<div id="upload-preview-stats" style="padding:1rem 1.5rem; border-bottom:1px solid var(--border-color); display:flex; justify-content:space-between; align-items:center; background:white;">
|
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
<span id="current-tab-name" style="font-weight:700; font-size:16px;">선택된 탭 없음</span>
|
|
<span id="current-tab-count" class="badge badge-primary">0건</span>
|
|
<button id="btn-bulk-generate-codes" class="btn btn-outline btn-sm hidden" style="margin-left:1rem; height:28px; font-size:12px; padding:0 0.75rem;">
|
|
<i data-lucide="refresh-ccw" style="width:14px; height:14px; margin-right:4px;"></i> 자산코드 일괄 생성
|
|
</button>
|
|
</div>
|
|
<div style="font-size:12px; color:var(--text-muted);">
|
|
* 아래 데이터가 신규로 추가되거나 기존 데이터가 갱신됩니다.
|
|
</div>
|
|
</div>
|
|
|
|
<div id="upload-preview-table-wrapper" style="flex:1; overflow:auto; padding:0;">
|
|
<!-- Table will be injected here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer" style="background:#f9fafb; border-top:1px solid var(--border-color); flex-shrink: 0;">
|
|
<div style="display:flex; gap:0.75rem; width:100%; justify-content:flex-end;">
|
|
<button id="btn-cancel-upload" class="btn btn-outline" style="height:40px; padding:0 1.5rem;">취소하기</button>
|
|
<button id="btn-confirm-upload" class="btn btn-primary" style="height:40px; padding:0 2rem;">
|
|
<i data-lucide="save"></i> 최종 데이터 저장하기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
export function initUploadPreviewModal(onSuccess?: () => void) {
|
|
if (onSuccess) onSuccessCallback = onSuccess;
|
|
if (!document.getElementById('upload-preview-modal')) {
|
|
document.body.insertAdjacentHTML('beforeend', UPLOAD_PREVIEW_MODAL_HTML);
|
|
}
|
|
|
|
document.getElementById('btn-close-upload-preview')?.addEventListener('click', closeModals);
|
|
document.getElementById('btn-cancel-upload')?.addEventListener('click', closeModals);
|
|
document.getElementById('btn-confirm-upload')?.addEventListener('click', () => {
|
|
confirmUpload();
|
|
});
|
|
document.getElementById('btn-bulk-generate-codes')?.addEventListener('click', () => {
|
|
generateBulkCodes();
|
|
});
|
|
}
|
|
|
|
export function openUploadPreview(data: any) {
|
|
parsedData = data;
|
|
const tabNames = Object.keys(data);
|
|
if (tabNames.length === 0) {
|
|
alert('업로드할 데이터가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
currentTab = tabNames[0];
|
|
renderTabs();
|
|
renderCurrentTable();
|
|
|
|
openModal('upload-preview-modal');
|
|
createIcons({ icons: { X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } });
|
|
}
|
|
|
|
function renderTabs() {
|
|
const container = document.getElementById('upload-tabs-container');
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
|
|
Object.keys(parsedData).forEach(tab => {
|
|
const btn = document.createElement('div');
|
|
btn.className = `upload-tab-btn ${tab === currentTab ? 'active' : ''}`;
|
|
btn.style.cssText = `
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: all 0.2s;
|
|
background: ${tab === currentTab ? 'white' : 'transparent'};
|
|
color: ${tab === currentTab ? 'var(--primary-color)' : 'var(--text-main)'};
|
|
box-shadow: ${tab === currentTab ? '0 2px 4px rgba(0,0,0,0.05)' : 'none'};
|
|
border: 1px solid ${tab === currentTab ? 'var(--border-color)' : 'transparent'};
|
|
`;
|
|
|
|
btn.innerHTML = `
|
|
<span>${tab}</span>
|
|
<span style="font-size:11px; opacity:0.6;">${parsedData[tab].length}</span>
|
|
`;
|
|
|
|
btn.onclick = () => {
|
|
currentTab = tab;
|
|
renderTabs();
|
|
renderCurrentTable();
|
|
};
|
|
container.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
function renderCurrentTable() {
|
|
const tableWrapper = document.getElementById('upload-preview-table-wrapper');
|
|
const tabNameEl = document.getElementById('current-tab-name');
|
|
const tabCountEl = document.getElementById('current-tab-count');
|
|
if (!tableWrapper || !tabNameEl || !tabCountEl) return;
|
|
|
|
const data = parsedData[currentTab];
|
|
tabNameEl.textContent = currentTab;
|
|
tabCountEl.textContent = `${data.length}건`;
|
|
|
|
const generateBtn = document.getElementById('btn-bulk-generate-codes');
|
|
const isHwTab = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'].includes(currentTab);
|
|
if (generateBtn) {
|
|
if (isHwTab) generateBtn.classList.remove('hidden');
|
|
else generateBtn.classList.add('hidden');
|
|
}
|
|
|
|
if (!data || data.length === 0) {
|
|
tableWrapper.innerHTML = '<div style="padding:4rem; text-align:center; color:var(--text-muted);">표시할 데이터가 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
// Get headers from first item keys, excluding 'id' and 'type' for cleaner view
|
|
const headers = Object.keys(data[0]).filter(k => k !== 'id' && k !== 'type');
|
|
|
|
let tableHTML = `
|
|
<table class="preview-table" style="width:100%; border-collapse:collapse; min-width:max-content;">
|
|
<thead style="position:sticky; top:0; z-index:10; background:#f8fafc; box-shadow:0 1px 0 var(--border-color);">
|
|
<tr>
|
|
<th style="padding:0.75rem 1rem; text-align:center; font-size:12px; border-bottom:1px solid var(--border-color); width:50px;">No.</th>
|
|
${headers.map(h => `<th style="padding:0.75rem 1rem; text-align:left; font-size:12px; border-bottom:1px solid var(--border-color); color:var(--text-muted);">${h}</th>`).join('')}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.map((row: any, idx: number) => `
|
|
<tr style="border-bottom:1px solid #f1f5f9;">
|
|
<td style="padding:0.75rem 1rem; text-align:center; font-size:13px; color:var(--text-muted);">${idx + 1}</td>
|
|
${headers.map(h => `<td style="padding:0.75rem 1rem; font-size:13px;">${row[h] || '-'}</td>`).join('')}
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
tableWrapper.innerHTML = tableHTML;
|
|
}
|
|
|
|
async function confirmUpload() {
|
|
const confirmBtn = document.getElementById('btn-confirm-upload') as HTMLButtonElement;
|
|
if (confirmBtn) {
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.innerHTML = '<i data-lucide="loader-2" class="animate-spin"></i> 저장 중...';
|
|
createIcons({ icons: { Save } });
|
|
}
|
|
|
|
try {
|
|
const tabNames = Object.keys(parsedData);
|
|
let successCount = 0;
|
|
|
|
for (const tab of tabNames) {
|
|
const data = parsedData[tab];
|
|
let endpoint = '';
|
|
|
|
const API_BASE = `http://${location.hostname}:3000`;
|
|
if (tab === '개인PC') endpoint = `${API_BASE}/api/pc/batch`;
|
|
else if (tab === '서버') endpoint = `${API_BASE}/api/server/batch`;
|
|
else if (tab === '스토리지') endpoint = `${API_BASE}/api/storage/batch`;
|
|
else if (tab === '전산비품') endpoint = `${API_BASE}/api/equip/batch`;
|
|
else if (tab === '모바일기기') endpoint = `${API_BASE}/api/mobile/batch`;
|
|
else if (tab === '구독SW') endpoint = `${API_BASE}/api/sw/sub/batch`;
|
|
else if (tab === '영구SW') endpoint = `${API_BASE}/api/sw/perm/batch`;
|
|
else if (tab === '클라우드') endpoint = `${API_BASE}/api/cloud/batch`;
|
|
else if (tab === '도메인') endpoint = `${API_BASE}/api/ops/domain/batch`;
|
|
|
|
if (endpoint) {
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (response.ok) {
|
|
successCount++;
|
|
} else {
|
|
const errRes = await response.json();
|
|
throw new Error(`[${tab}] ${errRes.error || '저장 실패'}`);
|
|
}
|
|
} catch (e: any) {
|
|
alert(`카테고리 '${tab}' 저장 중 오류: ${e.message}`);
|
|
throw e; // Stop processing further tabs
|
|
}
|
|
}
|
|
}
|
|
|
|
if (successCount > 0) {
|
|
if (onSuccessCallback) onSuccessCallback();
|
|
closeModals();
|
|
alert(`${successCount}개 카테고리의 데이터가 성공적으로 업로드되었습니다.`);
|
|
} else {
|
|
alert('데이터 업로드에 실패했습니다.');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
// 상세 에러는 내부 catch에서 이미 alert으로 띄움
|
|
} finally {
|
|
if (confirmBtn) {
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.innerHTML = '<i data-lucide="save"></i> 최종 데이터 저장하기';
|
|
createIcons({ icons: { Save } });
|
|
}
|
|
}
|
|
}
|
|
|
|
async function generateBulkCodes() {
|
|
const data = parsedData[currentTab];
|
|
if (!data) return;
|
|
|
|
const generateBtn = document.getElementById('btn-bulk-generate-codes') as HTMLButtonElement;
|
|
if (generateBtn) {
|
|
generateBtn.disabled = true;
|
|
generateBtn.innerHTML = '<i data-lucide="refresh-ccw" class="animate-spin"></i> 생성 중...';
|
|
createIcons({ icons: { RefreshCcw } });
|
|
}
|
|
|
|
try {
|
|
// Group rows by prefix (type + purchase_ym)
|
|
const rowsToProcess = data.filter((r: any) => !r.자산코드);
|
|
if (rowsToProcess.length === 0) {
|
|
alert('이미 모든 항목에 자산코드가 부여되어 있습니다.');
|
|
return;
|
|
}
|
|
|
|
const groups: Record<string, any[]> = {};
|
|
rowsToProcess.forEach((r: any) => {
|
|
const type = r.비품유형 || r.기기유형 || r.type || 'ETC';
|
|
const typeCode = TYPE_PREFIX_MAP[type] || 'ETC';
|
|
const purchaseYM = String(r.구매연월 || '').replace(/[^0-9]/g, '');
|
|
if (purchaseYM.length < 6) {
|
|
// Fallback or skip
|
|
return;
|
|
}
|
|
const prefix = `${typeCode}-${purchaseYM.substring(0, 6)}-`;
|
|
if (!groups[prefix]) groups[prefix] = [];
|
|
groups[prefix].push(r);
|
|
});
|
|
|
|
for (const prefix in groups) {
|
|
const rows = groups[prefix];
|
|
// Fetch current next code for this prefix
|
|
const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}`);
|
|
const result = await res.json();
|
|
if (result.nextCode) {
|
|
let baseNum = parseInt(result.nextCode.replace(prefix, ''));
|
|
rows.forEach((r, idx) => {
|
|
r.자산코드 = `${prefix}${(baseNum + idx).toString().padStart(4, '0')}`;
|
|
});
|
|
}
|
|
}
|
|
|
|
renderCurrentTable();
|
|
alert(`${rowsToProcess.length}건의 자산코드가 생성되었습니다.`);
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('자산코드 생성 중 오류가 발생했습니다.');
|
|
} finally {
|
|
if (generateBtn) {
|
|
generateBtn.disabled = false;
|
|
generateBtn.innerHTML = '<i data-lucide="refresh-ccw"></i> 자산코드 일괄 생성';
|
|
createIcons({ icons: { RefreshCcw } });
|
|
}
|
|
}
|
|
}
|