feat: 자산 카테고리별 6개 전용 테이블 분리 및 백엔드 API, 프론트엔드 상태 관리 전면 개편 (개인PC, 서버, 스토리지, 전산비품, 구독SW, 영구SW)

This commit is contained in:
2026-04-17 17:25:52 +09:00
parent 6904925146
commit 415727a866
11 changed files with 409 additions and 593 deletions

View File

@@ -20,52 +20,140 @@ async function initDB() {
await connection.query(`USE ${DB_NAME};`); await connection.query(`USE ${DB_NAME};`);
console.log(`✅ 데이터베이스 생성 완료: ${DB_NAME}`); console.log(`✅ 데이터베이스 생성 완료: ${DB_NAME}`);
// 2. 하드웨어 자산 테이블 // 2. 개인PC 테이블
const createHwTable = ` const createPcTable = `
CREATE TABLE IF NOT EXISTS hw_assets ( CREATE TABLE IF NOT EXISTS pc_assets (
id VARCHAR(50) PRIMARY KEY, id VARCHAR(50) PRIMARY KEY,
type VARCHAR(50) NOT NULL COMMENT '개인PC, 서버, 스토리지, 전산비품',
corp VARCHAR(100) COMMENT '구매법인', corp VARCHAR(100) COMMENT '구매법인',
asset_code VARCHAR(100) COMMENT '자산번호/코드', asset_code VARCHAR(100) COMMENT '자산번호/코드',
asset_name VARCHAR(255) COMMENT '명칭/용도', user VARCHAR(100) COMMENT '사용자',
location VARCHAR(255) COMMENT '설치위치', location VARCHAR(255) COMMENT '설치위치',
current_org VARCHAR(255) COMMENT '현 사용조직',
prev_org VARCHAR(255) COMMENT '이전 사용조직',
manager_main VARCHAR(100) COMMENT '담당자(정)',
manager_sub VARCHAR(100) COMMENT '담당자(부)',
ip_address VARCHAR(100) COMMENT 'IP 주소 1',
ip_address2 VARCHAR(100) COMMENT 'IP 주소 2',
mac_address VARCHAR(100) COMMENT 'MAC 주소',
os VARCHAR(100),
cpu VARCHAR(255), cpu VARCHAR(255),
gpu VARCHAR(255),
ram VARCHAR(100), ram VARCHAR(100),
storage1 VARCHAR(255), ssd1 VARCHAR(100),
storage2 VARCHAR(255), ssd2 VARCHAR(100),
model_name VARCHAR(255), hdd1 VARCHAR(100),
hdd2 VARCHAR(100),
ip_address VARCHAR(100),
hw_spec TEXT COMMENT 'HW사양 상세',
purchase_date VARCHAR(50), purchase_date VARCHAR(50),
price VARCHAR(100), price VARCHAR(100),
vendor VARCHAR(255) COMMENT '납품업체', vendor VARCHAR(255),
doc_name VARCHAR(255) COMMENT '품의서명', doc_name VARCHAR(255),
remote_tool VARCHAR(100) COMMENT '원격도구', remarks TEXT,
server_id VARCHAR(100),
server_pw VARCHAR(100),
monitoring VARCHAR(100),
remarks TEXT COMMENT '비고',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`; `;
// 3. 소프트웨어 자산 테이블 // 3. 서버 테이블
const createSwTable = ` const createServerTable = `
CREATE TABLE IF NOT EXISTS sw_assets ( CREATE TABLE IF NOT EXISTS server_assets (
id VARCHAR(50) PRIMARY KEY,
corp VARCHAR(100) COMMENT '구매법인',
asset_code VARCHAR(100) COMMENT '자산번호',
purchase_date VARCHAR(50) COMMENT '구매일자',
type VARCHAR(50) DEFAULT '물리' COMMENT '물리/가상',
purpose VARCHAR(255) COMMENT '용도',
details TEXT COMMENT '상세내용',
current_org VARCHAR(255) COMMENT '현 사용조직',
prev_org VARCHAR(255) COMMENT '이전 사용조직',
location VARCHAR(255) COMMENT '설치위치',
manager_main VARCHAR(100) COMMENT '담당자(정)',
manager_sub VARCHAR(100) COMMENT '담당자(부)',
ip_address VARCHAR(100) COMMENT 'IP 주소 1',
remote_tool VARCHAR(100) COMMENT '원격도구',
server_id VARCHAR(100),
server_pw VARCHAR(100),
model_name 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),
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
// 4. 스토리지 테이블
const createStorageTable = `
CREATE TABLE IF NOT EXISTS storage_assets (
id VARCHAR(50) PRIMARY KEY,
corp VARCHAR(100) COMMENT '구매법인',
type VARCHAR(50) COMMENT '유형',
asset_code VARCHAR(100) COMMENT '자산코드',
asset_name VARCHAR(255) COMMENT '명칭',
location VARCHAR(255) COMMENT '설치위치',
model_name VARCHAR(255),
capacity VARCHAR(100) COMMENT '용량',
manager_main VARCHAR(100) COMMENT '담당자(정)',
manager_sub VARCHAR(100) COMMENT '담당자(부)',
ip_address VARCHAR(100),
mac_address VARCHAR(100),
purchase_date VARCHAR(50),
price VARCHAR(100),
vendor VARCHAR(255),
doc_name VARCHAR(255),
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
// 5. 전산비품 테이블
const createEquipTable = `
CREATE TABLE IF NOT EXISTS equip_assets (
id VARCHAR(50) PRIMARY KEY,
corp VARCHAR(100) COMMENT '구매법인',
type VARCHAR(50) COMMENT '비품유형',
asset_code VARCHAR(100) COMMENT '자산코드',
asset_name VARCHAR(255) COMMENT '명칭',
location VARCHAR(255) COMMENT '설치위치',
manager VARCHAR(100) COMMENT '관리자',
ip_address VARCHAR(100),
mac_address VARCHAR(100),
hw_spec TEXT,
os VARCHAR(100),
purchase_date VARCHAR(50),
price VARCHAR(100),
vendor VARCHAR(255),
doc_name VARCHAR(255),
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
// 6. 구독SW 테이블
const createSubSwTable = `
CREATE TABLE IF NOT EXISTS subscription_sw (
id VARCHAR(50) PRIMARY KEY, id VARCHAR(50) PRIMARY KEY,
type VARCHAR(50) NOT NULL COMMENT '구독SW, 영구SW',
category VARCHAR(100) COMMENT '분야', category VARCHAR(100) COMMENT '분야',
corp VARCHAR(100) COMMENT '구매법인', corp VARCHAR(100) COMMENT '구매법인',
dept VARCHAR(100) COMMENT '부서', dept VARCHAR(100) COMMENT '부서',
product_name VARCHAR(255) NOT NULL, product_name VARCHAR(255) NOT NULL,
purchase_date VARCHAR(50), purchase_date VARCHAR(50),
subscription_date VARCHAR(50), subscription_date VARCHAR(50),
price VARCHAR(100),
quantity INT DEFAULT 1,
account_id VARCHAR(255) COMMENT '계정명',
vendor VARCHAR(255),
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
// 7. 영구SW 테이블
const createPermSwTable = `
CREATE TABLE IF NOT EXISTS permanent_sw (
id VARCHAR(50) PRIMARY KEY,
category VARCHAR(100) COMMENT '분야',
corp VARCHAR(100) COMMENT '구매법인',
dept VARCHAR(100) COMMENT '부서',
product_name VARCHAR(255) NOT NULL,
purchase_date VARCHAR(50),
maintenance_status TINYINT(1) DEFAULT 0, maintenance_status TINYINT(1) DEFAULT 0,
price VARCHAR(100), price VARCHAR(100),
quantity INT DEFAULT 1, quantity INT DEFAULT 1,
@@ -76,7 +164,7 @@ async function initDB() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`; `;
// 4. 소프트웨어 사용자 매핑 테이블 // 8. SW 사용자 매핑 테이블 (FK 제약조건 제거하여 유연하게 관리)
const createSwUsersTable = ` const createSwUsersTable = `
CREATE TABLE IF NOT EXISTS sw_users ( CREATE TABLE IF NOT EXISTS sw_users (
id VARCHAR(50) PRIMARY KEY, id VARCHAR(50) PRIMARY KEY,
@@ -87,16 +175,19 @@ async function initDB() {
position VARCHAR(50), position VARCHAR(50),
name VARCHAR(100), name VARCHAR(100),
usage_period VARCHAR(100), usage_period VARCHAR(100),
doc_name VARCHAR(255), doc_name VARCHAR(255)
FOREIGN KEY (sw_id) REFERENCES sw_assets(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`; `;
await connection.query(createHwTable); await connection.query(createPcTable);
await connection.query(createSwTable); await connection.query(createServerTable);
await connection.query(createStorageTable);
await connection.query(createEquipTable);
await connection.query(createSubSwTable);
await connection.query(createPermSwTable);
await connection.query(createSwUsersTable); await connection.query(createSwUsersTable);
console.log('✅ 테이블 생성 완료!'); console.log('✅ 6개 전용 테이블 생성 완료!');
await connection.end(); await connection.end();
console.log('🏁 DB 초기화 프로세스 종료.'); console.log('🏁 DB 초기화 프로세스 종료.');
} }

314
server.js
View File

@@ -23,202 +23,190 @@ const pool = mysql.createPool({
queueLimit: 0 queueLimit: 0
}); });
// --- API Routes --- // --- 공통 헬퍼 함수 ---
async function batchSave(tableName, assets, mapFn) {
// 1. 하드웨어 자산 조회
app.get('/api/hw', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM hw_assets');
// DB 컬럼명을 프론트엔드 인터페이스(한글)에 맞게 매핑
const mapped = rows.map(r => ({
id: r.id,
type: r.type,
법인: r.corp,
자산코드: r.asset_code,
명칭: r.asset_name,
위치: r.location,
현사용조직: r.current_org,
이전사용조직: r.prev_org,
담당자_정: r.manager_main,
관리자: r.manager_main,
담당자_부: r.manager_sub,
IP주소: r.ip_address,
IP2: r.ip_address2,
MACaddress: r.mac_address,
OS: r.os,
CPU: r.cpu,
RAM: r.ram,
SSD1: r.storage1,
SSD2: r.storage2,
모델명: r.model_name,
구매일: r.purchase_date,
금액: r.price,
납품업체: r.vendor,
품의서명: r.doc_name,
용도: r.asset_name, // 서버의 경우 명칭을 용도로 사용
상세: r.remarks,
원격접속: r.remote_tool,
서버ID: r.server_id,
서버PW: r.server_pw,
모니터링: r.monitoring,
비고: r.remarks
}));
res.json(mapped);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 2. 하드웨어 자산 일괄 저장 (항상 덮어쓰기)
app.post('/api/hw/batch', async (req, res) => {
const assets = req.body;
const connection = await pool.getConnection(); const connection = await pool.getConnection();
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
await connection.query(`DELETE FROM ${tableName}`);
await connection.query('DELETE FROM hw_assets');
if (assets.length > 0) { if (assets.length > 0) {
const sql = ` const { sql, values } = mapFn(assets);
INSERT INTO hw_assets (
id, type, corp, asset_code, asset_name, location, current_org, prev_org,
manager_main, manager_sub, ip_address, ip_address2, mac_address, os,
cpu, ram, storage1, storage2, model_name, purchase_date, price,
vendor, doc_name, remote_tool, server_id, server_pw, monitoring, remarks
) VALUES ?
`;
const values = assets.map(a => [
a.id, a.type, a.법인, a.자산코드, a.명칭 || a.용도, a.위치, a.현사용조직, a.이전사용조직,
a.담당자_정 || a.관리자, a.담당자_부, a.IP주소, a.IP2, a.MACaddress, a.OS,
a.CPU, a.RAM, a.SSD1, a.SSD2, a.모델명, a.구매일, a.금액,
a.납품업체, a.품의서명, a.원격접속, a.서버ID, a.서버PW, a.모니터링, a.비고 || a.상세
]);
await connection.query(sql, [values]); await connection.query(sql, [values]);
} }
await connection.commit(); await connection.commit();
res.json({ success: true, count: assets.length, mode: 'overwrite' }); return { success: true, count: assets.length };
} catch (err) { } catch (err) {
await connection.rollback(); await connection.rollback();
res.status(500).json({ error: err.message }); throw err;
} finally { } finally {
connection.release(); connection.release();
} }
}); }
// 3. 소프트웨어 자산 조회 // --- 1. 개인PC (PC) API ---
app.get('/api/sw', async (req, res) => { app.get('/api/pc', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM sw_assets'); const [rows] = await pool.query('SELECT * FROM pc_assets');
const mapped = rows.map(r => ({ const mapped = rows.map(r => ({
id: r.id, id: r.id, type: '개인PC', 법인: r.corp, 자산코드: r.asset_code, 사용자: r.user, 위치: r.location,
type: r.type, CPU: r.cpu, GPU: r.gpu, RAM: r.ram, SSD1: r.ssd1, SSD2: r.ssd2, HDD1: r.hdd1, HDD2: r.hdd2,
분야: r.category, IP주소: r.ip_address, HW사양: r.hw_spec, 구매일: r.purchase_date, 금액: r.price,
법인: r.corp, 납품업체: r.vendor, 품의서명: r.doc_name, 비고: r.remarks
부서: r.dept,
제품명: r.product_name,
구매일: r.purchase_date,
구독일: r.subscription_date,
유지보수여부: !!r.maintenance_status,
금액: r.price,
수량: r.quantity,
계정명: r.account_id,
납품업체: r.vendor,
비고: r.remarks
})); }));
res.json(mapped); res.json(mapped);
} catch (err) { } catch (err) { res.status(500).json({ error: err.message }); }
res.status(500).json({ error: err.message });
}
}); });
// 4. 소프트웨어 자산 일괄 저장 (항상 덮어쓰기) app.post('/api/pc/batch', async (req, res) => {
app.post('/api/sw/batch', async (req, res) => {
const assets = req.body;
const connection = await pool.getConnection();
try { try {
await connection.beginTransaction(); const result = await batchSave('pc_assets', req.body, (assets) => ({
sql: `INSERT INTO pc_assets (id, corp, asset_code, user, location, cpu, gpu, ram, ssd1, ssd2, hdd1, hdd2, ip_address, hw_spec, purchase_date, price, vendor, doc_name, remarks) VALUES ?`,
await connection.query('DELETE FROM sw_assets'); values: assets.map(a => [a.id, a.법인||'', a.자산코드||'', a.사용자||'', a.위치||'', a.CPU||'', a.GPU||'', a.RAM||'', a.SSD1||'', a.SSD2||'', a.HDD1||'', a.HDD2||'', a.IP주소||'', a.HW사양||'', a.구매일||'', a.금액||'', a.납품업체||'', a.품의서명||'', a.비고||''])
}));
if (assets.length > 0) { res.json(result);
const sql = ` } catch (err) { res.status(500).json({ error: err.message }); }
INSERT INTO sw_assets (
id, type, category, corp, dept, product_name, purchase_date,
subscription_date, maintenance_status, price, quantity,
account_id, vendor, remarks
) VALUES ?
`;
const values = assets.map(a => [
a.id, a.type, a.분야, a.법인, a.부서, a.제품명, a.구매일,
a.구독일, a.유지보수여부 ? 1 : 0, a.금액, a.수량,
a.계정명, a.납품업체, a.비고
]);
await connection.query(sql, [values]);
}
await connection.commit();
res.json({ success: true, count: assets.length, mode: 'overwrite' });
} catch (err) {
await connection.rollback();
res.status(500).json({ error: err.message });
} finally {
connection.release();
}
}); });
// 5. SW 사용자 매핑 조회 // --- 2. 서버 (Server) API ---
app.get('/api/server', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM server_assets');
const mapped = rows.map(r => ({
id: r.id, type: '서버', 법인: r.corp, 자산코드: r.asset_code, 구매일: r.purchase_date, storage유형: r.type,
용도: 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, OS: r.os, CPU: r.cpu, RAM: r.ram, GPU: r.gpu,
SSD1: r.storage1, SSD2: r.storage2, HDD1: r.storage3, 모니터링: r.monitoring, 비고: r.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/server/batch', async (req, res) => {
try {
const result = await batchSave('server_assets', req.body, (assets) => ({
sql: `INSERT INTO server_assets (id, corp, asset_code, purchase_date, type, purpose, details, current_org, prev_org, location, manager_main, manager_sub, ip_address, remote_tool, server_id, server_pw, model_name, os, cpu, ram, gpu, storage1, storage2, storage3, monitoring, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.storage유형||'물리', a.용도||'', a.상세||'', a.현사용조직||'', a.이전사용조직||'', a.위치||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'', a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'', a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 3. 스토리지 (Storage) API ---
app.get('/api/storage', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM storage_assets');
const mapped = rows.map(r => ({
id: r.id, type: '스토리지', 법인: r.corp, storage유형: r.type, 자산코드: r.asset_code, 명칭: r.asset_name,
위치: r.location, 모델명: r.model_name, 용량: r.capacity, 담당자_정: r.manager_main, 담당자_부: r.manager_sub,
IP주소: r.ip_address, MACaddress: r.mac_address, 구매일: r.purchase_date, 금액: r.price,
납품업체: r.vendor, 품의서명: r.doc_name, 비고: r.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/storage/batch', async (req, res) => {
try {
const result = await batchSave('storage_assets', req.body, (assets) => ({
sql: `INSERT INTO storage_assets (id, corp, type, asset_code, asset_name, location, model_name, capacity, manager_main, manager_sub, ip_address, mac_address, purchase_date, price, vendor, doc_name, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.storage유형||'', a.자산코드||'', a.명칭||'', a.위치||'', a.모델명||'', a.용량||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'', a.MACaddress||'', a.구매일||'', a.금액||'', a.납품업체||'', a.품의서명||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 4. 전산비품 (Equipment) API ---
app.get('/api/equip', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM equip_assets');
const mapped = rows.map(r => ({
id: r.id, type: '전산비품', 법인: r.corp, 비품유형: r.type, 자산코드: r.asset_code, 명칭: r.asset_name,
위치: r.location, 관리자: r.manager, IP주소: r.ip_address, MACaddress: r.mac_address,
HW사양: r.hw_spec, OS: r.os, 구매일: r.purchase_date, 금액: r.price,
납품업체: r.vendor, 품의서명: r.doc_name, 비고: r.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/equip/batch', async (req, res) => {
try {
const result = await batchSave('equip_assets', req.body, (assets) => ({
sql: `INSERT INTO equip_assets (id, corp, type, asset_code, asset_name, location, manager, ip_address, mac_address, hw_spec, os, purchase_date, price, vendor, doc_name, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.비품유형||'', a.자산코드||'', a.명칭||'', a.위치||'', a.관리자||'', a.IP주소||'', a.MACaddress||'', a.HW사양||'', a.OS||'', a.구매일||'', a.금액||'', a.납품업체||'', a.품의서명||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 5. 구독SW (Subscription) API ---
app.get('/api/sw/sub', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM subscription_sw');
const mapped = rows.map(r => ({
id: r.id, type: '구독SW', 분야: r.category, 법인: r.corp, 부서: r.dept, 제품명: r.product_name,
구매일: r.purchase_date, 구독일: r.subscription_date, 금액: r.price, 수량: r.quantity,
계정명: r.account_id, 납품업체: r.vendor, 비고: r.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw/sub/batch', async (req, res) => {
try {
const result = await batchSave('subscription_sw', req.body, (assets) => ({
sql: `INSERT INTO subscription_sw (id, category, corp, dept, product_name, purchase_date, subscription_date, price, quantity, account_id, vendor, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.분야||'', a.법인||'', a.부서||'', a.제품명||'', a.구매일||'', a.구독일||'', a.금액||'', a.수량||1, a.계정명||'', a.납품업체||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 6. 영구SW (Permanent) API ---
app.get('/api/sw/perm', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM permanent_sw');
const mapped = rows.map(r => ({
id: r.id, type: '영구SW', 분야: r.category, 법인: r.corp, 부서: r.dept, 제품명: r.product_name,
구매일: r.purchase_date, 유지보수여부: !!r.maintenance_status, 금액: r.price, 수량: r.quantity,
계정명: r.account_id, 납품업체: r.vendor, 비고: r.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw/perm/batch', async (req, res) => {
try {
const result = await batchSave('permanent_sw', req.body, (assets) => ({
sql: `INSERT INTO permanent_sw (id, category, corp, dept, product_name, purchase_date, maintenance_status, price, quantity, account_id, vendor, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.분야||'', a.법인||'', a.부서||'', a.제품명||'', a.구매일||'', a.유지보수여부?1:0, a.금액||'', a.수량||1, a.계정명||'', a.납품업체||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 7. SW 사용자 (SW Users) API ---
app.get('/api/sw-users', async (req, res) => { app.get('/api/sw-users', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM sw_users'); const [rows] = await pool.query('SELECT * FROM sw_users');
const mapped = rows.map(r => ({ const mapped = rows.map(r => ({
id: r.id, id: r.id, swId: r.sw_id, 법인: r.corp, 부서: r.dept, : r.team, 직위: r.position, 이름: r.name, 사용기간: r.usage_period, 신청서명: r.doc_name
swId: r.sw_id,
법인: r.corp,
부서: r.dept,
: r.team,
직위: r.position,
이름: r.name,
사용기간: r.usage_period,
신청서명: r.doc_name
})); }));
res.json(mapped); res.json(mapped);
} catch (err) { } catch (err) { res.status(500).json({ error: err.message }); }
res.status(500).json({ error: err.message });
}
}); });
// 6. SW 사용자 일괄 저장 (항상 덮어쓰기)
app.post('/api/sw-users/batch', async (req, res) => { app.post('/api/sw-users/batch', async (req, res) => {
const users = req.body;
const connection = await pool.getConnection();
try { try {
await connection.beginTransaction(); const result = await batchSave('sw_users', req.body, (users) => ({
sql: `INSERT INTO sw_users (id, sw_id, corp, dept, team, position, name, usage_period, doc_name) VALUES ?`,
await connection.query('DELETE FROM sw_users'); values: users.map(u => [u.id, u.swId, u.법인||'', u.부서||'', u.||'', u.직위||'', u.이름||'', u.사용기간||'', u.신청서명||''])
}));
if (users.length > 0) { res.json(result);
const sql = ` } catch (err) { res.status(500).json({ error: err.message }); }
INSERT INTO sw_users (
id, sw_id, corp, dept, team, position, name, usage_period, doc_name
) VALUES ?
`;
const values = users.map(u => [
u.id, u.swId, u.법인, u.부서, u., u.직위, u.이름, u.사용기간, u.신청서명
]);
await connection.query(sql, [values]);
}
await connection.commit();
res.json({ success: true, count: users.length, mode: 'overwrite' });
} catch (err) {
await connection.rollback();
res.status(500).json({ error: err.message });
} finally {
connection.release();
}
}); });
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`📡 ITAM API Server running on http://localhost:${PORT}`); console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`);
}); });

View File

@@ -204,6 +204,7 @@ function fillHwFormData(asset: HardwareAsset) {
(document.getElementById('hw-용도') as HTMLInputElement).value = asset. || ''; (document.getElementById('hw-용도') as HTMLInputElement).value = asset. || '';
(document.getElementById('hw-상세') as HTMLInputElement).value = asset. || ''; (document.getElementById('hw-상세') as HTMLInputElement).value = asset. || '';
(document.getElementById('hw-비고') as HTMLInputElement).value = asset. || ''; (document.getElementById('hw-비고') as HTMLInputElement).value = asset. || '';
(document.getElementById('hw-구매일') as HTMLInputElement).value = asset. || '';
(document.getElementById('hw-IP주소') as HTMLInputElement).value = asset.IP주소 || ''; (document.getElementById('hw-IP주소') as HTMLInputElement).value = asset.IP주소 || '';
(document.getElementById('hw-IP2') as HTMLInputElement).value = (asset as any).IP2 || ''; (document.getElementById('hw-IP2') as HTMLInputElement).value = (asset as any).IP2 || '';
(document.getElementById('hw-원격접속') as HTMLInputElement).value = asset. || ''; (document.getElementById('hw-원격접속') as HTMLInputElement).value = asset. || '';

View File

@@ -80,9 +80,14 @@ export interface HardwareLog {
user: string; user: string;
} }
// state.ts에서 정의된 구조와 일치해야 함
export interface MasterAssetData { export interface MasterAssetData {
hw: HardwareAsset[]; pc: HardwareAsset[];
sw: SoftwareAsset[]; server: HardwareAsset[];
storage: HardwareAsset[];
equip: HardwareAsset[];
subSw: SoftwareAsset[];
permSw: SoftwareAsset[];
swUsers: SWUser[]; swUsers: SWUser[];
logs: HardwareLog[]; logs: HardwareLog[];
} }
@@ -90,9 +95,8 @@ export interface MasterAssetData {
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품']; const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
const SW_TABS = ['구독SW', '영구SW']; const SW_TABS = ['구독SW', '영구SW'];
// 확장된 헤더 (상세 페이지의 모든 필드 포함)
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고']; const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고'];
const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '서버ID', '서버PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', '모니터링', '비고']; const SERVER_HEADERS = ['법인', '자산번호', '구매일자', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '서버ID', '서버PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', '모니터링', '비고'];
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고']; const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고'];
const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고']; const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
@@ -102,156 +106,61 @@ const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이
const HISTORY_HEADERS = ['id', 'assetId', 'date', 'details', 'user']; const HISTORY_HEADERS = ['id', 'assetId', 'date', 'details', 'user'];
/** /**
* 템플릿 엑셀 다중 시트로 다운로드 * 템플릿 엑셀 다운로드
*/ */
export function downloadTemplate() { export function downloadTemplate() {
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
HW_TABS.forEach(tab => { HW_TABS.forEach(tab => {
let hd = HW_HEADERS; let hd = HW_HEADERS;
let wscols: any[] = []; if (tab === '개인PC') hd = PC_HEADERS;
else if (tab === '서버') hd = SERVER_HEADERS;
if (tab === '개인PC') { else if (tab === '스토리지') hd = STORAGE_HEADERS;
hd = PC_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[1] = {wch: 25}; // 자산코드
wscols[12] = {wch: 30}; // HW사양
} else if (tab === '서버') {
hd = SERVER_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[3] = {wch: 25}; // 용도
wscols[4] = {wch: 30}; // 상세내용
} else if (tab === '스토리지') {
hd = STORAGE_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[2] = {wch: 25}; // 자산코드
wscols[3] = {wch: 25}; // 명칭
} else {
hd = HW_HEADERS;
wscols = Array(hd.length).fill({wch: 15});
wscols[2] = {wch: 25}; // 명칭
wscols[7] = {wch: 30}; // HW사양
}
const ws = XLSX.utils.aoa_to_sheet([hd]); const ws = XLSX.utils.aoa_to_sheet([hd]);
ws['!cols'] = wscols; ws['!cols'] = Array(hd.length).fill({wch: 15});
XLSX.utils.book_append_sheet(wb, ws, tab); XLSX.utils.book_append_sheet(wb, ws, tab);
}); });
SW_TABS.forEach(tab => { SW_TABS.forEach(tab => {
let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS; let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS;
const ws = XLSX.utils.aoa_to_sheet([hd]); const ws = XLSX.utils.aoa_to_sheet([hd]);
ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}]; ws['!cols'] = Array(hd.length).fill({wch: 15});
XLSX.utils.book_append_sheet(wb, ws, tab); XLSX.utils.book_append_sheet(wb, ws, tab);
}); });
const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS]); const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS]);
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}]; swUserWs['!cols'] = Array(SW_USER_HEADERS.length).fill({wch: 15});
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자'); XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
const historyWs = XLSX.utils.aoa_to_sheet([HISTORY_HEADERS]);
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
XLSX.writeFile(wb, 'itam_assets_template.xlsx'); XLSX.writeFile(wb, 'itam_assets_template.xlsx');
} }
/** /**
* 마스터 데이터를 여러 시트로 쪼개서 내보내기 * 분리된 배열들로부터 엑셀 추출
*/ */
export function exportToExcel(masterData: MasterAssetData) { export function exportToExcel(masterData: MasterAssetData) {
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
HW_TABS.forEach(tab => { const exportMap = [
const targetAssets = masterData.hw.filter(a => a.type === tab); { tab: '개인PC', list: masterData.pc, headers: PC_HEADERS, map: (a: any) => [a., a., a., a., a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.IP주소, a.HW사양, a., a., a., a., a.] },
let wsData; { tab: '서버', list: masterData.server, headers: SERVER_HEADERS, map: (a: any) => [a., a., a., a.storage유형 || '물리', a., a., a., a., a., a._정, a._부, a.IP주소, a., a.ID, a.PW, a., a.OS, a.CPU, a.RAM, a.GPU, a.SSD1, a.SSD2, a.HDD1, a., a.] },
let colsConfig; { tab: '스토리지', list: masterData.storage, headers: STORAGE_HEADERS, map: (a: any) => [a., a.storage유형, a., a., a., a., a., a._정, a._부, a.IP주소, a.MACaddress, a., a., a., a., a.] },
{ tab: '전산비품', list: masterData.equip, headers: HW_HEADERS, map: (a: any) => [a., a., a., a., a., a.IP주소, a.MACaddress, a.HW사양, a.OS, a., a., a., a., a.] },
{ tab: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a.id, a., a., a., a., a., a., a., a., a., a., a.] },
{ tab: '영구SW', list: masterData.permSw, headers: PERM_SW_HEADERS, map: (a: any) => [a.id, a., a., a., a., a., a. ? 'Y' : 'N', a., a., a., a., a.] },
];
if (tab === '개인PC') { exportMap.forEach(m => {
wsData = [ const ws = XLSX.utils.aoa_to_sheet([m.headers, ...m.list.map(m.map)]);
PC_HEADERS, ws['!cols'] = Array(m.headers.length).fill({wch: 15});
...targetAssets.map(a => [ XLSX.utils.book_append_sheet(wb, ws, m.tab);
a., a., a., a., a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.IP주소, a.HW사양, a., a., a., a., a.
])
];
colsConfig = Array(PC_HEADERS.length).fill({wch: 15});
colsConfig[1] = {wch: 25};
colsConfig[12] = {wch: 30};
} else if (tab === '서버') {
wsData = [
SERVER_HEADERS,
...targetAssets.map(a => [
a., a., a.storage유형 || '물리', a. || '', a. || '', a. || '', a. || '', a., a._정 || '', a._부 || '',
a.IP주소, a. || '', a.ID || '', a.PW || '', a. || '', a.OS, a.CPU, a.RAM, a.GPU || '', a.SSD1 || '', a.SSD2 || '', a.HDD1 || '', a. || '', a. || ''
])
];
colsConfig = Array(SERVER_HEADERS.length).fill({wch: 15});
colsConfig[3] = {wch: 25};
colsConfig[4] = {wch: 30};
} else if (tab === '스토리지') {
wsData = [
STORAGE_HEADERS,
...targetAssets.map(a => [
a., a.storage유형, a., a., a., a., a., a._정, a._부, a.IP주소, a.MACaddress, a., a., a., a., a.
])
];
colsConfig = Array(STORAGE_HEADERS.length).fill({wch: 15});
colsConfig[2] = {wch: 25};
colsConfig[3] = {wch: 25};
} else {
wsData = [
HW_HEADERS,
...targetAssets.map(a => [
a., a., a., a., a., a.IP주소, a.MACaddress, a.HW사양, a.OS, a., a., a., a., a.
])
];
colsConfig = Array(HW_HEADERS.length).fill({wch: 15});
colsConfig[2] = {wch: 25};
colsConfig[7] = {wch: 30};
}
const ws = XLSX.utils.aoa_to_sheet(wsData);
ws['!cols'] = colsConfig;
XLSX.utils.book_append_sheet(wb, ws, tab);
}); });
SW_TABS.forEach(tab => { const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS, ...masterData.swUsers.map(u => [u.id, u.swId, u., u., u., u., u., u., u.])]);
const targetAssets = masterData.sw.filter(a => a.type === tab);
let wsData;
if (tab === '구독SW') {
wsData = [
SUB_SW_HEADERS,
...targetAssets.map(a => [a.id, a.||'', a., a.||'', a., a., a., a., a., a., a., a.])
];
} else {
wsData = [
PERM_SW_HEADERS,
...targetAssets.map(a => [a.id, a.||'', a., a.||'', a., a., a. ? 'Y' : 'N', a., a., a., a., a.])
];
}
const ws = XLSX.utils.aoa_to_sheet(wsData);
ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}];
XLSX.utils.book_append_sheet(wb, ws, tab);
});
const swUserWsData = [
SW_USER_HEADERS,
...masterData.swUsers.map(u => [u.id, u.swId, u., u., u., u., u., u., u.])
];
const swUserWs = XLSX.utils.aoa_to_sheet(swUserWsData);
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자'); XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
const historyWsData = [ XLSX.writeFile(wb, `itam_assets_master_${new Date().toISOString().split('T')[0]}.xlsx`);
HISTORY_HEADERS,
...masterData.logs.map(l => [l.id, l.assetId, l.date, l.details, l.user])
];
const historyWs = XLSX.utils.aoa_to_sheet(historyWsData);
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
const dateStr = new Date().toISOString().split('T')[0];
XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`);
} }
export async function parseExcel(file: File): Promise<MasterAssetData> { export async function parseExcel(file: File): Promise<MasterAssetData> {
@@ -259,132 +168,30 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
try { try {
const data = e.target?.result; const workbook = XLSX.read(e.target?.result, { type: 'binary' });
const workbook = XLSX.read(data, { type: 'binary' }); const data: MasterAssetData = { pc: [], server: [], storage: [], equip: [], subSw: [], permSw: [], swUsers: [], logs: [] };
const hwAssets: HardwareAsset[] = [];
const swAssets: SoftwareAsset[] = [];
const swUsers: SWUser[] = [];
const logs: HardwareLog[] = [];
workbook.SheetNames.forEach(sheetName => { workbook.SheetNames.forEach(sheetName => {
const worksheet = workbook.Sheets[sheetName]; const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]) as any[];
const json = XLSX.utils.sheet_to_json(worksheet) as any[];
if (HW_TABS.includes(sheetName)) {
json.forEach(row => {
if (sheetName === '개인PC') { if (sheetName === '개인PC') {
hwAssets.push({ rows.forEach(r => data.pc.push({ id: Math.random().toString(36).substring(2, 9), type: '개인PC', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 사용자: r['사용자']||'', 위치: r['위치']||'', CPU: r['CPU']||'', GPU: r['GPU']||'', RAM: r['RAM']||'', SSD1: r['SSD1']||'', SSD2: r['SSD2']||'', HDD1: r['HDD1']||'', HDD2: r['HDD2']||'', IP주소: r['IP주소']||'', HW사양: r['HW사양']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }));
id: Math.random().toString(36).substring(2, 9),
type: sheetName,
법인: row['법인'] || '',
자산코드: row['자산코드'] || '',
: '',
위치: row['위치'] || '',
사용자: row['사용자'] || '',
: '',
IP주소: row['IP주소'] || '',
MACaddress: '',
HW사양: row['HW사양'] || '',
OS: row['OS'] || '',
CPU: row['CPU'] || '', GPU: row['GPU'] || '', RAM: row['RAM'] || '',
SSD1: row['SSD1'] || '', SSD2: row['SSD2'] || '', HDD1: row['HDD1'] || '', HDD2: row['HDD2'] || '',
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
비고: row['비고'] || ''
});
} else if (sheetName === '서버') { } else if (sheetName === '서버') {
hwAssets.push({ rows.forEach(r => data.server.push({ id: Math.random().toString(36).substring(2, 9), type: '서버', 법인: r['법인']||r['구매법인']||'', 자산코드: r['자산번호']||r['자산코드']||'', 구매일: r['구매일자']||r['구매일']||'', storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||r['현 사용조직']||'', 이전사용조직: r['이전사용조직']||r['이전 사용조직']||'', 위치: r['설치위치']||r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소']||r['IP주소']||r['IP 주소 1']||'', 원격접속: r['원격접속']||r['원격도구']||'', 서버ID: r['서버ID']||r['서버 ID']||'', 서버PW: r['서버PW']||r['서버 PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'', SSD1: r['SSD1']||r['Storage1']||'', SSD2: r['SSD2']||r['Storage2']||'', HDD1: r['HDD1']||r['Storage3']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'', MACaddress: '', HW사양: '', : '', : '', : '' }));
id: Math.random().toString(36).substring(2, 9),
type: sheetName,
법인: row['법인'] || '',
자산코드: row['자산번호'] || row['자산코드'] || '',
명칭: row['용도'] || row['명칭'] || '',
용도: row['용도'] || '',
상세: row['상세내용'] || row['상세'] || '',
현사용조직: row['현사용조직'] || '',
이전사용조직: row['이전사용조직'] || '',
위치: row['설치위치'] || row['위치'] || '',
관리자: row['담당자(정)'] || '', 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '',
IP주소: row['IP 주소'] || row['IP주소'] || '',
원격접속: row['원격접속'] || '',
서버ID: row['서버ID'] || '',
서버PW: row['서버PW'] || '',
모델명: row['모델명'] || '', OS: row['OS'] || '',
CPU: row['CPU'] || '', RAM: row['RAM'] || '', GPU: row['GPU'] || '',
SSD1: row['SSD1'] || row['Storage1'] || '',
SSD2: row['SSD2'] || row['Storage2'] || '',
HDD1: row['HDD1'] || row['Storage3'] || '',
모니터링: row['모니터링'] || '',
비고: row['비고'] || '',
storage유형: row['유형'] || '물리',
MACaddress: '', HW사양: '', : '', : '', : '', : '',
});
} else if (sheetName === '스토리지') { } else if (sheetName === '스토리지') {
hwAssets.push({ rows.forEach(r => data.storage.push({ id: Math.random().toString(36).substring(2, 9), type: '스토리지', 법인: r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'', HW사양: '', OS: '', : '' }));
id: Math.random().toString(36).substring(2, 9), } else if (sheetName === '전산비품') {
type: sheetName, rows.forEach(r => data.equip.push({ id: Math.random().toString(36).substring(2, 9), type: '전산비품', 법인: r['법인']||'', 비품유형: r['명칭']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }));
법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '', } else if (sheetName === '구독SW') {
: '', IP주소: row['IP주소'] || '', MACaddress: row['MAC주소'] || '', HW사양: '', OS: '', rows.forEach(r => data.subSw.push({ id: r['ID']||Math.random().toString(36).substring(2, 9), type: '구독SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 구독일: r['구독일']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }));
storage유형: row['유형'] || '', 모델명: row['모델명'] || '', 용량: row['용량'] || '', } else if (sheetName === '영구SW') {
담당자_정: row['담당자(정)'] || '', 담당자_: row['담당자(부)'] || '', rows.forEach(r => data.permSw.push({ id: r['ID']||Math.random().toString(36).substring(2, 9), type: '영구SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 유지보수여: r['유지보수여부']==='Y', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }));
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '', } else if (sheetName === 'SW_사용자') {
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '', rows.forEach(r => data.swUsers.push({ id: r['id']||Math.random().toString(36).substring(2, 9), swId: r['swId']||'', 법인: r['법인']||'', 부서: r['부서']||'', : r['팀']||'', 직위: r['직위']||'', 이름: r['이름']||'', 사용기간: r['사용기간']||'', 신청서명: r['신청서명']||'' }));
비고: row['비고'] || ''
});
} else {
hwAssets.push({
id: Math.random().toString(36).substring(2, 9),
type: sheetName,
법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '',
관리자: row['관리자'] || '', IP주소: row['IP주소'] || '', MACaddress: row['MACaddress'] || '',
HW사양: row['HW사양'] || '', OS: row['OS'] || '',
구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '',
납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '',
비고: row['비고'] || ''
});
} }
}); });
} resolve(data);
} catch (err) { reject(err); }
if (SW_TABS.includes(sheetName)) {
json.forEach(row => {
swAssets.push({
id: row['ID'] ? String(row['ID']) : Math.random().toString(36).substring(2, 9),
type: sheetName, 분야: row['분야'] || '', 법인: row['법인'] || '', 부서: row['부서'] || '', 제품명: row['제품명'] || '',
구매일: row['구매일'] || '', 구독일: row['구독일'] || '', 유지보수여부: row['유지보수여부'] === 'Y' || row['유지보수여부'] === true,
금액: row['금액'] ? String(row['금액']) : '', 수량: parseInt(row['수량'] || '1', 10),
계정명: row['계정명'] || '', 납품업체: row['납품업체'] || '', 비고: row['비고'] || '',
});
});
}
if (sheetName === 'SW_사용자') {
json.forEach(row => {
swUsers.push({
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
swId: row['swId'] ? String(row['swId']) : '', 법인: row['법인'] || '', 부서: row['부서'] || '',
: row['팀'] || '', 직위: row['직위'] || '', 이름: row['이름'] || '',
사용기간: row['사용기간'] || '', 신청서명: row['신청서명'] || '',
});
});
}
if (sheetName === 'History') {
json.forEach(row => {
logs.push({
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
assetId: row['assetId'] ? String(row['assetId']) : '',
date: row['date'] || '', details: row['details'] || '', user: row['user'] || '',
});
});
}
});
resolve({ hw: hwAssets, sw: swAssets, swUsers, logs });
} catch (err) {
reject(err);
}
}; };
reader.onerror = (err) => reject(err);
reader.readAsBinaryString(file); reader.readAsBinaryString(file);
}); });
} }

View File

@@ -1,96 +1,64 @@
import { MasterAssetData, HardwareAsset } from './excelHandler'; import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog } from './excelHandler';
import { generateDummyData } from './dummyDataGenerator';
import { realServerData } from './realServerData';
// --- State Definitions --- // --- State Definitions ---
export interface AppState { export interface MasterAssetData {
masterData: MasterAssetData; pc: HardwareAsset[];
activeCategory: 'hw' | 'sw' | 'ops'; server: HardwareAsset[];
activeSubTab: string; storage: HardwareAsset[];
activeCharts: any[]; equip: HardwareAsset[];
subSw: SoftwareAsset[];
permSw: SoftwareAsset[];
swUsers: SWUser[];
logs: HardwareLog[];
} }
const dummy = generateDummyData(); export interface AppState {
// 서버 데이터만 실제 데이터로 교체 activeCategory: 'dashboard' | 'hw' | 'sw';
const mergedHw: HardwareAsset[] = [ activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW'
...dummy.hw.filter(a => a.type !== '서버'), masterData: MasterAssetData;
...realServerData.map((serverData: any) => { }
const s = serverData;
return {
id: s.id || Math.random().toString(36).substring(2, 9),
type: '서버',
법인: s.법인,
자산코드: s.자산코드,
명칭: s.용도 || '',
위치: s.위치,
관리자: s.담당자_정 || '홍길동',
담당자_정: s.담당자_정 || '홍길동',
담당자_부: s.담당자_부 || '김철수',
IP주소: s.IP주소,
IP2: s.IP2 || '',
MACaddress: s.MACaddress || '',
HW사양: s.HW사양 || '',
OS: s.OS,
CPU: s.CPU,
RAM: s.RAM,
SSD1: s.SSD1,
SSD2: s.SSD2,
HDD1: s.HDD1,
storage유형: s.storage유형,
모델명: s.모델명,
구매일: s.구매일 || '',
금액: s.금액 || '',
납품업체: s.납품업체 || '',
품의서명: s.품의서명 || '',
용도: s.용도,
상세: s.상세,
원격접속: s.원격접속 || '',
서버ID: s.서버ID || '',
서버PW: s.서버PW || '',
모니터링: s.모니터링 || '',
비고: s.비고 || ''
}})
];
// --- Initial State --- // 초기 상태
export const state: AppState = { export const state: AppState = {
masterData: { activeCategory: 'dashboard',
...dummy,
hw: mergedHw, // 기본적으로 하드코딩된 데이터를 가지고 시작
logs: []
},
activeCategory: 'hw',
activeSubTab: '대시보드', activeSubTab: '대시보드',
activeCharts: [] masterData: {
pc: [],
server: [],
storage: [],
equip: [],
subSw: [],
permSw: [],
swUsers: [],
logs: []
}
}; };
/** /**
* DB에서 데이터 로드 * 전용 API 엔드포인트들로부터 데이터 로드
*/ */
export async function loadMasterDataFromDB() { export async function loadMasterDataFromDB() {
try { try {
const [hwRes, swRes, swUserRes] = await Promise.all([ const endpoints = [
fetch('http://localhost:3000/api/hw'), { key: 'pc', url: 'http://localhost:3000/api/pc' },
fetch('http://localhost:3000/api/sw'), { key: 'server', url: 'http://localhost:3000/api/server' },
fetch('http://localhost:3000/api/sw-users') { key: 'storage', url: 'http://localhost:3000/api/storage' },
]); { key: 'equip', url: 'http://localhost:3000/api/equip' },
{ key: 'subSw', url: 'http://localhost:3000/api/sw/sub' },
{ key: 'permSw', url: 'http://localhost:3000/api/sw/perm' },
{ key: 'swUsers', url: 'http://localhost:3000/api/sw-users' }
];
if (hwRes.ok) { const results = await Promise.all(endpoints.map(e => fetch(e.url)));
const hwData = await hwRes.json();
if (hwData && hwData.length > 0) state.masterData.hw = hwData; for (let i = 0; i < endpoints.length; i++) {
if (results[i].ok) {
const data = await results[i].json();
(state.masterData as any)[endpoints[i].key] = data || [];
}
} }
if (swRes.ok) { console.log('✅ 6개 테이블 데이터 로드 완료');
const swData = await swRes.json();
if (swData && swData.length > 0) state.masterData.sw = swData;
}
if (swUserRes.ok) {
const swUserData = await swUserRes.json();
if (swUserData && swUserData.length > 0) state.masterData.swUsers = swUserData;
}
console.log('✅ DB 데이터 로드 완료');
return true; return true;
} catch (err) { } catch (err) {
console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.'); console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.');

View File

@@ -12,101 +12,64 @@ import { initSwUserModal } from './components/Modal/SWUserModal';
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide'; import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide';
// --- DB 저장을 위한 헬퍼 함수 --- // --- DB 저장을 위한 세분화된 헬퍼 함수 ---
async function saveAllHwToDB(assets: HardwareAsset[]) { async function apiBatchSave(url: string, data: any[], label: string) {
try { try {
const response = await fetch('http://localhost:3000/api/hw/batch', { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(assets) body: JSON.stringify(data)
}); });
if (!response.ok) throw new Error('HW DB 저장 실패'); if (!response.ok) throw new Error(`${label} DB 저장 실패`);
console.log('✅ HW DB 저장 완료'); console.log(`${label} DB 저장 완료`);
} catch (err) { } catch (err) {
console.error('❌ HW DB 저장 실패:', err); console.error(`${label} DB 저장 오류:`, err);
} }
} }
async function saveAllSwToDB(assets: SoftwareAsset[]) { const savePcToDB = () => apiBatchSave('http://localhost:3000/api/pc/batch', state.masterData.pc, '개인PC');
try { const saveServerToDB = () => apiBatchSave('http://localhost:3000/api/server/batch', state.masterData.server, '서버');
const response = await fetch('http://localhost:3000/api/sw/batch', { const saveStorageToDB = () => apiBatchSave('http://localhost:3000/api/storage/batch', state.masterData.storage, '스토리지');
method: 'POST', const saveEquipToDB = () => apiBatchSave('http://localhost:3000/api/equip/batch', state.masterData.equip, '전산비품');
headers: { 'Content-Type': 'application/json' }, const saveSubSwToDB = () => apiBatchSave('http://localhost:3000/api/sw/sub/batch', state.masterData.subSw, '구독SW');
body: JSON.stringify(assets) const savePermSwToDB = () => apiBatchSave('http://localhost:3000/api/sw/perm/batch', state.masterData.permSw, '영구SW');
}); const saveSwUsersToDB = () => apiBatchSave('http://localhost:3000/api/sw-users/batch', state.masterData.swUsers, 'SW사용자');
if (!response.ok) throw new Error('SW DB 저장 실패');
console.log('✅ SW DB 저장 완료');
} catch (err) {
console.error('❌ SW DB 저장 실패:', err);
}
}
async function saveAllSwUsersToDB(users: SWUser[]) {
try {
const response = await fetch('http://localhost:3000/api/sw-users/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(users)
});
if (!response.ok) throw new Error('SW User DB 저장 실패');
console.log('✅ SW User DB 저장 완료');
} catch (err) {
console.error('❌ SW User DB 저장 실패:', err);
}
}
// --- App Initialization --- // --- App Initialization ---
function initApp() { function initApp() {
console.log('🚀 ITAM System Initializing...'); console.log('🚀 ITAM Dedicated System Initializing...');
const mainContent = document.getElementById('main-content')!; const mainContent = document.getElementById('main-content')!;
if (!mainContent) return; if (!mainContent) return;
// 1. 전역 모달 및 내비게이션 초기화
const { closeAllModals } = initBaseModal(); const { closeAllModals } = initBaseModal();
try { try {
renderNavigation((tab) => { renderNavigation((tab) => {
if (tab === '대시보드') { if (tab === '대시보드') renderDashboard(mainContent);
renderDashboard(mainContent); else renderTable(mainContent);
} else {
renderTable(mainContent);
}
}); });
initPcModal(() => { // 각 모달 초기화 시 해당 카테고리의 저장 API만 호출하도록 콜백 연결
saveAllHwToDB(state.masterData.hw); initPcModal(() => { savePcToDB(); renderTable(mainContent); }, closeAllModals);
renderTable(mainContent);
}, closeAllModals);
initHwModal(() => { initHwModal(() => {
saveAllHwToDB(state.masterData.hw); if (state.activeSubTab === '서버') saveServerToDB();
else if (state.activeSubTab === '스토리지') saveStorageToDB();
else if (state.activeSubTab === '전산비품') saveEquipToDB();
renderTable(mainContent); renderTable(mainContent);
}, closeAllModals); }, closeAllModals);
initStorageModal(() => { saveStorageToDB(); renderTable(mainContent); }, closeAllModals);
initStorageModal(() => {
saveAllHwToDB(state.masterData.hw);
renderTable(mainContent);
}, closeAllModals);
initSwModal(() => { initSwModal(() => {
saveAllSwToDB(state.masterData.sw); if (state.activeSubTab === '구독SW') saveSubSwToDB();
renderTable(mainContent); else savePermSwToDB();
}, closeAllModals);
initSwUserModal(() => {
saveAllSwUsersToDB(state.masterData.swUsers);
renderTable(mainContent); renderTable(mainContent);
}, closeAllModals); }, closeAllModals);
initSwUserModal(() => { saveSwUsersToDB(); renderTable(mainContent); }, closeAllModals);
initDashboardDetailModal(); initDashboardDetailModal();
} catch (e) { } catch (e) { console.error('❌ Initialization failed:', e); }
console.error('❌ Initialization failed:', e);
}
// 2. 초기 렌더링
renderDashboard(mainContent); renderDashboard(mainContent);
// 3. 비동기 데이터 로드
loadMasterDataFromDB().then((success) => { loadMasterDataFromDB().then((success) => {
if (success) { if (success) {
if (state.activeSubTab === '대시보드') renderDashboard(mainContent); if (state.activeSubTab === '대시보드') renderDashboard(mainContent);
@@ -114,7 +77,6 @@ function initApp() {
} }
}); });
// 4. 이벤트 바인딩
document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate()); document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate());
document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData)); document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData));
@@ -124,11 +86,10 @@ function initApp() {
if (file) { if (file) {
const data = await parseExcel(file); const data = await parseExcel(file);
state.masterData = data; state.masterData = data;
// 엑셀 업로드 시 모든 카테고리 일괄 덮어쓰기 저장 // 엑셀 업로드 시 모든 카테고리 병렬 덮어쓰기
await Promise.all([ await Promise.all([
saveAllHwToDB(data.hw), savePcToDB(), saveServerToDB(), saveStorageToDB(), saveEquipToDB(),
saveAllSwToDB(data.sw), saveSubSwToDB(), savePermSwToDB(), saveSwUsersToDB()
saveAllSwUsersToDB(data.swUsers)
]); ]);
renderTable(mainContent); renderTable(mainContent);
} }

View File

@@ -4,7 +4,7 @@ import { formatInline } from '../../core/utils';
import { createIcons, RefreshCcw } from 'lucide'; import { createIcons, RefreshCcw } from 'lucide';
export function renderEquipmentList(container: HTMLElement) { export function renderEquipmentList(container: HTMLElement) {
const fullList = state.masterData.hw.filter(a => a.type === '전산비품'); const fullList = state.masterData.equip;
const filterBar = document.createElement('div'); const filterBar = document.createElement('div');
filterBar.className = 'search-bar'; filterBar.className = 'search-bar';

View File

@@ -4,7 +4,7 @@ import { formatInline } from '../../core/utils';
import { createIcons, Paperclip, RefreshCcw } from 'lucide'; import { createIcons, Paperclip, RefreshCcw } from 'lucide';
export function renderPcList(container: HTMLElement) { export function renderPcList(container: HTMLElement) {
const fullList = state.masterData.hw.filter(a => a.type === '개인PC'); const fullList = state.masterData.pc;
const filterBar = document.createElement('div'); const filterBar = document.createElement('div');
filterBar.className = 'search-bar'; filterBar.className = 'search-bar';

View File

@@ -4,7 +4,7 @@ import { formatInline, createBadge } from '../../core/utils';
import { createIcons, RefreshCcw } from 'lucide'; import { createIcons, RefreshCcw } from 'lucide';
export function renderServerList(container: HTMLElement) { export function renderServerList(container: HTMLElement) {
const fullList = state.masterData.hw.filter(a => a.type === '서버'); const fullList = state.masterData.server;
const filterBar = document.createElement('div'); const filterBar = document.createElement('div');
filterBar.className = 'search-bar'; filterBar.className = 'search-bar';
@@ -17,7 +17,7 @@ export function renderServerList(container: HTMLElement) {
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off"> <input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div> </div>
<div class="search-item"> <div class="search-item">
<label>법인</label> <label>구매법인</label>
<select id="filter-corp"><option value="">전체 법인</option>${corps.map(c => `<option value="${c}">${c}</option>`).join('')}</select> <select id="filter-corp"><option value="">전체 법인</option>${corps.map(c => `<option value="${c}">${c}</option>`).join('')}</select>
</div> </div>
<div class="search-item"> <div class="search-item">
@@ -33,7 +33,7 @@ export function renderServerList(container: HTMLElement) {
const tableWrapper = document.createElement('div'); const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container'; tableWrapper.className = 'table-container';
const table = document.createElement('table'); const table = document.createElement('table');
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>현 사용조직</th><th>자산번호</th><th>용도</th><th>상세</th><th>설치위치</th><th>담당자</th><th>IP주소</th><th>모델명</th><th>OS</th><th>CPU/RAM</th><th>Storage</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`; table.innerHTML = `<thead><tr><th>No</th><th>구매법인</th><th>현 사용조직</th><th>자산번호</th><th>용도</th><th>상세</th><th>설치위치</th><th>담당자</th><th>IP주소</th><th>모델명</th><th>OS</th><th>CPU/RAM</th><th>Storage</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
tableWrapper.appendChild(table); tableWrapper.appendChild(table);
container.appendChild(tableWrapper); container.appendChild(tableWrapper);

View File

@@ -4,7 +4,7 @@ import { formatInline } from '../../core/utils';
import { createIcons, RefreshCcw } from 'lucide'; import { createIcons, RefreshCcw } from 'lucide';
export function renderStorageList(container: HTMLElement) { export function renderStorageList(container: HTMLElement) {
const fullList = state.masterData.hw.filter(a => a.type === '스토리지'); const fullList = state.masterData.storage;
const filterBar = document.createElement('div'); const filterBar = document.createElement('div');
filterBar.className = 'search-bar'; filterBar.className = 'search-bar';

View File

@@ -4,8 +4,8 @@ import { openSwUserModal } from '../../components/Modal/SWUserModal';
import { createIcons, Edit2, Users, RefreshCcw } from 'lucide'; import { createIcons, Edit2, Users, RefreshCcw } from 'lucide';
export function renderSwList(container: HTMLElement) { export function renderSwList(container: HTMLElement) {
const fullList = state.masterData.sw.filter(a => a.type === state.activeSubTab);
const isSub = state.activeSubTab === '구독SW'; const isSub = state.activeSubTab === '구독SW';
const fullList = isSub ? state.masterData.subSw : state.masterData.permSw;
const filterBar = document.createElement('div'); const filterBar = document.createElement('div');
filterBar.className = 'search-bar'; filterBar.className = 'search-bar';