398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
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();
|