feat: 엑셀 양식 고도화 및 DB/UI 연동 최적화 (드롭다운, 상세위치, 데이터 정합성)

1. exceljs 도입: 엑셀 파일 내 실제 드롭다운(건물-상세위치 연동) 구현\n2. 업로드 프리뷰: 위치/상세위치 Select Box 전환 및 실시간 동기화\n3. DB 마이그레이션: user_name, mainboard, detail_location 컬럼 자동화\n4. 양식 보강: 시트별 커스텀 유형 적용 및 누락 필드(모델명, 담당자 부, 용도/상세 등) 추가\n5. UI 개선: 편집 폰트 색상(#FF3D00) 고정 및 지능형 레드 박스 강조 로직 적용
This commit is contained in:
2026-04-24 15:35:59 +09:00
parent ab0d25b827
commit d711af7f69
8 changed files with 1472 additions and 417 deletions

288
server.js
View File

@@ -26,78 +26,75 @@ const pool = mysql.createPool({
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;
`);
// 1. 기본 테이블 생성
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),
current_org VARCHAR(100), prev_org VARCHAR(100), user_name VARCHAR(100), location VARCHAR(255), detail_location VARCHAR(255),
manager_main VARCHAR(100), manager_sub VARCHAR(100), ip_address VARCHAR(100),
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), remarks TEXT,
model_name VARCHAR(255), mainboard VARCHAR(255), os VARCHAR(100), cpu VARCHAR(255), ram VARCHAR(100), gpu VARCHAR(100),
storage1 VARCHAR(255), storage2 VARCHAR(255), storage3 VARCHAR(255), monitoring VARCHAR(100), price VARCHAR(100), remarks TEXT,
storage_location VARCHAR(255), status VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
// 다른 하드웨어 테이블들도 동일한 스키마로 생성 (서버, 스토리지, 비품, 모바일)
// 2. 누락된 컬럼 강제 추가 (Migration)
const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets'];
for (const table of tables) {
const [userCols] = await connection.query(`SHOW COLUMNS FROM ${table} LIKE 'user_name'`);
if (userCols.length === 0) await connection.query(`ALTER TABLE ${table} ADD COLUMN user_name VARCHAR(100) AFTER prev_org`);
const [mbCols] = await connection.query(`SHOW COLUMNS FROM ${table} LIKE 'mainboard'`);
if (mbCols.length === 0) await connection.query(`ALTER TABLE ${table} ADD COLUMN mainboard VARCHAR(255) AFTER model_name`);
const [dlCols] = await connection.query(`SHOW COLUMNS FROM ${table} LIKE 'detail_location'`);
if (dlCols.length === 0) await connection.query(`ALTER TABLE ${table} ADD COLUMN detail_location VARCHAR(255) AFTER location`);
}
// 다른 하드웨어 테이블들도 동일한 스키마로 보장
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
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
log_user VARCHAR(100), details TEXT, cost DECIMAL(15,2) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
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(255), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) 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(255), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) 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)
position VARCHAR(50), user_name VARCHAR(100), usage_period VARCHAR(100), doc_name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) 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),
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;
`);
@@ -108,6 +105,39 @@ async function ensureTables() {
}
}
// 하드웨어 쿼리 헬퍼 (detail_location 추가)
const hardwareInsertSQL = (table) => `
INSERT INTO ${table} (
id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details,
current_org, prev_org, user_name, location, detail_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, 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.담당자_부||'', a.IP주소||'',
a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.메인보드||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'',
a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.금액||'', 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,
용도: r.purpose, 상세: r.details, 사용자: r.user_name, 현사용조직: r.current_org,
이전사용조직: r.prev_org, 위치: r.location, 상세위치: r.detail_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, HDD1: r.storage3, 모니터링: r.monitoring, 금액: r.price,
비고: r.remarks, 보관위치: r.storage_location, 현재상태: r.status
};
};
// 공통 배치 저장 로직
async function batchSave(tableName, assets, getQuery) {
const connection = await pool.getConnection();
@@ -128,76 +158,13 @@ async function batchSave(tableName, assets, getQuery) {
}
}
// 하드웨어 쿼리 헬퍼
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, remarks,
storage_location, status
) VALUES ?
`;
// --- API 라우트 ---
const getHardwareValues = (a) => [
a.id, a.법인||'', a.자산코드||'', a.구매연월||'', a.type||'', 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.HDD1||'', a.모니터링||'', a.금액||'', 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,
용도: 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,
HDD1: r.storage3,
모니터링: r.monitoring,
금액: r.price,
비고: r.remarks,
보관위치: r.storage_location,
현재상태: r.status
};
};
// --- API 라우트 정의 ---
// 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 });
}
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/pc/batch', async (req, res) => {
@@ -210,7 +177,6 @@ app.post('/api/pc/batch', async (req, res) => {
} 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');
@@ -228,7 +194,6 @@ app.post('/api/server/batch', async (req, res) => {
} 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');
@@ -246,7 +211,6 @@ app.post('/api/storage/batch', async (req, res) => {
} 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');
@@ -264,7 +228,6 @@ app.post('/api/equip/batch', async (req, res) => {
} 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');
@@ -282,17 +245,10 @@ app.post('/api/mobile/batch', async (req, res) => {
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 구독 SW API
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.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
})));
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 }); }
});
@@ -300,53 +256,33 @@ 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, 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.비고||''
])
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 }); }
});
// 영구 SW API
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.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
})));
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 }); }
});
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, 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.비고||''
])
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 }); }
});
// 클라우드 API
app.get('/api/cloud', 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
})));
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 }); }
});
@@ -360,27 +296,13 @@ app.post('/api/cloud/batch', async (req, res) => {
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 로그 API
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, cost: r.cost
})));
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');
@@ -400,20 +322,13 @@ app.post('/api/sw-users/batch', async (req, res) => {
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]);
}
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]);
}
await connection.commit();
connection.release();
res.json({ success: true });
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');
@@ -431,39 +346,20 @@ app.post('/api/ops/domain/batch', async (req, res) => {
} 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' });
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 [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}` });
} catch (err) {
res.status(500).json({ error: err.message });
}
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 초기화 및 서버 기동
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.listen(PORT, () => { console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`); });
}).catch(err => { console.error('❌ Failed to start server:', err); });