ERP-node/frontend/lib/utils/buttonActions.ts

1985 lines
70 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React from "react";
import { toast } from "sonner";
import { screenApi } from "@/lib/api/screen";
import { DynamicFormApi } from "@/lib/api/dynamicForm";
import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor";
import type { ExtendedControlContext } from "@/types/control-management";
/**
* 버튼 액션 타입 정의
*/
export type ButtonActionType =
| "save" // 저장
| "delete" // 삭제
| "edit" // 편집
| "navigate" // 페이지 이동
| "modal" // 모달 열기
| "control" // 제어 흐름
| "view_table_history" // 테이블 이력 보기
| "excel_download" // 엑셀 다운로드
| "excel_upload" // 엑셀 업로드
| "barcode_scan"; // 바코드 스캔
/**
* 버튼 액션 설정
*/
export interface ButtonActionConfig {
type: ButtonActionType;
// 저장/제출 관련
saveEndpoint?: string;
validateForm?: boolean;
// 네비게이션 관련
targetUrl?: string;
targetScreenId?: number;
// 모달/팝업 관련
modalTitle?: string;
modalDescription?: string;
modalSize?: "sm" | "md" | "lg" | "xl";
popupWidth?: number;
popupHeight?: number;
// 확인 메시지
confirmMessage?: string;
successMessage?: string;
errorMessage?: string;
// 제어관리 관련
enableDataflowControl?: boolean;
dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용)
dataflowTiming?: "before" | "after" | "replace"; // 제어 실행 타이밍
// 테이블 이력 보기 관련
historyTableName?: string; // 이력을 조회할 테이블명 (자동 감지 또는 수동 지정)
historyRecordIdField?: string; // PK 필드명 (기본: "id")
historyRecordIdSource?: "selected_row" | "form_field" | "context"; // 레코드 ID 가져올 소스
historyRecordLabelField?: string; // 레코드 라벨로 표시할 필드 (선택사항)
historyDisplayColumn?: string; // 전체 이력에서 레코드 구분용 컬럼 (예: device_code, name)
// 엑셀 다운로드 관련
excelFileName?: string; // 다운로드할 파일명 (기본: 테이블명_날짜.xlsx)
excelSheetName?: string; // 시트명 (기본: "Sheet1")
excelIncludeHeaders?: boolean; // 헤더 포함 여부 (기본: true)
// 엑셀 업로드 관련
excelUploadMode?: "insert" | "update" | "upsert"; // 업로드 모드
excelKeyColumn?: string; // 업데이트/Upsert 시 키 컬럼
// 바코드 스캔 관련
barcodeTargetField?: string; // 스캔 결과를 입력할 필드명
barcodeFormat?: "all" | "1d" | "2d"; // 바코드 포맷 (기본: "all")
barcodeAutoSubmit?: boolean; // 스캔 후 자동 제출 여부
}
/**
* 버튼 액션 실행 컨텍스트
*/
export interface ButtonActionContext {
formData: Record<string, any>;
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
screenId?: number;
tableName?: string;
userId?: string; // 🆕 현재 로그인한 사용자 ID
userName?: string; // 🆕 현재 로그인한 사용자 이름
companyCode?: string; // 🆕 현재 사용자의 회사 코드
onFormDataChange?: (fieldName: string, value: any) => void;
onClose?: () => void;
onRefresh?: () => void;
onFlowRefresh?: () => void; // 플로우 새로고침 콜백
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
// 제어 실행을 위한 추가 정보
buttonId?: string;
userId?: string;
companyCode?: string;
}
/**
* 버튼 액션 실행기
*/
export class ButtonActionExecutor {
/**
* 액션 실행
*/
static async executeAction(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
// 확인 로직은 컴포넌트에서 처리하므로 여기서는 제거
switch (config.type) {
case "save":
return await this.handleSave(config, context);
case "delete":
return await this.handleDelete(config, context);
case "navigate":
return this.handleNavigate(config, context);
case "modal":
return await this.handleModal(config, context);
case "edit":
return await this.handleEdit(config, context);
case "control":
return this.handleControl(config, context);
case "view_table_history":
return this.handleViewTableHistory(config, context);
case "excel_download":
return await this.handleExcelDownload(config, context);
case "excel_upload":
return await this.handleExcelUpload(config, context);
case "barcode_scan":
return await this.handleBarcodeScan(config, context);
default:
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
return false;
}
} catch (error) {
console.error("버튼 액션 실행 오류:", error);
toast.error(config.errorMessage || "작업 중 오류가 발생했습니다.");
return false;
}
}
/**
* 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
*/
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
const { formData, originalData, tableName, screenId } = context;
// 폼 유효성 검사
if (config.validateForm) {
const validation = this.validateFormData(formData);
if (!validation.isValid) {
toast.error(`입력값을 확인해주세요: ${validation.errors.join(", ")}`);
return false;
}
}
try {
// API 엔드포인트가 지정된 경우
if (config.saveEndpoint) {
const response = await fetch(config.saveEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
if (!response.ok) {
throw new Error(`저장 실패: ${response.statusText}`);
}
} else if (tableName && screenId) {
// DB에서 실제 기본키 조회하여 INSERT/UPDATE 자동 판단
const primaryKeyResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
if (!primaryKeyResult.success) {
throw new Error(primaryKeyResult.message || "기본키 조회에 실패했습니다.");
}
const primaryKeys = primaryKeyResult.data || [];
const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys);
// 단순히 기본키 값 존재 여부로 판단 (임시)
// TODO: 실제 테이블에서 기본키로 레코드 존재 여부 확인하는 API 필요
const isUpdate = false; // 현재는 항상 INSERT로 처리
let saveResult;
if (isUpdate) {
// UPDATE 처리 - 부분 업데이트 사용 (원본 데이터가 있는 경우)
console.log("🔄 UPDATE 모드로 저장:", {
primaryKeyValue,
formData,
originalData,
hasOriginalData: !!originalData,
});
if (originalData) {
// 부분 업데이트: 변경된 필드만 업데이트
console.log("📝 부분 업데이트 실행 (변경된 필드만)");
saveResult = await DynamicFormApi.updateFormDataPartial(primaryKeyValue, originalData, formData, tableName);
} else {
// 전체 업데이트 (기존 방식)
console.log("📝 전체 업데이트 실행 (모든 필드)");
saveResult = await DynamicFormApi.updateFormData(primaryKeyValue, {
tableName,
data: formData,
});
}
} else {
// INSERT 처리
// 🆕 자동으로 작성자 정보 추가
if (!context.userId) {
throw new Error("사용자 정보를 불러올 수 없습니다. 다시 로그인해주세요.");
}
const writerValue = context.userId;
const companyCodeValue = context.companyCode || "";
console.log("👤 [buttonActions] 사용자 정보:", {
userId: context.userId,
userName: context.userName,
companyCode: context.companyCode, // ✅ 회사 코드
formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값
formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값
defaultWriterValue: writerValue,
companyCodeValue, // ✅ 최종 회사 코드 값
});
const dataWithUserInfo = {
...formData,
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
created_by: writerValue, // created_by는 항상 로그인한 사람
updated_by: writerValue, // updated_by는 항상 로그인한 사람
company_code: formData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
};
saveResult = await DynamicFormApi.saveFormData({
screenId,
tableName,
data: dataWithUserInfo,
});
}
if (!saveResult.success) {
throw new Error(saveResult.message || "저장에 실패했습니다.");
}
// 🔥 저장 성공 후 연결된 제어 실행 (dataflowTiming이 'after'인 경우)
if (config.enableDataflowControl && config.dataflowConfig) {
console.log("🎯 저장 후 제어 실행 시작:", config.dataflowConfig);
await this.executeAfterSaveControl(config, context);
}
} else {
throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
}
// 테이블과 플로우 새로고침 (모달 닫기 전에 실행)
context.onRefresh?.();
context.onFlowRefresh?.();
// 저장 성공 후 EditModal 닫기 이벤트 발생
window.dispatchEvent(new CustomEvent("closeEditModal"));
return true;
} catch (error) {
console.error("저장 오류:", error);
throw error; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
}
}
/**
* DB에서 조회한 실제 기본키로 formData에서 값 추출
* @param formData 폼 데이터
* @param primaryKeys DB에서 조회한 실제 기본키 컬럼명 배열
* @returns 기본키 값 (복합키의 경우 첫 번째 키 값)
*/
private static extractPrimaryKeyValueFromDB(formData: Record<string, any>, primaryKeys: string[]): any {
if (!primaryKeys || primaryKeys.length === 0) {
return null;
}
// 첫 번째 기본키 컬럼의 값을 사용 (복합키의 경우)
const primaryKeyColumn = primaryKeys[0];
if (formData.hasOwnProperty(primaryKeyColumn)) {
const value = formData[primaryKeyColumn];
console.log(`🔑 DB 기본키 발견: ${primaryKeyColumn} = ${value}`);
// 복합키인 경우 로그 출력
if (primaryKeys.length > 1) {
console.log(`🔗 복합 기본키 감지:`, primaryKeys);
console.log(`📍 첫 번째 키 (${primaryKeyColumn}) 값을 사용: ${value}`);
}
return value;
}
// 기본키 컬럼이 formData에 없는 경우
return null;
}
/**
* @deprecated DB 기반 조회로 대체됨. extractPrimaryKeyValueFromDB 사용 권장
* formData에서 기본 키값 추출 (추측 기반)
*/
private static extractPrimaryKeyValue(formData: Record<string, any>): any {
// 일반적인 기본 키 필드명들 (우선순위 순)
const commonPrimaryKeys = [
"id",
"ID", // 가장 일반적
"objid",
"OBJID", // 이 프로젝트에서 자주 사용
"pk",
"PK", // Primary Key 줄임말
"_id", // MongoDB 스타일
"uuid",
"UUID", // UUID 방식
"key",
"KEY", // 기타
];
// 우선순위에 따라 기본 키값 찾기
for (const keyName of commonPrimaryKeys) {
if (formData.hasOwnProperty(keyName)) {
const value = formData[keyName];
console.log(`🔑 추측 기반 기본 키 발견: ${keyName} = ${value}`);
return value;
}
}
// 기본 키를 찾지 못한 경우
return null;
}
/**
* 제출 액션 처리
*/
private static async handleSubmit(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
// 제출은 저장과 유사하지만 추가적인 처리가 있을 수 있음
return await this.handleSave(config, context);
}
/**
* 삭제 액션 처리
*/
private static async handleDelete(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
const { formData, tableName, screenId, selectedRowsData, flowSelectedData } = context;
try {
// 플로우 선택 데이터 우선 사용
let dataToDelete = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData;
console.log("🔍 handleDelete - 데이터 소스 확인:", {
hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0),
flowSelectedDataLength: flowSelectedData?.length || 0,
hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0),
selectedRowsDataLength: selectedRowsData?.length || 0,
dataToDeleteLength: dataToDelete?.length || 0,
});
// 다중 선택된 데이터가 있는 경우
if (dataToDelete && dataToDelete.length > 0) {
console.log(`다중 삭제 액션 실행: ${dataToDelete.length}개 항목`, dataToDelete);
// 테이블의 기본키 조회
let primaryKeys: string[] = [];
if (tableName) {
try {
const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
if (primaryKeysResult.success && primaryKeysResult.data) {
primaryKeys = primaryKeysResult.data;
console.log(`🔑 테이블 ${tableName}의 기본키:`, primaryKeys);
}
} catch (error) {
console.warn("기본키 조회 실패, 폴백 방법 사용:", error);
}
}
// 각 선택된 항목을 삭제
for (const rowData of dataToDelete) {
let deleteId: any = null;
// 1순위: 데이터베이스에서 조회한 기본키 사용
if (primaryKeys.length > 0) {
const primaryKey = primaryKeys[0]; // 첫 번째 기본키 사용
deleteId = rowData[primaryKey];
console.log(`📊 기본키 ${primaryKey}로 ID 추출:`, deleteId);
}
// 2순위: 폴백 - 일반적인 ID 필드명들 시도
if (!deleteId) {
deleteId =
rowData.id ||
rowData.objid ||
rowData.pk ||
rowData.ID ||
rowData.OBJID ||
rowData.PK ||
// 테이블별 기본키 패턴들
rowData.sales_no ||
rowData.contract_no ||
rowData.order_no ||
rowData.seq_no ||
rowData.code ||
rowData.code_id ||
rowData.user_id ||
rowData.menu_id;
// _no로 끝나는 필드들 찾기
if (!deleteId) {
const noField = Object.keys(rowData).find((key) => key.endsWith("_no") && rowData[key]);
if (noField) deleteId = rowData[noField];
}
// _id로 끝나는 필드들 찾기
if (!deleteId) {
const idField = Object.keys(rowData).find((key) => key.endsWith("_id") && rowData[key]);
if (idField) deleteId = rowData[idField];
}
console.log(`🔍 폴백 방법으로 ID 추출:`, deleteId);
}
console.log("선택된 행 데이터:", rowData);
console.log("최종 추출된 deleteId:", deleteId);
if (deleteId) {
console.log("다중 데이터 삭제:", { tableName, screenId, id: deleteId });
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName);
if (!deleteResult.success) {
throw new Error(`ID ${deleteId} 삭제 실패: ${deleteResult.message}`);
}
} else {
console.error("삭제 ID를 찾을 수 없습니다. 행 데이터:", rowData);
throw new Error(
`삭제 ID를 찾을 수 없습니다. 기본키: ${primaryKeys.join(", ")}, 사용 가능한 필드: ${Object.keys(rowData).join(", ")}`,
);
}
}
console.log(`✅ 다중 삭제 성공: ${dataToDelete.length}개 항목`);
// 데이터 소스에 따라 적절한 새로고침 호출
if (flowSelectedData && flowSelectedData.length > 0) {
console.log("🔄 플로우 데이터 삭제 완료, 플로우 새로고침 호출");
context.onFlowRefresh?.(); // 플로우 새로고침
} else {
console.log("🔄 테이블 데이터 삭제 완료, 테이블 새로고침 호출");
context.onRefresh?.(); // 테이블 새로고침
}
return true;
}
// 단일 삭제 (기존 로직)
if (tableName && screenId && formData.id) {
console.log("단일 데이터 삭제:", { tableName, screenId, id: formData.id });
// 실제 삭제 API 호출
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName);
if (!deleteResult.success) {
throw new Error(deleteResult.message || "삭제에 실패했습니다.");
}
console.log("✅ 단일 삭제 성공:", deleteResult);
} else {
throw new Error("삭제에 필요한 정보가 부족합니다. (ID, 테이블명 또는 화면ID 누락)");
}
context.onRefresh?.();
return true;
} catch (error) {
console.error("삭제 오류:", error);
throw error; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
}
}
/**
* 초기화 액션 처리
*/
private static handleReset(config: ButtonActionConfig, context: ButtonActionContext): boolean {
const { formData, onFormDataChange } = context;
// 폼 데이터 초기화 - 각 필드를 개별적으로 초기화
if (onFormDataChange && formData) {
Object.keys(formData).forEach((key) => {
onFormDataChange(key, "");
});
}
toast.success(config.successMessage || "초기화되었습니다.");
return true;
}
/**
* 취소 액션 처리
*/
private static handleCancel(config: ButtonActionConfig, context: ButtonActionContext): boolean {
const { onClose } = context;
onClose?.();
return true;
}
/**
* 네비게이션 액션 처리
*/
private static handleNavigate(config: ButtonActionConfig, context: ButtonActionContext): boolean {
let targetUrl = config.targetUrl;
// 화면 ID가 지정된 경우 URL 생성
if (config.targetScreenId) {
targetUrl = `/screens/${config.targetScreenId}`;
}
if (targetUrl) {
window.location.href = targetUrl;
return true;
}
toast.error("이동할 페이지가 지정되지 않았습니다.");
return false;
}
/**
* 모달 액션 처리
*/
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
// 모달 열기 로직
console.log("모달 열기:", {
title: config.modalTitle,
size: config.modalSize,
targetScreenId: config.targetScreenId,
});
if (config.targetScreenId) {
// 1. config에 modalDescription이 있으면 우선 사용
let description = config.modalDescription || "";
// 2. config에 없으면 화면 정보에서 가져오기
if (!description) {
try {
const screenInfo = await screenApi.getScreen(config.targetScreenId);
description = screenInfo?.description || "";
} catch (error) {
console.warn("화면 설명을 가져오지 못했습니다:", error);
}
}
// 전역 모달 상태 업데이트를 위한 이벤트 발생
const modalEvent = new CustomEvent("openScreenModal", {
detail: {
screenId: config.targetScreenId,
title: config.modalTitle || "화면",
description: description,
size: config.modalSize || "md",
},
});
window.dispatchEvent(modalEvent);
// 모달 열기는 조용히 처리 (토스트 불필요)
} else {
console.error("모달로 열 화면이 지정되지 않았습니다.");
return false;
}
return true;
}
/**
* 새 창 액션 처리
*/
private static handleNewWindow(config: ButtonActionConfig, context: ButtonActionContext): boolean {
let targetUrl = config.targetUrl;
// 화면 ID가 지정된 경우 URL 생성
if (config.targetScreenId) {
targetUrl = `/screens/${config.targetScreenId}`;
}
if (targetUrl) {
const windowFeatures = `width=${config.popupWidth || 800},height=${config.popupHeight || 600},scrollbars=yes,resizable=yes`;
window.open(targetUrl, "_blank", windowFeatures);
return true;
}
toast.error("열 페이지가 지정되지 않았습니다.");
return false;
}
/**
* 팝업 액션 처리
*/
private static handlePopup(config: ButtonActionConfig, context: ButtonActionContext): boolean {
// 팝업은 새 창과 유사하지만 더 작은 크기
return this.handleNewWindow(
{
...config,
popupWidth: config.popupWidth || 600,
popupHeight: config.popupHeight || 400,
},
context,
);
}
/**
* 검색 액션 처리
*/
private static handleSearch(config: ButtonActionConfig, context: ButtonActionContext): boolean {
const { formData, onRefresh } = context;
console.log("검색 실행:", formData);
// 검색 조건 검증
const hasSearchCriteria = Object.values(formData).some(
(value) => value !== null && value !== undefined && value !== "",
);
if (!hasSearchCriteria) {
toast.warning("검색 조건을 입력해주세요.");
return false;
}
// 검색 실행 (데이터 새로고침)
onRefresh?.();
// 검색 조건을 URL 파라미터로 추가 (선택사항)
const searchParams = new URLSearchParams();
Object.entries(formData).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== "") {
searchParams.set(key, String(value));
}
});
// URL 업데이트 (히스토리에 추가하지 않음)
if (searchParams.toString()) {
const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
window.history.replaceState({}, "", newUrl);
}
toast.success(config.successMessage || "검색을 실행했습니다.");
return true;
}
/**
* 추가 액션 처리
*/
private static handleAdd(config: ButtonActionConfig, context: ButtonActionContext): boolean {
console.log("추가 액션 실행:", context);
// 추가 로직 구현 (예: 새 레코드 생성 폼 열기)
return true;
}
/**
* 편집 액션 처리
*/
private static async handleEdit(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
const { selectedRowsData, flowSelectedData } = context;
// 플로우 선택 데이터 우선 사용
let dataToEdit = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData;
console.log("🔍 handleEdit - 데이터 소스 확인:", {
hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0),
flowSelectedDataLength: flowSelectedData?.length || 0,
hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0),
selectedRowsDataLength: selectedRowsData?.length || 0,
dataToEditLength: dataToEdit?.length || 0,
});
// 선택된 데이터가 없는 경우
if (!dataToEdit || dataToEdit.length === 0) {
toast.error("수정할 항목을 선택해주세요.");
return false;
}
// 편집 화면이 설정되지 않은 경우
if (!config.targetScreenId) {
toast.error("수정 폼 화면이 설정되지 않았습니다. 버튼 설정에서 수정 폼 화면을 선택해주세요.");
return false;
}
console.log(`📝 편집 액션 실행: ${dataToEdit.length}개 항목`, {
dataToEdit,
targetScreenId: config.targetScreenId,
editMode: config.editMode,
});
if (dataToEdit.length === 1) {
// 단일 항목 편집
const rowData = dataToEdit[0];
console.log("📝 단일 항목 편집:", rowData);
await this.openEditForm(config, rowData, context);
} else {
// 다중 항목 편집 - 현재는 단일 편집만 지원
toast.error("현재 단일 항목 편집만 지원됩니다. 하나의 항목만 선택해주세요.");
return false;
// TODO: 향후 다중 편집 지원
// console.log("📝 다중 항목 편집:", selectedRowsData);
// this.openBulkEditForm(config, selectedRowsData, context);
}
return true;
}
/**
* 편집 폼 열기 (단일 항목)
*/
private static async openEditForm(
config: ButtonActionConfig,
rowData: any,
context: ButtonActionContext,
): Promise<void> {
const editMode = config.editMode || "modal";
switch (editMode) {
case "modal":
// 모달로 편집 폼 열기
await this.openEditModal(config, rowData, context);
break;
case "navigate":
// 새 페이지로 이동
this.navigateToEditScreen(config, rowData, context);
break;
case "inline":
// 현재 화면에서 인라인 편집 (향후 구현)
toast.info("인라인 편집 기능은 향후 지원 예정입니다.");
break;
default:
// 기본값: 모달
this.openEditModal(config, rowData, context);
}
}
/**
* 편집 모달 열기
*/
private static async openEditModal(
config: ButtonActionConfig,
rowData: any,
context: ButtonActionContext,
): Promise<void> {
console.log("🎭 편집 모달 열기:", {
targetScreenId: config.targetScreenId,
modalSize: config.modalSize,
rowData,
});
// 1. config에 editModalDescription이 있으면 우선 사용
let description = config.editModalDescription || "";
// 2. config에 없으면 화면 정보에서 가져오기
if (!description && config.targetScreenId) {
try {
const screenInfo = await screenApi.getScreen(config.targetScreenId);
description = screenInfo?.description || "";
} catch (error) {
console.warn("화면 설명을 가져오지 못했습니다:", error);
}
}
// 모달 열기 이벤트 발생
const modalEvent = new CustomEvent("openEditModal", {
detail: {
screenId: config.targetScreenId,
title: config.editModalTitle || "데이터 수정",
description: description,
modalSize: config.modalSize || "lg",
editData: rowData,
onSave: () => {
// 저장 후 테이블 새로고침
console.log("💾 편집 저장 완료 - 테이블 새로고침");
context.onRefresh?.();
},
},
});
window.dispatchEvent(modalEvent);
// 편집 모달 열기는 조용히 처리 (토스트 없음)
}
/**
* 편집 화면으로 이동
*/
private static navigateToEditScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
const rowId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK;
if (!rowId) {
toast.error("수정할 항목의 ID를 찾을 수 없습니다.");
return;
}
const editUrl = `/screens/${config.targetScreenId}?mode=edit&id=${rowId}`;
console.log("🔄 편집 화면으로 이동:", editUrl);
window.location.href = editUrl;
}
/**
* 닫기 액션 처리
*/
private static handleClose(config: ButtonActionConfig, context: ButtonActionContext): boolean {
console.log("닫기 액션 실행:", context);
context.onClose?.();
return true;
}
/**
* 제어 전용 액션 처리 (조건 체크만 수행)
*/
private static async handleControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
console.log("🎯 ButtonActionExecutor.handleControl 실행:", {
formData: context.formData,
selectedRows: context.selectedRows,
selectedRowsData: context.selectedRowsData,
flowSelectedData: context.flowSelectedData,
flowSelectedStepId: context.flowSelectedStepId,
config,
});
// 🔥 제어 조건이 설정되어 있는지 확인
console.log("🔍 제어관리 활성화 상태 확인:", {
enableDataflowControl: config.enableDataflowControl,
hasDataflowConfig: !!config.dataflowConfig,
dataflowConfig: config.dataflowConfig,
fullConfig: config,
});
if (!config.dataflowConfig || !config.enableDataflowControl) {
console.warn("⚠️ 제어관리가 비활성화되어 있습니다:", {
enableDataflowControl: config.enableDataflowControl,
hasDataflowConfig: !!config.dataflowConfig,
});
toast.warning(
"제어관리가 활성화되지 않았습니다. 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.",
);
return false;
}
try {
// 🔥 확장된 제어 컨텍스트 생성
// 자동으로 적절한 controlDataSource 결정
let controlDataSource = config.dataflowConfig.controlDataSource;
if (!controlDataSource) {
// 설정이 없으면 자동 판단 (우선순위 순서대로)
if (context.flowSelectedData && context.flowSelectedData.length > 0) {
controlDataSource = "flow-selection";
console.log("🔄 자동 판단: flow-selection 모드 사용");
} else if (context.selectedRowsData && context.selectedRowsData.length > 0) {
controlDataSource = "table-selection";
console.log("🔄 자동 판단: table-selection 모드 사용");
} else if (context.formData && Object.keys(context.formData).length > 0) {
controlDataSource = "form";
console.log("🔄 자동 판단: form 모드 사용");
} else {
controlDataSource = "form"; // 기본값
console.log("🔄 기본값: form 모드 사용");
}
}
console.log("📊 데이터 소스 모드:", {
controlDataSource,
hasFormData: !!(context.formData && Object.keys(context.formData).length > 0),
hasTableSelection: !!(context.selectedRowsData && context.selectedRowsData.length > 0),
hasFlowSelection: !!(context.flowSelectedData && context.flowSelectedData.length > 0),
});
const extendedContext: ExtendedControlContext = {
formData: context.formData || {},
selectedRows: context.selectedRows || [],
selectedRowsData: context.selectedRowsData || [],
flowSelectedData: context.flowSelectedData || [],
flowSelectedStepId: context.flowSelectedStepId,
controlDataSource,
};
console.log("🔍 제어 조건 검증 시작:", {
dataflowConfig: config.dataflowConfig,
extendedContext,
});
// 🔥 새로운 버튼 액션 실행 시스템 사용
if (config.dataflowConfig?.controlMode === "flow" && config.dataflowConfig?.flowConfig) {
console.log("🎯 노드 플로우 실행:", config.dataflowConfig.flowConfig);
const { flowId, executionTiming } = config.dataflowConfig.flowConfig;
if (!flowId) {
console.error("❌ 플로우 ID가 없습니다");
toast.error("플로우가 설정되지 않았습니다.");
return false;
}
try {
// 노드 플로우 실행 API 호출 (API 클라이언트 사용)
const { executeNodeFlow } = await import("@/lib/api/nodeFlows");
// 데이터 소스 준비: controlDataSource 설정 기반
let sourceData: any = null;
let dataSourceType: string = controlDataSource || "none";
console.log("🔍 데이터 소스 결정:", {
controlDataSource,
hasFlowSelectedData: !!(context.flowSelectedData && context.flowSelectedData.length > 0),
hasSelectedRowsData: !!(context.selectedRowsData && context.selectedRowsData.length > 0),
hasFormData: !!(context.formData && Object.keys(context.formData).length > 0),
});
// controlDataSource 설정에 따라 데이터 선택
switch (controlDataSource) {
case "flow-selection":
if (context.flowSelectedData && context.flowSelectedData.length > 0) {
sourceData = context.flowSelectedData;
console.log("🌊 플로우 선택 데이터 사용:", {
stepId: context.flowSelectedStepId,
dataCount: sourceData.length,
sourceData,
});
} else {
console.warn("⚠️ flow-selection 모드이지만 선택된 플로우 데이터가 없습니다.");
}
break;
case "table-selection":
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
sourceData = context.selectedRowsData;
console.log("📊 테이블 선택 데이터 사용:", {
dataCount: sourceData.length,
sourceData,
});
} else {
console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다.");
}
break;
case "form":
if (context.formData && Object.keys(context.formData).length > 0) {
sourceData = [context.formData];
console.log("📝 폼 데이터 사용:", sourceData);
} else {
console.warn("⚠️ form 모드이지만 폼 데이터가 없습니다.");
}
break;
case "both":
// 폼 + 테이블 선택
sourceData = [];
if (context.formData && Object.keys(context.formData).length > 0) {
sourceData.push(context.formData);
}
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
sourceData.push(...context.selectedRowsData);
}
console.log("🔀 폼 + 테이블 선택 데이터 사용:", {
dataCount: sourceData.length,
sourceData,
});
break;
default:
// 자동 판단 (설정이 없는 경우)
if (context.flowSelectedData && context.flowSelectedData.length > 0) {
sourceData = context.flowSelectedData;
dataSourceType = "flow-selection";
console.log("🌊 [자동] 플로우 선택 데이터 사용");
} else if (context.selectedRowsData && context.selectedRowsData.length > 0) {
sourceData = context.selectedRowsData;
dataSourceType = "table-selection";
console.log("📊 [자동] 테이블 선택 데이터 사용");
} else if (context.formData && Object.keys(context.formData).length > 0) {
sourceData = [context.formData];
dataSourceType = "form";
console.log("📝 [자동] 폼 데이터 사용");
}
break;
}
console.log("📦 최종 전달 데이터:", {
dataSourceType,
sourceDataCount: sourceData ? (Array.isArray(sourceData) ? sourceData.length : 1) : 0,
sourceData,
});
const result = await executeNodeFlow(flowId, {
dataSourceType,
sourceData,
context,
});
if (result.success) {
console.log("✅ 노드 플로우 실행 완료:", result);
toast.success("플로우 실행이 완료되었습니다.");
// 플로우 새로고침 (플로우 위젯용)
if (context.onFlowRefresh) {
console.log("🔄 플로우 새로고침 호출");
context.onFlowRefresh();
}
// 테이블 새로고침 (일반 테이블용)
if (context.onRefresh) {
console.log("🔄 테이블 새로고침 호출");
context.onRefresh();
}
return true;
} else {
console.error("❌ 노드 플로우 실행 실패:", result);
toast.error(config.errorMessage || result.message || "플로우 실행 중 오류가 발생했습니다.");
return false;
}
} catch (error) {
console.error("❌ 노드 플로우 실행 오류:", error);
toast.error("플로우 실행 중 오류가 발생했습니다.");
return false;
}
} else if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
console.log("🔗 관계 기반 제어 실행:", config.dataflowConfig.relationshipConfig);
// 🔥 table-selection 모드일 때 선택된 행 데이터를 formData에 병합
let mergedFormData = { ...context.formData } || {};
if (
controlDataSource === "table-selection" &&
context.selectedRowsData &&
context.selectedRowsData.length > 0
) {
// 선택된 첫 번째 행의 데이터를 formData에 병합
const selectedRowData = context.selectedRowsData[0];
mergedFormData = { ...mergedFormData, ...selectedRowData };
console.log("🔄 선택된 행 데이터를 formData에 병합:", {
originalFormData: context.formData,
selectedRowData,
mergedFormData,
});
}
// 새로운 ImprovedButtonActionExecutor 사용
const buttonConfig = {
actionType: config.type,
dataflowConfig: config.dataflowConfig,
enableDataflowControl: true, // 관계 기반 제어가 설정된 경우 활성화
};
const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(buttonConfig, mergedFormData, {
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);
toast.success(config.successMessage || "관계 실행이 완료되었습니다.");
// 새로고침이 필요한 경우
if (context.onRefresh) {
context.onRefresh();
}
return true;
} else {
console.error("❌ 관계 실행 실패:", executionResult);
toast.error(config.errorMessage || "관계 실행 중 오류가 발생했습니다.");
return false;
}
} else {
// 제어 없음 - 성공 처리
console.log("⚡ 제어 없음 - 버튼 액션만 실행");
// 새로고침이 필요한 경우
if (context.onRefresh) {
context.onRefresh();
}
return true;
}
} catch (error) {
console.error("제어 조건 검증 중 오류:", error);
toast.error("제어 조건 검증 중 오류가 발생했습니다.");
return false;
}
}
/**
* 저장 후 제어 실행 (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("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
}
}
}
/**
* 관계도에서 가져온 액션들을 실행
*/
private static async executeRelationshipActions(actions: any[], context: ButtonActionContext): Promise<void> {
console.log("🚀 관계도 액션 실행 시작:", actions);
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
try {
console.log(`🔄 액션 ${i + 1}/${actions.length} 실행:`, action);
const actionType = action.actionType || action.type; // actionType 우선, type 폴백
switch (actionType) {
case "save":
await this.executeActionSave(action, context);
break;
case "update":
await this.executeActionUpdate(action, context);
break;
case "delete":
await this.executeActionDelete(action, context);
break;
case "insert":
await this.executeActionInsert(action, context);
break;
default:
console.warn(`❌ 지원되지 않는 액션 타입 (${i + 1}/${actions.length}):`, {
actionType,
actionName: action.name,
fullAction: action,
});
// 지원되지 않는 액션은 오류로 처리하여 중단
throw new Error(`지원되지 않는 액션 타입: ${actionType}`);
}
console.log(`✅ 액션 ${i + 1}/${actions.length} 완료:`, action.name);
// 성공 토스트 (개별 액션별)
toast.success(`${action.name || `액션 ${i + 1}`} 완료`);
} catch (error) {
const actionType = action.actionType || action.type;
console.error(`❌ 액션 ${i + 1}/${actions.length} 실행 실패:`, action.name, error);
// 실패 토스트
toast.error(
`${action.name || `액션 ${i + 1}`} 실행 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
);
// 🚨 순차 실행 중단: 하나라도 실패하면 전체 중단
throw new Error(
`액션 ${i + 1}(${action.name})에서 실패하여 제어 프로세스를 중단합니다: ${error instanceof Error ? error.message : error}`,
);
}
}
console.log("🎉 모든 액션 실행 완료!");
toast.success(`${actions.length}개 액션이 모두 성공적으로 완료되었습니다.`);
}
/**
* 저장 액션 실행
*/
private static async executeActionSave(action: any, context: ButtonActionContext): Promise<void> {
console.log("💾 저장 액션 실행:", action);
console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2));
// 🎯 필드 매핑 정보 사용하여 저장 데이터 구성
let saveData: Record<string, any> = {};
// 액션에 필드 매핑 정보가 있는지 확인
if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
console.log("📋 필드 매핑 정보 발견:", action.fieldMappings);
// 필드 매핑에 따라 데이터 구성
action.fieldMappings.forEach((mapping: any) => {
const { sourceField, targetField, defaultValue, valueType } = mapping;
let value: any;
// 값 소스에 따라 데이터 가져오기
if (valueType === "form" && context.formData && sourceField) {
value = context.formData[sourceField];
} else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) {
value = context.selectedRowsData[0][sourceField];
} else if (valueType === "default" || !sourceField) {
value = defaultValue;
}
// 타겟 필드에 값 설정
if (targetField && value !== undefined) {
saveData[targetField] = value;
console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`);
}
});
} else {
console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용");
// 폴백: 기존 방식
saveData = {
...context.formData,
...context.selectedRowsData?.[0], // 선택된 데이터도 포함
};
}
console.log("📊 최종 저장할 데이터:", saveData);
try {
// 🔥 실제 저장 API 호출
if (!context.tableName) {
throw new Error("테이블명이 설정되지 않았습니다.");
}
const result = await DynamicFormApi.saveFormData({
screenId: 0, // 임시값
tableName: context.tableName,
data: saveData,
});
if (result.success) {
console.log("✅ 저장 성공:", result);
toast.success("데이터가 저장되었습니다.");
} else {
throw new Error(result.message || "저장 실패");
}
} catch (error) {
console.error("❌ 저장 실패:", error);
toast.error(`저장 실패: ${error.message}`);
throw error;
}
}
/**
* 업데이트 액션 실행
*/
private static async executeActionUpdate(action: any, context: ButtonActionContext): Promise<void> {
console.log("🔄 업데이트 액션 실행:", action);
console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2));
// 🎯 필드 매핑 정보 사용하여 업데이트 데이터 구성
let updateData: Record<string, any> = {};
// 액션에 필드 매핑 정보가 있는지 확인
if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
console.log("📋 필드 매핑 정보 발견:", action.fieldMappings);
// 🔑 먼저 선택된 데이터의 모든 필드를 기본으로 포함 (기본키 보존)
if (context.selectedRowsData?.[0]) {
updateData = { ...context.selectedRowsData[0] };
console.log("🔑 선택된 데이터를 기본으로 설정 (기본키 보존):", updateData);
}
// 필드 매핑에 따라 데이터 구성 (덮어쓰기)
action.fieldMappings.forEach((mapping: any) => {
const { sourceField, targetField, defaultValue, valueType } = mapping;
let value: any;
// 값 소스에 따라 데이터 가져오기
if (valueType === "form" && context.formData && sourceField) {
value = context.formData[sourceField];
} else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) {
value = context.selectedRowsData[0][sourceField];
} else if (valueType === "default" || !sourceField) {
value = defaultValue;
}
// 타겟 필드에 값 설정 (덮어쓰기)
if (targetField && value !== undefined) {
updateData[targetField] = value;
console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`);
}
});
} else {
console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용");
// 폴백: 기존 방식
updateData = {
...context.formData,
...context.selectedRowsData?.[0],
};
}
console.log("📊 최종 업데이트할 데이터:", updateData);
try {
// 🔥 실제 업데이트 API 호출
if (!context.tableName) {
throw new Error("테이블명이 설정되지 않았습니다.");
}
// 먼저 ID 찾기
const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName);
let updateId: string | undefined;
if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) {
updateId = updateData[primaryKeysResult.data[0]];
}
if (!updateId) {
// 폴백: 일반적인 ID 필드들 확인
const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"];
for (const field of commonIdFields) {
if (updateData[field]) {
updateId = updateData[field];
break;
}
}
}
if (!updateId) {
throw new Error("업데이트할 항목의 ID를 찾을 수 없습니다.");
}
const result = await DynamicFormApi.updateFormData(updateId, {
tableName: context.tableName,
data: updateData,
});
if (result.success) {
console.log("✅ 업데이트 성공:", result);
toast.success("데이터가 업데이트되었습니다.");
} else {
throw new Error(result.message || "업데이트 실패");
}
} catch (error) {
console.error("❌ 업데이트 실패:", error);
toast.error(`업데이트 실패: ${error.message}`);
throw error;
}
}
/**
* 삭제 액션 실행
*/
private static async executeActionDelete(action: any, context: ButtonActionContext): Promise<void> {
console.log("🗑️ 삭제 액션 실행:", action);
// 실제 삭제 로직 (기존 handleDelete와 유사)
if (!context.selectedRowsData || context.selectedRowsData.length === 0) {
throw new Error("삭제할 항목을 선택해주세요.");
}
const deleteData = context.selectedRowsData[0];
console.log("삭제할 데이터:", deleteData);
try {
// 🔥 실제 삭제 API 호출
if (!context.tableName) {
throw new Error("테이블명이 설정되지 않았습니다.");
}
// 기존 handleDelete와 동일한 로직으로 ID 찾기
const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName);
let deleteId: string | undefined;
if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) {
deleteId = deleteData[primaryKeysResult.data[0]];
}
if (!deleteId) {
// 폴백: 일반적인 ID 필드들 확인
const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"];
for (const field of commonIdFields) {
if (deleteData[field]) {
deleteId = deleteData[field];
break;
}
}
}
if (!deleteId) {
throw new Error("삭제할 항목의 ID를 찾을 수 없습니다.");
}
const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName);
if (result.success) {
console.log("✅ 삭제 성공:", result);
toast.success("데이터가 삭제되었습니다.");
} else {
throw new Error(result.message || "삭제 실패");
}
} catch (error) {
console.error("❌ 삭제 실패:", error);
toast.error(`삭제 실패: ${error.message}`);
throw error;
}
}
/**
* 삽입 액션 실행 (체크박스 선택된 데이터를 필드매핑에 따라 새 테이블에 삽입)
*/
private static async executeActionInsert(action: any, context: ButtonActionContext): Promise<void> {
console.log(" 삽입 액션 실행:", action);
let insertData: Record<string, any> = {};
// 액션에 필드 매핑 정보가 있는지 확인
if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
console.log("📋 삽입 액션 - 필드 매핑 정보:", action.fieldMappings);
// 🎯 체크박스로 선택된 데이터가 있는지 확인
if (!context.selectedRowsData || context.selectedRowsData.length === 0) {
throw new Error("삽입할 소스 데이터를 선택해주세요. (테이블에서 체크박스 선택 필요)");
}
const sourceData = context.selectedRowsData[0]; // 첫 번째 선택된 데이터 사용
console.log("🎯 삽입 소스 데이터 (체크박스 선택):", sourceData);
console.log("🔍 소스 데이터 사용 가능한 키들:", Object.keys(sourceData));
// 필드 매핑에 따라 데이터 구성
action.fieldMappings.forEach((mapping: any) => {
const { sourceField, targetField, defaultValue } = mapping;
// valueType이 없으면 기본값을 "selection"으로 설정
const valueType = mapping.valueType || "selection";
let value: any;
console.log(`🔍 매핑 처리 중: ${sourceField}${targetField} (valueType: ${valueType})`);
// 값 소스에 따라 데이터 가져오기
if (valueType === "form" && context.formData && sourceField) {
// 폼 데이터에서 가져오기
value = context.formData[sourceField];
console.log(`📝 폼에서 매핑: ${sourceField}${targetField} = ${value}`);
} else if (valueType === "selection" && sourceField) {
// 선택된 테이블 데이터에서 가져오기 (다양한 필드명 시도)
value =
sourceData[sourceField] ||
sourceData[sourceField + "_name"] || // 조인된 필드 (_name 접미사)
sourceData[sourceField + "Name"]; // 카멜케이스
console.log(`📊 테이블에서 매핑: ${sourceField}${targetField} = ${value} (소스필드: ${sourceField})`);
} else if (valueType === "default" || (defaultValue !== undefined && defaultValue !== "")) {
// 기본값 사용 (valueType이 "default"이거나 defaultValue가 있을 때)
value = defaultValue;
console.log(`🔧 기본값 매핑: ${targetField} = ${value}`);
} else {
console.warn(`⚠️ 매핑 실패: ${sourceField}${targetField} (값을 찾을 수 없음)`);
console.warn(` - valueType: ${valueType}, defaultValue: ${defaultValue}`);
console.warn(` - 소스 데이터 키들:`, Object.keys(sourceData));
console.warn(` - sourceData[${sourceField}] =`, sourceData[sourceField]);
return; // 값이 없으면 해당 필드는 스킵
}
// 대상 필드에 값 설정
if (targetField && value !== undefined && value !== null) {
insertData[targetField] = value;
}
});
console.log("🎯 최종 삽입 데이터 (필드매핑 적용):", insertData);
} else {
// 필드 매핑이 없으면 폼 데이터를 기본으로 사용
insertData = { ...context.formData };
console.log("📝 기본 삽입 데이터 (폼 기반):", insertData);
}
try {
// 🔥 실제 삽입 API 호출 - 필수 매개변수 포함
// 필드 매핑에서 첫 번째 targetTable을 찾거나 기본값 사용
const targetTable = action.fieldMappings?.[0]?.targetTable || action.targetTable || "test_project_info";
const formDataPayload = {
screenId: 0, // 제어 관리에서는 screenId가 없으므로 0 사용
tableName: targetTable, // 필드 매핑에서 대상 테이블명 가져오기
data: insertData,
};
console.log("🎯 대상 테이블:", targetTable);
console.log("📋 삽입할 데이터:", insertData);
console.log("💾 폼 데이터 저장 요청:", formDataPayload);
const result = await DynamicFormApi.saveFormData(formDataPayload);
if (result.success) {
console.log("✅ 삽입 성공:", result);
toast.success(`데이터가 타겟 테이블에 성공적으로 삽입되었습니다.`);
} else {
throw new Error(result.message || "삽입 실패");
}
} catch (error) {
console.error("❌ 삽입 실패:", error);
toast.error(`삽입 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`);
throw error;
}
}
/**
* 테이블 이력 보기 액션 처리
*/
private static async handleViewTableHistory(
config: ButtonActionConfig,
context: ButtonActionContext,
): Promise<boolean> {
console.log("📜 테이블 이력 보기 액션 실행:", { config, context });
// 테이블명 결정 (설정 > 컨텍스트 > 폼 데이터)
const tableName = config.historyTableName || context.tableName;
if (!tableName) {
toast.error("테이블명이 지정되지 않았습니다.");
return false;
}
// 레코드 ID 가져오기 (선택사항 - 없으면 전체 테이블 이력)
const recordIdField = config.historyRecordIdField || "id";
const recordIdSource = config.historyRecordIdSource || "selected_row";
let recordId: any = null;
let recordLabel: string | undefined;
switch (recordIdSource) {
case "selected_row":
// 선택된 행에서 가져오기 (선택사항)
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
const selectedRow = context.selectedRowsData[0];
recordId = selectedRow[recordIdField];
// 라벨 필드가 지정되어 있으면 사용
if (config.historyRecordLabelField) {
recordLabel = selectedRow[config.historyRecordLabelField];
}
} else if (context.flowSelectedData && context.flowSelectedData.length > 0) {
// 플로우 선택 데이터 폴백
const selectedRow = context.flowSelectedData[0];
recordId = selectedRow[recordIdField];
if (config.historyRecordLabelField) {
recordLabel = selectedRow[config.historyRecordLabelField];
}
}
break;
case "form_field":
// 폼 필드에서 가져오기
recordId = context.formData?.[recordIdField];
if (config.historyRecordLabelField) {
recordLabel = context.formData?.[config.historyRecordLabelField];
}
break;
case "context":
// 원본 데이터에서 가져오기
recordId = context.originalData?.[recordIdField];
if (config.historyRecordLabelField) {
recordLabel = context.originalData?.[config.historyRecordLabelField];
}
break;
}
// recordId가 없어도 괜찮음 - 전체 테이블 이력 보기
console.log("📋 이력 조회 대상:", {
tableName,
recordId: recordId || "전체",
recordLabel,
mode: recordId ? "단일 레코드" : "전체 테이블",
});
// 이력 모달 열기 (동적 import)
try {
const { TableHistoryModal } = await import("@/components/common/TableHistoryModal");
const { createRoot } = await import("react-dom/client");
// 모달 컨테이너 생성
const modalContainer = document.createElement("div");
document.body.appendChild(modalContainer);
const root = createRoot(modalContainer);
const closeModal = () => {
root.unmount();
document.body.removeChild(modalContainer);
};
root.render(
React.createElement(TableHistoryModal, {
open: true,
onOpenChange: (open: boolean) => {
if (!open) closeModal();
},
tableName,
recordId,
recordLabel,
displayColumn: config.historyDisplayColumn,
}),
);
return true;
} catch (error) {
console.error("❌ 이력 모달 열기 실패:", error);
toast.error("이력 조회 중 오류가 발생했습니다.");
return false;
}
}
/**
* 엑셀 다운로드 액션 처리
*/
private static async handleExcelDownload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📥 엑셀 다운로드 시작:", { config, context });
// 동적 import로 엑셀 유틸리티 로드
const { exportToExcel } = await import("@/lib/utils/excelExport");
let dataToExport: any[] = [];
// 1순위: 선택된 행 데이터
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
dataToExport = context.selectedRowsData;
console.log("✅ 선택된 행 데이터 사용:", dataToExport.length);
}
// 2순위: 테이블 전체 데이터 (API 호출)
else if (context.tableName) {
console.log("🔄 테이블 전체 데이터 조회 중...", context.tableName);
try {
const { dynamicFormApi } = await import("@/lib/api/dynamicForm");
const response = await dynamicFormApi.getTableData(context.tableName, {
page: 1,
pageSize: 10000, // 최대 10,000개 행
sortBy: "id", // 기본 정렬: id 컬럼
sortOrder: "asc", // 오름차순
});
console.log("📦 API 응답 구조:", {
response,
responseSuccess: response.success,
responseData: response.data,
responseDataType: typeof response.data,
responseDataIsArray: Array.isArray(response.data),
responseDataLength: Array.isArray(response.data) ? response.data.length : "N/A",
});
if (response.success && response.data) {
dataToExport = response.data;
console.log("✅ 테이블 전체 데이터 조회 완료:", {
count: dataToExport.length,
firstRow: dataToExport[0],
});
} else {
console.error("❌ API 응답에 데이터가 없습니다:", response);
}
} catch (error) {
console.error("❌ 테이블 데이터 조회 실패:", error);
}
}
// 4순위: 폼 데이터
else if (context.formData && Object.keys(context.formData).length > 0) {
dataToExport = [context.formData];
console.log("✅ 폼 데이터 사용:", dataToExport);
}
console.log("📊 최종 다운로드 데이터:", {
selectedRowsData: context.selectedRowsData,
selectedRowsLength: context.selectedRowsData?.length,
formData: context.formData,
tableName: context.tableName,
dataToExport,
dataToExportType: typeof dataToExport,
dataToExportIsArray: Array.isArray(dataToExport),
dataToExportLength: Array.isArray(dataToExport) ? dataToExport.length : "N/A",
});
// 배열이 아니면 배열로 변환
if (!Array.isArray(dataToExport)) {
console.warn("⚠️ dataToExport가 배열이 아닙니다. 변환 시도:", dataToExport);
// 객체인 경우 배열로 감싸기
if (typeof dataToExport === "object" && dataToExport !== null) {
dataToExport = [dataToExport];
} else {
toast.error("다운로드할 데이터 형식이 올바르지 않습니다.");
return false;
}
}
if (dataToExport.length === 0) {
toast.error("다운로드할 데이터가 없습니다.");
return false;
}
// 파일명 생성
const fileName = config.excelFileName || `${context.tableName || "데이터"}_${new Date().toISOString().split("T")[0]}.xlsx`;
const sheetName = config.excelSheetName || "Sheet1";
const includeHeaders = config.excelIncludeHeaders !== false;
console.log("📥 엑셀 다운로드 실행:", {
fileName,
sheetName,
includeHeaders,
dataCount: dataToExport.length,
firstRow: dataToExport[0],
});
// 엑셀 다운로드 실행
await exportToExcel(dataToExport, fileName, sheetName, includeHeaders);
toast.success(config.successMessage || "엑셀 파일이 다운로드되었습니다.");
return true;
} catch (error) {
console.error("❌ 엑셀 다운로드 실패:", error);
toast.error(config.errorMessage || "엑셀 다운로드 중 오류가 발생했습니다.");
return false;
}
}
/**
* 엑셀 업로드 액션 처리
*/
private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📤 엑셀 업로드 모달 열기:", { config, context });
// 동적 import로 모달 컴포넌트 로드
const { ExcelUploadModal } = await import("@/components/common/ExcelUploadModal");
const { createRoot } = await import("react-dom/client");
// 모달 컨테이너 생성
const modalContainer = document.createElement("div");
document.body.appendChild(modalContainer);
const root = createRoot(modalContainer);
const closeModal = () => {
root.unmount();
document.body.removeChild(modalContainer);
};
root.render(
React.createElement(ExcelUploadModal, {
open: true,
onOpenChange: (open: boolean) => {
if (!open) closeModal();
},
tableName: context.tableName || "",
uploadMode: config.excelUploadMode || "insert",
keyColumn: config.excelKeyColumn,
onSuccess: () => {
// 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
context.onRefresh?.();
closeModal();
},
}),
);
return true;
} catch (error) {
console.error("❌ 엑셀 업로드 모달 열기 실패:", error);
toast.error(config.errorMessage || "엑셀 업로드 중 오류가 발생했습니다.");
return false;
}
}
/**
* 바코드 스캔 액션 처리
*/
private static async handleBarcodeScan(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📷 바코드 스캔 모달 열기:", { config, context });
// 동적 import로 모달 컴포넌트 로드
const { BarcodeScanModal } = await import("@/components/common/BarcodeScanModal");
const { createRoot } = await import("react-dom/client");
// 모달 컨테이너 생성
const modalContainer = document.createElement("div");
document.body.appendChild(modalContainer);
const root = createRoot(modalContainer);
const closeModal = () => {
root.unmount();
document.body.removeChild(modalContainer);
};
root.render(
React.createElement(BarcodeScanModal, {
open: true,
onOpenChange: (open: boolean) => {
if (!open) closeModal();
},
targetField: config.barcodeTargetField,
barcodeFormat: config.barcodeFormat || "all",
autoSubmit: config.barcodeAutoSubmit || false,
onScanSuccess: (barcode: string) => {
console.log("✅ 바코드 스캔 성공:", barcode);
// 대상 필드에 값 입력
if (config.barcodeTargetField && context.onFormDataChange) {
context.onFormDataChange({
...context.formData,
[config.barcodeTargetField]: barcode,
});
}
toast.success(`바코드 스캔 완료: ${barcode}`);
// 자동 제출 옵션이 켜져있으면 저장
if (config.barcodeAutoSubmit) {
this.handleSave(config, context);
}
closeModal();
},
}),
);
return true;
} catch (error) {
console.error("❌ 바코드 스캔 모달 열기 실패:", error);
toast.error("바코드 스캔 중 오류가 발생했습니다.");
return false;
}
}
/**
* 폼 데이터 유효성 검사
*/
private static validateFormData(formData: Record<string, any>): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
// 기본적인 유효성 검사 로직
Object.entries(formData).forEach(([key, value]) => {
// 빈 값 체크 (null, undefined, 빈 문자열)
if (value === null || value === undefined || value === "") {
// 필수 필드는 향후 컴포넌트 설정에서 확인 가능
console.warn(`필드 '${key}'가 비어있습니다.`);
}
// 기본 타입 검증
if (typeof value === "string" && value.trim() === "") {
console.warn(`필드 '${key}'가 공백만 포함되어 있습니다.`);
}
});
// 최소한 하나의 필드는 있어야 함
if (Object.keys(formData).length === 0) {
errors.push("저장할 데이터가 없습니다.");
}
return {
isValid: errors.length === 0,
errors,
};
}
}
/**
* 기본 버튼 액션 설정들
*/
export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActionConfig>> = {
save: {
type: "save",
validateForm: true,
confirmMessage: "저장하시겠습니까?",
successMessage: "저장되었습니다.",
errorMessage: "저장 중 오류가 발생했습니다.",
},
delete: {
type: "delete",
confirmMessage: "정말 삭제하시겠습니까?",
successMessage: "삭제되었습니다.",
errorMessage: "삭제 중 오류가 발생했습니다.",
},
navigate: {
type: "navigate",
},
modal: {
type: "modal",
modalSize: "md",
},
edit: {
type: "edit",
successMessage: "편집되었습니다.",
},
control: {
type: "control",
},
view_table_history: {
type: "view_table_history",
historyRecordIdField: "id",
historyRecordIdSource: "selected_row",
},
excel_download: {
type: "excel_download",
excelIncludeHeaders: true,
successMessage: "엑셀 파일이 다운로드되었습니다.",
errorMessage: "엑셀 다운로드 중 오류가 발생했습니다.",
},
excel_upload: {
type: "excel_upload",
excelUploadMode: "insert",
confirmMessage: "엑셀 파일을 업로드하시겠습니까?",
successMessage: "엑셀 파일이 업로드되었습니다.",
errorMessage: "엑셀 업로드 중 오류가 발생했습니다.",
},
barcode_scan: {
type: "barcode_scan",
barcodeFormat: "all",
barcodeAutoSubmit: false,
},
};