From 153e42218022cbcb3a3465a57535b6551bdb777e Mon Sep 17 00:00:00 2001 From: Taehoon Date: Tue, 21 Apr 2026 10:30:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=90=EC=82=B0=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B3=A0=EB=8F=84=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모바일 자산(Mobile) 카테고리 추가 및 엑셀 업로드/다운로드 지원 - 클라우드 자산(Cloud) 및 변경 이력(Logs) 테이블 및 API 구현 - 데이터베이스 초기화 로직 개선 및 테이블 자동 생성 기능 추가 - 하드웨어 저장 로직 통합 및 카테고리 판별 자동화 - SW 대시보드 사용량 산출 방식 개선 (sw_id 기반 맵핑) - 수동 모달(Storage)을 통합 하드웨어 모달(HWModal)로 통합 및 정리 --- db_init.js | 49 ++++-- server.js | 249 +++++++++++++++-------------- src/core/dummyDataGenerator.ts | 239 +++++++++------------------ src/core/excelHandler.ts | 39 ++--- src/core/state.ts | 62 +++++-- src/main.ts | 26 +-- src/views/Dashboard/SwDashboard.ts | 45 ++---- 7 files changed, 349 insertions(+), 360 deletions(-) diff --git a/db_init.js b/db_init.js index bc3d512..0aa41e8 100644 --- a/db_init.js +++ b/db_init.js @@ -18,7 +18,10 @@ async function initDB() { console.log('🔄 DB 초기화 시작 (표준화 스키마 적용)...'); // 기존 테이블 삭제 - const tablesToDrop = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets', 'sw_sub_assets', 'sw_perm_assets', 'sw_users']; + const tablesToDrop = [ + 'pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets', + 'sw_sub_assets', 'sw_perm_assets', 'cloud_assets', 'sw_users', 'asset_logs' + ]; for (const table of tablesToDrop) { await connection.query(`DROP TABLE IF EXISTS ${table}`); } @@ -65,7 +68,7 @@ async function initDB() { await connection.query(createHardwareTable('mobile_assets', '모바일기기 자산')); // 소프트웨어 구독 테이블 - const createSubSwTable = ` + await connection.query(` CREATE TABLE sw_sub_assets ( id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100) COMMENT '구매법인', @@ -80,10 +83,10 @@ async function initDB() { remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - `; + `); // 소프트웨어 영구 테이블 - const createPermSwTable = ` + await connection.query(` CREATE TABLE sw_perm_assets ( id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100) COMMENT '구매법인', @@ -97,10 +100,28 @@ async function initDB() { remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - `; + `); + + // 클라우드 자산 테이블 + await connection.query(` + CREATE TABLE cloud_assets ( + id VARCHAR(50) PRIMARY KEY, + platform_name VARCHAR(100), + corp VARCHAR(100), + dept VARCHAR(100), + product_name VARCHAR(255), + account_name VARCHAR(255), + pay_method VARCHAR(100), + pay_day VARCHAR(50), + card_num VARCHAR(100), + monthly_fee VARCHAR(100), + remarks TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); // 소프트웨어 사용자 매핑 테이블 - const createSwUsersTable = ` + await connection.query(` CREATE TABLE sw_users ( id INT AUTO_INCREMENT PRIMARY KEY, sw_id VARCHAR(50) COMMENT 'SW 자산 ID', @@ -112,11 +133,19 @@ async function initDB() { doc_name VARCHAR(255) COMMENT '신청서명', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - `; + `); - await connection.query(createSubSwTable); - await connection.query(createPermSwTable); - await connection.query(createSwUsersTable); + // 변경 이력 테이블 + await connection.query(` + CREATE TABLE asset_logs ( + id VARCHAR(50) PRIMARY KEY, + asset_id VARCHAR(50), + log_date VARCHAR(50), + log_user VARCHAR(100), + details TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); console.log('✅ 모든 테이블이 표준화된 스키마로 재생성되었습니다.'); await connection.end(); diff --git a/server.js b/server.js index 9a7df63..103bafb 100644 --- a/server.js +++ b/server.js @@ -22,6 +22,42 @@ const pool = mysql.createPool({ queueLimit: 0 }); +// 테이블 존재 여부 확인 및 자동 생성 +async function ensureTables() { + const connection = await pool.getConnection(); + try { + await connection.query(` + CREATE TABLE IF NOT EXISTS cloud_assets ( + id VARCHAR(50) PRIMARY KEY, + platform_name VARCHAR(100), + corp VARCHAR(100), + dept VARCHAR(100), + product_name VARCHAR(255), + account_name VARCHAR(255), + pay_method VARCHAR(100), + pay_day VARCHAR(50), + card_num VARCHAR(100), + monthly_fee VARCHAR(100), + remarks TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + await connection.query(` + CREATE TABLE IF NOT EXISTS asset_logs ( + id VARCHAR(50) PRIMARY KEY, + asset_id VARCHAR(50), + log_date VARCHAR(50), + log_user VARCHAR(100), + details TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + console.log('✅ Cloud & Logs tables ensured.'); + } finally { + connection.release(); + } +} + // 공통 배치 저장 로직 async function batchSave(tableName, assets, getQuery) { const connection = await pool.getConnection(); @@ -42,81 +78,45 @@ async function batchSave(tableName, assets, getQuery) { } } -// 공통 하드웨어 매핑 함수 -const mapHardware = (r, defaultType) => ({ - id: r.id, - 법인: r.corp, - 자산코드: r.asset_code, - 구매일: r.purchase_date, - purchase_date: r.purchase_date, - type: r.type || defaultType, - 상세용도: r.detail_purpose, - detail_purpose: r.detail_purpose, - 용도: r.purpose, - purpose: r.purpose, - 상세: r.details, - details: r.details, - 현사용조직: r.current_org, - current_org: r.current_org, - 이전사용조직: r.prev_org, - prev_org: r.prev_org, - 위치: r.location, - location: r.location, - 담당자_정: r.manager_main, - manager_main: r.manager_main, - 담당자_부: r.manager_sub, - manager_sub: r.manager_sub, - IP주소: r.ip_address, - ip_address: r.ip_address, - 원격접속: r.remote_tool, - remote_tool: r.remote_tool, - 서버ID: r.server_id, - server_id: r.server_id, - 서버PW: r.server_pw, - server_pw: r.server_pw, - 모델명: r.model_name, - model_name: r.model_name, - OS: r.os, - os: r.os, - CPU: r.cpu, - cpu: r.cpu, - RAM: r.ram, - ram: r.ram, - GPU: r.gpu, - gpu: r.gpu, - SSD1: r.storage1, - storage1: r.storage1, - SSD2: r.storage2, - storage2: r.storage2, - HDD1: r.storage3, - storage3: r.storage3, - 모니터링: r.monitoring, - monitoring: r.monitoring, - 금액: r.price, - price: r.price, - 비고: r.remarks, - remarks: r.remarks -}); +// 하드웨어 쿼리 헬퍼 +const hardwareInsertSQL = (table) => ` + INSERT INTO ${table} ( + id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details, + current_org, prev_org, location, manager_main, manager_sub, ip_address, + remote_tool, server_id, server_pw, model_name, os, cpu, ram, gpu, + storage1, storage2, storage3, monitoring, price, remarks + ) VALUES ? +`; -// 공통 하드웨어 저장 값 생성 함수 const getHardwareValues = (a) => [ - a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'', a.현사용조직||'', a.이전사용조직||'', a.위치||'', - a.담당자_정||'', a.담당자_부||'', a.IP주소||'', a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'', + a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.type||'', a.상세용도||'', 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.금액||'', a.비고||'' ]; -const hardwareInsertSQL = (table) => ` - INSERT INTO ${table} - (id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details, current_org, prev_org, location, manager_main, manager_sub, ip_address, remote_tool, server_id, server_pw, model_name, os, cpu, ram, gpu, storage1, storage2, storage3, monitoring, price, remarks) - VALUES ? -`; +const mapHardware = (r, defaultType) => ({ + id: r.id, 법인: r.corp, 자산코드: r.asset_code, 구매일: r.purchase_date, type: r.type || defaultType, + 상세용도: r.detail_purpose, 용도: r.purpose, 상세: r.details, 현사용조직: r.current_org, + 이전사용조직: r.prev_org, 위치: r.location, 담당자_정: r.manager_main, 담당자_부: r.manager_sub, + IP주소: r.ip_address, 원격접속: r.remote_tool, 서버ID: r.server_id, 서버PW: r.server_pw, + 모델명: r.model_name, OS: r.os, CPU: r.cpu, RAM: r.ram, GPU: r.gpu, SSD1: r.storage1, + SSD2: r.storage2, HDD1: r.storage3, 모니터링: r.monitoring, 금액: r.price, 비고: r.remarks +}); -// --- 1. 개인PC API --- +// --- API 라우트 정의 --- + +// PC API app.get('/api/pc', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM pc_assets'); - res.json(rows.map(r => mapHardware(r, 'PC'))); - } catch (err) { res.status(500).json({ error: err.message }); } + console.log('🔍 DB Raw Rows (PC):', rows.length, 'items found.'); + if (rows.length > 0) console.log('🔍 First row sample:', rows[0]); + res.json(rows.map(r => mapHardware(r, '개인PC'))); + } catch (err) { + console.error('❌ DB Query Error (PC):', err.message); + res.status(500).json({ error: err.message }); + } }); app.post('/api/pc/batch', async (req, res) => { @@ -129,7 +129,7 @@ app.post('/api/pc/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// --- 2. 서버 API --- +// 서버 API app.get('/api/server', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM server_assets'); @@ -147,7 +147,7 @@ app.post('/api/server/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// --- 3. 스토리지 API --- +// 스토리지 API app.get('/api/storage', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM storage_assets'); @@ -165,7 +165,7 @@ app.post('/api/storage/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// --- 4. 전산비품 API --- +// 전산비품 API app.get('/api/equip', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM equip_assets'); @@ -183,7 +183,7 @@ app.post('/api/equip/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// --- 5. 모바일기기 API --- +// 모바일 API app.get('/api/mobile', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM mobile_assets'); @@ -201,16 +201,15 @@ app.post('/api/mobile/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// --- 6. 소프트웨어 구독 API --- +// 구독 SW API app.get('/api/sw/sub', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM sw_sub_assets'); - const mapped = rows.map(r => ({ + res.json(rows.map(r => ({ id: r.id, type: '구독SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name, 라이선스유형: r.license_type, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date, 만료일: r.expiry_date, 납품업체: r.vendor, 비고: r.remarks - })); - res.json(mapped); + }))); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -224,16 +223,15 @@ app.post('/api/sw/sub/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// --- 7. 소프트웨어 영구 API --- +// 영구 SW API app.get('/api/sw/perm', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM sw_perm_assets'); - const mapped = rows.map(r => ({ + res.json(rows.map(r => ({ id: r.id, type: '영구SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name, 라이선스키: r.license_key, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date, 납품업체: r.vendor, 비고: r.remarks - })); - res.json(mapped); + }))); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -247,15 +245,58 @@ app.post('/api/sw/perm/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// --- 8. 소프트웨어 사용자 관리 API --- +// 클라우드 API +app.get('/api/cloud', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM cloud_assets'); + res.json(rows.map(r => ({ + id: r.id, type: '클라우드', 플랫폼명: r.platform_name, 법인: r.corp, 부서: r.dept, + 제품명: r.product_name, 계정명: r.account_name, 결제수단: r.pay_method, + 결제일: r.pay_day, 연결카드번호: r.card_num, 당월청구액: r.monthly_fee, 비고: r.remarks + }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/cloud/batch', async (req, res) => { + try { + const result = await batchSave('cloud_assets', req.body, (assets) => ({ + sql: `INSERT INTO cloud_assets (id, platform_name, corp, dept, product_name, account_name, pay_method, pay_day, card_num, monthly_fee, remarks) VALUES ?`, + values: assets.map(a => [a.id, a.플랫폼명||'', a.법인||'', a.부서||'', a.제품명||'', a.계정명||'', a.결제수단||'', a.결제일||'', a.연결카드번호||'', a.당월청구액||'', a.비고||'']) + })); + res.json(result); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// 로그 API +app.get('/api/logs', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM asset_logs ORDER BY log_date DESC'); + res.json(rows.map(r => ({ + id: r.id, assetId: r.asset_id, date: r.log_date, user: r.log_user, details: r.details + }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/logs/batch', async (req, res) => { + try { + const result = await batchSave('asset_logs', req.body, (assets) => ({ + sql: `INSERT INTO asset_logs (id, asset_id, log_date, log_user, details) VALUES ?`, + values: assets.map(a => [a.id, a.assetId||'', a.date||'', a.user||'', a.details||'']) + })); + res.json(result); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// SW 사용자 API app.get('/api/sw-users', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM sw_users'); - const result = rows.map(u => ({ - sw_id: u.sw_id, - userData: [u.corp||'', u.dept||'', u.position||'', u.user_name||'', u.usage_period||'', u.doc_name||''] - })); - res.json(result); + const grouped = rows.reduce((acc, u) => { + if (!acc[u.sw_id]) acc[u.sw_id] = []; + acc[u.sw_id].push([u.corp, u.dept, u.position, u.user_name, u.usage_period, u.doc_name]); + return acc; + }, {}); + res.json(Object.keys(grouped).map(sw_id => ({ sw_id, userData: grouped[sw_id] }))); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -267,7 +308,7 @@ app.post('/api/sw-users/batch', async (req, res) => { const allUsers = req.body; if (allUsers.length > 0) { const values = allUsers.flatMap(item => - item.userDataList.map(u => [item.sw_id, u.구매법인||u.법인||'', u.부서||'', u.직위||'', u.이름||'', u.사용기간||'', u.신청서명||'']) + (item.userData || []).map(u => [item.sw_id, u[0], u[1], u[2], u[3], u[4], u[5]]) ); if (values.length > 0) { await connection.query('INSERT INTO sw_users (sw_id, corp, dept, position, user_name, usage_period, doc_name) VALUES ?', [values]); @@ -279,35 +320,11 @@ app.post('/api/sw-users/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// 자산번호 자동 생성 API -app.get('/api/generate-asset-code', async (req, res) => { - const { prefix } = req.query; - if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); - - try { - const tables = ['server_assets', 'pc_assets', 'storage_assets', 'equip_assets', 'mobile_assets']; - let maxNum = 0; - - for (const table of tables) { - const [rows] = await pool.query( - `SELECT asset_code as 자산코드 FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`, - [`${prefix}%`] - ); - - if (rows.length > 0) { - const lastCode = rows[0].자산코드; - const lastNum = parseInt(lastCode.split('-').pop() || '0'); - if (lastNum > maxNum) maxNum = lastNum; - } - } - - const nextNum = String(maxNum + 1).padStart(3, '0'); - res.json({ nextCode: `${prefix}${nextNum}` }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -app.listen(PORT, () => { - console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`); +// 초기화 및 서버 기동 +ensureTables().then(() => { + app.listen(PORT, () => { + console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`); + }); +}).catch(err => { + console.error('❌ Failed to start server:', err); }); diff --git a/src/core/dummyDataGenerator.ts b/src/core/dummyDataGenerator.ts index cb163d2..fc421cd 100644 --- a/src/core/dummyDataGenerator.ts +++ b/src/core/dummyDataGenerator.ts @@ -20,15 +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), @@ -53,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), @@ -87,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), @@ -112,168 +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 0.5 ? '법인카드' : '인보이스', - 결제일: paymentDay, - 연결카드번호: String(Math.floor(Math.random() * 8999) + 1000), // 1000~9999 - 당월청구액: String(billing), - 비고: Math.random() > 0.8 ? '비용 한도 초과 경고' : '', - - // 더미 필수값 - 구매일: '', - 금액: '', - 수량: 1, - 납품업체: '' - }); - - // 4개월치 모의 결제 이력 생성 - for (let m = 0; m < 4; m++) { - const logDate = new Date(); - logDate.setMonth(logDate.getMonth() - m); - logDate.setDate(parseInt(paymentDay, 10)); - const historyBilling = Math.floor(billing * (1 + (Math.random() * 0.2 - 0.1))); - logs.push({ - id: Math.random().toString(36).substring(2, 9), - assetId: swId, - date: `${logDate.getFullYear()}-${String(logDate.getMonth()+1).padStart(2,'0')}-${String(logDate.getDate()).padStart(2,'0')}`, - user: `admin_${i}@hm.com`, - details: `정기 결제 완료 (비용: ₩ ${historyBilling.toLocaleString()})` - }); - } - } - - return { hw, sw, swUsers, logs }; + return { pc, server, storage, equip, mobile, subSw, permSw, swUsers, logs }; } diff --git a/src/core/excelHandler.ts b/src/core/excelHandler.ts index bf7da2f..2ffdfa6 100644 --- a/src/core/excelHandler.ts +++ b/src/core/excelHandler.ts @@ -2,7 +2,7 @@ import * as XLSX from 'xlsx'; export interface HardwareAsset { id: string; - type: string; // '개인PC', '서버', '스토리지', '전산비품' + type: string; // '개인PC', '서버', '스토리지', '전산비품', '모바일기기' 법인: string; 자산코드: string; 명칭: string; @@ -51,6 +51,9 @@ export interface SoftwareAsset { 제품명: string; 구매일: string; 구독일?: string; + 만료일?: string; + 라이선스유형?: string; + 라이선스키?: string; 유지보수여부?: boolean; 금액: string; 수량: number; @@ -66,7 +69,7 @@ export interface SoftwareAsset { export interface SWUser { id: string; - swId: string; + sw_id: string; 법인: string; 부서: string; 팀: string; @@ -74,7 +77,7 @@ export interface SWUser { 이름: string; 사용기간: string; 신청서명: string; - userData?: any[]; // 동료 작업 호환용 + userData?: any[]; } export interface HardwareLog { @@ -93,22 +96,22 @@ export interface MasterAssetData { mobile: HardwareAsset[]; subSw: SoftwareAsset[]; permSw: SoftwareAsset[]; - swUsers: SWUser[]; + swUsers: any[]; // { sw_id, userData: [] } 형태로 처리 logs: HardwareLog[]; } -const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품']; +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 주소 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 = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고']; -const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고']; +const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '만료일', '라이선스유형', '금액', '수량', '계정명', '납품업체', '비고']; +const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '라이선스키', '금액', '수량', '계정명', '납품업체', '비고']; const CLOUD_HEADERS = ['ID', '플랫폼명', '법인', '부서', '사용용도(제품명)', '계정명', '결제수단', '결제일', '연결카드번호', '당월청구액', '비고']; -const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명']; export function downloadTemplate() { const wb = XLSX.utils.book_new(); @@ -116,7 +119,8 @@ export function downloadTemplate() { { name: '개인PC', headers: PC_HEADERS }, { name: '서버', headers: SERVER_HEADERS }, { name: '스토리지', headers: STORAGE_HEADERS }, - { name: '전산비품', headers: EQUIP_HEADERS } + { name: '전산비품', headers: EQUIP_HEADERS }, + { name: '모바일기기', headers: MOBILE_HEADERS } ]; tabConfigs.forEach(config => { @@ -132,8 +136,6 @@ export function downloadTemplate() { XLSX.utils.book_append_sheet(wb, ws, tab); }); - const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS]); - XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자'); XLSX.writeFile(wb, 'itam_assets_template_full.xlsx'); } @@ -144,8 +146,9 @@ export function exportToExcel(masterData: MasterAssetData) { { tab: '서버', list: masterData.server, headers: SERVER_HEADERS, map: (a: any) => [a.법인, a.자산코드, a.구매일, a.storage유형 || '물리', 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.HDD1, a.모니터링, a.비고] }, { tab: '스토리지', list: masterData.storage, headers: STORAGE_HEADERS, map: (a: any) => [a.법인, a.storage유형, 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: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a.id, a.분야, a.법인, a.부서, a.제품명, a.구매일, a.구독일, a.금액, a.수량, a.계정명, a.납품업체, a.비고] }, - { tab: '영구SW', list: masterData.permSw, headers: PERM_SW_HEADERS, map: (a: any) => [a.id, a.분야, a.법인, a.부서, a.제품명, a.구매일, a.유지보수여부 ? 'Y' : 'N', a.금액, a.수량, a.계정명, a.납품업체, a.비고] } + { tab: '모바일기기', list: masterData.mobile, headers: MOBILE_HEADERS, map: (a: any) => [a.법인, a.자산코드, a.명칭, a.위치, a.관리자, a.type, a.OS, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고] }, + { tab: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a.id, 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.id, a.분야, a.법인, a.부서, a.제품명, a.구매일, a.라이선스키, a.금액, a.수량, a.계정명, a.납품업체, a.비고] } ]; exportMap.forEach(m => { @@ -167,17 +170,17 @@ export async function parseExcel(file: File): Promise { if (sheetName === '개인PC') { rows.forEach(r => data.pc.push({ id: Math.random().toString(36).substring(2, 9), type: '개인PC', 법인: 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['비고']||'', 관리자: '', MACaddress: '', OS: '', 명칭: '' })); } else if (sheetName === '서버') { - rows.forEach(r => data.server.push({ id: Math.random().toString(36).substring(2, 9), type: '서버', 법인: r['구매법인']||r['법인']||'', 자산코드: r['자산번호']||r['자산코드']||'', 구매일: r['구매일자']||r['구매일']||'', storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['설치위치']||r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소 1']||r['IP주소']||'', 원격접속: r['원격도구']||r['원격접속']||'', 서버ID: r['서버 ID']||r['서버ID']||'', 서버PW: r['서버 PW']||r['서버PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'', SSD1: r['Storage 1']||r['SSD1']||'', SSD2: r['Storage 2']||r['SSD2']||'', HDD1: r['Storage 3']||r['HDD1']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'', 관리자: '', 명칭: '', MACaddress: '', HW사양: '', 금액: '', 납품업체: '', 품의서명: '' })); + rows.forEach(r => data.server.push({ id: Math.random().toString(36).substring(2, 9), type: '서버', 법인: r['구매법인']||r['법인']||'', 자산코드: r['자산번호']||r['자산코드']||'', 구매일: r['구매일자']||r['구매일']||'', storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['설치위치']||r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소 1']||r['IP주소']||'', IP2: r['IP 주소 2']||'', 원격접속: r['원격도구']||r['원격접속']||'', 서버ID: r['서버 ID']||r['서버ID']||'', 서버PW: r['서버 PW']||r['서버PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'', SSD1: r['Storage 1']||r['SSD1']||'', SSD2: r['Storage 2']||r['SSD2']||'', HDD1: r['Storage 3']||r['HDD1']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'', 관리자: '', 명칭: '', MACaddress: '', HW사양: '', 금액: '', 납품업체: '', 품의서명: '' })); } else if (sheetName === '스토리지') { rows.forEach(r => data.storage.push({ id: Math.random().toString(36).substring(2, 9), type: '스토리지', 법인: r['구매법인']||r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'', HW사양: '', OS: '', 관리자: '' })); } else if (sheetName === '전산비품') { rows.forEach(r => data.equip.push({ id: Math.random().toString(36).substring(2, 9), type: '전산비품', 법인: r['구매법인']||r['법인']||'', 비품유형: r['비품유형']||r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'', 구매일: r['구매일']||'', 금액: 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['관리자']||'', OS: r['OS']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'', IP주소: '', MACaddress: '', HW사양: '' })); } 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['구독일']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' })); + rows.forEach(r => data.subSw.push({ id: r['ID']||Math.random().toString(36).substring(2, 9), type: '구독SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 만료일: r['만료일']||'', 라이선스유형: r['라이선스유형']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: 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['구매일']||'', 유지보수여부: r['유지보수여부']==='Y', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: 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['이름']||'', 사용기간: r['사용기간']||'', 신청서명: r['신청서명']||'' })); + rows.forEach(r => data.permSw.push({ id: r['ID']||Math.random().toString(36).substring(2, 9), type: '영구SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 라이선스키: r['라이선스키']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' })); } }); resolve(data); diff --git a/src/core/state.ts b/src/core/state.ts index 0e0bea4..fd5d181 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -9,13 +9,17 @@ export interface MasterAssetData { mobile: HardwareAsset[]; subSw: SoftwareAsset[]; permSw: SoftwareAsset[]; + cloud: SoftwareAsset[]; // 클라우드 배열 추가 swUsers: SWUser[]; logs: HardwareLog[]; + + // 동료 코드 호환용 통합 배열 (프론트엔드 로직용) + sw: SoftwareAsset[]; } export interface AppState { activeCategory: 'dashboard' | 'hw' | 'sw'; - activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW' + activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW', '클라우드' masterData: MasterAssetData; } @@ -31,6 +35,8 @@ export const state: AppState = { mobile: [], subSw: [], permSw: [], + cloud: [], + sw: [], // 호환용 swUsers: [], logs: [] } @@ -49,19 +55,42 @@ export async function loadMasterDataFromDB() { { 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: 'swUsers', url: 'http://localhost:3000/api/sw-users' } + { 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' } ]; 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(); - (state.masterData as any)[endpoints[i].key] = data || []; + 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 || []; + } } } - console.log('✅ 6개 테이블 데이터 로드 완료'); + // 동료 코드 호환을 위한 통합 sw 배열 생성 + state.masterData.sw = [ + ...state.masterData.subSw, + ...state.masterData.permSw, + ...state.masterData.cloud + ]; + + console.log('✅ 모든 DB 데이터 로드 및 통합 완료'); return true; } catch (err) { console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.'); @@ -78,18 +107,25 @@ export function updateState(newState: Partial) { * 하드웨어 자산 통합 저장 (자동 카테고리 분류) */ export function saveHardwareAsset(updatedAsset: HardwareAsset) { - const { type } = updatedAsset; - const detailPurpose = (updatedAsset as any).상세용도 || ''; + const type = updatedAsset.type || ''; + const detailPurpose = (updatedAsset as any).상세용도 || updatedAsset.detail_purpose || ''; - // 1. 타겟 카테고리 결정 + // 1. 타겟 카테고리 결정 (유연한 검색) let targetKey: keyof MasterAssetData = 'equip'; - if (type === '서버' || (type === 'PC' && detailPurpose === '서버')) targetKey = 'server'; - else if (['NAS', 'DAS', '스토리지'].includes(type)) targetKey = 'storage'; - else if (['CPU', 'GPU', 'RAM', 'HDD'].includes(type)) targetKey = 'equip'; - else if (['모바일', '태블릿', '노트북'].includes(type)) targetKey = 'mobile'; - else if (type === 'PC' && detailPurpose === '개인PC') targetKey = 'pc'; + + if (type.includes('서버') || detailPurpose.includes('서버')) { + targetKey = 'server'; + } else if (['NAS', 'DAS', '스토리지'].some(t => type.includes(t))) { + targetKey = 'storage'; + } else if (['모바일', '태블릿', '휴대폰', '핸드폰', '노트북'].some(t => type.includes(t))) { + targetKey = 'mobile'; + } else if (type === 'PC' || type === '개인PC' || detailPurpose === '개인PC') { + targetKey = 'pc'; + } else if (['CPU', 'GPU', 'RAM', 'HDD'].some(t => type.toUpperCase().includes(t))) { + targetKey = 'equip'; + } - // 2. 모든 카테고리에서 기존 ID 자산 삭제 (이동 가능성 대비) + // 2. 모든 카테고리에서 기존 ID 자산 삭제 (중복 방지) const hwKeys: (keyof MasterAssetData)[] = ['pc', 'server', 'storage', 'equip', 'mobile']; hwKeys.forEach(key => { const arr = state.masterData[key] as HardwareAsset[]; diff --git a/src/main.ts b/src/main.ts index 5ad030d..f22f246 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,14 +6,13 @@ import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset, SoftwareAss import { initBaseModal } from './components/Modal/BaseModal'; import { initPcModal } from './components/Modal/PCModal'; import { initHwModal, openHwModal } from './components/Modal/HWModal'; -import { initStorageModal } from './components/Modal/StorageModal'; import { initSwModal, openSwModal } from './components/Modal/SWModal'; import { initCloudModal, openCloudModal } from './components/Modal/CloudModal'; import { initSwUserModal } from './components/Modal/SWUserModal'; import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide'; -// --- DB 저장을 위한 세분화된 헬퍼 함수들 --- +// --- DB 저장을 위한 세분화된 헬퍼 함수들 (setting 브랜치 기반) --- async function apiBatchSave(url: string, data: any[], label: string) { try { const response = await fetch(url, { @@ -65,10 +64,9 @@ function initApp() { } }); - // 동료의 새로운 UI 방식(renderSWTable)과 우리의 통합 저장 로직 결합 + // 모달 초기화: StorageModal은 HWModal에 통합됨 initPcModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals); initHwModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals); - initStorageModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals); initSwModal(() => { if (state.activeSubTab === '구독SW') saveSubSwToDB(); @@ -77,7 +75,7 @@ function initApp() { }, closeAllModals); initCloudModal(() => { - // 클라우드 저장 로직 추가 필요시 여기에 구현 + // 클라우드 저장 로직 (필요 시 API 추가 구현 가능) renderSWTable(mainContent); }, closeAllModals); @@ -86,8 +84,10 @@ function initApp() { initDashboardDetailModal(); } catch (e) { console.error('❌ Initialization failed:', e); } + // 초기 로드 시 대시보드 렌더링 renderDashboard(mainContent); + // DB에서 데이터 로드 후 화면 갱신 loadMasterDataFromDB().then((success) => { if (success) { if (state.activeSubTab === '대시보드') renderDashboard(mainContent); @@ -95,6 +95,7 @@ function initApp() { } }); + // 버튼 이벤트 바인딩 document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate()); document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData)); @@ -113,16 +114,17 @@ function initApp() { }); document.getElementById('btn-add-asset')?.addEventListener('click', () => { - if (['개인PC', '서버', '전산비품', '스토리지'].includes(state.activeSubTab)) { + const tab = state.activeSubTab; + if (['개인PC', '서버', '전산비품', '스토리지', '모바일기기'].includes(tab)) { openHwModal({ id: Math.random().toString(36).substring(2, 9), - type: state.activeSubTab, + type: tab, 법인: '한맥', 자산코드: '', 명칭: '', 위치: '', 관리자: '', IP주소: '', MACaddress: '', HW사양: '', OS: '', 납품업체: '', 품의서명: '' } as any, 'add'); - } else if (state.activeSubTab === '클라우드') { - openCloudModal({ type: '클라우드', 제품명: '', 금액: '', 수량: 1, 계정명: '', 납품업체: '', 비고: '', 법인: '한맥', 플랫폼명: '' } as any); - } else if (state.activeSubTab === '구독SW' || state.activeSubTab === '영구SW') { - openSwModal({ type: state.activeSubTab, 제품명: '', 금액: '', 수량: 1, 계정명: '', 납품업체: '', 비고: '', 법인: '한맥' } as any); + } else if (tab === '클라우드') { + openCloudModal({ type: '클라우드', 플랫폼명: '', 법인: '한맥', 부서: '', 제품명: '', 계정명: '', 결제수단: '', 결제일: '', 연결카드번호: '', 당월청구액: '', 비고: '' } as any); + } else if (tab === '구독SW' || tab === '영구SW') { + openSwModal({ type: tab, 제품명: '', 금액: '', 수량: 1, 계정명: '', 납품업체: '', 비고: '', 법인: '한맥' } as any); } }); @@ -131,4 +133,4 @@ function initApp() { }); } -document.addEventListener('DOMContentLoaded', initApp); +document.addEventListener('DOMContentLoaded', initApp); \ No newline at end of file diff --git a/src/views/Dashboard/SwDashboard.ts b/src/views/Dashboard/SwDashboard.ts index 5aec49a..3bd18b8 100644 --- a/src/views/Dashboard/SwDashboard.ts +++ b/src/views/Dashboard/SwDashboard.ts @@ -9,12 +9,7 @@ export function renderSwDashboard(container: HTMLElement) { let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0; let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0; - let thisMonthCloudCost = 0; - let lastMonthCloudCost = 0; - let cloudExp = 0; - const currentYear = new Date().getFullYear(); - const today = new Date(); const corps = ['한맥', '삼안', '바론']; const categories = ['업무공통', '개발S/W', '디자인', '설계S/W']; @@ -23,11 +18,12 @@ export function renderSwDashboard(container: HTMLElement) { const costByCat: Record = {}; categories.forEach(c => costByCat[c] = 0); - // 통합 SW 데이터 (호환용) + // 통합 SW 데이터 const allSw = [...state.masterData.subSw, ...state.masterData.permSw]; allSw.forEach(sw => { - const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; + const userMapping = state.masterData.swUsers.find(u => u.sw_id === sw.id); + const assigned = userMapping ? (userMapping.userData ? userMapping.userData.length : 0) : 0; const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10); const priceStr = sw.금액 ? String(sw.금액).replace(/,/g, '') : '0'; const price = parseInt(priceStr, 10) || 0; @@ -46,22 +42,11 @@ export function renderSwDashboard(container: HTMLElement) { } }); - // 클라우드 데이터 처리 (필요시 추가) - // ... - 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; - const cloudCostTrend = [0, 0, 0, 0]; - const trendLabels: string[] = []; - for(let i=3; i>=0; i--) { - const d = new Date(); - d.setMonth(d.getMonth() - i); - trendLabels.push(`${d.getMonth()+1}월`); - } - container.innerHTML = `

소프트웨어 라이선스 현황

@@ -153,21 +138,17 @@ export function renderSwDashboard(container: HTMLElement) { } function isSWExpiring(sw: SoftwareAsset) { - if (sw.type === '구독SW' && sw.구독일) { - const parts = sw.구독일.split('~'); - if (parts.length > 1) { - const endMs = new Date(normalizeDate(parts[1])).getTime(); - const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); - return diffDays >= 0 && diffDays <= 30; - } - } else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('~')) { - // 임시 로직: 비고란에 날짜가 포함된 경우 + if (sw.type === '구독SW' && sw.만료일) { + const endMs = new Date(normalizeDate(sw.만료일)).getTime(); + const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); + return diffDays >= 0 && diffDays <= 30; + } else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('유지보수: ~')) { try { - const dateMatch = sw.비고.match(/\\d{4}-\\d{2}-\\d{2}/); - if (dateMatch) { - const endMs = new Date(normalizeDate(dateMatch[0])).getTime(); - const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); - return diffDays >= 0 && diffDays <= 30; + const parts = sw.비고.split('~'); + if (parts.length > 1) { + const endMs = new Date(normalizeDate(parts[1].trim())).getTime(); + const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); + return diffDays >= 0 && diffDays <= 30; } } catch { return false; } }