Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs
2025-11-25 16:05:56 +09:00
39 changed files with 2389 additions and 558 deletions

View File

@@ -0,0 +1,172 @@
import { pool } from "../database/db";
import logger from "../utils/logger";
export interface DigitalTwinLayoutTemplate {
id: string;
company_code: string;
name: string;
description?: string | null;
external_db_connection_id: number;
layout_type: string;
config: any;
created_by: string;
created_at: Date;
updated_by: string;
updated_at: Date;
}
interface ServiceResponse<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
export class DigitalTwinTemplateService {
static async listTemplates(
companyCode: string,
options: { externalDbConnectionId?: number; layoutType?: string } = {},
): Promise<ServiceResponse<DigitalTwinLayoutTemplate[]>> {
try {
const params: any[] = [companyCode];
let paramIndex = 2;
let query = `
SELECT *
FROM digital_twin_layout_template
WHERE company_code = $1
`;
if (options.layoutType) {
query += ` AND layout_type = $${paramIndex++}`;
params.push(options.layoutType);
}
if (options.externalDbConnectionId) {
query += ` AND external_db_connection_id = $${paramIndex++}`;
params.push(options.externalDbConnectionId);
}
query += `
ORDER BY updated_at DESC, name ASC
`;
const result = await pool.query(query, params);
logger.info("디지털 트윈 매핑 템플릿 목록 조회", {
companyCode,
count: result.rowCount,
});
return {
success: true,
data: result.rows as DigitalTwinLayoutTemplate[],
};
} catch (error: any) {
logger.error("디지털 트윈 매핑 템플릿 목록 조회 실패", error);
return {
success: false,
error: error.message,
message: "매핑 템플릿 목록 조회 중 오류가 발생했습니다.",
};
}
}
static async getTemplateById(
companyCode: string,
id: string,
): Promise<ServiceResponse<DigitalTwinLayoutTemplate>> {
try {
const query = `
SELECT *
FROM digital_twin_layout_template
WHERE id = $1 AND company_code = $2
`;
const result = await pool.query(query, [id, companyCode]);
if (result.rowCount === 0) {
return {
success: false,
message: "매핑 템플릿을 찾을 수 없습니다.",
};
}
return {
success: true,
data: result.rows[0] as DigitalTwinLayoutTemplate,
};
} catch (error: any) {
logger.error("디지털 트윈 매핑 템플릿 조회 실패", error);
return {
success: false,
error: error.message,
message: "매핑 템플릿 조회 중 오류가 발생했습니다.",
};
}
}
static async createTemplate(
companyCode: string,
userId: string,
payload: {
name: string;
description?: string;
externalDbConnectionId: number;
layoutType?: string;
config: any;
},
): Promise<ServiceResponse<DigitalTwinLayoutTemplate>> {
try {
const query = `
INSERT INTO digital_twin_layout_template (
company_code,
name,
description,
external_db_connection_id,
layout_type,
config,
created_by,
created_at,
updated_by,
updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $7, NOW())
RETURNING *
`;
const values = [
companyCode,
payload.name,
payload.description || null,
payload.externalDbConnectionId,
payload.layoutType || "yard-3d",
JSON.stringify(payload.config),
userId,
];
const result = await pool.query(query, values);
logger.info("디지털 트윈 매핑 템플릿 생성", {
companyCode,
templateId: result.rows[0].id,
externalDbConnectionId: payload.externalDbConnectionId,
});
return {
success: true,
data: result.rows[0] as DigitalTwinLayoutTemplate,
};
} catch (error: any) {
logger.error("디지털 트윈 매핑 템플릿 생성 실패", error);
return {
success: false,
error: error.message,
message: "매핑 템플릿 생성 중 오류가 발생했습니다.",
};
}
}
}

View File

@@ -811,9 +811,39 @@ export class DynamicFormService {
const primaryKeyColumn = primaryKeys[0];
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
// 동적 UPDATE SQL 생성 (변경된 필드만)
// 🆕 컬럼 타입 조회 (타입 캐스팅용)
const columnTypesQuery = `
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1 AND table_schema = 'public'
`;
const columnTypesResult = await query<{ column_name: string; data_type: string }>(
columnTypesQuery,
[tableName]
);
const columnTypes: Record<string, string> = {};
columnTypesResult.forEach((row) => {
columnTypes[row.column_name] = row.data_type;
});
console.log("📊 컬럼 타입 정보:", columnTypes);
// 🆕 동적 UPDATE SQL 생성 (타입 캐스팅 포함)
const setClause = Object.keys(changedFields)
.map((key, index) => `${key} = $${index + 1}`)
.map((key, index) => {
const dataType = columnTypes[key];
// 숫자 타입인 경우 명시적 캐스팅
if (dataType === 'integer' || dataType === 'bigint' || dataType === 'smallint') {
return `${key} = $${index + 1}::integer`;
} else if (dataType === 'numeric' || dataType === 'decimal' || dataType === 'real' || dataType === 'double precision') {
return `${key} = $${index + 1}::numeric`;
} else if (dataType === 'boolean') {
return `${key} = $${index + 1}::boolean`;
} else {
// 문자열 타입은 캐스팅 불필요
return `${key} = $${index + 1}`;
}
})
.join(", ");
const values: any[] = Object.values(changedFields);