refactor: rename asset_network to asset_remote

- DB 테이블명 변경 마이그레이션 스크립트 추가 (migrate_v5_rename_remote.js)

- Backend (server.js): 쿼리 및 매핑 로직을 asset_remote 및 remotes 속성으로 업데이트

- Frontend (HWModal.ts): 폼 필드와 데이터 바인딩을 remotes로 일괄 수정

- 유틸리티 스크립트의 레퍼런스 일괄 업데이트
This commit is contained in:
2026-06-09 18:44:53 +09:00
parent 2b9c965c91
commit 25ebaf4685
9 changed files with 797 additions and 96 deletions

59
backup_db.js Normal file
View File

@@ -0,0 +1,59 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import * as xlsx from 'xlsx';
import fs from 'fs';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function backup() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🚀 Starting Database Backup Process...');
const tables = [
'asset_pc', 'asset_server', 'asset_storage', 'asset_remote',
'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip'
];
const wb = xlsx.utils.book_new();
for (const table of tables) {
try {
// 1. Create table backup
await connection.query(`DROP TABLE IF EXISTS ${table}_backup`);
await connection.query(`CREATE TABLE ${table}_backup AS SELECT * FROM ${table}`);
console.log(`✅ Table backup created: ${table} -> ${table}_backup`);
// 2. Fetch data for Excel
const [rows] = await connection.query(`SELECT * FROM ${table}`);
if (rows.length > 0) {
const ws = xlsx.utils.json_to_sheet(rows);
// Sheet names max length is 31 chars
const sheetName = table.substring(0, 31);
xlsx.utils.book_append_sheet(wb, ws, sheetName);
}
} catch (e) {
console.warn(`⚠️ Skipped ${table}: ${e.message}`);
}
}
// 3. Write Excel file
const fileName = 'backupDB_20260608.xlsx';
xlsx.writeFile(wb, fileName);
console.log(`✅ Excel data exported successfully to ${fileName}`);
await connection.end();
}
backup().catch(err => {
console.error('❌ Backup Failed:', err);
process.exit(1);
});

29
check_network.js Normal file
View File

@@ -0,0 +1,29 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function checkRemote() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('--- Checking asset_remote table ---');
const [columns] = await connection.query('DESCRIBE asset_remote');
const cols = columns.map(c => c.Field);
console.log('Columns in asset_remote:', cols.join(', '));
const [count] = await connection.query('SELECT COUNT(*) as count FROM asset_remote WHERE remote_tool IS NOT NULL OR remote_id IS NOT NULL');
console.log(`Rows with remote info (tool or id): ${count[0].count}`);
await connection.end();
}
checkRemote().catch(console.error);

44
drop_legacy.js Normal file
View File

@@ -0,0 +1,44 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function dropLegacyTables() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🧹 Starting cleanup of obsolete legacy backup tables...');
const tablesToDrop = [
'asset_pc', 'asset_pc_backup',
'asset_server', 'asset_server_backup',
'asset_storage', 'asset_storage_backup',
'asset_remote_backup', // IMPORTANT: DO NOT drop asset_remote!
'asset_equipment', 'asset_equipment_backup',
'asset_office_supplies', 'asset_office_supplies_backup',
'asset_survey', 'asset_survey_backup',
'asset_vip', 'asset_vip_backup',
'asset_pc_parts'
];
for (const table of tablesToDrop) {
try {
await connection.query(`DROP TABLE IF EXISTS ${table}`);
console.log(`✅ Dropped table: ${table}`);
} catch (err) {
console.warn(`⚠️ Failed to drop table ${table}: ${err.message}`);
}
}
console.log('🎉 Cleanup complete. Database is now lean and mean.');
await connection.end();
}
dropLegacyTables().catch(console.error);

197
migrate_schema.js Normal file
View File

@@ -0,0 +1,197 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function migrateSchema() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🚀 Phase 1: Creating Normalized Tables & Migrating Data...');
try {
await connection.query('SET FOREIGN_KEY_CHECKS = 0');
// --- 1. Drop existing new tables if they exist ---
await connection.query('DROP TABLE IF EXISTS asset_core, asset_hardware, asset_location, asset_remote');
// --- 2. Create New Schema ---
await connection.query(`
CREATE TABLE asset_core (
id VARCHAR(50) PRIMARY KEY,
asset_code VARCHAR(100) UNIQUE NOT NULL,
category VARCHAR(100),
asset_type VARCHAR(100),
asset_purpose VARCHAR(255),
service_type VARCHAR(50),
purchase_corp VARCHAR(100),
purchase_date VARCHAR(50),
purchase_amount VARCHAR(100),
purchase_vendor VARCHAR(255),
approval_document VARCHAR(255),
memo TEXT,
manager_primary VARCHAR(100),
manager_secondary VARCHAR(100),
current_dept VARCHAR(255),
previous_dept VARCHAR(255),
user_current VARCHAR(100),
previous_user VARCHAR(100),
emp_no VARCHAR(20),
user_position VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE asset_hardware (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50) NOT NULL,
hw_status VARCHAR(50),
model_name VARCHAR(255),
mainboard VARCHAR(255),
os VARCHAR(100),
cpu VARCHAR(255),
ram VARCHAR(100),
gpu VARCHAR(100),
storage1 VARCHAR(255),
storage2 VARCHAR(255),
storage3 VARCHAR(255),
monitoring VARCHAR(100),
price VARCHAR(100),
volume VARCHAR(100),
monitor_inch VARCHAR(50),
serial_num VARCHAR(100),
FOREIGN KEY (asset_id) REFERENCES asset_core(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE asset_location (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50) NOT NULL,
location VARCHAR(255),
location_detail VARCHAR(255),
location_photo VARCHAR(255),
loc_x VARCHAR(20),
loc_y VARCHAR(20),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (asset_id) REFERENCES asset_core(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE asset_remote (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50) NOT NULL,
ip_address VARCHAR(100),
mac_address VARCHAR(100),
remote_tool VARCHAR(100),
remote_id VARCHAR(100),
remote_pw VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (asset_id) REFERENCES asset_core(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query('SET FOREIGN_KEY_CHECKS = 1');
console.log('✅ Normalized tables created.');
// --- 3. Migrate Data from Legacy Tables ---
const legacyTables = ['asset_pc', 'asset_server', 'asset_storage', 'asset_remote', 'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip'];
let totalMigrated = 0;
for (const table of legacyTables) {
try {
const [rows] = await connection.query(`SELECT * FROM ${table}`);
for (const row of rows) {
// 3.1 Insert into asset_core
await connection.query(`
INSERT IGNORE INTO asset_core (
id, asset_code, category, asset_type, asset_purpose, service_type, purchase_corp, purchase_date,
purchase_amount, purchase_vendor, approval_document, memo, manager_primary, manager_secondary,
current_dept, previous_dept, user_current, previous_user, emp_no, user_position, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
row.id, row.asset_code, row.category, row.asset_type, row.asset_purpose, row.service_type,
row.purchase_corp, row.purchase_date, row.purchase_amount, row.purchase_vendor, row.approval_document,
row.memo, row.manager_primary, row.manager_secondary, row.current_dept, row.previous_dept,
row.user_current, row.previous_user, row.emp_no, row.user_position, row.created_at
]);
// 3.2 Insert into asset_hardware (if hardware fields exist)
if (row.model_name || row.cpu || row.ram || row.hw_status) {
await connection.query(`
INSERT INTO asset_hardware (
asset_id, hw_status, model_name, mainboard, os, cpu, ram, gpu, storage1, storage2, storage3, monitoring, price, volume, monitor_inch, serial_num
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
row.id, row.hw_status, row.model_name, row.mainboard, row.os, row.cpu, row.ram, row.gpu,
row.ssd_1 || row.hdd_1, row.ssd_2 || row.hdd_2, row.hdd_3, row.monitoring, row.price,
row.volume, row.monitor_inch, row.serial_num
]);
}
// 3.3 Insert into asset_location (if location fields exist)
if (row.location || row.location_detail) {
await connection.query(`
INSERT INTO asset_location (
asset_id, location, location_detail, location_photo, loc_x, loc_y
) VALUES (?, ?, ?, ?, ?, ?)
`, [
row.id, row.location, row.location_detail, row.location_photo, row.loc_x, row.loc_y
]);
}
// 3.4 Insert into asset_remote (if network fields exist)
// Handle primary network interface
if (row.ip_address || row.mac_address || row.remote_tool) {
await connection.query(`
INSERT INTO asset_remote (
asset_id, ip_address, mac_address, remote_tool, remote_id, remote_pw
) VALUES (?, ?, ?, ?, ?, ?)
`, [
row.id, row.ip_address, row.mac_address, row.remote_tool, row.remote_id, row.remote_pw
]);
}
// Handle secondary network interface (e.g., from server table) if it exists
if (row.ip_address_2 || row.remote_tool_2) {
await connection.query(`
INSERT INTO asset_remote (
asset_id, ip_address, remote_tool, remote_id, remote_pw
) VALUES (?, ?, ?, ?, ?)
`, [
row.id, row.ip_address_2, row.remote_tool_2, row.remote_id_2, row.remote_pw_2
]);
}
totalMigrated++;
}
console.log(`- Migrated ${rows.length} records from ${table}`);
} catch (err) {
console.warn(`- Skipping legacy table ${table}: ${err.message}`);
}
}
console.log(`✅ Phase 1 Data Migration Completed. Total Assets Migrated: ${totalMigrated}`);
} catch (err) {
console.error('❌ Migration Failed:', err);
} finally {
await connection.end();
}
}
migrateSchema();

212
migrate_v2_final.js Normal file
View File

@@ -0,0 +1,212 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function migrateV2() {
const connection = await mysql.createConnection({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🚀 Phase 2: Final Migration to Normalized V2 Schema...');
try {
await connection.query('SET FOREIGN_KEY_CHECKS = 0');
// 1. Create/Enhance Core Tables
console.log('1. Creating/Enhancing Tables...');
await connection.query('DROP TABLE IF EXISTS asset_core, asset_hardware, asset_location, asset_remote');
await connection.query(`
CREATE TABLE asset_core (
id VARCHAR(50) PRIMARY KEY,
asset_code VARCHAR(100) UNIQUE NOT NULL,
category VARCHAR(100),
asset_type VARCHAR(100),
current_role VARCHAR(50) DEFAULT 'Normal' COMMENT 'Normal, Server, Personal, etc.',
asset_purpose VARCHAR(255),
service_type VARCHAR(50),
purchase_corp VARCHAR(100),
purchase_date VARCHAR(50),
purchase_amount VARCHAR(100),
purchase_vendor VARCHAR(255),
approval_document VARCHAR(255),
memo TEXT,
manager_primary VARCHAR(100),
manager_secondary VARCHAR(100),
current_dept VARCHAR(255),
previous_dept VARCHAR(255),
user_current VARCHAR(100),
previous_user VARCHAR(100),
emp_no VARCHAR(20),
user_position VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE asset_hardware (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50) NOT NULL,
hw_status VARCHAR(50),
model_name VARCHAR(255),
mainboard VARCHAR(255),
os VARCHAR(100),
cpu VARCHAR(255),
ram VARCHAR(100),
gpu VARCHAR(100),
storage1 VARCHAR(255),
storage2 VARCHAR(255),
storage3 VARCHAR(255),
storage4 VARCHAR(255),
monitoring VARCHAR(100),
price VARCHAR(100),
volume VARCHAR(100),
monitor_inch VARCHAR(50),
serial_num VARCHAR(100),
FOREIGN KEY (asset_id) REFERENCES asset_core(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE asset_location (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50) NOT NULL,
location VARCHAR(255),
location_detail VARCHAR(255),
location_photo VARCHAR(255),
loc_x VARCHAR(20),
loc_y VARCHAR(20),
is_active TINYINT(1) DEFAULT 1,
deactivated_at DATETIME NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (asset_id) REFERENCES asset_core(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await connection.query(`
CREATE TABLE asset_remote (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50) NOT NULL,
ip_address VARCHAR(100),
mac_address VARCHAR(100),
remote_tool VARCHAR(100),
remote_id VARCHAR(100),
remote_pw VARCHAR(100),
is_active TINYINT(1) DEFAULT 1,
deactivated_at DATETIME NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (asset_id) REFERENCES asset_core(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
console.log('✅ V2 Schema tables created.');
// 2. Migration Logic
const legacyTables = [
{ name: 'asset_pc', defaultRole: 'Personal' },
{ name: 'asset_server', defaultRole: 'Server' },
{ name: 'asset_storage', defaultRole: 'Normal' },
{ name: 'asset_equipment', defaultRole: 'Normal' },
{ name: 'asset_office_supplies', defaultRole: 'Normal' },
{ name: 'asset_survey', defaultRole: 'Normal' },
{ name: 'asset_vip', defaultRole: 'Normal' },
{ name: 'asset_pc_parts', defaultRole: 'Normal' }
];
let totalMigrated = 0;
for (const tableInfo of legacyTables) {
const table = tableInfo.name;
try {
const [rows] = await connection.query(`SELECT * FROM ${table}`);
console.log(`- Migrating ${rows.length} records from ${table}...`);
for (const row of rows) {
// 2.1 Insert into asset_core
const role = (table === 'asset_pc' && row.asset_type === '서버PC') ? 'Server' : tableInfo.defaultRole;
await connection.query(`
INSERT IGNORE INTO asset_core (
id, asset_code, category, asset_type, current_role, asset_purpose, service_type, purchase_corp, purchase_date,
purchase_amount, purchase_vendor, approval_document, memo, manager_primary, manager_secondary,
current_dept, previous_dept, user_current, previous_user, emp_no, user_position, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
row.id, row.asset_code, row.category, row.asset_type, role, row.asset_purpose, row.service_type,
row.purchase_corp, row.purchase_date, row.purchase_amount, row.purchase_vendor, row.approval_document,
row.memo, row.manager_primary, row.manager_secondary, row.current_dept, row.previous_dept,
row.user_current || row.current_user, row.previous_user, row.emp_no, row.user_position, row.created_at
]);
// 2.2 Insert into asset_hardware
await connection.query(`
INSERT INTO asset_hardware (
asset_id, hw_status, model_name, mainboard, os, cpu, ram, gpu, storage1, storage2, storage3, storage4, monitoring, price, volume, monitor_inch, serial_num
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
row.id, row.hw_status, row.model_name, row.mainboard, row.os, row.cpu, row.ram, row.gpu,
row.ssd_1 || row.storage1, row.ssd_2 || row.storage2, row.hdd_1 || row.storage3, row.hdd_2, row.monitoring, row.price,
row.volume, row.monitor_inch, row.serial_num
]);
// 2.3 Insert into asset_location
if (row.location || row.location_detail) {
await connection.query(`
INSERT INTO asset_location (
asset_id, location, location_detail, location_photo, loc_x, loc_y, is_active
) VALUES (?, ?, ?, ?, ?, ?, 1)
`, [
row.id, row.location, row.location_detail, row.location_photo, row.loc_x, row.loc_y
]);
}
// 2.4 Insert into asset_remote
// Primary Network
if (row.ip_address || row.mac_address || row.remote_tool) {
await connection.query(`
INSERT INTO asset_remote (
asset_id, ip_address, mac_address, remote_tool, remote_id, remote_pw, is_active
) VALUES (?, ?, ?, ?, ?, ?, 1)
`, [
row.id, row.ip_address, row.mac_address, row.remote_tool, row.remote_id, row.remote_pw
]);
}
// Secondary Network (for servers)
if (row.ip_address_2 || row.remote_tool_2) {
await connection.query(`
INSERT INTO asset_remote (
asset_id, ip_address, remote_tool, remote_id, remote_pw, is_active
) VALUES (?, ?, ?, ?, ?, 1)
`, [
row.id, row.ip_address_2, row.remote_tool_2, row.remote_id_2, row.remote_pw_2
]);
}
totalMigrated++;
}
} catch (err) {
console.warn(`- Skipping table ${table}: ${err.message}`);
}
}
await connection.query('SET FOREIGN_KEY_CHECKS = 1');
console.log(`✅ Phase 2 Data Migration Completed. Total Assets Migrated: ${totalMigrated}`);
} catch (err) {
console.error('❌ Migration Failed:', err);
} finally {
await connection.end();
}
}
migrateV2();

73
migrate_v4_network.js Normal file
View File

@@ -0,0 +1,73 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306'),
});
async function migrate() {
const conn = await pool.getConnection();
try {
console.log('1. Creating asset_remote_v4 table...');
await conn.query(`
CREATE TABLE IF NOT EXISTS asset_remote_v4 (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(50) NOT NULL,
net_type VARCHAR(20) NOT NULL, /* 'IP' or 'REMOTE' */
net_name VARCHAR(100), /* e.g., '기본망', 'AnyDesk' */
net_value1 VARCHAR(100), /* IP or ID */
net_value2 VARCHAR(100), /* MAC or PW */
is_active TINYINT(1) DEFAULT 1,
deactivated_at DATETIME NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (asset_id) REFERENCES asset_core(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
console.log('2. Migrating data from asset_remote...');
const [oldRows] = await conn.query('SELECT * FROM asset_remote WHERE is_active = 1');
let ipCount = 0;
let remoteCount = 0;
for (const row of oldRows) {
// Migrating IP/MAC
if (row.ip_address || row.mac_address) {
await conn.query(
'INSERT INTO asset_remote_v4 (asset_id, net_type, net_name, net_value1, net_value2, created_at) VALUES (?, ?, ?, ?, ?, ?)',
[row.asset_id, 'IP', '기본망', row.ip_address, row.mac_address, row.created_at]
);
ipCount++;
}
// Migrating Remote
if (row.remote_tool || row.remote_id || row.remote_pw) {
await conn.query(
'INSERT INTO asset_remote_v4 (asset_id, net_type, net_name, net_value1, net_value2, created_at) VALUES (?, ?, ?, ?, ?, ?)',
[row.asset_id, 'REMOTE', row.remote_tool, row.remote_id, row.remote_pw, row.created_at]
);
remoteCount++;
}
}
console.log(`Migrated ${ipCount} IP records and ${remoteCount} Remote records.`);
console.log('3. Renaming tables...');
await conn.query('DROP TABLE IF EXISTS asset_remote_legacy');
await conn.query('RENAME TABLE asset_remote TO asset_remote_legacy, asset_remote_v4 TO asset_remote;');
console.log('✅ Migration V4 (Remote) Complete.');
} catch (e) {
console.error('Migration failed:', e);
} finally {
conn.release();
pool.end();
}
}
migrate();

View File

@@ -0,0 +1,28 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306'),
});
async function migrate() {
const conn = await pool.getConnection();
try {
console.log('1. Renaming asset_network to asset_remote...');
await conn.query('RENAME TABLE asset_network TO asset_remote');
console.log('✅ Table renamed successfully.');
} catch (e) {
console.error('Migration failed:', e);
} finally {
conn.release();
pool.end();
}
}
migrate();

View File

@@ -39,7 +39,7 @@ const CATEGORY_TABLE_MAP = {
pc: 'asset_pc', pc: 'asset_pc',
server: 'asset_server', server: 'asset_server',
storage: 'asset_storage', storage: 'asset_storage',
network: 'asset_network', network: 'asset_remote',
equipment: 'asset_equipment', equipment: 'asset_equipment',
officeSupplies: 'asset_office_supplies', officeSupplies: 'asset_office_supplies',
survey: 'asset_survey', survey: 'asset_survey',
@@ -53,7 +53,7 @@ const CATEGORY_TABLE_MAP = {
}; };
const ASSET_TABLES = [ const ASSET_TABLES = [
'asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_pc', 'asset_server', 'asset_storage', 'asset_remote',
'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip' 'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip'
]; ];
@@ -115,7 +115,10 @@ app.get('/api/assets/master', async (req, res) => {
s.hw_status, s.model_name, s.mainboard, s.os, s.cpu, s.ram, s.gpu, s.hw_status, s.model_name, s.mainboard, s.os, s.cpu, s.ram, s.gpu,
s.monitoring, s.price, s.monitor_inch, s.serial_num, s.monitoring, s.price, s.monitor_inch, s.serial_num,
l.location, l.location_detail, l.location_photo, l.loc_x, l.loc_y, l.location, l.location_detail, l.location_photo, l.loc_x, l.loc_y,
n.ip_address, n.mac_address, n.remote_tool, n.remote_id, n.remote_pw, (
SELECT JSON_ARRAYAGG(JSON_OBJECT('type', net_type, 'name', net_name, 'val1', net_value1, 'val2', net_value2))
FROM asset_remote WHERE asset_id = c.id AND is_active = 1
) as remotes,
( (
SELECT JSON_ARRAYAGG(JSON_OBJECT('type', disk_type, 'capacity', capacity, 'unit', unit, 'slot', slot_no)) SELECT JSON_ARRAYAGG(JSON_OBJECT('type', disk_type, 'capacity', capacity, 'unit', unit, 'slot', slot_no))
FROM asset_volume WHERE asset_id = c.id FROM asset_volume WHERE asset_id = c.id
@@ -127,11 +130,6 @@ app.get('/api/assets/master', async (req, res) => {
WHERE asset_id = c.id AND is_active = 1 WHERE asset_id = c.id AND is_active = 1
ORDER BY created_at DESC LIMIT 1 ORDER BY created_at DESC LIMIT 1
) )
LEFT JOIN asset_network n ON n.id = (
SELECT id FROM asset_network
WHERE asset_id = c.id AND is_active = 1
ORDER BY created_at DESC LIMIT 1
)
`); `);
const catMap = { const catMap = {
@@ -223,14 +221,36 @@ app.post('/api/asset/:category/save', async (req, res) => {
} }
} }
// 3.5 asset_network // 3.5 asset_remote (Dynamic Array Logic)
if (asset.remotes) {
try {
let nets = typeof asset.remotes === 'string' ? JSON.parse(asset.remotes) : asset.remotes;
if (Array.isArray(nets)) {
await connection.query('UPDATE asset_remote SET is_active = 0, deactivated_at = NOW() WHERE asset_id = ? AND is_active = 1', [asset.id]);
for (const n of nets) {
if (n.type) {
await connection.query(
'INSERT INTO asset_remote (asset_id, net_type, net_name, net_value1, net_value2, is_active) VALUES (?, ?, ?, ?, ?, 1)',
[asset.id, n.type, n.name || '', n.val1 || '', n.val2 || '']
);
}
}
}
} catch(e) { console.error('Remote data parse error', e); }
} else {
// Fallback for UI that hasn't sent the networks array yet
if (asset.ip_address || asset.mac_address || asset.remote_tool) { if (asset.ip_address || asset.mac_address || asset.remote_tool) {
const [netActive] = await connection.query('SELECT * FROM asset_network WHERE asset_id = ? AND is_active = 1', [asset.id]); const [netActive] = await connection.query('SELECT * FROM asset_remote WHERE asset_id = ? AND is_active = 1', [asset.id]);
const isChanged = netActive.length === 0 || netActive[0].ip_address !== asset.ip_address || netActive[0].mac_address !== asset.mac_address || netActive[0].remote_tool !== asset.remote_tool || netActive[0].remote_id !== asset.remote_id || netActive[0].remote_pw !== asset.remote_pw; const isChanged = netActive.length === 0 || netActive[0].net_value1 !== asset.ip_address || netActive[0].net_value2 !== asset.mac_address || netActive[0].net_name !== asset.remote_tool;
if (isChanged) { if (isChanged) {
await connection.query('UPDATE asset_network SET is_active = 0, deactivated_at = NOW() WHERE asset_id = ? AND is_active = 1', [asset.id]); await connection.query('UPDATE asset_remote SET is_active = 0, deactivated_at = NOW() WHERE asset_id = ? AND is_active = 1', [asset.id]);
await connection.query(`INSERT INTO asset_network (asset_id, ip_address, mac_address, remote_tool, remote_id, remote_pw, is_active) VALUES (?, ?, ?, ?, ?, ?, 1)`, if (asset.ip_address || asset.mac_address) {
[asset.id, asset.ip_address, asset.mac_address, asset.remote_tool, asset.remote_id, asset.remote_pw]); await connection.query('INSERT INTO asset_remote (asset_id, net_type, net_name, net_value1, net_value2, is_active) VALUES (?, ?, ?, ?, ?, 1)', [asset.id, 'IP', '기본망', asset.ip_address, asset.mac_address]);
}
if (asset.remote_tool || asset.remote_id || asset.remote_pw) {
await connection.query('INSERT INTO asset_remote (asset_id, net_type, net_name, net_value1, net_value2, is_active) VALUES (?, ?, ?, ?, ?, 1)', [asset.id, 'REMOTE', asset.remote_tool, asset.remote_id, asset.remote_pw]);
}
}
} }
} }
@@ -272,7 +292,7 @@ app.delete('/api/asset/:category/:id', async (req, res) => {
try { try {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
// For asset_core, ON DELETE CASCADE will handle spec, location, network, volume // For asset_core, ON DELETE CASCADE will handle spec, location, remote, volume
await connection.query(`DELETE FROM ${table} WHERE id = ?`, [id]); await connection.query(`DELETE FROM ${table} WHERE id = ?`, [id]);
connection.release(); connection.release();
console.log(`🗑️ [ASSET DELETE] Category: ${category}, ID: ${id}`); console.log(`🗑️ [ASSET DELETE] Category: ${category}, ID: ${id}`);

View File

@@ -35,8 +35,9 @@ class HwAssetModal extends BaseModal {
<div class="modal-form-area"> <div class="modal-form-area">
<form id="hw-asset-form" class="grid-form"> <form id="hw-asset-form" class="grid-form">
<input type="hidden" id="hw-id" name="id" /> <input type="hidden" id="hw-id" name="id" />
<input type="hidden" id="hw-remotes-data" name="remotes" />
<!-- [SECTION 1] 기본 관리 정보 (필수 공통) --> <!-- [SECTION 1] 기본 관리 정보 -->
<div class="form-section-title" style="padding-top: 0; margin-bottom: 12px;">기본 관리 정보</div> <div class="form-section-title" style="padding-top: 0; margin-bottom: 12px;">기본 관리 정보</div>
<div class="form-group"> <div class="form-group">
<label>${ASSET_SCHEMA.ASSET_CODE.ui}</label> <label>${ASSET_SCHEMA.ASSET_CODE.ui}</label>
@@ -101,8 +102,8 @@ class HwAssetModal extends BaseModal {
<input type="text" id="hw-previous_user" name="previous_user" style="${inputStyle}" /> <input type="text" id="hw-previous_user" name="previous_user" style="${inputStyle}" />
</div> </div>
<!-- [SECTION 3] 하드웨어 사양 및 네트워크 --> <!-- [SECTION 3] 하드웨어 사양 -->
<div class="form-section-title hardware-section" style="margin-top: 24px; margin-bottom: 12px;">시스템 사양 및 네트워크</div> <div class="form-section-title hardware-section" style="margin-top: 24px; margin-bottom: 12px;">시스템 사양 정보</div>
<div class="form-group"> <div class="form-group">
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label> <label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
<input type="text" id="hw-model_name" name="model_name" style="${inputStyle}" /> <input type="text" id="hw-model_name" name="model_name" style="${inputStyle}" />
@@ -135,25 +136,6 @@ class HwAssetModal extends BaseModal {
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label> <label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
<input type="text" id="hw-mainboard" name="mainboard" style="${inputStyle}" /> <input type="text" id="hw-mainboard" name="mainboard" style="${inputStyle}" />
</div> </div>
<!-- 동적 디스크 할당 영역 (Plan B) -->
<div class="form-group spec-only full-width" style="grid-column: span 2;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<label style="margin: 0; font-size: 11px; font-weight: 700; color: var(--text-muted);">저장장치 (디스크)</label>
<button type="button" id="btn-add-volume" class="btn btn-outline" style="height: 26px !important; padding: 0 10px; font-size: 11px; display: none;">+ 디스크 추가</button>
</div>
<div id="hw-volume-container" style="display: flex; flex-direction: column; gap: 8px;"></div>
<input type="hidden" id="hw-volumes-data" name="volumes" />
</div>
<div class="form-group net-only">
<label>${ASSET_SCHEMA.IP_ADDR.ui}</label>
<input type="text" id="hw-ip_address" name="ip_address" style="${inputStyle}" />
</div>
<div class="form-group net-only">
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
<input type="text" id="hw-mac_address" name="mac_address" style="${inputStyle}" />
</div>
<div class="form-group monitor-only"> <div class="form-group monitor-only">
<label>${ASSET_SCHEMA.MONITOR_INCH.ui}</label> <label>${ASSET_SCHEMA.MONITOR_INCH.ui}</label>
<input type="text" id="hw-monitor_inch" name="monitor_inch" style="${inputStyle}" /> <input type="text" id="hw-monitor_inch" name="monitor_inch" style="${inputStyle}" />
@@ -167,26 +149,37 @@ class HwAssetModal extends BaseModal {
<input type="text" id="hw-asset_count" name="asset_count" style="${inputStyle}" /> <input type="text" id="hw-asset_count" name="asset_count" style="${inputStyle}" />
</div> </div>
<!-- [SECTION 4] 원격 접속 정보 (서버 전용) --> <!-- 동적 디스크 할당 영역 -->
<div class="form-section-title remote-section" style="margin-top: 24px; margin-bottom: 12px;">원격 접속 정보</div> <div class="form-group spec-only full-width" style="grid-column: span 2;">
<div class="form-group remote-field"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<label>${ASSET_SCHEMA.IP_ADDR2.ui}</label> <label style="margin: 0; font-size: 11px; font-weight: 700; color: var(--text-muted);">저장장치 (디스크)</label>
<input type="text" id="hw-ip_address_2" name="ip_address_2" style="${inputStyle}" /> <button type="button" id="btn-add-volume" class="btn btn-outline" style="height: 26px !important; padding: 0 10px; font-size: 11px; display: none;">+ 디스크 추가</button>
</div> </div>
<div class="form-group remote-field"> <div id="hw-volume-container" style="display: flex; flex-direction: column; gap: 8px;"></div>
<label>${ASSET_SCHEMA.REMOTE_TOOL.ui}</label> <input type="hidden" id="hw-volumes-data" name="volumes" />
<input type="text" id="hw-remote_tool" name="remote_tool" style="${inputStyle}" />
</div>
<div class="form-group remote-field">
<label>${ASSET_SCHEMA.REMOTE_ID.ui}</label>
<input type="text" id="hw-remote_id" name="remote_id" style="${inputStyle}" />
</div>
<div class="form-group remote-field">
<label>${ASSET_SCHEMA.REMOTE_PW.ui}</label>
<input type="text" id="hw-remote_pw" name="remote_pw" style="${inputStyle}" />
</div> </div>
<!-- [SECTION 5] 설치 위치 (인프라/실물 장비 전용) --> <!-- [SECTION 4] 동적 네트워크 (IP/MAC) 정보 -->
<div class="form-section-title net-only" style="margin-top: 24px; margin-bottom: 12px;">네트워크 인터페이스</div>
<div class="form-group net-only full-width" style="grid-column: span 2;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<label style="margin: 0; font-size: 11px; font-weight: 700; color: var(--text-muted);">IP / MAC 정보</label>
<button type="button" id="btn-add-network" class="btn btn-outline" style="height: 26px !important; padding: 0 10px; font-size: 11px; display: none;">+ 인터페이스 추가</button>
</div>
<div id="hw-network-container" style="display: flex; flex-direction: column; gap: 8px;"></div>
</div>
<!-- [SECTION 5] 동적 원격 접속 정보 -->
<div class="form-section-title remote-section" style="margin-top: 24px; margin-bottom: 12px;">원격 접속 정보</div>
<div class="form-group remote-field full-width" style="grid-column: span 2;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<label style="margin: 0; font-size: 11px; font-weight: 700; color: var(--text-muted);">접속 도구 및 인증 정보</label>
<button type="button" id="btn-add-remote" class="btn btn-outline" style="height: 26px !important; padding: 0 10px; font-size: 11px; display: none;">+ 접속 도구 추가</button>
</div>
<div id="hw-remote-container" style="display: flex; flex-direction: column; gap: 8px;"></div>
</div>
<!-- [SECTION 6] 설치 위치 -->
<div class="form-section-title location-section" style="margin-top: 24px; margin-bottom: 12px;">설치 위치</div> <div class="form-section-title location-section" style="margin-top: 24px; margin-bottom: 12px;">설치 위치</div>
<div class="form-group location-field"> <div class="form-group location-field">
<label>건물/위치</label> <label>건물/위치</label>
@@ -202,7 +195,7 @@ class HwAssetModal extends BaseModal {
<input type="hidden" id="hw-loc_x" name="loc_x" /><input type="hidden" id="hw-loc_y" name="loc_y" /><input type="hidden" id="hw-location_photo" name="location_photo" /> <input type="hidden" id="hw-loc_x" name="loc_x" /><input type="hidden" id="hw-loc_y" name="loc_y" /><input type="hidden" id="hw-location_photo" name="location_photo" />
</div> </div>
<!-- [SECTION 6] 구매 및 증빙 (공통) --> <!-- [SECTION 7] 구매 및 증빙 -->
<div class="form-section-title" style="margin-top: 24px; margin-bottom: 12px;">구매 및 증빙 정보</div> <div class="form-section-title" style="margin-top: 24px; margin-bottom: 12px;">구매 및 증빙 정보</div>
<div class="form-group"> <div class="form-group">
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label> <label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
@@ -331,9 +324,10 @@ class HwAssetModal extends BaseModal {
this.toggleEditOnlyBtns(false); this.toggleEditOnlyBtns(false);
}); });
// 동적 볼륨 추가 기능 연결 // 동적 기능 이벤트 연결
const btnAddVolume = document.getElementById('btn-add-volume')!; document.getElementById('btn-add-volume')?.addEventListener('click', () => this.addVolumeRow());
btnAddVolume.addEventListener('click', () => this.addVolumeRow()); document.getElementById('btn-add-network')?.addEventListener('click', () => this.addNetworkRow());
document.getElementById('btn-add-remote')?.addEventListener('click', () => this.addRemoteRow());
const fileInput = document.getElementById('hw-approval_document_file') as HTMLInputElement; const fileInput = document.getElementById('hw-approval_document_file') as HTMLInputElement;
const fileNameDisplay = document.getElementById('hw-file-name-display'); const fileNameDisplay = document.getElementById('hw-file-name-display');
@@ -374,7 +368,7 @@ class HwAssetModal extends BaseModal {
return; return;
} }
// 동적 볼륨 데이터 수집 및 배열 생성 // 동적 볼륨 데이터 수집
const vols: any[] = []; const vols: any[] = [];
document.querySelectorAll('#hw-volume-container .volume-row').forEach((row, idx) => { document.querySelectorAll('#hw-volume-container .volume-row').forEach((row, idx) => {
const type = (row.querySelector('.vol-type') as HTMLSelectElement).value; const type = (row.querySelector('.vol-type') as HTMLSelectElement).value;
@@ -384,6 +378,22 @@ class HwAssetModal extends BaseModal {
}); });
setFieldValue('hw-volumes-data', JSON.stringify(vols)); setFieldValue('hw-volumes-data', JSON.stringify(vols));
// 동적 네트워크/원격 데이터 수집
const nets: any[] = [];
document.querySelectorAll('#hw-network-container .net-row').forEach(row => {
const name = (row.querySelector('.net-name') as HTMLInputElement).value;
const ip = (row.querySelector('.net-ip') as HTMLInputElement).value;
const mac = (row.querySelector('.net-mac') as HTMLInputElement).value;
if (ip || mac) nets.push({ type: 'IP', name, val1: ip, val2: mac });
});
document.querySelectorAll('#hw-remote-container .remote-row').forEach(row => {
const name = (row.querySelector('.rem-name') as HTMLInputElement).value;
const id = (row.querySelector('.rem-id') as HTMLInputElement).value;
const pw = (row.querySelector('.rem-pw') as HTMLInputElement).value;
if (name || id) nets.push({ type: 'REMOTE', name, val1: id, val2: pw });
});
setFieldValue('hw-remotes-data', JSON.stringify(nets));
const formData = new FormData(this.formEl!); const formData = new FormData(this.formEl!);
const updated = { ...this.currentAsset }; const updated = { ...this.currentAsset };
formData.forEach((value, key) => { if (key !== 'id') updated[key] = value; }); formData.forEach((value, key) => { if (key !== 'id') updated[key] = value; });
@@ -399,15 +409,10 @@ class HwAssetModal extends BaseModal {
private addVolumeRow(vol: any = { type: 'SSD', capacity: '', unit: 'GB' }) { private addVolumeRow(vol: any = { type: 'SSD', capacity: '', unit: 'GB' }) {
const container = document.getElementById('hw-volume-container'); const container = document.getElementById('hw-volume-container');
if (!container) return; if (!container) return;
const row = document.createElement('div'); const row = document.createElement('div');
row.style.display = 'flex';
row.style.gap = '8px';
row.style.alignItems = 'center';
row.className = 'volume-row'; row.className = 'volume-row';
row.style.display = 'flex'; row.style.gap = '8px'; row.style.alignItems = 'center';
const inputStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0; padding: 0 8px;'; const inputStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0; padding: 0 8px;';
row.innerHTML = ` row.innerHTML = `
<select class="vol-type" style="${inputStyle} width: 80px;" ${!this.isEditMode ? 'disabled' : ''}> <select class="vol-type" style="${inputStyle} width: 80px;" ${!this.isEditMode ? 'disabled' : ''}>
<option value="SSD" ${vol.type === 'SSD' ? 'selected' : ''}>SSD</option> <option value="SSD" ${vol.type === 'SSD' ? 'selected' : ''}>SSD</option>
@@ -419,16 +424,51 @@ class HwAssetModal extends BaseModal {
<option value="GB" ${vol.unit === 'GB' ? 'selected' : ''}>GB</option> <option value="GB" ${vol.unit === 'GB' ? 'selected' : ''}>GB</option>
<option value="TB" ${vol.unit === 'TB' ? 'selected' : ''}>TB</option> <option value="TB" ${vol.unit === 'TB' ? 'selected' : ''}>TB</option>
</select> </select>
<button type="button" class="btn btn-outline btn-remove-vol edit-only-btn" style="height: 38px !important; padding: 0 12px; color: #E11D48; border-color: #E11D48; display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button> <button type="button" class="btn btn-outline btn-remove-row edit-only-btn" style="height: 38px !important; padding: 0 12px; color: #E11D48; border-color: #E11D48; display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button>
`; `;
row.querySelector('.btn-remove-row')?.addEventListener('click', () => row.remove());
container.appendChild(row);
}
row.querySelector('.btn-remove-vol')?.addEventListener('click', () => row.remove()); private addNetworkRow(net: any = { name: '기본망', val1: '', val2: '' }) {
const container = document.getElementById('hw-network-container');
if (!container) return;
const row = document.createElement('div');
row.className = 'net-row';
row.style.display = 'flex'; row.style.gap = '8px'; row.style.alignItems = 'center';
const inputStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0; padding: 0 8px;';
row.innerHTML = `
<input type="text" class="net-name" value="${net.name || ''}" placeholder="망구분(내부망)" style="${inputStyle} width: 100px;" ${!this.isEditMode ? 'readonly' : ''} />
<input type="text" class="net-ip" value="${net.val1 || ''}" placeholder="IP 주소" style="${inputStyle} flex: 1;" ${!this.isEditMode ? 'readonly' : ''} />
<input type="text" class="net-mac" value="${net.val2 || ''}" placeholder="MAC 주소" style="${inputStyle} flex: 1;" ${!this.isEditMode ? 'readonly' : ''} />
<button type="button" class="btn btn-outline btn-remove-row edit-only-btn" style="height: 38px !important; padding: 0 12px; color: #E11D48; border-color: #E11D48; display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button>
`;
row.querySelector('.btn-remove-row')?.addEventListener('click', () => row.remove());
container.appendChild(row);
}
private addRemoteRow(rem: any = { name: '', val1: '', val2: '' }) {
const container = document.getElementById('hw-remote-container');
if (!container) return;
const row = document.createElement('div');
row.className = 'remote-row';
row.style.display = 'flex'; row.style.gap = '8px'; row.style.alignItems = 'center';
const inputStyle = 'height: 38px !important; box-sizing: border-box !important; font-size: 13px; margin: 0; padding: 0 8px;';
row.innerHTML = `
<input type="text" class="rem-name" value="${rem.name || ''}" placeholder="접속 도구(AnyDesk)" style="${inputStyle} flex: 1;" ${!this.isEditMode ? 'readonly' : ''} />
<input type="text" class="rem-id" value="${rem.val1 || ''}" placeholder="접속 ID" style="${inputStyle} flex: 1;" ${!this.isEditMode ? 'readonly' : ''} />
<input type="text" class="rem-pw" value="${rem.val2 || ''}" placeholder="접속 PW" style="${inputStyle} flex: 1;" ${!this.isEditMode ? 'readonly' : ''} />
<button type="button" class="btn btn-outline btn-remove-row edit-only-btn" style="height: 38px !important; padding: 0 12px; color: #E11D48; border-color: #E11D48; display: ${this.isEditMode ? 'inline-flex' : 'none'};">&times;</button>
`;
row.querySelector('.btn-remove-row')?.addEventListener('click', () => row.remove());
container.appendChild(row); container.appendChild(row);
} }
private toggleEditOnlyBtns(isEdit: boolean) { private toggleEditOnlyBtns(isEdit: boolean) {
const addBtn = document.getElementById('btn-add-volume'); ['btn-add-volume', 'btn-add-network', 'btn-add-remote'].forEach(id => {
if (addBtn) addBtn.style.display = isEdit ? 'inline-flex' : 'none'; const btn = document.getElementById(id);
if (btn) btn.style.display = isEdit ? 'inline-flex' : 'none';
});
document.querySelectorAll('.edit-only-btn').forEach(btn => { document.querySelectorAll('.edit-only-btn').forEach(btn => {
(btn as HTMLElement).style.display = isEdit ? 'inline-flex' : 'none'; (btn as HTMLElement).style.display = isEdit ? 'inline-flex' : 'none';
}); });
@@ -458,23 +498,34 @@ class HwAssetModal extends BaseModal {
setFieldValue('hw-gpu', asset.gpu || ''); setFieldValue('hw-gpu', asset.gpu || '');
setFieldValue('hw-mainboard', asset.mainboard || ''); setFieldValue('hw-mainboard', asset.mainboard || '');
// 동적 볼륨 렌더링 초기화 및 생성 // 동적 볼륨 렌더링
const volumeContainer = document.getElementById('hw-volume-container'); const volumeContainer = document.getElementById('hw-volume-container');
if (volumeContainer) { if (volumeContainer) {
volumeContainer.innerHTML = ''; volumeContainer.innerHTML = '';
let vols = []; let vols = [];
try { try { vols = asset.volumes ? (typeof asset.volumes === 'string' ? JSON.parse(asset.volumes) : asset.volumes) : []; } catch(e) {}
vols = asset.volumes ? (typeof asset.volumes === 'string' ? JSON.parse(asset.volumes) : asset.volumes) : [];
} catch(e) {}
vols.forEach((v: any) => this.addVolumeRow(v)); vols.forEach((v: any) => this.addVolumeRow(v));
} }
setFieldValue('hw-ip_address', asset.ip_address || ''); // 동적 네트워크 및 원격 렌더링
setFieldValue('hw-ip_address_2', asset.ip_address_2 || ''); const netContainer = document.getElementById('hw-network-container');
setFieldValue('hw-mac_address', asset.mac_address || ''); const remContainer = document.getElementById('hw-remote-container');
setFieldValue('hw-remote_tool', asset.remote_tool || ''); if (netContainer) netContainer.innerHTML = '';
setFieldValue('hw-remote_id', asset.remote_id || ''); if (remContainer) remContainer.innerHTML = '';
setFieldValue('hw-remote_pw', asset.remote_pw || ''); let nets = [];
try { nets = asset.remotes ? (typeof asset.remotes === 'string' ? JSON.parse(asset.remotes) : asset.remotes) : []; } catch(e) {}
// Fallback: 서버에서 배열을 안 줬지만 기존 평탄화 데이터가 있는 경우
if (nets.length === 0 && (asset.ip_address || asset.remote_tool)) {
if (asset.ip_address || asset.mac_address) nets.push({ type: 'IP', name: '기본망', val1: asset.ip_address || '', val2: asset.mac_address || '' });
if (asset.remote_tool || asset.remote_id) nets.push({ type: 'REMOTE', name: asset.remote_tool || '', val1: asset.remote_id || '', val2: asset.remote_pw || '' });
}
nets.forEach((n: any) => {
if (n.type === 'IP') this.addNetworkRow(n);
else if (n.type === 'REMOTE') this.addRemoteRow(n);
});
setFieldValue('hw-monitoring', asset.monitoring || '비대상'); setFieldValue('hw-monitoring', asset.monitoring || '비대상');
setFieldValue('hw-serial_num', asset.serial_num || ''); setFieldValue('hw-serial_num', asset.serial_num || '');
setFieldValue('hw-monitor_inch', asset.monitor_inch || ''); setFieldValue('hw-monitor_inch', asset.monitor_inch || '');
@@ -484,6 +535,7 @@ class HwAssetModal extends BaseModal {
setFieldValue('hw-purchase_vendor', asset.purchase_vendor || ''); setFieldValue('hw-purchase_vendor', asset.purchase_vendor || '');
setFieldValue('hw-purchase_amount', asset.purchase_amount || ''); setFieldValue('hw-purchase_amount', asset.purchase_amount || '');
setFieldValue('hw-approval_document', asset.approval_document || ''); setFieldValue('hw-approval_document', asset.approval_document || '');
const docName = document.getElementById('hw-file-name-display'); const docName = document.getElementById('hw-file-name-display');
if (docName) docName.textContent = asset.approval_document ? asset.approval_document.split('/').pop() : '파일 선택...'; if (docName) docName.textContent = asset.approval_document ? asset.approval_document.split('/').pop() : '파일 선택...';
const fileLinkContainer = document.getElementById('hw-file-link-container'); const fileLinkContainer = document.getElementById('hw-file-link-container');
@@ -492,6 +544,7 @@ class HwAssetModal extends BaseModal {
} else if (fileLinkContainer) { } else if (fileLinkContainer) {
fileLinkContainer.innerHTML = ''; fileLinkContainer.innerHTML = '';
} }
setFieldValue('hw-memo', asset.memo || ''); setFieldValue('hw-memo', asset.memo || '');
setFieldValue('hw-location_detail', asset.location_detail || ''); setFieldValue('hw-location_detail', asset.location_detail || '');
setFieldValue('hw-loc_x', asset.loc_x || ''); setFieldValue('hw-loc_x', asset.loc_x || '');
@@ -520,32 +573,18 @@ class HwAssetModal extends BaseModal {
const category = (document.getElementById('hw-category') as HTMLSelectElement)?.value || ''; const category = (document.getElementById('hw-category') as HTMLSelectElement)?.value || '';
const type = (document.getElementById('hw-asset_type') as HTMLSelectElement)?.value || ''; const type = (document.getElementById('hw-asset_type') as HTMLSelectElement)?.value || '';
// 인프라 장비 (서버, 저장매체, 네트워크, 보안장비, 공간정보장비, 서버PC)
const infraCategories = ['서버', '저장매체', '네트워크', '보안장비', '공간정보장비']; const infraCategories = ['서버', '저장매체', '네트워크', '보안장비', '공간정보장비'];
const isInfra = infraCategories.includes(category) || type.includes('서버') || type.includes('저장시스템'); const isInfra = infraCategories.includes(category) || type.includes('서버') || type.includes('저장시스템');
// 개인 장비 (PC, 노트북, 모바일, 태블릿) - '서버PC'는 제외
const personalCategories = ['PC', '노트북', '모바일', '태블릿']; const personalCategories = ['PC', '노트북', '모바일', '태블릿'];
const isPersonal = (personalCategories.includes(category) || type.includes('개인PC') || type.includes('노트북')) && !type.includes('서버PC'); const isPersonal = (personalCategories.includes(category) || type.includes('개인PC') || type.includes('노트북')) && !type.includes('서버PC');
// 시스템 사양 (PC, 서버 등)
const specCategories = ['PC', '서버', '노트북', '스토리지', '워크스테이션']; const specCategories = ['PC', '서버', '노트북', '스토리지', '워크스테이션'];
const hasSpec = specCategories.includes(category) || type.includes('서버PC'); const hasSpec = specCategories.includes(category) || type.includes('서버PC');
// 네트워크 정보 (IP/MAC)
const noNetCategories = ['저장매체', '네트워크', '공간정보장비', 'PC부품', '사무가구']; const noNetCategories = ['저장매체', '네트워크', '공간정보장비', 'PC부품', '사무가구'];
const showNet = (isInfra || isPersonal) && !noNetCategories.includes(category); const showNet = (isInfra || isPersonal) && !noNetCategories.includes(category);
// 시리얼 번호
const hasSN = !['사무가구', 'PC부품'].includes(category); const hasSN = !['사무가구', 'PC부품'].includes(category);
// 수량/용량 전용 (부품)
const isParts = ['PC부품', '사무가구'].includes(category); const isParts = ['PC부품', '사무가구'].includes(category);
// 원격 접속 (서버 전용)
const showRemote = category === '서버' || type.includes('서버'); const showRemote = category === '서버' || type.includes('서버');
// JS에서 display: block 강제 대신 빈 문자열 할당하여 네이티브 CSS flex 활용
document.querySelectorAll('.remote-section, .remote-field, .monitoring-field').forEach(el => (el as HTMLElement).style.display = showRemote ? '' : 'none'); document.querySelectorAll('.remote-section, .remote-field, .monitoring-field').forEach(el => (el as HTMLElement).style.display = showRemote ? '' : 'none');
document.querySelectorAll('.net-only').forEach(el => (el as HTMLElement).style.display = showNet ? '' : 'none'); document.querySelectorAll('.net-only').forEach(el => (el as HTMLElement).style.display = showNet ? '' : 'none');
document.querySelectorAll('.spec-only').forEach(el => (el as HTMLElement).style.display = hasSpec ? '' : 'none'); document.querySelectorAll('.spec-only').forEach(el => (el as HTMLElement).style.display = hasSpec ? '' : 'none');