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); });