Compare commits

...

8 Commits

Author SHA1 Message Date
kjs b84f35d514 테이블 리스트 오류수정 2025-10-01 17:18:48 +09:00
kjs 767c031629 fix: Select Item 빈 문자열 value 에러 수정
문제:
- SelectItem에 빈 문자열 value 전달 시 에러
- col.columnName이 빈 문자열일 수 있음

수정:
- filter에 col.columnName 체크 추가
- 빈 문자열 컬럼 제외

에러 메시지:
A <Select.Item /> must have a value prop
that is not an empty string
2025-10-01 17:18:06 +09:00
kjs d22e83d234 fix: UPDATE 액션 formData 기본 포함 및 로깅 추가
UPDATE 액션 실행 시:
- formData를 기본으로 복사하여 기본키 포함
- 상세 로깅으로 디버깅 지원
- 백엔드 동적 기본키 조회 구현
2025-10-01 15:51:13 +09:00
kjs 151de4148c fix: UPDATE 액션 동적 기본키 조회 기능 추가
문제:
- UPDATE 액션 실행 시 ID 필요 에러
- executeUpdate가 하드코딩된 id 필드만 찾음
- 실제 테이블 기본키는 다를 수 있음

해결:
1. 테이블 기본키 동적 조회
2. 기본키 값 동적 추출
3. 동적 UPDATE 쿼리 생성
4. 상세 로깅 추가

결과:
- 모든 테이블의 UPDATE 동작
- 동적 기본키 처리
2025-10-01 15:48:29 +09:00
kjs cb1a6ad672 feat: 버튼 저장 후 제어 자동 실행 기능 추가
문제:
- 버튼에 제어를 연결했지만 실행되지 않음
- ButtonActionExecutor가 제어 실행 로직이 없었음

수정:
1. buttonActions.ts:
   - executeAfterSaveControl() 메서드 추가
   - handleSave()에서 저장 성공 후 제어 실행
   - dataflowTiming='after'일 때만 실행
   - ImprovedButtonActionExecutor 통해 관계 기반 제어 실행

2. ButtonActionConfig 타입 확장:
   - dataflowTiming 필드 추가

3. ButtonActionContext 타입 확장:
   - buttonId, userId, companyCode 필드 추가

4. Import 추가:
   - ExtendedControlContext 타입 import

동작 흐름:
save 버튼 클릭
→ handleSave() 실행
→ 데이터 저장 (INSERT/UPDATE)
→  저장 성공
→ executeAfterSaveControl() 자동 호출
→ ImprovedButtonActionExecutor로 관계 실행
→ 연결된 제어 액션들 순차 실행

결과:
-  저장 후 연결된 제어 자동 실행
-  제어 실패 시 에러 처리
-  기존 기능 영향 없음
2025-10-01 15:31:31 +09:00
kjs 352d4c3126 fix: query 함수 파라미터 배열 전달 오류 수정
문제:
- query() 함수에 스프레드 연산자로 파라미터 전달
- pg 라이브러리는 배열을 요구함
- 'Query values must be an array' 에러 발생

수정:
tableManagementService.ts (2곳):
- line 1501: query(...searchValues) → query(searchValues)
- line 1512: query(...searchValues, size, offset)
            → query([...searchValues, size, offset])

결과:
-  쿼리 파라미터 배열로 정확히 전달
-  테이블 데이터 조회 정상 동작
2025-10-01 15:21:08 +09:00
kjs 6fc140b423 fix: Docker 파일에서 Prisma 명령 제거
문제:
- docker/dev/backend.Dockerfile에 Prisma 명령 남아있음
- docker/prod/backend.Dockerfile에 Prisma 명령 남아있음
- Docker 빌드 시 '/prisma' not found 에러 발생

해결:

1. docker/dev/backend.Dockerfile:
   - COPY prisma ./prisma 제거
   - RUN npx prisma generate 제거

2. docker/prod/backend.Dockerfile:
   - Dependencies stage: Prisma 관련 코드 제거
   - Build stage: COPY prisma 제거, npx prisma generate 제거
   - Runtime stage: 주석 업데이트

결과:
-  Docker 빌드 에러 해결
-  Prisma 의존성 완전 제거
-  개발/운영 환경 모두 Raw Query 기반
2025-10-01 15:11:07 +09:00
kjs 808a0244d5 fix: @types/uuid 패키지 추가
문제:
- uuid 모듈의 타입 정의 파일 없음
- TypeScript 컴파일 에러 발생

해결:
- npm install --save-dev @types/uuid
- @types/uuid@10.0.0 설치

결과:
-  TypeScript 컴파일 에러 해결
-  uuid 모듈 타입 안전성 확보
2025-10-01 15:08:37 +09:00
9 changed files with 341 additions and 220 deletions

View File

@ -47,6 +47,7 @@
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/sanitize-html": "^2.9.5", "@types/sanitize-html": "^2.9.5",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"eslint": "^8.55.0", "eslint": "^8.55.0",

View File

@ -61,6 +61,7 @@
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/sanitize-html": "^2.9.5", "@types/sanitize-html": "^2.9.5",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"eslint": "^8.55.0", "eslint": "^8.55.0",

View File

@ -32,10 +32,20 @@ export async function executeDataAction(
if (connection && connection.id !== 0) { if (connection && connection.id !== 0) {
// 외부 데이터베이스 연결 // 외부 데이터베이스 연결
result = await executeExternalDatabaseAction(tableName, data, actionType, connection); result = await executeExternalDatabaseAction(
tableName,
data,
actionType,
connection
);
} else { } else {
// 메인 데이터베이스 (현재 시스템) // 메인 데이터베이스 (현재 시스템)
result = await executeMainDatabaseAction(tableName, data, actionType, companyCode); result = await executeMainDatabaseAction(
tableName,
data,
actionType,
companyCode
);
} }
logger.info(`데이터 액션 실행 완료: ${actionType} on ${tableName}`, result); logger.info(`데이터 액션 실행 완료: ${actionType} on ${tableName}`, result);
@ -45,7 +55,6 @@ export async function executeDataAction(
message: `데이터 액션 실행 완료: ${actionType}`, message: `데이터 액션 실행 완료: ${actionType}`,
data: result, data: result,
}); });
} catch (error: any) { } catch (error: any) {
logger.error("데이터 액션 실행 실패:", error); logger.error("데이터 액션 실행 실패:", error);
res.status(500).json({ res.status(500).json({
@ -73,13 +82,13 @@ async function executeMainDatabaseAction(
}; };
switch (actionType.toLowerCase()) { switch (actionType.toLowerCase()) {
case 'insert': case "insert":
return await executeInsert(tableName, dataWithCompany); return await executeInsert(tableName, dataWithCompany);
case 'update': case "update":
return await executeUpdate(tableName, dataWithCompany); return await executeUpdate(tableName, dataWithCompany);
case 'upsert': case "upsert":
return await executeUpsert(tableName, dataWithCompany); return await executeUpsert(tableName, dataWithCompany);
case 'delete': case "delete":
return await executeDelete(tableName, dataWithCompany); return await executeDelete(tableName, dataWithCompany);
default: default:
throw new Error(`지원하지 않는 액션 타입: ${actionType}`); throw new Error(`지원하지 않는 액션 타입: ${actionType}`);
@ -100,25 +109,37 @@ async function executeExternalDatabaseAction(
connection: any connection: any
): Promise<any> { ): Promise<any> {
try { try {
logger.info(`외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})`); logger.info(
`외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})`
);
logger.info(`테이블: ${tableName}, 액션: ${actionType}`, data); logger.info(`테이블: ${tableName}, 액션: ${actionType}`, data);
// 🔥 실제 외부 DB 연결 및 실행 로직 구현 // 🔥 실제 외부 DB 연결 및 실행 로직 구현
const { MultiConnectionQueryService } = await import('../services/multiConnectionQueryService'); const { MultiConnectionQueryService } = await import(
"../services/multiConnectionQueryService"
);
const queryService = new MultiConnectionQueryService(); const queryService = new MultiConnectionQueryService();
let result; let result;
switch (actionType.toLowerCase()) { switch (actionType.toLowerCase()) {
case 'insert': case "insert":
result = await queryService.insertDataToConnection(connection.id, tableName, data); result = await queryService.insertDataToConnection(
connection.id,
tableName,
data
);
logger.info(`외부 DB INSERT 성공:`, result); logger.info(`외부 DB INSERT 성공:`, result);
break; break;
case 'update': case "update":
// TODO: UPDATE 로직 구현 (조건 필요) // TODO: UPDATE 로직 구현 (조건 필요)
throw new Error('UPDATE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다.'); throw new Error(
case 'delete': "UPDATE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다."
);
case "delete":
// TODO: DELETE 로직 구현 (조건 필요) // TODO: DELETE 로직 구현 (조건 필요)
throw new Error('DELETE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다.'); throw new Error(
"DELETE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다."
);
default: default:
throw new Error(`지원하지 않는 액션 타입: ${actionType}`); throw new Error(`지원하지 않는 액션 타입: ${actionType}`);
} }
@ -139,12 +160,15 @@ async function executeExternalDatabaseAction(
/** /**
* INSERT * INSERT
*/ */
async function executeInsert(tableName: string, data: Record<string, any>): Promise<any> { async function executeInsert(
tableName: string,
data: Record<string, any>
): Promise<any> {
try { try {
// 동적 테이블 접근을 위한 raw query 사용 // 동적 테이블 접근을 위한 raw query 사용
const columns = Object.keys(data).join(', '); const columns = Object.keys(data).join(", ");
const values = Object.values(data); const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(', '); const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
const insertQuery = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`; const insertQuery = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
@ -154,7 +178,7 @@ async function executeInsert(tableName: string, data: Record<string, any>): Prom
return { return {
success: true, success: true,
action: 'insert', action: "insert",
tableName, tableName,
data: result, data: result,
affectedRows: result.length, affectedRows: result.length,
@ -168,29 +192,76 @@ async function executeInsert(tableName: string, data: Record<string, any>): Prom
/** /**
* UPDATE * UPDATE
*/ */
async function executeUpdate(tableName: string, data: Record<string, any>): Promise<any> { async function executeUpdate(
tableName: string,
data: Record<string, any>
): Promise<any> {
try { try {
// ID 또는 기본키를 기준으로 업데이트 logger.info(`UPDATE 액션 시작:`, { tableName, receivedData: data });
const { id, ...updateData } = data;
if (!id) { // 1. 테이블의 실제 기본키 조회
throw new Error('UPDATE를 위한 ID가 필요합니다'); const primaryKeyQuery = `
SELECT a.attname as column_name
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary
`;
const pkResult = await query<{ column_name: string }>(primaryKeyQuery, [
tableName,
]);
if (!pkResult || pkResult.length === 0) {
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다`);
} }
const primaryKeyColumn = pkResult[0].column_name;
logger.info(`테이블 ${tableName}의 기본키:`, primaryKeyColumn);
// 2. 기본키 값 추출
const primaryKeyValue = data[primaryKeyColumn];
if (!primaryKeyValue && primaryKeyValue !== 0) {
logger.error(`UPDATE 실패: 기본키 값이 없음`, {
primaryKeyColumn,
receivedData: data,
availableKeys: Object.keys(data),
});
throw new Error(
`UPDATE를 위한 기본키 값이 필요합니다 (${primaryKeyColumn})`
);
}
// 3. 업데이트할 데이터에서 기본키 제외
const updateData = { ...data };
delete updateData[primaryKeyColumn];
logger.info(`UPDATE 데이터 준비:`, {
primaryKeyColumn,
primaryKeyValue,
updateFields: Object.keys(updateData),
});
// 4. 동적 UPDATE 쿼리 생성
const setClause = Object.keys(updateData) const setClause = Object.keys(updateData)
.map((key, index) => `${key} = $${index + 1}`) .map((key, index) => `${key} = $${index + 1}`)
.join(', '); .join(", ");
const values = Object.values(updateData); const values = Object.values(updateData);
const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE id = $${values.length + 1} RETURNING *`; const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE ${primaryKeyColumn} = $${values.length + 1} RETURNING *`;
logger.info(`UPDATE 쿼리 실행:`, { query: updateQuery, values: [...values, id] }); logger.info(`UPDATE 쿼리 실행:`, {
query: updateQuery,
values: [...values, primaryKeyValue],
});
const result = await query<any>(updateQuery, [...values, id]); const result = await query<any>(updateQuery, [...values, primaryKeyValue]);
logger.info(`UPDATE 성공:`, { affectedRows: result.length });
return { return {
success: true, success: true,
action: 'update', action: "update",
tableName, tableName,
data: result, data: result,
affectedRows: result.length, affectedRows: result.length,
@ -204,7 +275,10 @@ async function executeUpdate(tableName: string, data: Record<string, any>): Prom
/** /**
* UPSERT * UPSERT
*/ */
async function executeUpsert(tableName: string, data: Record<string, any>): Promise<any> { async function executeUpsert(
tableName: string,
data: Record<string, any>
): Promise<any> {
try { try {
// 먼저 INSERT를 시도하고, 실패하면 UPDATE // 먼저 INSERT를 시도하고, 실패하면 UPDATE
try { try {
@ -223,12 +297,15 @@ async function executeUpsert(tableName: string, data: Record<string, any>): Prom
/** /**
* DELETE * DELETE
*/ */
async function executeDelete(tableName: string, data: Record<string, any>): Promise<any> { async function executeDelete(
tableName: string,
data: Record<string, any>
): Promise<any> {
try { try {
const { id } = data; const { id } = data;
if (!id) { if (!id) {
throw new Error('DELETE를 위한 ID가 필요합니다'); throw new Error("DELETE를 위한 ID가 필요합니다");
} }
const deleteQuery = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`; const deleteQuery = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`;
@ -239,7 +316,7 @@ async function executeDelete(tableName: string, data: Record<string, any>): Prom
return { return {
success: true, success: true,
action: 'delete', action: "delete",
tableName, tableName,
data: result, data: result,
affectedRows: result.length, affectedRows: result.length,

View File

@ -1498,7 +1498,7 @@ export class TableManagementService {
// 전체 개수 조회 // 전체 개수 조회
const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`; const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`;
const countResult = await query<any>(countQuery, ...searchValues); const countResult = await query<any>(countQuery, searchValues);
const total = parseInt(countResult[0].count); const total = parseInt(countResult[0].count);
// 데이터 조회 // 데이터 조회
@ -1509,7 +1509,7 @@ export class TableManagementService {
LIMIT $${paramIndex} OFFSET $${paramIndex + 1} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`; `;
let data = await query<any>(dataQuery, ...searchValues, size, offset); let data = await query<any>(dataQuery, [...searchValues, size, offset]);
// 🎯 파일 컬럼이 있으면 파일 정보 보강 // 🎯 파일 컬럼이 있으면 파일 정보 보강
if (fileColumns.length > 0) { if (fileColumns.length > 0) {

View File

@ -13,9 +13,6 @@ COPY package*.json ./
RUN npm ci --prefer-offline --no-audit RUN npm ci --prefer-offline --no-audit
# 소스 코드는 볼륨 마운트로 처리 # 소스 코드는 볼륨 마운트로 처리
# Prisma 클라이언트 생성용 스키마만 복사
COPY prisma ./prisma
RUN npx prisma generate
# 포트 노출 # 포트 노출
EXPOSE 8080 EXPOSE 8080

View File

@ -9,14 +9,10 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends openssl ca-certificates curl \ && apt-get install -y --no-install-recommends openssl ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Dependencies stage (install deps and generate Prisma client) # Dependencies stage (install production dependencies)
FROM base AS deps FROM base AS deps
COPY package*.json ./ COPY package*.json ./
RUN npm ci --omit=dev --prefer-offline --no-audit && npm cache clean --force RUN npm ci --omit=dev --prefer-offline --no-audit && npm cache clean --force
# Copy prisma schema and generate client (glibc target will be detected)
COPY prisma ./prisma
ENV PRISMA_SKIP_POSTINSTALL_GENERATE=true
RUN npx prisma generate
# Build stage (compile TypeScript) # Build stage (compile TypeScript)
FROM node:20-bookworm-slim AS build FROM node:20-bookworm-slim AS build
@ -25,8 +21,6 @@ COPY package*.json ./
RUN npm ci --prefer-offline --no-audit && npm cache clean --force RUN npm ci --prefer-offline --no-audit && npm cache clean --force
COPY tsconfig.json ./ COPY tsconfig.json ./
COPY src ./src COPY src ./src
COPY prisma ./prisma
RUN npx prisma generate
RUN npm run build RUN npm run build
# Runtime image - base 이미지 재사용으로 중복 설치 제거 # Runtime image - base 이미지 재사용으로 중복 설치 제거
@ -36,7 +30,7 @@ ENV NODE_ENV=production
# Create non-root user # Create non-root user
RUN groupadd -r appgroup && useradd -r -g appgroup appuser RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# Copy node_modules with generated Prisma client # Copy production node_modules
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
# Copy built files # Copy built files
COPY --from=build /app/dist ./dist COPY --from=build /app/dist ./dist

View File

@ -233,6 +233,7 @@ const ControlConditionStep: React.FC<ControlConditionStepProps> = ({ state, acti
{[...fromColumns, ...toColumns] {[...fromColumns, ...toColumns]
.filter( .filter(
(col, index, array) => (col, index, array) =>
col.columnName && // 빈 문자열 제외
array.findIndex((c) => c.columnName === col.columnName) === index, array.findIndex((c) => c.columnName === col.columnName) === index,
) )
.map((col) => ( .map((col) => (

View File

@ -4,6 +4,7 @@ import { toast } from "sonner";
import { screenApi } from "@/lib/api/screen"; import { screenApi } from "@/lib/api/screen";
import { DynamicFormApi } from "@/lib/api/dynamicForm"; import { DynamicFormApi } from "@/lib/api/dynamicForm";
import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor"; import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor";
import type { ExtendedControlContext } from "@/types/control-management";
/** /**
* *
@ -51,6 +52,7 @@ export interface ButtonActionConfig {
// 제어관리 관련 // 제어관리 관련
enableDataflowControl?: boolean; enableDataflowControl?: boolean;
dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용) dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용)
dataflowTiming?: "before" | "after" | "replace"; // 제어 실행 타이밍
} }
/** /**
@ -68,6 +70,11 @@ export interface ButtonActionContext {
// 테이블 선택된 행 정보 (다중 선택 액션용) // 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[]; selectedRows?: any[];
selectedRowsData?: any[]; selectedRowsData?: any[];
// 제어 실행을 위한 추가 정보
buttonId?: string;
userId?: string;
companyCode?: string;
} }
/** /**
@ -226,6 +233,12 @@ export class ButtonActionExecutor {
} }
console.log("✅ 저장 성공:", saveResult); console.log("✅ 저장 성공:", saveResult);
// 🔥 저장 성공 후 연결된 제어 실행 (dataflowTiming이 'after'인 경우)
if (config.enableDataflowControl && config.dataflowConfig) {
console.log("🎯 저장 후 제어 실행 시작:", config.dataflowConfig);
await this.executeAfterSaveControl(config, context);
}
} else { } else {
throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)"); throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
} }
@ -853,6 +866,68 @@ export class ButtonActionExecutor {
} }
} }
/**
* (After Timing)
*/
private static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<void> {
console.log("🎯 저장 후 제어 실행:", {
enableDataflowControl: config.enableDataflowControl,
dataflowConfig: config.dataflowConfig,
dataflowTiming: config.dataflowTiming,
});
// dataflowTiming이 'after'가 아니면 실행하지 않음
if (config.dataflowTiming && config.dataflowTiming !== "after") {
console.log("⏭️ dataflowTiming이 'after'가 아니므로 제어 실행 건너뜀:", config.dataflowTiming);
return;
}
// 제어 데이터 소스 결정
let controlDataSource = config.dataflowConfig?.controlDataSource;
if (!controlDataSource) {
controlDataSource = "form"; // 저장 후에는 기본적으로 form 데이터 사용
}
const extendedContext: ExtendedControlContext = {
formData: context.formData || {},
selectedRows: context.selectedRows || [],
selectedRowsData: context.selectedRowsData || [],
controlDataSource,
};
// 관계 기반 제어 실행
if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
console.log("🔗 저장 후 관계 기반 제어 실행:", config.dataflowConfig.relationshipConfig);
const buttonConfig = {
actionType: config.type,
dataflowConfig: config.dataflowConfig,
enableDataflowControl: true,
};
const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(
buttonConfig,
context.formData || {},
{
buttonId: context.buttonId || "unknown",
screenId: context.screenId || "unknown",
userId: context.userId || "unknown",
companyCode: context.companyCode || "*",
startTime: Date.now(),
contextData: context,
}
);
if (executionResult.success) {
console.log("✅ 저장 후 제어 실행 완료:", executionResult);
// 성공 토스트는 save 액션에서 이미 표시했으므로 추가로 표시하지 않음
} else {
console.error("❌ 저장 후 제어 실행 실패:", executionResult);
toast.error("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
}
}
}
/** /**
* *
*/ */

View File

@ -65,7 +65,7 @@ export class ImprovedButtonActionExecutor {
static async executeButtonAction( static async executeButtonAction(
buttonConfig: ExtendedButtonTypeConfig, buttonConfig: ExtendedButtonTypeConfig,
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<ButtonExecutionResult> { ): Promise<ButtonExecutionResult> {
console.log("🔥 ImprovedButtonActionExecutor 시작:", { console.log("🔥 ImprovedButtonActionExecutor 시작:", {
buttonConfig, buttonConfig,
@ -92,15 +92,11 @@ export class ImprovedButtonActionExecutor {
// 1. Before 타이밍 제어 실행 // 1. Before 타이밍 제어 실행
if (executionPlan.beforeControls.length > 0) { if (executionPlan.beforeControls.length > 0) {
console.log("⏰ Before 제어 실행 시작"); console.log("⏰ Before 제어 실행 시작");
const beforeResults = await this.executeControls( const beforeResults = await this.executeControls(executionPlan.beforeControls, formData, context);
executionPlan.beforeControls,
formData,
context
);
results.push(...beforeResults); results.push(...beforeResults);
// Before 제어 중 실패가 있으면 중단 // Before 제어 중 실패가 있으면 중단
const hasFailure = beforeResults.some(r => !r.success); const hasFailure = beforeResults.some((r) => !r.success);
if (hasFailure) { if (hasFailure) {
throw new Error("Before 제어 실행 중 오류가 발생했습니다."); throw new Error("Before 제어 실행 중 오류가 발생했습니다.");
} }
@ -109,11 +105,7 @@ export class ImprovedButtonActionExecutor {
// 2. 메인 액션 실행 (replace가 아닌 경우에만) // 2. 메인 액션 실행 (replace가 아닌 경우에만)
if (!executionPlan.hasReplaceControl) { if (!executionPlan.hasReplaceControl) {
console.log("⚡ 메인 액션 실행:", buttonConfig.actionType); console.log("⚡ 메인 액션 실행:", buttonConfig.actionType);
const mainResult = await this.executeMainAction( const mainResult = await this.executeMainAction(buttonConfig, formData, context);
buttonConfig,
formData,
context
);
results.push(mainResult); results.push(mainResult);
if (!mainResult.success) { if (!mainResult.success) {
@ -126,11 +118,7 @@ export class ImprovedButtonActionExecutor {
// 3. After 타이밍 제어 실행 // 3. After 타이밍 제어 실행
if (executionPlan.afterControls.length > 0) { if (executionPlan.afterControls.length > 0) {
console.log("⏰ After 제어 실행 시작"); console.log("⏰ After 제어 실행 시작");
const afterResults = await this.executeControls( const afterResults = await this.executeControls(executionPlan.afterControls, formData, context);
executionPlan.afterControls,
formData,
context
);
results.push(...afterResults); results.push(...afterResults);
} }
@ -210,18 +198,14 @@ export class ImprovedButtonActionExecutor {
private static async executeControls( private static async executeControls(
controls: ControlConfig[], controls: ControlConfig[],
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<ExecutionResult[]> { ): Promise<ExecutionResult[]> {
const results: ExecutionResult[] = []; const results: ExecutionResult[] = [];
for (const control of controls) { for (const control of controls) {
try { try {
// 관계 실행만 지원 // 관계 실행만 지원
const result = await this.executeRelationship( const result = await this.executeRelationship(control.relationshipConfig, formData, context);
control.relationshipConfig,
formData,
context
);
results.push(result); results.push(result);
@ -255,7 +239,7 @@ export class ImprovedButtonActionExecutor {
contextData?: Record<string, any>; contextData?: Record<string, any>;
}, },
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<ExecutionResult> { ): Promise<ExecutionResult> {
try { try {
console.log(`🔗 관계 실행 시작: ${config.relationshipName} (ID: ${config.relationshipId})`); console.log(`🔗 관계 실행 시작: ${config.relationshipName} (ID: ${config.relationshipId})`);
@ -266,13 +250,13 @@ export class ImprovedButtonActionExecutor {
throw new Error(`관계 정보를 찾을 수 없습니다: ${config.relationshipId}`); throw new Error(`관계 정보를 찾을 수 없습니다: ${config.relationshipId}`);
} }
console.log(`📋 관계 데이터 로드 완료:`, relationshipData); console.log("📋 관계 데이터 로드 완료:", relationshipData);
// 2. 관계 타입에 따른 실행 // 2. 관계 타입에 따른 실행
const relationships = relationshipData.relationships; const relationships = relationshipData.relationships;
const connectionType = relationships.connectionType; const connectionType = relationships.connectionType;
console.log(`🔍 관계 상세 정보:`, { console.log("🔍 관계 상세 정보:", {
connectionType, connectionType,
hasExternalCallConfig: !!relationships.externalCallConfig, hasExternalCallConfig: !!relationships.externalCallConfig,
externalCallConfig: relationships.externalCallConfig, externalCallConfig: relationships.externalCallConfig,
@ -301,7 +285,6 @@ export class ImprovedButtonActionExecutor {
} }
return result; return result;
} catch (error: any) { } catch (error: any) {
console.error(`❌ 관계 실행 실패: ${config.relationshipName}`, error); console.error(`❌ 관계 실행 실패: ${config.relationshipName}`, error);
const errorResult = { const errorResult = {
@ -325,15 +308,15 @@ export class ImprovedButtonActionExecutor {
const response = await apiClient.get(`/dataflow-diagrams/${relationshipId}`); const response = await apiClient.get(`/dataflow-diagrams/${relationshipId}`);
console.log(`✅ 관계 데이터 조회 성공:`, response.data); console.log("✅ 관계 데이터 조회 성공:", response.data);
if (!response.data.success) { if (!response.data.success) {
throw new Error(response.data.message || '관계 데이터 조회 실패'); throw new Error(response.data.message || "관계 데이터 조회 실패");
} }
return response.data.data; return response.data.data;
} catch (error) { } catch (error) {
console.error('관계 데이터 조회 오류:', error); console.error("관계 데이터 조회 오류:", error);
throw error; throw error;
} }
} }
@ -344,39 +327,39 @@ export class ImprovedButtonActionExecutor {
private static async executeExternalCall( private static async executeExternalCall(
relationships: any, relationships: any,
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<ExecutionResult> { ): Promise<ExecutionResult> {
try { try {
console.log(`🔍 외부 호출 실행 시작 - relationships 구조:`, relationships); console.log("🔍 외부 호출 실행 시작 - relationships 구조:", relationships);
const externalCallConfig = relationships.externalCallConfig; const externalCallConfig = relationships.externalCallConfig;
console.log(`🔍 externalCallConfig:`, externalCallConfig); console.log("🔍 externalCallConfig:", externalCallConfig);
if (!externalCallConfig) { if (!externalCallConfig) {
console.error('❌ 외부 호출 설정이 없습니다. relationships 구조:', relationships); console.error("❌ 외부 호출 설정이 없습니다. relationships 구조:", relationships);
throw new Error('외부 호출 설정이 없습니다'); throw new Error("외부 호출 설정이 없습니다");
} }
const restApiSettings = externalCallConfig.restApiSettings; const restApiSettings = externalCallConfig.restApiSettings;
if (!restApiSettings) { if (!restApiSettings) {
throw new Error('REST API 설정이 없습니다'); throw new Error("REST API 설정이 없습니다");
} }
console.log(`🌐 외부 API 호출: ${restApiSettings.apiUrl}`); console.log(`🌐 외부 API 호출: ${restApiSettings.apiUrl}`);
// API 호출 준비 // API 호출 준비
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', "Content-Type": "application/json",
...restApiSettings.headers, ...restApiSettings.headers,
}; };
// 인증 처리 // 인증 처리
if (restApiSettings.authentication?.type === 'api-key') { if (restApiSettings.authentication?.type === "api-key") {
headers['Authorization'] = `Bearer ${restApiSettings.authentication.apiKey}`; headers["Authorization"] = `Bearer ${restApiSettings.authentication.apiKey}`;
} }
// 요청 바디 준비 (템플릿 처리) // 요청 바디 준비 (템플릿 처리)
let requestBody = restApiSettings.bodyTemplate || ''; let requestBody = restApiSettings.bodyTemplate || "";
if (requestBody) { if (requestBody) {
// 간단한 템플릿 치환 ({{변수명}} 형태) // 간단한 템플릿 치환 ({{변수명}} 형태)
requestBody = requestBody.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => { requestBody = requestBody.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => {
@ -385,11 +368,11 @@ export class ImprovedButtonActionExecutor {
} }
// 백엔드 프록시를 통한 외부 API 호출 (CORS 문제 해결) // 백엔드 프록시를 통한 외부 API 호출 (CORS 문제 해결)
console.log(`🌐 백엔드 프록시를 통한 외부 API 호출 준비:`, { console.log("🌐 백엔드 프록시를 통한 외부 API 호출 준비:", {
originalUrl: restApiSettings.apiUrl, originalUrl: restApiSettings.apiUrl,
method: restApiSettings.httpMethod || 'GET', method: restApiSettings.httpMethod || "GET",
headers, headers,
body: restApiSettings.httpMethod !== 'GET' ? requestBody : undefined, body: restApiSettings.httpMethod !== "GET" ? requestBody : undefined,
}); });
// 백엔드 프록시 API 호출 - GenericApiSettings 형식에 맞게 전달 // 백엔드 프록시 API 호출 - GenericApiSettings 형식에 맞게 전달
@ -400,53 +383,48 @@ export class ImprovedButtonActionExecutor {
callType: "rest-api", callType: "rest-api",
apiType: "generic", apiType: "generic",
url: restApiSettings.apiUrl, url: restApiSettings.apiUrl,
method: restApiSettings.httpMethod || 'POST', method: restApiSettings.httpMethod || "POST",
headers: restApiSettings.headers || {}, headers: restApiSettings.headers || {},
body: requestBody, body: requestBody,
authentication: restApiSettings.authentication || { type: 'none' }, authentication: restApiSettings.authentication || { type: "none" },
timeout: restApiSettings.timeout || 30000, timeout: restApiSettings.timeout || 30000,
retryCount: restApiSettings.retryCount || 3, retryCount: restApiSettings.retryCount || 3,
}, },
templateData: restApiSettings.httpMethod !== 'GET' && requestBody ? JSON.parse(requestBody) : formData, templateData: restApiSettings.httpMethod !== "GET" && requestBody ? JSON.parse(requestBody) : formData,
}; };
console.log(`📤 백엔드로 전송할 데이터:`, requestPayload); console.log("📤 백엔드로 전송할 데이터:", requestPayload);
const proxyResponse = await apiClient.post(`/external-calls/execute`, requestPayload); const proxyResponse = await apiClient.post("/external-calls/execute", requestPayload);
console.log(`📡 백엔드 프록시 응답:`, proxyResponse.data); console.log("📡 백엔드 프록시 응답:", proxyResponse.data);
if (!proxyResponse.data.success) { if (!proxyResponse.data.success) {
throw new Error(`프록시 API 호출 실패: ${proxyResponse.data.error || proxyResponse.data.message}`); throw new Error(`프록시 API 호출 실패: ${proxyResponse.data.error || proxyResponse.data.message}`);
} }
const responseData = proxyResponse.data.result; const responseData = proxyResponse.data.result;
console.log(`✅ 외부 API 호출 성공 (프록시):`, responseData); console.log("✅ 외부 API 호출 성공 (프록시):", responseData);
// 데이터 매핑 처리 (inbound mapping) // 데이터 매핑 처리 (inbound mapping)
if (externalCallConfig.dataMappingConfig?.inboundMapping) { if (externalCallConfig.dataMappingConfig?.inboundMapping) {
console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`); console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`);
console.log(`📥 매핑 설정:`, externalCallConfig.dataMappingConfig.inboundMapping); console.log("📥 매핑 설정:", externalCallConfig.dataMappingConfig.inboundMapping);
console.log(`📥 응답 데이터:`, responseData); console.log("📥 응답 데이터:", responseData);
await this.processInboundMapping( await this.processInboundMapping(externalCallConfig.dataMappingConfig.inboundMapping, responseData, context);
externalCallConfig.dataMappingConfig.inboundMapping,
responseData,
context
);
} else { } else {
console.log(` 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`); console.log(` 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`);
} }
return { return {
success: true, success: true,
message: '외부 호출 실행 완료', message: "외부 호출 실행 완료",
executionTime: Date.now() - context.startTime, executionTime: Date.now() - context.startTime,
data: responseData, data: responseData,
}; };
} catch (error: any) { } catch (error: any) {
console.error('외부 호출 실행 오류:', error); console.error("외부 호출 실행 오류:", error);
return { return {
success: false, success: false,
message: `외부 호출 실행 실패: ${error.message}`, message: `외부 호출 실행 실패: ${error.message}`,
@ -462,10 +440,10 @@ export class ImprovedButtonActionExecutor {
private static async executeDataSave( private static async executeDataSave(
relationships: any, relationships: any,
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<ExecutionResult> { ): Promise<ExecutionResult> {
try { try {
console.log(`💾 데이터 저장 실행 시작`); console.log("💾 데이터 저장 실행 시작");
// 제어 조건 확인 // 제어 조건 확인
const controlConditions = relationships.controlConditions || []; const controlConditions = relationships.controlConditions || [];
@ -474,7 +452,7 @@ export class ImprovedButtonActionExecutor {
if (!conditionsMet) { if (!conditionsMet) {
return { return {
success: false, success: false,
message: '제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다', message: "제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다",
executionTime: Date.now() - context.startTime, executionTime: Date.now() - context.startTime,
}; };
} }
@ -498,12 +476,7 @@ export class ImprovedButtonActionExecutor {
continue; continue;
} }
const actionResult = await this.executeDataAction( const actionResult = await this.executeDataAction(action, relationships, formData, context);
action,
relationships,
formData,
context
);
results.push(actionResult); results.push(actionResult);
if (!actionResult.success) { if (!actionResult.success) {
@ -512,7 +485,7 @@ export class ImprovedButtonActionExecutor {
} }
} }
const successCount = results.filter(r => r.success).length; const successCount = results.filter((r) => r.success).length;
const totalCount = results.length; const totalCount = results.length;
return { return {
@ -525,9 +498,8 @@ export class ImprovedButtonActionExecutor {
totalCount, totalCount,
}, },
}; };
} catch (error: any) { } catch (error: any) {
console.error('데이터 저장 실행 오류:', error); console.error("데이터 저장 실행 오류:", error);
return { return {
success: false, success: false,
message: `데이터 저장 실행 실패: ${error.message}`, message: `데이터 저장 실행 실패: ${error.message}`,
@ -544,48 +516,53 @@ export class ImprovedButtonActionExecutor {
action: any, action: any,
relationships: any, relationships: any,
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<ExecutionResult> { ): Promise<ExecutionResult> {
try { try {
console.log(`🔧 데이터 액션 실행: ${action.name} (${action.actionType})`); console.log(`🔧 데이터 액션 실행: ${action.name} (${action.actionType})`);
console.log("📥 받은 formData:", formData);
console.log("📥 formData 키들:", Object.keys(formData));
// 필드 매핑 처리 // 🔥 UPDATE 액션의 경우 formData를 기본으로 시작 (기본키 포함)
const mappedData: Record<string, any> = {}; const mappedData: Record<string, any> = action.actionType === "update" ? { ...formData } : {};
// 필드 매핑 처리 (기존 데이터에 덮어쓰기)
for (const mapping of action.fieldMappings) { for (const mapping of action.fieldMappings) {
if (mapping.valueType === 'static') { if (mapping.valueType === "static") {
// 정적 값 처리 // 정적 값 처리
let value = mapping.value; let value = mapping.value;
if (value === '#NOW') { if (value === "#NOW") {
value = new Date().toISOString(); value = new Date().toISOString();
} }
mappedData[mapping.targetField] = value; mappedData[mapping.targetField] = value;
console.log(`🔧 정적 값 매핑: ${mapping.targetField} = ${value}`);
} else { } else {
// 필드 매핑 처리 // 필드 매핑 처리
const sourceField = mapping.fromField?.columnName; const sourceField = mapping.fromField?.columnName;
if (sourceField && formData[sourceField] !== undefined) { if (sourceField && formData[sourceField] !== undefined) {
mappedData[mapping.toField.columnName] = formData[sourceField]; mappedData[mapping.toField.columnName] = formData[sourceField];
console.log(`🔧 필드 매핑: ${sourceField}${mapping.toField.columnName} = ${formData[sourceField]}`);
} }
} }
} }
console.log(`📋 매핑된 데이터:`, mappedData); console.log("📋 최종 매핑된 데이터:", mappedData);
console.log("🔑 기본키 포함 여부 체크:", {
hasId: "id" in mappedData,
keys: Object.keys(mappedData),
values: Object.values(mappedData),
});
// 대상 연결 정보 // 대상 연결 정보
const toConnection = relationships.toConnection; const toConnection = relationships.toConnection;
const targetTable = relationships.toTable?.tableName; const targetTable = relationships.toTable?.tableName;
if (!targetTable) { if (!targetTable) {
throw new Error('대상 테이블이 지정되지 않았습니다'); throw new Error("대상 테이블이 지정되지 않았습니다");
} }
// 데이터 저장 API 호출 // 데이터 저장 API 호출
const saveResult = await this.saveDataToTable( const saveResult = await this.saveDataToTable(targetTable, mappedData, action.actionType, toConnection);
targetTable,
mappedData,
action.actionType,
toConnection
);
return { return {
success: true, success: true,
@ -593,7 +570,6 @@ export class ImprovedButtonActionExecutor {
executionTime: Date.now() - context.startTime, executionTime: Date.now() - context.startTime,
data: saveResult, data: saveResult,
}; };
} catch (error: any) { } catch (error: any) {
console.error(`데이터 액션 실행 오류: ${action.name}`, error); console.error(`데이터 액션 실행 오류: ${action.name}`, error);
return { return {
@ -612,17 +588,17 @@ export class ImprovedButtonActionExecutor {
tableName: string, tableName: string,
data: Record<string, any>, data: Record<string, any>,
actionType: string, actionType: string,
connection?: any connection?: any,
): Promise<any> { ): Promise<any> {
try { try {
console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, { console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, {
actionType, actionType,
data, data,
connection connection,
}); });
// 데이터 저장 API 호출 (apiClient 사용) // 데이터 저장 API 호출 (apiClient 사용)
const response = await apiClient.post('/dataflow/execute-data-action', { const response = await apiClient.post("/dataflow/execute-data-action", {
tableName, tableName,
data, data,
actionType, actionType,
@ -632,7 +608,7 @@ export class ImprovedButtonActionExecutor {
console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data); console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('테이블 데이터 저장 오류:', error); console.error("테이블 데이터 저장 오류:", error);
throw error; throw error;
} }
} }
@ -643,9 +619,9 @@ export class ImprovedButtonActionExecutor {
private static evaluateConditions( private static evaluateConditions(
conditions: any[], conditions: any[],
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): boolean { ): boolean {
console.log(`🔍 조건 평가 시작:`, { console.log("🔍 조건 평가 시작:", {
conditions, conditions,
formDataKeys: Object.keys(formData), formDataKeys: Object.keys(formData),
formData, formData,
@ -657,7 +633,7 @@ export class ImprovedButtonActionExecutor {
const conditionValue = condition.value; const conditionValue = condition.value;
const operator = condition.operator; const operator = condition.operator;
console.log(`🔍 개별 조건 검증:`, { console.log("🔍 개별 조건 검증:", {
field: condition.field, field: condition.field,
operator, operator,
expectedValue: conditionValue, expectedValue: conditionValue,
@ -668,22 +644,22 @@ export class ImprovedButtonActionExecutor {
let conditionMet = false; let conditionMet = false;
switch (operator) { switch (operator) {
case '=': case "=":
conditionMet = fieldValue === conditionValue; conditionMet = fieldValue === conditionValue;
break; break;
case '!=': case "!=":
conditionMet = fieldValue !== conditionValue; conditionMet = fieldValue !== conditionValue;
break; break;
case '>': case ">":
conditionMet = Number(fieldValue) > Number(conditionValue); conditionMet = Number(fieldValue) > Number(conditionValue);
break; break;
case '<': case "<":
conditionMet = Number(fieldValue) < Number(conditionValue); conditionMet = Number(fieldValue) < Number(conditionValue);
break; break;
case '>=': case ">=":
conditionMet = Number(fieldValue) >= Number(conditionValue); conditionMet = Number(fieldValue) >= Number(conditionValue);
break; break;
case '<=': case "<=":
conditionMet = Number(fieldValue) <= Number(conditionValue); conditionMet = Number(fieldValue) <= Number(conditionValue);
break; break;
default: default:
@ -693,13 +669,13 @@ export class ImprovedButtonActionExecutor {
if (!conditionMet) { if (!conditionMet) {
console.log(`❌ 조건 불만족: ${condition.field} ${operator} ${conditionValue} (실제값: ${fieldValue})`); console.log(`❌ 조건 불만족: ${condition.field} ${operator} ${conditionValue} (실제값: ${fieldValue})`);
console.log(`❌ 사용 가능한 필드들:`, Object.keys(formData)); console.log("❌ 사용 가능한 필드들:", Object.keys(formData));
console.log(`❌ 전체 formData:`, formData); console.log("❌ 전체 formData:", formData);
return false; return false;
} }
} }
console.log(`✅ 모든 조건 만족`); console.log("✅ 모든 조건 만족");
return true; return true;
} }
@ -711,47 +687,47 @@ export class ImprovedButtonActionExecutor {
// null이나 undefined인 경우 // null이나 undefined인 경우
if (!responseData) { if (!responseData) {
console.log(`⚠️ 응답 데이터가 null 또는 undefined`); console.log("⚠️ 응답 데이터가 null 또는 undefined");
return []; return [];
} }
// 이미 배열인 경우 (직접 배열 응답) // 이미 배열인 경우 (직접 배열 응답)
if (Array.isArray(responseData)) { if (Array.isArray(responseData)) {
console.log(`✅ 직접 배열 응답 감지`); console.log("✅ 직접 배열 응답 감지");
return responseData; return responseData;
} }
// 문자열인 경우 JSON 파싱 시도 // 문자열인 경우 JSON 파싱 시도
if (typeof responseData === 'string') { if (typeof responseData === "string") {
console.log(`🔄 JSON 문자열 파싱 시도`); console.log("🔄 JSON 문자열 파싱 시도");
try { try {
const parsed = JSON.parse(responseData); const parsed = JSON.parse(responseData);
console.log(`✅ JSON 파싱 성공, 재귀 호출`); console.log("✅ JSON 파싱 성공, 재귀 호출");
return this.extractActualData(parsed); return this.extractActualData(parsed);
} catch (error) { } catch (error) {
console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error); console.log("⚠️ JSON 파싱 실패, 원본 문자열 반환:", error);
return [responseData]; return [responseData];
} }
} }
// 객체가 아닌 경우 (숫자 등) // 객체가 아닌 경우 (숫자 등)
if (typeof responseData !== 'object') { if (typeof responseData !== "object") {
console.log(`⚠️ 객체가 아닌 응답: ${typeof responseData}`); console.log(`⚠️ 객체가 아닌 응답: ${typeof responseData}`);
return [responseData]; return [responseData];
} }
// 일반적인 데이터 필드명들을 우선순위대로 확인 // 일반적인 데이터 필드명들을 우선순위대로 확인
const commonDataFields = [ const commonDataFields = [
'data', // { data: [...] } "data", // { data: [...] }
'result', // { result: [...] } "result", // { result: [...] }
'results', // { results: [...] } "results", // { results: [...] }
'items', // { items: [...] } "items", // { items: [...] }
'list', // { list: [...] } "list", // { list: [...] }
'records', // { records: [...] } "records", // { records: [...] }
'rows', // { rows: [...] } "rows", // { rows: [...] }
'content', // { content: [...] } "content", // { content: [...] }
'payload', // { payload: [...] } "payload", // { payload: [...] }
'response', // { response: [...] } "response", // { response: [...] }
]; ];
for (const field of commonDataFields) { for (const field of commonDataFields) {
@ -761,21 +737,21 @@ export class ImprovedButtonActionExecutor {
const extractedData = responseData[field]; const extractedData = responseData[field];
// 추출된 데이터가 문자열인 경우 JSON 파싱 시도 // 추출된 데이터가 문자열인 경우 JSON 파싱 시도
if (typeof extractedData === 'string') { if (typeof extractedData === "string") {
console.log(`🔄 추출된 데이터가 JSON 문자열, 파싱 시도`); console.log("🔄 추출된 데이터가 JSON 문자열, 파싱 시도");
try { try {
const parsed = JSON.parse(extractedData); const parsed = JSON.parse(extractedData);
console.log(`✅ JSON 파싱 성공, 재귀 호출`); console.log("✅ JSON 파싱 성공, 재귀 호출");
return this.extractActualData(parsed); return this.extractActualData(parsed);
} catch (error) { } catch (error) {
console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error); console.log("⚠️ JSON 파싱 실패, 원본 문자열 반환:", error);
return [extractedData]; return [extractedData];
} }
} }
// 추출된 데이터가 객체이고 또 다른 중첩 구조일 수 있으므로 재귀 호출 // 추출된 데이터가 객체이고 또 다른 중첩 구조일 수 있으므로 재귀 호출
if (typeof extractedData === 'object' && !Array.isArray(extractedData)) { if (typeof extractedData === "object" && !Array.isArray(extractedData)) {
console.log(`🔄 중첩된 객체 감지, 재귀 추출 시도`); console.log("🔄 중첩된 객체 감지, 재귀 추출 시도");
return this.extractActualData(extractedData); return this.extractActualData(extractedData);
} }
@ -785,17 +761,17 @@ export class ImprovedButtonActionExecutor {
// 특별한 필드가 없는 경우, 객체의 값들 중에서 배열을 찾기 // 특별한 필드가 없는 경우, 객체의 값들 중에서 배열을 찾기
const objectValues = Object.values(responseData); const objectValues = Object.values(responseData);
const arrayValue = objectValues.find(value => Array.isArray(value)); const arrayValue = objectValues.find((value) => Array.isArray(value));
if (arrayValue) { if (arrayValue) {
console.log(`✅ 객체 값 중 배열 발견`); console.log("✅ 객체 값 중 배열 발견");
return arrayValue; return arrayValue;
} }
// 객체의 값들 중에서 객체를 찾아서 재귀 탐색 // 객체의 값들 중에서 객체를 찾아서 재귀 탐색
for (const value of objectValues) { for (const value of objectValues) {
if (value && typeof value === 'object' && !Array.isArray(value)) { if (value && typeof value === "object" && !Array.isArray(value)) {
console.log(`🔄 객체 값에서 재귀 탐색`); console.log("🔄 객체 값에서 재귀 탐색");
const nestedResult = this.extractActualData(value); const nestedResult = this.extractActualData(value);
if (Array.isArray(nestedResult) && nestedResult.length > 0) { if (Array.isArray(nestedResult) && nestedResult.length > 0) {
return nestedResult; return nestedResult;
@ -804,7 +780,7 @@ export class ImprovedButtonActionExecutor {
} }
// 모든 시도가 실패한 경우, 원본 객체를 단일 항목 배열로 반환 // 모든 시도가 실패한 경우, 원본 객체를 단일 항목 배열로 반환
console.log(`📦 원본 객체를 단일 항목으로 처리`); console.log("📦 원본 객체를 단일 항목으로 처리");
return [responseData]; return [responseData];
} }
@ -814,41 +790,41 @@ export class ImprovedButtonActionExecutor {
private static async processInboundMapping( private static async processInboundMapping(
inboundMapping: any, inboundMapping: any,
responseData: any, responseData: any,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<void> { ): Promise<void> {
try { try {
console.log(`📥 인바운드 데이터 매핑 처리 시작`); console.log("📥 인바운드 데이터 매핑 처리 시작");
console.log(`📥 원본 응답 데이터:`, responseData); console.log("📥 원본 응답 데이터:", responseData);
const targetTable = inboundMapping.targetTable; const targetTable = inboundMapping.targetTable;
const fieldMappings = inboundMapping.fieldMappings || []; const fieldMappings = inboundMapping.fieldMappings || [];
const insertMode = inboundMapping.insertMode || 'insert'; const insertMode = inboundMapping.insertMode || "insert";
console.log(`📥 매핑 설정:`, { console.log("📥 매핑 설정:", {
targetTable, targetTable,
fieldMappings, fieldMappings,
insertMode insertMode,
}); });
// 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원) // 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원)
let actualData = this.extractActualData(responseData); const actualData = this.extractActualData(responseData);
console.log(`📥 추출된 실제 데이터:`, actualData); console.log("📥 추출된 실제 데이터:", actualData);
// 배열이 아닌 경우 배열로 변환 // 배열이 아닌 경우 배열로 변환
const dataArray = Array.isArray(actualData) ? actualData : [actualData]; const dataArray = Array.isArray(actualData) ? actualData : [actualData];
console.log(`📥 처리할 데이터 배열:`, dataArray); console.log("📥 처리할 데이터 배열:", dataArray);
if (dataArray.length === 0) { if (dataArray.length === 0) {
console.log(`⚠️ 처리할 데이터가 없습니다`); console.log("⚠️ 처리할 데이터가 없습니다");
return; return;
} }
for (const item of dataArray) { for (const item of dataArray) {
const mappedData: Record<string, any> = {}; const mappedData: Record<string, any> = {};
console.log(`📥 개별 아이템 처리:`, item); console.log("📥 개별 아이템 처리:", item);
// 필드 매핑 적용 // 필드 매핑 적용
for (const mapping of fieldMappings) { for (const mapping of fieldMappings) {
@ -860,31 +836,30 @@ export class ImprovedButtonActionExecutor {
} }
} }
console.log(`📋 매핑된 데이터:`, mappedData); console.log("📋 매핑된 데이터:", mappedData);
// 매핑된 데이터가 비어있지 않은 경우에만 저장 // 매핑된 데이터가 비어있지 않은 경우에만 저장
if (Object.keys(mappedData).length > 0) { if (Object.keys(mappedData).length > 0) {
await this.saveDataToTable(targetTable, mappedData, insertMode); await this.saveDataToTable(targetTable, mappedData, insertMode);
} else { } else {
console.log(`⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다`); console.log("⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다");
} }
} }
console.log(`✅ 인바운드 데이터 매핑 완료`); console.log("✅ 인바운드 데이터 매핑 완료");
} catch (error) { } catch (error) {
console.error('인바운드 데이터 매핑 오류:', error); console.error("인바운드 데이터 매핑 오류:", error);
throw error; throw error;
} }
} }
/** /**
* 🔥 * 🔥
*/ */
private static async executeMainAction( private static async executeMainAction(
buttonConfig: ExtendedButtonTypeConfig, buttonConfig: ExtendedButtonTypeConfig,
formData: Record<string, any>, formData: Record<string, any>,
context: ButtonExecutionContext context: ButtonExecutionContext,
): Promise<ExecutionResult> { ): Promise<ExecutionResult> {
try { try {
// 기존 ButtonActionExecutor 로직을 여기서 호출하거나 // 기존 ButtonActionExecutor 로직을 여기서 호출하거나
@ -918,7 +893,7 @@ export class ImprovedButtonActionExecutor {
private static async handleExecutionError( private static async handleExecutionError(
error: Error, error: Error,
results: ExecutionResult[], results: ExecutionResult[],
buttonConfig: ExtendedButtonTypeConfig buttonConfig: ExtendedButtonTypeConfig,
): Promise<void> { ): Promise<void> {
console.error("🔄 실행 오류 처리 시작:", error.message); console.error("🔄 실행 오류 처리 시작:", error.message);
@ -928,7 +903,7 @@ export class ImprovedButtonActionExecutor {
console.log("🔄 롤백 처리 시작..."); console.log("🔄 롤백 처리 시작...");
// 성공한 결과들을 역순으로 롤백 // 성공한 결과들을 역순으로 롤백
const successfulResults = results.filter(r => r.success).reverse(); const successfulResults = results.filter((r) => r.success).reverse();
for (const result of successfulResults) { for (const result of successfulResults) {
try { try {