feat: 자산번호 4자리 일련번호 확장 및 날짜 기반 생성 로직 추가
- 백엔드(server.js): 자산번호 자동 생성 시 구매일자(YYYYMM)를 파싱하여 [접두사]-[YYYYMM]-[0000] 형태로 4자리 일련번호를 부여하도록 로직 전면 수정 - 프런트엔드(HWModal.ts): 자산번호 생성 API 호출 시 사용자가 입력한 구매일자 데이터를 파라미터로 함께 전송하도록 연동 - 전체 DB 및 로컬 마스터 데이터의 4자리 일련번호 및 날짜 복구 마이그레이션 반영
This commit is contained in:
299
server.js
299
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');
|
||||
|
||||
Reference in New Issue
Block a user