import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import { pool } from "../database/db"; import logger from "../utils/logger"; // 레이아웃 목록 조회 export const getLayouts = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { const companyCode = req.user?.companyCode; const { externalDbConnectionId, warehouseKey } = req.query; let query = ` SELECT l.*, u1.user_name as created_by_name, u2.user_name as updated_by_name, COUNT(o.id) as object_count FROM digital_twin_layout l LEFT JOIN user_info u1 ON l.created_by = u1.user_id LEFT JOIN user_info u2 ON l.updated_by = u2.user_id LEFT JOIN digital_twin_objects o ON l.id = o.layout_id `; const params: any[] = []; let paramIndex = 1; // 최고 관리자는 모든 레이아웃 조회 가능 if (companyCode && companyCode !== '*') { query += ` WHERE l.company_code = $${paramIndex}`; params.push(companyCode); paramIndex++; } else { query += ` WHERE 1=1`; } if (externalDbConnectionId) { query += ` AND l.external_db_connection_id = $${paramIndex}`; params.push(externalDbConnectionId); paramIndex++; } if (warehouseKey) { query += ` AND l.warehouse_key = $${paramIndex}`; params.push(warehouseKey); paramIndex++; } query += ` GROUP BY l.id, u1.user_name, u2.user_name ORDER BY l.updated_at DESC `; const result = await pool.query(query, params); logger.info("레이아웃 목록 조회", { companyCode, count: result.rowCount, }); return res.json({ success: true, data: result.rows, }); } catch (error: any) { logger.error("레이아웃 목록 조회 실패", error); return res.status(500).json({ success: false, message: "레이아웃 목록 조회 중 오류가 발생했습니다.", error: error.message, }); } }; // 레이아웃 상세 조회 (객체 포함) export const getLayoutById = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { const companyCode = req.user?.companyCode; const { id } = req.params; // 레이아웃 기본 정보 - 최고 관리자는 모든 레이아웃 조회 가능 let layoutQuery: string; let layoutParams: any[]; if (companyCode && companyCode !== '*') { layoutQuery = ` SELECT l.* FROM digital_twin_layout l WHERE l.id = $1 AND l.company_code = $2 `; layoutParams = [id, companyCode]; } else { layoutQuery = ` SELECT l.* FROM digital_twin_layout l WHERE l.id = $1 `; layoutParams = [id]; } const layoutResult = await pool.query(layoutQuery, layoutParams); if (layoutResult.rowCount === 0) { return res.status(404).json({ success: false, message: "레이아웃을 찾을 수 없습니다.", }); } // 배치된 객체들 조회 const objectsQuery = ` SELECT * FROM digital_twin_objects WHERE layout_id = $1 ORDER BY display_order, created_at `; const objectsResult = await pool.query(objectsQuery, [id]); logger.info("레이아웃 상세 조회", { companyCode, layoutId: id, objectCount: objectsResult.rowCount, }); return res.json({ success: true, data: { layout: layoutResult.rows[0], objects: objectsResult.rows, }, }); } catch (error: any) { logger.error("레이아웃 상세 조회 실패", error); return res.status(500).json({ success: false, message: "레이아웃 조회 중 오류가 발생했습니다.", error: error.message, }); } }; // 레이아웃 생성 export const createLayout = async ( req: AuthenticatedRequest, res: Response ): Promise => { const client = await pool.connect(); try { const companyCode = req.user?.companyCode; const userId = req.user?.userId; const { externalDbConnectionId, warehouseKey, layoutName, description, hierarchyConfig, objects, } = req.body; await client.query("BEGIN"); // 레이아웃 생성 const layoutQuery = ` INSERT INTO digital_twin_layout ( company_code, external_db_connection_id, warehouse_key, layout_name, description, hierarchy_config, created_by, updated_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $7) RETURNING * `; const layoutResult = await client.query(layoutQuery, [ companyCode, externalDbConnectionId, warehouseKey, layoutName, description, hierarchyConfig ? JSON.stringify(hierarchyConfig) : null, userId, ]); const layoutId = layoutResult.rows[0].id; // 객체들 저장 if (objects && objects.length > 0) { const objectQuery = ` INSERT INTO digital_twin_objects ( layout_id, object_type, object_name, position_x, position_y, position_z, size_x, size_y, size_z, rotation, color, area_key, loca_key, loc_type, material_count, material_preview_height, parent_id, display_order, locked, hierarchy_level, parent_key, external_key ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) `; for (const obj of objects) { await client.query(objectQuery, [ layoutId, obj.type, obj.name, obj.position.x, obj.position.y, obj.position.z, obj.size.x, obj.size.y, obj.size.z, obj.rotation || 0, obj.color, obj.areaKey || null, obj.locaKey || null, obj.locType || null, obj.materialCount || 0, obj.materialPreview?.height || null, obj.parentId || null, obj.displayOrder || 0, obj.locked || false, obj.hierarchyLevel || 1, obj.parentKey || null, obj.externalKey || null, ]); } } await client.query("COMMIT"); logger.info("레이아웃 생성", { companyCode, layoutId, objectCount: objects?.length || 0, }); return res.status(201).json({ success: true, data: layoutResult.rows[0], }); } catch (error: any) { await client.query("ROLLBACK"); logger.error("레이아웃 생성 실패", error); return res.status(500).json({ success: false, message: "레이아웃 생성 중 오류가 발생했습니다.", error: error.message, }); } finally { client.release(); } }; // 레이아웃 수정 export const updateLayout = async ( req: AuthenticatedRequest, res: Response ): Promise => { const client = await pool.connect(); try { const companyCode = req.user?.companyCode; const userId = req.user?.userId; const { id } = req.params; const { layoutName, description, hierarchyConfig, externalDbConnectionId, warehouseKey, objects, } = req.body; await client.query("BEGIN"); // 레이아웃 기본 정보 수정 const updateLayoutQuery = ` UPDATE digital_twin_layout SET layout_name = $1, description = $2, hierarchy_config = $3, external_db_connection_id = $4, warehouse_key = $5, updated_by = $6, updated_at = NOW() WHERE id = $7 AND company_code = $8 RETURNING * `; const layoutResult = await client.query(updateLayoutQuery, [ layoutName, description, hierarchyConfig ? JSON.stringify(hierarchyConfig) : null, externalDbConnectionId || null, warehouseKey || null, userId, id, companyCode, ]); if (layoutResult.rowCount === 0) { await client.query("ROLLBACK"); return res.status(404).json({ success: false, message: "레이아웃을 찾을 수 없습니다.", }); } // 기존 객체 삭제 await client.query( "DELETE FROM digital_twin_objects WHERE layout_id = $1", [id] ); // 새 객체 저장 (부모-자식 관계 처리) if (objects && objects.length > 0) { const objectQuery = ` INSERT INTO digital_twin_objects ( layout_id, object_type, object_name, position_x, position_y, position_z, size_x, size_y, size_z, rotation, color, area_key, loca_key, loc_type, material_count, material_preview_height, parent_id, display_order, locked, hierarchy_level, parent_key, external_key ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) RETURNING id `; // 임시 ID (음수) → 실제 DB ID 매핑 const idMapping: { [tempId: number]: number } = {}; // 1단계: 부모 객체 먼저 저장 (parentId가 없는 것들) for (const obj of objects.filter((o) => !o.parentId)) { const result = await client.query(objectQuery, [ id, obj.type, obj.name, obj.position.x, obj.position.y, obj.position.z, obj.size.x, obj.size.y, obj.size.z, obj.rotation || 0, obj.color, obj.areaKey || null, obj.locaKey || null, obj.locType || null, obj.materialCount || 0, obj.materialPreview?.height || null, null, // parent_id obj.displayOrder || 0, obj.locked || false, obj.hierarchyLevel || 1, obj.parentKey || null, obj.externalKey || null, ]); // 임시 ID와 실제 DB ID 매핑 if (obj.id) { idMapping[obj.id] = result.rows[0].id; } } // 2단계: 자식 객체 저장 (parentId가 있는 것들) for (const obj of objects.filter((o) => o.parentId)) { const realParentId = idMapping[obj.parentId!] || null; await client.query(objectQuery, [ id, obj.type, obj.name, obj.position.x, obj.position.y, obj.position.z, obj.size.x, obj.size.y, obj.size.z, obj.rotation || 0, obj.color, obj.areaKey || null, obj.locaKey || null, obj.locType || null, obj.materialCount || 0, obj.materialPreview?.height || null, realParentId, // 실제 DB ID 사용 obj.displayOrder || 0, obj.locked || false, obj.hierarchyLevel || 1, obj.parentKey || null, obj.externalKey || null, ]); } } await client.query("COMMIT"); logger.info("레이아웃 수정", { companyCode, layoutId: id, objectCount: objects?.length || 0, }); return res.json({ success: true, data: layoutResult.rows[0], }); } catch (error: any) { await client.query("ROLLBACK"); logger.error("레이아웃 수정 실패", error); return res.status(500).json({ success: false, message: "레이아웃 수정 중 오류가 발생했습니다.", error: error.message, }); } finally { client.release(); } }; // 레이아웃 삭제 export const deleteLayout = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { const companyCode = req.user?.companyCode; const { id } = req.params; const query = ` DELETE FROM digital_twin_layout WHERE id = $1 AND company_code = $2 RETURNING id `; const result = await pool.query(query, [id, companyCode]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "레이아웃을 찾을 수 없습니다.", }); } logger.info("레이아웃 삭제", { companyCode, layoutId: id, }); return res.json({ success: true, message: "레이아웃이 삭제되었습니다.", }); } catch (error: any) { logger.error("레이아웃 삭제 실패", error); return res.status(500).json({ success: false, message: "레이아웃 삭제 중 오류가 발생했습니다.", error: error.message, }); } };