feat: migrate ServerPC data to asset_pc, enhance filters with location, and standardize page headers

- 서버PC 자산을 asset_pc 테이블로 통합 마이그레이션 및 스키마 확장 (위치, IP 정보 복구 완료)

- 하드웨어 자산 페이지의 구매법인 필터를 자산위치 필터로 교체 및 동적 데이터 바인딩 적용

- 모든 자산 리스트 페이지 상단에 설명(Description) 필드 추가 및 헤더 표준화

- 상세 모달 내 삭제 버튼 기능 구현 및 서버PC 용도 필드 노출 오류 수정

- 현 사용조직 필터 리스트가 비어있던 DOM 셀렉터 버그 수정
This commit is contained in:
2026-05-26 17:33:03 +09:00
parent d34ebb8500
commit 82bbe85e23
43 changed files with 2055 additions and 1871 deletions

View File

@@ -50,7 +50,7 @@ export function generateDummyData(): MasterAssetData {
HDD2: '',
구매일: randDate(purchaseYear, purchaseYear),
금액: String(Math.floor(Math.random()*100 + 50) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','),
납품업체: rand(['다나와', '컴퓨존', '오피스디포']),
구매업체: rand(['다나와', '컴퓨존', '오피스디포']),
: '',
: '', IP주소: '', MACaddress: '', OS: '', HW사양: ''
});
@@ -87,7 +87,7 @@ export function generateDummyData(): MasterAssetData {
HW사양: 'Xeon 16Core, 64GB RAM',
구매일: randDate(purchaseYear, purchaseYear),
: '5,000,000',
: '서버뱅크',
: '서버뱅크',
: ''
});
}
@@ -111,7 +111,7 @@ export function generateDummyData(): MasterAssetData {
MACaddress: '',
구매일: randDate(purchaseYear, purchaseYear),
: '1,500,000',
: '스토리지넷',
: '스토리지넷',
: '',
: '', OS: '', HW사양: ''
});
@@ -131,7 +131,7 @@ export function generateDummyData(): MasterAssetData {
관리자: randUser(),
구매일: randDate(purchaseYear, purchaseYear),
: '300,000',
: '오피스공구',
: '오피스공구',
: '',
IP주소: '', MACaddress: '', OS: '', HW사양: ''
});
@@ -151,7 +151,7 @@ export function generateDummyData(): MasterAssetData {
OS: rand(['iOS', 'Android', 'iPadOS']),
구매일: randDate(purchaseYear, purchaseYear),
: '1,200,000',
: '통신사',
: '통신사',
: '',
IP주소: '', MACaddress: '', HW사양: '', : ''
});
@@ -171,7 +171,7 @@ export function generateDummyData(): MasterAssetData {
: '100,000',
수량: 5,
: `admin${i}@hm.com`,
: '총판',
: '총판',
: ''
});
swUsers.push({ sw_id: swId, userData: [[rand(corps), rand(depts), '사원', rand(users), '2024.01~12', '신청완료']] });
@@ -191,7 +191,7 @@ export function generateDummyData(): MasterAssetData {
: '500,000',
수량: 2,
: `license${i}`,
: '총판',
: '총판',
: ''
});
}

View File

@@ -1,50 +1,28 @@
import * as XLSX from 'xlsx';
import { ASSET_SCHEMA } from './schema';
/**
* ITAM 엑셀 핸들러 (Database Synchronized Edition)
* 데이터베이스 실제 스키마 컬럼과 엑셀 헤더를 1:1로 일치시킵니다.
*/
export interface HardwareAsset {
[key: string]: any;
id: string;
type: string;
법인: string;
자산코드: string;
명칭: string;
위치: string;
관리자: string;
IP주소: string;
MACaddress: string;
HW사양: string;
OS: string;
금액?: string;
납품업체: string;
품의서명: string;
비고?: string;
}
export interface SoftwareAsset {
[key: string]: any;
id: string;
type: string;
분야?: string;
법인: string;
부서?: string;
제품명: string;
금액: string;
수량: number;
계정명: string;
납품업체: string;
비고: string;
}
export interface SWUser {
id: string;
sw_id: string;
법인: string;
부서: string;
: string;
직위: string;
이름: string;
사용기간: string;
신청서명: string;
userData?: any[];
user_name: string;
dept: string;
corp: string;
[key: string]: any;
}
export interface HardwareLog {
@@ -53,106 +31,193 @@ export interface HardwareLog {
date: string;
details: string;
user: string;
cost?: number;
}
export interface MasterAssetData {
pc: HardwareAsset[];
server: HardwareAsset[];
storage: HardwareAsset[];
equip: HardwareAsset[];
mobile: HardwareAsset[];
subSw: SoftwareAsset[];
permSw: SoftwareAsset[];
network: HardwareAsset[];
equipment: HardwareAsset[];
survey: HardwareAsset[];
pcParts: HardwareAsset[];
swInternal: SoftwareAsset[];
swExternal: SoftwareAsset[];
cloud: SoftwareAsset[];
domain?: any[];
hw: HardwareAsset[];
sw: SoftwareAsset[];
domain: any[];
vip: HardwareAsset[];
officeSupplies: HardwareAsset[];
cost: any[];
swUsers: SWUser[];
logs: HardwareLog[];
[key: string]: any;
}
const PC_HEADERS = ['법인', '자산코드', '구매연월', '사용자', '현사용조직', '이전사용조직', '위치', '담당자(정)', '담당자(부)', '모델명', 'OS', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'SSD3', '메인보드', 'IP주소', '금액', '납품업체', '품의서명', '비고'];
const SERVER_HEADERS = ['법인', '자산코드', '구매연월', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '위치', '담당자(정)', '담당자(부)', 'IP 주소 1', 'IP 주소 2', '원격도구', '서버 ID', '서버 PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage 1', 'Storage 2', 'Storage 3', '모니터링', '금액', '납품업체', '품의서명', '비고'];
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매연월', '금액', '납품업체', '품의서명', '비고'];
const EQUIP_HEADERS = ['법인', '비품유형', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매연월', '금액', '납품업체', '품의서명', '비고'];
const MOBILE_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', '기기유형', 'OS', '구매연월', '금액', '납품업체', '품의서명', '비고'];
const SUB_SW_HEADERS = ['분야', '법인', '제품명', '부서', '수량', '금액', '구매일', '납품업체', '시작일', '만료일', '라이선스유형', '계정명', '비고'];
const PERM_SW_HEADERS = ['분야', '법인', '제품명', '부서', '수량', '금액', '구매일', '납품업체', '시작일', '만료일', '라이선스키', '계정명', '비고'];
const CLOUD_HEADERS = ['플랫폼명', '법인', '제품명', '부서', '계정명', '결제수단', '결제일', '연결카드번호', '당월청구액', '비고'];
const DOMAIN_HEADERS = ['유형', '법인', '서비스명', '관리도메인', '시작일', '만료일', '금액', '담당자', '담당자(부)', '비고'];
/**
* DB 컬럼 순서 및 구성 정의 (실제 DB 스키마 dump 기준)
*/
const DB_MAPPING: Record<string, (keyof typeof ASSET_SCHEMA)[]> = {
pc: [
'ASSET_TYPE', 'HW_STATUS', 'CURRENT_DEPT', 'PREV_DEPT', 'USER_POSITION',
'EMP_NO', 'CURRENT_USER',
'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'HDD3', 'HDD4', 'MAC_ADDR',
'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT',
'PURCHASE_VENDOR', 'MEMO', 'MAINBOARD'
],
server: [
'ASSET_TYPE', 'MODEL_NAME', 'ASSET_PURPOSE', 'HW_STATUS',
'CURRENT_DEPT', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP_ADDR',
'REMOTE_TOOL', 'REMOTE_ID', 'REMOTE_PW', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN',
'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR',
'MEMO', 'PREV_DEPT', 'MANAGER_SUB', 'IP_ADDR2', 'MONITORING', 'HDD3', 'HDD4', 'EMP_NO'
],
storage: [
'ASSET_TYPE', 'HW_STATUS', 'VOLUME', 'MODEL_NAME',
'EMP_NO', 'CURRENT_USER',
'SERIAL_NUM', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB',
'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR',
'MEMO', 'CURRENT_DEPT', 'PREV_DEPT'
],
network: [
'PURCHASE_CORP', 'HW_STATUS', 'CURRENT_DEPT', 'PREV_DEPT',
'EMP_NO', 'CURRENT_USER',
'ASSET_TYPE', 'ASSET_MFR', 'MODEL_NAME', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN',
'MANAGER_SUB', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO'
],
survey: [ // asset_survey (공간정보장비)
'HW_STATUS', 'ASSET_NAME', 'LOCATION', 'LOC_DETAIL',
'EMP_NO', 'CURRENT_USER',
'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT',
'PURCHASE_VENDOR', 'MEMO'
],
pcParts: [
'HW_STATUS', 'ASSET_TYPE', 'ASSET_MFR', 'MODEL_NAME', 'VOLUME',
'EMP_NO', 'CURRENT_USER',
'MONITOR_INCH', 'LOCATION', 'LOC_DETAIL', 'PURCHASE_CORP', 'PURCHASE_DATE',
'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO'
],
equipment: [
'HW_STATUS', 'ASSET_STATUS', 'ASSET_TYPE', 'ASSET_MFR',
'EMP_NO', 'CURRENT_USER',
'MODEL_NAME', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB',
'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR',
'MEMO'
],
officeSupplies: [ // asset_office_supplies (시설자산)
'HW_STATUS', 'ASSET_TYPE', 'ASSET_MFR', 'MODEL_NAME',
'EMP_NO', 'CURRENT_USER',
'ASSET_COUNT', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB',
'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR',
'MEMO'
],
swInternal: [
'SW_FIELD', 'DEV_OBJ', 'SW_STATUS', 'SW_TYPE', 'MANAGER_MAIN',
'DEV_MGR', 'PLANNING_MGR', 'SALES_MGR', 'PURCHASE_CORP', 'MEMO'
],
swExternal: [
'PRODUCT_NAME', 'SW_TYPE', 'SW_STATUS', 'SW_FIELD', 'CURRENT_DEPT',
'PREV_DEPT', 'MANAGER_MAIN', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT',
'PURCHASE_VENDOR', 'EMAIL_ACCOUNT', 'MEMO', 'EMP_NO', 'CURRENT_USER'
],
cloud: [
'ASSET_PURPOSE', 'PURCHASE_METHOD', 'PURCHASE_VENDOR', 'PURCHASE_CORP',
'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'MANAGER_MAIN', 'MANAGER_SUB',
'MEMO', 'SW_ID', 'SW_PW'
],
domain: [
'DOMAIN_ADDR', 'ASSET_PURPOSE', 'PURCHASE_VENDOR', 'ASSET_TYPE',
'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'MANAGER_MAIN', 'MANAGER_SUB',
'MEMO'
],
cost: [
'ASSET_TYPE', 'ASSET_PURPOSE', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN',
'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR',
'EMAIL_ACCOUNT', 'EMAIL_PW', 'MEMO', 'EMP_NO', 'CURRENT_USER'
],
vip: [ // asset_vip (선물)
'ASSET_NAME', 'MODEL_NAME', 'LOCATION', 'LOC_DETAIL',
'PURCHASE_CORP', 'PURCHASE_DATE', 'EXPIRED_DATE', 'PURCHASE_VENDOR', 'MEMO'
]
};
export function downloadTemplate() {
const wb = XLSX.utils.book_new();
const tabConfigs = [
{ name: '개인PC', headers: PC_HEADERS },
{ name: '서버', headers: SERVER_HEADERS },
{ name: '스토리지', headers: STORAGE_HEADERS },
{ name: '전산비품', headers: EQUIP_HEADERS },
{ name: '모바일기기', headers: MOBILE_HEADERS },
{ name: '구독SW', headers: SUB_SW_HEADERS },
{ name: '영구SW', headers: PERM_SW_HEADERS },
{ name: '클라우드', headers: CLOUD_HEADERS },
{ name: '도메인', headers: DOMAIN_HEADERS }
{ name: 'PC', key: 'pc' },
{ name: '서버', key: 'server' },
{ name: '스토리지', key: 'storage' },
{ name: '공간정보장비', key: 'survey' },
{ name: 'PC부품', key: 'pcParts' },
{ name: '네트워크', key: 'network' },
{ name: '업무지원장비', key: 'equipment' },
{ name: '내부SW', key: 'swInternal' },
{ name: '외부SW', key: 'swExternal' },
{ name: '클라우드', key: 'cloud' },
{ name: '도메인', key: 'domain' },
{ name: '비용관리', key: 'cost' },
{ name: '선물', key: 'vip' },
{ name: '시설자산', key: 'officeSupplies' }
];
const sampleData: Record<string, any[]> = {
'개인PC': ['(주)에이치엠', 'PC-24001', '202401', '홍길동', '기술팀', '-', '서울본사 7층', '김관리', '이부관', 'LG Gram 16', 'Windows 11', 'i7-1360P', 'RTX 3050', '16GB', '512GB', '-', '-', 'LG Mainboard', '192.168.0.10', '1500000', 'LG전자', '2024_상반기_PC구매.pdf', '신규 입사자 지급용'],
'서버': ['(주)에이치엠', 'SRV-24001', '202401', '물리', '웹서버', '운영 웹 서버', '인프라팀', '-', 'IDC 센터 1-A', '박서버', '최백업', '10.0.0.1', '10.0.0.2', 'RDP', 'admin', '********', 'Dell PowerEdge R750', 'Ubuntu 22.04', 'Xeon Gold 6330', '128GB', '-', '1TB SSD', '1TB SSD', '2TB HDD', 'Zabbix', '8500000', '델테크놀로지스', '2024_IDC_확장품의.pdf', '운영 환경 전용'],
'도메인': ['도메인', '(주)에이치엠', '대표홈페이지', 'hm-corp.com', '2024-01-01', '2025-01-01', '55000', '홍길동', '이부관', '가비아 자동갱신']
};
tabConfigs.forEach(config => {
const data = [config.headers];
if (sampleData[config.name]) {
data.push(sampleData[config.name]);
}
const ws = XLSX.utils.aoa_to_sheet(data);
ws['!cols'] = Array(config.headers.length).fill({ wch: 20 });
const keys = DB_MAPPING[config.key];
const headers = keys.map(k => ASSET_SCHEMA[k].ui);
const ws = XLSX.utils.aoa_to_sheet([headers]);
ws['!cols'] = Array(headers.length).fill({ wch: 20 });
XLSX.utils.book_append_sheet(wb, ws, config.name);
});
XLSX.writeFile(wb, 'itam_assets_template.xlsx');
XLSX.writeFile(wb, 'itam_template_db_aligned.xlsx');
}
export function exportToExcel(masterData: MasterAssetData) {
const wb = XLSX.utils.book_new();
const exportMap = [
{ tab: '개인PC', list: masterData.pc, headers: PC_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a._정, a._부, a., a.OS, a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.SSD3, a., a.IP주소, a., a., a., a.] },
{ tab: '서버', list: masterData.server, headers: SERVER_HEADERS, map: (a: any) => [a., a., a., a.type, a., a., a., a., a., a._정, a._부, a.IP주소, a.IP2, a., a.ID, a.PW, a., a.OS, a.CPU, a.RAM, a.GPU, a.SSD1, a.SSD2, a.SSD3, a., a., a., a., a.] },
{ tab: '스토리지', list: masterData.storage, headers: STORAGE_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a._정, a._부, a.IP주소, a.MACaddress, a., a., a., a., a.] },
{ tab: '전산비품', list: masterData.equip, headers: EQUIP_HEADERS, map: (a: any) => [a., a., a., a., a., a., a.IP주소, a.MACaddress, a.HW사양, a.OS, a., a., a., a., a.] },
{ tab: '모바일기기', list: masterData.mobile, headers: MOBILE_HEADERS, map: (a: any) => [a., a., a., a., a., a., a.OS, a., a., a., a., a.] },
{ tab: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a., a., a., a., a., a.] },
{ tab: '영구SW', list: masterData.permSw, headers: PERM_SW_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a., a., a., a., a., a.] },
{ tab: '클라우드', list: masterData.cloud, headers: CLOUD_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a., a., a.] },
{ tab: '도메인', list: masterData.domain || [], headers: DOMAIN_HEADERS, map: (a: any) => [a.type, a.corp, a.service_name, a.domain_name, a.start_date, a.expiry_date, a.price, a.manager_main, a.manager_sub, a.remarks] }
const exportConfigs = [
{ name: 'PC', list: masterData.pc, key: 'pc' },
{ name: '서버', list: masterData.server, key: 'server' },
{ name: '스토리지', list: masterData.storage, key: 'storage' },
{ name: '공간정보장비', list: masterData.survey || [], key: 'survey' },
{ name: 'PC부품', list: masterData.pcParts || [], key: 'pcParts' },
{ name: '네트워크', list: masterData.network || [], key: 'network' },
{ name: '업무지원장비', list: masterData.equipment || [], key: 'equipment' },
{ name: '내부SW', list: masterData.swInternal, key: 'swInternal' },
{ name: '외부SW', list: masterData.swExternal, key: 'swExternal' },
{ name: '클라우드', list: masterData.cloud || [], key: 'cloud' },
{ name: '도메인', list: masterData.domain || [], key: 'domain' },
{ name: '비용관리', list: masterData.cost || [], key: 'cost' },
{ name: '선물', list: masterData.vip || [], key: 'vip' },
{ name: '시설자산', list: masterData.officeSupplies || [], key: 'officeSupplies' }
];
exportMap.forEach(m => {
const ws = XLSX.utils.aoa_to_sheet([m.headers, ...m.list.map(m.map)]);
XLSX.utils.book_append_sheet(wb, ws, m.tab);
exportConfigs.forEach(config => {
const schemaKeys = DB_MAPPING[config.key];
const headers = schemaKeys.map(k => ASSET_SCHEMA[k].ui);
const rows = config.list.map(asset =>
schemaKeys.map(k => {
const dbField = ASSET_SCHEMA[k].db;
return asset[dbField] || asset[ASSET_SCHEMA[k].key] || '';
})
);
const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]);
XLSX.utils.book_append_sheet(wb, ws, config.name);
});
XLSX.writeFile(wb, `itam_master_${new Date().toISOString().split('T')[0]}.xlsx`);
XLSX.writeFile(wb, `itam_export_${new Date().toISOString().split('T')[0]}.xlsx`);
}
/**
* 엑셀 날짜 데이터(숫자 또는 문자열)를 YYYY-MM-DD 형식의 문자열로 변환
*/
export 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 val ? String(val) : '';
return String(val);
}
export async function parseExcel(file: File): Promise<any> {
@@ -163,44 +228,41 @@ export async function parseExcel(file: File): Promise<any> {
const workbook = XLSX.read(e.target?.result, { type: 'array' });
const parsedData: any = {};
workbook.SheetNames.forEach(rawSheetName => {
const sheetName = rawSheetName.trim();
const ws = workbook.Sheets[rawSheetName];
workbook.SheetNames.forEach(sheetName => {
const ws = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json(ws, { defval: "" }) as any[];
const list: any[] = [];
rows.forEach(rawR => {
// 헤더명에 공백이 포함된 경우 대비하여 키 정리 (trim)
const r: any = {};
Object.keys(rawR).forEach(k => { r[k.trim()] = rawR[k]; });
rows.forEach(r => {
const data: any = { id: Math.random().toString(36).substring(2, 9) };
const common = { id: Math.random().toString(36).substring(2, 9) };
if (sheetName === '개인PC') {
const purchaseYM = formatExcelDate(r['구매연월']).replace(/-/g, '').substring(0, 6);
list.push({ ...common, type: '개인PC', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 구매연월: purchaseYM, 사용자: r['사용자']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', GPU: r['GPU']||'', RAM: r['RAM']||'', SSD1: r['SSD1']||'', SSD2: r['SSD2']||'', SSD3: r['SSD3']||'', 메인보드: r['메인보드']||'', IP주소: r['IP주소']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' });
} else if (sheetName === '서버') {
const purchaseYM = formatExcelDate(r['구매연월']).replace(/-/g, '').substring(0, 6);
list.push({ ...common, type: '서버', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 구매연월: purchaseYM, 상세용도: 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']||'', SSD3: r['Storage 3']||'', 모니터링: r['모니터링']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'', type2: r['유형']||'물리' });
} else if (sheetName === '스토리지') {
const purchaseYM = formatExcelDate(r['구매연월']).replace(/-/g, '').substring(0, 6);
list.push({ ...common, type: '스토리지', 법인: r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'', 구매연월: purchaseYM, 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' });
} else if (sheetName === '전산비품') {
const purchaseYM = formatExcelDate(r['구매연월']).replace(/-/g, '').substring(0, 6);
list.push({ ...common, type: '전산비품', 법인: r['법인']||'', 비품유형: r['비품유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'', 구매연월: purchaseYM, 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' });
} else if (sheetName === '모바일기기') {
const purchaseYM = formatExcelDate(r['구매연월']).replace(/-/g, '').substring(0, 6);
list.push({ ...common, type: '모바일기기', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', 기기유형: r['기기유형']||'', OS: r['OS']||'', 구매연월: purchaseYM, 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' });
} else if (sheetName === '구독SW') {
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['제품명']||'', 구매일: 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: formatExcelDate(r['시작일']), expiry_date: formatExcelDate(r['만료일']), price: r['금액']||'', manager_main: r['담당자']||'', manager_sub: r['담당자(부)']||'', remarks: r['비고']||'' });
}
// Set default category based on sheet name
data['category'] = sheetName;
Object.keys(r).forEach(label => {
const schemaEntry = Object.values(ASSET_SCHEMA).find(s => s.ui === label);
const key = schemaEntry ? schemaEntry.db : label;
let val = r[label];
if (label.includes('일자') || label.includes('연월') || label.includes('만료일') || label.includes('시작일')) {
val = formatExcelDate(val);
}
data[key] = val;
});
list.push(data);
});
if (list.length > 0) parsedData[sheetName] = list;
// Sheet Name Mapping back to state keys
const nameMap: Record<string, string> = {
'PC': 'pc', '서버': 'server', '스토리지': 'storage', '공간정보장비': 'survey',
'PC부품': 'pcParts', '네트워크': 'network', '업무지원장비': 'equipment',
'내부SW': 'swInternal', '외부SW': 'swExternal', '클라우드': 'cloud',
'도메인': 'domain', '비용관리': 'cost', '선물': 'vip', '시설자산': 'officeSupplies'
};
const stateKey = nameMap[sheetName] || sheetName;
if (list.length > 0) parsedData[stateKey] = list;
});
resolve(parsedData);
} catch (err) { reject(err); }

104
src/core/filterHandler.ts Normal file
View File

@@ -0,0 +1,104 @@
import { ASSET_SCHEMA, UI_TEXT } from './schema';
import { getActionButtonsHTML } from './utils';
import { generateOptionsHTML } from '../components/Modal/ModalUtils';
import { CORP_LIST } from '../components/Modal/SharedData';
/**
* ITAM Unified Filter Bar Component
* 검색 UI를 표준화하고 한 곳에서 관리합니다.
*/
export interface FilterOptions {
keywordLabel?: string;
showCorp?: boolean;
showDept?: boolean;
showLoc?: boolean;
showField?: boolean;
extraHTML?: string;
onFilterChange: (filters: any) => void;
}
export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
const { keywordLabel = '통합 검색', showCorp = false, showDept = false, showLoc = false, showField = false, extraHTML = '', onFilterChange } = options;
container.innerHTML = `
<div class="search-item flex-1">
<label>${keywordLabel}</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
${showField ? `
<div class="search-item">
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
<select id="filter-field">
<option value="">전체 분야</option>
<option value="업무공통">업무공통</option>
<option value="개발S/W">개발S/W</option>
<option value="디자인">디자인</option>
<option value="설계S/W">설계S/W</option>
</select>
</div>` : ''}
${showCorp ? `
<div class="search-item">
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
<select id="filter-corp">${generateOptionsHTML(CORP_LIST, '', true)}</select>
</div>` : ''}
${showLoc ? `
<div class="search-item">
<label>${ASSET_SCHEMA.LOCATION.ui}</label>
<select id="filter-loc"><option value="">전체 위치</option></select>
</div>` : ''}
${showDept ? `
<div class="search-item">
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
<select id="filter-dept"><option value="">전체 조직</option></select>
</div>` : ''}
${extraHTML}
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> ${UI_TEXT.ACTION.RESET_FILTER}
</button>
${getActionButtonsHTML()}
`;
// Bind Events
const triggerChange = () => {
const filters = {
keyword: (container.querySelector('#filter-keyword') as HTMLInputElement)?.value.toLowerCase().trim() || '',
corp: (container.querySelector('#filter-corp') as HTMLSelectElement)?.value || '',
dept: (container.querySelector('#filter-dept') as HTMLSelectElement)?.value || '',
loc: (container.querySelector('#filter-loc') as HTMLSelectElement)?.value || '',
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || ''
};
onFilterChange(filters);
};
container.querySelector('#filter-keyword')?.addEventListener('input', triggerChange);
container.querySelector('#filter-corp')?.addEventListener('change', triggerChange);
container.querySelector('#filter-dept')?.addEventListener('change', triggerChange);
container.querySelector('#filter-loc')?.addEventListener('change', triggerChange);
container.querySelector('#filter-field')?.addEventListener('change', triggerChange);
container.querySelector('#btn-reset-filters')?.addEventListener('click', () => {
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field'].forEach(id => {
const el = container.querySelector(`#${id}`);
if (el) (el as any).value = '';
});
triggerChange();
});
}
/**
* 공통 필터링 로직
*/
export function applyCommonFilters(list: any[], filters: any, searchKeys: (keyof typeof ASSET_SCHEMA)[]) {
return list.filter(item => {
const matchKeyword = !filters.keyword || searchKeys.some(key =>
String(item[ASSET_SCHEMA[key].key] || item[ASSET_SCHEMA[key].db] || '').toLowerCase().includes(filters.keyword)
);
const matchCorp = !filters.corp || (item[ASSET_SCHEMA.PURCHASE_CORP.key] || item[ASSET_SCHEMA.PURCHASE_CORP.db]) === filters.corp;
const matchDept = !filters.dept || (item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db]) === filters.dept;
const matchLoc = !filters.loc || (item[ASSET_SCHEMA.LOCATION.key] || item[ASSET_SCHEMA.LOCATION.db]) === filters.loc;
const matchField = !filters.field || (item[ASSET_SCHEMA.SW_FIELD.key] || item[ASSET_SCHEMA.SW_FIELD.db]) === filters.field;
return matchKeyword && matchCorp && matchDept && matchLoc && matchField;
});
}

View File

@@ -19,28 +19,30 @@ export const ASSET_SCHEMA = {
APPROVAL_DOC: { key: 'approval_document', db: 'approval_document', ui: '품의서' },
MANAGER_MAIN: { key: 'manager_primary', db: 'manager_primary', ui: '담당자(정)' },
MANAGER_SUB: { key: 'manager_secondary', db: 'manager_secondary', ui: '담당자(부)' },
LOCATION: { key: 'location', db: 'location', ui: '설치위치' },
LOCATION: { key: 'location', db: 'location', ui: '자산위치' },
LOC_DETAIL: { key: 'location_detail', db: 'location_detail', ui: '상세위치' },
MEMO: { key: 'memo', db: 'memo', ui: '메모' },
// ─── 하드웨어 상세 (Hardware) ───
HW_STATUS: { key: 'hw_status', db: 'hw_status', ui: '상태' },
MODEL_NAME: { key: 'model_name', db: 'model_name', ui: '모델명' },
ASSET_NAME: { key: 'asset_name', db: 'asset_name', ui: '모델명' },
ASSET_NAME: { key: 'asset_name', db: 'asset_name', ui: '자산명' },
ASSET_MFR: { key: 'asset_mfr', db: 'asset_mfr', ui: '제조사' },
CURRENT_DEPT: { key: 'current_dept', db: 'current_dept', ui: '현 사용조직' },
PREV_DEPT: { key: 'previous_dept',db: 'previous_dept', ui: '직전 사용조직' },
CURRENT_USER: { key: 'current_user', db: 'current_user', ui: '현 사용자' },
CURRENT_USER: { key: 'user_current', db: 'user_current', ui: '현 사용자' },
EMP_NO: { key: 'emp_no', db: 'emp_no', ui: '사번' },
USER_POSITION: { key: 'user_position', db: 'user_position', ui: '직무' },
PREV_USER: { key: 'previous_user',db: 'previous_user', ui: '직전 사용자' },
CPU: { key: 'cpu', db: 'cpu', ui: 'CPU' },
RAM: { key: 'ram', db: 'ram', ui: 'RAM' },
GPU: { key: 'gpu', db: 'gpu', ui: 'GPU' },
SSD1: { key: 'ssd_1', db: 'ssd_1', ui: 'Storage 1' },
SSD2: { key: 'ssd_2', db: 'ssd_2', ui: 'Storage 2' },
HDD1: { key: 'hdd_1', db: 'hdd_1', ui: 'HDD 1' },
HDD2: { key: 'hdd_2', db: 'hdd_2', ui: 'HDD 2' },
HDD3: { key: 'hdd_3', db: 'hdd_3', ui: 'HDD 3' },
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD 4' },
SSD1: { key: 'ssd_1', db: 'ssd_1', ui: 'SSD1' },
SSD2: { key: 'ssd_2', db: 'ssd_2', ui: 'SSD2' },
HDD1: { key: 'hdd_1', db: 'hdd_1', ui: 'HDD1' },
HDD2: { key: 'hdd_2', db: 'hdd_2', ui: 'HDD2' },
HDD3: { key: 'hdd_3', db: 'hdd_3', ui: 'HDD3' },
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD4' },
MAINBOARD: { key: 'mainboard', db: 'mainboard', ui: '메인보드' },
IP_ADDR: { key: 'ip_address', db: 'ip_address', ui: 'IP 주소' },
IP_ADDR2: { key: 'ip_address_2', db: 'ip_address_2', ui: 'IP 주소 2' },
@@ -51,23 +53,104 @@ export const ASSET_SCHEMA = {
MONITORING: { key: 'monitoring', db: 'monitoring', ui: '모니터링' },
VOLUME: { key: 'volume', db: 'volume', ui: '용량' },
MONITOR_INCH: { key: 'monitor_inch', db: 'monitor_inch', ui: '인치' },
ASSET_COUNT: { key: 'asset_count', db: 'asset_count', ui: '수량(개수)' },
ASSET_COUNT: { key: 'asset_count', db: 'asset_count', ui: '수량' },
SERIAL_NUM: { key: 'serial_num', db: 'serial_num', ui: 'S/N' },
// ─── 소프트웨어/클라우드 상세 (SW/Cloud/Domain) ───
SW_STATUS: { key: 'sw_status', db: 'sw_status', ui: 'SW 상태' },
SW_STATUS: { key: 'sw_status', db: 'sw_status', ui: '상태' },
SW_FIELD: { key: 'sw_field', db: 'sw_field', ui: '분야' },
SW_TYPE: { key: 'sw_type', db: 'sw_type', ui: 'SW 유형' },
DEV_OBJ: { key: 'dev_objective',db: 'dev_objective', ui: '개발목적' },
DEV_MGR: { key: 'dev_manager', db: 'dev_manager', ui: '담당개발자' },
SW_TYPE: { key: 'sw_type', db: 'sw_type', ui: '유형' },
DEV_OBJ: { key: 'dev_objective',db: 'dev_objective', ui: '목적' },
DEV_MGR: { key: 'dev_manager', db: 'dev_manager', ui: '개발담당자' },
PLANNING_MGR: { key: 'planning_manager', db: 'planning_manager', ui: '기획담당자' },
SALES_MGR: { key: 'sales_manager',db: 'sales_manager', ui: '영업담당자' },
PRODUCT_NAME: { key: 'product_name', db: 'product_name', ui: '제품명' },
DOMAIN_ADDR: { key: 'domain_address', db: 'domain_address',ui: '도메인주소' },
EMAIL_ACCOUNT: { key: 'email_account', db: 'email_account', ui: '관리계정' },
EMAIL_PW: { key: 'email_pw', db: 'email_pw', ui: '계정PW' },
EMAIL_ACCOUNT: { key: 'email_account', db: 'email_account', ui: '이메일주소' },
EMAIL_PW: { key: 'email_pw', db: 'email_pw', ui: '이메일비밀번호' },
SW_ID: { key: 'sw_id', db: 'sw_id', ui: '계정ID' },
SW_PW: { key: 'sw_pw', db: 'sw_pw', ui: '비밀번호' },
PURCHASE_METHOD:{ key: 'purchase_method', db: 'purchase_method', ui: '결제수단' },
ASSET_PURPOSE: { key: 'asset_purpose', db: 'asset_purpose', ui: '용도' },
EXPIRY_DATE: { key: 'expiry_date', db: 'expiry_date', ui: '만료일' }
ASSET_STATUS: { key: 'asset_status', db: 'asset_status', ui: '상태' },
START_DATE: { key: 'start_date', db: 'start_date', ui: '시작일' },
EXPIRED_DATE: { key: 'expired_date', db: 'expired_date', ui: '만료일' }
};
/**
* 페이지별 헤더 정보 (타이틀, 설명, 아이콘)
*/
export const PAGE_DESCRIPTIONS: Record<string, { title: string; description: string; icon: string }> = {
'PC': {
title: '개인PC 자산 관리',
description: '임직원에게 지급된 데스크톱 및 노트북 자산의 할당 현황과 하드웨어 사양을 통합 관리합니다.',
icon: 'laptop'
},
'서버': {
title: '서버 자산 관리',
description: 'IDC 및 사내 서버실에 운영 중인 물리 서버 장비의 도입, 운영, 폐기 현황을 관리합니다.',
icon: 'server'
},
'스토리지': {
title: '스토리지 자산 관리',
description: '데이터 저장 및 백업을 위한 NAS, DAS 등 스토리지 장비의 용량과 연결 상태를 관리합니다.',
icon: 'database'
},
'네트워크': {
title: '네트워크 장비 관리',
description: '스위치, 방화벽, 공유기 등 사내 네트워크 인프라를 구성하는 주요 장비 현황을 관리합니다.',
icon: 'layers'
},
'업무지원장비': {
title: '업무 지원 장비 관리',
description: '모니터, 프린터, 스캐너 등 원활한 업무 수행을 보조하는 전산 비품들을 관리합니다.',
icon: 'monitor'
},
'PC부품': {
title: 'PC 부품 자산 관리',
description: 'CPU, RAM, GPU 등 PC 조립 및 유지보수를 위해 보유 중인 주요 부품 재고를 관리합니다.',
icon: 'cpu'
},
'공간정보장비': {
title: '공간 정보 장비 관리',
description: '측량 및 공간 정보 수집에 사용되는 특수 정밀 장비들의 이력과 상태를 관리합니다.',
icon: 'map'
},
'내부': {
title: '사내 개발 S/W 관리',
description: '사내에서 자체 개발하거나 운영 중인 시스템 및 소프트웨어 서비스 현황을 관리합니다.',
icon: 'code'
},
'외부': {
title: '외부 상용 S/W 관리',
description: '상용 소프트웨어의 라이선스 보유 현황, 사용자 할당 및 만료 일정을 관리합니다.',
icon: 'package'
},
'도메인': {
title: '도메인 자산 관리',
description: '운영 중인 서비스 도메인의 등록 정보, 관리 업체 및 갱신 만료일을 관리합니다.',
icon: 'globe'
},
'클라우드': {
title: '클라우드 자산 관리',
description: 'AWS, Azure, GCP 등 클라우드 인프라 자원 및 구독 서비스 이용 현황을 관리합니다.',
icon: 'cloud'
},
'비용관리': {
title: 'IT 비용 집행 관리',
description: '전산 자산 도입 및 유지보수에 소요되는 정기/비정기 지출 비용을 통합 관리합니다.',
icon: 'credit-card'
},
'선물': {
title: '내빈/외빈 선물 관리',
description: '내외빈 방문 시 지급되는 기념품 및 선물용 자산의 재고와 지급 이력을 관리합니다.',
icon: 'gift'
},
'사무가구': {
title: '사무용 가구 관리',
description: '책상, 의자, 캐비닛 등 사무 환경 구성을 위한 가구 자산의 배치 현황을 관리합니다.',
icon: 'armchair'
}
};
/**

View File

@@ -2,264 +2,214 @@ import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog } from './excelHandle
// --- State Definitions ---
export interface MasterAssetData {
pc: HardwareAsset[];
server: HardwareAsset[];
storage: HardwareAsset[];
equip: HardwareAsset[];
mobile: HardwareAsset[];
subSw: SoftwareAsset[];
permSw: SoftwareAsset[];
cloud: SoftwareAsset[]; // 클라우드 배열 추가
users: any[];
pc: any[];
server: any[];
storage: any[];
network: any[];
survey: any[];
pcParts: any[];
equipment: any[];
officeSupplies: any[];
swInternal: any[];
swExternal: any[];
cloud: any[];
domain: any[];
cost: any[];
vip: any[];
// Backward compatibility
subSw: any[];
permSw: any[];
swUsers: SWUser[];
logs: HardwareLog[];
domain: any[];
// 동료 코드 호환용 통합 배열 (프론트엔드 로직용)
hw: HardwareAsset[];
sw: SoftwareAsset[];
// 통합 배열
hw: any[];
sw: any[];
}
export interface AppState {
activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops';
activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW', '클라우드'
activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops' | 'vip' | 'fac' | 'users' | 'etc';
activeSubTab: string;
masterData: MasterAssetData;
activeCharts?: any[];
activeCharts: any[];
}
// 초기 상태
export const state: AppState = {
activeCategory: 'hw',
activeSubTab: '대시보드',
activeSubTab: '서버', // 대시보드 제거됨에 따라 기본값 변경
activeCharts: [],
masterData: {
pc: [],
server: [],
storage: [],
equip: [],
mobile: [],
subSw: [],
permSw: [],
cloud: [],
hw: [], // 호환용
sw: [], // 호환용
swUsers: [],
logs: [],
domain: []
users: [],
pc: [], server: [], storage: [], network: [],
survey: [], pcParts: [], equipment: [], officeSupplies: [],
swInternal: [], swExternal: [], cloud: [], domain: [],
cost: [], vip: [],
subSw: [], permSw: [],
hw: [], sw: [],
swUsers: [], logs: []
}
};
/**
* 전용 API 엔드포인트들로부터 데이터 로드 (Modernized Paths)
* 신규 14개 테이블 구조에 맞춘 데이터 로드
*/
export async function loadMasterDataFromDB() {
try {
const endpoints = [
{ key: 'pc', url: `http://${location.hostname}:3000/api/pc` },
{ key: 'server', url: `http://${location.hostname}:3000/api/server` },
{ key: 'storage', url: `http://${location.hostname}:3000/api/storage` },
{ key: 'equip', url: `http://${location.hostname}:3000/api/equip` },
{ key: 'mobile', url: `http://${location.hostname}:3000/api/mobile` },
{ key: 'subSw', url: `http://${location.hostname}:3000/api/asset/software/subscription` },
{ key: 'permSw', url: `http://${location.hostname}:3000/api/asset/software/perpetual` },
{ key: 'cloud', url: `http://${location.hostname}:3000/api/asset/cloud` },
{ key: 'domain', url: `http://${location.hostname}:3000/api/asset/domain` },
{ key: 'swUsers', url: `http://${location.hostname}:3000/api/asset/software/assignment` },
{ key: 'logs', url: `http://${location.hostname}:3000/api/asset/history` }
{ key: 'users', url: '/api/users' },
{ key: 'pc', url: '/api/pc' },
{ key: 'server', url: '/api/server' },
{ key: 'storage', url: '/api/storage' },
{ key: 'network', url: '/api/network' },
{ key: 'survey', url: '/api/survey' },
{ key: 'pcParts', url: '/api/pc-parts' },
{ key: 'equipment', url: '/api/equipment' },
{ key: 'officeSupplies', url: '/api/office-supplies' },
{ key: 'swInternal', url: '/api/sw/internal' },
{ key: 'swExternal', url: '/api/sw/external' },
{ key: 'cloud', url: '/api/cloud' },
{ key: 'domain', url: '/api/domain' },
{ key: 'cost', url: '/api/cost' },
{ key: 'vip', url: '/api/vip' },
{ key: 'swUsers', url: '/api/asset/software/assignment' },
{ key: 'logs', url: '/api/asset/history' }
];
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 = [];
const host = `http://${location.hostname}:3000`;
const results = await Promise.all(endpoints.map(e => fetch(host + e.url)));
for (let i = 0; i < endpoints.length; i++) {
if (results[i].ok) {
const data = await results[i].json();
const key = endpoints[i].key;
console.log(`📡 Loaded ${key}: ${Array.isArray(data) ? data.length : 'not an array'} items`);
if (['pc', 'server', 'storage', 'equip', 'mobile'].includes(key)) {
(Array.isArray(data) ? data : []).forEach(asset => saveHardwareAsset(asset));
} else {
(state.masterData as any)[key] = Array.isArray(data) ? data : [];
}
} else {
console.error(`❌ Failed to load ${endpoints[i].key}: ${results[i].status} ${results[i].statusText}`);
(state.masterData as any)[key] = Array.isArray(data) ? data : [];
}
}
// 동료 코드 호환을 위한 통합 sw 배열 생성
state.masterData.sw = [
...state.masterData.subSw,
...state.masterData.permSw,
...state.masterData.cloud
];
// Mapping for backward compatibility
state.masterData.equip = state.masterData.equipment;
state.masterData.subSw = state.masterData.swExternal;
state.masterData.permSw = state.masterData.swInternal;
// 하드웨어 통합 배열 생성 (대시보드 등에서 사용)
// 하드웨어 통합 (대시보드 호환용)
state.masterData.hw = [
...state.masterData.pc,
...state.masterData.server,
...state.masterData.storage,
...state.masterData.equip,
...state.masterData.mobile
...state.masterData.network,
...state.masterData.survey,
...state.masterData.equipment,
...state.masterData.officeSupplies
];
console.log('✅ 모든 DB 데이터 로드 및 통합 완료');
// 소프트웨어 통합
state.masterData.sw = [
...state.masterData.swInternal,
...state.masterData.swExternal,
...state.masterData.cloud
];
console.log('✅ All data (including users) loaded and unified');
return true;
} catch (err) {
console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.');
console.warn('⚠️ 서버 연결 실패:', err);
}
return false;
}
// --- State Helpers ---
export function updateState(newState: Partial<AppState>) {
Object.assign(state, newState);
}
/**
* 하드웨어 자산 통합 저장 (자동 카테고리 분류)
* 자산 저장 (Generic API)
*/
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
];
}
/**
* 소프트웨어 자산 저장 (API 연동 - 개선된 일괄 저장 경로)
*/
export async function saveSoftwareAsset(asset: SoftwareAsset) {
export async function saveAsset(category: string, asset: any) {
try {
const type = asset.type;
let url = '';
let categoryKey: keyof MasterAssetData = 'subSw';
const endpointMap: Record<string, string> = {
'users': '/api/users/batch',
'pc': '/api/pc/batch',
'server': '/api/server/batch',
'storage': '/api/storage/batch',
'network': '/api/network/batch',
'survey': '/api/survey/batch',
'pcParts': '/api/pc-parts/batch',
'equipment': '/api/equipment/batch',
'officeSupplies': '/api/office-supplies/batch',
'swInternal': '/api/sw/internal/batch',
'swExternal': '/api/sw/external/batch',
'cloud': '/api/cloud/batch',
'domain': '/api/domain/batch',
'cost': '/api/cost/batch',
'vip': '/api/vip/batch'
};
if (type === '구독SW') {
url = `http://${location.hostname}:3000/api/asset/software/subscription/batch`;
categoryKey = 'subSw';
} else if (type === '영구SW') {
url = `http://${location.hostname}:3000/api/asset/software/perpetual/batch`;
categoryKey = 'permSw';
} else {
url = `http://${location.hostname}:3000/api/asset/cloud/batch`;
categoryKey = 'cloud';
}
const arr = state.masterData[categoryKey] as SoftwareAsset[];
const idx = arr.findIndex(a => a.id === asset.id);
if (idx > -1) arr[idx] = asset;
else arr.push(asset);
const url = `http://${location.hostname}:3000${endpointMap[category]}`;
const currentList = [...(state.masterData as any)[category]];
const idx = currentList.findIndex(a => a.id === asset.id);
if (idx > -1) currentList[idx] = asset;
else currentList.push(asset);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(arr)
body: JSON.stringify(currentList)
});
if (response.ok) {
state.masterData.sw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud];
return true;
await loadMasterDataFromDB(); // 전역 상태 갱신
return true;
}
} catch (err) {
console.error('SW 저장 실패:', err);
console.error('자산 저장 실패:', err);
}
return false;
}
/**
* 소프트웨어 자산 삭제 (API 연동 - 개선된 일괄 저장 경로)
* 자산 삭제 (Generic API - Batch 방식 활용)
*/
export async function deleteSoftwareAsset(type: string, id: string) {
export async function deleteAsset(category: string, assetId: string) {
try {
const key = type === '구독SW' ? 'subSw' : (type === '영구SW' ? 'permSw' : 'cloud');
const path = type === '구독SW' ? 'subscription' : (type === '영구SW' ? 'perpetual' : 'cloud');
const arr = state.masterData[key] as SoftwareAsset[];
const filtered = arr.filter(a => a.id !== id);
const endpointMap: Record<string, string> = {
'users': '/api/users/batch',
'pc': '/api/pc/batch',
'server': '/api/server/batch',
'storage': '/api/storage/batch',
'network': '/api/network/batch',
'survey': '/api/survey/batch',
'pcParts': '/api/pc-parts/batch',
'equipment': '/api/equipment/batch',
'officeSupplies': '/api/office-supplies/batch',
'swInternal': '/api/sw/internal/batch',
'swExternal': '/api/sw/external/batch',
'cloud': '/api/cloud/batch',
'domain': '/api/domain/batch',
'cost': '/api/cost/batch',
'vip': '/api/vip/batch'
};
const response = await fetch(`http://${location.hostname}:3000/api/asset/software/${path}/batch`, {
const url = `http://${location.hostname}:3000${endpointMap[category]}`;
const currentList = [...(state.masterData as any)[category]];
const filteredList = currentList.filter(a => a.id !== assetId);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filtered)
body: JSON.stringify(filteredList)
});
if (response.ok) {
(state.masterData as any)[key] = filtered;
state.masterData.sw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud];
return true;
await loadMasterDataFromDB(); // 전역 상태 갱신
return true;
}
} catch (err) {
console.error('SW 삭제 실패:', err);
console.error('자산 삭제 실패:', err);
}
return false;
}

View File

@@ -33,7 +33,7 @@ export function setupTableSorting(
th.classList.remove('asc', 'desc');
}
th.onclick = () => {
(th as HTMLElement).onclick = () => {
let nextDirection: SortDirection = 'asc';
if (currentState.key === key) {

View File

@@ -1,7 +1,27 @@
import { PAGE_DESCRIPTIONS } from './schema';
/**
* ITAM 공통 유틸리티 함수
*/
/**
* 페이지 헤더(타이틀 및 설명) 렌더링
*/
export function renderPageHeader(container: HTMLElement, pageId: string) {
const config = PAGE_DESCRIPTIONS[pageId];
if (!config) return;
const header = document.createElement('div');
header.className = 'page-header';
header.innerHTML = `
<div class="page-title-group">
<h2 class="page-title"><i data-lucide="${config.icon}"></i> ${config.title}</h2>
<p class="page-description">${config.description}</p>
</div>
`;
container.appendChild(header);
}
/**
* 숫자에 천 단위 콤마 추가 (금액 표시용)
*/
@@ -131,20 +151,11 @@ export function dynamicSort<T>(list: T[], key: string, direction: 'asc' | 'desc'
}
/**
* 목록 뷰용 액션 버튼 HTML 생성 (양식, 업로드, 엑셀저장, 자산추가)
* 목록 뷰용 액션 버튼 HTML 생성 (자산추가)
*/
export function getActionButtonsHTML(): string {
return `
<div class="search-actions">
<button id="btn-download-template" class="btn btn-outline" title="통합 양식 다운로드">
<i data-lucide="download"></i> 양식
</button>
<label for="excel-upload" class="btn btn-outline" title="엑셀 파일 업로드">
<i data-lucide="upload"></i> 업로드
</label>
<button id="btn-export-excel" class="btn btn-primary" title="일괄 엑셀 저장">
<i data-lucide="file-spreadsheet"></i> 엑셀저장
</button>
<button id="btn-add-asset" class="btn btn-primary">
<i data-lucide="plus"></i> 자산추가
</button>