190 lines
8.0 KiB
JavaScript
190 lines
8.0 KiB
JavaScript
const mysql = require('mysql2/promise');
|
||
const fs = require('fs');
|
||
require('dotenv').config();
|
||
|
||
function getCleanMapKey(path) {
|
||
let clean = path.replace('img/location_photo/', '').replace('.png', '');
|
||
clean = clean.replace('서관', 'W').replace('동관', 'E');
|
||
clean = clean.replace('한맥빌딩/MDF실/MDF_', 'HAN-MDF-');
|
||
clean = clean.replace('기술개발센터/서버실/서버실_', 'DEV-SVR-');
|
||
clean = clean.replace(/\//g, '-');
|
||
return clean;
|
||
}
|
||
|
||
function getLocationName(path) {
|
||
if (path.includes('IDC')) return 'IDC';
|
||
if (path.includes('한맥빌딩')) return '한맥빌딩';
|
||
if (path.includes('기술개발센터')) return '기술개발센터';
|
||
return '기타';
|
||
}
|
||
|
||
function getLocationDetail(path, idx) {
|
||
let clean = path.replace('img/location_photo/', '').replace('.png', '');
|
||
let parts = clean.split('/');
|
||
let lastPart = parts[parts.length - 1]; // e.g. "서관205", "MDF_1", "서버실_1"
|
||
return `${lastPart} 구역 자리 #${idx + 1}`;
|
||
}
|
||
|
||
async function main() {
|
||
console.log('🏁 Starting DB migration...');
|
||
|
||
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: process.env.DB_PORT
|
||
});
|
||
|
||
const connection = await pool.getConnection();
|
||
|
||
try {
|
||
// 1. Create physical_locations table
|
||
console.log('⏳ Creating physical_locations table...');
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS physical_locations (
|
||
location_code VARCHAR(50) NOT NULL COMMENT '위치 식별 코드 (예: LOC-IDC-W205-001)',
|
||
location_name VARCHAR(100) NOT NULL COMMENT '물리 위치 대분류 (예: IDC 서관)',
|
||
location_detail VARCHAR(100) NOT NULL COMMENT '상세 위치/랙 번호 (예: 205호 1번 랙)',
|
||
map_image VARCHAR(150) NOT NULL COMMENT '해당 도면 파일 경로 (예: img/location_photo/IDC/서관205.png)',
|
||
map_x DECIMAL(5,2) NOT NULL COMMENT '도면 내 X 백분율 좌표',
|
||
map_y DECIMAL(5,2) NOT NULL COMMENT '도면 내 Y 백분율 좌표',
|
||
map_w DECIMAL(5,2) NOT NULL DEFAULT 4.00 COMMENT '도면 내 박스 너비(%)',
|
||
map_h DECIMAL(5,2) NOT NULL DEFAULT 4.00 COMMENT '도면 내 박스 높이(%)',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (location_code)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||
`);
|
||
console.log('✅ physical_locations table ready.');
|
||
|
||
// 2. Create asset_audit_pending table
|
||
console.log('⏳ Creating asset_audit_pending table...');
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS asset_audit_pending (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
asset_code VARCHAR(50) NOT NULL COMMENT '스캔된 자산 고유번호 (예: server_1779761946023_14)',
|
||
physical_location_code VARCHAR(50) NOT NULL COMMENT '스캔된 위치 마스터 코드 (예: LOC-IDC-W205-001)',
|
||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '상태: PENDING(대기), APPROVED(승인), REJECTED(반려)',
|
||
scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
processed_at TIMESTAMP NULL COMMENT '승인/반려 처리 일시',
|
||
processed_by VARCHAR(50) NULL COMMENT '처리한 관리자',
|
||
CONSTRAINT fk_audit_physical FOREIGN KEY (physical_location_code) REFERENCES physical_locations(location_code)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||
`);
|
||
console.log('✅ asset_audit_pending table ready.');
|
||
|
||
// 3. Add physical_location_code to asset_location
|
||
console.log('⏳ Checking physical_location_code column in asset_location...');
|
||
const [cols] = await connection.query('DESCRIBE asset_location');
|
||
const hasCol = cols.some(c => c.Field === 'physical_location_code');
|
||
if (!hasCol) {
|
||
await connection.query(`
|
||
ALTER TABLE asset_location
|
||
ADD COLUMN physical_location_code VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT 'physical_locations의 location_code FK'
|
||
`);
|
||
console.log('✅ physical_location_code column added with utf8mb4_unicode_ci collation.');
|
||
} else {
|
||
console.log('ℹ️ physical_location_code column already exists. Enforcing collation...');
|
||
await connection.query(`
|
||
ALTER TABLE asset_location
|
||
MODIFY COLUMN physical_location_code VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT 'physical_locations의 location_code FK'
|
||
`);
|
||
console.log('✅ physical_location_code column collation enforced.');
|
||
}
|
||
|
||
// Add constraint if not exists
|
||
console.log('⏳ Checking foreign key constraint fk_asset_loc_physical...');
|
||
const [constraints] = await connection.query(`
|
||
SELECT CONSTRAINT_NAME
|
||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||
WHERE TABLE_NAME = 'asset_location'
|
||
AND CONSTRAINT_NAME = 'fk_asset_loc_physical'
|
||
AND TABLE_SCHEMA = DATABASE()
|
||
`);
|
||
|
||
if (constraints.length === 0) {
|
||
console.log('⏳ Adding foreign key constraint...');
|
||
await connection.query(`
|
||
ALTER TABLE asset_location
|
||
ADD CONSTRAINT fk_asset_loc_physical
|
||
FOREIGN KEY (physical_location_code) REFERENCES physical_locations(location_code)
|
||
`);
|
||
console.log('✅ Foreign key constraint added.');
|
||
} else {
|
||
console.log('ℹ️ Foreign key constraint already exists.');
|
||
}
|
||
|
||
// 4. Load map_config.json and migrate
|
||
console.log('⏳ Migrating map_config.json data to physical_locations...');
|
||
if (fs.existsSync('map_config.json')) {
|
||
const mapConfig = JSON.parse(fs.readFileSync('map_config.json', 'utf8') || '{}');
|
||
let insertCount = 0;
|
||
let syncCount = 0;
|
||
|
||
for (const [mapPath, boxes] of Object.entries(mapConfig)) {
|
||
const cleanKey = getCleanMapKey(mapPath);
|
||
const locName = getLocationName(mapPath);
|
||
|
||
for (let i = 0; i < boxes.length; i++) {
|
||
const box = boxes[i];
|
||
const padIdx = String(i + 1).padStart(3, '0');
|
||
const locCode = `LOC-${cleanKey}-${padIdx}`;
|
||
const locDetail = getLocationDetail(mapPath, i);
|
||
|
||
const bx = parseFloat(box.x);
|
||
const by = parseFloat(box.y);
|
||
const bw = parseFloat(box.w || 4.00);
|
||
const bh = parseFloat(box.h || 4.00);
|
||
|
||
// Insert into physical_locations (ignore if duplicate)
|
||
await connection.query(`
|
||
INSERT INTO physical_locations
|
||
(location_code, location_name, location_detail, map_image, map_x, map_y, map_w, map_h)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
location_name = VALUES(location_name),
|
||
location_detail = VALUES(location_detail),
|
||
map_image = VALUES(map_image),
|
||
map_x = VALUES(map_x),
|
||
map_y = VALUES(map_y),
|
||
map_w = VALUES(map_w),
|
||
map_h = VALUES(map_h)
|
||
`, [locCode, locName, locDetail, mapPath, bx, by, bw, bh]);
|
||
|
||
insertCount++;
|
||
|
||
// Sync database asset if box.asset_id exists
|
||
if (box.asset_id) {
|
||
const [rows] = await connection.query(
|
||
'SELECT id FROM asset_location WHERE asset_id = ? AND is_active = 1',
|
||
[box.asset_id]
|
||
);
|
||
if (rows.length > 0) {
|
||
await connection.query(
|
||
'UPDATE asset_location SET physical_location_code = ? WHERE asset_id = ? AND is_active = 1',
|
||
[locCode, box.asset_id]
|
||
);
|
||
syncCount++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
console.log(`✅ Migrated ${insertCount} physical locations and synced ${syncCount} existing assets.`);
|
||
} else {
|
||
console.log('⚠️ map_config.json not found, skipping initial migration.');
|
||
}
|
||
|
||
console.log('🎉 DB Migration successfully completed!');
|
||
} catch (err) {
|
||
console.error('❌ Migration failed:', err);
|
||
throw err;
|
||
} finally {
|
||
connection.release();
|
||
await pool.end();
|
||
}
|
||
}
|
||
|
||
main().catch(err => {
|
||
process.exit(1);
|
||
});
|