diff --git a/db_init.js b/db_init.js index ca95364..5921bef 100644 --- a/db_init.js +++ b/db_init.js @@ -99,6 +99,7 @@ async function initDB() { price VARCHAR(100) COMMENT '금액', purchase_date VARCHAR(50) COMMENT '구매일', start_date VARCHAR(50) COMMENT '시작일', + expiry_date VARCHAR(50) COMMENT '만료일', vendor VARCHAR(255) COMMENT '납품업체', remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP @@ -138,11 +139,12 @@ async function initDB() { await connection.query(` CREATE TABLE asset_logs ( - id VARCHAR(50) PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, asset_id VARCHAR(50), log_date VARCHAR(50), log_user VARCHAR(100), details TEXT, + cost DECIMAL(15,2) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); diff --git a/server.js b/server.js index 7797f69..3d2d3f7 100644 --- a/server.js +++ b/server.js @@ -44,12 +44,8 @@ async function ensureTables() { `); await connection.query(` CREATE TABLE IF NOT EXISTS asset_logs ( - id VARCHAR(50) PRIMARY KEY, - asset_id VARCHAR(50), - log_date VARCHAR(50), - log_user VARCHAR(100), - details TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + id INT AUTO_INCREMENT PRIMARY KEY, asset_id VARCHAR(50), log_date VARCHAR(50), + log_user VARCHAR(100), details TEXT, cost DECIMAL(15,2) DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); await connection.query(` @@ -71,7 +67,7 @@ async function ensureTables() { await connection.query(` CREATE TABLE IF NOT EXISTS sw_sub_assets ( - id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), + id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), category VARCHAR(100), dept VARCHAR(100), product_name VARCHAR(255), license_type VARCHAR(100), quantity INT, price VARCHAR(100), purchase_date VARCHAR(50), start_date VARCHAR(50), expiry_date VARCHAR(50), vendor VARCHAR(100), remarks TEXT @@ -79,10 +75,16 @@ async function ensureTables() { `); await connection.query(` CREATE TABLE IF NOT EXISTS sw_perm_assets ( - id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), + id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), category VARCHAR(100), dept VARCHAR(100), product_name VARCHAR(255), license_key VARCHAR(255), quantity INT, price VARCHAR(100), purchase_date VARCHAR(50), - start_date VARCHAR(50), vendor VARCHAR(100), remarks TEXT + start_date VARCHAR(50), expiry_date VARCHAR(50), vendor VARCHAR(100), remarks TEXT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + await connection.query(` + CREATE TABLE IF NOT EXISTS asset_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, asset_id VARCHAR(50), log_date VARCHAR(50), + log_user VARCHAR(100), details TEXT, cost DECIMAL(15,2) DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); await connection.query(` @@ -277,7 +279,7 @@ app.get('/api/sw/sub', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM sw_sub_assets'); res.json(rows.map(r => ({ - id: r.id, type: '구독SW', 법인: r.corp, 자산번호: r.asset_code, + id: r.id, type: '구독SW', 법인: r.corp, 분야: r.category, 부서: r.dept, 제품명: r.product_name, 라이선스유형: r.license_type, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date, 시작일: r.start_date, 만료일: r.expiry_date, @@ -289,9 +291,9 @@ app.get('/api/sw/sub', async (req, res) => { app.post('/api/sw/sub/batch', async (req, res) => { try { const result = await batchSave('sw_sub_assets', req.body, (assets) => ({ - sql: `INSERT INTO sw_sub_assets (id, corp, asset_code, category, dept, product_name, license_type, quantity, price, purchase_date, start_date, expiry_date, vendor, remarks) VALUES ?`, + sql: `INSERT INTO sw_sub_assets (id, corp, category, dept, product_name, license_type, quantity, price, purchase_date, start_date, expiry_date, vendor, remarks) VALUES ?`, values: assets.map(a => [ - a.id, a.법인||'', a.자산번호||'', a.분야||'', a.부서||'', a.제품명||'', + a.id, a.법인||'', a.분야||'', a.부서||'', a.제품명||'', a.라이선스유형||'', a.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||'' ]) })); @@ -304,10 +306,10 @@ app.get('/api/sw/perm', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM sw_perm_assets'); res.json(rows.map(r => ({ - id: r.id, type: '영구SW', 법인: r.corp, 자산번호: r.asset_code, + id: r.id, type: '영구SW', 법인: r.corp, 분야: r.category, 부서: r.dept, 제품명: r.product_name, 라이선스키: r.license_key, 수량: r.quantity, 금액: r.price, - 구매일: r.purchase_date, 시작일: r.start_date, + 구매일: r.purchase_date, 시작일: r.start_date, 만료일: r.expiry_date, 납품업체: r.vendor, 비고: r.remarks }))); } catch (err) { res.status(500).json({ error: err.message }); } @@ -315,11 +317,13 @@ app.get('/api/sw/perm', async (req, res) => { app.post('/api/sw/perm/batch', async (req, res) => { try { + console.log('📦 Permanent SW Batch Save Request:', req.body.length, 'items'); + if (req.body.length > 0) console.log('Sample:', req.body[0]); const result = await batchSave('sw_perm_assets', req.body, (assets) => ({ - sql: `INSERT INTO sw_perm_assets (id, corp, asset_code, category, dept, product_name, license_key, quantity, price, purchase_date, start_date, vendor, remarks) VALUES ?`, + sql: `INSERT INTO sw_perm_assets (id, corp, category, dept, product_name, license_key, quantity, price, purchase_date, start_date, expiry_date, vendor, remarks) VALUES ?`, values: assets.map(a => [ - a.id, a.법인||'', a.자산번호||'', a.분야||'', a.부서||'', a.제품명||'', - a.라이선스키||'', a.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.납품업체||'', a.비고||'' + a.id, a.법인||'', a.분야||'', a.부서||'', a.제품명||'', + a.라이선스키||'', a.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||'' ]) })); res.json(result); @@ -353,16 +357,16 @@ app.get('/api/logs', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM asset_logs ORDER BY log_date DESC'); res.json(rows.map(r => ({ - id: r.id, assetId: r.asset_id, date: r.log_date, user: r.log_user, details: r.details + id: r.id, assetId: r.asset_id, date: r.log_date, user: r.log_user, details: r.details, cost: r.cost }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/logs/batch', async (req, res) => { try { - const result = await batchSave('asset_logs', req.body, (assets) => ({ - sql: `INSERT INTO asset_logs (id, asset_id, log_date, log_user, details) VALUES ?`, - values: assets.map(a => [a.id, a.assetId||'', a.date||'', a.user||'', a.details||'']) + const result = await batchSave('asset_logs', req.body, (logs) => ({ + sql: `INSERT INTO asset_logs (asset_id, log_date, log_user, details, cost) VALUES ?`, + values: logs.map(l => [l.assetId, l.date, l.user, l.details, l.cost || 0]) })); res.json(result); } catch (err) { res.status(500).json({ error: err.message }); } diff --git a/src/components/Modal/BaseModal.ts b/src/components/Modal/BaseModal.ts index 7e1c518..8bb9a81 100644 --- a/src/components/Modal/BaseModal.ts +++ b/src/components/Modal/BaseModal.ts @@ -1,26 +1,26 @@ /** * 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다. */ -export function initBaseModal() { - const closeAllModals = () => { - const modals = document.querySelectorAll('.modal-overlay'); - modals.forEach(modal => modal.classList.add('hidden')); - }; +export function closeModals() { + const modals = document.querySelectorAll('.modal-overlay'); + modals.forEach(modal => modal.classList.add('hidden')); +} +export function initBaseModal() { // ESC 키로 닫기 window.addEventListener('keydown', (e) => { - if (e.key === 'Escape') closeAllModals(); + if (e.key === 'Escape') closeModals(); }); - // 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현) + // 배경(Overlay) 클릭 시 닫기 document.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (target.classList.contains('modal-overlay')) { - closeAllModals(); + closeModals(); } }); - return { closeAllModals }; + return { closeAllModals: closeModals }; } /** diff --git a/src/components/Modal/ModalUtils.ts b/src/components/Modal/ModalUtils.ts index 4aca748..4a226d6 100644 --- a/src/components/Modal/ModalUtils.ts +++ b/src/components/Modal/ModalUtils.ts @@ -211,3 +211,35 @@ export function autoExtractForm(idPrefix: string, fieldMap: Record { + let value = el.value.replace(/[^0-9]/g, ''); // 숫자만 남김 + let result = ''; + + if (value.length <= 4) { + result = value; + } else if (value.length <= 6) { + result = value.substring(0, 4) + '-' + value.substring(4); + } else { + result = value.substring(0, 4) + '-' + value.substring(4, 6) + '-' + value.substring(6, 10); + } + + el.value = result; + }); + + // 엔터 키나 입력 완료 시 유효성 검사 (선택 사항) + el.addEventListener('blur', () => { + const val = el.value; + if (val && !/^\d{4}-\d{2}-\d{2}$/.test(val)) { + // 형식이 맞지 않으면 경고 효과 등을 줄 수 있음 + } + }); +} diff --git a/src/components/Modal/SWModal.ts b/src/components/Modal/SWModal.ts index fa577d9..5e99dd4 100644 --- a/src/components/Modal/SWModal.ts +++ b/src/components/Modal/SWModal.ts @@ -1,135 +1,231 @@ import { state } from '../../core/state'; import { SoftwareAsset } from '../../core/excelHandler'; -import { openModal } from './BaseModal'; +import { openModal, closeModals } from './BaseModal'; import { openSwUserModal } from './SWUserModal'; -import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; -import { createIcons, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide'; +import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Calendar } from 'lucide'; import { CORP_LIST } from './SharedData'; import { generateOptionsHTML, setFieldValue, getFieldValue, setEditLock, - createModalFrameHTML, - autoFillForm, - autoExtractForm + applyDateMask } from './ModalUtils'; let currentSwAsset: SoftwareAsset | null = null; let isEditMode = false; -/** - * 소프트웨어 필드 매핑 (통합 스키마 기반) - * 소프트웨어는 자산번호를 사용하지 않으므로 제거함 - */ -const SW_FIELD_MAP: Record = { - '법인': ASSET_SCHEMA.CORP.key, - '제품명': ASSET_SCHEMA.PRODUCT.key, - '수량': ASSET_SCHEMA.QTY.key, - '금액': ASSET_SCHEMA.PRICE.key, - '구매일': ASSET_SCHEMA.PURCHASE_YM.key, - '시작일': '시작일', - '납품업체': ASSET_SCHEMA.VENDOR.key, - '비고': ASSET_SCHEMA.REMARKS.key, - '플랫폼명': ASSET_SCHEMA.PLATFORM.key, - '부서': '부서', - '계정명': ASSET_SCHEMA.ACCOUNT.key, - '결제수단': ASSET_SCHEMA.PAY_METHOD.key, - '연결카드번호': ASSET_SCHEMA.CARD_NUM.key, - '결제일': ASSET_SCHEMA.PAY_DAY.key, - '당월청구액': ASSET_SCHEMA.BILLING.key, - '라이선스유형': ASSET_SCHEMA.LICENSE_TYPE.key, - '만료일': ASSET_SCHEMA.EXPIRY.key, - '라이선스키': ASSET_SCHEMA.LICENSE_KEY.key -}; +const SW_MODAL_HTML = ` +