From 82bbe85e231712d3b186d737887723154f808848 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Tue, 26 May 2026 17:33:03 +0900 Subject: [PATCH] feat: migrate ServerPC data to asset_pc, enhance filters with location, and standardize page headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버PC 자산을 asset_pc 테이블로 통합 마이그레이션 및 스키마 확장 (위치, IP 정보 복구 완료) - 하드웨어 자산 페이지의 구매법인 필터를 자산위치 필터로 교체 및 동적 데이터 바인딩 적용 - 모든 자산 리스트 페이지 상단에 설명(Description) 필드 추가 및 헤더 표준화 - 상세 모달 내 삭제 버튼 기능 구현 및 서버PC 용도 필드 노출 오류 수정 - 현 사용조직 필터 리스트가 비어있던 DOM 셀렉터 버그 수정 --- db_init.js | 4 +- expand_pc_schema.js | 52 +++ index.html | 1 - migrate_server_pc.js | 81 ++++ package-lock.json | 1 + package.json | 1 + restore_server_pc_data.js | 83 ++++ server.js | 421 ++++++------------- src/components/Guide.ts | 6 +- src/components/Modal/DashboardDetailModal.ts | 2 +- src/components/Modal/DomainModal.ts | 155 +------ src/components/Modal/HWModal.ts | 132 ++++-- src/components/Modal/ModalUtils.ts | 15 +- src/components/Modal/SWModal.ts | 309 ++++++-------- src/components/Modal/SharedData.ts | 35 +- src/components/Navigation.ts | 2 +- src/core/dummyDataGenerator.ts | 14 +- src/core/excelHandler.ts | 314 ++++++++------ src/core/filterHandler.ts | 104 +++++ src/core/schema.ts | 117 +++++- src/core/state.ts | 314 ++++++-------- src/core/tableHandler.ts | 2 +- src/core/utils.ts | 31 +- src/main.ts | 51 +-- src/styles/modal.css | 102 +++++ src/styles/table.css | 49 +++ src/views/Dashboard/HwDashboard.ts | 66 +-- src/views/Dashboard/SwDashboard.ts | 164 ++------ src/views/List/CloudListView.ts | 81 ++-- src/views/List/CostListView.ts | 77 ++-- src/views/List/DomainListView.ts | 70 ++- src/views/List/EquipmentListView.ts | 103 ++--- src/views/List/FacilityListView.ts | 87 ++-- src/views/List/GiftListView.ts | 67 +-- src/views/List/MobileListView.ts | 74 ++-- src/views/List/NetworkListView.ts | 91 ++-- src/views/List/PcListView.ts | 106 +++-- src/views/List/PcPartListView.ts | 87 ++-- src/views/List/ServerListView.ts | 124 +++--- src/views/List/SpaceInfoListView.ts | 95 +++-- src/views/List/StorageListView.ts | 119 +++--- src/views/List/SwListView.ts | 112 ++--- src/views/SW_Table.ts | 5 +- 43 files changed, 2055 insertions(+), 1871 deletions(-) create mode 100644 expand_pc_schema.js create mode 100644 migrate_server_pc.js create mode 100644 restore_server_pc_data.js create mode 100644 src/core/filterHandler.ts diff --git a/db_init.js b/db_init.js index 16f661d..df98336 100644 --- a/db_init.js +++ b/db_init.js @@ -81,7 +81,7 @@ async function initDB() { purchase_date VARCHAR(50) COMMENT '구매일', start_date VARCHAR(50) COMMENT '시작일', expiry_date VARCHAR(50) COMMENT '만료일', - vendor VARCHAR(255) COMMENT '납품업체', + vendor VARCHAR(255) COMMENT '구매업체', remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -100,7 +100,7 @@ async function initDB() { purchase_date VARCHAR(50) COMMENT '구매일', start_date VARCHAR(50) COMMENT '시작일', expiry_date VARCHAR(50) COMMENT '만료일', - vendor VARCHAR(255) COMMENT '납품업체', + vendor VARCHAR(255) COMMENT '구매업체', remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/expand_pc_schema.js b/expand_pc_schema.js new file mode 100644 index 0000000..28fac2c --- /dev/null +++ b/expand_pc_schema.js @@ -0,0 +1,52 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +async function expandSchema() { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + port: parseInt(process.env.DB_PORT || '3306') + }); + + try { + console.log('🏗️ Expanding asset_pc table schema...'); + + const columnsToAdd = [ + { name: 'location', type: 'TEXT' }, + { name: 'location_detail', type: 'TEXT' }, + { name: 'ip_address', type: 'TEXT' }, + { name: 'ip_address_2', type: 'TEXT' }, + { name: 'remote_tool', type: 'TEXT' }, + { name: 'remote_id', type: 'TEXT' }, + { name: 'remote_pw', type: 'TEXT' }, + { name: 'monitoring', type: 'TEXT' }, + { name: 'asset_purpose', type: 'TEXT' } + ]; + + for (const col of columnsToAdd) { + try { + await connection.query(`ALTER TABLE asset_pc ADD COLUMN \`${col.name}\` ${col.type}`); + console.log(`✅ Added column: ${col.name}`); + } catch (err) { + if (err.code === 'ER_DUP_COLUMN_NAME') { + console.log(`ℹ️ Column ${col.name} already exists.`); + } else { + throw err; + } + } + } + + console.log('🎉 Schema expansion completed!'); + + } catch (err) { + console.error('❌ Schema expansion failed:', err); + } finally { + await connection.end(); + } +} + +expandSchema(); diff --git a/index.html b/index.html index 2706895..5fc7d78 100644 --- a/index.html +++ b/index.html @@ -37,7 +37,6 @@ - diff --git a/migrate_server_pc.js b/migrate_server_pc.js new file mode 100644 index 0000000..60d64a5 --- /dev/null +++ b/migrate_server_pc.js @@ -0,0 +1,81 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +async function migrate() { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + port: parseInt(process.env.DB_PORT || '3306') + }); + + try { + console.log('🚀 ' + 'Starting migration: asset_server (서버PC) -> asset_pc'); + + // 1. 서버PC 데이터 조회 + const [serverPcs] = await connection.query( + "SELECT * FROM asset_server WHERE asset_type = '서버PC'" + ); + + if (serverPcs.length === 0) { + console.log('✅ ' + 'No ServerPC assets found in asset_server table.'); + return; + } + + console.log(`📦 Found ${serverPcs.length} ServerPC assets to migrate.`); + + // 2. asset_pc 컬럼 정보 조회 + const [pcColumnsRows] = await connection.query('DESCRIBE asset_pc'); + const pcColumns = pcColumnsRows.map(r => r.Field); + console.log('📋 Target columns:', pcColumns); + + // 3. asset_pc 테이블로 이동 (category를 'PC'로 변경) + for (const asset of serverPcs) { + const dataToInsert = {}; + + // 공통 컬럼 매핑 + pcColumns.forEach(col => { + if (col === 'category') { + dataToInsert[col] = 'PC'; + } else if (asset.hasOwnProperty(col)) { + dataToInsert[col] = asset[col]; + } + }); + + // 특수한 매핑 (예: model_name -> asset_pc에 컬럼이 없다면 memo에 추가하거나 무시) + // 현재 asset_pc에는 model_name이 없으므로 memo에 보존하는 것이 안전함 + if (asset.model_name) { + const currentMemo = dataToInsert.memo || ''; + dataToInsert.memo = `[모델명: ${asset.model_name}] ${currentMemo}`.trim(); + } + + const columns = Object.keys(dataToInsert).map(col => `\`${col}\``).join(', '); + const placeholders = Object.keys(dataToInsert).map(() => '?').join(', '); + const values = Object.values(dataToInsert); + + await connection.query( + `INSERT INTO asset_pc (${columns}) VALUES (${placeholders})`, + values + ); + console.log(` - Migrated: ${asset.asset_code || asset.id}`); + } + + // 4. asset_server 테이블에서 삭제 + const [deleteResult] = await connection.query( + "DELETE FROM asset_server WHERE asset_type = '서버PC'" + ); + console.log(`🗑️ Deleted ${deleteResult.affectedRows} records from asset_server.`); + + console.log('🎉 Migration completed successfully!'); + + } catch (err) { + console.error('❌ Migration failed:', err); + } finally { + await connection.end(); + } +} + +migrate(); diff --git a/package-lock.json b/package-lock.json index 469f08b..af34cc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "iconv-lite": "^0.7.2", "lucide": "^0.364.0", "mysql2": "^3.22.1", "xlsx": "^0.18.5" diff --git a/package.json b/package.json index 8b0150a..f82fbff 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "iconv-lite": "^0.7.2", "lucide": "^0.364.0", "mysql2": "^3.22.1", "xlsx": "^0.18.5" diff --git a/restore_server_pc_data.js b/restore_server_pc_data.js new file mode 100644 index 0000000..94c482c --- /dev/null +++ b/restore_server_pc_data.js @@ -0,0 +1,83 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; +import xlsx from 'xlsx'; + +dotenv.config(); + +async function restoreData() { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + port: parseInt(process.env.DB_PORT || '3306') + }); + + try { + console.log('🔄 Restoring ServerPC metadata (Final Attempt)...'); + + // 1. Excel 로드 + const workbook = xlsx.readFile('server_db.xlsx'); + const sheetName = workbook.SheetNames[0]; + const excelRows = xlsx.utils.sheet_to_json(workbook.Sheets[sheetName]); + + // 2. DB에서 현재 서버PC 데이터 로드 + const [dbAssets] = await connection.query( + "SELECT id, memo, asset_code FROM asset_pc WHERE asset_type = '서버PC'" + ); + + console.log(`📊 DB: ${dbAssets.length} assets, Excel: ${excelRows.length} rows.`); + + for (const asset of dbAssets) { + // 메모에서 "서버코드 : [ID]" 패턴 추출 + // 인코딩 문제로 깨져 보일 수 있으므로 유연하게 매칭 ( jh-idc-001 패턴 찾기 ) + const memo = asset.memo || ''; + const match = memo.match(/[a-z]+-idc-[0-9]+/i); + const serverCode = match ? match[0] : null; + + if (!serverCode) { + console.log(` ❓ Could not find server code in memo for asset ${asset.asset_code}`); + continue; + } + + // Excel에서 해당 서버코드 찾기 (memo 필드 안에 포함되어 있음) + const matchedExcelRow = excelRows.find(r => + r.memo && r.memo.includes(serverCode) + ); + + if (matchedExcelRow) { + const updateData = { + location: matchedExcelRow.location, + location_detail: matchedExcelRow.location_detail, + ip_address: matchedExcelRow.ip_address, + ip_address_2: matchedExcelRow.ip_address_2, + remote_tool: matchedExcelRow.remote_tool, + remote_id: matchedExcelRow.remote_id, + remote_pw: matchedExcelRow.remote_pw, + monitoring: matchedExcelRow.monitoring, + asset_purpose: matchedExcelRow.asset_purpose + }; + + const setClause = Object.keys(updateData).map(key => `\`${key}\` = ?`).join(', '); + const values = [...Object.values(updateData), asset.id]; + + await connection.query( + `UPDATE asset_pc SET ${setClause} WHERE id = ?`, + values + ); + console.log(` ✅ Restored: ${asset.asset_code} (Code: ${serverCode})`); + } else { + console.log(` ⚠️ No Excel match for server code: ${serverCode}`); + } + } + + console.log('🎉 Restoration completed!'); + + } catch (err) { + console.error('❌ Restoration failed:', err); + } finally { + await connection.end(); + } +} + +restoreData(); diff --git a/server.js b/server.js index 66b1588..5762053 100644 --- a/server.js +++ b/server.js @@ -30,335 +30,158 @@ const handleError = (res, err, context, isGet = false) => { else res.status(500).json({ error: err.message }); }; -// --- Mapping Definitions --- +// --- API Implementation --- -const HW_SELECT_FIELDS = ` - id, - asset_type AS \`type\`, - corp AS \`법인\`, - asset_code AS \`자산코드\`, - purchase_date AS \`구매일\`, - user_name AS \`사용자\`, - dept AS \`현사용조직\`, - prev_org AS \`이전사용조직\`, - location AS \`위치\`, - manager_primary AS \`담당자_정\`, - manager_secondary AS \`담당자_부\`, - product_name AS \`모델명\`, - usage_category AS \`상세용도\`, - usage_description AS \`상세\`, - os AS \`OS\`, - cpu AS \`CPU\`, - gpu AS \`GPU\`, - ram AS \`RAM\`, - storage1 AS \`SSD1\`, - storage2 AS \`SSD2\`, - storage3 AS \`SSD3\`, - mainboard AS \`메인보드\`, - ip_address AS \`IP주소\`, - remote_tool AS \`원격접속\`, - server_id AS \`서버ID\`, - server_pw AS \`서버PW\`, - monitoring AS \`모니터링\`, - price AS \`금액\`, - vendor AS \`납품업체\`, - remarks AS \`비고\`, - asset_category AS \`category\` -`; - -const HW_REVERSE_MAP = { - 'id': 'id', - 'type': 'asset_type', - '법인': 'corp', - '자산코드': 'asset_code', - '구매일': 'purchase_date', - '구매연월': 'purchase_date', - '사용자': 'user_name', - '현사용조직': 'dept', - '이전사용조직': 'prev_org', - '위치': 'location', - '담당자_정': 'manager_primary', - '담당자_부': 'manager_secondary', - '모델명': 'product_name', - '상세용도': 'usage_category', - '상세': 'usage_description', - 'OS': 'os', - 'CPU': 'cpu', - 'GPU': 'gpu', - 'RAM': 'ram', - 'SSD1': 'storage1', - 'SSD2': 'storage2', - 'SSD3': 'storage3', - '메인보드': 'mainboard', - 'IP주소': 'ip_address', - '원격접속': 'remote_tool', - '서버ID': 'server_id', - '서버PW': 'server_pw', - '모니터링': 'monitoring', - '금액': 'price', - '납품업체': 'vendor', - '비고': 'remarks', - 'category': 'asset_category' +/** + * Generic Fetcher for Asset Tables + */ +const fetchAssets = async (tableName, res, context) => { + try { + const [rows] = await pool.query(`SELECT * FROM ${tableName}`); + console.log(`📡 [GET ${context}] Returning ${rows.length} rows from ${tableName}`); + res.json(rows); + } catch (err) { + handleError(res, err, context, true); + } }; -const mapObject = (obj, mapping) => { - const mapped = {}; - Object.entries(obj).forEach(([key, val]) => { - const dbKey = mapping[key] || key; - mapped[dbKey] = val; - }); - return mapped; -}; - -// --- GET Routes --- - -app.get('/api/pc', async (req, res) => { - try { - const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "개인PC" OR asset_code LIKE "PC%"`); - console.log(`📡 [GET /api/pc] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '개인PC', 구매연월: r.구매일 }))); - } catch (err) { handleError(res, err, 'GET /api/pc', true); } -}); - -app.get('/api/server', async (req, res) => { - try { - const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "서버" OR asset_code LIKE "SVR%"`); - console.log(`📡 [GET /api/server] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '서버', 구매연월: r.구매일 }))); - } catch (err) { handleError(res, err, 'GET /api/server', true); } -}); - -app.get('/api/storage', async (req, res) => { - try { - const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "스토리지" OR asset_code LIKE "STO%"`); - console.log(`📡 [GET /api/storage] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '스토리지', 구매연월: r.구매일 }))); - } catch (err) { handleError(res, err, 'GET /api/storage', true); } -}); - -app.get('/api/equip', async (req, res) => { - try { - const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "전산비품" OR asset_code LIKE "EQP%"`); - console.log(`📡 [GET /api/equip] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '전산비품', 구매연월: r.구매일 }))); - } catch (err) { handleError(res, err, 'GET /api/equip', true); } -}); - -app.get('/api/mobile', async (req, res) => { - try { - const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "모바일기기" OR asset_category LIKE "%모바일%" OR asset_code LIKE "MOB%"`); - console.log(`📡 [GET /api/mobile] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '모바일기기', 구매연월: r.구매일 }))); - } catch (err) { handleError(res, err, 'GET /api/mobile', true); } -}); - -app.get('/api/asset/software/subscription', async (req, res) => { - try { - const [rows] = await pool.query(` - SELECT - id, category AS \`분야\`, corp AS \`법인\`, dept AS \`부서\`, product_name AS \`제품명\`, - quantity AS \`수량\`, price AS \`금액\`, purchase_date AS \`구매일\`, start_date AS \`시작일\`, - expiry_date AS \`만료일\`, vendor AS \`납품업체\`, remarks AS \`비고\`, license_type AS \`라이선스유형\`, account_name AS \`계정명\` - FROM asset_software_subscription - `); - console.log(`📡 [GET /api/asset/software/subscription] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '구독SW' }))); - } catch (err) { handleError(res, err, 'GET /api/asset/software/subscription', true); } -}); - -app.get('/api/asset/software/perpetual', async (req, res) => { - try { - const [rows] = await pool.query(` - SELECT - id, category AS \`분야\`, corp AS \`법인\`, dept AS \`부서\`, product_name AS \`제품명\`, - quantity AS \`수량\`, price AS \`금액\`, purchase_date AS \`구매일\`, start_date AS \`시작일\`, - expiry_date AS \`만료일\`, vendor AS \`납품업체\`, remarks AS \`비고\`, license_key AS \`라이선스키\`, account_name AS \`계정명\` - FROM asset_software_perpetual - `); - console.log(`📡 [GET /api/asset/software/perpetual] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '영구SW' }))); - } catch (err) { handleError(res, err, 'GET /api/asset/software/perpetual', true); } -}); - -app.get('/api/asset/cloud', async (req, res) => { - try { - const [rows] = await pool.query(` - SELECT - id, platform_name AS \`플랫폼명\`, corp AS \`법인\`, dept AS \`부서\`, product_name AS \`제품명\`, - account_name AS \`계정명\`, pay_method AS \`결제수단\`, pay_day AS \`결제일\`, card_num AS \`연결카드번호\`, - monthly_cost AS \`당월청구액\`, remarks AS \`비고\` - FROM asset_cloud - `); - console.log(`📡 [GET /api/asset/cloud] Returning ${rows.length} rows`); - res.json(rows.map(r => ({ ...r, type: '클라우드' }))); - } catch (err) { handleError(res, err, 'GET /api/asset/cloud', true); } -}); - -app.get('/api/asset/domain', async (req, res) => { - try { - const [rows] = await pool.query(`SELECT * FROM asset_domain`); - res.json(rows); - } catch (err) { handleError(res, err, 'GET /api/asset/domain', true); } -}); - -app.get('/api/asset/software/assignment', async (req, res) => { - try { - const [rows] = await pool.query(` - SELECT - id, sw_id, corp AS \`법인\`, dept AS \`부서\`, position AS \`직위\`, user_name AS \`이름\`, usage_period AS \`사용기간\`, doc_name AS \`신청서명\` - FROM asset_software_assignment - `); - res.json(rows); - } catch (err) { handleError(res, err, 'GET /api/asset/software/assignment', true); } -}); - -app.get('/api/asset/history', async (req, res) => { - try { - const [rows] = await pool.query(` - SELECT - id, asset_id AS assetId, log_date AS \`date\`, log_user AS \`user\`, details, cost - FROM asset_history - `); - res.json(rows); - } catch (err) { handleError(res, err, 'GET /api/asset/history', true); } -}); - -// --- POST Batch Routes --- - -async function saveHwBatch(category, items, res) { +/** + * Generic Batch Saver for Asset Tables + */ +const saveAssetsBatch = async (tableName, items, res, context) => { const connection = await pool.getConnection(); try { await connection.beginTransaction(); - // 1. 해당 카테고리 기존 데이터 삭제 - await connection.query('DELETE FROM asset_hardware WHERE asset_category = ?', [category]); - // 2. 새 데이터 삽입 + // Get valid columns for this table + const [cols] = await connection.query(`DESCRIBE ${tableName}`); + const validColumns = cols.map(c => c.Field); + + // 1. Clear existing (or we could use UPSERT logic, but existing code used DELETE-INSERT pattern) + await connection.query(`DELETE FROM ${tableName}`); + + // 2. Insert new items for (const item of items) { - const dbRow = mapObject(item, HW_REVERSE_MAP); - dbRow.asset_category = category; - if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9); - - // DB 컬럼에 없는 필드 제거 - const validColumns = ['id', 'asset_category', 'corp', 'asset_code', 'purchase_date', 'asset_type', 'usage_category', 'user_name', 'usage_description', 'dept', 'prev_org', 'location', 'manager_primary', 'manager_secondary', 'ip_address', 'remote_tool', 'server_id', 'server_pw', 'product_name', 'mainboard', 'os', 'cpu', 'ram', 'gpu', 'storage1', 'storage2', 'storage3', 'monitoring', 'price', 'vendor', 'remarks', 'storage_location', 'status']; const filteredRow = {}; - validColumns.forEach(col => { if (dbRow[col] !== undefined) filteredRow[col] = dbRow[col]; }); - - await connection.query('INSERT INTO asset_hardware SET ?', [filteredRow]); + validColumns.forEach(col => { + // Exclude auto-managed timestamps from manual insertion + if (col === 'created_at' || col === 'updated_at') return; + + if (item[col] !== undefined) filteredRow[col] = item[col]; + }); + + // Auto-generate ID if missing + if (!filteredRow.id) filteredRow.id = Math.random().toString(36).substring(2, 9); + + await connection.query(`INSERT INTO ${tableName} SET ?`, [filteredRow]); } + await connection.commit(); res.json({ success: true, count: items.length }); } catch (err) { await connection.rollback(); - handleError(res, err, `BATCH SAVE ${category}`); + handleError(res, err, context); } finally { connection.release(); } -} +}; -app.post('/api/pc/batch', (req, res) => saveHwBatch('개인PC', req.body, res)); -app.post('/api/server/batch', (req, res) => saveHwBatch('서버', req.body, res)); -app.post('/api/storage/batch', (req, res) => saveHwBatch('스토리지', req.body, res)); -app.post('/api/equip/batch', (req, res) => saveHwBatch('전산비품', req.body, res)); -app.post('/api/mobile/batch', (req, res) => saveHwBatch('모바일기기', req.body, res)); +// --- Routes --- -app.post('/api/asset/software/subscription/batch', async (req, res) => { - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - await connection.query('DELETE FROM asset_software_subscription'); - const mapping = { '분야': 'category', '법인': 'corp', '부서': 'dept', '제품명': 'product_name', '수량': 'quantity', '금액': 'price', '구매일': 'purchase_date', '시작일': 'start_date', '만료일': 'expiry_date', '납품업체': 'vendor', '비고': 'remarks', '라이선스유형': 'license_type', '계정명': 'account_name' }; - for (const item of req.body) { - const dbRow = mapObject(item, mapping); - if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9); - const filteredRow = {}; - ['id', 'corp', 'category', 'dept', 'product_name', 'license_type', 'quantity', 'price', 'purchase_date', 'start_date', 'expiry_date', 'vendor', 'remarks', 'account_name'].forEach(c => { if (dbRow[c] !== undefined) filteredRow[c] = dbRow[c]; }); - await connection.query('INSERT INTO asset_software_subscription SET ?', [filteredRow]); - } - await connection.commit(); - res.json({ success: true }); - } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH SW SUB'); } finally { connection.release(); } -}); +// 0. User Management +app.get('/api/users', (req, res) => fetchAssets('system_users', res, 'USERS')); +app.post('/api/users/batch', (req, res) => saveAssetsBatch('system_users', req.body, res, 'USERS BATCH')); -app.post('/api/asset/software/perpetual/batch', async (req, res) => { - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - await connection.query('DELETE FROM asset_software_perpetual'); - const mapping = { '분야': 'category', '법인': 'corp', '부서': 'dept', '제품명': 'product_name', '수량': 'quantity', '금액': 'price', '구매일': 'purchase_date', '시작일': 'start_date', '만료일': 'expiry_date', '납품업체': 'vendor', '비고': 'remarks', '라이선스키': 'license_key', '계정명': 'account_name' }; - for (const item of req.body) { - const dbRow = mapObject(item, mapping); - if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9); - const filteredRow = {}; - ['id', 'corp', 'category', 'dept', 'product_name', 'license_key', 'quantity', 'price', 'purchase_date', 'start_date', 'expiry_date', 'vendor', 'remarks', 'account_name'].forEach(c => { if (dbRow[c] !== undefined) filteredRow[c] = dbRow[c]; }); - await connection.query('INSERT INTO asset_software_perpetual SET ?', [filteredRow]); - } - await connection.commit(); - res.json({ success: true }); - } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH SW PERM'); } finally { connection.release(); } -}); +// 1. Hardware Assets +app.get('/api/pc', (req, res) => fetchAssets('asset_pc', res, 'PC')); +app.post('/api/pc/batch', (req, res) => saveAssetsBatch('asset_pc', req.body, res, 'PC BATCH')); -app.post('/api/asset/cloud/batch', async (req, res) => { - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - await connection.query('DELETE FROM asset_cloud'); - const mapping = { '플랫폼명': 'platform_name', '법인': 'corp', '부서': 'dept', '제품명': 'product_name', '계정명': 'account_name', '결제수단': 'pay_method', '결제일': 'pay_day', '연결카드번호': 'card_num', '당월청구액': 'monthly_cost', '비고': 'remarks' }; - for (const item of req.body) { - const dbRow = mapObject(item, mapping); - if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9); - const filteredRow = {}; - ['id', 'platform_name', 'corp', 'dept', 'product_name', 'account_name', 'pay_method', 'pay_day', 'card_num', 'monthly_cost', 'remarks'].forEach(c => { if (dbRow[c] !== undefined) filteredRow[c] = dbRow[c]; }); - await connection.query('INSERT INTO asset_cloud SET ?', [filteredRow]); - } - await connection.commit(); - res.json({ success: true }); - } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH CLOUD'); } finally { connection.release(); } -}); +app.get('/api/server', (req, res) => fetchAssets('asset_server', res, 'SERVER')); +app.post('/api/server/batch', (req, res) => saveAssetsBatch('asset_server', req.body, res, 'SERVER BATCH')); -app.post('/api/asset/software/assignment/batch', async (req, res) => { - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - await connection.query('DELETE FROM asset_software_assignment'); - const mapping = { '법인': 'corp', '부서': 'dept', '직위': 'position', '이름': 'user_name', '사용기간': 'usage_period', '신청서명': 'doc_name' }; - for (const item of req.body) { - const dbRow = mapObject(item, mapping); - delete dbRow.id; // Auto-increment - await connection.query('INSERT INTO asset_software_assignment SET ?', [dbRow]); - } - await connection.commit(); - res.json({ success: true }); - } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH SW ASSIGN'); } finally { connection.release(); } -}); +app.get('/api/storage', (req, res) => fetchAssets('asset_storage', res, 'STORAGE')); +app.post('/api/storage/batch', (req, res) => saveAssetsBatch('asset_storage', req.body, res, 'STORAGE BATCH')); +app.get('/api/network', (req, res) => fetchAssets('asset_network', res, 'NETWORK')); +app.post('/api/network/batch', (req, res) => saveAssetsBatch('asset_network', req.body, res, 'NETWORK BATCH')); + +// 2. Software Assets +app.get('/api/sw/internal', (req, res) => fetchAssets('asset_sw_internal', res, 'SW INTERNAL')); +app.post('/api/sw/internal/batch', (req, res) => saveAssetsBatch('asset_sw_internal', req.body, res, 'SW INTERNAL BATCH')); + +app.get('/api/sw/external', (req, res) => fetchAssets('asset_sw_external', res, 'SW EXTERNAL')); +app.post('/api/sw/external/batch', (req, res) => saveAssetsBatch('asset_sw_external', req.body, res, 'SW EXTERNAL BATCH')); + +// 3. Other Assets +app.get('/api/survey', (req, res) => fetchAssets('asset_survey', res, 'SURVEY')); +app.post('/api/survey/batch', (req, res) => saveAssetsBatch('asset_survey', req.body, res, 'SURVEY BATCH')); + +app.get('/api/pc-parts', (req, res) => fetchAssets('asset_pc_parts', res, 'PC PARTS')); +app.post('/api/pc-parts/batch', (req, res) => saveAssetsBatch('asset_pc_parts', req.body, res, 'PC PARTS BATCH')); + +app.get('/api/equipment', (req, res) => fetchAssets('asset_equipment', res, 'EQUIPMENT')); +app.post('/api/equipment/batch', (req, res) => saveAssetsBatch('asset_equipment', req.body, res, 'EQUIPMENT BATCH')); + +app.get('/api/office-supplies', (req, res) => fetchAssets('asset_office_supplies', res, 'OFFICE SUPPLIES')); +app.post('/api/office-supplies/batch', (req, res) => saveAssetsBatch('asset_office_supplies', req.body, res, 'OFFICE SUPPLIES BATCH')); + +app.get('/api/cloud', (req, res) => fetchAssets('asset_cloud', res, 'CLOUD')); +app.post('/api/cloud/batch', (req, res) => saveAssetsBatch('asset_cloud', req.body, res, 'CLOUD BATCH')); + +app.get('/api/domain', (req, res) => fetchAssets('asset_domain', res, 'DOMAIN')); +app.post('/api/domain/batch', (req, res) => saveAssetsBatch('asset_domain', req.body, res, 'DOMAIN BATCH')); + +app.get('/api/cost', (req, res) => fetchAssets('asset_cost', res, 'COST')); +app.post('/api/cost/batch', (req, res) => saveAssetsBatch('asset_cost', req.body, res, 'COST BATCH')); + +app.get('/api/vip', (req, res) => fetchAssets('asset_vip', res, 'VIP')); +app.post('/api/vip/batch', (req, res) => saveAssetsBatch('asset_vip', req.body, res, 'VIP BATCH')); + +// 4. Legacy/Auxiliary (History & Assignment) +app.get('/api/asset/history', (req, res) => fetchAssets('asset_history', res, 'HISTORY')); app.post('/api/asset/history/batch', async (req, res) => { - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - await connection.query('DELETE FROM asset_history'); - for (const item of req.body) { - const dbRow = { - asset_id: item.assetId, - log_date: item.date, - log_user: item.user, - details: item.details, - cost: item.cost || 0 - }; - await connection.query('INSERT INTO asset_history SET ?', [dbRow]); - } - await connection.commit(); - res.json({ success: true }); - } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH HISTORY'); } finally { connection.release(); } + // Custom logic for history as it might not follow the random-id pattern + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + await connection.query('DELETE FROM asset_history'); + for (const item of req.body) { + const dbRow = { + asset_id: item.assetId, + log_date: item.date, + log_user: item.user, + details: item.details, + cost: item.cost || 0 + }; + await connection.query('INSERT INTO asset_history SET ?', [dbRow]); + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH HISTORY'); } finally { connection.release(); } }); +app.get('/api/asset/software/assignment', (req, res) => fetchAssets('asset_software_assignment', res, 'SW ASSIGN')); +app.post('/api/asset/software/assignment/batch', (req, res) => saveAssetsBatch('asset_software_assignment', req.body, res, 'SW ASSIGN BATCH')); + +// 5. Utility app.get('/api/generate-asset-code', async (req, res) => { try { const { prefix } = req.query; if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); - const [rows] = await pool.query('SELECT asset_code FROM asset_hardware WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1', [`${prefix}%`]); + + // Search in multiple tables if necessary, but typically prefix-based tables are known + const tables = ['asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_survey', 'asset_pc_parts', 'asset_equipment', 'asset_office_supplies', 'asset_vip']; + let lastCode = ''; + + for (const table of tables) { + const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`, [`${prefix}%`]); + if (rows.length > 0 && rows[0].asset_code > lastCode) { + lastCode = rows[0].asset_code; + } + } + let nextNum = 1; - if (rows.length > 0) { - const lastCode = rows[0].asset_code; + if (lastCode) { const lastNum = parseInt(lastCode.split('-').pop() || '0'); nextNum = lastNum + 1; } @@ -367,5 +190,5 @@ app.get('/api/generate-asset-code', async (req, res) => { }); app.listen(3000, '0.0.0.0', () => { - console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Safe Korean Mapping)'); + console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Multi-Table Optimized)'); }); diff --git a/src/components/Guide.ts b/src/components/Guide.ts index 932ef18..b67dde0 100644 --- a/src/components/Guide.ts +++ b/src/components/Guide.ts @@ -56,9 +56,6 @@ const GUIDE_TABS: GuideTabConfig[] = [ 자산 조회상단 카테고리(하드웨어/소프트웨어) 및 하위 탭 선택 후 데이터 조회 자산 등록[자산 추가] 버튼 클릭 후 상세 정보 입력 및 저장 정보 수정목록 행 클릭 후 나타나는 모달에서 내용 변경 및 저장 - 엑셀 업로드[업로드] 버튼 선택 후 표준 양식의 .xlsx 파일 선택 - 전체 엑셀저장[엑셀저장] 버튼 클릭 시 현재 전체 자산 데이터를 Excel로 백업 - 표준 양식[양식] 버튼 클릭 시 데이터 업로드용 빈 양식 다운로드 @@ -125,8 +122,7 @@ const GUIDE_TABS: GuideTabConfig[] = [ 사용자/조직실제 사용자 및 소속 부서변동 시 자산번호고유 식별 번호 (바코드)등록 시 모델명/사양제조사 모델 및 CPU/RAM 등등록 시 - 도입금액구매 비용 (부가세 포함)등록 시 - + 구매금액구매 비용 (부가세 포함)등록 시 diff --git a/src/components/Modal/DashboardDetailModal.ts b/src/components/Modal/DashboardDetailModal.ts index 9905809..80955b7 100644 --- a/src/components/Modal/DashboardDetailModal.ts +++ b/src/components/Modal/DashboardDetailModal.ts @@ -58,7 +58,7 @@ export function openDashboardDetail(title: string, list: any[]) { tbody.innerHTML = `해당 조건의 자산이 없습니다.`; } else { list.forEach((asset, idx) => { - let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.current_user || '-'; + let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.user_current || '-'; let name = asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-'; const tr = document.createElement('tr'); tr.innerHTML = `${idx+1}${asset.category || asset[ASSET_SCHEMA.ASSET_TYPE.key]}${name}${asset[ASSET_SCHEMA.LOCATION.key]||'-'}${manager}${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||'-'}${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||'-'}`; diff --git a/src/components/Modal/DomainModal.ts b/src/components/Modal/DomainModal.ts index fb9c7c0..3aedd02 100644 --- a/src/components/Modal/DomainModal.ts +++ b/src/components/Modal/DomainModal.ts @@ -1,113 +1,15 @@ -import { state } from '../../core/state'; +import { state, saveAsset, deleteAsset } from '../../core/state'; import { closeModals, openModal } from './BaseModal'; import { CORP_LIST } from './SharedData'; import { generateOptionsHTML, setEditLock } from './ModalUtils'; import { createIcons, X, Save, Database, CalendarClock, Edit2 } from 'lucide'; import { formatExcelDate } from '../../core/excelHandler'; +import { UI_TEXT } from '../../core/schema'; let currentItem: any = null; const DOMAIN_MODAL_HTML = ` - +... (rest of DOMAIN_MODAL_HTML remains same) ... `; export function initDomainModal() { @@ -126,7 +28,7 @@ export function initDomainModal() { saveBtn?.addEventListener('click', () => { if (!currentItem) return; - if (saveBtn.textContent === '수정') { + if (saveBtn.textContent?.includes('수정')) { setEditLock('domain-asset-form', 'edit', { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' }); return; } @@ -142,10 +44,14 @@ export function initDomainModal() { if (currentItem) openDomainModal(currentItem); }); - deleteBtn?.addEventListener('click', () => { - if (currentItem && confirm('정말 삭제하시겠습니까?')) { - state.masterData.domain = state.masterData.domain.filter(d => d.id !== currentItem.id); - saveDomainBatch(); + deleteBtn?.addEventListener('click', async () => { + if (currentItem && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) { + const success = await deleteAsset('domain', currentItem.id); + if (success) { + alert('성공적으로 삭제되었습니다.'); + closeModals(); + window.dispatchEvent(new CustomEvent('refresh-view')); + } } }); } @@ -183,26 +89,6 @@ export function openDomainModal(item: any = null) { createIcons({ icons: { X, Save, Database, CalendarClock, Edit2 } }); } -async function saveDomainBatch() { - try { - const response = await fetch(`http://${location.hostname}:3000/api/ops/domain/batch`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(state.masterData.domain) - }); - - if (response.ok) { - closeModals(); - window.dispatchEvent(new CustomEvent('refresh-view')); - } else { - throw new Error('DB 저장 실패'); - } - } catch (err) { - console.error(err); - alert('저장 중 오류가 발생했습니다.'); - } -} - async function saveDomain() { const getVal = (id: string) => (document.getElementById(id) as HTMLInputElement)?.value || ''; @@ -225,17 +111,10 @@ async function saveDomain() { return; } - if (currentItem && currentItem.id.startsWith('DOM-')) { - // 신규 추가 후 바로 수정하는 경우 등 대응 - const idx = state.masterData.domain.findIndex(d => d.id === currentItem.id); - if (idx > -1) state.masterData.domain[idx] = newDomain; - else state.masterData.domain.push(newDomain); - } else if (currentItem) { - const idx = state.masterData.domain.findIndex(d => d.id === currentItem.id); - if (idx > -1) state.masterData.domain[idx] = newDomain; - } else { - state.masterData.domain.push(newDomain); + const success = await saveAsset('domain', newDomain); + if (success) { + alert(UI_TEXT.MESSAGES.SAVE_SUCCESS); + closeModals(); + window.dispatchEvent(new CustomEvent('refresh-view')); } - - await saveDomainBatch(); } diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 089be42..babf962 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,4 +1,4 @@ -import { state, saveAsset } from '../../core/state'; +import { state, saveAsset, deleteAsset } from '../../core/state'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { generateOptionsHTML, @@ -10,7 +10,7 @@ import { getCombinedLocation, applyDateMask } from './ModalUtils'; -import { CORP_LIST, LOCATION_DATA, ORG_LIST } from './SharedData'; +import { CORP_LIST, LOCATION_DATA, ORG_LIST, CATEGORY_TYPE_MAP, HW_STATUS_LIST } from './SharedData'; import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide'; let currentHwAsset: any | null = null; @@ -44,16 +44,20 @@ const HW_MODAL_HTML = `
- + +
+
+ +
- +
@@ -71,11 +75,15 @@ const HW_MODAL_HTML = `
-
+
- +
-
+
+ + +
+
@@ -88,15 +96,13 @@ const HW_MODAL_HTML = `
설치 위치
- +
- - -
- @@ -250,13 +256,52 @@ export function initHwModal(onSave: () => void, closeModals: () => void) { const btnCloseHeader = document.getElementById('btn-close-hw-modal')!; const btnCancelFooter = document.getElementById('btn-cancel-hw-modal')!; - bindLocationEvents('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc'); + bindLocationEvents('hw-bldg-select', 'hw-location_detail', '', ''); applyDateMask(document.getElementById('hw-purchase_date') as HTMLInputElement); + // Category -> Asset Type Cascading + const categorySelect = document.getElementById('hw-category') as HTMLSelectElement; + const typeSelect = document.getElementById('hw-asset_type') as HTMLSelectElement; + + categorySelect.addEventListener('change', () => { + const selectedCat = categorySelect.value; + const types = CATEGORY_TYPE_MAP[selectedCat] || []; + typeSelect.innerHTML = types.length > 0 + ? generateOptionsHTML(types, '', true) + : ''; + }); + const closeModalAction = () => { closeModals(); isEditMode = false; }; btnCloseHeader.addEventListener('click', closeModalAction); btnCancelFooter.addEventListener('click', closeModalAction); + deleteBtn.addEventListener('click', async () => { + if (!currentHwAsset) return; + if (!confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return; + + let categoryKey = 'pc'; + const cat = currentHwAsset.category; + const type = currentHwAsset.asset_type; + const code = currentHwAsset.asset_code || ''; + + if (type === '서버PC') categoryKey = 'pc'; + else if (cat === '서버' || code.startsWith('SVR')) categoryKey = 'server'; + else if (cat === '스토리지' || code.startsWith('STO')) categoryKey = 'storage'; + else if (cat === '네트워크' || code.startsWith('NET')) categoryKey = 'network'; + else if (cat === '업무지원장비' || code.startsWith('EQP')) categoryKey = 'equipment'; + else if (cat === '공간정보장비') categoryKey = 'survey'; + else if (cat === 'PC부품') categoryKey = 'pcParts'; + else if (cat === '사무가구' || cat === '사무소모품') categoryKey = 'officeSupplies'; + else if (cat === 'PC' || code.startsWith('PC')) categoryKey = 'pc'; + + const success = await deleteAsset(categoryKey, currentHwAsset.id); + if (success) { + alert('성공적으로 삭제되었습니다.'); + onSave(); // Refresh list + closeModalAction(); + } + }); + revertBtn.addEventListener('click', () => { setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' }); isEditMode = false; @@ -277,13 +322,28 @@ export function initHwModal(onSave: () => void, closeModals: () => void) { if (key !== 'id') updated[key] = value; }); - // Handle combined location - updated.location = getCombinedLocation('hw-bldg-select', 'hw-floor-select', 'hw-loc-etc'); + // Handle location columns: + // 'location' stores only the building name + // 'location_detail' is already handled via the dynamic FormData loop + updated.location = getFieldValue('hw-bldg-select'); let categoryKey = 'pc'; - if (updated.asset_code?.startsWith('SVR')) categoryKey = 'server'; - else if (updated.asset_code?.startsWith('STO')) categoryKey = 'storage'; - else if (updated.asset_code?.startsWith('EQP')) categoryKey = 'equipment'; + + // 서버PC인 경우 category는 PC이지만 UI상 서버로 취급되므로, + // 저장은 반드시 'pc' 엔드포인트(/api/pc)로 되어야 함. + if (updated.asset_type === '서버PC') { + categoryKey = 'pc'; + } else if (updated.asset_code?.startsWith('SVR') || updated.category === '서버') { + categoryKey = 'server'; + } else if (updated.asset_code?.startsWith('STO') || updated.category === '스토리지') { + categoryKey = 'storage'; + } else if (updated.asset_code?.startsWith('EQP') || updated.category === '업무지원장비') { + categoryKey = 'equipment'; + } else if (updated.category === '공간정보장비') { + categoryKey = 'survey'; + } else if (updated.category === 'PC부품') { + categoryKey = 'pcParts'; + } const success = await saveAsset(categoryKey, updated); if (success) { @@ -314,13 +374,16 @@ export function openHwModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') const serverOnly = document.querySelectorAll('.server-only'); const nonServer = document.querySelectorAll('.non-server'); const pcOnly = document.querySelectorAll('.pc-only'); + const userFields = document.querySelectorAll('.user-tracking-field'); - const isServer = asset.category === '서버' || asset.asset_code?.startsWith('SVR'); + const isServer = asset.category === '서버' || asset.asset_code?.startsWith('SVR') || asset.asset_type === '서버PC'; const isPc = asset.category === 'PC' || asset.asset_code?.startsWith('PC'); + const isVip = asset.category === '선물' || asset.category === 'VIP'; serverOnly.forEach(el => (el as HTMLElement).style.display = isServer ? 'flex' : 'none'); nonServer.forEach(el => (el as HTMLElement).style.display = !isServer ? 'flex' : 'none'); pcOnly.forEach(el => (el as HTMLElement).style.display = isPc ? 'flex' : 'none'); + userFields.forEach(el => (el as HTMLElement).style.display = (!isServer && !isVip) ? 'flex' : 'none'); modal.classList.remove('hidden'); } @@ -330,12 +393,24 @@ function fillHwFormData(asset: any) { setFieldValue('hw-asset_code', asset.asset_code || ''); setFieldValue('hw-purchase_corp', asset.purchase_corp || ''); setFieldValue('hw-category', asset.category || ''); + + // Populate asset_type options based on category + const typeSelect = document.getElementById('hw-asset_type') as HTMLSelectElement; + const types = CATEGORY_TYPE_MAP[asset.category] || []; + if (typeSelect) { + typeSelect.innerHTML = types.length > 0 + ? generateOptionsHTML(types, asset.asset_type, true) + : ''; + } + setFieldValue('hw-asset_type', asset.asset_type || ''); + setFieldValue('hw-hw_status', asset.hw_status || '운영'); setFieldValue('hw-current_dept', asset.current_dept || ''); setFieldValue('hw-previous_dept', asset.previous_dept || ''); setFieldValue('hw-manager_primary', asset.manager_primary || ''); setFieldValue('hw-manager_secondary', asset.manager_secondary || ''); - setFieldValue('hw-current_user', asset.current_user || ''); + setFieldValue('hw-user_current', asset.user_current || ''); + setFieldValue('hw-user_position', asset.user_position || ''); setFieldValue('hw-previous_user', asset.previous_user || ''); setFieldValue('hw-asset_purpose', asset.asset_purpose || ''); @@ -365,8 +440,9 @@ function fillHwFormData(asset: any) { (document.getElementById('hw-approval_document_name') as HTMLElement).textContent = asset.approval_document || ''; setFieldValue('hw-memo', asset.memo || ''); + setFieldValue('hw-location_detail', asset.location_detail || ''); - parseAndSetLocation(asset.location || '', 'hw-bldg-select', 'hw-floor-select', 'hw-loc-etc-group', 'hw-loc-etc'); + parseAndSetLocation(asset.location || '', asset.location_detail || '', 'hw-bldg-select', 'hw-location_detail'); renderHwHistory(asset.id); } diff --git a/src/components/Modal/ModalUtils.ts b/src/components/Modal/ModalUtils.ts index 4a226d6..266149e 100644 --- a/src/components/Modal/ModalUtils.ts +++ b/src/components/Modal/ModalUtils.ts @@ -26,11 +26,11 @@ export function getFieldValue(id: string): string { } // 4. 위치 정보 파싱 및 UI 세팅 -export function parseAndSetLocation(locationStr: string, bldgId: string, detailId: string, etcGroupId: string, etcInputId: string) { +export function parseAndSetLocation(bldg: string, detail: string, bldgId: string, detailId: string, etcGroupId?: string, etcInputId?: string) { const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement; const detailSelect = document.getElementById(detailId) as HTMLSelectElement; - const etcGroup = document.getElementById(etcGroupId); - const etcInput = document.getElementById(etcInputId) as HTMLInputElement; + const etcGroup = etcGroupId ? document.getElementById(etcGroupId) : null; + const etcInput = etcInputId ? document.getElementById(etcInputId) as HTMLInputElement : null; if (!bldgSelect || !detailSelect) return; @@ -39,22 +39,19 @@ export function parseAndSetLocation(locationStr: string, bldgId: string, detailI detailSelect.innerHTML = ''; if (etcGroup) etcGroup.style.display = 'none'; - if (!locationStr) return; + if (!bldg) return; - const parts = locationStr.split(' '); - const bldg = parts[0]; - if (LOCATION_DATA[bldg]) { bldgSelect.value = bldg; // 상세 목록 갱신 detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg]); - const detail = parts[1]; if (detail) { detailSelect.value = detail; if (detail === '기타' && etcGroup && etcInput) { etcGroup.style.display = 'flex'; - etcInput.value = parts.slice(2).join(' '); + // 기타 입력값은 기존 로직 보존을 위해 location_detail을 그대로 쓰거나 + // 하위 호환성을 위해 남겨둠 } } } diff --git a/src/components/Modal/SWModal.ts b/src/components/Modal/SWModal.ts index 5e99dd4..ebdf65f 100644 --- a/src/components/Modal/SWModal.ts +++ b/src/components/Modal/SWModal.ts @@ -1,9 +1,9 @@ -import { state } from '../../core/state'; -import { SoftwareAsset } from '../../core/excelHandler'; +import { state, saveAsset, deleteAsset } from '../../core/state'; import { openModal, closeModals } from './BaseModal'; import { openSwUserModal } from './SWUserModal'; import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Calendar } from 'lucide'; import { CORP_LIST } from './SharedData'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { generateOptionsHTML, setFieldValue, @@ -12,7 +12,7 @@ import { applyDateMask } from './ModalUtils'; -let currentSwAsset: SoftwareAsset | null = null; +let currentSwAsset: any | null = null; let isEditMode = false; const SW_MODAL_HTML = ` @@ -26,21 +26,21 @@ const SW_MODAL_HTML = ` -
@@ -226,53 +218,59 @@ function applySwTypeUI(type: string) { const swFields = document.querySelectorAll('.sw-standard-field'); const userSection = document.getElementById('sw-user-section'); const expiryGroup = document.getElementById('sw-expiry-group'); + const userTracking = document.querySelectorAll('.sw-user-tracking'); if (type === '클라우드') { cloudFields.forEach(el => (el as HTMLElement).style.display = 'flex'); swFields.forEach(el => (el as HTMLElement).style.display = 'none'); if (userSection) userSection.style.display = 'none'; + userTracking.forEach(el => (el as HTMLElement).style.display = 'none'); } else { cloudFields.forEach(el => (el as HTMLElement).style.display = 'none'); swFields.forEach(el => (el as HTMLElement).style.display = 'flex'); if (userSection) userSection.style.display = 'block'; - if (type === '구독SW' || type === '영구SW') { + if (type === '외부SW' || type === '내부SW') { if (expiryGroup) expiryGroup.style.display = 'flex'; + + // 외부SW에만 현 사용자/직전 사용자 표시 (내부SW는 user tracking 제외 요청됨) + userTracking.forEach(el => (el as HTMLElement).style.display = (type === '외부SW') ? 'flex' : 'none'); } } } -function fillSwFormData(asset: SoftwareAsset) { +function fillSwFormData(asset: any) { setFieldValue('sw-asset-id', asset.id); - setFieldValue('sw-asset-type', asset.type); - setFieldValue('sw-분야', asset.분야 || '업무공통'); - setFieldValue('sw-법인', asset.법인); + setFieldValue('sw-asset-type', asset.asset_type || asset.type); + setFieldValue('sw-분야', asset.sw_field || ''); + setFieldValue('sw-법인', asset.purchase_corp || ''); - setFieldValue('sw-부서', asset.부서 || ''); - setFieldValue('sw-제품명', asset.제품명); - setFieldValue('sw-수량', asset.수량); - setFieldValue('sw-금액', asset.금액); - setFieldValue('sw-구매일', asset.구매일 || ''); - setFieldValue('sw-시작일', asset.시작일 || ''); - setFieldValue('sw-납품업체', asset.납품업체 || ''); - setFieldValue('sw-비고', asset.비고 || ''); + setFieldValue('sw-부서', asset.current_dept || ''); + setFieldValue('sw-user-current', asset.user_current || ''); + setFieldValue('sw-previous-user', asset.previous_user || ''); + setFieldValue('sw-previous_dept', asset.previous_dept || ''); + setFieldValue('sw-제품명', asset.product_name || ''); + setFieldValue('sw-수량', asset.asset_count || ''); + setFieldValue('sw-금액', asset.purchase_amount || ''); + setFieldValue('sw-구매일', asset.purchase_date || ''); + setFieldValue('sw-시작일', asset.start_date || ''); + setFieldValue('sw-납품업체', asset.purchase_vendor || ''); + setFieldValue('sw-개발담당자', asset.dev_manager || ''); + setFieldValue('sw-기획담당자', asset.planning_manager || ''); + setFieldValue('sw-영업담당자', asset.sales_manager || ''); + setFieldValue('sw-비고', asset.memo || ''); - if (asset.type === '클라우드') { - setFieldValue('sw-플랫폼명', (asset as any).플랫폼명 || ''); - setFieldValue('sw-계정명', (asset as any).계정명 || ''); - setFieldValue('sw-결제수단', (asset as any).결제수단 || ''); - setFieldValue('sw-연결카드번호', (asset as any).연결카드번호 || ''); - setFieldValue('sw-결제일', (asset as any).결제일 || ''); - setFieldValue('sw-당월청구액', (asset as any).당월청구액 || ''); - } else if (asset.type === '구독SW' || asset.type === '영구SW') { - setFieldValue('sw-만료일', (asset as any).만료일 || ''); + if (asset.type === '클라우드' || asset.asset_type === '클라우드') { + setFieldValue('sw-플랫폼명', asset.dev_objective || ''); + setFieldValue('sw-계정명', asset.email_account || ''); + setFieldValue('sw-결제수단', asset.purchase_method || ''); + } else { + setFieldValue('sw-만료일', asset.expiry_date || ''); } renderSwHistory(asset.id); } - - function renderSwHistory(swId: string) { const container = document.getElementById('sw-history-list'); if (!container) return; @@ -290,11 +288,10 @@ function renderSwHistory(swId: string) { `).join(''); } -export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' | 'edit' = 'view') { +export function openSwModal(asset: any, mode: 'view' | 'add' | 'edit' = 'view') { currentSwAsset = asset; const modal = document.getElementById('sw-asset-modal')!; - // 수정 잠금 상태 제어 setEditLock('sw-asset-form', mode, { saveBtnId: 'btn-save-sw-asset', revertBtnId: 'btn-revert-sw-edit' @@ -303,7 +300,7 @@ export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' | 'edit' isEditMode = (mode === 'add' || mode === 'edit'); fillSwFormData(asset); - applySwTypeUI(asset.type); + applySwTypeUI(asset.asset_type || asset.type); modal.classList.remove('hidden'); createIcons({ icons: { X, History, Plus } }); @@ -326,7 +323,6 @@ export function initSwModal(onSave: () => void, closeModals: () => void) { applySwTypeUI(typeSelect.value); }); - // 날짜 스마트 마스킹 적용 ['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => { applyDateMask(document.getElementById(id) as HTMLInputElement); }); @@ -346,7 +342,7 @@ export function initSwModal(onSave: () => void, closeModals: () => void) { if (currentSwAsset) fillSwFormData(currentSwAsset); }); - saveBtn.addEventListener('click', () => { + saveBtn.addEventListener('click', async () => { if (!currentSwAsset) return; if (!isEditMode) { setEditLock('sw-asset-form', 'edit', { @@ -358,65 +354,37 @@ export function initSwModal(onSave: () => void, closeModals: () => void) { } const type = getFieldValue('sw-asset-type'); - const updated: any = { - ...currentSwAsset, - 분야: getFieldValue('sw-분야'), - 법인: getFieldValue('sw-법인'), - 부서: getFieldValue('sw-부서'), + const formData = new FormData(form); + const updated: any = { ...currentSwAsset }; + formData.forEach((value, key) => { + updated[key] = value; + }); + + // Mapping for generic saveAsset + let categoryKey = 'swExternal'; + if (type === '내부SW') categoryKey = 'swInternal'; + else if (type === '클라우드') categoryKey = 'cloud'; - 제품명: getFieldValue('sw-제품명'), - 수량: parseInt(getFieldValue('sw-수량') || '0'), - 금액: getFieldValue('sw-금액'), - 구매일: getFieldValue('sw-구매일'), - 시작일: getFieldValue('sw-시작일'), - 납품업체: getFieldValue('sw-납품업체'), - 비고: getFieldValue('sw-비고'), - type: type - }; - - if (type === '클라우드') { - updated.플랫폼명 = getFieldValue('sw-플랫폼명'); - updated.계정명 = getFieldValue('sw-계정명'); - updated.결제수단 = getFieldValue('sw-결제수단'); - updated.연결카드번호 = getFieldValue('sw-연결카드번호'); - updated.결제일 = getFieldValue('sw-결제일'); - updated.당월청구액 = getFieldValue('sw-당월청구액').replace(/,/g, ''); - } else if (type === '구독SW' || type === '영구SW') { - updated.만료일 = getFieldValue('sw-만료일'); + const success = await saveAsset(categoryKey, updated); + if (success) { + onSave(); + closeModalAction(); } - - // 데이터 저장 로직 (state 업데이트) - const oldType = currentSwAsset.type; - const newType = updated.type; - - // 유형이 변경된 경우 기존 리스트에서 삭제 - if (oldType !== newType) { - if (oldType === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== updated.id); - else if (oldType === '영구SW') state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== updated.id); - else if (oldType === '클라우드') state.masterData.cloud = state.masterData.cloud.filter(a => a.id !== updated.id); - } - - let targetList: SoftwareAsset[] = []; - if (newType === '구독SW') targetList = state.masterData.subSw; - else if (newType === '영구SW') targetList = state.masterData.permSw; - else if (newType === '클라우드') targetList = state.masterData.cloud; - - const idx = targetList.findIndex(a => a.id === updated.id); - if (idx > -1) targetList[idx] = updated; - else targetList.push(updated); - - onSave(); - closeModalAction(); }); - deleteBtn.addEventListener('click', () => { + deleteBtn.addEventListener('click', async () => { if (!currentSwAsset) return; - if (confirm('삭제하시겠습니까?')) { - const type = currentSwAsset.type; - if (type === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== currentSwAsset!.id); - else if (type === '영구SW') state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== currentSwAsset!.id); - else if (type === '클라우드') state.masterData.cloud = state.masterData.cloud.filter(a => a.id !== currentSwAsset!.id); - onSave(); + if (!confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return; + + const type = currentSwAsset.asset_type || currentSwAsset.type; + let categoryKey = 'swExternal'; + if (type === '내부SW') categoryKey = 'swInternal'; + else if (type === '클라우드') categoryKey = 'cloud'; + + const success = await deleteAsset(categoryKey, currentSwAsset.id); + if (success) { + alert('성공적으로 삭제되었습니다.'); + onSave(); // Refresh list closeModalAction(); } }); @@ -441,62 +409,37 @@ export function initSwModal(onSave: () => void, closeModals: () => void) { alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.'); return; } - subModal.classList.remove('hidden'); - - (document.getElementById('sw-update-date') as HTMLInputElement).value = new Date().toISOString().substring(0, 10); - (document.getElementById('sw-update-start') as HTMLInputElement).value = ''; - (document.getElementById('sw-update-end') as HTMLInputElement).value = ''; - (document.getElementById('sw-update-cost') as HTMLInputElement).value = ''; - (document.getElementById('sw-update-note') as HTMLInputElement).value = ''; - - document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;'); - document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:none'); }); - btnSaveUpdate?.addEventListener('click', (e) => { + btnSaveUpdate?.addEventListener('click', async (e) => { e.preventDefault(); - const isSub = getFieldValue('sw-asset-type') === '구독SW'; const date = (document.getElementById('sw-update-date') as HTMLInputElement).value; const start = (document.getElementById('sw-update-start') as HTMLInputElement).value; const end = (document.getElementById('sw-update-end') as HTMLInputElement).value; - const maintenance = (document.getElementById('sw-update-maintenance') as HTMLInputElement).checked; const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value; const note = (document.getElementById('sw-update-note') as HTMLInputElement).value; - const periodStr = (start || end) ? `${start || ''} ~ ${end || ''}` : ''; - - let details = `[업데이트] ${note || '계약 갱신'}\n`; - if (cost) details += `비용 추가: ${cost}원\n`; - - if (periodStr) details += `계약 변경: -> ${periodStr}\n`; - // 메인 폼에 시작일 만료일 자동 세팅 if (start) setFieldValue('sw-시작일', start); if (end) setFieldValue('sw-만료일', end); - - // 금액 갱신 (선택사항) - if (cost) { - if (getFieldValue('sw-asset-type') === '클라우드') { - setFieldValue('sw-당월청구액', cost); - } else { - setFieldValue('sw-금액', cost); - } - } + if (cost) setFieldValue('sw-금액', cost); - // 이력 탭 갱신 (메모리상) - if (!state.masterData.logs) state.masterData.logs = []; - state.masterData.logs.push({ - id: Math.random().toString(36).substring(2, 9), - assetId: currentSwAsset ? currentSwAsset.id : 'NEW', - date, - details, - cost: cost ? Number(String(cost).replace(/,/g, '')) : 0, - user: '관리자' + // Save as log + const log = { + assetId: currentSwAsset.id, + date, + details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, + user: '관리자' + }; + + // Call generic API for logs (could be added to state.ts) + await fetch(`http://${location.hostname}:3000/api/asset/history/batch`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify([...state.masterData.logs, log]) }); closeUpdateModal(); - renderSwHistory(currentSwAsset ? currentSwAsset.id : ''); - onSave(); // 로그 즉시 저장 + onSave(); }); } - diff --git a/src/components/Modal/SharedData.ts b/src/components/Modal/SharedData.ts index 65a2f19..56fa1e4 100644 --- a/src/components/Modal/SharedData.ts +++ b/src/components/Modal/SharedData.ts @@ -8,17 +8,29 @@ export const CORP_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '바론 // 사용조직 목록 export const ORG_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '기술개발센터', '총괄기획실']; -// 하드웨어 자산 유형 목록 -export const HW_TYPE_LIST = [ - '서버', 'PC', '스토리지', 'NAS', 'DAS', - 'CPU', 'HDD', 'RAM', 'GPU', - '모바일', '노트북', '태블릿' -]; +// 하드웨어 상태 목록 +export const HW_STATUS_LIST = ['운영', '재고', '수리', '폐기', '기타']; + +// 구분(Category) -> 유형(Asset Type) 관계 정의 (통합 관리) +export const CATEGORY_TYPE_MAP: Record = { + '서버': ['서버 렉', '가상서버(VM)', '워크스테이션', 'NAS', 'DAS', '서버PC', '스토리지 렉'], + 'PC': ['개인PC', '노트북', '공용PC', '서버PC'], + '스토리지': ['SSD', 'HDD', '외장HDD'], + '네트워크': ['스위치', '허브', '방화벽', '라우터', '공유기', '허브'], + 'PC부품': ['CPU', 'RAM', 'GPU', 'SSD', 'HDD', 'RAM', '모니터'], + '공간정보장비': ['드론', '측량장비', '보조기기'], + '업무지원장비': ['카메라', '스피커', 'TV', '모바일', '유선전화기', 'XR', '프린터', '전산소모품'], + '외부': ['영구', '구독'], + '내부': ['판매용', 'Solutions', 'Inhouse', 'Engine&Module'], + '비용관리': ['클라우드', '도메인', '전화', '인터넷', '이메일'], + '내빈/외빈': ['선물'], + '시설자산': ['사무가구'] +}; // 설치위치 종속성 데이터 export const LOCATION_DATA: Record = { '한맥빌딩': ['MDF실', '1층', '2층', '3층', '4층', '5층', '6층', '7층', '파고라'], - '기술개발센터': ['서버실', '기타'], + '기술개발센터': ['서버실', '1층', '기타'], '유니온빌딩': ['4층', '5층', '6층'], '뉴코아빌딩': ['4층', '6층', '7층'], 'IDC': ['서관202', '서관203', '서관204', '서관205', '동관53', '동관54'] @@ -26,9 +38,8 @@ export const LOCATION_DATA: Record = { // 유형별 자산번호 접두사(Prefix) 매핑 export const TYPE_PREFIX_MAP: Record = { - '서버': 'SVR', 'PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO', - 'CPU': 'CPU', 'HDD': 'HDD', 'RAM': 'RAM', 'GPU': 'GPU', - '모바일': 'MOB', '노트북': 'PC', '태블릿': 'TAB', - '개인PC': 'PC', '모바일기기': 'MOB', - '구독SW': 'SSW', '영구SW': 'PSW' + '서버': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO', + 'HDD': 'HDD', 'SSD': 'SSD', '노트북': 'NBK', '태블릿': 'TAB', + '드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET', + '구독SW': 'SW', '영구SW': 'SW', '내부' : 'INT' }; diff --git a/src/components/Navigation.ts b/src/components/Navigation.ts index 80f4c35..564a646 100644 --- a/src/components/Navigation.ts +++ b/src/components/Navigation.ts @@ -1,6 +1,6 @@ import { state } from '../core/state'; -const MENU_CONFIG = { +const MENU_CONFIG: any = { hw: { label: '하드웨어', tabs: ['서버', 'PC', '스토리지', '공간정보장비', 'PC부품', '네트워크', '업무지원장비'] diff --git a/src/core/dummyDataGenerator.ts b/src/core/dummyDataGenerator.ts index 93731f9..fde0f2e 100644 --- a/src/core/dummyDataGenerator.ts +++ b/src/core/dummyDataGenerator.ts @@ -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}`, - 납품업체: '총판', + 구매업체: '총판', 비고: '' }); } diff --git a/src/core/excelHandler.ts b/src/core/excelHandler.ts index 71b173b..b248939 100644 --- a/src/core/excelHandler.ts +++ b/src/core/excelHandler.ts @@ -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 = { + 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 = { - '개인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 { @@ -163,44 +228,41 @@ export async function parseExcel(file: File): Promise { 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 = { + '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); } diff --git a/src/core/filterHandler.ts b/src/core/filterHandler.ts new file mode 100644 index 0000000..25b24ab --- /dev/null +++ b/src/core/filterHandler.ts @@ -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 = ` +
+ + +
+ ${showField ? ` +
+ + +
` : ''} + ${showCorp ? ` +
+ + +
` : ''} + ${showLoc ? ` +
+ + +
` : ''} + ${showDept ? ` +
+ + +
` : ''} + ${extraHTML} + + ${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; + }); +} diff --git a/src/core/schema.ts b/src/core/schema.ts index 6feb39b..dfeac0f 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -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 = { + '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' + } }; /** diff --git a/src/core/state.ts b/src/core/state.ts index 96d1240..51962c5 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -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) { 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 = { + '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 = { + '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; } diff --git a/src/core/tableHandler.ts b/src/core/tableHandler.ts index 6b00041..900363a 100644 --- a/src/core/tableHandler.ts +++ b/src/core/tableHandler.ts @@ -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) { diff --git a/src/core/utils.ts b/src/core/utils.ts index 51a6b75..28d0bbc 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -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 = ` +
+

${config.title}

+

${config.description}

+
+ `; + container.appendChild(header); +} + /** * 숫자에 천 단위 콤마 추가 (금액 표시용) */ @@ -131,20 +151,11 @@ export function dynamicSort(list: T[], key: string, direction: 'asc' | 'desc' } /** - * 목록 뷰용 액션 버튼 HTML 생성 (양식, 업로드, 엑셀저장, 자산추가) + * 목록 뷰용 액션 버튼 HTML 생성 (자산추가) */ export function getActionButtonsHTML(): string { return `
- - - diff --git a/src/main.ts b/src/main.ts index 41ff588..3b1a92b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,16 +2,14 @@ import { state, loadMasterDataFromDB, saveAsset } from './core/state'; import { renderNavigation } from './components/Navigation'; import { renderDashboard } from './views/DashboardView'; import { renderSWTable } from './views/SW_Table'; -import { downloadTemplate, exportToExcel, parseExcel } from './core/excelHandler'; import { initBaseModal } from './components/Modal/BaseModal'; import { initHwModal, openHwModal } from './components/Modal/HWModal'; import { initSwModal, openSwModal } from './components/Modal/SWModal'; import { initSwUserModal } from './components/Modal/SWUserModal'; import { initDomainModal, openDomainModal } from './components/Modal/DomainModal'; -import { initUploadPreviewModal, openUploadPreview } from './components/Modal/UploadPreviewModal'; import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; import { initGuide } from './components/Guide'; -import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } from 'lucide'; +import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } from 'lucide'; // --- DB 저장을 위한 세분화된 헬퍼 함수들 --- async function apiBatchSave(url: string, data: any[], label: string) { @@ -42,6 +40,7 @@ const saveSwExternalToDB = () => apiBatchSave(`http://${location.hostname}:3000/ const saveCloudToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/cloud/batch`, state.masterData.cloud, '클라우드'); const saveSwUsersToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/software/assignment/batch`, state.masterData.swUsers, 'SW사용자'); const saveLogsToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/history/batch`, state.masterData.logs, '자산 로그'); +const saveUsersToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/users/batch`, state.masterData.users, '사용자마스터'); // 화면 갱신 통합 핸들러 function refreshView() { @@ -60,7 +59,7 @@ async function saveAllDataToDB() { await Promise.all([ savePcToDB(), saveServerToDB(), saveStorageToDB(), saveNetworkToDB(), saveEquipToDB(), saveSwInternalToDB(), saveSwExternalToDB(), - saveCloudToDB(), saveSwUsersToDB(), saveLogsToDB() + saveCloudToDB(), saveSwUsersToDB(), saveLogsToDB(), saveUsersToDB() ]); await loadMasterDataFromDB(); refreshView(); @@ -93,10 +92,6 @@ function initApp() { initDashboardDetailModal(); initDomainModal(); - initUploadPreviewModal(async () => { - await loadMasterDataFromDB(); - refreshView(); - }); initGuide(); loadMasterDataFromDB().then((success) => { @@ -112,54 +107,32 @@ function initApp() { document.addEventListener('click', (e) => { const target = e.target as HTMLElement; - // 양식 다운로드 - if (target.closest('#btn-download-template')) { - downloadTemplate(); - return; - } - - // 엑셀 내보내기 - if (target.closest('#btn-export-excel')) { - exportToExcel(state.masterData); - return; - } - // 자산 추가 if (target.closest('#btn-add-asset')) { const tab = state.activeSubTab; const cat = state.activeCategory; const newId = Math.random().toString(36).substring(2, 9); + if (cat === 'users') { + // 사용자 추가는 renderUserList 내부에서 별도로 처리하거나 여기서 호출 가능 + // 현재 renderUserList에서 별도로 핸들링하고 있으므로 중복 실행 방지 + return; + } + if (cat === 'hw') { openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add'); } else if (cat === 'sw') { - openSwModal({ id: newId, asset_type: tab === '대시보드' ? '외부SW' : tab } as any, 'add'); + const swType = tab === '외부' ? '외부SW' : (tab === '내부' ? '내부SW' : '외부SW'); + openSwModal({ id: newId, asset_type: swType } as any, 'add'); } else if (cat === 'ops') { if (tab === '도메인') openDomainModal(null); } return; } }); - - // 엑셀 업로드 (Change 이벤트 위임) - document.addEventListener('change', async (e) => { - const target = e.target as HTMLInputElement; - if (target.id === 'excel-upload') { - const file = target.files?.[0]; - if (file) { - try { - const data = await parseExcel(file); - openUploadPreview(data); - target.value = ''; - } catch (err) { - alert('엑셀 파일을 읽는 중 오류가 발생했습니다.'); - } - } - } - }); createIcons({ - icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } + icons: { Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } }); window.addEventListener('refresh-view', () => refreshView()); } diff --git a/src/styles/modal.css b/src/styles/modal.css index c76345e..6cdf7bb 100644 --- a/src/styles/modal.css +++ b/src/styles/modal.css @@ -121,6 +121,14 @@ box-shadow: none !important; } +.grid-form.is-view-mode input[type="file"] { + display: none !important; +} + +.grid-form.is-view-mode .btn-helper { + display: none !important; +} + .grid-form.is-view-mode button { pointer-events: none !important; background: none !important; @@ -210,6 +218,100 @@ max-width: 950px; } +/* Upload Preview Specific */ +.upload-sidebar { + width: 240px; + border-right: 1px solid var(--border-color); + background-color: var(--bg-light); + padding: 1.5rem 1rem; + overflow-y: auto; + flex-shrink: 0; +} + +.upload-tab-container { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.upload-tab-btn { + padding: 0.75rem 1rem; + border-radius: 8px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.2s; + border: 1px solid transparent; + color: var(--text-main); +} + +.upload-tab-btn:hover { + background-color: var(--white); + border-color: var(--border-color); +} + +.upload-tab-btn.active { + background-color: var(--white); + color: var(--primary-color); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + border-color: var(--border-color); + font-weight: 700; +} + +.preview-table-container { + flex: 1; + display: flex; + flex-direction: column; + background-color: var(--white); + overflow: hidden; +} + +.preview-stats-bar { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + background-color: var(--white); +} + +.preview-table { + width: 100%; + border-collapse: collapse; + min-width: max-content; +} + +.preview-table thead { + position: sticky; + top: 0; + z-index: 10; + background-color: var(--bg-light); + box-shadow: 0 1px 0 var(--border-color); +} + +.preview-table th { + padding: 0.75rem 1rem; + text-align: left; + font-size: 12px; + font-weight: 600; + border-bottom: 1px solid var(--border-color); + color: var(--text-muted); +} + +.preview-table td { + padding: 0.75rem 1rem; + font-size: 13px; + border-bottom: 1px solid #f1f5f9; + color: var(--text-main); +} + +.preview-table tr:hover { + background-color: var(--bg-light); +} + .modal-body-split { display: flex; gap: 2rem; diff --git a/src/styles/table.css b/src/styles/table.css index 6cca604..ede3eac 100644 --- a/src/styles/table.css +++ b/src/styles/table.css @@ -1,3 +1,42 @@ +/* --- Page Header for Description --- */ +.page-header { + padding: 1rem 0 0.2rem 0; +} + +.page-title-group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.page-title { + font-size: 16px; + font-weight: 700; + color: var(--primary-color); + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; +} + +.page-title i { + display: flex; + align-items: center; +} + +.page-title svg { + width: 18px; + height: 18px; +} + +.page-description { + font-size: 12px; + color: var(--text-muted); + margin: 0; + line-height: 1.4; + opacity: 0.8; +} + /* --- Table View & Filter Styles --- */ .search-bar { @@ -127,6 +166,16 @@ tbody tr:hover { .text-right { text-align: right !important; } .text-left { text-align: left !important; } +/* 메모 컬럼 전용: 가장 길게 표시되도록 너비 조정 및 줄바꿈 허용 */ +.col-memo { + width: 20%; + min-width: 250px; + white-space: normal !important; + word-break: break-all; + line-height: 1.4; + text-align: left !important; +} + .btn-icon { padding: 0.25rem; border: none; diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts index 9c43aed..fa1a49f 100644 --- a/src/views/Dashboard/HwDashboard.ts +++ b/src/views/Dashboard/HwDashboard.ts @@ -1,7 +1,7 @@ import { state } from '../../core/state'; -import { HardwareAsset } from '../../core/excelHandler'; import { openHwModal } from '../../components/Modal/HWModal'; import { calculateAssetAge, normalizeDate } from '../../core/utils'; +import { ASSET_SCHEMA } from '../../core/schema'; declare var Chart: any; @@ -12,14 +12,14 @@ export function renderHwDashboard(container: HTMLElement) { let totalAge = 0; let countWithDate = 0; let over5YearsCount = 0; - let latestAsset: HardwareAsset | null = null; + let latestAsset: any | null = null; let latestYear = 0; const ageGroups = { stable: 0, warning: 0, critical: 0 }; const yearlyCount: Record = {}; allHw.forEach(a => { - const pDate = a.구매일 || (a as any).purchase_date; + const pDate = a[ASSET_SCHEMA.PURCHASE_DATE.key]; if (!pDate) return; const age = calculateAssetAge(pDate); @@ -53,10 +53,10 @@ export function renderHwDashboard(container: HTMLElement) { // 교체 시급 대상 TOP 10 (오래된 순) const criticalList = [...allHw] - .filter(a => (a.구매일 || (a as any).purchase_date)) + .filter(a => a[ASSET_SCHEMA.PURCHASE_DATE.key]) .sort((a, b) => { - const dateA = new Date(normalizeDate(a.구매일 || (a as any).purchase_date)).getTime(); - const dateB = new Date(normalizeDate(b.구매일 || (b as any).purchase_date)).getTime(); + const dateA = new Date(normalizeDate(a[ASSET_SCHEMA.PURCHASE_DATE.key])).getTime(); + const dateB = new Date(normalizeDate(b[ASSET_SCHEMA.PURCHASE_DATE.key])).getTime(); return dateA - dateB; }) .slice(0, 10); @@ -69,19 +69,23 @@ export function renderHwDashboard(container: HTMLElement) { 전체 평균 사용 연수
전체 자산 기준 (권장 4.5년)
${avgAge}년
-
+
+
+
5년 이상 노후 자산 비율
총 ${over5YearsCount}대 해당
${over5Rate}%
-
+
+
+
최신 도입 모델 (${latestYear}년) -
자산번호: ${(latestAsset as any)?.자산코드 || '-'}
-
- ${(latestAsset as any)?.모델명 || '정보 없음'} +
자산번호: ${latestAsset ? latestAsset[ASSET_SCHEMA.ASSET_CODE.key] : '-'}
+
+ ${latestAsset ? (latestAsset[ASSET_SCHEMA.MODEL_NAME.key] || latestAsset[ASSET_SCHEMA.ASSET_NAME.key] || '정보 없음') : '정보 없음'}
@@ -103,27 +107,31 @@ export function renderHwDashboard(container: HTMLElement) { - - - - - - - + + + + + + + - ${criticalList.map((a, i) => ` - + ${criticalList.map((a, i) => { + const pDate = a[ASSET_SCHEMA.PURCHASE_DATE.key]; + const age = calculateAssetAge(pDate); + return ` + - - - - - - + + + + + + - `).join('')} + `; + }).join('')}
순위자산번호유형모델명사용자/담당자구매연월연령순위자산번호유형모델명담당자구매일자연령
${i + 1}${a.자산코드 || '-'}${a.type}${a.모델명 || a.명칭 || '-'}${a.사용자 || a.담당자_정 || '-'}${a.구매일 || (a as any).purchase_date || '-'}${calculateAssetAge(a.구매일 || (a as any).purchase_date)}년${a[ASSET_SCHEMA.ASSET_CODE.key] || '-'}${a[ASSET_SCHEMA.ASSET_TYPE.key] || a.category || ''}${a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-'}${a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'}${pDate || '-'}${age}년
@@ -147,7 +155,7 @@ export function renderHwDashboard(container: HTMLElement) { function initAgingCharts(ageGroups: any, yearlyCount: Record) { const agingCtx = document.getElementById('chart-aging-dist') as HTMLCanvasElement; - if (agingCtx) { + if (agingCtx && typeof Chart !== 'undefined') { new Chart(agingCtx, { type: 'doughnut', data: { @@ -168,7 +176,7 @@ function initAgingCharts(ageGroups: any, yearlyCount: Record) { } const trendCtx = document.getElementById('chart-purchase-trend') as HTMLCanvasElement; - if (trendCtx) { + if (trendCtx && typeof Chart !== 'undefined') { const years = Object.keys(yearlyCount).sort(); new Chart(trendCtx, { type: 'bar', diff --git a/src/views/Dashboard/SwDashboard.ts b/src/views/Dashboard/SwDashboard.ts index 20bfa15..67f4db2 100644 --- a/src/views/Dashboard/SwDashboard.ts +++ b/src/views/Dashboard/SwDashboard.ts @@ -1,119 +1,56 @@ import { state } from '../../core/state'; -import { SoftwareAsset } from '../../core/excelHandler'; -import { openSwDashboardDetail, openSwUsageDetail, openCloudDashboardDetail } from '../../components/Modal/DashboardDetailModal'; +import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal'; import { normalizeDate } from '../../core/utils'; - -declare var Chart: any; +import { ASSET_SCHEMA } from '../../core/schema'; export function renderSwDashboard(container: HTMLElement) { - let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0; - let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0; + let extQty = 0, extUsed = 0, extExp = 0, extTotal = 0; + let intQty = 0, intUsed = 0, intExp = 0, intTotal = 0; - let subCost2026 = 0; - let permCost2026 = 0; + let extCost2026 = 0; + let intCost2026 = 0; - const currentYear = new Date().getFullYear(); - - const corps = ['한맥', '삼안', '바론']; - const categories = ['업무공통', '개발S/W', '디자인', '설계S/W']; - - const costByCorp: Record = { '한맥': 0, '삼안': 0, '바론': 0 }; - const costByCat: Record = {}; - categories.forEach(c => costByCat[c] = 0); - - // 통합 SW 데이터 (클라우드 제외) - const allSw = [...state.masterData.subSw, ...state.masterData.permSw]; + // 통합 SW 데이터 + const allSw = [...state.masterData.swExternal, ...state.masterData.swInternal]; allSw.forEach(sw => { const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length; - const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10); - const priceStr = sw.금액 ? String(sw.금액).replace(/,/g, '') : '0'; + const qty = typeof sw[ASSET_SCHEMA.ASSET_COUNT.key] === 'number' ? sw[ASSET_SCHEMA.ASSET_COUNT.key] : parseInt(sw[ASSET_SCHEMA.ASSET_COUNT.key]||'0', 10); + const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '') : '0'; const price = parseInt(priceStr, 10) || 0; - if (sw.type === '구독SW') { - subQty += qty; subUsed += assigned; subTotal++; - if (isSWExpiring(sw)) subExp++; - } else if (sw.type === '영구SW') { - permQty += qty; permUsed += assigned; permTotal++; - if (isSWExpiring(sw)) permExp++; - } - - // 초기 도입 비용 (2026년 구매건) - if (sw.구매일 && sw.구매일.startsWith('2026')) { - if (sw.type === '구독SW') subCost2026 += price; - else if (sw.type === '영구SW') permCost2026 += price; - - if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price; - if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price; + if (sw.asset_type === '외부SW' || sw.type === '외부SW') { + extQty += qty; extUsed += assigned; extTotal++; + if (isSWExpiring(sw)) extExp++; + if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) extCost2026 += price; + } else { + intQty += qty; intUsed += assigned; intTotal++; + if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) intCost2026 += price; } }); - // 누적 추가 비용 집계 (2026년 계약 업데이트 로그 기반) - if (state.masterData.logs) { - state.masterData.logs.forEach(log => { - if (log.date && log.date.startsWith('2026') && log.cost) { - const asset = allSw.find(a => a.id === log.assetId); - if (asset) { - const cost = Number(log.cost) || 0; - if (asset.type === '구독SW') subCost2026 += cost; - else if (asset.type === '영구SW') permCost2026 += cost; - - if (costByCorp[asset.법인] !== undefined) costByCorp[asset.법인] += cost; - if (asset.분야 && costByCat[asset.분야] !== undefined) costByCat[asset.분야] += cost; - } - } - }); - } - - 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 extPer = extQty > 0 ? Math.round((extUsed/extQty)*100) : 0; + const intPer = intQty > 0 ? Math.round((intUsed/intQty)*100) : 0; container.innerHTML = `

소프트웨어 라이선스 현황

-
- 구독 소프트웨어 사용율 -
${subQty}카피 중 ${subUsed}개 할당
-
${subPer}%
+
+ 외부 소프트웨어 사용율 +
${extQty}카피 중 ${extUsed}개 할당
+
${extPer}%
-
+
-
- 영구 소프트웨어 사용율 -
${permQty}카피 중 ${permUsed}개 할당
-
${permPer}%
-
-
-
-
-
- -
-
-
- 구독 SW 만료 예정
(30일 이내)
-
${subExp}개 제품
-
-
-
- ${subExpPer}% -
-
-
-
-
- 유지보수 만료 예정
(30일 이내)
-
${permExp}개 제품
-
-
-
- ${permExpPer}% -
+
+ 내부 소프트웨어 현황 +
등록된 내부 솔루션: ${intTotal}개
+
${intPer}%
+
+
@@ -122,42 +59,25 @@ export function renderSwDashboard(container: HTMLElement) {
- 구독 SW 누적 비용 (2026) -
갱신 및 추가 비용 합계
-
₩ ${subCost2026.toLocaleString()}
-
+ 외부 SW 누적 비용 (2026) +
₩ ${extCost2026.toLocaleString()}
- 영구 SW 누적 비용 (2026) -
유지보수 및 신규 도입 합계
-
₩ ${permCost2026.toLocaleString()}
-
+ 내부 SW 누적 비용 (2026) +
₩ ${intCost2026.toLocaleString()}
-
`; - container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.subSw)); - container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.permSw)); - container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.subSw.filter(sw => isSWExpiring(sw)))); - container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.permSw.filter(sw => isSWExpiring(sw)))); + container.querySelector('[data-action="ext-usage"]')?.addEventListener('click', () => openSwUsageDetail('외부 소프트웨어 사용 목록', state.masterData.swExternal)); + container.querySelector('[data-action="int-usage"]')?.addEventListener('click', () => openSwUsageDetail('내부 소프트웨어 사용 목록', state.masterData.swInternal)); } -function isSWExpiring(sw: SoftwareAsset) { - 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 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; } - } - return false; +function isSWExpiring(sw: any) { + const expiry = sw[ASSET_SCHEMA.EXPIRY_DATE.key]; + if (!expiry) return false; + const endMs = new Date(normalizeDate(expiry)).getTime(); + const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); + return diffDays >= 0 && diffDays <= 30; } diff --git a/src/views/List/CloudListView.ts b/src/views/List/CloudListView.ts index 304f5ae..a91b6d4 100644 --- a/src/views/List/CloudListView.ts +++ b/src/views/List/CloudListView.ts @@ -1,38 +1,23 @@ import { state } from '../../core/state'; import { openSwModal } from '../../components/Modal/SWModal'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; -import { dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { dynamicSort, formatInline, getActionButtonsHTML, renderPageHeader } from '../../core/utils'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw, Plus } from 'lucide'; /** * 클라우드(운영 서비스) 자산 목록 뷰 - * 라인 정렬 보정 및 헤더 통일 */ export function renderCloudList(container: HTMLElement) { - const getFullList = () => state.masterData.cloud || []; + renderPageHeader(container, '클라우드'); + + const fullList = state.masterData.cloud || []; let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
-
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -41,12 +26,11 @@ export function renderCloudList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.PRODUCT_NAME.ui} ${ASSET_SCHEMA.ASSET_PURPOSE.ui} ${ASSET_SCHEMA.PURCHASE_VENDOR.ui} ${ASSET_SCHEMA.PURCHASE_AMOUNT.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -57,20 +41,7 @@ export function renderCloudList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const paymentSelect = document.getElementById('filter-payment') as HTMLSelectElement; - - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - const payment = paymentSelect ? paymentSelect.value : ''; - - let filtered = getFullList().filter(asset => { - const kwMatch = !keyword || - (asset[ASSET_SCHEMA.PRODUCT_NAME.key] || '').toLowerCase().includes(keyword) || - (asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '').toLowerCase().includes(keyword) || - (asset[ASSET_SCHEMA.PURCHASE_VENDOR.key] || '').toLowerCase().includes(keyword); - const payMatch = !payment || asset[ASSET_SCHEMA.PURCHASE_METHOD.key] === payment; - return kwMatch && payMatch; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -78,7 +49,7 @@ export function renderCloudList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -87,12 +58,11 @@ export function renderCloudList(container: HTMLElement) { tr.style.cursor = 'pointer'; tr.innerHTML = ` - ${idx+1} ${asset[ASSET_SCHEMA.PRODUCT_NAME.key]||''} ${asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||''} ${asset[ASSET_SCHEMA.PURCHASE_VENDOR.key]||''} ₩ ${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '')).toLocaleString() : '0'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'')} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'')} `; tr.addEventListener('click', () => openSwModal(asset, 'view')); @@ -104,16 +74,31 @@ export function renderCloudList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { Cloud, CreditCard, DollarSign, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { Cloud, CreditCard, DollarSign, RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('filter-payment')?.addEventListener('change', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - if (document.getElementById('filter-keyword')) (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - if (document.getElementById('filter-payment')) (document.getElementById('filter-payment') as HTMLSelectElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`, + showCorp: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/CostListView.ts b/src/views/List/CostListView.ts index c69fdf6..590bbd2 100644 --- a/src/views/List/CostListView.ts +++ b/src/views/List/CostListView.ts @@ -1,29 +1,22 @@ import { state } from '../../core/state'; -import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * 비용관리 자산 목록 뷰 */ export function renderCostList(container: HTMLElement) { - // 비용관리 데이터는 cloud 또는 별도 테이블에 있을 수 있음. + renderPageHeader(container, '비용관리'); + const fullList = sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -32,14 +25,12 @@ export function renderCostList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.ASSET_TYPE.ui} ${ASSET_SCHEMA.ASSET_PURPOSE.ui} 현 사용자 - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} + ${ASSET_SCHEMA.LOCATION.ui} ${ASSET_SCHEMA.EMAIL_ACCOUNT.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -50,16 +41,7 @@ export function renderCostList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.PRODUCT_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.EMAIL_ACCOUNT.key]||'').toLowerCase().includes(keyword); - return matchKeyword; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -67,24 +49,26 @@ export function renderCostList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx + 1} ${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''} ${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-')} ${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'} - ${asset[ASSET_SCHEMA.LOCATION.key] || '-'} - ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} + ${displayLoc} ${asset[ASSET_SCHEMA.EMAIL_ACCOUNT.key] || '-'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; - // 비용관리 모달이 따로 없으면 일단 SW모달 또는 알림 tr.addEventListener('click', () => alert('상세 정보 준비 중입니다.')); tbody.appendChild(tr); }); @@ -94,14 +78,31 @@ export function renderCostList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`, + showCorp: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/DomainListView.ts b/src/views/List/DomainListView.ts index e09e86c..75eff70 100644 --- a/src/views/List/DomainListView.ts +++ b/src/views/List/DomainListView.ts @@ -1,31 +1,23 @@ import { state } from '../../core/state'; -import { formatPrice, dynamicSort, createBadge, getActionButtonsHTML } from '../../core/utils'; -import { createIcons, Plus, Edit2, Trash2, RefreshCcw, Download, Upload, FileSpreadsheet } from 'lucide'; +import { dynamicSort, formatInline, getActionButtonsHTML, renderPageHeader } from '../../core/utils'; +import { createIcons, Plus, Edit2, Trash2, RefreshCcw } from 'lucide'; import { openDomainModal } from '../../components/Modal/DomainModal'; import { setupTableSorting, SortState } from '../../core/tableHandler'; import { ASSET_SCHEMA } from '../../core/schema'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; // 정렬 상태를 모듈 수준에서 관리하여 화면 갱신 시에도 유지되도록 함 let persistentSortState: SortState = { key: '', direction: 'asc' }; export function renderDomainList(container: HTMLElement) { - container.innerHTML = ''; + renderPageHeader(container, '도메인'); const fullList = state.masterData.domain; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; - // 검색바 및 액션 버튼 추가 + // 검색바 추가 const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -34,13 +26,12 @@ export function renderDomainList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.DOMAIN_ADDR.ui} ${ASSET_SCHEMA.ASSET_PURPOSE.ui} ${ASSET_SCHEMA.ASSET_TYPE.ui} ${ASSET_SCHEMA.PURCHASE_CORP.ui} - ${ASSET_SCHEMA.EXPIRY_DATE.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.EXPIRED_DATE.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -51,16 +42,7 @@ export function renderDomainList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - - let filtered = fullList.filter(item => { - const matchKeyword = !keyword || - (item[ASSET_SCHEMA.DOMAIN_ADDR.key] || '').toLowerCase().includes(keyword) || - (item[ASSET_SCHEMA.ASSET_PURPOSE.key] || '').toLowerCase().includes(keyword) || - (item[ASSET_SCHEMA.PRODUCT_NAME.key] || '').toLowerCase().includes(keyword); - return matchKeyword; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME']); if (persistentSortState.key) { filtered = dynamicSort(filtered, persistentSortState.key, persistentSortState.direction); @@ -68,7 +50,7 @@ export function renderDomainList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `등록된 도메인 정보가 없습니다.`; + tbody.innerHTML = `등록된 도메인 정보가 없습니다.`; return; } @@ -78,13 +60,12 @@ export function renderDomainList(container: HTMLElement) { tr.style.cursor = 'pointer'; tr.innerHTML = ` - ${idx + 1} ${item[ASSET_SCHEMA.DOMAIN_ADDR.key] || ''} ${item[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''} ${item[ASSET_SCHEMA.ASSET_TYPE.key] || '-'} ${item[ASSET_SCHEMA.PURCHASE_CORP.key] || ''} - ${item[ASSET_SCHEMA.EXPIRY_DATE.key] || ''} - ${formatInline(item[ASSET_SCHEMA.MEMO.key]||'-')} + ${item[ASSET_SCHEMA.EXPIRED_DATE.key] || ''} + ${formatInline(item[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', (e) => { openDomainModal(item); @@ -97,14 +78,31 @@ export function renderDomainList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { Plus, Edit2, Trash2, RefreshCcw, Download, Upload, FileSpreadsheet } }); + createIcons({ icons: { Plus, Edit2, Trash2, RefreshCcw } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - if (document.getElementById('filter-keyword')) (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`, + showCorp: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/EquipmentListView.ts b/src/views/List/EquipmentListView.ts index 8738416..bafc3b6 100644 --- a/src/views/List/EquipmentListView.ts +++ b/src/views/List/EquipmentListView.ts @@ -1,36 +1,23 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * 전산비품 자산 목록 뷰 - * 라인 정렬 보정 및 헤더 통일 */ export function renderEquipmentList(container: HTMLElement) { + renderPageHeader(container, '업무지원장비'); + const fullList = sortAssets(state.masterData.equipment); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', loc: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort(); - - filterBar.innerHTML = ` -
- - -
-
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -39,16 +26,14 @@ export function renderEquipmentList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.HW_STATUS.ui} - 현 사용자 + ${ASSET_SCHEMA.CURRENT_USER.ui} ${ASSET_SCHEMA.ASSET_TYPE.ui} ${ASSET_SCHEMA.ASSET_MFR.ui} ${ASSET_SCHEMA.MODEL_NAME.ui} ${ASSET_SCHEMA.ASSET_COUNT.ui} - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.LOCATION.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -59,20 +44,7 @@ export function renderEquipmentList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; - - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - const corp = corpSelect ? corpSelect.value : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.ASSET_MFR.key]||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp; - return matchKeyword && matchCorp; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -80,7 +52,7 @@ export function renderEquipmentList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -88,17 +60,19 @@ export function renderEquipmentList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx + 1} ${asset[ASSET_SCHEMA.HW_STATUS.key] || '보관중'} - ${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'} + ${asset[ASSET_SCHEMA.CURRENT_USER.key] || '-'} ${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''} ${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''} ${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key] || asset.명칭)} ${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'} - ${asset[ASSET_SCHEMA.LOCATION.key] || '-'} - ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${displayLoc} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -109,16 +83,43 @@ export function renderEquipmentList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('filter-corp')?.addEventListener('change', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Location Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/FacilityListView.ts b/src/views/List/FacilityListView.ts index 3663c94..335d917 100644 --- a/src/views/List/FacilityListView.ts +++ b/src/views/List/FacilityListView.ts @@ -1,30 +1,23 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * 시설자산 자산 목록 뷰 */ export function renderFacilityList(container: HTMLElement) { - // 시설자산 데이터는 equipment 또는 별도 테이블에 있을 수 있음. + renderPageHeader(container, '사무가구'); + const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -33,15 +26,13 @@ export function renderFacilityList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.HW_STATUS.ui} ${ASSET_SCHEMA.ASSET_TYPE.ui} ${ASSET_SCHEMA.ASSET_MFR.ui} ${ASSET_SCHEMA.MODEL_NAME.ui} - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} + ${ASSET_SCHEMA.LOCATION.ui} ${ASSET_SCHEMA.ASSET_COUNT.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -52,15 +43,7 @@ export function renderFacilityList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.ASSET_MFR.key]||'').toLowerCase().includes(keyword); - return matchKeyword; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'ASSET_MFR']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -68,23 +51,26 @@ export function renderFacilityList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx + 1} ${asset[ASSET_SCHEMA.HW_STATUS.key] || '보관중'} ${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''} ${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''} ${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')} - ${asset[ASSET_SCHEMA.LOCATION.key] || '-'} - ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} + ${displayLoc} ${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -95,14 +81,43 @@ export function renderFacilityList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Loc Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/GiftListView.ts b/src/views/List/GiftListView.ts index 3d9ddc5..3c63cbd 100644 --- a/src/views/List/GiftListView.ts +++ b/src/views/List/GiftListView.ts @@ -1,29 +1,22 @@ import { state } from '../../core/state'; -import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * 선물(내빈/외빈) 자산 목록 뷰 */ export function renderGiftList(container: HTMLElement) { - // 선물 데이터는 equipment 또는 별도 테이블에 있을 수 있음. + renderPageHeader(container, '선물'); + const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -32,12 +25,11 @@ export function renderGiftList(container: HTMLElement) { table.innerHTML = ` - No. 자산명 구매연월 - ${ASSET_SCHEMA.EXPIRY_DATE.ui} + ${ASSET_SCHEMA.EXPIRED_DATE.ui} ${ASSET_SCHEMA.ASSET_COUNT.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -48,14 +40,7 @@ export function renderGiftList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.PRODUCT_NAME.key]||asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword); - return matchKeyword; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'MODEL_NAME']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -63,7 +48,7 @@ export function renderGiftList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -71,12 +56,11 @@ export function renderGiftList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; tr.innerHTML = ` - ${idx + 1} ${formatInline(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')} ${asset[ASSET_SCHEMA.PURCHASE_DATE.key] || ''} - ${asset[ASSET_SCHEMA.EXPIRY_DATE.key] || ''} + ${asset[ASSET_SCHEMA.EXPIRED_DATE.key] || ''} ${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => alert('상세 정보 준비 중입니다.')); tbody.appendChild(tr); @@ -87,14 +71,31 @@ export function renderGiftList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`, + showCorp: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/MobileListView.ts b/src/views/List/MobileListView.ts index 79477cc..7f04760 100644 --- a/src/views/List/MobileListView.ts +++ b/src/views/List/MobileListView.ts @@ -1,37 +1,23 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, Paperclip, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, Paperclip, RefreshCcw, Plus } from 'lucide'; /** * 모바일 자산 목록 뷰 (레거시 지원용) */ export function renderMobileList(container: HTMLElement) { - // 모바일 데이터가 별도 테이블에 없으므로 일단 빈 배열 또는 장비군에서 필터링 시도 + renderPageHeader(container, 'PC'); + const fullList = sortAssets(state.masterData.mobile || []); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - - const corps = Array.from(new Set(fullList.map((a: any) => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort(); - - filterBar.innerHTML = ` -
- - -
-
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -40,7 +26,6 @@ export function renderMobileList(container: HTMLElement) { table.innerHTML = ` - No ${ASSET_SCHEMA.HW_STATUS.ui} ${ASSET_SCHEMA.PURCHASE_CORP.ui} ${ASSET_SCHEMA.MODEL_NAME.ui} @@ -48,6 +33,7 @@ export function renderMobileList(container: HTMLElement) { ${ASSET_SCHEMA.PURCHASE_DATE.ui} ${ASSET_SCHEMA.PURCHASE_AMOUNT.ui} 담당자 + ${ASSET_SCHEMA.MEMO.ui} @@ -58,18 +44,7 @@ export function renderMobileList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; - - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - const corp = corpSelect ? corpSelect.value : ''; - - let filtered = fullList.filter((asset: any) => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp; - return matchKeyword && matchCorp; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -88,14 +63,14 @@ export function renderMobileList(container: HTMLElement) { const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || ''; tr.innerHTML = ` - ${idx+1} ${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'} ${asset[ASSET_SCHEMA.PURCHASE_CORP.key] || ''} ${asset[ASSET_SCHEMA.MODEL_NAME.key] || ''} - ${asset[ASSET_SCHEMA.LOCATION.key] || '-'} + ${(asset[ASSET_SCHEMA.LOCATION.key] || '') + (asset[ASSET_SCHEMA.LOC_DETAIL.key] ? `(${asset[ASSET_SCHEMA.LOC_DETAIL.key]})` : (asset[ASSET_SCHEMA.LOCATION.key] ? '' : '-'))} ${asset[ASSET_SCHEMA.PURCHASE_DATE.key] || ''} ${Number(asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||0).toLocaleString()} ${mainManager} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -106,16 +81,31 @@ export function renderMobileList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { Paperclip, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { Paperclip, RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('filter-corp')?.addEventListener('change', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`, + showCorp: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/NetworkListView.ts b/src/views/List/NetworkListView.ts index 02d0ad0..dcab486 100644 --- a/src/views/List/NetworkListView.ts +++ b/src/views/List/NetworkListView.ts @@ -1,29 +1,23 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * 네트워크 자산 목록 뷰 */ export function renderNetworkList(container: HTMLElement) { + renderPageHeader(container, '네트워크'); + const fullList = sortAssets(state.masterData.network || []); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', loc: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -32,16 +26,14 @@ export function renderNetworkList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.HW_STATUS.ui} - 현 사용자 + ${ASSET_SCHEMA.CURRENT_USER.ui} ${ASSET_SCHEMA.ASSET_TYPE.ui} ${ASSET_SCHEMA.ASSET_MFR.ui} ${ASSET_SCHEMA.MODEL_NAME.ui} ${ASSET_SCHEMA.ASSET_COUNT.ui} - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.LOCATION.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -52,16 +44,7 @@ export function renderNetworkList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.ASSET_MFR.key]||'').toLowerCase().includes(keyword); - return matchKeyword; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -69,24 +52,27 @@ export function renderNetworkList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx + 1} ${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'} - ${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'} + ${asset[ASSET_SCHEMA.CURRENT_USER.key] || '-'} ${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''} ${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''} ${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')} ${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'} - ${asset[ASSET_SCHEMA.LOCATION.key] || '-'} - ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${displayLoc} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -97,14 +83,43 @@ export function renderNetworkList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Location Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/PcListView.ts b/src/views/List/PcListView.ts index 6882a1b..552973f 100644 --- a/src/views/List/PcListView.ts +++ b/src/views/List/PcListView.ts @@ -1,37 +1,21 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, Paperclip, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, Paperclip, RefreshCcw, Plus } from 'lucide'; -/** - * PC 자산 목록 뷰 - * 담당자(부) 추가 및 정렬 보정 - */ export function renderPcList(container: HTMLElement) { - const fullList = sortAssets(state.masterData.pc); + renderPageHeader(container, 'PC'); + + // asset_pc 데이터 중 '서버PC' 유형은 제외하고 렌더링 (서버 리스트에서 보여줌) + const fullList = sortAssets((state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC')); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - - const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort(); - - filterBar.innerHTML = ` -
- - -
-
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -40,10 +24,7 @@ export function renderPcList(container: HTMLElement) { table.innerHTML = ` - No - ${ASSET_SCHEMA.CURRENT_DEPT.ui} ${ASSET_SCHEMA.CURRENT_USER.ui} - ${ASSET_SCHEMA.MANAGER_MAIN.ui} ${ASSET_SCHEMA.CPU.ui} ${ASSET_SCHEMA.MAINBOARD.ui} ${ASSET_SCHEMA.RAM.ui} @@ -52,8 +33,10 @@ export function renderPcList(container: HTMLElement) { SSD2 HDD1 HDD2 + HDD3 + HDD4 ${ASSET_SCHEMA.MAC_ADDR.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -64,22 +47,7 @@ export function renderPcList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; - - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - const corp = corpSelect ? corpSelect.value : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MAC_ADDR.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.CURRENT_USER.key]||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp; - return matchKeyword && matchCorp; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -87,7 +55,7 @@ export function renderPcList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -96,10 +64,7 @@ export function renderPcList(container: HTMLElement) { tr.style.cursor = 'pointer'; tr.innerHTML = ` - ${idx+1} - ${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'} ${asset[ASSET_SCHEMA.CURRENT_USER.key]||'-'} - ${asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'-'} ${asset[ASSET_SCHEMA.CPU.key]||''} ${asset[ASSET_SCHEMA.MAINBOARD.key]||'-'} ${asset[ASSET_SCHEMA.RAM.key]||''} @@ -108,8 +73,10 @@ export function renderPcList(container: HTMLElement) { ${asset[ASSET_SCHEMA.SSD2.key]||'-'} ${asset[ASSET_SCHEMA.HDD1.key]||'-'} ${asset[ASSET_SCHEMA.HDD2.key]||'-'} + ${asset[ASSET_SCHEMA.HDD3.key]||'-'} + ${asset[ASSET_SCHEMA.HDD4.key]||'-'} ${asset[ASSET_SCHEMA.MAC_ADDR.key]||'-'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -119,17 +86,42 @@ export function renderPcList(container: HTMLElement) { sortState = { key, direction: dir }; updateTable(); }); - - createIcons({ icons: { Paperclip, RefreshCcw } }); + createIcons({ icons: { Paperclip, RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('filter-corp')?.addEventListener('change', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Loc Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || a['현사용부서'] || a['현사용조직']))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } diff --git a/src/views/List/PcPartListView.ts b/src/views/List/PcPartListView.ts index 5f190eb..718accd 100644 --- a/src/views/List/PcPartListView.ts +++ b/src/views/List/PcPartListView.ts @@ -1,30 +1,23 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * PC부품 자산 목록 뷰 */ export function renderPcPartList(container: HTMLElement) { - // PC부품 데이터는 survey 또는 별도 테이블에 있을 수 있음. 여기선 equipment에서 필터링하거나 빈 배열 지원 + renderPageHeader(container, 'PC부품'); + const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === 'PC부품') || []); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', loc: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -33,7 +26,6 @@ export function renderPcPartList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.HW_STATUS.ui} ${ASSET_SCHEMA.ASSET_TYPE.ui} ${ASSET_SCHEMA.ASSET_MFR.ui} @@ -41,9 +33,8 @@ export function renderPcPartList(container: HTMLElement) { ${ASSET_SCHEMA.VOLUME.ui} ${ASSET_SCHEMA.MONITOR_INCH.ui} ${ASSET_SCHEMA.ASSET_COUNT.ui} - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.LOCATION.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -54,15 +45,7 @@ export function renderPcPartList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.ASSET_TYPE.key]||'').toLowerCase().includes(keyword); - return matchKeyword; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'ASSET_TYPE']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -70,15 +53,19 @@ export function renderPcPartList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx + 1} ${asset[ASSET_SCHEMA.HW_STATUS.key] || '보관중'} ${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''} ${asset[ASSET_SCHEMA.ASSET_MFR.key] || ''} @@ -86,9 +73,8 @@ export function renderPcPartList(container: HTMLElement) { ${asset[ASSET_SCHEMA.VOLUME.key] || '-'} ${asset[ASSET_SCHEMA.MONITOR_INCH.key] || '-'} ${asset[ASSET_SCHEMA.ASSET_COUNT.key] || '1'} - ${asset[ASSET_SCHEMA.LOCATION.key] || '-'} - ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${displayLoc} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -99,14 +85,43 @@ export function renderPcPartList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Location Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/ServerListView.ts b/src/views/List/ServerListView.ts index 8c47a03..a9126db 100644 --- a/src/views/List/ServerListView.ts +++ b/src/views/List/ServerListView.ts @@ -1,42 +1,24 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; -/** - * 서버 자산 목록 뷰 - * 라인 정렬 보정 및 헤더 통일 - */ export function renderServerList(container: HTMLElement) { - const fullList = sortAssets(state.masterData.server); - let sortState: SortState = { key: '', direction: 'asc' }; + renderPageHeader(container, '서버'); + + // asset_server 데이터와 asset_pc 데이터 중 '서버PC' 유형만 추출하여 병합 + const serverList = state.masterData.server || []; + const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC'); + const fullList = sortAssets([...serverList, ...serverPcList]); + let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', loc: '', dept: '', field: '' }; + const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - - const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort(); - const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); - - filterBar.innerHTML = ` -
- - -
-
- - -
-
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -45,13 +27,11 @@ export function renderServerList(container: HTMLElement) { table.innerHTML = ` - No ${ASSET_SCHEMA.CURRENT_DEPT.ui} ${ASSET_SCHEMA.ASSET_PURPOSE.ui} - ${ASSET_SCHEMA.MODEL_NAME.ui} - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MODEL_NAME.ui} + ${ASSET_SCHEMA.LOCATION.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -62,23 +42,7 @@ export function renderServerList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; - const orgSelect = document.getElementById('filter-org-unit') as HTMLSelectElement; - - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - const corp = corpSelect ? corpSelect.value : ''; - const orgUnit = orgSelect ? orgSelect.value : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp; - const matchOrg = !orgUnit || asset[ASSET_SCHEMA.CURRENT_DEPT.key] === orgUnit; - return matchKeyword && matchCorp && matchOrg; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -86,7 +50,7 @@ export function renderServerList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -94,14 +58,16 @@ export function renderServerList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx+1} ${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'} - ${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key])} + ${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||'-')} ${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.ASSET_NAME.key]||'-')} - ${formatInline(asset[ASSET_SCHEMA.LOCATION.key])} - ${formatInline(asset[ASSET_SCHEMA.LOC_DETAIL.key]||'-')} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${displayLoc} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -111,18 +77,42 @@ export function renderServerList(container: HTMLElement) { sortState = { key, direction: dir }; updateTable(); }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('filter-corp')?.addEventListener('change', updateTable); - document.getElementById('filter-org-unit')?.addEventListener('change', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; - (document.getElementById('filter-org-unit') as HTMLSelectElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Location Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = dept; + opt.textContent = dept; + deptSelect.appendChild(opt); + }); + } + updateTable(); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); } diff --git a/src/views/List/SpaceInfoListView.ts b/src/views/List/SpaceInfoListView.ts index 96d1106..58225b0 100644 --- a/src/views/List/SpaceInfoListView.ts +++ b/src/views/List/SpaceInfoListView.ts @@ -1,30 +1,23 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * 공간정보장비 자산 목록 뷰 */ export function renderSpaceInfoList(container: HTMLElement) { - // 공간정보장비 데이터는 survey 또는 별도 테이블에 있을 수 있음. + renderPageHeader(container, '공간정보장비'); + const fullList = sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', loc: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -33,14 +26,12 @@ export function renderSpaceInfoList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.HW_STATUS.ui} - 현 사용자 - 자산명 + ${ASSET_SCHEMA.CURRENT_USER.ui} + ${ASSET_SCHEMA.ASSET_NAME.ui} ${ASSET_SCHEMA.ASSET_TYPE.ui} - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.LOCATION.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -51,15 +42,7 @@ export function renderSpaceInfoList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.PRODUCT_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword); - return matchKeyword; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -67,22 +50,25 @@ export function renderSpaceInfoList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx + 1} ${asset[ASSET_SCHEMA.HW_STATUS.key] || '운영중'} - ${asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'} - ${formatInline(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || asset[ASSET_SCHEMA.MODEL_NAME.key] || '-')} + ${asset[ASSET_SCHEMA.CURRENT_USER.key] || '-'} + ${formatInline(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-')} ${asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''} - ${asset[ASSET_SCHEMA.LOCATION.key] || '-'} - ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${displayLoc} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -93,14 +79,43 @@ export function renderSpaceInfoList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Location Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/List/StorageListView.ts b/src/views/List/StorageListView.ts index 0f6b684..b3e1647 100644 --- a/src/views/List/StorageListView.ts +++ b/src/views/List/StorageListView.ts @@ -1,42 +1,23 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets, dynamicSort, getActionButtonsHTML } from '../../core/utils'; +import { formatInline, sortAssets, dynamicSort, renderPageHeader } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { setupTableSorting, SortState } from '../../core/tableHandler'; -import { createIcons, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, RefreshCcw, Plus } from 'lucide'; /** * 스토리지 자산 목록 뷰 - * 라인 정렬 보정 및 헤더 통일 */ export function renderStorageList(container: HTMLElement) { + renderPageHeader(container, '스토리지'); + const fullList = sortAssets(state.masterData.storage); let sortState: SortState = { key: '', direction: 'asc' }; - + let currentFilters = { keyword: '', loc: '', dept: '', field: '' }; + const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - - const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.PURCHASE_CORP.key]))).filter(Boolean).sort(); - const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); - - filterBar.innerHTML = ` -
- - -
-
- - -
-
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -45,43 +26,25 @@ export function renderStorageList(container: HTMLElement) { table.innerHTML = ` - No ${ASSET_SCHEMA.HW_STATUS.ui} - 현 사용자 + ${ASSET_SCHEMA.CURRENT_USER.ui} ${ASSET_SCHEMA.ASSET_TYPE.ui} ${ASSET_SCHEMA.VOLUME.ui} ${ASSET_SCHEMA.MODEL_NAME.ui} ${ASSET_SCHEMA.SERIAL_NUM.ui} - ${ASSET_SCHEMA.LOCATION.ui}(건물) - ${ASSET_SCHEMA.LOC_DETAIL.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.LOCATION.ui} + ${ASSET_SCHEMA.MEMO.ui} `; - + tableWrapper.appendChild(table); container.appendChild(tableWrapper); const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; - const orgSelect = document.getElementById('filter-org-unit') as HTMLSelectElement; - - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - const corp = corpSelect ? corpSelect.value : ''; - const orgUnit = orgSelect ? orgUnit.value : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - String(asset[ASSET_SCHEMA.MODEL_NAME.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword) || - String(asset[ASSET_SCHEMA.SERIAL_NUM.key]||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp; - const matchOrg = !orgUnit || asset[ASSET_SCHEMA.CURRENT_DEPT.key] === orgUnit; - return matchKeyword && matchCorp && matchOrg; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -89,25 +52,27 @@ export function renderStorageList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - + + const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; + const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || ''; + const displayLoc = detail ? `${loc}(${detail})` : (loc || '-'); + tr.innerHTML = ` - ${idx+1} ${asset[ASSET_SCHEMA.HW_STATUS.key]||'-'} - ${asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'-'} + ${asset[ASSET_SCHEMA.CURRENT_USER.key]||'-'} ${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'-'} ${asset[ASSET_SCHEMA.VOLUME.key]||'-'} ${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.ASSET_NAME.key]||'-')} ${asset[ASSET_SCHEMA.SERIAL_NUM.key]||'-'} - ${formatInline(asset[ASSET_SCHEMA.LOCATION.key])} - ${formatInline(asset[ASSET_SCHEMA.LOC_DETAIL.key]||'-')} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${displayLoc} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); @@ -117,18 +82,42 @@ export function renderStorageList(container: HTMLElement) { sortState = { key, direction: dir }; updateTable(); }); + createIcons({ icons: { RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('filter-corp')?.addEventListener('change', updateTable); - document.getElementById('filter-org-unit')?.addEventListener('change', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; - (document.getElementById('filter-org-unit') as HTMLSelectElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`, + showLoc: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Location Options + const locSelect = container.querySelector('#filter-loc') as HTMLSelectElement; + if (locSelect) { + const locations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key]))).filter(Boolean).sort(); + locations.forEach(loc => { + const opt = document.createElement('option'); + opt.value = String(loc); + opt.textContent = String(loc); + locSelect.appendChild(opt); + }); + } + + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); - createIcons({ icons: { RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); } diff --git a/src/views/List/SwListView.ts b/src/views/List/SwListView.ts index 6899f60..2bdfe01 100644 --- a/src/views/List/SwListView.ts +++ b/src/views/List/SwListView.ts @@ -1,45 +1,22 @@ import { state } from '../../core/state'; import { openSwModal } from '../../components/Modal/SWModal'; -import { openSwUserModal } from '../../components/Modal/SWUserModal'; -import { sortAssets, dynamicSort, formatPrice, getActionButtonsHTML } from '../../core/utils'; +import { sortAssets, dynamicSort, formatInline, getActionButtonsHTML, renderPageHeader } from '../../core/utils'; import { setupTableSorting, SortState } from '../../core/tableHandler'; import { ASSET_SCHEMA } from '../../core/schema'; -import { CORP_LIST } from '../../components/Modal/SharedData'; -import { generateOptionsHTML } from '../../components/Modal/ModalUtils'; -import { createIcons, Edit2, Users, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide'; +import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; +import { createIcons, Edit2, Users, RefreshCcw, Plus } from 'lucide'; export function renderSwList(container: HTMLElement) { const isInternal = state.activeSubTab === '내부'; + renderPageHeader(container, isInternal ? '내부' : '외부'); + const fullList = sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal); let sortState: SortState = { key: '', direction: 'asc' }; + let currentFilters = { keyword: '', corp: '', dept: '', field: '' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
-
- - -
-
- - -
- - ${getActionButtonsHTML()} - `; container.appendChild(filterBar); const tableWrapper = document.createElement('div'); @@ -50,12 +27,11 @@ export function renderSwList(container: HTMLElement) { table.innerHTML = ` - No. ${ASSET_SCHEMA.SW_FIELD.ui} ${ASSET_SCHEMA.DEV_OBJ.ui} ${ASSET_SCHEMA.SW_STATUS.ui} ${ASSET_SCHEMA.SW_TYPE.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -64,17 +40,17 @@ export function renderSwList(container: HTMLElement) { table.innerHTML = ` - No. 자산명 유형 ${ASSET_SCHEMA.SW_STATUS.ui} ${ASSET_SCHEMA.SW_FIELD.ui} ${ASSET_SCHEMA.CURRENT_DEPT.ui} - 현 사용자 + ${ASSET_SCHEMA.CURRENT_USER.ui} + ${ASSET_SCHEMA.PREV_USER.ui} 구매연월 시작일 - 만료일 - ${ASSET_SCHEMA.MEMO.ui} + 만료일 + ${ASSET_SCHEMA.MEMO.ui} @@ -86,22 +62,7 @@ export function renderSwList(container: HTMLElement) { const tbody = table.querySelector('tbody')!; const updateTable = () => { - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const fieldSelect = document.getElementById('filter-field') as HTMLSelectElement; - const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; - - const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; - const field = fieldSelect ? fieldSelect.value : ''; - const corp = corpSelect ? corpSelect.value : ''; - - let filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - (asset[ASSET_SCHEMA.PRODUCT_NAME.key] || '').toLowerCase().includes(keyword) || - (asset[ASSET_SCHEMA.CURRENT_DEPT.key] || '').toLowerCase().includes(keyword); - const matchField = !field || asset[ASSET_SCHEMA.SW_FIELD.key] === field; - const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp; - return matchKeyword && matchField && matchCorp; - }); + let filtered = applyCommonFilters(fullList, currentFilters, ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT']); if (sortState.key) { filtered = dynamicSort(filtered, sortState.key, sortState.direction); @@ -109,7 +70,7 @@ export function renderSwList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `검색 결과가 없습니다.`; + tbody.innerHTML = `검색 결과가 없습니다.`; return; } @@ -118,33 +79,30 @@ export function renderSwList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; tr.innerHTML = ` - ${idx+1} ${asset[ASSET_SCHEMA.SW_FIELD.key]||''} ${asset[ASSET_SCHEMA.DEV_OBJ.key]||''} ${asset[ASSET_SCHEMA.SW_STATUS.key]||'보유중'} ${asset[ASSET_SCHEMA.SW_TYPE.key]||'내부'} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openSwModal(asset, 'view')); tbody.appendChild(tr); } else { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - const users = state.masterData.swUsers.filter(u => u.sw_id === asset.id); - const userText = users.length > 0 ? `${users[0].user_name}${users.length > 1 ? ' 외 ' + (users.length - 1) : ''}` : '-'; - + tr.innerHTML = ` - ${idx+1} ${asset[ASSET_SCHEMA.PRODUCT_NAME.key]||''} ${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'외부'} ${asset[ASSET_SCHEMA.SW_STATUS.key]||'사용중'} ${asset[ASSET_SCHEMA.SW_FIELD.key]||''} ${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||''} - ${userText} + ${asset[ASSET_SCHEMA.CURRENT_USER.key]||'-'} + ${asset[ASSET_SCHEMA.PREV_USER.key]||'-'} ${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||''} ${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||''} - ${asset[ASSET_SCHEMA.EXPIRY_DATE.key]||''} - ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} + ${asset[ASSET_SCHEMA.EXPIRED_DATE.key]||''} + ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; tr.addEventListener('click', () => openSwModal(asset, 'view')); tbody.appendChild(tr); @@ -156,18 +114,32 @@ export function renderSwList(container: HTMLElement) { updateTable(); }); - createIcons({ icons: { Edit2, Users, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } }); + createIcons({ icons: { Edit2, Users, RefreshCcw, Plus } }); }; - document.getElementById('filter-keyword')?.addEventListener('input', updateTable); - document.getElementById('filter-field')?.addEventListener('change', updateTable); - document.getElementById('filter-corp')?.addEventListener('change', updateTable); - document.getElementById('btn-reset-filters')?.addEventListener('click', () => { - (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; - (document.getElementById('filter-field') as HTMLSelectElement).value = ''; - (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; - updateTable(); + renderFilterBar(filterBar, { + keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`, + showField: true, + showCorp: true, + showDept: true, + onFilterChange: (filters) => { + currentFilters = filters; + updateTable(); + } }); + // Populate Dept Options + const deptSelect = container.querySelector('#filter-dept') as HTMLSelectElement; + if (deptSelect) { + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CURRENT_DEPT.key]))).filter(Boolean).sort(); + orgUnits.forEach(dept => { + const opt = document.createElement('option'); + opt.value = String(dept); + opt.textContent = String(dept); + deptSelect.appendChild(opt); + }); + } + updateTable(); } + diff --git a/src/views/SW_Table.ts b/src/views/SW_Table.ts index dd94b86..cc15dfc 100644 --- a/src/views/SW_Table.ts +++ b/src/views/SW_Table.ts @@ -13,7 +13,8 @@ import { renderSpaceInfoList } from './List/SpaceInfoListView'; import { renderGiftList } from './List/GiftListView'; import { renderFacilityList } from './List/FacilityListView'; import { renderCostList } from './List/CostListView'; -import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide'; +import { renderUserList } from './List/UserListView'; +import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide'; /** * 자산 목록 테이블 렌더링 통합 허브 @@ -69,7 +70,7 @@ export function renderSWTable(mainContent: HTMLElement) { // 전역 아이콘 초기화 (한 번 더 실행하여 누락 방지) createIcons({ - icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } + icons: { Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } }); } catch (err: any) { console.error('❌ Error rendering table view:', err);