diff --git a/server.js b/server.js index 3113884..4da8052 100644 --- a/server.js +++ b/server.js @@ -8,170 +8,221 @@ dotenv.config(); const app = express(); app.use(cors()); -app.use(express.json({ limit: '100mb' })); - -// Request Logger -app.use((req, res, next) => { - console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); - next(); -}); +app.use(express.json({ limit: '50mb' })); +// MySQL Pool Configuration const pool = mysql.createPool({ 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'), - charset: 'utf8mb4' + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 }); -const handleError = (res, err, context, isGet = false) => { - console.error(`❌ [${context}] Error:`, err.message); - if (isGet) res.json([]); - else res.status(500).json({ error: err.message }); +// Error Handler +const handleError = (res, err, label) => { + console.error(`❌ [${label}] Error:`, err); + res.status(500).json({ error: err.message }); }; -// --- API Implementation --- +// --- API Endpoints --- -/** - * 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); - } -}; +// 1. Generic Batch Save (Dynamic Table Detection) +app.post('/api/:table/batch', async (req, res) => { + const { table } = req.params; + const data = req.body; + if (!Array.isArray(data)) return res.status(400).json({ error: 'Data must be an array' }); -/** - * Generic Batch Saver for Asset Tables - */ -const saveAssetsBatch = async (tableName, items, res, context) => { - const connection = await pool.getConnection(); + let connection; try { + connection = await pool.getConnection(); await connection.beginTransaction(); - - // Get valid columns for this table - const [cols] = await connection.query(`DESCRIBE ${tableName}`); - const validColumns = cols.map(c => c.Field); - - // 1. Clear existing - await connection.query(`DELETE FROM ${tableName}`); - - // 2. Insert new items - for (const item of items) { - const filteredRow = {}; - validColumns.forEach(col => { - if (col === 'created_at' || col === 'updated_at') return; - if (item[col] !== undefined) filteredRow[col] = item[col]; - }); - - 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, context); - } finally { - connection.release(); - } -}; -// --- Routes --- + // 1. Get Table Schema + const [columns] = await connection.query(`DESCRIBE ${table}`); + const validFields = columns.map(c => c.Field); -const routeMap = { - '/api/users': { table: 'system_users', context: 'USERS' }, - '/api/pc': { table: 'asset_pc', context: 'PC' }, - '/api/server': { table: 'asset_server', context: 'SERVER' }, - '/api/storage': { table: 'asset_storage', context: 'STORAGE' }, - '/api/network': { table: 'asset_network', context: 'NETWORK' }, - '/api/sw/internal': { table: 'asset_sw_internal', context: 'SW INTERNAL' }, - '/api/sw/external': { table: 'asset_sw_external', context: 'SW EXTERNAL' }, - '/api/survey': { table: 'asset_survey', context: 'SURVEY' }, - '/api/pc-parts': { table: 'asset_pc_parts', context: 'PC PARTS' }, - '/api/equipment': { table: 'asset_equipment', context: 'EQUIPMENT' }, - '/api/office-supplies': { table: 'asset_office_supplies', context: 'OFFICE SUPPLIES' }, - '/api/cloud': { table: 'asset_cloud', context: 'CLOUD' }, - '/api/domain': { table: 'asset_domain', context: 'DOMAIN' }, - '/api/cost': { table: 'asset_cost', context: 'COST' }, - '/api/vip': { table: 'asset_vip', context: 'VIP' }, - '/api/asset/software/assignment': { table: 'asset_software_assignment', context: 'SW ASSIGN' } -}; + // 2. Clear Existing Data (Optional - depending on strategy) + // For now, we use REPLACE INTO or similar. But user requested batch save as sync. + await connection.query(`DELETE FROM ${table}`); -Object.entries(routeMap).forEach(([route, { table, context }]) => { - app.get(route, (req, res) => fetchAssets(table, res, context)); - app.post(`${route}/batch`, (req, res) => saveAssetsBatch(table, req.body, res, `${context} BATCH`)); -}); - -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]); + // 3. Insert New Data + if (data.length > 0) { + const placeholders = validFields.map(() => '?').join(', '); + const sql = `INSERT INTO ${table} (${validFields.join(', ')}) VALUES (${placeholders})`; + + for (const item of data) { + const values = validFields.map(field => { + const val = item[field]; + return val === undefined ? null : val; + }); + await connection.query(sql, values); } - await connection.commit(); - res.json({ success: true }); - } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH HISTORY'); } finally { connection.release(); } + } + + await connection.commit(); + console.log(`✅ [BATCH SAVE] Table: ${table}, Count: ${data.length}`); + res.json({ success: true, count: data.length }); + } catch (err) { + if (connection) await connection.rollback(); + handleError(res, err, 'BATCH SAVE'); + } finally { + if (connection) connection.release(); + } }); -app.get('/api/generate-asset-code', async (req, res) => { +// 2. Get All Assets (Integrated Master Data) +app.get('/api/assets/master', async (req, res) => { try { - const { prefix } = req.query; - if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); + const connection = await pool.getConnection(); + const tables = { + pc: 'asset_pc', + server: 'asset_server', + storage: 'asset_storage', + network: 'asset_network', + equipment: 'asset_equipment', + officeSupplies: 'asset_office_supplies', + survey: 'asset_survey', + vip: 'asset_vip', + swInternal: 'sw_internal', + swExternal: 'sw_external', + cloud: 'asset_cloud', + users: 'user_master', + swUsers: 'sw_assignment', + logs: 'asset_history' + }; + + const masterData = {}; + for (const [key, tableName] of Object.entries(tables)) { + try { + const [rows] = await connection.query(`SELECT * FROM ${tableName}`); + masterData[key] = rows; + } catch (err) { + console.warn(`[MASTER DATA] Skipping ${tableName}: ${err.message}`); + masterData[key] = []; + } + } + + connection.release(); + res.json(masterData); + } catch (err) { + handleError(res, err, 'MASTER DATA'); + } +}); + +// 3. Single Asset Save (Update or Insert) +app.post('/api/asset/:category/save', async (req, res) => { + const { category } = req.params; + const asset = req.body; + const tableMap = { + pc: 'asset_pc', + server: 'asset_server', + storage: 'asset_storage', + network: 'asset_network', + equipment: 'asset_equipment', + officeSupplies: 'asset_office_supplies', + survey: 'asset_survey', + vip: 'asset_vip', + swInternal: 'sw_internal', + swExternal: 'sw_external', + cloud: 'asset_cloud' + }; + const table = tableMap[category]; + + if (!table) return res.status(400).json({ error: 'Invalid category' }); + + try { + const connection = await pool.getConnection(); + const [columns] = await connection.query(`DESCRIBE ${table}`); + const validFields = columns.map(c => c.Field); + + const dataObj = {}; + validFields.forEach(f => { if (asset[f] !== undefined) dataObj[f] = asset[f]; }); + + const keys = Object.keys(dataObj); + const values = Object.values(dataObj); + const placeholders = keys.map(() => '?').join(', '); + const updates = keys.map(k => `${k} = VALUES(${k})`).join(', '); + + const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`; - // asset_code 컬럼이 있는 것으로 확인된 테이블 목록 (DESCRIBE 결과 기반) + await connection.query(sql, values); + connection.release(); + console.log(`💾 [ASSET SAVE] Category: ${category}, ID: ${asset.id}`); + res.json({ success: true }); + } catch (err) { + handleError(res, err, 'ASSET SAVE'); + } +}); + +// 4. Asset Delete +app.delete('/api/asset/:category/:id', async (req, res) => { + const { category, id } = req.params; + const tableMap = { + pc: 'asset_pc', server: 'asset_server', storage: 'asset_storage', network: 'asset_network', + equipment: 'asset_equipment', officeSupplies: 'asset_office_supplies', survey: 'asset_survey', + vip: 'asset_vip', swInternal: 'sw_internal', swExternal: 'sw_external', cloud: 'asset_cloud' + }; + const table = tableMap[category]; + if (!table) return res.status(400).json({ error: 'Invalid category' }); + + try { + const connection = await pool.getConnection(); + await connection.query(`DELETE FROM ${table} WHERE id = ?`, [id]); + connection.release(); + console.log(`🗑️ [ASSET DELETE] Category: ${category}, ID: ${id}`); + res.json({ success: true }); + } catch (err) { + handleError(res, err, 'ASSET DELETE'); + } +}); + +// 5. Generate Next Asset Code +app.get('/api/generate-asset-code', async (req, res) => { + const { prefix, purchaseDate } = req.query; + if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); + + try { + const connection = await pool.getConnection(); const tables = [ 'asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip' ]; - let lastCode = ''; + // Extract YYYYMM from purchaseDate (format: YYYY-MM-DD) + const datePart = purchaseDate ? purchaseDate.toString().replace(/-/g, '').substring(0, 6) : ''; + const searchPattern = datePart ? `${prefix}-${datePart}-%` : `${prefix}-%`; + let maxNum = 0; for (const table of tables) { try { - const [rows] = await pool.query( - `SELECT asset_code FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`, - [`${prefix}%`] + const [rows] = await connection.query( + `SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, + [searchPattern] ); - if (rows.length > 0) { - const code = rows[0].asset_code; - // 숫자 부분 추출 (예: SVR048 -> 48) - const numMatch = code.match(/\d+/); - if (numMatch) { - const num = parseInt(numMatch[0]); - if (num > maxNum) { - maxNum = num; - lastCode = code; - } - } - } + rows.forEach(row => { + const parts = row.asset_code.split('-'); + const seqPart = parts[parts.length - 1]; // Last part is sequence + const num = parseInt(seqPart); + if (!isNaN(num) && num > maxNum) maxNum = num; + }); } catch (err) { - console.warn(`[GENERATE CODE] Skipping ${table}: ${err.message}`); + // Table might not exist or column missing } } const nextNum = maxNum + 1; - const nextCode = `${prefix}${String(nextNum).padStart(3, '0')}`; + const nextCode = datePart + ? `${prefix}-${datePart}-${String(nextNum).padStart(4, '0')}` + : `${prefix}-${String(nextNum).padStart(4, '0')}`; // Fallback if no date - console.log(`🆕 [GENERATE CODE] Prefix: ${prefix}, Last: ${lastCode}, Next: ${nextCode}`); + connection.release(); + console.log(`🆕 [GENERATE CODE] Prefix: ${prefix}, Date: ${datePart}, Next: ${nextCode}`); res.json({ nextCode }); } catch (err) { handleError(res, err, 'GENERATE CODE'); diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 892893f..87f9a9c 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -283,9 +283,10 @@ class HwAssetModal extends BaseModal { if (!cat) { alert('구분을 먼저 선택해주세요.'); return; } const prefix = TYPE_PREFIX_MAP[cat] || 'ETC'; + const purchaseDate = (document.getElementById('hw-purchase_date') as HTMLInputElement)?.value || ''; try { - const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}`); + const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`); const data = await res.json(); if (data.nextCode) { setFieldValue('hw-asset_code', data.nextCode);