feat: Digital Twin Editor 테이블 매핑 UI 및 백엔드 API 구현
This commit is contained in:
386
backend-node/src/controllers/digitalTwinLayoutController.ts
Normal file
386
backend-node/src/controllers/digitalTwinLayoutController.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { Request, Response } from "express";
|
||||
import { pool } from "../database/db";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
// 레이아웃 목록 조회
|
||||
export const getLayouts = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
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
|
||||
WHERE l.company_code = $1
|
||||
`;
|
||||
|
||||
const params: any[] = [companyCode];
|
||||
let paramIndex = 2;
|
||||
|
||||
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: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
// 레이아웃 기본 정보
|
||||
const layoutQuery = `
|
||||
SELECT l.*
|
||||
FROM digital_twin_layout l
|
||||
WHERE l.id = $1 AND l.company_code = $2
|
||||
`;
|
||||
|
||||
const layoutResult = await pool.query(layoutQuery, [id, companyCode]);
|
||||
|
||||
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: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
const {
|
||||
externalDbConnectionId,
|
||||
warehouseKey,
|
||||
layoutName,
|
||||
description,
|
||||
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, created_by, updated_by
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $6)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const layoutResult = await client.query(layoutQuery, [
|
||||
companyCode,
|
||||
externalDbConnectionId,
|
||||
warehouseKey,
|
||||
layoutName,
|
||||
description,
|
||||
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
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
`;
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
const { id } = req.params;
|
||||
const { layoutName, description, objects } = req.body;
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 레이아웃 기본 정보 수정
|
||||
const updateLayoutQuery = `
|
||||
UPDATE digital_twin_layout
|
||||
SET layout_name = $1,
|
||||
description = $2,
|
||||
updated_by = $3,
|
||||
updated_at = NOW()
|
||||
WHERE id = $4 AND company_code = $5
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const layoutResult = await client.query(updateLayoutQuery, [
|
||||
layoutName,
|
||||
description,
|
||||
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
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
`;
|
||||
|
||||
for (const obj of objects) {
|
||||
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,
|
||||
obj.parentId || null,
|
||||
obj.displayOrder || 0,
|
||||
obj.locked || false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user