472 lines
12 KiB
TypeScript
472 lines
12 KiB
TypeScript
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<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
|
|
`;
|
|
|
|
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<Response> => {
|
|
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<Response> => {
|
|
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<Response> => {
|
|
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<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,
|
|
});
|
|
}
|
|
};
|