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

View File

@@ -37,6 +37,7 @@ async function initDB() {
details TEXT, details TEXT,
current_org VARCHAR(255), current_org VARCHAR(255),
prev_org VARCHAR(255), prev_org VARCHAR(255),
user_name VARCHAR(100) COMMENT '사용자',
location VARCHAR(255), location VARCHAR(255),
manager_main VARCHAR(100), manager_main VARCHAR(100),
manager_sub VARCHAR(100), manager_sub VARCHAR(100),

962
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"cors": "^2.8.6", "cors": "^2.8.6",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"exceljs": "^4.4.0",
"express": "^5.2.1", "express": "^5.2.1",
"lucide": "^0.364.0", "lucide": "^0.364.0",
"mysql2": "^3.22.1", "mysql2": "^3.22.1",

288
server.js
View File

@@ -26,78 +26,75 @@ const pool = mysql.createPool({
async function ensureTables() { async function ensureTables() {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
try { try {
await connection.query(` // 1. 기본 테이블 생성
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(` await connection.query(`
CREATE TABLE IF NOT EXISTS pc_assets ( CREATE TABLE IF NOT EXISTS pc_assets (
id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), purchase_date VARCHAR(50), 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, type VARCHAR(50), detail_purpose VARCHAR(100), purpose VARCHAR(255), details TEXT,
current_org VARCHAR(100), prev_org VARCHAR(100), location VARCHAR(255), 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(50), manager_main VARCHAR(100), manager_sub VARCHAR(100), ip_address VARCHAR(100),
remote_tool VARCHAR(100), server_id VARCHAR(100), server_pw 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), model_name VARCHAR(255), mainboard VARCHAR(255), os VARCHAR(100), cpu VARCHAR(255), ram VARCHAR(100), gpu VARCHAR(100),
storage1 VARCHAR(100), storage2 VARCHAR(100), storage3 VARCHAR(100), monitoring VARCHAR(100), price VARCHAR(100), remarks TEXT, storage1 VARCHAR(255), storage2 VARCHAR(255), storage3 VARCHAR(255), monitoring VARCHAR(100), price VARCHAR(100), remarks TEXT,
storage_location VARCHAR(255), status VARCHAR(50) storage_location VARCHAR(255), status VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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']) { 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 ${table} LIKE pc_assets`);
} }
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS sw_sub_assets ( CREATE TABLE IF NOT EXISTS cloud_assets (
id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), id VARCHAR(50) PRIMARY KEY, platform_name VARCHAR(100), corp VARCHAR(100), dept VARCHAR(100), product_name VARCHAR(255),
category 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),
license_type VARCHAR(100), quantity INT, price VARCHAR(100), purchase_date VARCHAR(50), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`); `);
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS asset_logs ( CREATE TABLE IF NOT EXISTS asset_logs (
id INT AUTO_INCREMENT PRIMARY KEY, asset_id VARCHAR(50), log_date VARCHAR(50), 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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`); `);
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS sw_users ( CREATE TABLE IF NOT EXISTS sw_users (
id INT AUTO_INCREMENT PRIMARY KEY, sw_id VARCHAR(50), corp VARCHAR(100), dept VARCHAR(100), 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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`); `);
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS ops_domain_assets ( CREATE TABLE IF NOT EXISTS ops_domain_assets (
id VARCHAR(50) PRIMARY KEY, type VARCHAR(50), corp VARCHAR(100), id VARCHAR(50) PRIMARY KEY, type VARCHAR(50), corp VARCHAR(100), service_name VARCHAR(255), domain_name VARCHAR(255),
service_name VARCHAR(255), domain_name VARCHAR(255), start_date VARCHAR(50), start_date VARCHAR(50), expiry_date VARCHAR(50), price VARCHAR(100), manager_main VARCHAR(100),
expiry_date VARCHAR(50), price VARCHAR(100), manager_main VARCHAR(100),
manager_sub VARCHAR(100), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP manager_sub VARCHAR(100), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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) { async function batchSave(tableName, assets, getQuery) {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
@@ -128,76 +158,13 @@ async function batchSave(tableName, assets, getQuery) {
} }
} }
// 하드웨어 쿼리 헬퍼 // --- API 라우트 ---
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 ?
`;
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) => { app.get('/api/pc', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM pc_assets'); 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'))); res.json(rows.map(r => mapHardware(r, '개인PC')));
} catch (err) { } catch (err) { res.status(500).json({ error: err.message }); }
console.error('❌ DB Query Error (PC):', err.message);
res.status(500).json({ error: err.message });
}
}); });
app.post('/api/pc/batch', async (req, res) => { 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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 서버 API
app.get('/api/server', async (req, res) => { app.get('/api/server', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM server_assets'); 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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 스토리지 API
app.get('/api/storage', async (req, res) => { app.get('/api/storage', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM storage_assets'); 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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 전산비품 API
app.get('/api/equip', async (req, res) => { app.get('/api/equip', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM equip_assets'); 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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 모바일 API
app.get('/api/mobile', async (req, res) => { app.get('/api/mobile', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM mobile_assets'); 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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 구독 SW API
app.get('/api/sw/sub', async (req, res) => { app.get('/api/sw/sub', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM sw_sub_assets'); const [rows] = await pool.query('SELECT * FROM sw_sub_assets');
res.json(rows.map(r => ({ 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 })));
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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
@@ -300,53 +256,33 @@ app.post('/api/sw/sub/batch', async (req, res) => {
try { try {
const result = await batchSave('sw_sub_assets', req.body, (assets) => ({ 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 ?`, 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 => [ values: assets.map(a => [a.id, a.법인||'', a.분야||'', a.부서||'', a.제품명||'', a.라이선스유형||'', a.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||''])
a.id, a.법인||'', a.분야||'', a.부서||'', a.제품명||'',
a.라이선스유형||'', a.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||''
])
})); }));
res.json(result); res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 영구 SW API
app.get('/api/sw/perm', async (req, res) => { app.get('/api/sw/perm', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM sw_perm_assets'); const [rows] = await pool.query('SELECT * FROM sw_perm_assets');
res.json(rows.map(r => ({ 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 })));
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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
app.post('/api/sw/perm/batch', async (req, res) => { app.post('/api/sw/perm/batch', async (req, res) => {
try { 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) => ({ 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 ?`, 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 => [ values: assets.map(a => [a.id, a.법인||'', a.분야||'', a.부서||'', a.제품명||'', a.라이선스키||'', a.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||''])
a.id, a.법인||'', a.분야||'', a.부서||'', a.제품명||'',
a.라이선스키||'', a.수량||0, a.금액||'', a.구매일||'', a.시작일||'', a.만료일||'', a.납품업체||'', a.비고||''
])
})); }));
res.json(result); res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 클라우드 API
app.get('/api/cloud', async (req, res) => { app.get('/api/cloud', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM cloud_assets'); const [rows] = await pool.query('SELECT * FROM cloud_assets');
res.json(rows.map(r => ({ 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 })));
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 }); } } 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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 로그 API
app.get('/api/logs', async (req, res) => { app.get('/api/logs', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM asset_logs ORDER BY log_date DESC'); const [rows] = await pool.query('SELECT * FROM asset_logs ORDER BY log_date DESC');
res.json(rows.map(r => ({ 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 })));
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 }); } } 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) => { 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');
@@ -400,20 +322,13 @@ app.post('/api/sw-users/batch', async (req, res) => {
await connection.query('DELETE FROM sw_users'); await connection.query('DELETE FROM sw_users');
const allUsers = req.body; const allUsers = req.body;
if (allUsers.length > 0) { if (allUsers.length > 0) {
const values = allUsers.flatMap(item => const values = allUsers.flatMap(item => (item.userData || []).map(u => [item.sw_id, u[0], u[1], u[2], u[3], u[4], u[5]]));
(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]);
);
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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 도메인 관리 API
app.get('/api/ops/domain', async (req, res) => { app.get('/api/ops/domain', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM ops_domain_assets ORDER BY created_at DESC'); 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 }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 자산번호 자동 생성 API
app.get('/api/generate-asset-code', async (req, res) => { app.get('/api/generate-asset-code', async (req, res) => {
const { prefix } = req.query; const { prefix } = req.query; if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
try { try {
const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']; const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets'];
let maxNum = 0; let maxNum = 0;
for (const table of tables) { for (const table of tables) {
const [rows] = await pool.query( const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, [`${prefix}%`]);
`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, rows.forEach(r => { const numPart = r.asset_code.replace(prefix, ''); const num = parseInt(numPart); if (!isNaN(num) && num > maxNum) maxNum = num; });
[`${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'); const nextNum = (maxNum + 1).toString().padStart(4, '0');
res.json({ nextCode: `${prefix}${nextNum}` }); res.json({ nextCode: `${prefix}${nextNum}` });
} catch (err) { } catch (err) { res.status(500).json({ error: err.message }); }
res.status(500).json({ error: err.message });
}
}); });
// 초기화 및 서버 기동
ensureTables().then(() => { ensureTables().then(() => {
app.listen(PORT, () => { app.listen(PORT, () => { console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`); });
console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`); }).catch(err => { console.error('❌ Failed to start server:', err); });
});
}).catch(err => {
console.error('❌ Failed to start server:', err);
});

View File

@@ -1,38 +1,36 @@
import { openModal, closeModals } from './BaseModal'; import { openModal, closeModals } from './BaseModal';
import { createIcons, X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } from 'lucide'; import { createIcons, X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } from 'lucide';
import { state, loadMasterDataFromDB } from '../../core/state'; import { state, loadMasterDataFromDB } from '../../core/state';
import { TYPE_PREFIX_MAP } from './SharedData'; import { TYPE_PREFIX_MAP, LOCATION_DATA } from './SharedData';
import { ASSET_SCHEMA } from '../../core/schema';
let parsedData: any = null; let parsedData: any = null;
let currentTab: string = ''; let currentTab: string = '';
let onSuccessCallback: (() => void) | null = null; let onSuccessCallback: (() => void) | null = null;
let hasAttemptedBulkGenerate: boolean = false;
const UPLOAD_PREVIEW_MODAL_HTML = ` const UPLOAD_PREVIEW_MODAL_HTML = `
<div id="upload-preview-modal" class="modal-overlay hidden"> <div id="upload-preview-modal" class="modal-overlay hidden">
<div class="modal-content wide" style="width: 90vw; max-width: 1400px; height: 85vh; display: flex; flex-direction: column;"> <div class="modal-content wide" style="width: 95vw; max-width: 1500px; height: 90vh; display: flex; flex-direction: column;">
<div class="modal-header"> <div class="modal-header">
<div style="display:flex; align-items:center; gap:0.75rem;"> <div style="display:flex; align-items:center; gap:0.75rem;">
<div style="background:var(--primary-light); padding:0.5rem; border-radius:8px;"> <div style="background:rgba(255,255,255,0.2); padding:0.5rem; border-radius:8px;">
<i data-lucide="file-spreadsheet" style="width:20px; height:20px; color:var(--primary-color);"></i> <i data-lucide="file-spreadsheet" style="width:20px; height:20px; color:white;"></i>
</div> </div>
<div> <div>
<h2 id="upload-preview-title">데이터 업로드 검토</h2> <h2 id="upload-preview-title" style="color:white;">데이터 업로드 최종 검토</h2>
<p style="font-size:12px; color:var(--text-muted); margin-top:2px;">업로드 전 데이터를 확인하고 수정 사항이 있는지 검토하세요.</p> <p style="font-size:12px; color:rgba(255,255,255,0.8); margin-top:2px;">수정 가능한 텍스트는 <span style="color:#ff3d00; font-weight:700;">다홍색(#FF3D00)</span>으로 표시됩니다.</p>
</div> </div>
</div> </div>
<button id="btn-close-upload-preview" class="btn-icon"><i data-lucide="x"></i></button> <button id="btn-close-upload-preview" class="btn-icon" style="color:white;"><i data-lucide="x"></i></button>
</div> </div>
<div class="modal-body" style="display:flex; padding:0; overflow:hidden; flex: 1;"> <div class="modal-body" style="display:flex; padding:0; overflow:hidden; flex: 1;">
<!-- Sidebar for Tabs --> <div id="upload-tab-sidebar" style="width:200px; border-right:1px solid var(--border-color); background:#fafafa; padding:1.5rem 1rem; overflow-y:auto; flex-shrink: 0;">
<div id="upload-tab-sidebar" style="width:240px; border-right:1px solid var(--border-color); background:#fafafa; padding:1.5rem 1rem; overflow-y:auto; flex-shrink: 0;"> <div style="font-size:11px; font-weight:700; color:var(--text-muted); text-transform:uppercase; margin-bottom:1rem; letter-spacing:0.05em;">카테고리</div>
<div style="font-size:11px; font-weight:700; color:var(--text-muted); text-transform:uppercase; margin-bottom:1rem; letter-spacing:0.05em;">데이터 카테고리</div> <div id="upload-tabs-container" style="display:flex; flex-direction:column; gap:0.4rem;"></div>
<div id="upload-tabs-container" style="display:flex; flex-direction:column; gap:0.5rem;">
<!-- Tabs will be injected here -->
</div>
</div> </div>
<!-- Content Area -->
<div style="flex:1; display:flex; flex-direction:column; background:white; overflow:hidden;"> <div style="flex:1; display:flex; flex-direction:column; background:white; overflow:hidden;">
<div id="upload-preview-stats" style="padding:1rem 1.5rem; border-bottom:1px solid var(--border-color); display:flex; justify-content:space-between; align-items:center; background:white;"> <div id="upload-preview-stats" style="padding:1rem 1.5rem; border-bottom:1px solid var(--border-color); display:flex; justify-content:space-between; align-items:center; background:white;">
<div style="display:flex; align-items:center; gap:0.5rem;"> <div style="display:flex; align-items:center; gap:0.5rem;">
@@ -42,20 +40,18 @@ const UPLOAD_PREVIEW_MODAL_HTML = `
<i data-lucide="refresh-ccw" style="width:14px; height:14px; margin-right:4px;"></i> 자산코드 일괄 생성 <i data-lucide="refresh-ccw" style="width:14px; height:14px; margin-right:4px;"></i> 자산코드 일괄 생성
</button> </button>
</div> </div>
<div style="font-size:12px; color:var(--text-muted);"> <div id="upload-warning-text" style="font-size:12px; color:#dc2626; font-weight:600; display:none;">
* 아래 데이터가 신규로 추가되거나 기존 데이터가 갱신됩니다. * 자산번호가 누락된 항목이 있습니다. 일괄 생성을 눌러주세요.
</div> </div>
</div> </div>
<div id="upload-preview-table-wrapper" style="flex:1; overflow:auto; padding:0;"> <div id="upload-preview-table-wrapper" style="flex:1; overflow:auto; padding:0;"></div>
<!-- Table will be injected here -->
</div>
</div> </div>
</div> </div>
<div class="modal-footer" style="background:#f9fafb; border-top:1px solid var(--border-color); flex-shrink: 0;"> <div class="modal-footer" style="background:#f9fafb; border-top:1px solid var(--border-color); flex-shrink: 0;">
<div style="display:flex; gap:0.75rem; width:100%; justify-content:flex-end;"> <div style="display:flex; gap:0.75rem; width:100%; justify-content:flex-end;">
<button id="btn-cancel-upload" class="btn btn-outline" style="height:40px; padding:0 1.5rem;">취소하기</button> <button id="btn-cancel-upload" class="btn btn-outline" style="height:40px; padding:0 1.5rem;">취소</button>
<button id="btn-confirm-upload" class="btn btn-primary" style="height:40px; padding:0 2rem;"> <button id="btn-confirm-upload" class="btn btn-primary" style="height:40px; padding:0 2rem;">
<i data-lucide="save"></i> 최종 데이터 저장하기 <i data-lucide="save"></i> 최종 데이터 저장하기
</button> </button>
@@ -70,29 +66,20 @@ export function initUploadPreviewModal(onSuccess?: () => void) {
if (!document.getElementById('upload-preview-modal')) { if (!document.getElementById('upload-preview-modal')) {
document.body.insertAdjacentHTML('beforeend', UPLOAD_PREVIEW_MODAL_HTML); document.body.insertAdjacentHTML('beforeend', UPLOAD_PREVIEW_MODAL_HTML);
} }
document.getElementById('btn-close-upload-preview')?.addEventListener('click', closeModals); document.getElementById('btn-close-upload-preview')?.addEventListener('click', closeModals);
document.getElementById('btn-cancel-upload')?.addEventListener('click', closeModals); document.getElementById('btn-cancel-upload')?.addEventListener('click', closeModals);
document.getElementById('btn-confirm-upload')?.addEventListener('click', () => { document.getElementById('btn-confirm-upload')?.addEventListener('click', () => confirmUpload());
confirmUpload(); document.getElementById('btn-bulk-generate-codes')?.addEventListener('click', () => generateBulkCodes());
});
document.getElementById('btn-bulk-generate-codes')?.addEventListener('click', () => {
generateBulkCodes();
});
} }
export function openUploadPreview(data: any) { export function openUploadPreview(data: any) {
parsedData = data; parsedData = data;
hasAttemptedBulkGenerate = false;
const tabNames = Object.keys(data); const tabNames = Object.keys(data);
if (tabNames.length === 0) { if (tabNames.length === 0) { alert('업로드할 데이터가 없습니다.'); return; }
alert('업로드할 데이터가 없습니다.');
return;
}
currentTab = tabNames[0]; currentTab = tabNames[0];
renderTabs(); renderTabs();
renderCurrentTable(); renderCurrentTable();
openModal('upload-preview-modal'); openModal('upload-preview-modal');
createIcons({ icons: { X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } }); createIcons({ icons: { X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } });
} }
@@ -101,36 +88,19 @@ function renderTabs() {
const container = document.getElementById('upload-tabs-container'); const container = document.getElementById('upload-tabs-container');
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
Object.keys(parsedData).forEach(tab => { Object.keys(parsedData).forEach(tab => {
const btn = document.createElement('div'); const btn = document.createElement('div');
btn.className = `upload-tab-btn ${tab === currentTab ? 'active' : ''}`; btn.className = `upload-tab-btn ${tab === currentTab ? 'active' : ''}`;
btn.style.cssText = ` btn.style.cssText = `
padding: 0.75rem 1rem; padding: 0.6rem 0.8rem; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500;
border-radius: 8px; display: flex; justify-content: space-between; align-items: center; transition: all 0.2s;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
background: ${tab === currentTab ? 'white' : 'transparent'}; background: ${tab === currentTab ? 'white' : 'transparent'};
color: ${tab === currentTab ? 'var(--primary-color)' : 'var(--text-main)'}; color: ${tab === currentTab ? 'var(--primary-color)' : 'var(--text-main)'};
box-shadow: ${tab === currentTab ? '0 2px 4px rgba(0,0,0,0.05)' : 'none'}; box-shadow: ${tab === currentTab ? '0 2px 4px rgba(0,0,0,0.05)' : 'none'};
border: 1px solid ${tab === currentTab ? 'var(--border-color)' : 'transparent'}; border: 1px solid ${tab === currentTab ? 'var(--border-color)' : 'transparent'};
`; `;
btn.innerHTML = `<span>${tab}</span><span style="font-size:11px; opacity:0.6;">${parsedData[tab].length}</span>`;
btn.innerHTML = ` btn.onclick = () => { syncCurrentDataFromDOM(); currentTab = tab; renderTabs(); renderCurrentTable(); };
<span>${tab}</span>
<span style="font-size:11px; opacity:0.6;">${parsedData[tab].length}</span>
`;
btn.onclick = () => {
currentTab = tab;
renderTabs();
renderCurrentTable();
};
container.appendChild(btn); container.appendChild(btn);
}); });
} }
@@ -139,6 +109,7 @@ function renderCurrentTable() {
const tableWrapper = document.getElementById('upload-preview-table-wrapper'); const tableWrapper = document.getElementById('upload-preview-table-wrapper');
const tabNameEl = document.getElementById('current-tab-name'); const tabNameEl = document.getElementById('current-tab-name');
const tabCountEl = document.getElementById('current-tab-count'); const tabCountEl = document.getElementById('current-tab-count');
const warningText = document.getElementById('upload-warning-text');
if (!tableWrapper || !tabNameEl || !tabCountEl) return; if (!tableWrapper || !tabNameEl || !tabCountEl) return;
const data = parsedData[currentTab]; const data = parsedData[currentTab];
@@ -147,58 +118,155 @@ function renderCurrentTable() {
const generateBtn = document.getElementById('btn-bulk-generate-codes'); const generateBtn = document.getElementById('btn-bulk-generate-codes');
const isHwTab = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'].includes(currentTab); const isHwTab = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'].includes(currentTab);
if (generateBtn) { if (generateBtn) { isHwTab ? generateBtn.classList.remove('hidden') : generateBtn.classList.add('hidden'); }
if (isHwTab) generateBtn.classList.remove('hidden');
else generateBtn.classList.add('hidden');
}
if (!data || data.length === 0) { if (!data || data.length === 0) {
tableWrapper.innerHTML = '<div style="padding:4rem; text-align:center; color:var(--text-muted);">표시할 데이터가 없습니다.</div>'; tableWrapper.innerHTML = '<div style="padding:4rem; text-align:center; color:var(--text-muted);">표시할 데이터가 없습니다.</div>';
return; return;
} }
// Get headers from first item keys, excluding 'id' and 'type' for cleaner view const rawHeaders = Object.keys(data[0]).filter(k => k !== 'id' && k !== 'type' && k !== '상세용도');
const headers = Object.keys(data[0]).filter(k => k !== 'id' && k !== 'type'); const headerMap: Record<string, string> = {};
Object.values(ASSET_SCHEMA).forEach(item => { headerMap[item.key] = item.ui; });
const centerCols = ['No.', '구매법인', '자산번호', '현 사용조직', '설치위치', '보관위치', '상세위치', '담당자', '구매연월', '현재상태'];
const locations = Object.keys(LOCATION_DATA);
let hasAnyMissingCode = false;
let tableHTML = ` let tableHTML = `
<table class="preview-table" style="width:100%; border-collapse:collapse; min-width:max-content;"> <table class="preview-table" id="upload-data-table" style="width:100%; border-collapse:collapse; min-width:max-content; table-layout:auto;">
<thead style="position:sticky; top:0; z-index:10; background:#f8fafc; box-shadow:0 1px 0 var(--border-color);"> <thead style="position:sticky; top:0; z-index:10; background:#f8fafc; box-shadow:0 1px 0 var(--border-color);">
<tr> <tr>
<th style="padding:0.75rem 1rem; text-align:center; font-size:12px; border-bottom:1px solid var(--border-color); width:50px;">No.</th> <th style="padding:0.8rem 1rem; text-align:center; font-size:12px; border-bottom:1px solid var(--border-color); width:50px; background:#FAFAFA;">No.</th>
${headers.map(h => `<th style="padding:0.75rem 1rem; text-align:left; font-size:12px; border-bottom:1px solid var(--border-color); color:var(--text-muted);">${h}</th>`).join('')} ${rawHeaders.map(h => {
const label = headerMap[h] || h;
const isCenter = centerCols.includes(label);
return `<th data-key="${h}" style="padding:0.8rem 1rem; text-align:${isCenter ? 'center' : 'left'}; font-size:12px; border-bottom:1px solid var(--border-color); color:var(--text-muted); font-weight:600; background:#FAFAFA;">${label}</th>`;
}).join('')}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
${data.map((row: any, idx: number) => ` ${data.map((row: any, idx: number) => {
<tr style="border-bottom:1px solid #f1f5f9;"> const isCodeMissing = isHwTab && !row[ASSET_SCHEMA.ASSET_CODE.key];
<td style="padding:0.75rem 1rem; text-align:center; font-size:13px; color:var(--text-muted);">${idx + 1}</td> if (isCodeMissing) hasAnyMissingCode = true;
${headers.map(h => `<td style="padding:0.75rem 1rem; font-size:13px;">${row[h] || '-'}</td>`).join('')} const showRedBox = hasAttemptedBulkGenerate && isCodeMissing;
const trStyle = showRedBox ? 'outline: 2px solid #ff3d00; outline-offset: -2px; background: #fff5f2;' : 'border-bottom:1px solid #f1f5f9;';
return `
<tr data-id="${row.id}" style="${trStyle}">
<td style="padding:0.8rem 1rem; text-align:center; font-size:13px; color:var(--text-muted); background:#fafafa;">${idx + 1}</td>
${rawHeaders.map(h => {
const label = headerMap[h] || h;
const isCenter = centerCols.includes(label);
const isCodeCol = h === ASSET_SCHEMA.ASSET_CODE.key;
const isLocCol = h === ASSET_SCHEMA.LOCATION.key || h === ASSET_SCHEMA.STORE_LOC.key;
const isDetailLocCol = h === ASSET_SCHEMA.DETAIL_LOCATION.key;
const fontColor = isCodeCol ? '#111111' : '#ff3d00';
if (isLocCol) {
return `
<td style="padding:0; min-width:120px; border-right:1px solid #f8fafc;">
<select class="preview-select loc-select" data-key="${h}" style="width:100%; height:36px; border:none; background:transparent; padding:0 0.5rem; font-size:13px; color:#ff3d00; font-weight:600; outline:none; cursor:pointer;">
<option value="">선택</option>
${locations.map(loc => `<option value="${loc}" ${String(row[h]||'').trim() === loc ? 'selected' : ''}>${loc}</option>`).join('')}
</select>
</td>
`;
} else if (isDetailLocCol) {
const parentLocKey = row[ASSET_SCHEMA.LOCATION.key] ? ASSET_SCHEMA.LOCATION.key : ASSET_SCHEMA.STORE_LOC.key;
const currentLoc = String(row[parentLocKey] || '').trim();
const subLocs = LOCATION_DATA[currentLoc] || [];
return `
<td style="padding:0; min-width:120px; border-right:1px solid #f8fafc;">
<select class="preview-select detail-select" data-key="${h}" style="width:100%; height:36px; border:none; background:transparent; padding:0 0.5rem; font-size:13px; color:#ff3d00; font-weight:600; outline:none; cursor:pointer;">
<option value=""></option>
${subLocs.map(sl => `<option value="${sl}" ${String(row[h]||'').trim() === sl ? 'selected' : ''}>${sl}</option>`).join('')}
${(row[h] && !subLocs.includes(String(row[h]).trim())) ? `<option value="${row[h]}" selected>${row[h]}</option>` : ''}
</select>
</td>
`;
} else {
return `<td contenteditable="${!isCodeCol}" data-key="${h}" style="padding:0.8rem 1rem; font-size:13px; text-align:${isCenter ? 'center' : 'left'}; outline:none; color: ${fontColor} !important; border-right:1px solid #f8fafc;">${row[h] || ''}</td>`;
}
}).join('')}
</tr> </tr>
`).join('')} `;
}).join('')}
</tbody> </tbody>
</table> </table>
`; `;
tableWrapper.innerHTML = tableHTML; tableWrapper.innerHTML = tableHTML;
if (warningText) warningText.style.display = (hasAttemptedBulkGenerate && hasAnyMissingCode) ? 'block' : 'none';
// ─── 드롭다운 실시간 연동 이벤트 ───
tableWrapper.querySelectorAll('.loc-select').forEach(sel => {
sel.addEventListener('change', (e) => {
const target = e.target as HTMLSelectElement;
const tr = target.closest('tr')!;
const detailSel = tr.querySelector('.detail-select') as HTMLSelectElement;
if (detailSel) {
const subLocs = LOCATION_DATA[target.value] || [];
detailSel.innerHTML = '<option value="">선택</option>' + subLocs.map(sl => `<option value="${sl}">${sl}</option>`).join('');
// 데이터 객체도 즉시 동기화
syncCurrentDataFromDOM();
}
});
});
// 편집 포커스 스타일
tableWrapper.querySelectorAll('td[contenteditable="true"]').forEach(td => {
td.addEventListener('focus', () => (td as HTMLElement).style.background = '#fff8f6');
td.addEventListener('blur', () => (td as HTMLElement).style.background = 'transparent');
});
}
function syncCurrentDataFromDOM() {
const table = document.getElementById('upload-data-table') as HTMLTableElement;
if (!table) return;
const rows = table.querySelectorAll('tbody tr');
const data = parsedData[currentTab];
rows.forEach((tr) => {
const asset = data.find((a: any) => a.id === tr.getAttribute('data-id'));
if (asset) {
tr.querySelectorAll('td[contenteditable="true"]').forEach(td => {
const key = td.getAttribute('data-key')!;
asset[key] = td.textContent?.trim() || '';
});
tr.querySelectorAll('select.preview-select').forEach(sel => {
const key = sel.getAttribute('data-key')!;
asset[key] = (sel as HTMLSelectElement).value;
});
}
});
} }
async function confirmUpload() { async function confirmUpload() {
const confirmBtn = document.getElementById('btn-confirm-upload') as HTMLButtonElement; syncCurrentDataFromDOM();
if (confirmBtn) { const tabNames = Object.keys(parsedData);
confirmBtn.disabled = true; const hwTabs = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'];
confirmBtn.innerHTML = '<i data-lucide="loader-2" class="animate-spin"></i> 저장 중...'; for (const tab of tabNames) {
createIcons({ icons: { Save } }); if (hwTabs.includes(tab)) {
const missingCodeRows = parsedData[tab].filter((r: any) => !r[ASSET_SCHEMA.ASSET_CODE.key]);
if (missingCodeRows.length > 0) {
alert(`[${tab}] 카테고리에 자산번호가 누락된 항목이 있습니다.\n번호를 생성하거나 수정한 후 다시 시도해주세요.`);
hasAttemptedBulkGenerate = true;
renderCurrentTable();
return;
}
}
} }
try { const confirmBtn = document.getElementById('btn-confirm-upload') as HTMLButtonElement;
const tabNames = Object.keys(parsedData); if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.innerHTML = '저장 중...'; }
let successCount = 0;
try {
let successCount = 0;
const API_BASE = `http://${location.hostname}:3000`;
for (const tab of tabNames) { for (const tab of tabNames) {
const data = parsedData[tab]; const data = parsedData[tab];
let endpoint = ''; let endpoint = '';
const API_BASE = `http://${location.hostname}:3000`;
if (tab === '개인PC') endpoint = `${API_BASE}/api/pc/batch`; if (tab === '개인PC') endpoint = `${API_BASE}/api/pc/batch`;
else if (tab === '서버') endpoint = `${API_BASE}/api/server/batch`; else if (tab === '서버') endpoint = `${API_BASE}/api/server/batch`;
else if (tab === '스토리지') endpoint = `${API_BASE}/api/storage/batch`; else if (tab === '스토리지') endpoint = `${API_BASE}/api/storage/batch`;
@@ -208,92 +276,48 @@ async function confirmUpload() {
else if (tab === '영구SW') endpoint = `${API_BASE}/api/sw/perm/batch`; else if (tab === '영구SW') endpoint = `${API_BASE}/api/sw/perm/batch`;
else if (tab === '클라우드') endpoint = `${API_BASE}/api/cloud/batch`; else if (tab === '클라우드') endpoint = `${API_BASE}/api/cloud/batch`;
else if (tab === '도메인') endpoint = `${API_BASE}/api/ops/domain/batch`; else if (tab === '도메인') endpoint = `${API_BASE}/api/ops/domain/batch`;
if (endpoint) { if (endpoint) {
const response = await fetch(endpoint, { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) successCount++; if (response.ok) successCount++;
} }
} }
if (successCount > 0) { if (onSuccessCallback) onSuccessCallback(); closeModals(); alert(`${successCount}개 카테고리의 데이터가 성공적으로 업로드되었습니다.`); }
if (successCount > 0) { else { alert('데이터 업로드에 실패했습니다.'); }
if (onSuccessCallback) onSuccessCallback(); } catch (err) { alert('업로드 중 오류가 발생했습니다.'); }
closeModals(); finally { if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.innerHTML = '<i data-lucide="save"></i> 최종 데이터 저장하기'; createIcons({ icons: { Save } }); } }
alert(`${successCount}개 카테고리의 데이터가 성공적으로 업로드되었습니다.`);
} else {
alert('데이터 업로드에 실패했습니다.');
}
} catch (err) {
console.error(err);
alert('업로드 중 오류가 발생했습니다.');
} finally {
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i data-lucide="save"></i> 최종 데이터 저장하기';
createIcons({ icons: { Save } });
}
}
} }
async function generateBulkCodes() { async function generateBulkCodes() {
syncCurrentDataFromDOM();
const data = parsedData[currentTab]; const data = parsedData[currentTab];
if (!data) return; if (!data) return;
hasAttemptedBulkGenerate = true;
const generateBtn = document.getElementById('btn-bulk-generate-codes') as HTMLButtonElement; const generateBtn = document.getElementById('btn-bulk-generate-codes') as HTMLButtonElement;
if (generateBtn) { if (generateBtn) { generateBtn.disabled = true; generateBtn.innerHTML = '생성 중...'; }
generateBtn.disabled = true;
generateBtn.innerHTML = '<i data-lucide="refresh-ccw" class="animate-spin"></i> 생성 중...';
createIcons({ icons: { RefreshCcw } });
}
try { try {
// Group rows by prefix (type + purchase_ym) const rowsToProcess = data.filter((r: any) => !r[ASSET_SCHEMA.ASSET_CODE.key]);
const rowsToProcess = data.filter((r: any) => !r.); if (rowsToProcess.length === 0) { alert('이미 모든 항목에 자산코드가 부여되어 있습니다.'); return; }
if (rowsToProcess.length === 0) {
alert('이미 모든 항목에 자산코드가 부여되어 있습니다.');
return;
}
const groups: Record<string, any[]> = {}; const groups: Record<string, any[]> = {};
rowsToProcess.forEach((r: any) => { rowsToProcess.forEach((r: any) => {
const type = r. || r. || r.type || 'ETC'; const type = r. || r. || r.type || 'ETC';
const typeCode = TYPE_PREFIX_MAP[type] || 'ETC'; const typeCode = TYPE_PREFIX_MAP[type] || 'ETC';
const purchaseYM = String(r. || '').replace(/[^0-9]/g, ''); const purchaseYM = String(r[ASSET_SCHEMA.PURCHASE_YM.key] || '').replace(/[^0-9]/g, '');
if (purchaseYM.length < 6) { if (purchaseYM.length < 6) return;
// Fallback or skip
return;
}
const prefix = `${typeCode}-${purchaseYM.substring(0, 6)}-`; const prefix = `${typeCode}-${purchaseYM.substring(0, 6)}-`;
if (!groups[prefix]) groups[prefix] = []; if (!groups[prefix]) groups[prefix] = [];
groups[prefix].push(r); groups[prefix].push(r);
}); });
for (const prefix in groups) { for (const prefix in groups) {
const rows = groups[prefix]; const rows = groups[prefix];
// Fetch current next code for this prefix const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}`);
const res = await fetch(`http://172.16.40.100:3000/api/generate-asset-code?prefix=${prefix}`);
const result = await res.json(); const result = await res.json();
if (result.nextCode) { if (result.nextCode) {
let baseNum = parseInt(result.nextCode.replace(prefix, '')); let baseNum = parseInt(result.nextCode.replace(prefix, ''));
rows.forEach((r, idx) => { rows.forEach((r, idx) => { r[ASSET_SCHEMA.ASSET_CODE.key] = `${prefix}${(baseNum + idx).toString().padStart(4, '0')}`; });
r. = `${prefix}${(baseNum + idx).toString().padStart(4, '0')}`;
});
} }
} }
renderCurrentTable(); renderCurrentTable();
alert(`${rowsToProcess.length}건의 자산코드 생성되었습니다.`); alert('자산코드 생성이 완료되었습니다.');
} catch (err) { } catch (err) { alert('자산코드 생성 중 오류가 발생했습니다.'); }
console.error(err); finally { if (generateBtn) { generateBtn.disabled = false; generateBtn.innerHTML = '<i data-lucide="refresh-ccw"></i> 자산코드 일괄 생성'; createIcons({ icons: { RefreshCcw } }); } }
alert('자산코드 생성 중 오류가 발생했습니다.');
} finally {
if (generateBtn) {
generateBtn.disabled = false;
generateBtn.innerHTML = '<i data-lucide="refresh-ccw"></i> 자산코드 일괄 생성';
createIcons({ icons: { RefreshCcw } });
}
}
} }

View File

@@ -1,4 +1,7 @@
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import ExcelJS from 'exceljs';
import { ASSET_SCHEMA } from './schema';
import { LOCATION_DATA, CORP_LIST, ORG_LIST, HW_TYPE_LIST } from '../components/Modal/SharedData';
export interface HardwareAsset { export interface HardwareAsset {
[key: string]: any; [key: string]: any;
@@ -6,8 +9,10 @@ export interface HardwareAsset {
type: string; type: string;
법인: string; 법인: string;
자산코드: string; 자산코드: string;
사용자: string;
명칭: string; 명칭: string;
위치: string; 위치: string;
상세위치: string;
관리자: string; 관리자: string;
IP주소: string; IP주소: string;
MACaddress: string; MACaddress: string;
@@ -72,55 +77,176 @@ export interface MasterAssetData {
logs: HardwareLog[]; logs: HardwareLog[];
} }
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', '모델명', '메인보드', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매연월', '금액', '납품업체', '품의서명', '비고']; const PC_HEADERS = [
const SERVER_HEADERS = ['법인', '자산코드', '구매연월', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '위치', '담당자(정)', '담당자(부)', 'IP 주소 1', 'IP 주소 2', '원격도구', '서버 ID', '서버 PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage 1', 'Storage 2', 'Storage 3', '모니터링', '비고']; ASSET_SCHEMA.CORP.ui, ASSET_SCHEMA.ASSET_CODE.ui, ASSET_SCHEMA.USER.ui, ASSET_SCHEMA.ORG.ui,
const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매연월', '금액', '납품업체', '품의서명', '비고']; ASSET_SCHEMA.MAINBOARD.ui, ASSET_SCHEMA.OS.ui, ASSET_SCHEMA.CPU.ui, 'GPU', ASSET_SCHEMA.RAM.ui,
const EQUIP_HEADERS = ['법인', '비품유형', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매연월', '금액', '납품업체', '품의서명', '비고']; ASSET_SCHEMA.STORAGE1.ui, ASSET_SCHEMA.STORAGE2.ui, ASSET_SCHEMA.STORAGE3.ui,
const MOBILE_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', '기기유형', 'OS', '구매연월', '금액', '납품업체', '품의서명', '비고']; ASSET_SCHEMA.IP_ADDR.ui, ASSET_SCHEMA.MANAGER_MAIN.ui, ASSET_SCHEMA.MANAGER_SUB.ui,
ASSET_SCHEMA.PURCHASE_YM.ui, ASSET_SCHEMA.PRICE.ui, ASSET_SCHEMA.VENDOR.ui, ASSET_SCHEMA.DOC_NAME.ui, ASSET_SCHEMA.REMARKS.ui
];
const SUB_SW_HEADERS = ['분야', '법인', '제품명', '부서', '수량', '금액', '구매일', '납품업체', '시작일', '만료일', '라이선스유형', '계정명', '비고']; const BASE_HW_HEADERS = [
const PERM_SW_HEADERS = ['분야', '법인', '제품명', '부서', '수량', '금액', '구매일', '납품업체', '시작일', '만료일', '라이선스키', '계정명', '비고']; ASSET_SCHEMA.CORP.ui, '유형', ASSET_SCHEMA.ASSET_CODE.ui, ASSET_SCHEMA.PURCHASE_YM.ui,
const CLOUD_HEADERS = ['플랫폼명', '법인', '제품명', '부서', '계정명', '결제수단', '결제일', '연결카드번호', '당월청구액', '비고']; ASSET_SCHEMA.ORG.ui, ASSET_SCHEMA.LOCATION.ui, ASSET_SCHEMA.DETAIL_LOCATION.ui,
ASSET_SCHEMA.PURPOSE.ui, ASSET_SCHEMA.DETAILS.ui, // 용도, 상세 추가
ASSET_SCHEMA.MANAGER_MAIN.ui, ASSET_SCHEMA.MANAGER_SUB.ui,
ASSET_SCHEMA.MODEL.ui, ASSET_SCHEMA.OS.ui, ASSET_SCHEMA.CPU.ui, ASSET_SCHEMA.RAM.ui, 'GPU',
ASSET_SCHEMA.STORAGE1.ui, ASSET_SCHEMA.STORAGE2.ui, ASSET_SCHEMA.STORAGE3.ui,
ASSET_SCHEMA.IP_ADDR.ui, ASSET_SCHEMA.IP_ADDR2.ui, '원격도구', '서버ID', '서버PW', ASSET_SCHEMA.MONITORING.ui, ASSET_SCHEMA.REMARKS.ui
];
const DOMAIN_HEADERS = ['유형', '법인', '서비스명', '관리도메인', '시작일', '만료일', '금액', '담당자', '담당자(부)', '비고']; const SERVER_HEADERS = [...BASE_HW_HEADERS];
const STORAGE_HEADERS = [...BASE_HW_HEADERS];
// 1. 전산비품/모바일기기: 현 사용조직 추가
const EQUIP_HEADERS = [
ASSET_SCHEMA.CORP.ui, '유형', ASSET_SCHEMA.ASSET_CODE.ui, ASSET_SCHEMA.MODEL.ui,
ASSET_SCHEMA.ORG.ui, // 현 사용조직 추가
ASSET_SCHEMA.STORE_LOC.ui, ASSET_SCHEMA.DETAIL_LOCATION.ui,
ASSET_SCHEMA.MANAGER_MAIN.ui, ASSET_SCHEMA.MANAGER_SUB.ui,
ASSET_SCHEMA.PURCHASE_YM.ui, ASSET_SCHEMA.PRICE.ui, ASSET_SCHEMA.REMARKS.ui
];
const MOBILE_HEADERS = [...EQUIP_HEADERS];
const SUB_SW_HEADERS = ['분야', ASSET_SCHEMA.CORP.ui, ASSET_SCHEMA.PRODUCT.ui, '부서', ASSET_SCHEMA.QTY.ui, ASSET_SCHEMA.PRICE.ui, ASSET_SCHEMA.PURCHASE_YM.ui, '시작일', ASSET_SCHEMA.EXPIRY.ui, ASSET_SCHEMA.VENDOR.ui, ASSET_SCHEMA.REMARKS.ui];
const PERM_SW_HEADERS = ['분야', ASSET_SCHEMA.CORP.ui, ASSET_SCHEMA.PRODUCT.ui, '부서', ASSET_SCHEMA.QTY.ui, ASSET_SCHEMA.PRICE.ui, ASSET_SCHEMA.PURCHASE_YM.ui, '시작일', ASSET_SCHEMA.LICENSE_KEY.ui, ASSET_SCHEMA.VENDOR.ui, ASSET_SCHEMA.REMARKS.ui];
const CLOUD_HEADERS = [ASSET_SCHEMA.PLATFORM.ui, ASSET_SCHEMA.CORP.ui, ASSET_SCHEMA.PRODUCT.ui, '부서', ASSET_SCHEMA.ACCOUNT.ui, ASSET_SCHEMA.PAY_METHOD.ui, ASSET_SCHEMA.PAY_DAY.ui, ASSET_SCHEMA.CARD_NUM.ui, ASSET_SCHEMA.BILLING.ui, ASSET_SCHEMA.REMARKS.ui];
const DOMAIN_HEADERS = ['유형', ASSET_SCHEMA.CORP.ui, '서비스명', '관리도메인', '시작일', '만료일', ASSET_SCHEMA.PRICE.ui, '담당자', '담당자(부)', ASSET_SCHEMA.REMARKS.ui];
/**
* 엑셀 양식 다운로드
*/
export async function downloadTemplate() {
const workbook = new ExcelJS.Workbook();
const refSheet = workbook.addWorksheet('RefData');
const buildings = Object.keys(LOCATION_DATA);
const corps = CORP_LIST;
const orgs = ORG_LIST;
const typeLists = {
server: ['서버', 'PC'],
storage: ['스토리지', 'NAS', 'DAS'],
equip: ['CPU', 'GPU', 'HDD'],
mobile: ['태블릿', '모바일', '노트북']
};
refSheet.getColumn(1).values = ['CorpList', ...corps];
refSheet.getColumn(2).values = ['OrgList', ...orgs];
refSheet.getColumn(3).values = ['LocList', ...buildings];
refSheet.getColumn(4).values = ['ServerTypes', ...typeLists.server];
refSheet.getColumn(5).values = ['StorageTypes', ...typeLists.storage];
refSheet.getColumn(6).values = ['EquipTypes', ...typeLists.equip];
refSheet.getColumn(7).values = ['MobileTypes', ...typeLists.mobile];
workbook.definedNames.add('RefData!$A$2:$A$' + (corps.length + 1), 'CorpList');
workbook.definedNames.add('RefData!$B$2:$B$' + (orgs.length + 1), 'OrgList');
workbook.definedNames.add('RefData!$C$2:$C$' + (buildings.length + 1), 'LocList');
workbook.definedNames.add('RefData!$D$2:$D$' + (typeLists.server.length + 1), 'ServerTypes');
workbook.definedNames.add('RefData!$E$2:$E$' + (typeLists.storage.length + 1), 'StorageTypes');
workbook.definedNames.add('RefData!$F$2:$F$' + (typeLists.equip.length + 1), 'EquipTypes');
workbook.definedNames.add('RefData!$G$2:$G$' + (typeLists.mobile.length + 1), 'MobileTypes');
buildings.forEach((building, idx) => {
const colIdx = 8 + idx;
const subLocs = LOCATION_DATA[building];
const column = refSheet.getColumn(colIdx);
column.values = [building, ...subLocs];
const safeName = building.replace(/\s/g, '_');
workbook.definedNames.add(`RefData!$${column.letter}$2:$${column.letter}$${subLocs.length + 1}`, safeName);
});
export function downloadTemplate() {
const wb = XLSX.utils.book_new();
const tabConfigs = [ const tabConfigs = [
{ name: '개인PC', headers: PC_HEADERS }, { name: '개인PC', headers: PC_HEADERS, typeRef: '' },
{ name: '서버', headers: SERVER_HEADERS }, { name: '서버', headers: SERVER_HEADERS, typeRef: 'ServerTypes' },
{ name: '스토리지', headers: STORAGE_HEADERS }, { name: '스토리지', headers: STORAGE_HEADERS, typeRef: 'StorageTypes' },
{ name: '전산비품', headers: EQUIP_HEADERS }, { name: '전산비품', headers: EQUIP_HEADERS, typeRef: 'EquipTypes' },
{ name: '모바일기기', headers: MOBILE_HEADERS }, { name: '모바일기기', headers: MOBILE_HEADERS, typeRef: 'MobileTypes' },
{ name: '구독SW', headers: SUB_SW_HEADERS }, { name: '구독SW', headers: SUB_SW_HEADERS, typeRef: '' },
{ name: '영구SW', headers: PERM_SW_HEADERS }, { name: '영구SW', headers: PERM_SW_HEADERS, typeRef: '' },
{ name: '클라우드', headers: CLOUD_HEADERS }, { name: '클라우드', headers: CLOUD_HEADERS, typeRef: '' },
{ name: '도메인', headers: DOMAIN_HEADERS } { name: '도메인', headers: DOMAIN_HEADERS, typeRef: '' }
]; ];
tabConfigs.forEach(config => { tabConfigs.forEach(config => {
const ws = XLSX.utils.aoa_to_sheet([config.headers]); const sheet = workbook.addWorksheet(config.name);
ws['!cols'] = Array(config.headers.length).fill({ wch: 18 }); sheet.getRow(1).values = config.headers;
XLSX.utils.book_append_sheet(wb, ws, config.name); sheet.getRow(1).font = { bold: true };
sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE9EEED' } };
const rowCount = 200;
const codeIdx = config.headers.indexOf(ASSET_SCHEMA.ASSET_CODE.ui) + 1;
if (codeIdx > 0) sheet.getColumn(codeIdx).hidden = true;
config.headers.forEach((header, hIdx) => {
const colNum = hIdx + 1;
const isFreeTextGroup = (config.name === '전산비품' || config.name === '모바일기기');
for (let r = 2; r <= rowCount; r++) {
const cell = sheet.getCell(r, colNum);
if (header === ASSET_SCHEMA.CORP.ui) {
cell.dataValidation = { type: 'list', allowBlank: true, formulae: ['CorpList'] };
} else if (header === ASSET_SCHEMA.ORG.ui) {
cell.dataValidation = { type: 'list', allowBlank: true, formulae: ['OrgList'] };
} else if ((header === ASSET_SCHEMA.LOCATION.ui || header === ASSET_SCHEMA.STORE_LOC.ui) && !isFreeTextGroup) {
cell.dataValidation = { type: 'list', allowBlank: true, formulae: ['LocList'] };
} else if (header === ASSET_SCHEMA.DETAIL_LOCATION.ui && !isFreeTextGroup) {
const parentColIdx = config.headers.indexOf(ASSET_SCHEMA.LOCATION.ui) + 1 || config.headers.indexOf(ASSET_SCHEMA.STORE_LOC.ui) + 1;
if (parentColIdx > 0) {
const parentLetter = sheet.getColumn(parentColIdx).letter;
cell.dataValidation = { type: 'list', allowBlank: true, formulae: [`=INDIRECT(SUBSTITUTE(${parentLetter}${r}," ","_"))`] };
}
} else if (header === '유형' && config.typeRef) {
cell.dataValidation = { type: 'list', allowBlank: true, formulae: [config.typeRef] };
}
}
});
sheet.columns.forEach(col => { col.width = col.hidden ? 0 : 18; });
}); });
XLSX.writeFile(wb, 'itam_assets_template.xlsx'); refSheet.state = 'veryHidden';
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ITAM_Standard_Template_${new Date().toISOString().split('T')[0]}.xlsx`;
a.click();
window.URL.revokeObjectURL(url);
} }
export function exportToExcel(masterData: MasterAssetData) { export function exportToExcel(masterData: MasterAssetData) {
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
const getVal = (obj: any, schemaItem: any) => obj[schemaItem.key] || '';
const exportMap = [ const exportMap = [
{ tab: '개인PC', list: masterData.pc, headers: PC_HEADERS, map: (a: any) => [a., a., 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.] }, {
{ 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.IP2, a., a.ID, a.PW, a., a.OS, a.CPU, a.RAM, a.GPU, a.SSD1, a.SSD2, a.HDD1, a., a.] }, tab: '개인PC', list: masterData.pc, headers: PC_HEADERS,
{ 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.] }, map: (a: any) => [
{ tab: '전산비품', list: masterData.equip, headers: EQUIP_HEADERS, map: (a: any) => [a., a., a., a., a., a., a.IP주소, a.MACaddress, a.HW사양, a.OS, a., a., a., a., a.] }, getVal(a, ASSET_SCHEMA.CORP), getVal(a, ASSET_SCHEMA.ASSET_CODE), getVal(a, ASSET_SCHEMA.USER), getVal(a, ASSET_SCHEMA.ORG),
{ tab: '모바일기기', list: masterData.mobile, headers: MOBILE_HEADERS, map: (a: any) => [a., a., a., a., a., a.type, a.OS, a., a., a., a., a.] }, getVal(a, ASSET_SCHEMA.MAINBOARD), getVal(a, ASSET_SCHEMA.OS), getVal(a, ASSET_SCHEMA.CPU), a.GPU, getVal(a, ASSET_SCHEMA.RAM),
{ tab: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a., a., a., a., a., a.] }, getVal(a, ASSET_SCHEMA.STORAGE1), getVal(a, ASSET_SCHEMA.STORAGE2), getVal(a, ASSET_SCHEMA.STORAGE3),
{ tab: '영구SW', list: masterData.permSw, headers: PERM_SW_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a., a., a., a., a., a.] }, getVal(a, ASSET_SCHEMA.IP_ADDR), getVal(a, ASSET_SCHEMA.MANAGER_MAIN), getVal(a, ASSET_SCHEMA.MANAGER_SUB),
{ tab: '클라우드', list: masterData.cloud, headers: CLOUD_HEADERS, map: (a: any) => [a., a., a., a., a., a., a., a., a., a.] }, getVal(a, ASSET_SCHEMA.PURCHASE_YM), getVal(a, ASSET_SCHEMA.PRICE), getVal(a, ASSET_SCHEMA.VENDOR), getVal(a, ASSET_SCHEMA.DOC_NAME), getVal(a, ASSET_SCHEMA.REMARKS)
]
},
{
tab: '서버', list: masterData.server, headers: SERVER_HEADERS,
map: (a: any) => [
getVal(a, ASSET_SCHEMA.CORP), a.type, getVal(a, ASSET_SCHEMA.ASSET_CODE), getVal(a, ASSET_SCHEMA.PURCHASE_YM), getVal(a, ASSET_SCHEMA.ORG),
getVal(a, ASSET_SCHEMA.LOCATION), getVal(a, ASSET_SCHEMA.DETAIL_LOCATION), getVal(a, ASSET_SCHEMA.MANAGER_MAIN), getVal(a, ASSET_SCHEMA.MANAGER_SUB), getVal(a, ASSET_SCHEMA.MODEL),
getVal(a, ASSET_SCHEMA.OS), getVal(a, ASSET_SCHEMA.CPU), getVal(a, ASSET_SCHEMA.RAM), a.GPU, getVal(a, ASSET_SCHEMA.STORAGE1),
getVal(a, ASSET_SCHEMA.STORAGE2), getVal(a, ASSET_SCHEMA.STORAGE3), getVal(a, ASSET_SCHEMA.IP_ADDR), getVal(a, ASSET_SCHEMA.IP_ADDR2),
a., a.ID, a.PW, getVal(a, ASSET_SCHEMA.MONITORING), getVal(a, ASSET_SCHEMA.REMARKS)
]
},
{ tab: '스토리지', list: masterData.storage, headers: STORAGE_HEADERS, map: (a: any) => [getVal(a, ASSET_SCHEMA.CORP), a.type, getVal(a, ASSET_SCHEMA.ASSET_CODE), getVal(a, ASSET_SCHEMA.ORG), getVal(a, ASSET_SCHEMA.LOCATION), getVal(a, ASSET_SCHEMA.DETAIL_LOCATION), getVal(a, ASSET_SCHEMA.MANAGER_MAIN), getVal(a, ASSET_SCHEMA.MANAGER_SUB), getVal(a, ASSET_SCHEMA.MODEL), a., getVal(a, ASSET_SCHEMA.IP_ADDR), getVal(a, ASSET_SCHEMA.PURCHASE_YM), getVal(a, ASSET_SCHEMA.PRICE), getVal(a, ASSET_SCHEMA.REMARKS)] },
{ tab: '전산비품', list: masterData.equip, headers: EQUIP_HEADERS, map: (a: any) => [getVal(a, ASSET_SCHEMA.CORP), a.type, getVal(a, ASSET_SCHEMA.ASSET_CODE), getVal(a, ASSET_SCHEMA.MODEL), getVal(a, ASSET_SCHEMA.ORG), getVal(a, ASSET_SCHEMA.STORE_LOC), getVal(a, ASSET_SCHEMA.DETAIL_LOCATION), getVal(a, ASSET_SCHEMA.MANAGER_MAIN), getVal(a, ASSET_SCHEMA.MANAGER_SUB), getVal(a, ASSET_SCHEMA.PURCHASE_YM), getVal(a, ASSET_SCHEMA.PRICE), getVal(a, ASSET_SCHEMA.REMARKS)] },
{ tab: '모바일기기', list: masterData.mobile, headers: MOBILE_HEADERS, map: (a: any) => [getVal(a, ASSET_SCHEMA.CORP), a.type, getVal(a, ASSET_SCHEMA.ASSET_CODE), getVal(a, ASSET_SCHEMA.MODEL), getVal(a, ASSET_SCHEMA.ORG), getVal(a, ASSET_SCHEMA.STORE_LOC), getVal(a, ASSET_SCHEMA.DETAIL_LOCATION), getVal(a, ASSET_SCHEMA.MANAGER_MAIN), getVal(a, ASSET_SCHEMA.MANAGER_SUB), getVal(a, ASSET_SCHEMA.PURCHASE_YM), getVal(a, ASSET_SCHEMA.PRICE), getVal(a, ASSET_SCHEMA.REMARKS)] },
{ tab: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a., getVal(a, ASSET_SCHEMA.CORP), getVal(a, ASSET_SCHEMA.PRODUCT), a., getVal(a, ASSET_SCHEMA.QTY), getVal(a, ASSET_SCHEMA.PRICE), getVal(a, ASSET_SCHEMA.PURCHASE_YM), a., getVal(a, ASSET_SCHEMA.EXPIRY), getVal(a, ASSET_SCHEMA.VENDOR), getVal(a, ASSET_SCHEMA.REMARKS)] },
{ tab: '영구SW', list: masterData.permSw, headers: PERM_SW_HEADERS, map: (a: any) => [a., getVal(a, ASSET_SCHEMA.CORP), getVal(a, ASSET_SCHEMA.PRODUCT), a., getVal(a, ASSET_SCHEMA.QTY), getVal(a, ASSET_SCHEMA.PRICE), getVal(a, ASSET_SCHEMA.PURCHASE_YM), a., getVal(a, ASSET_SCHEMA.LICENSE_KEY), getVal(a, ASSET_SCHEMA.VENDOR), getVal(a, ASSET_SCHEMA.REMARKS)] },
{ tab: '클라우드', list: masterData.cloud, headers: CLOUD_HEADERS, map: (a: any) => [getVal(a, ASSET_SCHEMA.PLATFORM), getVal(a, ASSET_SCHEMA.CORP), getVal(a, ASSET_SCHEMA.PRODUCT), a., getVal(a, ASSET_SCHEMA.ACCOUNT), getVal(a, ASSET_SCHEMA.PAY_METHOD), getVal(a, ASSET_SCHEMA.PAY_DAY), getVal(a, ASSET_SCHEMA.CARD_NUM), getVal(a, ASSET_SCHEMA.BILLING), getVal(a, ASSET_SCHEMA.REMARKS)] },
{ tab: '도메인', list: masterData.domain || [], headers: DOMAIN_HEADERS, map: (a: any) => [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] } { tab: '도메인', list: masterData.domain || [], headers: DOMAIN_HEADERS, map: (a: any) => [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] }
]; ];
exportMap.forEach(m => { exportMap.forEach(m => {
const ws = XLSX.utils.aoa_to_sheet([m.headers, ...m.list.map(m.map)]); const ws = XLSX.utils.aoa_to_sheet([m.headers, ...m.list.map(m.map)]);
XLSX.utils.book_append_sheet(wb, ws, m.tab); XLSX.utils.book_append_sheet(wb, ws, m.tab);
@@ -135,29 +261,72 @@ export async function parseExcel(file: File): Promise<any> {
try { try {
const workbook = XLSX.read(e.target?.result, { type: 'binary' }); const workbook = XLSX.read(e.target?.result, { type: 'binary' });
const parsedData: any = {}; const parsedData: any = {};
workbook.SheetNames.forEach(sheetName => { workbook.SheetNames.forEach(sheetName => {
if (sheetName === 'RefData') return;
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]) as any[]; const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]) as any[];
const list: any[] = []; const list: any[] = [];
rows.forEach(r => { rows.forEach(r => {
const common = { id: Math.random().toString(36).substring(2, 9) }; const common = { id: Math.random().toString(36).substring(2, 9) };
const mapVal = (schemaItem: any) => r[schemaItem.ui] || '';
if (sheetName === '개인PC') { if (sheetName === '개인PC') {
list.push({ ...common, type: '개인PC', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 사용자: 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['비고']||'' }); list.push({
} else if (sheetName === '서버') { ...common, type: 'PC', : '개인PC',
list.push({ ...common, type: '서버', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 구매연월: r['구매연월']||'', storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소 1']||'', IP2: r['IP 주소 2']||'', 원격접속: r['원격도구']||'', 서버ID: r['서버 ID']||'', 서버PW: r['서버 PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'', SSD1: r['Storage 1']||'', SSD2: r['Storage 2']||'', HDD1: r['Storage 3']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'' }); [ASSET_SCHEMA.CORP.key]: mapVal(ASSET_SCHEMA.CORP),
} else if (sheetName === '스토리지') { [ASSET_SCHEMA.ASSET_CODE.key]: mapVal(ASSET_SCHEMA.ASSET_CODE),
list.push({ ...common, type: '스토리지', 법인: r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'', 구매연월: r['구매연월']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); [ASSET_SCHEMA.USER.key]: mapVal(ASSET_SCHEMA.USER),
} else if (sheetName === '전산비품') { [ASSET_SCHEMA.ORG.key]: mapVal(ASSET_SCHEMA.ORG),
list.push({ ...common, type: '전산비품', 법인: r['법인']||'', 비품유형: r['비품유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'', 구매연월: r['구매연월']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); [ASSET_SCHEMA.MAINBOARD.key]: mapVal(ASSET_SCHEMA.MAINBOARD),
} else if (sheetName === '모바일기기') { [ASSET_SCHEMA.OS.key]: mapVal(ASSET_SCHEMA.OS),
list.push({ ...common, type: '모바일기기', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', 기기유형: r['기기유형']||'', OS: r['OS']||'', 구매연월: r['구매연월']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }); [ASSET_SCHEMA.CPU.key]: mapVal(ASSET_SCHEMA.CPU),
GPU: r['GPU'] || '',
[ASSET_SCHEMA.RAM.key]: mapVal(ASSET_SCHEMA.RAM),
[ASSET_SCHEMA.STORAGE1.key]: mapVal(ASSET_SCHEMA.STORAGE1),
[ASSET_SCHEMA.STORAGE2.key]: mapVal(ASSET_SCHEMA.STORAGE2),
[ASSET_SCHEMA.STORAGE3.key]: mapVal(ASSET_SCHEMA.STORAGE3),
[ASSET_SCHEMA.IP_ADDR.key]: mapVal(ASSET_SCHEMA.IP_ADDR),
[ASSET_SCHEMA.MANAGER_MAIN.key]: mapVal(ASSET_SCHEMA.MANAGER_MAIN),
[ASSET_SCHEMA.MANAGER_SUB.key]: mapVal(ASSET_SCHEMA.MANAGER_SUB),
[ASSET_SCHEMA.PURCHASE_YM.key]: mapVal(ASSET_SCHEMA.PURCHASE_YM),
[ASSET_SCHEMA.PRICE.key]: mapVal(ASSET_SCHEMA.PRICE),
[ASSET_SCHEMA.VENDOR.key]: mapVal(ASSET_SCHEMA.VENDOR),
[ASSET_SCHEMA.DOC_NAME.key]: mapVal(ASSET_SCHEMA.DOC_NAME),
[ASSET_SCHEMA.REMARKS.key]: mapVal(ASSET_SCHEMA.REMARKS)
});
} else if (['서버', '스토리지', '전산비품', '모바일기기'].includes(sheetName)) {
const typeValue = r['유형'] || sheetName;
list.push({
...common,
type: typeValue,
상세용도: sheetName === '서버' ? '서버' : typeValue,
[ASSET_SCHEMA.CORP.key]: mapVal(ASSET_SCHEMA.CORP),
[ASSET_SCHEMA.ASSET_CODE.key]: mapVal(ASSET_SCHEMA.ASSET_CODE),
[ASSET_SCHEMA.PURCHASE_YM.key]: mapVal(ASSET_SCHEMA.PURCHASE_YM),
[ASSET_SCHEMA.ORG.key]: mapVal(ASSET_SCHEMA.ORG),
[ASSET_SCHEMA.LOCATION.key]: mapVal(ASSET_SCHEMA.LOCATION) || r[ASSET_SCHEMA.STORE_LOC.ui] || '',
[ASSET_SCHEMA.DETAIL_LOCATION.key]: mapVal(ASSET_SCHEMA.DETAIL_LOCATION),
[ASSET_SCHEMA.MANAGER_MAIN.key]: mapVal(ASSET_SCHEMA.MANAGER_MAIN),
[ASSET_SCHEMA.MANAGER_SUB.key]: mapVal(ASSET_SCHEMA.MANAGER_SUB),
[ASSET_SCHEMA.IP_ADDR.key]: mapVal(ASSET_SCHEMA.IP_ADDR),
[ASSET_SCHEMA.IP_ADDR2.key]: mapVal(ASSET_SCHEMA.IP_ADDR2),
원격접속: r['원격도구'] || '', 서버ID: r['서버ID'] || '', 서버PW: r['서버PW'] || '',
[ASSET_SCHEMA.MODEL.key]: mapVal(ASSET_SCHEMA.MODEL),
[ASSET_SCHEMA.OS.key]: mapVal(ASSET_SCHEMA.OS),
[ASSET_SCHEMA.CPU.key]: mapVal(ASSET_SCHEMA.CPU),
[ASSET_SCHEMA.RAM.key]: mapVal(ASSET_SCHEMA.RAM),
GPU: r['GPU'] || '',
[ASSET_SCHEMA.STORAGE1.key]: mapVal(ASSET_SCHEMA.STORAGE1),
[ASSET_SCHEMA.STORAGE2.key]: mapVal(ASSET_SCHEMA.STORAGE2),
[ASSET_SCHEMA.STORAGE3.key]: mapVal(ASSET_SCHEMA.STORAGE3),
[ASSET_SCHEMA.MONITORING.key]: mapVal(ASSET_SCHEMA.MONITORING),
[ASSET_SCHEMA.REMARKS.key]: mapVal(ASSET_SCHEMA.REMARKS)
});
} else if (sheetName === '구독SW') { } else if (sheetName === '구독SW') {
list.push({ ...common, type: '구독SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 시작일: r['시작일']||'', 만료일: r['만료일']||'', 라이선스유형: r['라이선스유형']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }); list.push({ ...common, type: '구독SW', 분야: r['분야'] || '', [ASSET_SCHEMA.CORP.key]: mapVal(ASSET_SCHEMA.CORP), [ASSET_SCHEMA.PRODUCT.key]: mapVal(ASSET_SCHEMA.PRODUCT), 부서: r['부서'] || '', [ASSET_SCHEMA.QTY.key]: mapVal(ASSET_SCHEMA.QTY), [ASSET_SCHEMA.PRICE.key]: mapVal(ASSET_SCHEMA.PRICE), [ASSET_SCHEMA.PURCHASE_YM.key]: mapVal(ASSET_SCHEMA.PURCHASE_YM), 시작일: r['시작일'] || '', [ASSET_SCHEMA.VENDOR.key]: mapVal(ASSET_SCHEMA.VENDOR), [ASSET_SCHEMA.EXPIRY.key]: mapVal(ASSET_SCHEMA.EXPIRY), [ASSET_SCHEMA.REMARKS.key]: mapVal(ASSET_SCHEMA.REMARKS) });
} else if (sheetName === '영구SW') { } else if (sheetName === '영구SW') {
list.push({ ...common, type: '영구SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 시작일: r['시작일']||'', 만료일: r['만료일']||'', 라이선스키: r['라이선스키']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }); list.push({ ...common, type: '영구SW', 분야: r['분야'] || '', [ASSET_SCHEMA.CORP.key]: mapVal(ASSET_SCHEMA.CORP), [ASSET_SCHEMA.PRODUCT.key]: mapVal(ASSET_SCHEMA.PRODUCT), 부서: r['부서'] || '', [ASSET_SCHEMA.QTY.key]: mapVal(ASSET_SCHEMA.QTY), [ASSET_SCHEMA.PRICE.key]: mapVal(ASSET_SCHEMA.PRICE), [ASSET_SCHEMA.PURCHASE_YM.key]: mapVal(ASSET_SCHEMA.PURCHASE_YM), 시작일: r['시작일'] || '', [ASSET_SCHEMA.VENDOR.key]: mapVal(ASSET_SCHEMA.VENDOR), [ASSET_SCHEMA.LICENSE_KEY.key]: mapVal(ASSET_SCHEMA.LICENSE_KEY), [ASSET_SCHEMA.REMARKS.key]: mapVal(ASSET_SCHEMA.REMARKS) });
} else if (sheetName === '클라우드') { } else if (sheetName === '클라우드') {
list.push({ ...common, type: '클라우드', 플랫폼명: r['플랫폼명']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 계정명: r['계정명']||'', 결제수단: r['결제수단']||'', 결제일: r['결제일']||'', 연결카드번호: r['연결카드번호']||'', 당월청구액: r['당월청구액']||'', 비고: r['비고']||'' }); list.push({ ...common, type: '클라우드', [ASSET_SCHEMA.PLATFORM.key]: mapVal(ASSET_SCHEMA.PLATFORM), [ASSET_SCHEMA.CORP.key]: mapVal(ASSET_SCHEMA.CORP), [ASSET_SCHEMA.PRODUCT.key]: mapVal(ASSET_SCHEMA.PRODUCT), 부서: r['부서'] || '', [ASSET_SCHEMA.ACCOUNT.key]: mapVal(ASSET_SCHEMA.ACCOUNT), [ASSET_SCHEMA.PAY_METHOD.key]: mapVal(ASSET_SCHEMA.PAY_METHOD), [ASSET_SCHEMA.PAY_DAY.key]: mapVal(ASSET_SCHEMA.PAY_DAY), [ASSET_SCHEMA.CARD_NUM.key]: mapVal(ASSET_SCHEMA.CARD_NUM), [ASSET_SCHEMA.BILLING.key]: mapVal(ASSET_SCHEMA.BILLING), [ASSET_SCHEMA.REMARKS.key]: mapVal(ASSET_SCHEMA.REMARKS) });
} else if (sheetName === '도메인') { } else if (sheetName === '도메인') {
list.push({ ...common, type: r['유형']||'도메인', corp: r['법인']||'', service_name: r['서비스명']||'', domain_name: r['관리도메인']||'', start_date: r['시작일']||'', expiry_date: r['만료일']||'', price: r['금액']||'', manager_main: r['담당자']||'', manager_sub: r['담당자(부)']||'', remarks: r['비고']||'' }); list.push({ ...common, type: r['유형']||'도메인', corp: r['법인']||'', service_name: r['서비스명']||'', domain_name: r['관리도메인']||'', start_date: r['시작일']||'', expiry_date: r['만료일']||'', price: r['금액']||'', manager_main: r['담당자']||'', manager_sub: r['담당자(부)']||'', remarks: r['비고']||'' });
} }

View File

@@ -13,9 +13,13 @@ export const ASSET_SCHEMA = {
CORP: { key: '법인', db: 'corp', ui: '구매법인' }, CORP: { key: '법인', db: 'corp', ui: '구매법인' },
ASSET_CODE: { key: '자산코드', db: 'asset_code', ui: '자산번호' }, ASSET_CODE: { key: '자산코드', db: 'asset_code', ui: '자산번호' },
PURCHASE_YM: { key: '구매연월', db: 'purchase_date', ui: '구매연월' }, PURCHASE_YM: { key: '구매연월', db: 'purchase_date', ui: '구매연월' },
START_DATE: { key: '시작일', db: 'start_date', ui: '시작일' },
ORG: { key: '현사용조직', db: 'current_org', ui: '현 사용조직' }, ORG: { key: '현사용조직', db: 'current_org', ui: '현 사용조직' },
PREV_ORG: { key: '이전사용조직', db: 'prev_org', ui: '이전 사용조직' }, PREV_ORG: { key: '이전사용조직', db: 'prev_org', ui: '이전 사용조직' },
LOCATION: { key: '위치', db: 'location', ui: '설치위치' }, LOCATION: { key: '위치', db: 'location', ui: '설치위치' },
DETAIL_LOCATION:{ key: '상세위치', db: 'detail_location', ui: '상세위치' },
PURPOSE: { key: '용도', db: 'purpose', ui: '용도' },
DETAILS: { key: '상세', db: 'details', ui: '상세' },
MANAGER_MAIN: { key: '담당자_정', db: 'manager_main', ui: '담당자' }, MANAGER_MAIN: { key: '담당자_정', db: 'manager_main', ui: '담당자' },
MANAGER_SUB: { key: '담당자_부', db: 'manager_sub', ui: '담당자(부)' }, MANAGER_SUB: { key: '담당자_부', db: 'manager_sub', ui: '담당자(부)' },
PRICE: { key: '금액', db: 'price', ui: '도입금액' }, PRICE: { key: '금액', db: 'price', ui: '도입금액' },
@@ -24,7 +28,7 @@ export const ASSET_SCHEMA = {
REMARKS: { key: '비고', db: 'remarks', ui: '비고' }, REMARKS: { key: '비고', db: 'remarks', ui: '비고' },
// ─── 하드웨어 상세 (Hardware) ─── // ─── 하드웨어 상세 (Hardware) ───
USER: { key: '사용자', db: 'purpose', ui: '사용자' }, USER: { key: '사용자', db: 'user_name', ui: '사용자' },
MODEL: { key: '모델명', db: 'model_name', ui: '모델명' }, MODEL: { key: '모델명', db: 'model_name', ui: '모델명' },
MAINBOARD: { key: '메인보드', db: 'mainboard', ui: '메인보드' }, MAINBOARD: { key: '메인보드', db: 'mainboard', ui: '메인보드' },
OS: { key: 'OS', db: 'os', ui: '운영체제' }, OS: { key: 'OS', db: 'os', ui: '운영체제' },
@@ -32,11 +36,13 @@ export const ASSET_SCHEMA = {
RAM: { key: 'RAM', db: 'ram', ui: 'RAM' }, RAM: { key: 'RAM', db: 'ram', ui: 'RAM' },
STORAGE1: { key: 'SSD1', db: 'storage1', ui: 'Storage 1' }, STORAGE1: { key: 'SSD1', db: 'storage1', ui: 'Storage 1' },
STORAGE2: { key: 'SSD2', db: 'storage2', ui: 'Storage 2' }, STORAGE2: { key: 'SSD2', db: 'storage2', ui: 'Storage 2' },
STORAGE3: { key: 'HDD1', db: 'storage3', ui: 'Storage 3' },
IP_ADDR: { key: 'IP주소', db: 'ip_address', ui: 'IP 주소 1' }, IP_ADDR: { key: 'IP주소', db: 'ip_address', ui: 'IP 주소 1' },
IP_ADDR2: { key: 'IP2', db: 'ip2', ui: 'IP 주소 2' }, IP_ADDR2: { key: 'IP2', db: 'ip2', ui: 'IP 주소 2' },
MAC_ADDR: { key: 'MACaddress', db: 'mac_address', ui: 'MAC 주소' }, MAC_ADDR: { key: 'MACaddress', db: 'mac_address', ui: 'MAC 주소' },
STATUS: { key: '현재상태', db: 'status', ui: '현재상태' }, STATUS: { key: '현재상태', db: 'status', ui: '현재상태' },
STORE_LOC: { key: '보관위치', db: 'storage_location',ui: '보관위치' }, STORE_LOC: { key: '보관위치', db: 'storage_location',ui: '보관위치' },
MONITORING: { key: '모니터링', db: 'monitoring', ui: '모니터링' },
// ─── 소프트웨어/클라우드 상세 (SW/Cloud) ─── // ─── 소프트웨어/클라우드 상세 (SW/Cloud) ───
PRODUCT: { key: '제품명', db: 'product_name', ui: '제품/서비스명' }, PRODUCT: { key: '제품명', db: 'product_name', ui: '제품/서비스명' },

View File

@@ -6,7 +6,7 @@ import { createIcons, Paperclip, RefreshCcw } from 'lucide';
/** /**
* PC 자산 목록 뷰 * PC 자산 목록 뷰
* 담당자(부) 추가 및 정렬 보정 * 설치위치 컬럼 제거
*/ */
export function renderPcList(container: HTMLElement) { export function renderPcList(container: HTMLElement) {
const fullList = sortAssets(state.masterData.pc); const fullList = sortAssets(state.masterData.pc);
@@ -37,20 +37,19 @@ export function renderPcList(container: HTMLElement) {
table.innerHTML = ` table.innerHTML = `
<thead> <thead>
<tr> <tr>
<th style="text-align:center;">No</th> <th class="text-center">No</th>
<th style="text-align:center;">${ASSET_SCHEMA.CORP.ui}</th> <th class="text-center">${ASSET_SCHEMA.CORP.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.ORG.ui}</th> <th class="text-center">${ASSET_SCHEMA.ORG.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.ASSET_CODE.ui}</th> <th class="text-center">${ASSET_SCHEMA.ASSET_CODE.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.USER.ui}</th> <th class="text-center">${ASSET_SCHEMA.USER.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.LOCATION.ui}</th> <th class="text-center">담당자(정/부)</th>
<th style="text-align:center;">담당자(정/부)</th> <th class="text-center">${ASSET_SCHEMA.MAINBOARD.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.MAINBOARD.ui}</th> <th class="text-center">${ASSET_SCHEMA.CPU.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.CPU.ui}</th> <th class="text-center">${ASSET_SCHEMA.RAM.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.RAM.ui}</th> <th class="text-center">Storage</th>
<th style="text-align:center;">Storage</th> <th class="text-center">${ASSET_SCHEMA.PURCHASE_YM.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.PURCHASE_YM.ui}</th> <th class="text-center">${ASSET_SCHEMA.PRICE.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.PRICE.ui}</th> <th class="text-center">${ASSET_SCHEMA.DOC_NAME.ui}</th>
<th style="text-align:center;">${ASSET_SCHEMA.DOC_NAME.ui}</th>
</tr> </tr>
</thead> </thead>
<tbody id="dynamic-tbody"></tbody> <tbody id="dynamic-tbody"></tbody>
@@ -63,7 +62,6 @@ export function renderPcList(container: HTMLElement) {
const updateTable = () => { const updateTable = () => {
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement;
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
const corp = corpSelect ? corpSelect.value : ''; const corp = corpSelect ? corpSelect.value : '';
@@ -79,7 +77,7 @@ export function renderPcList(container: HTMLElement) {
tbody.innerHTML = ''; tbody.innerHTML = '';
if (filtered.length === 0) { if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="14" style="text-align:center; padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`; tbody.innerHTML = `<tr><td colspan="13" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
return; return;
} }
@@ -88,7 +86,6 @@ export function renderPcList(container: HTMLElement) {
tr.style.cursor = 'pointer'; tr.style.cursor = 'pointer';
const storage = [asset[ASSET_SCHEMA.STORAGE1.key], asset[ASSET_SCHEMA.STORAGE2.key]].filter(v => v).join(' / '); const storage = [asset[ASSET_SCHEMA.STORAGE1.key], asset[ASSET_SCHEMA.STORAGE2.key]].filter(v => v).join(' / ');
const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || ''; const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '';
const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || ''; const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '';
const managerHtml = [ const managerHtml = [
@@ -97,20 +94,19 @@ export function renderPcList(container: HTMLElement) {
].filter(v => v !== '').join(' / '); ].filter(v => v !== '').join(' / ');
tr.innerHTML = ` tr.innerHTML = `
<td style="text-align:center;">${idx+1}</td> <td class="text-center">${idx+1}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.CORP.key]}</td> <td class="text-center">${asset[ASSET_SCHEMA.CORP.key]}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.ORG.key]||'-'}</td> <td class="text-center">${asset[ASSET_SCHEMA.ORG.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.ASSET_CODE.key]}</td> <td class="text-center">${asset[ASSET_SCHEMA.ASSET_CODE.key]}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.USER.key]||''}</td> <td class="text-center">${asset[ASSET_SCHEMA.USER.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.LOCATION.key]||''}</td> <td class="text-center">${managerHtml || '-'}</td>
<td style="text-align:center;">${managerHtml || '-'}</td> <td class="text-center">${asset[ASSET_SCHEMA.MAINBOARD.key]||'-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.MAINBOARD.key]||'-'}</td> <td class="text-center">${asset[ASSET_SCHEMA.CPU.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.CPU.key]||''}</td> <td class="text-center">${asset[ASSET_SCHEMA.RAM.key]||''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.RAM.key]||''}</td> <td class="text-center">${formatInline(storage)}</td>
<td style="text-align:center;">${formatInline(storage)}</td> <td class="text-center">${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''}</td> <td class="text-right">${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()}</td>
<td style="text-align:right;">${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()}</td> <td class="text-center">${asset[ASSET_SCHEMA.DOC_NAME.key] ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td>
<td style="text-align:center;">${asset[ASSET_SCHEMA.DOC_NAME.key] ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td>
`; `;
tr.addEventListener('click', () => openHwModal(asset, 'view')); tr.addEventListener('click', () => openHwModal(asset, 'view'));
tbody.appendChild(tr); tbody.appendChild(tr);