Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into external-connections
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 *
|
||||
`;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user