ERP-node/backend-node/src/services/dynamicFormService.ts

1181 lines
38 KiB
TypeScript

import prisma from "../config/database";
import { Prisma } from "@prisma/client";
import { EventTriggerService } from "./eventTriggerService";
import { DataflowControlService } from "./dataflowControlService";
export interface FormDataResult {
id: number;
screenId: number;
tableName: string;
data: Record<string, any>;
createdAt: Date | null;
updatedAt: Date | null;
createdBy: string;
updatedBy: string;
}
export interface PartialUpdateResult {
success: boolean;
data: any;
message: string;
}
export interface PaginatedFormData {
content: FormDataResult[];
totalElements: number;
totalPages: number;
currentPage: number;
size: number;
}
export interface ValidationError {
field: string;
message: string;
code: string;
}
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
}
export interface TableColumn {
columnName: string;
dataType: string;
nullable: boolean;
primaryKey: boolean;
maxLength?: number;
defaultValue?: any;
}
export class DynamicFormService {
private dataflowControlService = new DataflowControlService();
/**
* 값을 PostgreSQL 타입에 맞게 변환
*/
private convertValueForPostgreSQL(value: any, dataType: string): any {
if (value === null || value === undefined || value === "") {
return null;
}
const lowerDataType = dataType.toLowerCase();
// 숫자 타입 처리
if (
lowerDataType.includes("integer") ||
lowerDataType.includes("bigint") ||
lowerDataType.includes("serial")
) {
return parseInt(value) || null;
}
if (
lowerDataType.includes("numeric") ||
lowerDataType.includes("decimal") ||
lowerDataType.includes("real") ||
lowerDataType.includes("double")
) {
return parseFloat(value) || null;
}
// 불린 타입 처리
if (lowerDataType.includes("boolean")) {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
return value.toLowerCase() === "true" || value === "1";
}
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;
}
/**
* 테이블의 컬럼 정보 조회 (타입 포함)
*/
private async getTableColumnInfo(
tableName: string
): Promise<Array<{ column_name: string; data_type: string }>> {
try {
const result = await prisma.$queryRaw<
Array<{ column_name: string; data_type: string }>
>`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = ${tableName}
AND table_schema = 'public'
`;
return result;
} catch (error) {
console.error(`테이블 ${tableName}의 컬럼 정보 조회 실패:`, error);
return [];
}
}
/**
* 테이블의 컬럼명 목록 조회 (간단 버전)
*/
private async getTableColumnNames(tableName: string): Promise<string[]> {
try {
const result = (await prisma.$queryRawUnsafe(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = '${tableName}'
AND table_schema = 'public'
`)) as any[];
return result.map((row) => row.column_name);
} catch (error) {
console.error(`❌ 테이블 ${tableName} 컬럼 정보 조회 실패:`, error);
return [];
}
}
/**
* 테이블의 Primary Key 컬럼 조회 (공개 메서드로 변경)
*/
async getTablePrimaryKeys(tableName: string): Promise<string[]> {
try {
const result = (await prisma.$queryRawUnsafe(`
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = '${tableName}'
AND tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = 'public'
`)) as any[];
return result.map((row) => row.column_name);
} catch (error) {
console.error(`❌ 테이블 ${tableName} Primary Key 조회 실패:`, error);
return [];
}
}
/**
* 폼 데이터 저장 (실제 테이블에 직접 저장)
*/
async saveFormData(
screenId: number,
tableName: string,
data: Record<string, any>
): Promise<FormDataResult> {
try {
console.log("💾 서비스: 실제 테이블에 폼 데이터 저장 시작:", {
screenId,
tableName,
data,
});
// 테이블의 실제 컬럼 정보와 Primary Key 조회
const tableColumns = await this.getTableColumnNames(tableName);
const primaryKeys = await this.getTablePrimaryKeys(tableName);
console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns);
console.log(`🔑 테이블 ${tableName}의 Primary Key:`, primaryKeys);
// 메타데이터 제거 (실제 테이블 컬럼이 아님)
const { created_by, updated_by, company_code, screen_id, ...actualData } =
data;
// 기본 데이터 준비
const dataToInsert: any = { ...actualData };
// 테이블에 존재하는 공통 필드들만 추가
if (tableColumns.includes("created_at")) {
dataToInsert.created_at = new Date();
}
if (tableColumns.includes("updated_at")) {
dataToInsert.updated_at = new Date();
}
if (tableColumns.includes("regdate") && !dataToInsert.regdate) {
dataToInsert.regdate = new Date();
}
// 생성자/수정자 정보가 있고 해당 컬럼이 존재한다면 추가
if (created_by && tableColumns.includes("created_by")) {
dataToInsert.created_by = created_by;
}
if (updated_by && tableColumns.includes("updated_by")) {
dataToInsert.updated_by = updated_by;
}
if (company_code && tableColumns.includes("company_code")) {
// company_code가 UUID 형태(36자)라면 하이픈 제거하여 32자로 만듦
let processedCompanyCode = company_code;
if (
typeof company_code === "string" &&
company_code.length === 36 &&
company_code.includes("-")
) {
processedCompanyCode = company_code.replace(/-/g, "");
console.log(
`🔧 company_code 길이 조정: "${company_code}" -> "${processedCompanyCode}" (${processedCompanyCode.length}자)`
);
}
// 여전히 32자를 초과하면 앞의 32자만 사용
if (
typeof processedCompanyCode === "string" &&
processedCompanyCode.length > 32
) {
processedCompanyCode = processedCompanyCode.substring(0, 32);
console.log(
`⚠️ company_code 길이 제한: 앞의 32자로 자름 -> "${processedCompanyCode}"`
);
}
dataToInsert.company_code = processedCompanyCode;
}
// 날짜/시간 문자열을 적절한 형태로 변환
Object.keys(dataToInsert).forEach((key) => {
const value = dataToInsert[key];
// 날짜/시간 관련 컬럼명 패턴 체크 (regdate, created_at, updated_at 등)
if (
typeof value === "string" &&
(key.toLowerCase().includes("date") ||
key.toLowerCase().includes("time") ||
key.toLowerCase().includes("created") ||
key.toLowerCase().includes("updated") ||
key.toLowerCase().includes("reg"))
) {
// YYYY-MM-DD HH:mm:ss 형태의 문자열을 Date 객체로 변환
if (value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) {
console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`);
dataToInsert[key] = new Date(value);
}
// YYYY-MM-DD 형태의 문자열을 Date 객체로 변환
else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) {
console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`);
dataToInsert[key] = new Date(value + "T00:00:00");
}
}
});
// 존재하지 않는 컬럼 제거
Object.keys(dataToInsert).forEach((key) => {
if (!tableColumns.includes(key)) {
console.log(
`⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제거됨`
);
delete dataToInsert[key];
}
});
console.log("🎯 실제 테이블에 삽입할 데이터:", {
tableName,
dataToInsert,
});
// 테이블 컬럼 정보 조회하여 타입 변환 적용
console.log("🔍 테이블 컬럼 정보 조회 중...");
const columnInfo = await this.getTableColumnInfo(tableName);
console.log("📊 테이블 컬럼 정보:", columnInfo);
// 각 컬럼의 타입에 맞게 데이터 변환
Object.keys(dataToInsert).forEach((columnName) => {
const column = columnInfo.find((col) => col.column_name === columnName);
if (column) {
const originalValue = dataToInsert[columnName];
const convertedValue = this.convertValueForPostgreSQL(
originalValue,
column.data_type
);
if (originalValue !== convertedValue) {
console.log(
`🔄 타입 변환: ${columnName} (${column.data_type}) = "${originalValue}" -> ${convertedValue}`
);
dataToInsert[columnName] = convertedValue;
}
}
});
console.log("✅ 타입 변환 완료된 데이터:", dataToInsert);
// 동적 SQL을 사용하여 실제 테이블에 UPSERT
const columns = Object.keys(dataToInsert);
const values: any[] = Object.values(dataToInsert);
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
let upsertQuery: string;
if (primaryKeys.length > 0) {
// Primary Key가 있는 경우 UPSERT 사용
const conflictColumns = primaryKeys.join(", ");
const updateSet = columns
.filter((col) => !primaryKeys.includes(col)) // Primary Key는 UPDATE에서 제외
.map((col) => `${col} = EXCLUDED.${col}`)
.join(", ");
if (updateSet) {
upsertQuery = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
ON CONFLICT (${conflictColumns})
DO UPDATE SET ${updateSet}
RETURNING *
`;
} else {
// 업데이트할 컬럼이 없는 경우 (Primary Key만 있는 테이블)
upsertQuery = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
ON CONFLICT (${conflictColumns})
DO NOTHING
RETURNING *
`;
}
} else {
// Primary Key가 없는 경우 일반 INSERT
upsertQuery = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
RETURNING *
`;
}
console.log("📝 실행할 UPSERT SQL:", upsertQuery);
console.log("📊 SQL 파라미터:", values);
const result = await prisma.$queryRawUnsafe(upsertQuery, ...values);
console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
// 결과를 표준 형식으로 변환
const insertedRecord = Array.isArray(result) ? result[0] : result;
// 🔥 조건부 연결 실행 (INSERT 트리거)
try {
if (company_code) {
await EventTriggerService.executeEventTriggers(
"insert",
tableName,
insertedRecord as Record<string, any>,
company_code
);
console.log("🚀 조건부 연결 트리거 실행 완료 (INSERT)");
}
} catch (triggerError) {
console.error("⚠️ 조건부 연결 트리거 실행 오류:", triggerError);
// 트리거 오류는 로그만 남기고 메인 저장 프로세스는 계속 진행
}
// 🎯 제어관리 실행 (새로 추가)
try {
await this.executeDataflowControlIfConfigured(
screenId,
tableName,
insertedRecord as Record<string, any>,
"insert"
);
} catch (controlError) {
console.error("⚠️ 제어관리 실행 오류:", controlError);
// 제어관리 오류는 로그만 남기고 메인 저장 프로세스는 계속 진행
}
return {
id: insertedRecord.id || insertedRecord.objid || 0,
screenId: screenId,
tableName: tableName,
data: insertedRecord as Record<string, any>,
createdAt: insertedRecord.created_at || new Date(),
updatedAt: insertedRecord.updated_at || new Date(),
createdBy: insertedRecord.created_by || created_by || "system",
updatedBy: insertedRecord.updated_by || updated_by || "system",
};
} catch (error) {
console.error("❌ 서비스: 실제 테이블 저장 실패:", error);
throw new Error(`실제 테이블 저장 실패: ${error}`);
}
}
/**
* 폼 데이터 부분 업데이트 (변경된 필드만 업데이트)
*/
async updateFormDataPartial(
id: number,
tableName: string,
originalData: Record<string, any>,
newData: Record<string, any>
): Promise<PartialUpdateResult> {
try {
console.log("🔄 서비스: 부분 업데이트 시작:", {
id,
tableName,
originalData,
newData,
});
// 테이블의 실제 컬럼 정보 조회
const tableColumns = await this.getTableColumnNames(tableName);
console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns);
// 변경된 필드만 찾기
const changedFields: Record<string, any> = {};
for (const [key, value] of Object.entries(newData)) {
// 메타데이터 필드 제외
if (
["created_by", "updated_by", "company_code", "screen_id"].includes(
key
)
) {
continue;
}
// 테이블에 존재하지 않는 컬럼 제외
if (!tableColumns.includes(key)) {
console.log(
`⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제외됨`
);
continue;
}
// 값이 실제로 변경된 경우만 포함
if (originalData[key] !== value) {
changedFields[key] = value;
console.log(
`📝 변경된 필드: ${key} = "${originalData[key]}" → "${value}"`
);
}
}
// 변경된 필드가 없으면 업데이트 건너뛰기
if (Object.keys(changedFields).length === 0) {
console.log("📋 변경된 필드가 없습니다. 업데이트를 건너뜁니다.");
return {
success: true,
data: originalData,
message: "변경사항이 없어 업데이트하지 않았습니다.",
};
}
// 업데이트 관련 필드 추가 (변경사항이 있는 경우에만)
if (tableColumns.includes("updated_at")) {
changedFields.updated_at = new Date();
}
console.log("🎯 실제 업데이트할 필드들:", changedFields);
// 동적으로 기본키 조회
const primaryKeys = await this.getTablePrimaryKeys(tableName);
if (!primaryKeys || primaryKeys.length === 0) {
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
}
const primaryKeyColumn = primaryKeys[0];
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
// 동적 UPDATE SQL 생성 (변경된 필드만)
const setClause = Object.keys(changedFields)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const values: any[] = Object.values(changedFields);
values.push(id); // WHERE 조건용 ID 추가
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${primaryKeyColumn} = $${values.length}::text
RETURNING *
`;
console.log("📝 실행할 부분 UPDATE SQL:", updateQuery);
console.log("📊 SQL 파라미터:", values);
const result = await prisma.$queryRawUnsafe(updateQuery, ...values);
console.log("✅ 서비스: 부분 업데이트 성공:", result);
const updatedRecord = Array.isArray(result) ? result[0] : result;
return {
success: true,
data: updatedRecord,
message: "데이터가 성공적으로 업데이트되었습니다.",
};
} catch (error: any) {
console.error("❌ 서비스: 부분 업데이트 실패:", error);
throw new Error(`부분 업데이트 실패: ${error}`);
}
}
/**
* 폼 데이터 업데이트 (실제 테이블에서 직접 업데이트)
*/
async updateFormData(
id: string | number,
tableName: string,
data: Record<string, any>
): Promise<FormDataResult> {
try {
console.log("🔄 서비스: 실제 테이블에서 폼 데이터 업데이트 시작:", {
id,
tableName,
data,
});
// 테이블의 실제 컬럼 정보 조회
const tableColumns = await this.getTableColumnNames(tableName);
console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns);
// 메타데이터 제거
const { created_by, updated_by, company_code, screen_id, ...actualData } =
data;
// 기본 데이터 준비
const dataToUpdate: any = { ...actualData };
// 테이블에 존재하는 업데이트 관련 필드들만 추가
if (tableColumns.includes("updated_at")) {
dataToUpdate.updated_at = new Date();
}
if (tableColumns.includes("regdate") && !dataToUpdate.regdate) {
dataToUpdate.regdate = new Date();
}
// 수정자 정보가 있고 해당 컬럼이 존재한다면 추가
if (updated_by && tableColumns.includes("updated_by")) {
dataToUpdate.updated_by = updated_by;
}
// 존재하지 않는 컬럼 제거
Object.keys(dataToUpdate).forEach((key) => {
if (!tableColumns.includes(key)) {
console.log(
`⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제거됨`
);
delete dataToUpdate[key];
}
});
// 컬럼 타입에 맞는 데이터 변환 (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,
dataToUpdate,
});
// 동적 UPDATE SQL 생성
const setClause = Object.keys(dataToUpdate)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const values: any[] = Object.values(dataToUpdate);
values.push(id); // WHERE 조건용 ID 추가
// 동적으로 기본키 조회
const primaryKeys = await this.getTablePrimaryKeys(tableName);
if (!primaryKeys || primaryKeys.length === 0) {
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
}
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}${typeCastSuffix}
RETURNING *
`;
console.log("📝 실행할 UPDATE SQL:", updateQuery);
console.log("📊 SQL 파라미터:", values);
const result = await prisma.$queryRawUnsafe(updateQuery, ...values);
console.log("✅ 서비스: 실제 테이블 업데이트 성공:", result);
const updatedRecord = Array.isArray(result) ? result[0] : result;
// 🔥 조건부 연결 실행 (UPDATE 트리거)
try {
if (company_code) {
await EventTriggerService.executeEventTriggers(
"update",
tableName,
updatedRecord as Record<string, any>,
company_code
);
console.log("🚀 조건부 연결 트리거 실행 완료 (UPDATE)");
}
} catch (triggerError) {
console.error("⚠️ 조건부 연결 트리거 실행 오류:", triggerError);
// 트리거 오류는 로그만 남기고 메인 업데이트 프로세스는 계속 진행
}
// 🎯 제어관리 실행 (UPDATE 트리거)
try {
await this.executeDataflowControlIfConfigured(
0, // UPDATE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
tableName,
updatedRecord as Record<string, any>,
"update"
);
} catch (controlError) {
console.error("⚠️ 제어관리 실행 오류:", controlError);
// 제어관리 오류는 로그만 남기고 메인 업데이트 프로세스는 계속 진행
}
return {
id: updatedRecord.id || updatedRecord.objid || id,
screenId: 0, // 실제 테이블에는 screenId가 없으므로 0으로 설정
tableName: tableName,
data: updatedRecord as Record<string, any>,
createdAt: updatedRecord.created_at || new Date(),
updatedAt: updatedRecord.updated_at || new Date(),
createdBy: updatedRecord.created_by || "system",
updatedBy: updatedRecord.updated_by || updated_by || "system",
};
} catch (error) {
console.error("❌ 서비스: 실제 테이블 업데이트 실패:", error);
throw new Error(`실제 테이블 업데이트 실패: ${error}`);
}
}
/**
* 폼 데이터 삭제 (실제 테이블에서 직접 삭제)
*/
async deleteFormData(
id: string | number,
tableName: string,
companyCode?: string
): Promise<void> {
try {
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
id,
tableName,
});
// 1. 먼저 테이블의 기본키 컬럼명과 데이터 타입을 동적으로 조회
const primaryKeyQuery = `
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
`;
console.log("🔍 기본키 조회 SQL:", primaryKeyQuery);
console.log("🔍 테이블명:", tableName);
const primaryKeyResult = await prisma.$queryRawUnsafe(
primaryKeyQuery,
tableName
);
if (
!primaryKeyResult ||
!Array.isArray(primaryKeyResult) ||
primaryKeyResult.length === 0
) {
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
}
const primaryKeyInfo = primaryKeyResult[0] as any;
const primaryKeyColumn = primaryKeyInfo.column_name;
const primaryKeyDataType = primaryKeyInfo.data_type;
console.log("🔑 발견된 기본키:", {
column: primaryKeyColumn,
dataType: primaryKeyDataType,
});
// 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${typeCastSuffix}
RETURNING *
`;
console.log("📝 실행할 DELETE SQL:", deleteQuery);
console.log("📊 SQL 파라미터:", [id]);
const result = await prisma.$queryRawUnsafe(deleteQuery, id);
console.log("✅ 서비스: 실제 테이블 삭제 성공:", result);
// 🔥 조건부 연결 실행 (DELETE 트리거)
try {
if (
companyCode &&
result &&
Array.isArray(result) &&
result.length > 0
) {
const deletedRecord = result[0] as Record<string, any>;
await EventTriggerService.executeEventTriggers(
"delete",
tableName,
deletedRecord,
companyCode
);
console.log("🚀 조건부 연결 트리거 실행 완료 (DELETE)");
}
} catch (triggerError) {
console.error("⚠️ 조건부 연결 트리거 실행 오류:", triggerError);
// 트리거 오류는 로그만 남기고 메인 삭제 프로세스는 계속 진행
}
// 🎯 제어관리 실행 (DELETE 트리거)
try {
if (result && Array.isArray(result) && result.length > 0) {
const deletedRecord = result[0] as Record<string, any>;
await this.executeDataflowControlIfConfigured(
0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
tableName,
deletedRecord,
"delete"
);
}
} catch (controlError) {
console.error("⚠️ 제어관리 실행 오류:", controlError);
// 제어관리 오류는 로그만 남기고 메인 삭제 프로세스는 계속 진행
}
} catch (error) {
console.error("❌ 서비스: 실제 테이블 삭제 실패:", error);
throw new Error(`실제 테이블 삭제 실패: ${error}`);
}
}
/**
* 단일 폼 데이터 조회
*/
async getFormData(id: number): Promise<FormDataResult | null> {
try {
console.log("📄 서비스: 폼 데이터 단건 조회 시작:", { id });
const result = await prisma.dynamic_form_data.findUnique({
where: { id },
});
if (!result) {
console.log("❌ 서비스: 폼 데이터를 찾을 수 없음");
return null;
}
console.log("✅ 서비스: 폼 데이터 단건 조회 성공");
return {
id: result.id,
screenId: result.screen_id,
tableName: result.table_name,
data: result.form_data as Record<string, any>,
createdAt: result.created_at,
updatedAt: result.updated_at,
createdBy: result.created_by,
updatedBy: result.updated_by,
};
} catch (error) {
console.error("❌ 서비스: 폼 데이터 단건 조회 실패:", error);
throw new Error(`폼 데이터 조회 실패: ${error}`);
}
}
/**
* 화면별 폼 데이터 목록 조회 (페이징)
*/
async getFormDataList(
screenId: number,
params: {
page: number;
size: number;
search?: string;
sortBy?: string;
sortOrder?: "asc" | "desc";
}
): Promise<PaginatedFormData> {
try {
console.log("📋 서비스: 폼 데이터 목록 조회 시작:", { screenId, params });
const {
page,
size,
search,
sortBy = "created_at",
sortOrder = "desc",
} = params;
const skip = (page - 1) * size;
// 검색 조건 구성
const where: Prisma.dynamic_form_dataWhereInput = {
screen_id: screenId,
};
// 검색어가 있는 경우 form_data 필드에서 검색
if (search) {
where.OR = [
{
form_data: {
path: [],
string_contains: search,
},
},
{
table_name: {
contains: search,
mode: "insensitive",
},
},
];
}
// 정렬 조건 구성
const orderBy: Prisma.dynamic_form_dataOrderByWithRelationInput = {};
if (sortBy === "created_at" || sortBy === "updated_at") {
orderBy[sortBy] = sortOrder;
} else {
orderBy.created_at = "desc"; // 기본값
}
// 데이터 조회
const [results, totalCount] = await Promise.all([
prisma.dynamic_form_data.findMany({
where,
orderBy,
skip,
take: size,
}),
prisma.dynamic_form_data.count({ where }),
]);
const formDataResults: FormDataResult[] = results.map((result) => ({
id: result.id,
screenId: result.screen_id,
tableName: result.table_name,
data: result.form_data as Record<string, any>,
createdAt: result.created_at,
updatedAt: result.updated_at,
createdBy: result.created_by,
updatedBy: result.updated_by,
}));
const totalPages = Math.ceil(totalCount / size);
console.log("✅ 서비스: 폼 데이터 목록 조회 성공:", {
totalCount,
totalPages,
currentPage: page,
});
return {
content: formDataResults,
totalElements: totalCount,
totalPages,
currentPage: page,
size,
};
} catch (error) {
console.error("❌ 서비스: 폼 데이터 목록 조회 실패:", error);
throw new Error(`폼 데이터 목록 조회 실패: ${error}`);
}
}
/**
* 폼 데이터 검증
*/
async validateFormData(
tableName: string,
data: Record<string, any>
): Promise<ValidationResult> {
try {
console.log("✅ 서비스: 폼 데이터 검증 시작:", { tableName, data });
const errors: ValidationError[] = [];
// 기본 검증 로직 (실제로는 테이블 스키마를 확인해야 함)
Object.entries(data).forEach(([key, value]) => {
// 예시: 빈 값 검증
if (value === null || value === undefined || value === "") {
// 특정 필드가 required인지 확인하는 로직이 필요
// 지금은 간단히 모든 필드를 선택사항으로 처리
}
// 예시: 데이터 타입 검증
// 실제로는 테이블 스키마의 컬럼 타입과 비교해야 함
});
const result: ValidationResult = {
valid: errors.length === 0,
errors,
};
console.log("✅ 서비스: 폼 데이터 검증 완료:", result);
return result;
} catch (error) {
console.error("❌ 서비스: 폼 데이터 검증 실패:", error);
throw new Error(`폼 데이터 검증 실패: ${error}`);
}
}
/**
* 테이블 컬럼 정보 조회 (PostgreSQL 시스템 테이블 활용)
*/
async getTableColumns(tableName: string): Promise<TableColumn[]> {
try {
console.log("📊 서비스: 테이블 컬럼 정보 조회 시작:", { tableName });
// PostgreSQL의 information_schema를 사용하여 컬럼 정보 조회
const columns = await prisma.$queryRaw<any[]>`
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length
FROM information_schema.columns
WHERE table_name = ${tableName}
AND table_schema = 'public'
ORDER BY ordinal_position
`;
// Primary key 정보 조회
const primaryKeys = await prisma.$queryRaw<any[]>`
SELECT
kcu.column_name
FROM
information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE
tc.constraint_type = 'PRIMARY KEY'
AND tc.table_name = ${tableName}
AND tc.table_schema = 'public'
`;
const primaryKeyColumns = new Set(
primaryKeys.map((pk) => pk.column_name)
);
const result: TableColumn[] = columns.map((col) => ({
columnName: col.column_name,
dataType: col.data_type,
nullable: col.is_nullable === "YES",
primaryKey: primaryKeyColumns.has(col.column_name),
maxLength: col.character_maximum_length,
defaultValue: col.column_default,
}));
console.log("✅ 서비스: 테이블 컬럼 정보 조회 성공:", result);
return result;
} catch (error) {
console.error("❌ 서비스: 테이블 컬럼 정보 조회 실패:", error);
throw new Error(`테이블 컬럼 정보 조회 실패: ${error}`);
}
}
/**
* 제어관리 실행 (화면에 설정된 경우)
*/
private async executeDataflowControlIfConfigured(
screenId: number,
tableName: string,
savedData: Record<string, any>,
triggerType: "insert" | "update" | "delete"
): Promise<void> {
try {
console.log(`🎯 제어관리 설정 확인 중... (screenId: ${screenId})`);
// 화면의 저장 버튼에서 제어관리 설정 조회
const screenLayouts = await prisma.screen_layouts.findMany({
where: {
screen_id: screenId,
component_type: "component",
},
});
console.log(`📋 화면 컴포넌트 조회 결과:`, screenLayouts.length);
// 저장 버튼 중에서 제어관리가 활성화된 것 찾기
for (const layout of screenLayouts) {
const properties = layout.properties as any;
// 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우
if (
properties?.componentType === "button-primary" &&
properties?.componentConfig?.action?.type === "save" &&
properties?.webTypeConfig?.enableDataflowControl === true &&
properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId
) {
const diagramId =
properties.webTypeConfig.dataflowConfig.selectedDiagramId;
const relationshipId =
properties.webTypeConfig.dataflowConfig.selectedRelationshipId;
console.log(`🎯 제어관리 설정 발견:`, {
componentId: layout.component_id,
diagramId,
relationshipId,
triggerType,
});
// 제어관리 실행
const controlResult =
await this.dataflowControlService.executeDataflowControl(
diagramId,
relationshipId,
triggerType,
savedData,
tableName
);
console.log(`🎯 제어관리 실행 결과:`, controlResult);
if (controlResult.success) {
console.log(`✅ 제어관리 실행 성공: ${controlResult.message}`);
if (
controlResult.executedActions &&
controlResult.executedActions.length > 0
) {
console.log(`📊 실행된 액션들:`, controlResult.executedActions);
}
// 오류가 있는 경우 경고 로그 출력 (성공이지만 일부 액션 실패)
if (controlResult.errors && controlResult.errors.length > 0) {
console.warn(
`⚠️ 제어관리 실행 중 일부 오류 발생:`,
controlResult.errors
);
// 오류 정보를 별도로 저장하여 필요시 사용자에게 알림 가능
// 현재는 로그만 출력하고 메인 저장 프로세스는 계속 진행
}
} else {
console.warn(`⚠️ 제어관리 실행 실패: ${controlResult.message}`);
// 제어관리 실패는 메인 저장 프로세스에 영향을 주지 않음
}
// 첫 번째 설정된 제어관리만 실행 (여러 개가 있을 경우)
break;
}
}
} catch (error) {
console.error("❌ 제어관리 설정 확인 및 실행 오류:", error);
// 에러를 다시 던지지 않음 - 메인 저장 프로세스에 영향 주지 않기 위해
}
}
}
// 싱글톤 인스턴스 생성 및 export
export const dynamicFormService = new DynamicFormService();