feat: Implement Excel bulk upload with review modal and domain support
This commit is contained in:
224
src/components/Modal/UploadPreviewModal.ts
Normal file
224
src/components/Modal/UploadPreviewModal.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { openModal, closeModals } from './BaseModal';
|
||||
import { createIcons, X, Check, Database, Save, FileSpreadsheet, Layers } from 'lucide';
|
||||
import { state, loadMasterDataFromDB } from '../../core/state';
|
||||
|
||||
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>
|
||||
</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();
|
||||
});
|
||||
}
|
||||
|
||||
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 } });
|
||||
}
|
||||
|
||||
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}건`;
|
||||
|
||||
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 = '';
|
||||
|
||||
if (tab === '개인PC') endpoint = '/api/pc/batch';
|
||||
else if (tab === '서버') endpoint = '/api/server/batch';
|
||||
else if (tab === '스토리지') endpoint = '/api/storage/batch';
|
||||
else if (tab === '전산비품') endpoint = '/api/equip/batch';
|
||||
else if (tab === '모바일기기') endpoint = '/api/mobile/batch';
|
||||
else if (tab === '구독SW') endpoint = '/api/sw/sub/batch';
|
||||
else if (tab === '영구SW') endpoint = '/api/sw/perm/batch';
|
||||
else if (tab === '클라우드') endpoint = '/api/sw/cloud/batch';
|
||||
else if (tab === '도메인') endpoint = '/api/ops/domain/batch';
|
||||
|
||||
if (endpoint) {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (response.ok) successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
if (onSuccessCallback) onSuccessCallback();
|
||||
closeModals();
|
||||
alert(`${successCount}개 카테고리의 데이터가 성공적으로 업로드되었습니다.`);
|
||||
} else {
|
||||
alert('데이터 업로드에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('업로드 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.innerHTML = '<i data-lucide="save"></i> 최종 데이터 저장하기';
|
||||
createIcons({ icons: { Save } });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user