Merge latest code from main into server_dashboard and resolve conflicts

This commit is contained in:
2026-04-23 09:35:53 +09:00
30 changed files with 7014 additions and 2186 deletions

View File

@@ -20,14 +20,20 @@ function randUser() { // 25% 확률로 유휴자산 할당
}
export function generateDummyData(): MasterAssetData {
const hw: HardwareAsset[] = [];
const sw: SoftwareAsset[] = [];
const swUsers: SWUser[] = [];
const pc: HardwareAsset[] = [];
const server: HardwareAsset[] = [];
const storage: HardwareAsset[] = [];
const equip: HardwareAsset[] = [];
const mobile: HardwareAsset[] = [];
const subSw: SoftwareAsset[] = [];
const permSw: SoftwareAsset[] = [];
const swUsers: any[] = [];
const logs: any[] = [];
// 1. 개인PC 50개
for (let i = 1; i <= 50; i++) {
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
hw.push({
const purchaseYear = Math.floor(Math.random() * 10) + 2017;
pc.push({
id: Math.random().toString(36).substring(2, 9),
type: '개인PC',
법인: rand(corps),
@@ -52,8 +58,8 @@ export function generateDummyData(): MasterAssetData {
// 2. 서버 20개
for (let i = 1; i <= 20; i++) {
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
hw.push({
const purchaseYear = Math.floor(Math.random() * 10) + 2017;
server.push({
id: Math.random().toString(36).substring(2, 9),
type: '서버',
법인: rand(corps),
@@ -86,10 +92,10 @@ export function generateDummyData(): MasterAssetData {
});
}
// 3. 스토리지 20개
for (let i = 1; i <= 20; i++) {
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
hw.push({
// 3. 스토리지 10개
for (let i = 1; i <= 10; i++) {
const purchaseYear = Math.floor(Math.random() * 10) + 2017;
storage.push({
id: Math.random().toString(36).substring(2, 9),
type: '스토리지',
법인: rand(corps),
@@ -111,122 +117,84 @@ export function generateDummyData(): MasterAssetData {
});
}
// 4. 전산비품 (노트북, 태블릿, 휴대폰 각각 5개씩)
const equips = [
{ type: '노트북', code: 'NB', name: 'LG 그램 16인치', price: '1,800,000' },
{ type: '태블릿', code: 'TB', name: '아이패드 프로 12.9', price: '1,500,000' },
{ type: '휴대폰', code: 'PH', name: '갤럭시 S24', price: '1,200,000' }
];
equips.forEach((eq) => {
for (let i = 1; i <= 5; i++) {
const purchaseYear = Math.floor(Math.random() * 8) + 2019; // 2019~2026
hw.push({
id: Math.random().toString(36).substring(2, 9),
type: '전산비품',
법인: rand(corps),
비품유형: eq.type,
: `HM-${eq.code}-${purchaseYear}-${String(i).padStart(3, '0')}`,
명칭: eq.name,
위치: rand(['본사', '지사']),
관리자: randUser(),
구매일: randDate(purchaseYear, purchaseYear),
금액: eq.price,
: '브랜드 총판',
: '',
IP주소: '', MACaddress: '', OS: '', HW사양: ''
});
}
});
// 4. 전산비품 15개
for (let i = 1; i <= 15; i++) {
const purchaseYear = Math.floor(Math.random() * 8) + 2019;
equip.push({
id: Math.random().toString(36).substring(2, 9),
type: '전산비품',
법인: rand(corps),
비품유형: rand(['프린터', '모니터', 'UPS']),
: `HM-EQ-${purchaseYear}-${String(i).padStart(3, '0')}`,
: `비품 #${i}`,
위치: rand(['본사', '지사']),
관리자: randUser(),
구매일: randDate(purchaseYear, purchaseYear),
: '300,000',
: '오피스공구',
: '',
IP주소: '', MACaddress: '', OS: '', HW사양: ''
});
}
// 5. 구독형 S/W 40개
for (let i = 1; i <= 40; i++) {
// 5. 모바일기기 10개
for (let i = 1; i <= 10; i++) {
const purchaseYear = Math.floor(Math.random() * 5) + 2022;
mobile.push({
id: Math.random().toString(36).substring(2, 9),
type: '모바일기기',
법인: rand(corps),
: `HM-MO-${purchaseYear}-${String(i).padStart(3, '0')}`,
명칭: rand(['아이폰 15', '갤럭시 S24', '아이패드 에어']),
: '개인 지급',
관리자: randUser(),
OS: rand(['iOS', 'Android', 'iPadOS']),
구매일: randDate(purchaseYear, purchaseYear),
: '1,200,000',
: '통신사',
: '',
IP주소: '', MACaddress: '', HW사양: '', : ''
});
}
// 6. 구독 SW 20개
for (let i = 1; i <= 20; i++) {
const swId = Math.random().toString(36).substring(2, 9);
const purchaseYear = Math.random() < 0.3 ? 2026 : 2024;
let isExpiring = Math.random() < 0.25;
let endDt = new Date();
if (isExpiring) {
endDt.setDate(endDt.getDate() + Math.floor(Math.random() * 25) + 1); // 1~25일 뒤 만료
} else {
endDt.setMonth(endDt.getMonth() + Math.floor(Math.random() * 11) + 2); // 넉넉히 남음
}
const endStr = `${endDt.getFullYear()}.${String(endDt.getMonth()+1).padStart(2,'0')}.${String(endDt.getDate()).padStart(2,'0')}`;
sw.push({
subSw.push({
id: swId,
type: '구독SW',
분야: rand(['업무공통', '개발S/W', '디자인', '설계S/W']),
분야: rand(['업무공통', '개발S/W']),
법인: rand(corps),
부서: rand(depts),
제품명: rand(['Adobe CC All Apps', 'Microsoft 365', 'Slack Pro', 'Notion Team']),
: `${purchaseYear}-01-01`,
: `${purchaseYear}.01.01 ~ ${endStr}`,
금액: String(Math.floor(Math.random() * 100 + 10) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','),
수량: Math.floor(Math.random() * 5) + 3, // 3~7
: `user${i}@hm.com`,
제품명: rand(['Adobe CC', 'M365']),
: '2024-01-01',
: '2025-01-01',
: '100,000',
수량: 5,
: `admin${i}@hm.com`,
: '총판',
: '연간구독'
: ''
});
const assignCount = Math.floor(Math.random() * 2) + 1;
for (let j=0; j<assignCount; j++) {
swUsers.push({
id: Math.random().toString(36).substring(2, 9),
swId: swId,
법인: rand(corps),
부서: rand(depts),
: rand(['1팀', '2팀', '기획팀']),
직위: rand(['사원', '대리', '과장']),
이름: rand(users),
사용기간: '2024.01~12',
신청서명: ''
});
}
swUsers.push({ sw_id: swId, userData: [[rand(corps), rand(depts), '사원', rand(users), '2024.01~12', '신청완료']] });
}
// 6. 영구 S/W 40개
for (let i = 1; i <= 40; i++) {
// 7. 영구 SW 20개
for (let i = 1; i <= 20; i++) {
const swId = Math.random().toString(36).substring(2, 9);
let isExpiring = Math.random() < 0.25;
let endDt = new Date();
if (isExpiring) {
endDt.setDate(endDt.getDate() + Math.floor(Math.random() * 25) + 1); // 1~25일 뒤 만료
} else {
endDt.setMonth(endDt.getMonth() + Math.floor(Math.random() * 11) + 2); // 넉넉히 남음
}
const endStr = `${endDt.getFullYear()}.${String(endDt.getMonth()+1).padStart(2,'0')}.${String(endDt.getDate()).padStart(2,'0')}`;
sw.push({
permSw.push({
id: swId,
type: '영구SW',
분야: rand(['업무공통', '개발S/W', '디자인', '설계S/W']),
분야: rand(['설계S/W']),
법인: rand(corps),
부서: rand(depts),
제품명: rand(['AutoCAD 2024', 'Windows 10 Pro', '한컴오피스 2022', 'Visual Studio 2022']),
구매일: '2020-05-15',
유지보수여부: true,
비고: `유지보수: ~ ${endStr}`,
금액: '1,500,000',
수량: Math.floor(Math.random() * 3) + 2, // 2~4
계정명: `sn-2020-${i}`,
납품업체: '오토데스크 / MS'
제품명: rand(['AutoCAD', '한컴오피스']),
: '2023-01-01',
: `KEY-${swId}`,
: '500,000',
수량: 2,
: `license${i}`,
: '총판',
: ''
});
const assignCount = Math.floor(Math.random() * 2) + 1;
for (let j=0; j<assignCount; j++) {
swUsers.push({
id: Math.random().toString(36).substring(2, 9),
swId: swId,
법인: rand(corps),
부서: rand(depts),
: rand(['1팀', '2팀']),
직위: rand(['과장', '차장', '부장']),
이름: rand(users),
사용기간: '영구',
신청서명: ''
});
}
}
return { hw, sw, swUsers, logs: [] };
return { pc, server, storage, equip, mobile, subSw, permSw, swUsers, logs };
}

View File

@@ -1,390 +1,183 @@
import * as XLSX from 'xlsx';
export interface HardwareAsset {
id: string;
type: string; // '개인PC', '서버', '스토리지', '전산비품'
법인: string;
자산코드: string;
명칭: string;
위치: string;
관리자: string;
IP주소: string;
IP2?: string;
MACaddress: string;
HW사양: string;
OS: string;
사용자?: string;
CPU?: string;
GPU?: string;
RAM?: string;
SSD1?: string;
SSD2?: string;
HDD1?: string;
HDD2?: string;
storage유형?: string;
비품유형?: string;
모델명?: string;
용량?: string;
담당자_정?: string;
담당자_부?: string;
구매일?: string;
금액?: string;
납품업체: string;
품의서명: string;
용도?: string;
상세?: string;
원격접속?: string;
서버ID?: string;
서버PW?: string;
모니터링?: string;
비고?: string;
현사용조직?: string;
이전사용조직?: string;
}
export interface SoftwareAsset {
id: string;
type: string; // '구독SW', '영구SW'
분야?: string;
법인: string;
부서?: string;
제품명: string;
구매일: string;
구독일?: string;
유지보수여부?: boolean;
금액: string;
수량: number;
계정명: string;
납품업체: string;
비고: string;
}
export interface SWUser {
id: string;
swId: string;
법인: string;
부서: string;
: string;
직위: string;
이름: string;
사용기간: string;
신청서명: string;
}
export interface HardwareLog {
id: string;
assetId: string;
date: string;
details: string;
user: string;
}
export interface MasterAssetData {
hw: HardwareAsset[];
sw: SoftwareAsset[];
swUsers: SWUser[];
logs: HardwareLog[];
}
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
const SW_TABS = ['구독SW', '영구SW'];
// 확장된 헤더 (상세 페이지의 모든 필드 포함)
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고'];
const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '서버ID', '서버PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', '모니터링', '비고'];
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고'];
const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
const HISTORY_HEADERS = ['id', 'assetId', 'date', 'details', 'user'];
/**
* 템플릿 엑셀 다중 시트로 다운로드
*/
export function downloadTemplate() {
const wb = XLSX.utils.book_new();
HW_TABS.forEach(tab => {
let hd = HW_HEADERS;
let wscols: any[] = [];
if (tab === '개인PC') {
hd = PC_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[1] = {wch: 25}; // 자산코드
wscols[12] = {wch: 30}; // HW사양
} else if (tab === '서버') {
hd = SERVER_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[3] = {wch: 25}; // 용도
wscols[4] = {wch: 30}; // 상세내용
} else if (tab === '스토리지') {
hd = STORAGE_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[2] = {wch: 25}; // 자산코드
wscols[3] = {wch: 25}; // 명칭
} else {
hd = HW_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[2] = {wch: 25}; // 명칭
wscols[7] = {wch: 30}; // HW사양
}
const ws = XLSX.utils.aoa_to_sheet([hd]);
ws['!cols'] = wscols;
XLSX.utils.book_append_sheet(wb, ws, tab);
});
SW_TABS.forEach(tab => {
let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS;
const ws = XLSX.utils.aoa_to_sheet([hd]);
ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}];
XLSX.utils.book_append_sheet(wb, ws, tab);
});
const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS]);
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
const historyWs = XLSX.utils.aoa_to_sheet([HISTORY_HEADERS]);
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
XLSX.writeFile(wb, 'itam_assets_template.xlsx');
}
/**
* 마스터 데이터를 여러 시트로 쪼개서 내보내기
*/
export function exportToExcel(masterData: MasterAssetData) {
const wb = XLSX.utils.book_new();
HW_TABS.forEach(tab => {
const targetAssets = masterData.hw.filter(a => a.type === tab);
let wsData;
let colsConfig;
if (tab === '개인PC') {
wsData = [
PC_HEADERS,
...targetAssets.map(a => [
a., a., a., a., a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.IP주소, a.HW사양, a., a., a., a., a.
])
];
colsConfig = Array(PC_HEADERS.length).fill({wch: 15});
colsConfig[1] = {wch: 25};
colsConfig[12] = {wch: 30};
} else if (tab === '서버') {
wsData = [
SERVER_HEADERS,
...targetAssets.map(a => [
a., a., a.storage유형 || '물리', a. || '', a. || '', a. || '', a. || '', a., a._정 || '', a._부 || '',
a.IP주소, a. || '', a.ID || '', a.PW || '', a. || '', a.OS, a.CPU, a.RAM, a.GPU || '', a.SSD1 || '', a.SSD2 || '', a.HDD1 || '', a. || '', a. || ''
])
];
colsConfig = Array(SERVER_HEADERS.length).fill({wch: 15});
colsConfig[3] = {wch: 25};
colsConfig[4] = {wch: 30};
} else if (tab === '스토리지') {
wsData = [
STORAGE_HEADERS,
...targetAssets.map(a => [
a., a.storage유형, a., a., a., a., a., a._정, a._부, a.IP주소, a.MACaddress, a., a., a., a., a.
])
];
colsConfig = Array(STORAGE_HEADERS.length).fill({wch: 15});
colsConfig[2] = {wch: 25};
colsConfig[3] = {wch: 25};
} else {
wsData = [
HW_HEADERS,
...targetAssets.map(a => [
a., a., a., a., a., a.IP주소, a.MACaddress, a.HW사양, a.OS, a., a., a., a., a.
])
];
colsConfig = Array(HW_HEADERS.length).fill({wch: 15});
colsConfig[2] = {wch: 25};
colsConfig[7] = {wch: 30};
}
const ws = XLSX.utils.aoa_to_sheet(wsData);
ws['!cols'] = colsConfig;
XLSX.utils.book_append_sheet(wb, ws, tab);
});
SW_TABS.forEach(tab => {
const targetAssets = masterData.sw.filter(a => a.type === tab);
let wsData;
if (tab === '구독SW') {
wsData = [
SUB_SW_HEADERS,
...targetAssets.map(a => [a.id, a.||'', a., a.||'', a., a., a., a., a., a., a., a.])
];
} else {
wsData = [
PERM_SW_HEADERS,
...targetAssets.map(a => [a.id, a.||'', a., a.||'', a., a., a. ? 'Y' : 'N', a., a., a., a., a.])
];
}
const ws = XLSX.utils.aoa_to_sheet(wsData);
ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}];
XLSX.utils.book_append_sheet(wb, ws, tab);
});
const swUserWsData = [
SW_USER_HEADERS,
...masterData.swUsers.map(u => [u.id, u.swId, u., u., u., u., u., u., u.])
];
const swUserWs = XLSX.utils.aoa_to_sheet(swUserWsData);
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
const historyWsData = [
HISTORY_HEADERS,
...masterData.logs.map(l => [l.id, l.assetId, l.date, l.details, l.user])
];
const historyWs = XLSX.utils.aoa_to_sheet(historyWsData);
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
const dateStr = new Date().toISOString().split('T')[0];
XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`);
}
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog, MasterAssetData } from './state';
export async function parseExcel(file: File): Promise<MasterAssetData> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = e.target?.result;
const workbook = XLSX.read(data, { type: 'binary' });
const hwAssets: HardwareAsset[] = [];
const swAssets: SoftwareAsset[] = [];
const swUsers: SWUser[] = [];
const logs: HardwareLog[] = [];
const workbook = XLSX.read(e.target?.result, { type: 'binary' });
const data: MasterAssetData = {
pc: [],
server: [],
storage: [],
equip: [],
mobile: [],
subSw: [],
permSw: [],
swUsers: [],
logs: []
};
workbook.SheetNames.forEach(sheetName => {
const worksheet = workbook.Sheets[sheetName];
const json = XLSX.utils.sheet_to_json(worksheet) as any[];
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]) as any[];
if (HW_TABS.includes(sheetName)) {
json.forEach(row => {
if (sheetName === '개인PC') {
hwAssets.push({
id: Math.random().toString(36).substring(2, 9),
type: sheetName,
법인: row['법인'] || '',
자산코드: row['자산코드'] || '',
: '',
위치: row['위치'] || '',
사용자: row['사용자'] || '',
: '',
IP주소: row['IP주소'] || '',
MACaddress: '',
HW사양: row['HW사양'] || '',
OS: row['OS'] || '',
CPU: row['CPU'] || '', GPU: row['GPU'] || '', RAM: row['RAM'] || '',
SSD1: row['SSD1'] || '', SSD2: row['SSD2'] || '', HDD1: row['HDD1'] || '', HDD2: row['HDD2'] || '',
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
비고: row['비고'] || ''
});
} else if (sheetName === '서버') {
hwAssets.push({
id: Math.random().toString(36).substring(2, 9),
type: sheetName,
법인: row['법인'] || row['구매법인'] || '',
자산코드: row['자산번호'] || row['자산코드'] || '',
명칭: row['용도'] || row['명칭'] || '',
용도: row['용도'] || '',
상세: row['상세내용'] || row['상세'] || '',
현사용조직: row['현사용조직'] || row['현 사용조직'] || '',
이전사용조직: row['이전사용조직'] || row['이전 사용조직'] || '',
위치: row['설치위치'] || row['위치'] || '',
관리자: row['담당자(정)'] || '', 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
IP주소: row['IP 주소'] || row['IP주소'] || row['IP 주소 1'] || '',
원격접속: row['원격접속'] || row['원격도구'] || '',
서버ID: row['서버ID'] || row['서버 ID'] || '',
서버PW: row['서버PW'] || row['서버 PW'] || '',
모델명: row['모델명'] || '', OS: row['OS'] || '',
CPU: row['CPU'] || '', RAM: row['RAM'] || '', GPU: row['GPU'] || '',
SSD1: row['SSD1'] || row['Storage1'] || '',
SSD2: row['SSD2'] || row['Storage2'] || '',
HDD1: row['HDD1'] || row['Storage3'] || '',
모니터링: row['모니터링'] || '',
비고: row['비고'] || '',
storage유형: row['유형'] || '물리',
MACaddress: '', HW사양: '', 구매일: row['구매일자'] || row['구매일'] || '', : '', : '', : '',
});
} else if (sheetName === '스토리지') {
hwAssets.push({
id: Math.random().toString(36).substring(2, 9),
type: sheetName,
법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '',
: '', IP주소: row['IP주소'] || '', MACaddress: row['MAC주소'] || '', HW사양: '', OS: '',
storage유형: row['유형'] || '', 모델명: row['모델명'] || '', 용량: row['용량'] || '',
담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
비고: row['비고'] || ''
});
} else {
hwAssets.push({
id: Math.random().toString(36).substring(2, 9),
type: sheetName,
법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '',
관리자: row['관리자'] || '', IP주소: row['IP주소'] || '', MACaddress: row['MACaddress'] || '',
HW사양: row['HW사양'] || '', OS: row['OS'] || '',
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
비고: row['비고'] || ''
});
}
});
}
if (SW_TABS.includes(sheetName)) {
json.forEach(row => {
swAssets.push({
id: row['ID'] ? String(row['ID']) : Math.random().toString(36).substring(2, 9),
type: sheetName, 분야: row['분야'] || '', 법인: row['법인'] || '', 부서: row['부서'] || '', 제품명: row['제품명'] || '',
구매: row['구매일'] || '', 구독일: row['구독일'] || '', 유지보수여부: row['유지보수여부'] === 'Y' || row['유지보수여부'] === true,
금액: row['금액'] ? String(row['금액']) : '', 수량: parseInt(row['수량'] || '1', 10),
계정명: row['계정명'] || '', 납품업체: row['납품업체'] || '', 비고: row['비고'] || '',
});
});
}
if (sheetName === 'SW_사용자') {
json.forEach(row => {
swUsers.push({
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
swId: row['swId'] ? String(row['swId']) : '', 법인: row['법인'] || '', 부서: row['부서'] || '',
: row['팀'] || '', 직위: row['직위'] || '', 이름: row['이름'] || '',
사용기간: row['사용기간'] || '', 신청서명: row['신청서명'] || '',
});
});
}
if (sheetName === 'History') {
json.forEach(row => {
logs.push({
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
assetId: row['assetId'] ? String(row['assetId']) : '',
date: row['date'] || '', details: row['details'] || '', user: row['user'] || '',
});
});
if (sheetName === '개인PC') {
rows.forEach(r => data.pc.push({
id: Math.random().toString(36).substring(2, 9),
type: '개인PC',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || r['관리코드'] || '',
실사용조직: r['실사용조직'] || '',
모델명: r['모델명'] || '',
OS: r['OS'] || '',
CPU: r['CPU'] || '',
RAM: r['RAM'] || '',
SSD1: r['SSD1'] || '',
SSD2: r['SSD2'] || '',
HDD1: r['HDD1'] || '',
IP주소: r['IP주소'] || '',
HW사양: r['HW사양'] || '',
도입일: r['도입일'] || '',
구매가: r['구매가'] || r['금액'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '서버') {
rows.forEach(r => data.server.push({
id: Math.random().toString(36).substring(2, 9),
type: '서버',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || r['관리코드'] || '',
서버용도: r['서버용도'] || r['용도'] || '',
상태: r['상태'] || r['운영단계'] || '',
실사용조직: r['실사용조직'] || r['사용조직'] || '',
관리조직: r['관리조직'] || '',
설치장소: r['설치장소'] || r['위치'] || '',
세부위치: r['세부위치'] || '',
IP주소: r['IP주소'] || r['IP 주소 1'] || '',
IP2: r['IP2'] || r['IP 주소 2'] || '',
원격방법: r['원격방법'] || r['원격접속'] || '',
서버ID: r['서버ID'] || '',
서버PW: r['서버PW'] || '',
모델명: r['모델명'] || '',
OS: r['OS'] || '',
CPU: r['CPU'] || '',
RAM: r['RAM'] || '',
SSD1: r['SSD1'] || r['Storage 1'] || '',
SSD2: r['SSD2'] || r['Storage 2'] || '',
모니터링: r['모니터링'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '스토리지') {
rows.forEach(r => data.storage.push({
id: Math.random().toString(36).substring(2, 9),
type: '스토리지',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || '',
모델명: r['모델명'] || '',
용량: r['용량'] || '',
IP주소: r['IP주소'] || '',
도입일: r['도입일'] || '',
구매가: r['구매가'] || r['금액'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '기타자산') {
rows.forEach(r => data.equip.push({
id: Math.random().toString(36).substring(2, 9),
type: '기타자산',
자산구분: r['자산구분'] || r['구분'] || '',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || '',
모델명: r['모델명'] || '',
IP주소: r['IP주소'] || '',
OS: r['OS'] || '',
도입일: r['도입일'] || '',
구매가: r['구매가'] || r['금액'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '모바일') {
rows.forEach(r => data.mobile.push({
id: Math.random().toString(36).substring(2, 9),
type: '모바일',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || '',
모델명: r['모델명'] || '',
사용자: r['사용자'] || '',
도입: r['도입일'] || '',
구매가: r['구매가'] || r['금액'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '구독SW') {
rows.forEach(r => data.subSw.push({
id: r['ID'] || Math.random().toString(36).substring(2, 9),
type: '구독SW',
부서: r['부서'] || '',
자산번호: r['자산번호'] || '',
소프트웨어명: r['소프트웨어명'] || '',
구매일: r['구매일'] || '',
만료일: r['만료일'] || '',
금액: r['금액'] || '',
수량: parseInt(r['수량'] || '1', 10),
비고: r['비고'] || ''
}));
} else if (sheetName === '영구SW') {
rows.forEach(r => data.permSw.push({
id: r['ID'] || Math.random().toString(36).substring(2, 9),
type: '영구SW',
부서: r['부서'] || '',
자산번호: r['자산번호'] || '',
소프트웨어명: r['소프트웨어명'] || '',
구매일: r['구매일'] || '',
금액: r['금액'] || '',
수량: parseInt(r['수량'] || '1', 10),
비고: r['비고'] || ''
}));
} else if (sheetName === 'SW_사용현황') {
rows.forEach(r => data.swUsers.push({
id: r['id'] || Math.random().toString(36).substring(2, 9),
swId: r['swId'] || '',
부서: r['부서'] || '',
이름: r['이름'] || '',
직급: r['직급'] || '',
사번: r['사번'] || '',
사용기간: r['사용기간'] || ''
}));
} else if (sheetName === 'History') {
rows.forEach(r => data.logs.push({
id: r['id'] || Math.random().toString(36).substring(2, 9),
assetId: r['assetId'] || '',
date: r['date'] || '',
details: r['details'] || '',
user: r['user'] || '관리자'
}));
}
});
resolve({ hw: hwAssets, sw: swAssets, swUsers, logs });
resolve(data);
} catch (err) {
reject(err);
}
};
reader.onerror = (err) => reject(err);
reader.readAsBinaryString(file);
});
}
export function exportToExcel(data: any, fileName: string) {
const wb = XLSX.utils.book_new();
// HW Tabs
if (data.pc) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.pc), '개인PC');
if (data.server) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.server), '서버');
if (data.storage) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.storage), '스토리지');
if (data.equip) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.equip), '기타자산');
if (data.mobile) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.mobile), '모바일');
// SW Tabs
if (data.subSw) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.subSw), '구독SW');
if (data.permSw) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.permSw), '영구SW');
if (data.swUsers) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.swUsers), 'SW_사용현황');
// Logs
if (data.logs) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.logs), 'History');
XLSX.writeFile(wb, fileName);
}

View File

@@ -1,96 +1,107 @@
import { MasterAssetData, HardwareAsset } from './excelHandler';
import { generateDummyData } from './dummyDataGenerator';
import { realServerData } from './realServerData';
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog } from './excelHandler';
// --- State Definitions ---
export interface AppState {
masterData: MasterAssetData;
activeCategory: 'hw' | 'sw' | 'ops';
activeSubTab: string;
activeCharts: any[];
export interface MasterAssetData {
pc: HardwareAsset[];
server: HardwareAsset[];
storage: HardwareAsset[];
equip: HardwareAsset[];
mobile: HardwareAsset[];
subSw: SoftwareAsset[];
permSw: SoftwareAsset[];
cloud: SoftwareAsset[]; // 클라우드 배열 추가
swUsers: SWUser[];
logs: HardwareLog[];
// 동료 코드 호환용 통합 배열 (프론트엔드 로직용)
hw: HardwareAsset[];
sw: SoftwareAsset[];
}
const dummy = generateDummyData();
// 서버 데이터만 실제 데이터로 교체
const mergedHw: HardwareAsset[] = [
...dummy.hw.filter(a => a.type !== '서버'),
...realServerData.map((serverData: any) => {
const s = serverData;
return {
id: s.id || Math.random().toString(36).substring(2, 9),
type: '서버',
법인: s.법인,
자산코드: s.자산코드,
명칭: s.용도 || '',
위치: s.위치,
관리자: s.담당자_정 || '홍길동',
담당자_정: s.담당자_정 || '홍길동',
담당자_부: s.담당자_부 || '김철수',
IP주소: s.IP주소,
IP2: s.IP2 || '',
MACaddress: s.MACaddress || '',
HW사양: s.HW사양 || '',
OS: s.OS,
CPU: s.CPU,
RAM: s.RAM,
SSD1: s.SSD1,
SSD2: s.SSD2,
HDD1: s.HDD1,
storage유형: s.storage유형,
모델명: s.모델명,
구매일: s.구매일 || '',
금액: s.금액 || '',
납품업체: s.납품업체 || '',
품의서명: s.품의서명 || '',
용도: s.용도,
상세: s.상세,
원격접속: s.원격접속 || '',
서버ID: s.서버ID || '',
서버PW: s.서버PW || '',
모니터링: s.모니터링 || '',
비고: s.비고 || ''
}})
];
export interface AppState {
activeCategory: 'dashboard' | 'hw' | 'sw';
activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW', '클라우드'
masterData: MasterAssetData;
}
// --- Initial State ---
// 초기 상태
export const state: AppState = {
masterData: {
...dummy,
hw: mergedHw, // 기본적으로 하드코딩된 데이터를 가지고 시작
logs: []
},
activeCategory: 'hw',
activeCategory: 'dashboard',
activeSubTab: '대시보드',
activeCharts: []
masterData: {
pc: [],
server: [],
storage: [],
equip: [],
mobile: [],
subSw: [],
permSw: [],
cloud: [],
hw: [], // 호환용
sw: [], // 호환용
swUsers: [],
logs: []
}
};
/**
* DB에서 데이터 로드
* 전용 API 엔드포인트들로부터 데이터 로드
*/
export async function loadMasterDataFromDB() {
try {
const [hwRes, swRes, swUserRes] = await Promise.all([
fetch('http://localhost:3000/api/hw'),
fetch('http://localhost:3000/api/sw'),
fetch('http://localhost:3000/api/sw-users')
]);
const endpoints = [
{ key: 'pc', url: 'http://localhost:3000/api/pc' },
{ key: 'server', url: 'http://localhost:3000/api/server' },
{ key: 'storage', url: 'http://localhost:3000/api/storage' },
{ key: 'equip', url: 'http://localhost:3000/api/equip' },
{ key: 'mobile', url: 'http://localhost:3000/api/mobile' },
{ key: 'subSw', url: 'http://localhost:3000/api/sw/sub' },
{ key: 'permSw', url: 'http://localhost:3000/api/sw/perm' },
{ key: 'cloud', url: 'http://localhost:3000/api/cloud' },
{ key: 'swUsers', url: 'http://localhost:3000/api/sw-users' },
{ key: 'logs', url: 'http://localhost:3000/api/logs' }
];
if (hwRes.ok) {
const hwData = await hwRes.json();
if (hwData && hwData.length > 0) state.masterData.hw = hwData;
const results = await Promise.all(endpoints.map(e => fetch(e.url)));
// 기존 데이터 초기화 (재분류 전)
state.masterData.pc = [];
state.masterData.server = [];
state.masterData.storage = [];
state.masterData.equip = [];
state.masterData.mobile = [];
for (let i = 0; i < endpoints.length; i++) {
if (results[i].ok) {
const data = await results[i].json();
const key = endpoints[i].key;
if (['pc', 'server', 'storage', 'equip', 'mobile'].includes(key)) {
// 하드웨어 데이터는 자동 재분류 로직 통과
(data as HardwareAsset[]).forEach(asset => saveHardwareAsset(asset));
} else {
(state.masterData as any)[key] = data || [];
}
}
}
if (swRes.ok) {
const swData = await swRes.json();
if (swData && swData.length > 0) state.masterData.sw = swData;
}
// 동료 코드 호환을 위한 통합 sw 배열 생성
state.masterData.sw = [
...state.masterData.subSw,
...state.masterData.permSw,
...state.masterData.cloud
];
if (swUserRes.ok) {
const swUserData = await swUserRes.json();
if (swUserData && swUserData.length > 0) state.masterData.swUsers = swUserData;
}
// 하드웨어 통합 배열 생성 (대시보드 등에서 사용)
state.masterData.hw = [
...state.masterData.pc,
...state.masterData.server,
...state.masterData.storage,
...state.masterData.equip,
...state.masterData.mobile
];
console.log('✅ DB 데이터 로드 완료');
console.log('✅ 모든 DB 데이터 로드 및 통합 완료');
return true;
} catch (err) {
console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.');
@@ -102,3 +113,78 @@ export async function loadMasterDataFromDB() {
export function updateState(newState: Partial<AppState>) {
Object.assign(state, newState);
}
/**
* 하드웨어 자산 통합 저장 (자동 카테고리 분류)
*/
export function saveHardwareAsset(updatedAsset: HardwareAsset) {
const type = updatedAsset.type || '';
const detailPurpose = (updatedAsset as any). || updatedAsset.detail_purpose || '';
// 1. 타겟 카테고리 결정 (사용자 정의 그룹 기준)
let targetKey: keyof MasterAssetData = 'equip';
const upperType = type.toUpperCase();
const isServer = type.includes('서버') || detailPurpose.includes('서버');
const isStorage = ['NAS', 'DAS', '스토리지'].some(t => type.includes(t));
const isMobileGroup = ['모바일', '태블릿', '노트북', '휴대폰', '핸드폰'].some(t => type.includes(t));
const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t));
const isPc = type === 'PC' || type === '개인PC' || detailPurpose === '개인PC';
if (isServer) {
targetKey = 'server';
} else if (isStorage) {
targetKey = 'storage';
} else if (isMobileGroup) {
targetKey = 'mobile';
} else if (isPc) {
targetKey = 'pc';
} else if (isEquipGroup) {
targetKey = 'equip';
}
// 2. 모든 카테고리에서 기존 ID 자산 삭제 (중복 방지)
const hwKeys: (keyof MasterAssetData)[] = ['pc', 'server', 'storage', 'equip', 'mobile'];
hwKeys.forEach(key => {
const arr = state.masterData[key] as HardwareAsset[];
if (Array.isArray(arr)) {
const idx = arr.findIndex(a => a.id === updatedAsset.id);
if (idx > -1) arr.splice(idx, 1);
}
});
// 3. 새로운 타겟 카테고리에 추가
(state.masterData[targetKey] as HardwareAsset[]).push(updatedAsset);
// 4. 통합 hw 배열 동기화
state.masterData.hw = [
...state.masterData.pc,
...state.masterData.server,
...state.masterData.storage,
...state.masterData.equip,
...state.masterData.mobile
];
}
/**
* 하드웨어 자산 통합 삭제
*/
export function deleteHardwareAsset(assetId: string) {
const hwKeys: (keyof MasterAssetData)[] = ['pc', 'server', 'storage', 'equip', 'mobile'];
hwKeys.forEach(key => {
const arr = state.masterData[key] as HardwareAsset[];
if (Array.isArray(arr)) {
const idx = arr.findIndex(a => a.id === assetId);
if (idx > -1) arr.splice(idx, 1);
}
});
// 통합 hw 배열 동기화
state.masterData.hw = [
...state.masterData.pc,
...state.masterData.server,
...state.masterData.storage,
...state.masterData.equip,
...state.masterData.mobile
];
}

View File

@@ -33,6 +33,21 @@ export function normalizeDate(dateStr: string): string {
return (dateStr || '').replace(/\./g, '-').trim();
}
/**
* 구매일로부터 현재까지의 경과 연수 계산 (소수점 첫째자리)
*/
export function calculateAssetAge(purchaseDate: string): number {
const normalized = normalizeDate(purchaseDate);
if (!normalized) return 0;
const purchase = new Date(normalized);
if (isNaN(purchase.getTime())) return 0;
const diffMs = Date.now() - purchase.getTime();
const age = diffMs / (1000 * 60 * 60 * 24 * 365.25);
return Math.max(0, parseFloat(age.toFixed(1)));
}
/**
* 고유 ID 생성 (7자리 랜덤 문자열)
*/
@@ -54,3 +69,24 @@ export function getAssetChanges(oldAsset: any, newAsset: any, fields: {key: stri
});
return changes.join('\n');
}
/**
* 자산 목록 정렬 (방안 C: 구매법인별 -> 자산번호 순)
*/
export function sortAssets<T>(list: T[]): T[] {
return [...list].sort((a: any, b: any) => {
// 1순위: 구매법인 (한글 가나다순)
const corpA = String(a. || '').trim();
const corpB = String(b. || '').trim();
if (corpA < corpB) return -1;
if (corpA > corpB) return 1;
// 2순위: 자산번호 (영문/숫자순)
const codeA = String(a. || a. || '').trim();
const codeB = String(b. || b. || '').trim();
if (codeA < codeB) return -1;
if (codeA > codeB) return 1;
return 0;
});
}