import { getPool } from "../database/db"; export class YardLayoutService { // 모든 야드 레이아웃 목록 조회 async getAllLayouts() { const query = ` SELECT yl.id, yl.name, yl.description, yl.created_by, yl.created_at, yl.updated_at, COUNT(ymp.id) as placement_count FROM yard_layout yl LEFT JOIN yard_material_placement ymp ON yl.id = ymp.yard_layout_id GROUP BY yl.id ORDER BY yl.updated_at DESC `; const pool = getPool(); const result = await pool.query(query); return result.rows; } // 특정 야드 레이아웃 상세 조회 async getLayoutById(id: number) { const query = ` SELECT id, name, description, created_by, created_at, updated_at FROM yard_layout WHERE id = $1 `; const pool = getPool(); const result = await pool.query(query, [id]); return result.rows[0] || null; } // 새 야드 레이아웃 생성 async createLayout(data: { name: string; description?: string; created_by?: string; }) { const query = ` INSERT INTO yard_layout (name, description, created_by) VALUES ($1, $2, $3) RETURNING * `; const pool = getPool(); const result = await pool.query(query, [ data.name, data.description || null, data.created_by || null, ]); return result.rows[0]; } // 야드 레이아웃 수정 (이름, 설명만) async updateLayout( id: number, data: { name?: string; description?: string } ) { const query = ` UPDATE yard_layout SET name = COALESCE($1, name), description = COALESCE($2, description), updated_at = CURRENT_TIMESTAMP WHERE id = $3 RETURNING * `; const pool = getPool(); const result = await pool.query(query, [ data.name || null, data.description || null, id, ]); return result.rows[0] || null; } // 야드 레이아웃 삭제 async deleteLayout(id: number) { const query = `DELETE FROM yard_layout WHERE id = $1 RETURNING *`; const pool = getPool(); const result = await pool.query(query, [id]); return result.rows[0] || null; } // 특정 야드의 모든 배치 자재 조회 async getPlacementsByLayoutId(layoutId: number) { const query = ` SELECT id, yard_layout_id, material_code, material_name, quantity, unit, position_x, position_y, position_z, size_x, size_y, size_z, color, data_source_type, data_source_config, data_binding, memo, created_at, updated_at FROM yard_material_placement WHERE yard_layout_id = $1 ORDER BY created_at ASC `; const pool = getPool(); const result = await pool.query(query, [layoutId]); return result.rows; } // 야드에 자재 배치 추가 (빈 요소 또는 설정된 요소) async addMaterialPlacement(layoutId: number, data: any) { const query = ` INSERT INTO yard_material_placement ( yard_layout_id, material_code, material_name, quantity, unit, position_x, position_y, position_z, size_x, size_y, size_z, color, data_source_type, data_source_config, data_binding, memo ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING * `; const pool = getPool(); // NaN 방지를 위한 안전한 변환 함수 const safeParseInt = ( value: any, defaultValue: number | null = null ): number | null => { if (!value && value !== 0) return defaultValue; const parsed = parseInt(String(value), 10); return isNaN(parsed) ? defaultValue : parsed; }; const safeParseFloat = (value: any, defaultValue: number): number => { if (!value && value !== 0) return defaultValue; const parsed = parseFloat(String(value)); return isNaN(parsed) ? defaultValue : parsed; }; const result = await pool.query(query, [ layoutId, data.material_code || null, data.material_name || null, safeParseInt(data.quantity, null), data.unit || null, safeParseFloat(data.position_x, 0), safeParseFloat(data.position_y, 0), safeParseFloat(data.position_z, 0), safeParseFloat(data.size_x, 5), safeParseFloat(data.size_y, 5), safeParseFloat(data.size_z, 5), data.color || "#9ca3af", // 미설정 시 회색 data.data_source_type || null, data.data_source_config ? JSON.stringify(data.data_source_config) : null, data.data_binding ? JSON.stringify(data.data_binding) : null, data.memo || null, ]); return result.rows[0]; } // 배치 정보 수정 (위치, 크기, 색상, 데이터 바인딩 등) async updatePlacement(placementId: number, data: any) { const query = ` UPDATE yard_material_placement SET material_code = COALESCE($1, material_code), material_name = COALESCE($2, material_name), quantity = COALESCE($3, quantity), unit = COALESCE($4, unit), position_x = COALESCE($5, position_x), position_y = COALESCE($6, position_y), position_z = COALESCE($7, position_z), size_x = COALESCE($8, size_x), size_y = COALESCE($9, size_y), size_z = COALESCE($10, size_z), color = COALESCE($11, color), data_source_type = COALESCE($12, data_source_type), data_source_config = COALESCE($13, data_source_config), data_binding = COALESCE($14, data_binding), memo = COALESCE($15, memo), updated_at = CURRENT_TIMESTAMP WHERE id = $16 RETURNING * `; const pool = getPool(); // NaN 방지를 위한 안전한 변환 함수 const safeParseInt = (value: any): number | null => { if (value === null || value === undefined) return null; const parsed = parseInt(String(value), 10); return isNaN(parsed) ? null : parsed; }; const safeParseFloat = (value: any): number | null => { if (value === null || value === undefined) return null; const parsed = parseFloat(String(value)); return isNaN(parsed) ? null : parsed; }; const result = await pool.query(query, [ data.material_code !== undefined ? data.material_code : null, data.material_name !== undefined ? data.material_name : null, data.quantity !== undefined ? safeParseInt(data.quantity) : null, data.unit !== undefined ? data.unit : null, data.position_x !== undefined ? safeParseFloat(data.position_x) : null, data.position_y !== undefined ? safeParseFloat(data.position_y) : null, data.position_z !== undefined ? safeParseFloat(data.position_z) : null, data.size_x !== undefined ? safeParseFloat(data.size_x) : null, data.size_y !== undefined ? safeParseFloat(data.size_y) : null, data.size_z !== undefined ? safeParseFloat(data.size_z) : null, data.color !== undefined ? data.color : null, data.data_source_type !== undefined ? data.data_source_type : null, data.data_source_config !== undefined ? JSON.stringify(data.data_source_config) : null, data.data_binding !== undefined ? JSON.stringify(data.data_binding) : null, data.memo !== undefined ? data.memo : null, placementId, ]); return result.rows[0] || null; } // 배치 해제 (자재는 삭제되지 않음) async removePlacement(placementId: number) { const query = `DELETE FROM yard_material_placement WHERE id = $1 RETURNING *`; const pool = getPool(); const result = await pool.query(query, [placementId]); return result.rows[0] || null; } // 여러 배치 일괄 업데이트 async batchUpdatePlacements(layoutId: number, placements: any[]) { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const results = []; for (const placement of placements) { const query = ` UPDATE yard_material_placement SET position_x = $1, position_y = $2, position_z = $3, size_x = $4, size_y = $5, size_z = $6, color = $7, updated_at = CURRENT_TIMESTAMP WHERE id = $8 AND yard_layout_id = $9 RETURNING * `; const result = await client.query(query, [ placement.position_x, placement.position_y, placement.position_z, placement.size_x, placement.size_y, placement.size_z, placement.color, placement.id, layoutId, ]); if (result.rows[0]) { results.push(result.rows[0]); } } await client.query("COMMIT"); return results; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } // 야드 레이아웃 복제 async duplicateLayout(id: number, newName: string) { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); // 원본 레이아웃 조회 const layoutQuery = `SELECT * FROM yard_layout WHERE id = $1`; const layoutResult = await client.query(layoutQuery, [id]); const originalLayout = layoutResult.rows[0]; if (!originalLayout) { throw new Error("Layout not found"); } // 새 레이아웃 생성 const newLayoutQuery = ` INSERT INTO yard_layout (name, description, created_by) VALUES ($1, $2, $3) RETURNING * `; const newLayoutResult = await client.query(newLayoutQuery, [ newName, originalLayout.description, originalLayout.created_by, ]); const newLayout = newLayoutResult.rows[0]; // 배치 자재 복사 const placementsQuery = `SELECT * FROM yard_material_placement WHERE yard_layout_id = $1`; const placementsResult = await client.query(placementsQuery, [id]); for (const placement of placementsResult.rows) { await client.query( ` INSERT INTO yard_material_placement ( yard_layout_id, material_code, material_name, quantity, unit, position_x, position_y, position_z, size_x, size_y, size_z, color, data_source_type, data_source_config, data_binding, memo ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) `, [ newLayout.id, placement.material_code, placement.material_name, placement.quantity, placement.unit, placement.position_x, placement.position_y, placement.position_z, placement.size_x, placement.size_y, placement.size_z, placement.color, placement.data_source_type, placement.data_source_config, placement.data_binding, placement.memo, ] ); } await client.query("COMMIT"); return newLayout; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } } export default new YardLayoutService();