Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into external-connections

This commit is contained in:
hyeonsu
2025-09-21 11:10:31 +09:00
48 changed files with 4234 additions and 289 deletions

View File

@@ -4086,3 +4086,57 @@ model table_relationships_backup {
@@ignore
}
model test_sales_info {
sales_no String @id @db.VarChar(20)
contract_type String? @db.VarChar(50)
order_seq Int?
domestic_foreign String? @db.VarChar(20)
customer_name String? @db.VarChar(200)
product_type String? @db.VarChar(100)
machine_type String? @db.VarChar(100)
customer_project_name String? @db.VarChar(200)
expected_delivery_date DateTime? @db.Date
receiving_location String? @db.VarChar(200)
setup_location String? @db.VarChar(200)
equipment_direction String? @db.VarChar(100)
equipment_count Int? @default(0)
equipment_type String? @db.VarChar(100)
equipment_length Decimal? @db.Decimal(10,2)
manager_name String? @db.VarChar(100)
reg_date DateTime? @default(now()) @db.Timestamp(6)
status String? @default("진행중") @db.VarChar(50)
// 관계 정의: 영업 정보에서 프로젝트로
projects test_project_info[]
}
model test_project_info {
project_no String @id @db.VarChar(200)
sales_no String? @db.VarChar(20)
contract_type String? @db.VarChar(50)
order_seq Int?
domestic_foreign String? @db.VarChar(20)
customer_name String? @db.VarChar(200)
// 프로젝트 전용 컬럼들
project_status String? @default("PLANNING") @db.VarChar(50)
project_start_date DateTime? @db.Date
project_end_date DateTime? @db.Date
project_manager String? @db.VarChar(100)
project_description String? @db.Text
// 시스템 관리 컬럼들
created_by String? @db.VarChar(100)
created_date DateTime? @default(now()) @db.Timestamp(6)
updated_by String? @db.VarChar(100)
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
// 관계 정의: 영업 정보 참조
sales test_sales_info? @relation(fields: [sales_no], references: [sales_no])
@@index([sales_no], map: "idx_project_sales_no")
@@index([project_status], map: "idx_project_status")
@@index([customer_name], map: "idx_project_customer")
@@index([project_manager], map: "idx_project_manager")
}

View File

@@ -167,6 +167,19 @@ export async function getDiagramRelationships(
const relationships = (diagram.relationships as any)?.relationships || [];
console.log("🔍 백엔드 - 관계도 데이터:", {
diagramId: diagram.diagram_id,
diagramName: diagram.diagram_name,
relationshipsRaw: diagram.relationships,
relationshipsArray: relationships,
relationshipsCount: relationships.length,
});
// 각 관계의 구조도 로깅
relationships.forEach((rel: any, index: number) => {
console.log(`🔍 백엔드 - 관계 ${index + 1}:`, rel);
});
res.json({
success: true,
data: relationships,
@@ -213,14 +226,77 @@ export async function getRelationshipPreview(
}
// 관계 정보 찾기
const relationship = (diagram.relationships as any)?.relationships?.find(
console.log("🔍 관계 미리보기 요청:", {
diagramId,
relationshipId,
diagramRelationships: diagram.relationships,
relationshipsArray: (diagram.relationships as any)?.relationships,
});
const relationships = (diagram.relationships as any)?.relationships || [];
console.log(
"🔍 사용 가능한 관계 목록:",
relationships.map((rel: any) => ({
id: rel.id,
name: rel.relationshipName || rel.name, // relationshipName 사용
sourceTable: rel.fromTable || rel.sourceTable, // fromTable 사용
targetTable: rel.toTable || rel.targetTable, // toTable 사용
originalData: rel, // 디버깅용
}))
);
const relationship = relationships.find(
(rel: any) => rel.id === relationshipId
);
console.log("🔍 찾은 관계:", relationship);
if (!relationship) {
console.log("❌ 관계를 찾을 수 없음:", {
requestedId: relationshipId,
availableIds: relationships.map((rel: any) => rel.id),
});
// 🔧 임시 해결책: 첫 번째 관계를 사용하거나 기본 응답 반환
if (relationships.length > 0) {
console.log("🔧 첫 번째 관계를 대신 사용:", relationships[0].id);
const fallbackRelationship = relationships[0];
console.log("🔍 fallback 관계 선택:", fallbackRelationship);
console.log("🔍 diagram.control 전체 구조:", diagram.control);
console.log("🔍 diagram.plan 전체 구조:", diagram.plan);
const fallbackControl = Array.isArray(diagram.control)
? diagram.control.find((c: any) => c.id === fallbackRelationship.id)
: null;
const fallbackPlan = Array.isArray(diagram.plan)
? diagram.plan.find((p: any) => p.id === fallbackRelationship.id)
: null;
console.log("🔍 찾은 fallback control:", fallbackControl);
console.log("🔍 찾은 fallback plan:", fallbackPlan);
const fallbackPreviewData = {
relationship: fallbackRelationship,
control: fallbackControl,
plan: fallbackPlan,
conditionsCount: (fallbackControl as any)?.conditions?.length || 0,
actionsCount: (fallbackPlan as any)?.actions?.length || 0,
};
console.log("🔍 최종 fallback 응답 데이터:", fallbackPreviewData);
res.json({
success: true,
data: fallbackPreviewData,
});
return;
}
res.status(404).json({
success: false,
message: "관계를 찾을 수 없습니다.",
message: `관계를 찾을 수 없습니다. 요청된 ID: ${relationshipId}, 사용 가능한 ID: ${relationships.map((rel: any) => rel.id).join(", ")}`,
});
return;
}

View File

@@ -11,8 +11,8 @@ export const saveFormData = async (
const { companyCode, userId } = req.user as any;
const { screenId, tableName, data } = req.body;
// 필수 필드 검증
if (!screenId || !tableName || !data) {
// 필수 필드 검증 (screenId는 0일 수 있으므로 undefined 체크)
if (screenId === undefined || screenId === null || !tableName || !data) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (screenId, tableName, data)",
@@ -80,7 +80,7 @@ export const updateFormData = async (
};
const result = await dynamicFormService.updateFormData(
parseInt(id),
id, // parseInt 제거 - 문자열 ID 지원
tableName,
formDataWithMeta
);
@@ -168,7 +168,7 @@ export const deleteFormData = async (
});
}
await dynamicFormService.deleteFormData(parseInt(id), tableName);
await dynamicFormService.deleteFormData(id, tableName); // parseInt 제거 - 문자열 ID 지원
res.json({
success: true,

View File

@@ -87,6 +87,48 @@ export class DynamicFormService {
return Boolean(value);
}
// 날짜/시간 타입 처리
if (
lowerDataType.includes("date") ||
lowerDataType.includes("timestamp") ||
lowerDataType.includes("time")
) {
if (typeof value === "string") {
// 빈 문자열이면 null 반환
if (value.trim() === "") {
return null;
}
try {
// YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
return new Date(value + "T00:00:00");
}
// 다른 날짜 형식도 Date 객체로 변환
else {
console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
return new Date(value);
}
} catch (error) {
console.error(`❌ 날짜 변환 실패: ${value}`, error);
return null;
}
}
// 이미 Date 객체인 경우 그대로 반환
if (value instanceof Date) {
return value;
}
// 숫자인 경우 timestamp로 처리
if (typeof value === "number") {
return new Date(value);
}
return null;
}
// 기본적으로 문자열로 반환
return value;
}
@@ -479,7 +521,7 @@ export class DynamicFormService {
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${primaryKeyColumn} = $${values.length}
WHERE ${primaryKeyColumn} = $${values.length}::text
RETURNING *
`;
@@ -507,7 +549,7 @@ export class DynamicFormService {
* 폼 데이터 업데이트 (실제 테이블에서 직접 업데이트)
*/
async updateFormData(
id: number,
id: string | number,
tableName: string,
data: Record<string, any>
): Promise<FormDataResult> {
@@ -552,6 +594,31 @@ export class DynamicFormService {
}
});
// 컬럼 타입에 맞는 데이터 변환 (UPDATE용)
const columnInfo = await this.getTableColumnInfo(tableName);
console.log(`📊 테이블 ${tableName}의 컬럼 타입 정보:`, columnInfo);
// 각 컬럼의 타입에 맞게 데이터 변환
Object.keys(dataToUpdate).forEach((columnName) => {
const column = columnInfo.find((col) => col.column_name === columnName);
if (column) {
const originalValue = dataToUpdate[columnName];
const convertedValue = this.convertValueForPostgreSQL(
originalValue,
column.data_type
);
if (originalValue !== convertedValue) {
console.log(
`🔄 UPDATE 타입 변환: ${columnName} (${column.data_type}) = "${originalValue}" -> ${convertedValue}`
);
dataToUpdate[columnName] = convertedValue;
}
}
});
console.log("✅ UPDATE 타입 변환 완료된 데이터:", dataToUpdate);
console.log("🎯 실제 테이블에서 업데이트할 데이터:", {
tableName,
id,
@@ -575,10 +642,36 @@ export class DynamicFormService {
const primaryKeyColumn = primaryKeys[0]; // 첫 번째 기본키 사용
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
// 기본키 데이터 타입 조회하여 적절한 캐스팅 적용
const primaryKeyInfo = (await prisma.$queryRawUnsafe(`
SELECT data_type
FROM information_schema.columns
WHERE table_name = '${tableName}'
AND column_name = '${primaryKeyColumn}'
AND table_schema = 'public'
`)) as any[];
let typeCastSuffix = "";
if (primaryKeyInfo.length > 0) {
const dataType = primaryKeyInfo[0].data_type;
console.log(`🔍 기본키 ${primaryKeyColumn}의 데이터 타입: ${dataType}`);
if (dataType.includes("character") || dataType.includes("text")) {
typeCastSuffix = "::text";
} else if (dataType.includes("bigint")) {
typeCastSuffix = "::bigint";
} else if (
dataType.includes("integer") ||
dataType.includes("numeric")
) {
typeCastSuffix = "::numeric";
}
}
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${primaryKeyColumn} = $${values.length}
WHERE ${primaryKeyColumn} = $${values.length}${typeCastSuffix}
RETURNING *
`;
@@ -640,7 +733,7 @@ export class DynamicFormService {
* 폼 데이터 삭제 (실제 테이블에서 직접 삭제)
*/
async deleteFormData(
id: number,
id: string | number,
tableName: string,
companyCode?: string
): Promise<void> {
@@ -650,12 +743,15 @@ export class DynamicFormService {
tableName,
});
// 1. 먼저 테이블의 기본키 컬럼명을 동적으로 조회
// 1. 먼저 테이블의 기본키 컬럼명과 데이터 타입을 동적으로 조회
const primaryKeyQuery = `
SELECT kcu.column_name
SELECT kcu.column_name, c.data_type
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.columns c
ON kcu.column_name = c.column_name
AND kcu.table_name = c.table_name
WHERE tc.table_name = $1
AND tc.constraint_type = 'PRIMARY KEY'
LIMIT 1
@@ -677,13 +773,37 @@ export class DynamicFormService {
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
}
const primaryKeyColumn = (primaryKeyResult[0] as any).column_name;
console.log("🔑 발견된 기본키 컬럼:", primaryKeyColumn);
const primaryKeyInfo = primaryKeyResult[0] as any;
const primaryKeyColumn = primaryKeyInfo.column_name;
const primaryKeyDataType = primaryKeyInfo.data_type;
console.log("🔑 발견된 기본키:", {
column: primaryKeyColumn,
dataType: primaryKeyDataType,
});
// 2. 동적으로 발견된 기본키를 사용한 DELETE SQL 생성
// 2. 데이터 타입에 맞는 타입 캐스팅 적용
let typeCastSuffix = "";
if (
primaryKeyDataType.includes("character") ||
primaryKeyDataType.includes("text")
) {
typeCastSuffix = "::text";
} else if (
primaryKeyDataType.includes("integer") ||
primaryKeyDataType.includes("bigint")
) {
typeCastSuffix = "::bigint";
} else if (
primaryKeyDataType.includes("numeric") ||
primaryKeyDataType.includes("decimal")
) {
typeCastSuffix = "::numeric";
}
// 3. 동적으로 발견된 기본키와 타입 캐스팅을 사용한 DELETE SQL 생성
const deleteQuery = `
DELETE FROM ${tableName}
WHERE ${primaryKeyColumn} = $1
WHERE ${primaryKeyColumn} = $1${typeCastSuffix}
RETURNING *
`;

View File

@@ -18,6 +18,41 @@ const prisma = new PrismaClient();
export class TableManagementService {
constructor() {}
/**
* 컬럼이 코드 타입인지 확인하고 코드 카테고리 반환
*/
private async getCodeTypeInfo(
tableName: string,
columnName: string
): Promise<{ isCodeType: boolean; codeCategory?: string }> {
try {
// column_labels 테이블에서 해당 컬럼의 web_type이 'code'인지 확인
const result = await prisma.$queryRaw`
SELECT web_type, code_category
FROM column_labels
WHERE table_name = ${tableName}
AND column_name = ${columnName}
AND web_type = 'code'
`;
if (Array.isArray(result) && result.length > 0) {
const row = result[0] as any;
return {
isCodeType: true,
codeCategory: row.code_category,
};
}
return { isCodeType: false };
} catch (error) {
logger.warn(
`코드 타입 컬럼 확인 중 오류: ${tableName}.${columnName}`,
error
);
return { isCodeType: false };
}
}
/**
* 테이블 목록 조회 (PostgreSQL information_schema 활용)
* 메타데이터 조회는 Prisma로 변경 불가
@@ -915,8 +950,36 @@ export class TableManagementService {
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
if (typeof value === "string") {
whereConditions.push(`${safeColumn}::text ILIKE $${paramIndex}`);
searchValues.push(`%${value}%`);
// 🎯 코드 타입 컬럼의 경우 코드값과 코드명 모두로 검색
const codeTypeInfo = await this.getCodeTypeInfo(
tableName,
safeColumn
);
if (codeTypeInfo.isCodeType && codeTypeInfo.codeCategory) {
// 코드 타입 컬럼: 코드값 또는 코드명으로 검색
// 1) 컬럼 값이 직접 검색어와 일치하는 경우
// 2) 컬럼 값이 코드값이고, 해당 코드의 코드명이 검색어와 일치하는 경우
whereConditions.push(`(
${safeColumn}::text ILIKE $${paramIndex} OR
EXISTS (
SELECT 1 FROM code_info ci
WHERE ci.code_category = $${paramIndex + 1}
AND ci.code_value = ${safeColumn}
AND ci.code_name ILIKE $${paramIndex + 2}
)
)`);
searchValues.push(`%${value}%`); // 직접 값 검색용
searchValues.push(codeTypeInfo.codeCategory); // 코드 카테고리
searchValues.push(`%${value}%`); // 코드명 검색용
paramIndex += 2; // 추가 파라미터로 인해 인덱스 증가
} else {
// 일반 컬럼: 기존 방식
whereConditions.push(
`${safeColumn}::text ILIKE $${paramIndex}`
);
searchValues.push(`%${value}%`);
}
} else {
whereConditions.push(`${safeColumn} = $${paramIndex}`);
searchValues.push(value);