Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary, ; especially if it merges an updated upstream into a topic branch. ; ; Lines starting with ';' will be ignored, and an empty message aborts ; the commit.
This commit is contained in:
@@ -103,12 +103,16 @@ export class DynamicFormService {
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
// DATE 타입이면 문자열 그대로 유지
|
||||
if (lowerDataType === "date") {
|
||||
console.log(`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`);
|
||||
console.log(
|
||||
`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`
|
||||
);
|
||||
return value; // 문자열 그대로 반환
|
||||
}
|
||||
// TIMESTAMP 타입이면 Date 객체로 변환
|
||||
else {
|
||||
console.log(`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`);
|
||||
console.log(
|
||||
`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`
|
||||
);
|
||||
return new Date(value + "T00:00:00");
|
||||
}
|
||||
}
|
||||
@@ -250,7 +254,8 @@ export class DynamicFormService {
|
||||
if (tableColumns.includes("regdate") && !dataToInsert.regdate) {
|
||||
dataToInsert.regdate = new Date();
|
||||
}
|
||||
if (tableColumns.includes("created_date") && !dataToInsert.created_date) {
|
||||
// created_date는 항상 현재 시간으로 설정 (기존 값 무시)
|
||||
if (tableColumns.includes("created_date")) {
|
||||
dataToInsert.created_date = new Date();
|
||||
}
|
||||
if (tableColumns.includes("updated_date") && !dataToInsert.updated_date) {
|
||||
@@ -313,7 +318,9 @@ export class DynamicFormService {
|
||||
}
|
||||
// YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장)
|
||||
else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||
console.log(`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`);
|
||||
console.log(
|
||||
`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`
|
||||
);
|
||||
// dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식)
|
||||
}
|
||||
}
|
||||
@@ -346,35 +353,37 @@ export class DynamicFormService {
|
||||
) {
|
||||
try {
|
||||
parsedArray = JSON.parse(value);
|
||||
console.log(
|
||||
console.log(
|
||||
`🔄 JSON 문자열 Repeater 데이터 감지: ${key}, ${parsedArray?.length || 0}개 항목`
|
||||
);
|
||||
);
|
||||
} catch (parseError) {
|
||||
console.log(`⚠️ JSON 파싱 실패: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 파싱된 배열이 있으면 처리
|
||||
if (parsedArray && Array.isArray(parsedArray) && parsedArray.length > 0) {
|
||||
// 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해)
|
||||
// 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음
|
||||
let targetTable: string | undefined;
|
||||
let actualData = parsedArray;
|
||||
if (
|
||||
parsedArray &&
|
||||
Array.isArray(parsedArray) &&
|
||||
parsedArray.length > 0
|
||||
) {
|
||||
// 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해)
|
||||
// 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음
|
||||
let targetTable: string | undefined;
|
||||
let actualData = parsedArray;
|
||||
|
||||
// 첫 번째 항목에 _targetTable이 있는지 확인 (프론트엔드에서 메타데이터 전달)
|
||||
if (parsedArray[0] && parsedArray[0]._targetTable) {
|
||||
targetTable = parsedArray[0]._targetTable;
|
||||
actualData = parsedArray.map(
|
||||
({ _targetTable, ...item }) => item
|
||||
);
|
||||
}
|
||||
// 첫 번째 항목에 _targetTable이 있는지 확인 (프론트엔드에서 메타데이터 전달)
|
||||
if (parsedArray[0] && parsedArray[0]._targetTable) {
|
||||
targetTable = parsedArray[0]._targetTable;
|
||||
actualData = parsedArray.map(({ _targetTable, ...item }) => item);
|
||||
}
|
||||
|
||||
repeaterData.push({
|
||||
data: actualData,
|
||||
targetTable,
|
||||
componentId: key,
|
||||
});
|
||||
delete dataToInsert[key]; // 원본 배열 데이터는 제거
|
||||
repeaterData.push({
|
||||
data: actualData,
|
||||
targetTable,
|
||||
componentId: key,
|
||||
});
|
||||
delete dataToInsert[key]; // 원본 배열 데이터는 제거
|
||||
|
||||
console.log(`✅ Repeater 데이터 추가: ${key}`, {
|
||||
targetTable: targetTable || "없음 (화면 설계에서 설정 필요)",
|
||||
@@ -387,8 +396,8 @@ export class DynamicFormService {
|
||||
// 🔥 Repeater targetTable이 메인 테이블과 같으면 분리해서 저장
|
||||
const separateRepeaterData: typeof repeaterData = [];
|
||||
const mergedRepeaterData: typeof repeaterData = [];
|
||||
|
||||
repeaterData.forEach(repeater => {
|
||||
|
||||
repeaterData.forEach((repeater) => {
|
||||
if (repeater.targetTable && repeater.targetTable !== tableName) {
|
||||
// 다른 테이블: 나중에 별도 저장
|
||||
separateRepeaterData.push(repeater);
|
||||
@@ -397,10 +406,10 @@ export class DynamicFormService {
|
||||
mergedRepeaterData.push(repeater);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log(`🔄 Repeater 데이터 분류:`, {
|
||||
separate: separateRepeaterData.length, // 별도 테이블
|
||||
merged: mergedRepeaterData.length, // 메인 테이블과 병합
|
||||
merged: mergedRepeaterData.length, // 메인 테이블과 병합
|
||||
});
|
||||
|
||||
// 존재하지 않는 컬럼 제거
|
||||
@@ -494,23 +503,30 @@ export class DynamicFormService {
|
||||
const clientIp = ipAddress || "unknown";
|
||||
|
||||
let result: any[];
|
||||
|
||||
|
||||
// 🔥 메인 테이블과 병합할 Repeater가 있으면 각 품목별로 INSERT
|
||||
if (mergedRepeaterData.length > 0) {
|
||||
console.log(`🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장`);
|
||||
|
||||
console.log(
|
||||
`🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장`
|
||||
);
|
||||
|
||||
result = [];
|
||||
|
||||
|
||||
for (const repeater of mergedRepeaterData) {
|
||||
for (const item of repeater.data) {
|
||||
// 헤더 + 품목을 병합
|
||||
const rawMergedData = { ...dataToInsert, ...item };
|
||||
|
||||
// item에서 created_date 제거 (dataToInsert의 현재 시간 유지)
|
||||
const { created_date: _, ...itemWithoutCreatedDate } = item;
|
||||
const rawMergedData = {
|
||||
...dataToInsert,
|
||||
...itemWithoutCreatedDate,
|
||||
};
|
||||
|
||||
// 🆕 새 레코드 저장 시 id 제거하여 새 UUID 생성되도록 함
|
||||
// _existingRecord가 명시적으로 true인 경우에만 기존 레코드로 처리 (UPDATE)
|
||||
// 그 외의 경우는 모두 새 레코드로 처리 (INSERT)
|
||||
const isExistingRecord = rawMergedData._existingRecord === true;
|
||||
|
||||
|
||||
if (!isExistingRecord) {
|
||||
// 새 레코드: id 제거하여 새 UUID 자동 생성
|
||||
const oldId = rawMergedData.id;
|
||||
@@ -519,37 +535,43 @@ export class DynamicFormService {
|
||||
} else {
|
||||
console.log(`📝 기존 레코드 수정 (id 유지: ${rawMergedData.id})`);
|
||||
}
|
||||
|
||||
|
||||
// 메타 플래그 제거
|
||||
delete rawMergedData._isNewItem;
|
||||
delete rawMergedData._existingRecord;
|
||||
|
||||
|
||||
// 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외)
|
||||
const validColumnNames = columnInfo.map((col) => col.column_name);
|
||||
const mergedData: Record<string, any> = {};
|
||||
|
||||
|
||||
Object.keys(rawMergedData).forEach((columnName) => {
|
||||
// 실제 테이블 컬럼인지 확인
|
||||
if (validColumnNames.includes(columnName)) {
|
||||
const column = columnInfo.find((col) => col.column_name === columnName);
|
||||
if (column) {
|
||||
// 타입 변환
|
||||
mergedData[columnName] = this.convertValueForPostgreSQL(
|
||||
rawMergedData[columnName],
|
||||
column.data_type
|
||||
const column = columnInfo.find(
|
||||
(col) => col.column_name === columnName
|
||||
);
|
||||
if (column) {
|
||||
// 타입 변환
|
||||
mergedData[columnName] = this.convertValueForPostgreSQL(
|
||||
rawMergedData[columnName],
|
||||
column.data_type
|
||||
);
|
||||
} else {
|
||||
mergedData[columnName] = rawMergedData[columnName];
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`);
|
||||
console.log(
|
||||
`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const mergedColumns = Object.keys(mergedData);
|
||||
const mergedValues: any[] = Object.values(mergedData);
|
||||
const mergedPlaceholders = mergedValues.map((_, index) => `$${index + 1}`).join(", ");
|
||||
|
||||
const mergedPlaceholders = mergedValues
|
||||
.map((_, index) => `$${index + 1}`)
|
||||
.join(", ");
|
||||
|
||||
let mergedUpsertQuery: string;
|
||||
if (primaryKeys.length > 0) {
|
||||
const conflictColumns = primaryKeys.join(", ");
|
||||
@@ -557,7 +579,7 @@ export class DynamicFormService {
|
||||
.filter((col) => !primaryKeys.includes(col))
|
||||
.map((col) => `${col} = EXCLUDED.${col}`)
|
||||
.join(", ");
|
||||
|
||||
|
||||
mergedUpsertQuery = updateSet
|
||||
? `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
|
||||
VALUES (${mergedPlaceholders})
|
||||
@@ -574,20 +596,20 @@ export class DynamicFormService {
|
||||
VALUES (${mergedPlaceholders})
|
||||
RETURNING *`;
|
||||
}
|
||||
|
||||
|
||||
console.log(`📝 병합 INSERT:`, { mergedData });
|
||||
|
||||
|
||||
const itemResult = await transaction(async (client) => {
|
||||
await client.query(`SET LOCAL app.user_id = '${userId}'`);
|
||||
await client.query(`SET LOCAL app.ip_address = '${clientIp}'`);
|
||||
const res = await client.query(mergedUpsertQuery, mergedValues);
|
||||
return res.rows[0];
|
||||
});
|
||||
|
||||
|
||||
result.push(itemResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(`✅ 병합 저장 완료: ${result.length}개 레코드`);
|
||||
} else {
|
||||
// 일반 모드: 헤더만 저장
|
||||
@@ -597,7 +619,7 @@ export class DynamicFormService {
|
||||
const res = await client.query(upsertQuery, values);
|
||||
return res.rows;
|
||||
});
|
||||
|
||||
|
||||
console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
|
||||
}
|
||||
|
||||
@@ -732,12 +754,19 @@ export class DynamicFormService {
|
||||
|
||||
// 🎯 제어관리 실행 (새로 추가)
|
||||
try {
|
||||
// savedData 또는 insertedRecord에서 company_code 추출
|
||||
const recordCompanyCode =
|
||||
(insertedRecord as Record<string, any>)?.company_code ||
|
||||
dataToInsert.company_code ||
|
||||
"*";
|
||||
|
||||
await this.executeDataflowControlIfConfigured(
|
||||
screenId,
|
||||
tableName,
|
||||
insertedRecord as Record<string, any>,
|
||||
"insert",
|
||||
created_by || "system"
|
||||
created_by || "system",
|
||||
recordCompanyCode
|
||||
);
|
||||
} catch (controlError) {
|
||||
console.error("⚠️ 제어관리 실행 오류:", controlError);
|
||||
@@ -843,10 +872,10 @@ export class DynamicFormService {
|
||||
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 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;
|
||||
@@ -859,11 +888,20 @@ export class DynamicFormService {
|
||||
.map((key, index) => {
|
||||
const dataType = columnTypes[key];
|
||||
// 숫자 타입인 경우 명시적 캐스팅
|
||||
if (dataType === 'integer' || dataType === 'bigint' || dataType === 'smallint') {
|
||||
if (
|
||||
dataType === "integer" ||
|
||||
dataType === "bigint" ||
|
||||
dataType === "smallint"
|
||||
) {
|
||||
return `${key} = $${index + 1}::integer`;
|
||||
} else if (dataType === 'numeric' || dataType === 'decimal' || dataType === 'real' || dataType === 'double precision') {
|
||||
} else if (
|
||||
dataType === "numeric" ||
|
||||
dataType === "decimal" ||
|
||||
dataType === "real" ||
|
||||
dataType === "double precision"
|
||||
) {
|
||||
return `${key} = $${index + 1}::numeric`;
|
||||
} else if (dataType === 'boolean') {
|
||||
} else if (dataType === "boolean") {
|
||||
return `${key} = $${index + 1}::boolean`;
|
||||
} else if (dataType === 'jsonb' || dataType === 'json') {
|
||||
// 🆕 JSONB/JSON 타입은 명시적 캐스팅
|
||||
@@ -890,13 +928,17 @@ export class DynamicFormService {
|
||||
|
||||
// 🔑 Primary Key 타입에 맞게 캐스팅
|
||||
const pkDataType = columnTypes[primaryKeyColumn];
|
||||
let pkCast = '';
|
||||
if (pkDataType === 'integer' || pkDataType === 'bigint' || pkDataType === 'smallint') {
|
||||
pkCast = '::integer';
|
||||
} else if (pkDataType === 'numeric' || pkDataType === 'decimal') {
|
||||
pkCast = '::numeric';
|
||||
} else if (pkDataType === 'uuid') {
|
||||
pkCast = '::uuid';
|
||||
let pkCast = "";
|
||||
if (
|
||||
pkDataType === "integer" ||
|
||||
pkDataType === "bigint" ||
|
||||
pkDataType === "smallint"
|
||||
) {
|
||||
pkCast = "::integer";
|
||||
} else if (pkDataType === "numeric" || pkDataType === "decimal") {
|
||||
pkCast = "::numeric";
|
||||
} else if (pkDataType === "uuid") {
|
||||
pkCast = "::uuid";
|
||||
}
|
||||
// text, varchar 등은 캐스팅 불필요
|
||||
|
||||
@@ -1085,12 +1127,19 @@ export class DynamicFormService {
|
||||
|
||||
// 🎯 제어관리 실행 (UPDATE 트리거)
|
||||
try {
|
||||
// updatedRecord에서 company_code 추출
|
||||
const recordCompanyCode =
|
||||
(updatedRecord as Record<string, any>)?.company_code ||
|
||||
company_code ||
|
||||
"*";
|
||||
|
||||
await this.executeDataflowControlIfConfigured(
|
||||
0, // UPDATE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
|
||||
tableName,
|
||||
updatedRecord as Record<string, any>,
|
||||
"update",
|
||||
updated_by || "system"
|
||||
updated_by || "system",
|
||||
recordCompanyCode
|
||||
);
|
||||
} catch (controlError) {
|
||||
console.error("⚠️ 제어관리 실행 오류:", controlError);
|
||||
@@ -1229,12 +1278,17 @@ export class DynamicFormService {
|
||||
try {
|
||||
if (result && Array.isArray(result) && result.length > 0) {
|
||||
const deletedRecord = result[0] as Record<string, any>;
|
||||
// deletedRecord에서 company_code 추출
|
||||
const recordCompanyCode =
|
||||
deletedRecord?.company_code || companyCode || "*";
|
||||
|
||||
await this.executeDataflowControlIfConfigured(
|
||||
0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
|
||||
tableName,
|
||||
deletedRecord,
|
||||
"delete",
|
||||
userId || "system"
|
||||
userId || "system",
|
||||
recordCompanyCode
|
||||
);
|
||||
}
|
||||
} catch (controlError) {
|
||||
@@ -1540,7 +1594,8 @@ export class DynamicFormService {
|
||||
tableName: string,
|
||||
savedData: Record<string, any>,
|
||||
triggerType: "insert" | "update" | "delete",
|
||||
userId: string = "system"
|
||||
userId: string = "system",
|
||||
companyCode: string = "*"
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log(`🎯 제어관리 설정 확인 중... (screenId: ${screenId})`);
|
||||
@@ -1569,9 +1624,11 @@ export class DynamicFormService {
|
||||
componentId: layout.component_id,
|
||||
componentType: properties?.componentType,
|
||||
actionType: properties?.componentConfig?.action?.type,
|
||||
enableDataflowControl: properties?.webTypeConfig?.enableDataflowControl,
|
||||
enableDataflowControl:
|
||||
properties?.webTypeConfig?.enableDataflowControl,
|
||||
hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig,
|
||||
hasDiagramId: !!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId,
|
||||
hasDiagramId:
|
||||
!!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId,
|
||||
});
|
||||
|
||||
// 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우
|
||||
@@ -1596,21 +1653,27 @@ export class DynamicFormService {
|
||||
|
||||
// 노드 플로우 실행 (relationshipId가 없는 경우 노드 플로우로 간주)
|
||||
let controlResult: any;
|
||||
|
||||
|
||||
if (!relationshipId) {
|
||||
// 노드 플로우 실행
|
||||
console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`);
|
||||
const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService");
|
||||
|
||||
const executionResult = await NodeFlowExecutionService.executeFlow(diagramId, {
|
||||
sourceData: [savedData],
|
||||
dataSourceType: "formData",
|
||||
buttonId: "save-button",
|
||||
screenId: screenId,
|
||||
userId: userId,
|
||||
formData: savedData,
|
||||
});
|
||||
|
||||
const { NodeFlowExecutionService } = await import(
|
||||
"./nodeFlowExecutionService"
|
||||
);
|
||||
|
||||
const executionResult = await NodeFlowExecutionService.executeFlow(
|
||||
diagramId,
|
||||
{
|
||||
sourceData: [savedData],
|
||||
dataSourceType: "formData",
|
||||
buttonId: "save-button",
|
||||
screenId: screenId,
|
||||
userId: userId,
|
||||
companyCode: companyCode,
|
||||
formData: savedData,
|
||||
}
|
||||
);
|
||||
|
||||
controlResult = {
|
||||
success: executionResult.success,
|
||||
message: executionResult.message,
|
||||
@@ -1625,15 +1688,18 @@ export class DynamicFormService {
|
||||
};
|
||||
} else {
|
||||
// 관계 기반 제어관리 실행
|
||||
console.log(`🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})`);
|
||||
controlResult = await this.dataflowControlService.executeDataflowControl(
|
||||
diagramId,
|
||||
relationshipId,
|
||||
triggerType,
|
||||
savedData,
|
||||
tableName,
|
||||
userId
|
||||
console.log(
|
||||
`🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})`
|
||||
);
|
||||
controlResult =
|
||||
await this.dataflowControlService.executeDataflowControl(
|
||||
diagramId,
|
||||
relationshipId,
|
||||
triggerType,
|
||||
savedData,
|
||||
tableName,
|
||||
userId
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🎯 제어관리 실행 결과:`, controlResult);
|
||||
@@ -1690,7 +1756,7 @@ export class DynamicFormService {
|
||||
): Promise<{ affectedRows: number }> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
|
||||
try {
|
||||
console.log("🔄 [updateFieldValue] 업데이트 실행:", {
|
||||
tableName,
|
||||
@@ -1708,11 +1774,13 @@ export class DynamicFormService {
|
||||
WHERE table_name = $1 AND column_name IN ('updated_by', 'updated_at', 'company_code')
|
||||
`;
|
||||
const columnResult = await client.query(columnQuery, [tableName]);
|
||||
const existingColumns = columnResult.rows.map((row: any) => row.column_name);
|
||||
|
||||
const hasUpdatedBy = existingColumns.includes('updated_by');
|
||||
const hasUpdatedAt = existingColumns.includes('updated_at');
|
||||
const hasCompanyCode = existingColumns.includes('company_code');
|
||||
const existingColumns = columnResult.rows.map(
|
||||
(row: any) => row.column_name
|
||||
);
|
||||
|
||||
const hasUpdatedBy = existingColumns.includes("updated_by");
|
||||
const hasUpdatedAt = existingColumns.includes("updated_at");
|
||||
const hasCompanyCode = existingColumns.includes("company_code");
|
||||
|
||||
console.log("🔍 [updateFieldValue] 테이블 컬럼 확인:", {
|
||||
hasUpdatedBy,
|
||||
@@ -1909,7 +1977,8 @@ export class DynamicFormService {
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const limitClause = params.limit ? `LIMIT ${params.limit}` : "LIMIT 1000";
|
||||
|
||||
const sqlQuery = `
|
||||
|
||||
Reference in New Issue
Block a user