배치 대략적인 완료

This commit is contained in:
dohyeons
2025-11-21 02:25:25 +09:00
parent eb6fe57839
commit 0450390b2a
11 changed files with 2064 additions and 357 deletions

View File

@@ -36,7 +36,138 @@ export async function getExternalDbConnector(connectionId: number) {
);
}
// 창고 목록 조회 (사용자 지정 테이블)
// 동적 계층 구조 데이터 조회 (범용)
export const getHierarchyData = async (req: Request, res: Response): Promise<Response> => {
try {
const { externalDbConnectionId, hierarchyConfig } = req.body;
if (!externalDbConnectionId || !hierarchyConfig) {
return res.status(400).json({
success: false,
message: "외부 DB 연결 ID와 계층 구조 설정이 필요합니다.",
});
}
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
const config = JSON.parse(hierarchyConfig);
const result: any = {
warehouse: null,
levels: [],
materials: [],
};
// 창고 데이터 조회
if (config.warehouse) {
const warehouseQuery = `SELECT * FROM ${config.warehouse.tableName} LIMIT 100`;
const warehouseResult = await connector.executeQuery(warehouseQuery);
result.warehouse = warehouseResult.rows;
}
// 각 레벨 데이터 조회
if (config.levels && Array.isArray(config.levels)) {
for (const level of config.levels) {
const levelQuery = `SELECT * FROM ${level.tableName} LIMIT 1000`;
const levelResult = await connector.executeQuery(levelQuery);
result.levels.push({
level: level.level,
name: level.name,
data: levelResult.rows,
});
}
}
// 자재 데이터 조회 (개수만)
if (config.material) {
const materialQuery = `
SELECT
${config.material.locationKeyColumn} as location_key,
COUNT(*) as count
FROM ${config.material.tableName}
GROUP BY ${config.material.locationKeyColumn}
`;
const materialResult = await connector.executeQuery(materialQuery);
result.materials = materialResult.rows;
}
logger.info("동적 계층 구조 데이터 조회", {
externalDbConnectionId,
warehouseCount: result.warehouse?.length || 0,
levelCounts: result.levels.map((l: any) => ({ level: l.level, count: l.data.length })),
});
return res.json({
success: true,
data: result,
});
} catch (error: any) {
logger.error("동적 계층 구조 데이터 조회 실패", error);
return res.status(500).json({
success: false,
message: "데이터 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
};
// 특정 레벨의 하위 데이터 조회
export const getChildrenData = async (req: Request, res: Response): Promise<Response> => {
try {
const { externalDbConnectionId, hierarchyConfig, parentLevel, parentKey } = req.body;
if (!externalDbConnectionId || !hierarchyConfig || !parentLevel || !parentKey) {
return res.status(400).json({
success: false,
message: "필수 파라미터가 누락되었습니다.",
});
}
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
const config = JSON.parse(hierarchyConfig);
// 다음 레벨 찾기
const nextLevel = config.levels?.find((l: any) => l.level === parentLevel + 1);
if (!nextLevel) {
return res.json({
success: true,
data: [],
message: "하위 레벨이 없습니다.",
});
}
// 하위 데이터 조회
const query = `
SELECT * FROM ${nextLevel.tableName}
WHERE ${nextLevel.parentKeyColumn} = '${parentKey}'
LIMIT 1000
`;
const result = await connector.executeQuery(query);
logger.info("하위 데이터 조회", {
externalDbConnectionId,
parentLevel,
parentKey,
count: result.rows.length,
});
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 getWarehouses = async (req: Request, res: Response): Promise<Response> => {
try {
const { externalDbConnectionId, tableName } = req.query;
@@ -83,32 +214,29 @@ export const getWarehouses = async (req: Request, res: Response): Promise<Respon
}
};
// Area 목록 조회 (사용자 지정 테이블)
// 구역 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
export const getAreas = async (req: Request, res: Response): Promise<Response> => {
try {
const { externalDbConnectionId, tableName, warehouseKey } = req.query;
const { externalDbConnectionId, warehouseKey, tableName } = req.query;
if (!externalDbConnectionId || !tableName) {
if (!externalDbConnectionId || !warehouseKey || !tableName) {
return res.status(400).json({
success: false,
message: "외부 DB 연결 ID와 테이블명이 필요합니다.",
message: "필수 파라미터가 누락되었습니다.",
});
}
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
// 테이블명을 사용하여 모든 컬럼 조회
let query = `SELECT * FROM ${tableName}`;
if (warehouseKey) {
query += ` WHERE WAREKEY = '${warehouseKey}'`;
}
query += ` LIMIT 1000`;
const query = `
SELECT * FROM ${tableName}
WHERE WAREKEY = '${warehouseKey}'
LIMIT 1000
`;
const result = await connector.executeQuery(query);
logger.info("Area 목록 조회", {
logger.info("구역 목록 조회", {
externalDbConnectionId,
tableName,
warehouseKey,
@@ -120,41 +248,38 @@ export const getAreas = async (req: Request, res: Response): Promise<Response> =
data: result.rows,
});
} catch (error: any) {
logger.error("Area 목록 조회 실패", error);
logger.error("구역 목록 조회 실패", error);
return res.status(500).json({
success: false,
message: "Area 목록 조회 중 오류가 발생했습니다.",
message: "구역 목록 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
};
// Location 목록 조회 (사용자 지정 테이블)
// 위치 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
export const getLocations = async (req: Request, res: Response): Promise<Response> => {
try {
const { externalDbConnectionId, tableName, areaKey } = req.query;
const { externalDbConnectionId, areaKey, tableName } = req.query;
if (!externalDbConnectionId || !tableName) {
if (!externalDbConnectionId || !areaKey || !tableName) {
return res.status(400).json({
success: false,
message: "외부 DB 연결 ID와 테이블명이 필요합니다.",
message: "필수 파라미터가 누락되었습니다.",
});
}
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
// 테이블명을 사용하여 모든 컬럼 조회
let query = `SELECT * FROM ${tableName}`;
if (areaKey) {
query += ` WHERE AREAKEY = '${areaKey}'`;
}
query += ` LIMIT 1000`;
const query = `
SELECT * FROM ${tableName}
WHERE AREAKEY = '${areaKey}'
LIMIT 1000
`;
const result = await connector.executeQuery(query);
logger.info("Location 목록 조회", {
logger.info("위치 목록 조회", {
externalDbConnectionId,
tableName,
areaKey,
@@ -166,37 +291,46 @@ export const getLocations = async (req: Request, res: Response): Promise<Respons
data: result.rows,
});
} catch (error: any) {
logger.error("Location 목록 조회 실패", error);
logger.error("위치 목록 조회 실패", error);
return res.status(500).json({
success: false,
message: "Location 목록 조회 중 오류가 발생했습니다.",
message: "위치 목록 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
};
// 자재 목록 조회 (사용자 지정 테이블)
// 자재 목록 조회 (동적 컬럼 매핑 지원)
export const getMaterials = async (req: Request, res: Response): Promise<Response> => {
try {
const { externalDbConnectionId, tableName, locaKey } = req.query;
const {
externalDbConnectionId,
locaKey,
tableName,
keyColumn,
locationKeyColumn,
layerColumn
} = req.query;
if (!externalDbConnectionId || !tableName) {
if (!externalDbConnectionId || !locaKey || !tableName || !locationKeyColumn) {
return res.status(400).json({
success: false,
message: "외부 DB 연결 ID와 테이블명이 필요합니다.",
message: "필수 파라미터가 누락되었습니다.",
});
}
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
// 테이블명을 사용하여 모든 컬럼 조회
let query = `SELECT * FROM ${tableName}`;
if (locaKey) {
query += ` WHERE LOCAKEY = '${locaKey}'`;
}
query += ` LIMIT 1000`;
// 동적 쿼리 생성
const orderByClause = layerColumn ? `ORDER BY ${layerColumn}` : '';
const query = `
SELECT * FROM ${tableName}
WHERE ${locationKeyColumn} = '${locaKey}'
${orderByClause}
LIMIT 1000
`;
logger.info(`자재 조회 쿼리: ${query}`);
const result = await connector.executeQuery(query);
@@ -221,31 +355,28 @@ export const getMaterials = async (req: Request, res: Response): Promise<Respons
}
};
// Location별 자재 개수 조회 (배치 시 사용 - 사용자 지정 테이블)
// 자재 개수 조회 (여러 Location 일괄) - 레거시, 호환성 유지
export const getMaterialCounts = async (req: Request, res: Response): Promise<Response> => {
try {
const { externalDbConnectionId, tableName, locaKeys } = req.query;
const { externalDbConnectionId, locationKeys, tableName } = req.body;
if (!externalDbConnectionId || !tableName || !locaKeys) {
if (!externalDbConnectionId || !locationKeys || !tableName) {
return res.status(400).json({
success: false,
message: "외부 DB 연결 ID, 테이블명, Location 키 목록이 필요합니다.",
message: "필수 파라미터가 누락되었습니다.",
});
}
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
// locaKeys는 쉼표로 구분된 문자열
const locaKeyArray = (locaKeys as string).split(",");
const quotedKeys = locaKeyArray.map((key) => `'${key}'`).join(",");
const keysString = locationKeys.map((key: string) => `'${key}'`).join(",");
const query = `
SELECT
LOCAKEY,
COUNT(*) as material_count,
MAX(LOLAYER) as max_layer
LOCAKEY as location_key,
COUNT(*) as count
FROM ${tableName}
WHERE LOCAKEY IN (${quotedKeys})
WHERE LOCAKEY IN (${keysString})
GROUP BY LOCAKEY
`;
@@ -254,7 +385,7 @@ export const getMaterialCounts = async (req: Request, res: Response): Promise<Re
logger.info("자재 개수 조회", {
externalDbConnectionId,
tableName,
locaKeyCount: locaKeyArray.length,
locationCount: locationKeys.length,
});
return res.json({
@@ -270,4 +401,3 @@ export const getMaterialCounts = async (req: Request, res: Response): Promise<Re
});
}
};

View File

@@ -138,6 +138,7 @@ export const createLayout = async (
warehouseKey,
layoutName,
description,
hierarchyConfig,
objects,
} = req.body;
@@ -147,9 +148,9 @@ export const createLayout = async (
const layoutQuery = `
INSERT INTO digital_twin_layout (
company_code, external_db_connection_id, warehouse_key,
layout_name, description, created_by, updated_by
layout_name, description, hierarchy_config, created_by, updated_by
)
VALUES ($1, $2, $3, $4, $5, $6, $6)
VALUES ($1, $2, $3, $4, $5, $6, $7, $7)
RETURNING *
`;
@@ -159,6 +160,7 @@ export const createLayout = async (
warehouseKey,
layoutName,
description,
hierarchyConfig ? JSON.stringify(hierarchyConfig) : null,
userId,
]);
@@ -174,9 +176,10 @@ export const createLayout = async (
rotation, color,
area_key, loca_key, loc_type,
material_count, material_preview_height,
parent_id, display_order, locked
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)
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) {
@@ -200,6 +203,9 @@ export const createLayout = async (
obj.parentId || null,
obj.displayOrder || 0,
obj.locked || false,
obj.hierarchyLevel || 1,
obj.parentKey || null,
obj.externalKey || null,
]);
}
}
@@ -240,7 +246,14 @@ export const updateLayout = async (
const companyCode = req.user?.companyCode;
const userId = req.user?.userId;
const { id } = req.params;
const { layoutName, description, objects } = req.body;
const {
layoutName,
description,
hierarchyConfig,
externalDbConnectionId,
warehouseKey,
objects,
} = req.body;
await client.query("BEGIN");
@@ -249,15 +262,21 @@ export const updateLayout = async (
UPDATE digital_twin_layout
SET layout_name = $1,
description = $2,
updated_by = $3,
hierarchy_config = $3,
external_db_connection_id = $4,
warehouse_key = $5,
updated_by = $6,
updated_at = NOW()
WHERE id = $4 AND company_code = $5
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,
@@ -277,7 +296,7 @@ export const updateLayout = async (
[id]
);
// 새 객체 저장
// 새 객체 저장 (부모-자식 관계 처리)
if (objects && objects.length > 0) {
const objectQuery = `
INSERT INTO digital_twin_objects (
@@ -287,12 +306,53 @@ export const updateLayout = async (
rotation, color,
area_key, loca_key, loc_type,
material_count, material_preview_height,
parent_id, display_order, locked
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)
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
`;
for (const obj of objects) {
// 임시 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,
@@ -310,9 +370,12 @@ export const updateLayout = async (
obj.locType || null,
obj.materialCount || 0,
obj.materialPreview?.height || null,
obj.parentId || null,
realParentId, // 실제 DB ID 사용
obj.displayOrder || 0,
obj.locked || false,
obj.hierarchyLevel || 1,
obj.parentKey || null,
obj.externalKey || null,
]);
}
}