refactor: integrate software assets into unified schema and optimize backend API
This commit is contained in:
713
server.js
713
server.js
@@ -6,10 +6,14 @@ import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
|
||||
// Request Logger
|
||||
app.use((req, res, next) => {
|
||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
@@ -17,464 +21,351 @@ const pool = mysql.createPool({
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
port: parseInt(process.env.DB_PORT || '3306'),
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
charset: 'utf8mb4'
|
||||
});
|
||||
|
||||
// 테이블 존재 여부 확인 및 자동 생성
|
||||
async function ensureTables() {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS cloud_assets (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
platform_name VARCHAR(100),
|
||||
corp VARCHAR(100),
|
||||
dept VARCHAR(100),
|
||||
product_name VARCHAR(255),
|
||||
account_name VARCHAR(255),
|
||||
pay_method VARCHAR(100),
|
||||
pay_day VARCHAR(50),
|
||||
card_num VARCHAR(100),
|
||||
monthly_fee VARCHAR(100),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS asset_logs (
|
||||
id 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(`
|
||||
CREATE TABLE IF NOT EXISTS pc_assets (
|
||||
id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), purchase_date VARCHAR(50),
|
||||
type VARCHAR(50), detail_purpose VARCHAR(100), purpose VARCHAR(255), details TEXT,
|
||||
current_org VARCHAR(100), prev_org VARCHAR(100), location VARCHAR(255),
|
||||
manager_main VARCHAR(100), manager_sub VARCHAR(100), ip_address VARCHAR(50),
|
||||
remote_tool VARCHAR(100), server_id VARCHAR(100), server_pw VARCHAR(100),
|
||||
model_name VARCHAR(255), mainboard VARCHAR(255), os VARCHAR(100), cpu VARCHAR(100), ram VARCHAR(100), gpu VARCHAR(100),
|
||||
storage1 VARCHAR(100), storage2 VARCHAR(100), storage3 VARCHAR(100), monitoring VARCHAR(100), price VARCHAR(100), vendor VARCHAR(100), remarks TEXT,
|
||||
storage_location VARCHAR(255), status VARCHAR(50)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
// 다른 하드웨어 테이블들도 동일한 스키마로 생성 (서버, 스토리지, 비품, 모바일)
|
||||
for (const table of ['server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']) {
|
||||
await connection.query(`CREATE TABLE IF NOT EXISTS ${table} LIKE pc_assets`);
|
||||
}
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS sw_sub_assets (
|
||||
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
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS sw_perm_assets (
|
||||
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), 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(`
|
||||
CREATE TABLE IF NOT EXISTS sw_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY, sw_id VARCHAR(50), corp VARCHAR(100), dept VARCHAR(100),
|
||||
position VARCHAR(100), user_name VARCHAR(100), usage_period VARCHAR(255), doc_name VARCHAR(255)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS ops_domain_assets (
|
||||
id VARCHAR(50) PRIMARY KEY, type VARCHAR(50), corp VARCHAR(100),
|
||||
service_name VARCHAR(255), domain_name VARCHAR(255), start_date VARCHAR(50),
|
||||
expiry_date VARCHAR(50), price VARCHAR(100), manager_main VARCHAR(100),
|
||||
manager_sub VARCHAR(100), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
|
||||
// 기존 테이블들에 vendor 컬럼이 없는 경우 추가 (Migration)
|
||||
const [cols] = await pool.query("SHOW COLUMNS FROM pc_assets LIKE 'vendor'");
|
||||
if (cols.length === 0) {
|
||||
for (const table of ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']) {
|
||||
await pool.query(`ALTER TABLE ${table} ADD COLUMN vendor VARCHAR(100) AFTER price`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ All ITAM tables ensured.');
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 공통 배치 저장 로직
|
||||
async function batchSave(tableName, assets, getQuery) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
await connection.query(`DELETE FROM ${tableName}`);
|
||||
if (assets.length > 0) {
|
||||
const { sql, values } = getQuery(assets);
|
||||
await connection.query(sql, [values]);
|
||||
}
|
||||
await connection.commit();
|
||||
return { success: true, count: assets.length };
|
||||
} catch (err) {
|
||||
console.error(`❌ Batch Save Error (${tableName}):`, err.message);
|
||||
await connection.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 하드웨어 쿼리 헬퍼
|
||||
const hardwareInsertSQL = (table) => `
|
||||
INSERT INTO ${table} (
|
||||
id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details,
|
||||
current_org, prev_org, location, manager_main, manager_sub, ip_address,
|
||||
remote_tool, server_id, server_pw, model_name, mainboard, os, cpu, ram, gpu,
|
||||
storage1, storage2, storage3, monitoring, price, vendor, remarks,
|
||||
storage_location, status
|
||||
) VALUES ?
|
||||
`;
|
||||
|
||||
const getHardwareValues = (a) => [
|
||||
a.id, a.법인||'', a.자산코드||'', a.구매연월||'', a.type||'', a.상세용도||'', a['사용자']||a.용도||'', a.상세||'',
|
||||
a.현사용조직||'', a.이전사용조직||'', a.위치||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'',
|
||||
a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.메인보드||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'',
|
||||
a.SSD1||'', a.SSD2||'', a.SSD3||'', a.모니터링||'', a.금액||'', a.납품업체||a.vendor||'', a.비고||'',
|
||||
a.보관위치||'', a.현재상태||''
|
||||
];
|
||||
|
||||
const mapHardware = (r, defaultType) => {
|
||||
const type = r.type || defaultType;
|
||||
return {
|
||||
id: r.id,
|
||||
법인: r.corp,
|
||||
자산코드: r.asset_code,
|
||||
구매연월: r.purchase_date,
|
||||
구매일: r.purchase_date,
|
||||
type: type,
|
||||
상세용도: (type !== '개인PC' && !r.detail_purpose) ? type : r.detail_purpose,
|
||||
용도: (type !== '개인PC' && !r.detail_purpose) ? type : r.detail_purpose,
|
||||
사용자: r.purpose,
|
||||
상세: r.details,
|
||||
현사용조직: r.current_org,
|
||||
이전사용조직: r.prev_org,
|
||||
위치: r.location,
|
||||
담당자_정: r.manager_main,
|
||||
담당자_부: r.manager_sub,
|
||||
IP주소: r.ip_address,
|
||||
원격접속: r.remote_tool,
|
||||
서버ID: r.server_id,
|
||||
서버PW: r.server_pw,
|
||||
모델명: r.model_name,
|
||||
메인보드: r.mainboard,
|
||||
OS: r.os,
|
||||
CPU: r.cpu,
|
||||
RAM: r.ram,
|
||||
GPU: r.gpu,
|
||||
SSD1: r.storage1,
|
||||
SSD2: r.storage2,
|
||||
SSD3: r.storage3,
|
||||
모니터링: r.monitoring,
|
||||
금액: r.price,
|
||||
납품업체: r.vendor,
|
||||
비고: r.remarks,
|
||||
보관위치: r.storage_location,
|
||||
현재상태: r.status
|
||||
};
|
||||
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 });
|
||||
};
|
||||
|
||||
// --- API 라우트 정의 ---
|
||||
// --- Mapping Definitions ---
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
const mapObject = (obj, mapping) => {
|
||||
const mapped = {};
|
||||
Object.entries(obj).forEach(([key, val]) => {
|
||||
const dbKey = mapping[key] || key;
|
||||
mapped[dbKey] = val;
|
||||
});
|
||||
return mapped;
|
||||
};
|
||||
|
||||
// --- GET Routes ---
|
||||
|
||||
// PC API
|
||||
app.get('/api/pc', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM pc_assets');
|
||||
console.log('🔍 DB Raw Rows (PC):', rows.length, 'items found.');
|
||||
if (rows.length > 0) console.log('🔍 First row sample:', rows[0]);
|
||||
res.json(rows.map(r => mapHardware(r, '개인PC')));
|
||||
} catch (err) {
|
||||
console.error('❌ DB Query Error (PC):', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
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.post('/api/pc/batch', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('pc_assets', req.body, (assets) => ({
|
||||
sql: hardwareInsertSQL('pc_assets'),
|
||||
values: assets.map(getHardwareValues)
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// 서버 API
|
||||
app.get('/api/server', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM server_assets');
|
||||
res.json(rows.map(r => mapHardware(r, '서버')));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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.post('/api/server/batch', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('server_assets', req.body, (assets) => ({
|
||||
sql: hardwareInsertSQL('server_assets'),
|
||||
values: assets.map(getHardwareValues)
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// 스토리지 API
|
||||
app.get('/api/storage', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM storage_assets');
|
||||
res.json(rows.map(r => mapHardware(r, '스토리지')));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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.post('/api/storage/batch', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('storage_assets', req.body, (assets) => ({
|
||||
sql: hardwareInsertSQL('storage_assets'),
|
||||
values: assets.map(getHardwareValues)
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// 전산비품 API
|
||||
app.get('/api/equip', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM equip_assets');
|
||||
res.json(rows.map(r => mapHardware(r, '전산비품')));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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.post('/api/equip/batch', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('equip_assets', req.body, (assets) => ({
|
||||
sql: hardwareInsertSQL('equip_assets'),
|
||||
values: assets.map(getHardwareValues)
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// 모바일 API
|
||||
app.get('/api/mobile', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM mobile_assets');
|
||||
res.json(rows.map(r => mapHardware(r, '모바일기기')));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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.post('/api/mobile/batch', async (req, res) => {
|
||||
app.get('/api/asset/software/subscription', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('mobile_assets', req.body, (assets) => ({
|
||||
sql: hardwareInsertSQL('mobile_assets'),
|
||||
values: assets.map(getHardwareValues)
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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); }
|
||||
});
|
||||
|
||||
// 구독 SW API
|
||||
app.get('/api/sw/sub', async (req, res) => {
|
||||
app.get('/api/asset/software/perpetual', 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.category, 부서: r.dept, 제품명: r.product_name,
|
||||
라이선스유형: r.license_type, 수량: r.quantity, 금액: r.price,
|
||||
구매일: r.purchase_date, 시작일: r.start_date, 만료일: r.expiry_date,
|
||||
납품업체: r.vendor, 비고: r.remarks
|
||||
})));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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.post('/api/sw/sub/batch', async (req, res) => {
|
||||
app.get('/api/asset/cloud', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('sw_sub_assets', req.body, (assets) => ({
|
||||
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.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||''
|
||||
])
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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); }
|
||||
});
|
||||
|
||||
// 영구 SW API
|
||||
app.get('/api/sw/perm', async (req, res) => {
|
||||
app.get('/api/asset/domain', 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.category, 부서: r.dept, 제품명: r.product_name,
|
||||
라이선스키: r.license_key, 수량: r.quantity, 금액: r.price,
|
||||
구매일: r.purchase_date, 시작일: r.start_date, 만료일: r.expiry_date,
|
||||
납품업체: r.vendor, 비고: r.remarks
|
||||
})));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
const [rows] = await pool.query(`SELECT * FROM asset_domain`);
|
||||
res.json(rows);
|
||||
} catch (err) { handleError(res, err, 'GET /api/asset/domain', true); }
|
||||
});
|
||||
|
||||
app.post('/api/sw/perm/batch', async (req, res) => {
|
||||
app.get('/api/asset/software/assignment', 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, 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.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||''
|
||||
])
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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); }
|
||||
});
|
||||
|
||||
// 클라우드 API
|
||||
app.get('/api/cloud', async (req, res) => {
|
||||
app.get('/api/asset/history', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM cloud_assets');
|
||||
res.json(rows.map(r => ({
|
||||
id: r.id, type: '클라우드', 플랫폼명: r.platform_name, 법인: r.corp, 부서: r.dept,
|
||||
제품명: r.product_name, 계정명: r.account_name, 결제수단: r.pay_method,
|
||||
결제일: r.pay_day, 연결카드번호: r.card_num, 당월청구액: r.monthly_fee, 비고: r.remarks
|
||||
})));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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); }
|
||||
});
|
||||
|
||||
app.post('/api/cloud/batch', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('cloud_assets', req.body, (assets) => ({
|
||||
sql: `INSERT INTO cloud_assets (id, platform_name, corp, dept, product_name, account_name, pay_method, pay_day, card_num, monthly_fee, remarks) VALUES ?`,
|
||||
values: assets.map(a => [a.id, a.플랫폼명||'', a.법인||'', a.부서||'', a.제품명||'', a.계정명||'', a.결제수단||'', a.결제일||'', a.연결카드번호||'', a.당월청구액||'', a.비고||''])
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
// --- POST Batch Routes ---
|
||||
|
||||
// 로그 API
|
||||
app.get('/api/logs', async (req, res) => {
|
||||
async function saveHwBatch(category, items, res) {
|
||||
const connection = await pool.getConnection();
|
||||
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, 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, (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 }); }
|
||||
});
|
||||
|
||||
// SW 사용자 API
|
||||
app.get('/api/sw-users', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM sw_users');
|
||||
const grouped = rows.reduce((acc, u) => {
|
||||
if (!acc[u.sw_id]) acc[u.sw_id] = [];
|
||||
acc[u.sw_id].push([u.corp, u.dept, u.position, u.user_name, u.usage_period, u.doc_name]);
|
||||
return acc;
|
||||
}, {});
|
||||
res.json(Object.keys(grouped).map(sw_id => ({ sw_id, userData: grouped[sw_id] })));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/sw-users/batch', async (req, res) => {
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
await connection.beginTransaction();
|
||||
await connection.query('DELETE FROM sw_users');
|
||||
const allUsers = req.body;
|
||||
if (allUsers.length > 0) {
|
||||
const values = allUsers.flatMap(item =>
|
||||
(item.userData || []).map(u => [item.sw_id, u[0], u[1], u[2], u[3], u[4], u[5]])
|
||||
);
|
||||
if (values.length > 0) {
|
||||
await connection.query('INSERT INTO sw_users (sw_id, corp, dept, position, user_name, usage_period, doc_name) VALUES ?', [values]);
|
||||
}
|
||||
// 1. 해당 카테고리 기존 데이터 삭제
|
||||
await connection.query('DELETE FROM asset_hardware WHERE asset_category = ?', [category]);
|
||||
|
||||
// 2. 새 데이터 삽입
|
||||
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]);
|
||||
}
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
res.json({ success: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// 도메인 관리 API
|
||||
app.get('/api/ops/domain', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM ops_domain_assets ORDER BY created_at DESC');
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/ops/domain/batch', async (req, res) => {
|
||||
try {
|
||||
const result = await batchSave('ops_domain_assets', req.body, (assets) => ({
|
||||
sql: `INSERT INTO ops_domain_assets (id, type, corp, service_name, domain_name, start_date, expiry_date, price, manager_main, manager_sub, remarks) VALUES ?`,
|
||||
values: assets.map(a => [a.id, 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||''])
|
||||
}));
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// 자산번호 자동 생성 API
|
||||
app.get('/api/generate-asset-code', async (req, res) => {
|
||||
const { prefix } = req.query;
|
||||
if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
|
||||
|
||||
try {
|
||||
const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets'];
|
||||
let maxNum = 0;
|
||||
|
||||
for (const table of tables) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`,
|
||||
[`${prefix}%`]
|
||||
);
|
||||
rows.forEach(r => {
|
||||
const numPart = r.asset_code.replace(prefix, '');
|
||||
const num = parseInt(numPart);
|
||||
if (!isNaN(num) && num > maxNum) maxNum = num;
|
||||
});
|
||||
}
|
||||
|
||||
const nextNum = (maxNum + 1).toString().padStart(4, '0');
|
||||
res.json({ nextCode: `${prefix}${nextNum}` });
|
||||
res.json({ success: true, count: items.length });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
await connection.rollback();
|
||||
handleError(res, err, `BATCH SAVE ${category}`);
|
||||
} 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));
|
||||
|
||||
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(); }
|
||||
});
|
||||
|
||||
// 초기화 및 서버 기동
|
||||
ensureTables().then(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('❌ Failed to start server:', err);
|
||||
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(); }
|
||||
});
|
||||
|
||||
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.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.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(); }
|
||||
});
|
||||
|
||||
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}%`]);
|
||||
let nextNum = 1;
|
||||
if (rows.length > 0) {
|
||||
const lastCode = rows[0].asset_code;
|
||||
const lastNum = parseInt(lastCode.split('-').pop() || '0');
|
||||
nextNum = lastNum + 1;
|
||||
}
|
||||
res.json({ nextCode: `${prefix}${String(nextNum).padStart(3, '0')}` });
|
||||
} catch (err) { handleError(res, err, 'GENERATE CODE'); }
|
||||
});
|
||||
|
||||
app.listen(3000, '0.0.0.0', () => {
|
||||
console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Safe Korean Mapping)');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user