diff --git a/branch_diff_issues.md b/branch_diff_issues.md deleted file mode 100644 index 6fa8ef1..0000000 --- a/branch_diff_issues.md +++ /dev/null @@ -1,58 +0,0 @@ -# ITAM 프로젝트 브랜치별 변경 사항 및 이슈 정리 - -본 문서는 `setting` 브랜치를 기준으로 `SW_Table`, `Operation_Table`, `Upload` 브랜치의 주요 변경 사항을 정리한 내용입니다. Gitea 이슈 등록 시 참고하시기 바랍니다. - ---- - -## 1. [SW_Table] 소프트웨어 자산 관리 고도화 및 대시보드 강화 - -**제목:** `[SW_Table] 소프트웨어 자산 관리 고도화 및 대시보드 강화` - -### 주요 변경 사항 -- **SW 상세 모달 리팩토링** - - 구독형, 영구형, 클라우드 자산 유형에 따른 동적 필드 전환 기능 구현 - - 자산 유형 명칭 일원화 및 필드 매핑 로직 개선 -- **데이터 표준화 및 확장** - - 시작일/만료일 'yyyy-mm-dd' 형식 통일 및 데이터 일관성 확보 - - 클라우드 자산 통합 관리 및 관련 스키마 확장 -- **대시보드 분석 기능 강화** - - 월별 누적 비용 분석 그래프 도입 - - 카테고리별 자산 보유 현황 및 비용 통계 시각화 고도화 -- **사용자 관리 개선** - - SW 사용자 할당 및 이력 관리 기능 강화 (`SWUserModal` 도입) - ---- - -## 2. [Operation_Table] 운영 서비스 도메인 관리 모듈 및 UI 최적화 - -**제목:** `[Operation_Table] 운영 서비스 도메인 관리 모듈 및 UI 최적화` - -### 주요 변경 사항 -- **도메인 관리 모듈 신규 도입** - - `ops_domain_assets` 테이블 신규 생성 및 서버 API 연동 - - 유형(호스팅/SSL/도메인/네임서버), 법인, 서비스명, 관리도메인 등 상세 필드 관리 -- **도메인 전용 UI 구현** - - 도메인 전용 등록/수정 모달 및 리스트 뷰 인터페이스 구축 -- **사용자 경험(UX) 및 스타일 최적화** - - 상단바와 본문 사이 간격 조정 (여백 최적화) - - 전역 스타일 가이드에 맞춘 UI 레이아웃 정밀 조정 -- **기능 통합** - - SW_Table의 모든 고도화 사항을 포함하여 운영 환경에 맞춰 통합 완료 - ---- - -## 3. [Upload] 엑셀 대량 업로드 워크플로우 및 자산코드 자동 생성 - -**제목:** `[Upload] 엑셀 대량 업로드 워크플로우 및 자산코드 자동 생성` - -### 주요 변경 사항 -- **통합 엑셀 업로드 시스템** - - 9개 자산 카테고리(HW, SW, 클라우드, 도메인)를 아우르는 통합 엑셀 양식 파싱 엔진 구현 -- **업로드 데이터 검토 프로세스 (`UploadPreviewModal`)** - - 업로드 전 데이터를 미리 확인하고 검증할 수 있는 중간 검토 모달 도입 - - 시트별 데이터 요약 및 상세 내역 확인 기능 제공 -- **자산코드 일괄 생성 기능** - - 하드웨어 자산 대상, 카테고리별 접두사 및 구매연월 기반 자동 번호 부여 시스템 구축 - - 서버 API와 연동된 중복 방지 및 자동 증분 로직 적용 -- **안정성 및 네트워크 최적화** - - 대량 데이터 처리를 위한 배치 저장 API(Port 3000) 연동 및 절대 경로 통신 적용 diff --git a/src/core/excelHandler.ts b/src/core/excelHandler.ts index 8598da0..8611246 100644 --- a/src/core/excelHandler.ts +++ b/src/core/excelHandler.ts @@ -128,6 +128,23 @@ export function exportToExcel(masterData: MasterAssetData) { XLSX.writeFile(wb, `itam_master_${new Date().toISOString().split('T')[0]}.xlsx`); } +/** + * 엑셀 날짜 데이터(숫자 또는 문자열)를 YYYY-MM-DD 형식의 문자열로 변환 + */ +function formatExcelDate(val: any): string { + if (!val) return ''; + if (typeof val === 'number') { + // 엑셀 날짜 숫자 (1899-12-30 기준 일수) + const date = new Date(Math.round((val - 25569) * 86400 * 1000)); + return date.toISOString().split('T')[0]; + } + // 이미 문자열인 경우 기호 통일 (YYYY.MM.DD -> YYYY-MM-DD) + if (typeof val === 'string') { + return val.replace(/\./g, '-').trim(); + } + return String(val); +} + export async function parseExcel(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -143,23 +160,23 @@ export async function parseExcel(file: File): Promise { rows.forEach(r => { const common = { id: Math.random().toString(36).substring(2, 9) }; if (sheetName === '개인PC') { - list.push({ ...common, type: '개인PC', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 사용자: r['사용자']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 메인보드: r['메인보드']||'', CPU: r['CPU']||'', GPU: r['GPU']||'', RAM: r['RAM']||'', SSD1: r['SSD1']||'', SSD2: r['SSD2']||'', HDD1: r['HDD1']||'', HDD2: r['HDD2']||'', IP주소: r['IP주소']||'', HW사양: r['HW사양']||'', 구매연월: r['구매연월']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); + list.push({ ...common, type: '개인PC', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 사용자: r['사용자']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 메인보드: r['메인보드']||'', CPU: r['CPU']||'', GPU: r['GPU']||'', RAM: r['RAM']||'', SSD1: r['SSD1']||'', SSD2: r['SSD2']||'', HDD1: r['HDD1']||'', HDD2: r['HDD2']||'', IP주소: r['IP주소']||'', HW사양: r['HW사양']||'', 구매연월: formatExcelDate(r['구매연월']), 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); } else if (sheetName === '서버') { - list.push({ ...common, type: '서버', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 구매연월: r['구매연월']||'', storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소 1']||'', IP2: r['IP 주소 2']||'', 원격접속: r['원격도구']||'', 서버ID: r['서버 ID']||'', 서버PW: r['서버 PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'', SSD1: r['Storage 1']||'', SSD2: r['Storage 2']||'', HDD1: r['Storage 3']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'' }); + list.push({ ...common, type: '서버', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 구매연월: formatExcelDate(r['구매연월']), storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소 1']||'', IP2: r['IP 주소 2']||'', 원격접속: r['원격도구']||'', 서버ID: r['서버 ID']||'', 서버PW: r['서버 PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'', SSD1: r['Storage 1']||'', SSD2: r['Storage 2']||'', HDD1: r['Storage 3']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'' }); } else if (sheetName === '스토리지') { - list.push({ ...common, type: '스토리지', 법인: r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'', 구매연월: r['구매연월']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); + list.push({ ...common, type: '스토리지', 법인: r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'', 구매연월: formatExcelDate(r['구매연월']), 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); } else if (sheetName === '전산비품') { - list.push({ ...common, type: '전산비품', 법인: r['법인']||'', 비품유형: r['비품유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'', 구매연월: r['구매연월']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); + list.push({ ...common, type: '전산비품', 법인: r['법인']||'', 비품유형: r['비품유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'', 구매연월: formatExcelDate(r['구매연월']), 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); } else if (sheetName === '모바일기기') { - list.push({ ...common, type: '모바일기기', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', 기기유형: r['기기유형']||'', OS: r['OS']||'', 구매연월: r['구매연월']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); + list.push({ ...common, type: '모바일기기', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', 기기유형: r['기기유형']||'', OS: r['OS']||'', 구매연월: formatExcelDate(r['구매연월']), 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); } else if (sheetName === '구독SW') { - list.push({ ...common, type: '구독SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 시작일: r['시작일']||'', 만료일: r['만료일']||'', 라이선스유형: r['라이선스유형']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }); + list.push({ ...common, type: '구독SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: formatExcelDate(r['구매일']), 시작일: formatExcelDate(r['시작일']), 만료일: formatExcelDate(r['만료일']), 라이선스유형: r['라이선스유형']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }); } else if (sheetName === '영구SW') { - list.push({ ...common, type: '영구SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 시작일: r['시작일']||'', 만료일: r['만료일']||'', 라이선스키: r['라이선스키']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }); + list.push({ ...common, type: '영구SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: formatExcelDate(r['구매일']), 시작일: formatExcelDate(r['시작일']), 만료일: formatExcelDate(r['만료일']), 라이선스키: r['라이선스키']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }); } else if (sheetName === '클라우드') { list.push({ ...common, type: '클라우드', 플랫폼명: r['플랫폼명']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 계정명: r['계정명']||'', 결제수단: r['결제수단']||'', 결제일: r['결제일']||'', 연결카드번호: r['연결카드번호']||'', 당월청구액: r['당월청구액']||'', 비고: r['비고']||'' }); } else if (sheetName === '도메인') { - list.push({ ...common, type: r['유형']||'도메인', corp: r['법인']||'', service_name: r['서비스명']||'', domain_name: r['관리도메인']||'', start_date: r['시작일']||'', expiry_date: r['만료일']||'', price: r['금액']||'', manager_main: r['담당자']||'', manager_sub: r['담당자(부)']||'', remarks: r['비고']||'' }); + list.push({ ...common, type: r['유형']||'도메인', corp: r['법인']||'', service_name: r['서비스명']||'', domain_name: r['관리도메인']||'', start_date: formatExcelDate(r['시작일']), expiry_date: formatExcelDate(r['만료일']), price: r['금액']||'', manager_main: r['담당자']||'', manager_sub: r['담당자(부)']||'', remarks: r['비고']||'' }); } }); if (list.length > 0) parsedData[sheetName] = list; diff --git a/src/core/state.ts b/src/core/state.ts index dcf807c..f51f87a 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -28,7 +28,7 @@ export interface AppState { // 초기 상태 export const state: AppState = { - activeCategory: 'dashboard', + activeCategory: 'hw', activeSubTab: '대시보드', masterData: { pc: [], diff --git a/src/views/List/SwListView.ts b/src/views/List/SwListView.ts index 0243082..49d93ed 100644 --- a/src/views/List/SwListView.ts +++ b/src/views/List/SwListView.ts @@ -88,7 +88,8 @@ export function renderSwList(container: HTMLElement) { } filtered.forEach((asset, idx) => { - const assigned = state.masterData.swUsers.filter(u => u.sw_id === asset.id).length; + const mapping = state.masterData.swUsers.find(u => u.sw_id === asset.id); + const assigned = mapping ? (mapping.userData || []).length : 0; const qty = typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10); const avail = qty - assigned;