feat: 공동작업을 위한 프로젝트 구조 최적화 및 가이드 배포
This commit is contained in:
46
src/components/Modal/BaseModal.ts
Normal file
46
src/components/Modal/BaseModal.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다.
|
||||
*/
|
||||
export function initBaseModal() {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
const closeButtons = document.querySelectorAll('.btn-icon, [id^="btn-cancel-"]');
|
||||
|
||||
// 모든 모달 닫기 함수
|
||||
const closeAllModals = () => {
|
||||
modals.forEach(modal => modal.classList.add('hidden'));
|
||||
// SW 관련 추가 모달 처리
|
||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
||||
document.getElementById('dashboard-detail-modal')?.classList.add('hidden');
|
||||
};
|
||||
|
||||
// 닫기 버튼 이벤트 바인딩
|
||||
closeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', closeAllModals);
|
||||
});
|
||||
|
||||
// ESC 키로 닫기
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeAllModals();
|
||||
});
|
||||
|
||||
// 배경(Overlay) 클릭 시 닫기
|
||||
modals.forEach(modal => {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closeAllModals();
|
||||
});
|
||||
});
|
||||
|
||||
return { closeAllModals };
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 모달을 엽니다.
|
||||
* @param modalId 모달 엘리먼트의 ID
|
||||
*/
|
||||
export function openModal(modalId: string) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
112
src/components/Modal/HWModal.ts
Normal file
112
src/components/Modal/HWModal.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { state } from '../../state';
|
||||
import { HardwareAsset } from '../../excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
|
||||
/**
|
||||
* 하드웨어(서버, 전산비품 등) 모달 초기화 및 로직 제어
|
||||
*/
|
||||
export function initHWModal(renderContent: () => void, closeModals: () => void) {
|
||||
const hwForm = document.getElementById('hw-asset-form') as HTMLFormElement;
|
||||
const btnSaveHw = document.getElementById('btn-save-hw-asset') as HTMLButtonElement;
|
||||
const btnDeleteHw = document.getElementById('btn-delete-hw-asset') as HTMLButtonElement;
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
btnSaveHw?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!hwForm.checkValidity()) { hwForm.reportValidity(); return; }
|
||||
|
||||
const id = (document.getElementById('hw-asset-id') as HTMLInputElement).value;
|
||||
const fileInput = document.getElementById('hw-품의서') as HTMLInputElement;
|
||||
const 품의서명 = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('hw-품의서명') as HTMLElement).innerText.replace('📎', '');
|
||||
|
||||
const newAsset: HardwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: (document.getElementById('hw-asset-type') as HTMLInputElement).value,
|
||||
법인: (document.getElementById('hw-법인') as HTMLInputElement).value,
|
||||
자산코드: (document.getElementById('hw-자산코드') as HTMLInputElement).value,
|
||||
명칭: (document.getElementById('hw-명칭') as HTMLInputElement).value,
|
||||
위치: (document.getElementById('hw-위치') as HTMLInputElement).value,
|
||||
관리자: (document.getElementById('hw-관리자') as HTMLInputElement).value,
|
||||
IP주소: (document.getElementById('hw-IP주소') as HTMLInputElement).value,
|
||||
MACaddress: (document.getElementById('hw-MACaddress') as HTMLInputElement).value,
|
||||
OS: (document.getElementById('hw-OS') as HTMLInputElement).value,
|
||||
HW사양: (document.getElementById('hw-HW사양') as HTMLTextAreaElement).value,
|
||||
구매일: (document.getElementById('hw-구매일') as HTMLInputElement).value,
|
||||
금액: (document.getElementById('hw-금액') as HTMLInputElement).value,
|
||||
납품업체: (document.getElementById('hw-납품업체') as HTMLInputElement).value,
|
||||
품의서명,
|
||||
비품유형: (document.getElementById('hw-asset-type') as HTMLInputElement).value === '전산비품'
|
||||
? (document.getElementById('hw-비품유형') as HTMLSelectElement).value : undefined
|
||||
};
|
||||
|
||||
if (id) {
|
||||
const idx = state.masterData.hw.findIndex(a => a.id === id);
|
||||
if(idx !== -1) state.masterData.hw[idx] = newAsset;
|
||||
} else {
|
||||
state.masterData.hw.push(newAsset);
|
||||
}
|
||||
|
||||
closeModals();
|
||||
renderContent();
|
||||
});
|
||||
|
||||
// 삭제 버튼 이벤트
|
||||
btnDeleteHw?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const id = (document.getElementById('hw-asset-id') as HTMLInputElement).value;
|
||||
if (confirm('삭제하시겠습니까?')) {
|
||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
|
||||
closeModals();
|
||||
renderContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 하드웨어 상세 모달 열기
|
||||
* @param asset 수정 시 자산 데이터, 신규 시 undefined
|
||||
*/
|
||||
export function openHwModal(asset?: HardwareAsset) {
|
||||
const hwModal = document.getElementById('hw-asset-modal') as HTMLDivElement;
|
||||
const hwForm = document.getElementById('hw-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
|
||||
|
||||
openModal('hw-asset-modal');
|
||||
hwForm.reset();
|
||||
|
||||
if (asset) {
|
||||
document.getElementById('hw-modal-title')!.textContent = '자산 상세 정보 수정';
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
(document.getElementById('hw-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('hw-asset-type') as HTMLInputElement).value = asset.type;
|
||||
(document.getElementById('hw-법인') as HTMLInputElement).value = asset.법인;
|
||||
(document.getElementById('hw-자산코드') as HTMLInputElement).value = asset.자산코드;
|
||||
(document.getElementById('hw-명칭') as HTMLInputElement).value = asset.명칭;
|
||||
(document.getElementById('hw-위치') as HTMLInputElement).value = asset.위치;
|
||||
(document.getElementById('hw-관리자') as HTMLInputElement).value = asset.관리자;
|
||||
(document.getElementById('hw-IP주소') as HTMLInputElement).value = asset.IP주소;
|
||||
(document.getElementById('hw-MACaddress') as HTMLInputElement).value = asset.MACaddress;
|
||||
(document.getElementById('hw-OS') as HTMLInputElement).value = asset.OS;
|
||||
(document.getElementById('hw-HW사양') as HTMLTextAreaElement).value = asset.HW사양;
|
||||
(document.getElementById('hw-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('hw-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : '';
|
||||
(document.getElementById('hw-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('hw-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : '';
|
||||
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = asset.비품유형 || '노트북';
|
||||
} else {
|
||||
document.getElementById('hw-modal-title')!.textContent = `새 ${state.activeSubTab} 자산 추가`;
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('hw-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('hw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
||||
(document.getElementById('hw-품의서명') as HTMLElement).innerText = '';
|
||||
(document.getElementById('hw-비품유형') as HTMLSelectElement).value = '노트북';
|
||||
}
|
||||
|
||||
// 전산비품일 경우 유형 선택 필드 노출
|
||||
if (state.activeSubTab === '전산비품') {
|
||||
document.getElementById('hw-비품유형-group')!.style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('hw-비품유형-group')!.style.display = 'none';
|
||||
}
|
||||
}
|
||||
111
src/components/Modal/PCModal.ts
Normal file
111
src/components/Modal/PCModal.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { state } from '../../state';
|
||||
import { HardwareAsset } from '../../excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
|
||||
/**
|
||||
* 개인PC 모달 초기화 및 로직 제어
|
||||
*/
|
||||
export function initPCModal(renderContent: () => void, closeModals: () => void) {
|
||||
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
|
||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
|
||||
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
btnSavePc?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; }
|
||||
|
||||
const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value;
|
||||
const fileInput = document.getElementById('pc-품의서') as HTMLInputElement;
|
||||
const 품의서명 = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('pc-품의서명') as HTMLElement).innerText.replace('📎', '');
|
||||
|
||||
const newAsset: HardwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: '개인PC',
|
||||
법인: (document.getElementById('pc-법인') as HTMLSelectElement).value,
|
||||
자산코드: (document.getElementById('pc-자산코드') as HTMLInputElement).value,
|
||||
명칭: '',
|
||||
위치: (document.getElementById('pc-위치') as HTMLInputElement).value,
|
||||
관리자: '',
|
||||
IP주소: '',
|
||||
MACaddress: '',
|
||||
HW사양: '',
|
||||
OS: '',
|
||||
사용자: (document.getElementById('pc-사용자') as HTMLInputElement).value,
|
||||
CPU: (document.getElementById('pc-CPU') as HTMLInputElement).value,
|
||||
GPU: (document.getElementById('pc-GPU') as HTMLInputElement).value,
|
||||
RAM: (document.getElementById('pc-RAM') as HTMLInputElement).value,
|
||||
SSD1: (document.getElementById('pc-SSD1') as HTMLInputElement).value,
|
||||
SSD2: (document.getElementById('pc-SSD2') as HTMLInputElement).value,
|
||||
HDD1: (document.getElementById('pc-HDD1') as HTMLInputElement).value,
|
||||
HDD2: (document.getElementById('pc-HDD2') as HTMLInputElement).value,
|
||||
구매일: (document.getElementById('pc-구매일') as HTMLInputElement).value,
|
||||
금액: (document.getElementById('pc-금액') as HTMLInputElement).value,
|
||||
납품업체: (document.getElementById('pc-납품업체') as HTMLInputElement).value,
|
||||
품의서명
|
||||
};
|
||||
|
||||
if (id) {
|
||||
const idx = state.masterData.hw.findIndex(a => a.id === id);
|
||||
if(idx !== -1) state.masterData.hw[idx] = newAsset;
|
||||
} else {
|
||||
state.masterData.hw.push(newAsset);
|
||||
}
|
||||
|
||||
closeModals();
|
||||
renderContent();
|
||||
});
|
||||
|
||||
// 삭제 버튼 이벤트
|
||||
btnDeletePc?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value;
|
||||
if (confirm('삭제하시겠습니까?')) {
|
||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
|
||||
closeModals();
|
||||
renderContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 개인PC 상세 모달 열기
|
||||
* @param asset 수정 시 자산 데이터, 신규 시 undefined
|
||||
*/
|
||||
export function openPcModal(asset?: HardwareAsset) {
|
||||
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
|
||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-pc-asset')!;
|
||||
|
||||
openModal('pc-asset-modal');
|
||||
pcForm.reset();
|
||||
|
||||
if (asset) {
|
||||
document.getElementById('pc-modal-title')!.textContent = '개인PC 상세 정보 수정';
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
(document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('pc-법인') as HTMLSelectElement).value = asset.법인;
|
||||
(document.getElementById('pc-자산코드') as HTMLInputElement).value = asset.자산코드;
|
||||
(document.getElementById('pc-사용자') as HTMLInputElement).value = asset.사용자 || '';
|
||||
(document.getElementById('pc-위치') as HTMLInputElement).value = asset.위치 || '';
|
||||
(document.getElementById('pc-CPU') as HTMLInputElement).value = asset.CPU || '';
|
||||
(document.getElementById('pc-GPU') as HTMLInputElement).value = asset.GPU || '';
|
||||
(document.getElementById('pc-RAM') as HTMLInputElement).value = asset.RAM || '';
|
||||
(document.getElementById('pc-SSD1') as HTMLInputElement).value = asset.SSD1 || '';
|
||||
(document.getElementById('pc-SSD2') as HTMLInputElement).value = asset.SSD2 || '';
|
||||
(document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || '';
|
||||
(document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || '';
|
||||
(document.getElementById('pc-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : '';
|
||||
(document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : '';
|
||||
} else {
|
||||
document.getElementById('pc-modal-title')!.textContent = '새 개인PC 자산 추가';
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('pc-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('pc-법인') as HTMLSelectElement).value = '한맥';
|
||||
(document.getElementById('pc-품의서명') as HTMLElement).innerText = '';
|
||||
}
|
||||
}
|
||||
102
src/components/Modal/SWModal.ts
Normal file
102
src/components/Modal/SWModal.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { state } from '../../state';
|
||||
import { SoftwareAsset } from '../../excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
|
||||
/**
|
||||
* 소프트웨어 모달 초기화 및 로직 제어
|
||||
*/
|
||||
export function initSWModal(renderContent: () => void, closeModals: () => void) {
|
||||
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
||||
const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement;
|
||||
const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement;
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
btnSaveSw?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!swForm.checkValidity()) { swForm.reportValidity(); return; }
|
||||
|
||||
const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value;
|
||||
const newAsset: SoftwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: (document.getElementById('sw-asset-type') as HTMLInputElement).value,
|
||||
법인: (document.getElementById('sw-법인') as HTMLSelectElement).value,
|
||||
제품명: (document.getElementById('sw-제품명') as HTMLInputElement).value,
|
||||
구매일: (document.getElementById('sw-구매일') as HTMLInputElement).value,
|
||||
구독일: (document.getElementById('sw-구독일') as HTMLInputElement).value,
|
||||
유지보수여부: (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked,
|
||||
금액: (document.getElementById('sw-금액') as HTMLInputElement).value,
|
||||
수량: parseInt((document.getElementById('sw-수량') as HTMLInputElement).value || '1', 10),
|
||||
계정명: (document.getElementById('sw-계정명') as HTMLInputElement).value,
|
||||
납품업체: (document.getElementById('sw-납품업체') as HTMLInputElement).value,
|
||||
비고: (document.getElementById('sw-비고') as HTMLInputElement).value,
|
||||
};
|
||||
|
||||
if (id) {
|
||||
const idx = state.masterData.sw.findIndex(a => a.id === id);
|
||||
if(idx !== -1) state.masterData.sw[idx] = newAsset;
|
||||
} else {
|
||||
state.masterData.sw.push(newAsset);
|
||||
}
|
||||
|
||||
closeModals();
|
||||
renderContent();
|
||||
});
|
||||
|
||||
// 삭제 버튼 이벤트
|
||||
btnDeleteSw?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value;
|
||||
if (confirm('삭제하시겠습니까?')) {
|
||||
state.masterData.sw = state.masterData.sw.filter(a => a.id !== id);
|
||||
closeModals();
|
||||
renderContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 소프트웨어 상세 모달 열기
|
||||
* @param asset 수정 시 자산 데이터, 신규 시 undefined
|
||||
*/
|
||||
export function openSwModal(asset?: SoftwareAsset) {
|
||||
const swModal = document.getElementById('sw-asset-modal') as HTMLDivElement;
|
||||
const swForm = document.getElementById('sw-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
||||
|
||||
openModal('sw-asset-modal');
|
||||
swForm.reset();
|
||||
|
||||
const subGroup = document.getElementById('sw-구독일-group')!;
|
||||
const permGroup = document.getElementById('sw-유지보수-group')!;
|
||||
if (state.activeSubTab === '구독SW') {
|
||||
subGroup.style.display = 'block';
|
||||
permGroup.style.display = 'none';
|
||||
} else {
|
||||
subGroup.style.display = 'none';
|
||||
permGroup.style.display = 'block';
|
||||
}
|
||||
|
||||
if (asset) {
|
||||
document.getElementById('sw-modal-title')!.textContent = `${state.activeSubTab} 상세 정보 수정`;
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type;
|
||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = asset.법인;
|
||||
(document.getElementById('sw-제품명') as HTMLInputElement).value = asset.제품명;
|
||||
(document.getElementById('sw-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('sw-구독일') as HTMLInputElement).value = asset.구독일 || '';
|
||||
(document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = !!asset.유지보수여부;
|
||||
(document.getElementById('sw-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : '';
|
||||
(document.getElementById('sw-수량') as HTMLInputElement).value = String(asset.수량);
|
||||
(document.getElementById('sw-계정명') as HTMLInputElement).value = asset.계정명 || '';
|
||||
(document.getElementById('sw-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('sw-비고') as HTMLInputElement).value = asset.비고 || '';
|
||||
} else {
|
||||
document.getElementById('sw-modal-title')!.textContent = `새 ${state.activeSubTab} 자산 추가`;
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('sw-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab;
|
||||
(document.getElementById('sw-법인') as HTMLSelectElement).value = '한맥';
|
||||
}
|
||||
}
|
||||
171
src/components/Modal/SWUserModal.ts
Normal file
171
src/components/Modal/SWUserModal.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { state } from '../../state';
|
||||
import { SoftwareAsset, SWUser } from '../../excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
import { createIcons, Edit2, X, Paperclip } from 'lucide';
|
||||
|
||||
let currentSwUserAssetId: string = '';
|
||||
let tempSwUsers: SWUser[] = [];
|
||||
|
||||
/**
|
||||
* 소프트웨어 사용자 할당 모달 초기화
|
||||
*/
|
||||
export function initSWUserModal(renderContent: () => void, closeModals: () => void) {
|
||||
const btnOpenAddUser = document.getElementById('btn-open-add-user');
|
||||
const btnSaveEditUser = document.getElementById('btn-save-edit-user');
|
||||
const btnSaveSwUserMapping = document.getElementById('btn-save-sw-user-mapping');
|
||||
|
||||
btnOpenAddUser?.addEventListener('click', () => {
|
||||
openUserEditModal(-1);
|
||||
});
|
||||
|
||||
btnSaveEditUser?.addEventListener('click', () => {
|
||||
saveUserEdit();
|
||||
});
|
||||
|
||||
btnSaveSwUserMapping?.addEventListener('click', () => {
|
||||
// 변경사항 전역 상태에 반영
|
||||
state.masterData.swUsers = state.masterData.swUsers.filter(u => u.swId !== currentSwUserAssetId);
|
||||
state.masterData.swUsers.push(...tempSwUsers);
|
||||
|
||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
||||
renderContent();
|
||||
});
|
||||
|
||||
// 취소 버튼들
|
||||
document.getElementById('btn-cancel-sw-user-edit')?.addEventListener('click', () => {
|
||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
||||
});
|
||||
document.getElementById('btn-cancel-sw-user-modal')?.addEventListener('click', () => {
|
||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 소프트웨어 사용자 목록 렌더링
|
||||
*/
|
||||
function renderUserList() {
|
||||
const tbody = document.getElementById('user-list-body')!;
|
||||
tbody.innerHTML = '';
|
||||
if (tempSwUsers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="padding: 1rem; text-align: center; color: var(--text-muted);">할당된 사용자가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tempSwUsers.forEach((user, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cssText = 'border-bottom: 1px solid var(--border); transition: background-color 0.2s;';
|
||||
|
||||
const deptTeam = [user.부서, user.팀].filter(Boolean).join(' / ') || '-';
|
||||
const attachIcon = user.신청서명 ? `<i data-lucide="paperclip" class="text-primary" style="width:16px; height:16px;" title="${user.신청서명}"></i>` : '-';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="padding:0.5rem; text-align:left;">${user.법인}</td>
|
||||
<td style="padding:0.5rem; text-align:left;">${deptTeam}</td>
|
||||
<td style="padding:0.5rem; text-align:left;">${user.직위 || '-'}</td>
|
||||
<td style="padding:0.5rem; text-align:left;"><strong>${user.이름}</strong></td>
|
||||
<td style="padding:0.5rem; text-align:center;">${user.사용기간 || '-'}</td>
|
||||
<td style="padding:0.5rem; text-align:center; color: var(--text-light);">${attachIcon}</td>
|
||||
<td style="padding:0.5rem; text-align:center; display:flex; justify-content:center; gap:0.25rem;">
|
||||
<button type="button" class="btn-icon btn-edit-user" data-idx="${idx}" style="color: var(--primary);" title="수정"><i data-lucide="edit-2" style="width:14px; height:14px;"></i></button>
|
||||
<button type="button" class="btn-icon btn-remove-user" data-idx="${idx}" style="color: var(--danger);" title="삭제"><i data-lucide="x" style="width:14px; height:14px;"></i></button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
createIcons({ icons: { Edit2, X, Paperclip } });
|
||||
|
||||
tbody.querySelectorAll('.btn-edit-user').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
||||
openUserEditModal(idx);
|
||||
});
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.btn-remove-user').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt((e.currentTarget as HTMLButtonElement).getAttribute('data-idx')!);
|
||||
tempSwUsers.splice(idx, 1);
|
||||
renderUserList();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 할당 모달 열기
|
||||
*/
|
||||
export function openSwUserModal(asset: SoftwareAsset) {
|
||||
openModal('sw-user-modal');
|
||||
currentSwUserAssetId = asset.id;
|
||||
tempSwUsers = state.masterData.swUsers.filter(u => u.swId === asset.id).map(u => ({...u}));
|
||||
renderUserList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 추가/수정 모달 열기
|
||||
*/
|
||||
function openUserEditModal(idx: number) {
|
||||
const editModal = document.getElementById('sw-user-edit-modal')!;
|
||||
editModal.classList.remove('hidden');
|
||||
(document.getElementById('edit-user-idx') as HTMLInputElement).value = String(idx);
|
||||
|
||||
if (idx === -1) {
|
||||
document.getElementById('sw-user-edit-modal-title')!.innerText = '새 사용자 추가';
|
||||
(document.getElementById('new-user-법인') as HTMLSelectElement).value = '한맥';
|
||||
(document.getElementById('new-user-부서') as HTMLInputElement).value = '';
|
||||
(document.getElementById('new-user-팀') as HTMLInputElement).value = '';
|
||||
(document.getElementById('new-user-직위') as HTMLInputElement).value = '';
|
||||
(document.getElementById('new-user-이름') as HTMLInputElement).value = '';
|
||||
(document.getElementById('new-user-사용기간') as HTMLInputElement).value = '';
|
||||
(document.getElementById('new-user-신청서') as HTMLInputElement).value = '';
|
||||
document.getElementById('new-user-신청서명')!.innerText = '';
|
||||
} else {
|
||||
document.getElementById('sw-user-edit-modal-title')!.innerText = '사용자 수정';
|
||||
const u = tempSwUsers[idx];
|
||||
(document.getElementById('new-user-법인') as HTMLSelectElement).value = u.법인;
|
||||
(document.getElementById('new-user-부서') as HTMLInputElement).value = u.부서;
|
||||
(document.getElementById('new-user-팀') as HTMLInputElement).value = u.팀;
|
||||
(document.getElementById('new-user-직위') as HTMLInputElement).value = u.직위;
|
||||
(document.getElementById('new-user-이름') as HTMLInputElement).value = u.이름;
|
||||
(document.getElementById('new-user-사용기간') as HTMLInputElement).value = u.사용기간;
|
||||
(document.getElementById('new-user-신청서') as HTMLInputElement).value = '';
|
||||
document.getElementById('new-user-신청서명')!.innerText = u.신청서명 ? `📎${u.신청서명}` : '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 추가/수정 내용 저장 (임시 목록에 반영)
|
||||
*/
|
||||
function saveUserEdit() {
|
||||
const idx = parseInt((document.getElementById('edit-user-idx') as HTMLInputElement).value);
|
||||
const 법인 = (document.getElementById('new-user-법인') as HTMLSelectElement).value;
|
||||
const 부서 = (document.getElementById('new-user-부서') as HTMLInputElement).value;
|
||||
const 팀 = (document.getElementById('new-user-팀') as HTMLInputElement).value;
|
||||
const 직위 = (document.getElementById('new-user-직위') as HTMLInputElement).value;
|
||||
const 이름 = (document.getElementById('new-user-이름') as HTMLInputElement).value.trim();
|
||||
const 사용기간 = (document.getElementById('new-user-사용기간') as HTMLInputElement).value;
|
||||
|
||||
const fileInput = document.getElementById('new-user-신청서') as HTMLInputElement;
|
||||
let 신청서명 = '';
|
||||
if (fileInput.files && fileInput.files.length > 0) {
|
||||
신청서명 = fileInput.files[0].name;
|
||||
} else if (idx !== -1) {
|
||||
신청서명 = tempSwUsers[idx].신청서명;
|
||||
}
|
||||
|
||||
if (!이름) { alert('이름을 입력해주세요.'); return; }
|
||||
|
||||
if (idx === -1) {
|
||||
tempSwUsers.push({
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
swId: currentSwUserAssetId,
|
||||
법인, 부서, 팀, 직위, 이름, 사용기간, 신청서명
|
||||
});
|
||||
} else {
|
||||
tempSwUsers[idx] = { ...tempSwUsers[idx], 법인, 부서, 팀, 직위, 이름, 사용기간, 신청서명 };
|
||||
}
|
||||
|
||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
||||
renderUserList();
|
||||
}
|
||||
108
src/components/Modal/StorageModal.ts
Normal file
108
src/components/Modal/StorageModal.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { state } from '../../state';
|
||||
import { HardwareAsset } from '../../excelHandler';
|
||||
import { openModal } from './BaseModal';
|
||||
|
||||
/**
|
||||
* 스토리지 모달 초기화 및 로직 제어
|
||||
*/
|
||||
export function initStorageModal(renderContent: () => void, closeModals: () => void) {
|
||||
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
|
||||
const btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement;
|
||||
const btnDeleteStorage = document.getElementById('btn-delete-storage-asset') as HTMLButtonElement;
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
btnSaveStorage?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!storageForm.checkValidity()) { storageForm.reportValidity(); return; }
|
||||
|
||||
const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value;
|
||||
const fileInput = document.getElementById('storage-품의서') as HTMLInputElement;
|
||||
const 품의서명 = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('storage-품의서명') as HTMLElement).innerText.replace('📎', '');
|
||||
|
||||
const newAsset: HardwareAsset = {
|
||||
id: id || Math.random().toString(36).substring(2, 9),
|
||||
type: '스토리지',
|
||||
법인: (document.getElementById('storage-법인') as HTMLSelectElement).value,
|
||||
storage유형: (document.getElementById('storage-유형') as HTMLSelectElement).value,
|
||||
자산코드: (document.getElementById('storage-자산코드') as HTMLInputElement).value,
|
||||
명칭: (document.getElementById('storage-명칭') as HTMLInputElement).value,
|
||||
위치: (document.getElementById('storage-위치') as HTMLInputElement).value,
|
||||
관리자: '',
|
||||
IP주소: (document.getElementById('storage-IP주소') as HTMLInputElement).value,
|
||||
MACaddress: (document.getElementById('storage-MAC주소') as HTMLInputElement).value,
|
||||
HW사양: '',
|
||||
OS: '',
|
||||
모델명: (document.getElementById('storage-모델명') as HTMLInputElement).value,
|
||||
용량: (document.getElementById('storage-용량') as HTMLInputElement).value,
|
||||
담당자_정: (document.getElementById('storage-담당자_정') as HTMLInputElement).value,
|
||||
담당자_부: (document.getElementById('storage-담당자_부') as HTMLInputElement).value,
|
||||
구매일: (document.getElementById('storage-구매일') as HTMLInputElement).value,
|
||||
금액: (document.getElementById('storage-금액') as HTMLInputElement).value,
|
||||
납품업체: (document.getElementById('storage-납품업체') as HTMLInputElement).value,
|
||||
품의서명
|
||||
};
|
||||
|
||||
if (id) {
|
||||
const idx = state.masterData.hw.findIndex(a => a.id === id);
|
||||
if(idx !== -1) state.masterData.hw[idx] = newAsset;
|
||||
} else {
|
||||
state.masterData.hw.push(newAsset);
|
||||
}
|
||||
|
||||
closeModals();
|
||||
renderContent();
|
||||
});
|
||||
|
||||
// 삭제 버튼 이벤트
|
||||
btnDeleteStorage?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value;
|
||||
if (confirm('삭제하시겠습니까?')) {
|
||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
|
||||
closeModals();
|
||||
renderContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 스토리지 상세 모달 열기
|
||||
* @param asset 수정 시 자산 데이터, 신규 시 undefined
|
||||
*/
|
||||
export function openStorageModal(asset?: HardwareAsset) {
|
||||
const storageModal = document.getElementById('storage-asset-modal') as HTMLDivElement;
|
||||
const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement;
|
||||
const deleteBtn = document.getElementById('btn-delete-storage-asset')!;
|
||||
|
||||
openModal('storage-asset-modal');
|
||||
storageForm.reset();
|
||||
|
||||
if (asset) {
|
||||
document.getElementById('storage-modal-title')!.textContent = '스토리지 상세 정보 수정';
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
(document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id;
|
||||
(document.getElementById('storage-법인') as HTMLSelectElement).value = asset.법인;
|
||||
(document.getElementById('storage-유형') as HTMLSelectElement).value = asset.storage유형 || 'NAS';
|
||||
(document.getElementById('storage-자산코드') as HTMLInputElement).value = asset.자산코드;
|
||||
(document.getElementById('storage-명칭') as HTMLInputElement).value = asset.명칭;
|
||||
(document.getElementById('storage-위치') as HTMLInputElement).value = asset.위치 || '';
|
||||
(document.getElementById('storage-모델명') as HTMLInputElement).value = asset.모델명 || '';
|
||||
(document.getElementById('storage-용량') as HTMLInputElement).value = asset.용량 || '';
|
||||
(document.getElementById('storage-담당자_정') as HTMLInputElement).value = asset.담당자_정 || '';
|
||||
(document.getElementById('storage-담당자_부') as HTMLInputElement).value = asset.담당자_부 || '';
|
||||
(document.getElementById('storage-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
|
||||
(document.getElementById('storage-MAC주소') as HTMLInputElement).value = asset.MACaddress || '';
|
||||
(document.getElementById('storage-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||
(document.getElementById('storage-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : '';
|
||||
(document.getElementById('storage-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||
(document.getElementById('storage-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : '';
|
||||
} else {
|
||||
document.getElementById('storage-modal-title')!.textContent = '새 스토리지 자산 추가';
|
||||
deleteBtn.style.display = 'none';
|
||||
(document.getElementById('storage-asset-id') as HTMLInputElement).value = '';
|
||||
(document.getElementById('storage-법인') as HTMLSelectElement).value = '한맥';
|
||||
(document.getElementById('storage-유형') as HTMLSelectElement).value = 'NAS';
|
||||
(document.getElementById('storage-품의서명') as HTMLElement).innerText = '';
|
||||
}
|
||||
}
|
||||
37
src/components/Sidebar.ts
Normal file
37
src/components/Sidebar.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { state } from '../state';
|
||||
|
||||
export function initSidebar(renderContent: () => void) {
|
||||
const navItems = document.querySelectorAll('.nav-list li');
|
||||
const titleElement = document.getElementById('current-tab-title') as HTMLHeadingElement;
|
||||
const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement;
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
// 탭 UI 업데이트
|
||||
navItems.forEach(nav => nav.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// 상태 업데이트
|
||||
state.activeCategory = item.getAttribute('data-category') as 'hw' | 'sw';
|
||||
state.activeSubTab = item.getAttribute('data-tab') || '대시보드';
|
||||
|
||||
// 타이틀 업데이트 (Deep Green 포인트 컬러 유지)
|
||||
const catName = state.activeCategory === 'hw' ? '하드웨어' : '소프트웨어';
|
||||
if (titleElement) {
|
||||
titleElement.textContent = `${catName} / ${state.activeSubTab}`;
|
||||
}
|
||||
|
||||
// 추가 버튼 노출 여부 (대시보드에서는 숨김)
|
||||
if (btnAddAsset) {
|
||||
if (state.activeSubTab === '대시보드') {
|
||||
btnAddAsset.classList.add('hidden');
|
||||
} else {
|
||||
btnAddAsset.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 화면 리렌더링
|
||||
renderContent();
|
||||
});
|
||||
});
|
||||
}
|
||||
1230
src/main.ts
1230
src/main.ts
File diff suppressed because it is too large
Load Diff
23
src/state.ts
Normal file
23
src/state.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { MasterAssetData } from './excelHandler';
|
||||
import { generateDummyData } from './dummyDataGenerator';
|
||||
|
||||
// --- State Definitions ---
|
||||
export interface AppState {
|
||||
masterData: MasterAssetData;
|
||||
activeCategory: 'hw' | 'sw';
|
||||
activeSubTab: string;
|
||||
activeCharts: any[];
|
||||
}
|
||||
|
||||
// --- Initial State ---
|
||||
export const state: AppState = {
|
||||
masterData: generateDummyData(),
|
||||
activeCategory: 'hw',
|
||||
activeSubTab: '대시보드',
|
||||
activeCharts: []
|
||||
};
|
||||
|
||||
// --- State Helpers ---
|
||||
export function updateState(newState: Partial<AppState>) {
|
||||
Object.assign(state, newState);
|
||||
}
|
||||
95
src/views/AssetTableView.ts
Normal file
95
src/views/AssetTableView.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { state } from '../state';
|
||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 } from 'lucide';
|
||||
import { openPcModal } from '../components/Modal/PCModal';
|
||||
import { openHwModal } from '../components/Modal/HWModal';
|
||||
import { openStorageModal } from '../components/Modal/StorageModal';
|
||||
import { openSwModal } from '../components/Modal/SWModal';
|
||||
import { openSwUserModal } from '../components/Modal/SWUserModal';
|
||||
|
||||
/**
|
||||
* 자산 목록 테이블 렌더링 메인 함수
|
||||
*/
|
||||
export function renderTable(mainContent: HTMLElement) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'table-container';
|
||||
const table = document.createElement('table');
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
renderHwTable(table, container, mainContent);
|
||||
} else {
|
||||
renderSwTable(table, container, mainContent);
|
||||
}
|
||||
|
||||
// 테이블 내 아이콘 초기화
|
||||
createIcons({
|
||||
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 }
|
||||
});
|
||||
}
|
||||
|
||||
function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) {
|
||||
const list = state.masterData.hw.filter(a => a.type === state.activeSubTab);
|
||||
|
||||
if (state.activeSubTab === '개인PC') {
|
||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산코드</th><th>사용자</th><th>위치</th><th>CPU</th><th>GPU</th><th>RAM</th><th>SSD1</th><th>SSD2</th><th>HDD1</th><th>HDD2</th><th>구매일</th><th>금액</th><th>납품업체</th><th>품의서</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="17">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||
list.forEach((asset, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.자산코드}</td><td>${asset.사용자||''}</td><td>${asset.위치||''}</td><td>${asset.CPU||''}</td><td>${asset.GPU||''}</td><td>${asset.RAM||''}</td><td>${asset.SSD1||'-'}</td><td>${asset.SSD2||'-'}</td><td>${asset.HDD1||'-'}</td><td>${asset.HDD2||'-'}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td>${asset.납품업체||''}</td><td style="text-align:center;">${asset.품의서명 ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
|
||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); });
|
||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openPcModal(asset));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else if (state.activeSubTab === '스토리지') {
|
||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>유형</th><th>자산코드</th><th>명칭</th><th>위치</th><th>모델명</th><th>용량</th><th>담당자(정)</th><th>IP주소</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="13">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||
list.forEach((asset, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.storage유형||''}</td><td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치||''}</td><td>${asset.모델명||''}</td><td>${asset.용량||''}</td><td>${asset.담당자_정||''}</td><td>${asset.IP주소||''}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
|
||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); });
|
||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openStorageModal(asset));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th>${state.activeSubTab === '전산비품' ? '<th>유형</th>' : ''}<th>자산코드</th><th>명칭</th><th>위치</th><th>관리자</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="10">등록된 자산이 없습니다.</td></tr>`; return; }
|
||||
list.forEach((asset, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td>${state.activeSubTab === '전산비품' ? `<td>${asset.비품유형||'-'}</td>` : ''}<td>${asset.자산코드}</td><td>${asset.명칭}</td><td>${asset.위치}</td><td>${asset.관리자}</td><td>${asset.구매일||''}</td><td>${asset.금액||''}</td><td><button class="btn-outline btn-edit">수정</button></td>`;
|
||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
|
||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openHwModal(asset));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) {
|
||||
const list = state.masterData.sw.filter(a => a.type === state.activeSubTab);
|
||||
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>제품명</th><th>구매일</th><th>수량</th><th>사용가능</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
|
||||
container.appendChild(table);
|
||||
mainContent.appendChild(container);
|
||||
const tbody = document.getElementById('dynamic-tbody')!;
|
||||
if (list.length === 0) { tbody.innerHTML = `<tr><td colspan="7">정보가 없습니다.</td></tr>`; return; }
|
||||
list.forEach((asset, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length;
|
||||
const avail = asset.수량 - assigned;
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.법인}</td><td>${asset.제품명}</td><td>${asset.구매일||''}</td><td>${asset.수량}</td><td><strong style="color: ${avail > 0 ? 'var(--primary)' : 'var(--danger)'}">${avail}</strong></td><td style="display:flex; gap:0.25rem;"><button class="btn-outline btn-edit">수정</button><button class="btn-outline btn-users"><i data-lucide="users" style="width:14px; height:14px;"></i></button></td>`;
|
||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); });
|
||||
tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset));
|
||||
tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
287
src/views/DashboardView.ts
Normal file
287
src/views/DashboardView.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { state } from '../state';
|
||||
import { HardwareAsset, SoftwareAsset } from '../excelHandler';
|
||||
|
||||
/**
|
||||
* 대시보드 렌더링 메인 함수
|
||||
*/
|
||||
export function renderDashboard(mainContent: HTMLElement) {
|
||||
mainContent.innerHTML = '';
|
||||
|
||||
// 기존 차트 리소스 해제
|
||||
state.activeCharts.forEach(c => c.destroy());
|
||||
state.activeCharts = [];
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
renderHwDashboard(mainContent);
|
||||
} else {
|
||||
renderSwDashboard(mainContent);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 하드웨어 대시보드 ---
|
||||
function renderHwDashboard(container: HTMLElement) {
|
||||
const types = ['개인PC', '서버', '스토리지', '전산비품'];
|
||||
const units = ['대', '대', '대', '개'];
|
||||
const groups: any = {};
|
||||
|
||||
types.forEach(t => { groups[t] = { idle: [], active: [], aged: [], normal: [] }; });
|
||||
|
||||
state.masterData.hw.forEach(a => {
|
||||
if (!groups[a.type]) return;
|
||||
if (isHwIdle(a)) groups[a.type].idle.push(a);
|
||||
else groups[a.type].active.push(a);
|
||||
|
||||
const ageY = getHwAgeYears(a);
|
||||
const isAged = a.type === '전산비품' ? ageY >= 3 : ageY >= 5;
|
||||
if (isAged) groups[a.type].aged.push(a);
|
||||
else groups[a.type].normal.push(a);
|
||||
});
|
||||
|
||||
// 사용현황 카드 생성
|
||||
let usageCards = '';
|
||||
types.forEach((t, i) => {
|
||||
const total = groups[t].idle.length + groups[t].active.length;
|
||||
const used = groups[t].active.length;
|
||||
const per = total > 0 ? Math.round((used / total) * 100) : 0;
|
||||
const barColor = per >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)';
|
||||
|
||||
usageCards += `
|
||||
<div class="dashboard-card" data-action="idle" data-type="${t}" style="padding: 1.25rem 1.5rem; cursor:pointer;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 0.5rem;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">${t} 사용현황</span>
|
||||
</div>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">
|
||||
${total}${units[i]} 중 ${used}${units[i]} 사용 중 · 유휴 ${groups[t].idle.length}${units[i]}
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-end; margin-bottom: 0.5rem;">
|
||||
<div style="font-size: 2rem; font-weight:700; color:${barColor}; line-height:1;">${per}%</div>
|
||||
<div style="font-size:0.75rem; color:${per >= 50 ? '#4a8220' : 'var(--dash-danger)'}; background:${per >= 50 ? 'var(--dash-light)' : '#fef2f2'}; padding:4px 8px; border-radius:4px; font-weight:500;">
|
||||
${per >= 50 ? '잘 사용하고 있어요' : '점검이 필요합니다'}
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:100%; height:4px; background-color:var(--border-color); border-radius:2px; overflow:hidden; margin-top:0.25rem;">
|
||||
<div style="width:${per}%; height:100%; background-color:${barColor};"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// 노후화 카드 생성
|
||||
let agedCards = '';
|
||||
types.forEach((t, i) => {
|
||||
const total = groups[t].aged.length + groups[t].normal.length;
|
||||
const agedCount = groups[t].aged.length;
|
||||
const agedPer = total > 0 ? Math.round((agedCount / total) * 100) : 0;
|
||||
const threshold = t === '전산비품' ? '3년' : '5년';
|
||||
|
||||
agedCards += `
|
||||
<div class="dashboard-card" data-action="aged" data-type="${t}" style="padding: 1.25rem 1.5rem; flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer;">
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; align-items:center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">${t} 노후화 현황</span>
|
||||
<span style="font-size:0.75rem; color:#bfbfbf; background:#f9f9f9; padding:2px 6px; border-radius:4px;">${threshold} 초과</span>
|
||||
</div>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1.25rem;">
|
||||
전체 ${total}${units[i]} 중 ${agedCount}${units[i]} 노후 장비
|
||||
</div>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${agedCount > 0 ? 'var(--dash-danger)' : 'var(--text-main)'};">${agedCount}${units[i]}</div>
|
||||
</div>
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${agedPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 64px; height: 64px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 1rem; color:var(--text-muted); font-weight:600;">${agedPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
container.innerHTML = `
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; color: var(--text-main);">사용현황</h3>
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">${usageCards}</div>
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; color: var(--text-main);">노후화 자산 비율</h3>
|
||||
<div class="dashboard-layout-2col">${agedCards}</div>
|
||||
`;
|
||||
|
||||
// 클릭 이벤트 바인딩
|
||||
container.querySelectorAll('[data-action="idle"]').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const t = card.getAttribute('data-type')!;
|
||||
openDashboardDetail(`[${t}] 유휴 자산 목록`, groups[t].idle);
|
||||
});
|
||||
});
|
||||
container.querySelectorAll('[data-action="aged"]').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const t = card.getAttribute('data-type')!;
|
||||
openDashboardDetail(`[${t}] 노후 장비 목록`, groups[t].aged);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- 소프트웨어 대시보드 ---
|
||||
function renderSwDashboard(container: HTMLElement) {
|
||||
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
|
||||
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
|
||||
|
||||
state.masterData.sw.forEach(sw => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
||||
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
||||
if (sw.type === '구독SW') {
|
||||
subQty += qty; subUsed += assigned; subTotal++;
|
||||
if (isSWExpiring(sw)) subExp++;
|
||||
} else {
|
||||
permQty += qty; permUsed += assigned; permTotal++;
|
||||
if (isSWExpiring(sw)) permExp++;
|
||||
}
|
||||
});
|
||||
|
||||
const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0;
|
||||
const permPer = permQty > 0 ? Math.round((permUsed/permQty)*100) : 0;
|
||||
const subExpPer = subTotal > 0 ? Math.round((subExp/subTotal)*100) : 0;
|
||||
const permExpPer = permTotal > 0 ? Math.round((permExp/permTotal)*100) : 0;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||
<div class="dashboard-card" data-action="sub-usage" style="padding: 1.25rem 1.5rem; cursor:pointer;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 소프트웨어 사용정보</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">${subQty}개의 제품 중 ${subUsed}개 사용 중</div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-end;">
|
||||
<div style="font-size: 2rem; font-weight:700; color:${subPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};">${subPer}%</div>
|
||||
</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${subPer}%; height: 100%; background-color: ${subPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" data-action="perm-usage" style="padding: 1.25rem 1.5rem; cursor:pointer;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">영구 소프트웨어 사용정보</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">${permQty}개의 제품 중 ${permUsed}개 사용 중</div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-end;">
|
||||
<div style="font-size: 2rem; font-weight:700; color:${permPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};">${permPer}%</div>
|
||||
</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${permPer}%; height: 100%; background-color: ${permPer >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'};"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-layout-2col">
|
||||
<div class="dashboard-card" data-action="sub-exp" style="padding: 1.25rem 1.5rem; cursor:pointer; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted);">${subExp}개 만료 예정</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0);"></div>
|
||||
</div>
|
||||
<div class="dashboard-card" data-action="perm-exp" style="padding: 1.25rem 1.5rem; cursor:pointer; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted);">${permExp}개 만료 예정</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0);"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 클릭 이벤트 바인딩
|
||||
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => {
|
||||
openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW'));
|
||||
});
|
||||
container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => {
|
||||
openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '영구SW'));
|
||||
});
|
||||
container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => {
|
||||
openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '구독SW' && isSWExpiring(sw)));
|
||||
});
|
||||
container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => {
|
||||
openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '영구SW' && isSWExpiring(sw)));
|
||||
});
|
||||
}
|
||||
|
||||
// --- 공통 헬퍼 함수들 ---
|
||||
function isHwIdle(a: HardwareAsset) {
|
||||
if (a.type === '개인PC') return !a.사용자 || a.사용자.trim() === '' || a.사용자.trim() === '-';
|
||||
if (a.type === '스토리지') return !a.담당자_정 || a.담당자_정.trim() === '' || a.담당자_정.trim() === '-';
|
||||
return !a.관리자 || a.관리자.trim() === '' || a.관리자.trim() === '-';
|
||||
}
|
||||
|
||||
function getHwAgeYears(a: HardwareAsset) {
|
||||
if (!a.구매일) return 0;
|
||||
return (Date.now() - new Date(a.구매일).getTime()) / (1000 * 60 * 60 * 24 * 365.25);
|
||||
}
|
||||
|
||||
function isSWExpiring(sw: SoftwareAsset) {
|
||||
if (sw.type === '구독SW' && sw.구독일) {
|
||||
const parts = sw.구독일.split('~');
|
||||
if (parts.length > 1) {
|
||||
const endStr = parts[1].trim();
|
||||
const endMs = new Date(endStr.replace(/\./g, '-')).getTime();
|
||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||
return diffDays >= 0 && diffDays <= 30;
|
||||
}
|
||||
} else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('유지보수: ~')) {
|
||||
const endStr = sw.비고.split('~')[1].trim();
|
||||
const endMs = new Date(endStr.replace(/\./g, '-')).getTime();
|
||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||
return diffDays >= 0 && diffDays <= 30;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 대시보드 상세 모달 제어 (main.ts의 함수 재사용 또는 이동 필요) ---
|
||||
// 일단 main.ts에 있는 함수를 전역에서 가져와 쓸 수 없으므로, 여기서 직접 정의하거나 main.ts에서 export 해야 합니다.
|
||||
// 구조 개선을 위해 main.ts에서 이 함수들도 DashboardView로 옮기는 것이 좋습니다.
|
||||
|
||||
function openDashboardDetail(title: string, list: HardwareAsset[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
|
||||
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
|
||||
const thead = tbody.closest('table')!.querySelector('thead')!;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>자산코드</th><th>명칭/모델</th><th>위치</th><th>담당/사용자</th><th>구매일</th><th>금액</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
if (list.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;">해당 조건의 자산이 없습니다.</td></tr>`;
|
||||
} else {
|
||||
list.forEach((asset, idx) => {
|
||||
let manager = asset.관리자 || asset.사용자 || asset.담당자_정 || '-';
|
||||
let name = asset.명칭 || asset.모델명 || '-';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.type}</td><td>${asset.자산코드}</td><td>${name}</td><td>${asset.위치||'-'}</td><td>${manager}</td><td>${asset.구매일||'-'}</td><td>${asset.금액||'-'}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function openSwDashboardDetail(title: string, list: SoftwareAsset[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
|
||||
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
|
||||
const thead = tbody.closest('table')!.querySelector('thead')!;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>법인</th><th>제품명</th><th>수량</th><th>금액</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.type}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${sw.수량}</td><td>${sw.금액}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal') as HTMLDivElement;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title') as HTMLHeadingElement;
|
||||
const tbody = document.getElementById('dashboard-detail-tbody') as HTMLTableSectionElement;
|
||||
const thead = tbody.closest('table')!.querySelector('thead')!;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
||||
const avail = (typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10)) - assigned;
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${sw.수량}</td><td>${assigned}</td><td>${avail}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
Reference in New Issue
Block a user