2025-09-12 14:24:25 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
|
import { screenApi } from "@/lib/api/screen";
|
|
|
|
|
|
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
2025-09-19 12:19:34 +09:00
|
|
|
|
import { OptimizedButtonDataflowService, ExtendedControlContext } from "@/lib/services/optimizedButtonDataflowService";
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 버튼 액션 타입 정의
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type ButtonActionType =
|
|
|
|
|
|
| "save" // 저장
|
|
|
|
|
|
| "cancel" // 취소
|
|
|
|
|
|
| "delete" // 삭제
|
|
|
|
|
|
| "edit" // 편집
|
|
|
|
|
|
| "add" // 추가
|
|
|
|
|
|
| "search" // 검색
|
|
|
|
|
|
| "reset" // 초기화
|
|
|
|
|
|
| "submit" // 제출
|
|
|
|
|
|
| "close" // 닫기
|
|
|
|
|
|
| "popup" // 팝업 열기
|
|
|
|
|
|
| "navigate" // 페이지 이동
|
|
|
|
|
|
| "modal" // 모달 열기
|
|
|
|
|
|
| "newWindow"; // 새 창 열기
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 버튼 액션 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ButtonActionConfig {
|
|
|
|
|
|
type: ButtonActionType;
|
|
|
|
|
|
|
|
|
|
|
|
// 저장/제출 관련
|
|
|
|
|
|
saveEndpoint?: string;
|
|
|
|
|
|
validateForm?: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
// 네비게이션 관련
|
|
|
|
|
|
targetUrl?: string;
|
|
|
|
|
|
targetScreenId?: number;
|
|
|
|
|
|
|
|
|
|
|
|
// 모달/팝업 관련
|
|
|
|
|
|
modalTitle?: string;
|
|
|
|
|
|
modalSize?: "sm" | "md" | "lg" | "xl";
|
|
|
|
|
|
popupWidth?: number;
|
|
|
|
|
|
popupHeight?: number;
|
|
|
|
|
|
|
|
|
|
|
|
// 확인 메시지
|
|
|
|
|
|
confirmMessage?: string;
|
|
|
|
|
|
successMessage?: string;
|
|
|
|
|
|
errorMessage?: string;
|
2025-09-19 12:19:34 +09:00
|
|
|
|
|
|
|
|
|
|
// 제어관리 관련
|
|
|
|
|
|
enableDataflowControl?: boolean;
|
|
|
|
|
|
dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용)
|
2025-09-12 14:24:25 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 버튼 액션 실행 컨텍스트
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ButtonActionContext {
|
|
|
|
|
|
formData: Record<string, any>;
|
2025-09-18 18:49:30 +09:00
|
|
|
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
2025-09-12 14:24:25 +09:00
|
|
|
|
screenId?: number;
|
|
|
|
|
|
tableName?: string;
|
|
|
|
|
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
|
|
|
|
|
onClose?: () => void;
|
|
|
|
|
|
onRefresh?: () => void;
|
2025-09-18 18:49:30 +09:00
|
|
|
|
|
|
|
|
|
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
|
|
|
|
|
selectedRows?: any[];
|
|
|
|
|
|
selectedRowsData?: any[];
|
2025-09-12 14:24:25 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 버튼 액션 실행기
|
|
|
|
|
|
*/
|
|
|
|
|
|
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 "submit":
|
|
|
|
|
|
return await this.handleSubmit(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "delete":
|
|
|
|
|
|
return await this.handleDelete(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "reset":
|
|
|
|
|
|
return this.handleReset(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "cancel":
|
|
|
|
|
|
return this.handleCancel(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "navigate":
|
|
|
|
|
|
return this.handleNavigate(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "modal":
|
|
|
|
|
|
return this.handleModal(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "newWindow":
|
|
|
|
|
|
return this.handleNewWindow(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "popup":
|
|
|
|
|
|
return this.handlePopup(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "search":
|
|
|
|
|
|
return this.handleSearch(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "add":
|
|
|
|
|
|
return this.handleAdd(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "edit":
|
|
|
|
|
|
return this.handleEdit(config, context);
|
|
|
|
|
|
|
|
|
|
|
|
case "close":
|
|
|
|
|
|
return this.handleClose(config, context);
|
|
|
|
|
|
|
2025-09-19 12:19:34 +09:00
|
|
|
|
case "control":
|
|
|
|
|
|
return this.handleControl(config, context);
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("버튼 액션 실행 오류:", error);
|
|
|
|
|
|
toast.error(config.errorMessage || "작업 중 오류가 발생했습니다.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-18 18:49:30 +09:00
|
|
|
|
* 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
|
2025-09-12 14:24:25 +09:00
|
|
|
|
*/
|
|
|
|
|
|
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
2025-09-18 18:49:30 +09:00
|
|
|
|
const { formData, originalData, tableName, screenId } = context;
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
// 폼 유효성 검사
|
|
|
|
|
|
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) {
|
2025-09-18 18:49:30 +09:00
|
|
|
|
// DB에서 실제 기본키 조회하여 INSERT/UPDATE 자동 판단
|
|
|
|
|
|
const primaryKeyResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
|
|
|
|
|
|
|
|
|
|
|
|
if (!primaryKeyResult.success) {
|
|
|
|
|
|
throw new Error(primaryKeyResult.message || "기본키 조회에 실패했습니다.");
|
|
|
|
|
|
}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
const primaryKeys = primaryKeyResult.data || [];
|
|
|
|
|
|
const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys);
|
2025-09-19 02:15:21 +09:00
|
|
|
|
|
|
|
|
|
|
// 단순히 기본키 값 존재 여부로 판단 (임시)
|
|
|
|
|
|
// TODO: 실제 테이블에서 기본키로 레코드 존재 여부 확인하는 API 필요
|
|
|
|
|
|
const isUpdate = false; // 현재는 항상 INSERT로 처리
|
2025-09-18 18:49:30 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("💾 저장 모드 판단 (DB 기반):", {
|
2025-09-12 14:24:25 +09:00
|
|
|
|
tableName,
|
2025-09-18 18:49:30 +09:00
|
|
|
|
formData,
|
|
|
|
|
|
primaryKeys,
|
|
|
|
|
|
primaryKeyValue,
|
|
|
|
|
|
isUpdate: isUpdate ? "UPDATE" : "INSERT",
|
2025-09-12 14:24:25 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
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 처리
|
|
|
|
|
|
console.log("🆕 INSERT 모드로 저장:", { formData });
|
|
|
|
|
|
|
|
|
|
|
|
saveResult = await DynamicFormApi.saveFormData({
|
|
|
|
|
|
screenId,
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
data: formData,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
if (!saveResult.success) {
|
|
|
|
|
|
throw new Error(saveResult.message || "저장에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 저장 성공:", saveResult);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
context.onRefresh?.();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("저장 오류:", error);
|
|
|
|
|
|
throw error; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* DB에서 조회한 실제 기본키로 formData에서 값 추출
|
|
|
|
|
|
* @param formData 폼 데이터
|
|
|
|
|
|
* @param primaryKeys DB에서 조회한 실제 기본키 컬럼명 배열
|
|
|
|
|
|
* @returns 기본키 값 (복합키의 경우 첫 번째 키 값)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static extractPrimaryKeyValueFromDB(formData: Record<string, any>, primaryKeys: string[]): any {
|
|
|
|
|
|
if (!primaryKeys || primaryKeys.length === 0) {
|
|
|
|
|
|
console.log("🔍 DB에서 기본키를 찾을 수 없습니다. INSERT 모드로 처리됩니다.");
|
|
|
|
|
|
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에 없는 경우
|
|
|
|
|
|
console.log(`❌ 기본키 컬럼 '${primaryKeyColumn}'이 formData에 없습니다. INSERT 모드로 처리됩니다.`);
|
|
|
|
|
|
console.log("📋 DB 기본키 컬럼들:", primaryKeys);
|
|
|
|
|
|
console.log("📋 사용 가능한 필드들:", Object.keys(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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 기본 키를 찾지 못한 경우
|
|
|
|
|
|
console.log("🔍 추측 기반으로 기본 키를 찾을 수 없습니다. INSERT 모드로 처리됩니다.");
|
|
|
|
|
|
console.log("📋 사용 가능한 필드들:", Object.keys(formData));
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 제출 액션 처리
|
|
|
|
|
|
*/
|
|
|
|
|
|
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> {
|
2025-09-18 18:49:30 +09:00
|
|
|
|
const { formData, tableName, screenId, selectedRowsData } = context;
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-18 18:49:30 +09:00
|
|
|
|
// 다중 선택된 행이 있는 경우 (테이블에서 체크박스로 선택)
|
|
|
|
|
|
if (selectedRowsData && selectedRowsData.length > 0) {
|
|
|
|
|
|
console.log(`다중 삭제 액션 실행: ${selectedRowsData.length}개 항목`, selectedRowsData);
|
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
|
// 테이블의 기본키 조회
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
// 각 선택된 항목을 삭제
|
|
|
|
|
|
for (const rowData of selectedRowsData) {
|
2025-09-19 02:15:21 +09:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
console.log("선택된 행 데이터:", rowData);
|
2025-09-19 02:15:21 +09:00
|
|
|
|
console.log("최종 추출된 deleteId:", deleteId);
|
2025-09-18 18:49:30 +09:00
|
|
|
|
|
|
|
|
|
|
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);
|
2025-09-19 02:15:21 +09:00
|
|
|
|
throw new Error(
|
|
|
|
|
|
`삭제 ID를 찾을 수 없습니다. 기본키: ${primaryKeys.join(", ")}, 사용 가능한 필드: ${Object.keys(rowData).join(", ")}`,
|
|
|
|
|
|
);
|
2025-09-18 18:49:30 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 다중 삭제 성공: ${selectedRowsData.length}개 항목`);
|
|
|
|
|
|
context.onRefresh?.(); // 테이블 새로고침
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 단일 삭제 (기존 로직)
|
2025-09-12 14:24:25 +09:00
|
|
|
|
if (tableName && screenId && formData.id) {
|
2025-09-18 18:49:30 +09:00
|
|
|
|
console.log("단일 데이터 삭제:", { tableName, screenId, id: formData.id });
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
// 실제 삭제 API 호출
|
2025-09-18 18:49:30 +09:00
|
|
|
|
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName);
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
if (!deleteResult.success) {
|
|
|
|
|
|
throw new Error(deleteResult.message || "삭제에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
console.log("✅ 단일 삭제 성공:", deleteResult);
|
2025-09-12 14:24:25 +09:00
|
|
|
|
} 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 handleModal(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
|
|
|
|
|
// 모달 열기 로직
|
|
|
|
|
|
console.log("모달 열기:", {
|
|
|
|
|
|
title: config.modalTitle,
|
|
|
|
|
|
size: config.modalSize,
|
|
|
|
|
|
targetScreenId: config.targetScreenId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (config.targetScreenId) {
|
|
|
|
|
|
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
|
|
|
|
|
const modalEvent = new CustomEvent("openScreenModal", {
|
|
|
|
|
|
detail: {
|
|
|
|
|
|
screenId: config.targetScreenId,
|
|
|
|
|
|
title: config.modalTitle || "화면",
|
|
|
|
|
|
size: config.modalSize || "md",
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-09-18 18:49:30 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
window.dispatchEvent(modalEvent);
|
|
|
|
|
|
toast.success("모달 화면이 열렸습니다.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.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 handleEdit(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
2025-09-18 18:49:30 +09:00
|
|
|
|
const { selectedRowsData } = context;
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 행이 없는 경우
|
|
|
|
|
|
if (!selectedRowsData || selectedRowsData.length === 0) {
|
|
|
|
|
|
toast.error("수정할 항목을 선택해주세요.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 편집 화면이 설정되지 않은 경우
|
|
|
|
|
|
if (!config.targetScreenId) {
|
|
|
|
|
|
toast.error("수정 폼 화면이 설정되지 않았습니다. 버튼 설정에서 수정 폼 화면을 선택해주세요.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📝 편집 액션 실행: ${selectedRowsData.length}개 항목`, {
|
|
|
|
|
|
selectedRowsData,
|
|
|
|
|
|
targetScreenId: config.targetScreenId,
|
|
|
|
|
|
editMode: config.editMode,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedRowsData.length === 1) {
|
|
|
|
|
|
// 단일 항목 편집
|
|
|
|
|
|
const rowData = selectedRowsData[0];
|
|
|
|
|
|
console.log("📝 단일 항목 편집:", rowData);
|
|
|
|
|
|
|
|
|
|
|
|
this.openEditForm(config, rowData, context);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 다중 항목 편집 - 현재는 단일 편집만 지원
|
|
|
|
|
|
toast.error("현재 단일 항목 편집만 지원됩니다. 하나의 항목만 선택해주세요.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: 향후 다중 편집 지원
|
|
|
|
|
|
// console.log("📝 다중 항목 편집:", selectedRowsData);
|
|
|
|
|
|
// this.openBulkEditForm(config, selectedRowsData, context);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 편집 폼 열기 (단일 항목)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static openEditForm(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
|
|
|
|
|
|
const editMode = config.editMode || "modal";
|
|
|
|
|
|
|
|
|
|
|
|
switch (editMode) {
|
|
|
|
|
|
case "modal":
|
|
|
|
|
|
// 모달로 편집 폼 열기
|
|
|
|
|
|
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 openEditModal(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
|
|
|
|
|
|
console.log("🎭 편집 모달 열기:", {
|
|
|
|
|
|
targetScreenId: config.targetScreenId,
|
|
|
|
|
|
modalSize: config.modalSize,
|
|
|
|
|
|
rowData,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 모달 열기 이벤트 발생
|
|
|
|
|
|
const modalEvent = new CustomEvent("openEditModal", {
|
|
|
|
|
|
detail: {
|
|
|
|
|
|
screenId: config.targetScreenId,
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 닫기 액션 처리
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static handleClose(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
|
|
|
|
|
console.log("닫기 액션 실행:", context);
|
|
|
|
|
|
context.onClose?.();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-19 12:19:34 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 제어 전용 액션 처리 (조건 체크만 수행)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static async handleControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
|
|
|
|
|
console.log("🎯 ButtonActionExecutor.handleControl 실행:", {
|
|
|
|
|
|
formData: context.formData,
|
|
|
|
|
|
selectedRows: context.selectedRows,
|
|
|
|
|
|
selectedRowsData: context.selectedRowsData,
|
|
|
|
|
|
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.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 모드 사용");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const extendedContext: ExtendedControlContext = {
|
|
|
|
|
|
formData: context.formData || {},
|
|
|
|
|
|
selectedRows: context.selectedRows || [],
|
|
|
|
|
|
selectedRowsData: context.selectedRowsData || [],
|
|
|
|
|
|
controlDataSource,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 제어 조건 검증 시작:", {
|
|
|
|
|
|
dataflowConfig: config.dataflowConfig,
|
|
|
|
|
|
extendedContext,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 실제 제어 조건 검증 수행
|
|
|
|
|
|
const validationResult = await OptimizedButtonDataflowService.executeExtendedValidation(
|
|
|
|
|
|
config.dataflowConfig,
|
|
|
|
|
|
extendedContext,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (validationResult.success) {
|
|
|
|
|
|
console.log("✅ 제어 조건 만족 - 액션 실행 시작:", {
|
|
|
|
|
|
actions: validationResult.actions,
|
|
|
|
|
|
context,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 조건을 만족했으므로 실제 액션 실행
|
|
|
|
|
|
if (validationResult.actions && validationResult.actions.length > 0) {
|
|
|
|
|
|
console.log("🚀 액션 실행 시작:", validationResult.actions);
|
|
|
|
|
|
await this.executeRelationshipActions(validationResult.actions, context);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ 실행할 액션이 없습니다:", {
|
|
|
|
|
|
hasActions: !!validationResult.actions,
|
|
|
|
|
|
actionsLength: validationResult.actions?.length,
|
|
|
|
|
|
validationResult,
|
|
|
|
|
|
});
|
|
|
|
|
|
toast.success(config.successMessage || "제어 조건을 만족합니다. (실행할 액션 없음)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 새로고침이 필요한 경우
|
|
|
|
|
|
if (context.onRefresh) {
|
|
|
|
|
|
context.onRefresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.error(validationResult.message || "제어 조건을 만족하지 않습니다.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("제어 조건 검증 중 오류:", error);
|
|
|
|
|
|
toast.error("제어 조건 검증 중 오류가 발생했습니다.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 관계도에서 가져온 액션들을 실행
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 폼 데이터 유효성 검사
|
|
|
|
|
|
*/
|
|
|
|
|
|
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: "저장 중 오류가 발생했습니다.",
|
|
|
|
|
|
},
|
|
|
|
|
|
submit: {
|
|
|
|
|
|
type: "submit",
|
|
|
|
|
|
validateForm: true,
|
|
|
|
|
|
successMessage: "제출되었습니다.",
|
|
|
|
|
|
errorMessage: "제출 중 오류가 발생했습니다.",
|
|
|
|
|
|
},
|
|
|
|
|
|
delete: {
|
|
|
|
|
|
type: "delete",
|
|
|
|
|
|
confirmMessage: "정말 삭제하시겠습니까?",
|
|
|
|
|
|
successMessage: "삭제되었습니다.",
|
|
|
|
|
|
errorMessage: "삭제 중 오류가 발생했습니다.",
|
|
|
|
|
|
},
|
|
|
|
|
|
reset: {
|
|
|
|
|
|
type: "reset",
|
|
|
|
|
|
confirmMessage: "초기화하시겠습니까?",
|
|
|
|
|
|
successMessage: "초기화되었습니다.",
|
|
|
|
|
|
},
|
|
|
|
|
|
cancel: {
|
|
|
|
|
|
type: "cancel",
|
|
|
|
|
|
},
|
|
|
|
|
|
navigate: {
|
|
|
|
|
|
type: "navigate",
|
|
|
|
|
|
},
|
|
|
|
|
|
modal: {
|
|
|
|
|
|
type: "modal",
|
|
|
|
|
|
modalSize: "md",
|
|
|
|
|
|
},
|
|
|
|
|
|
newWindow: {
|
|
|
|
|
|
type: "newWindow",
|
|
|
|
|
|
popupWidth: 800,
|
|
|
|
|
|
popupHeight: 600,
|
|
|
|
|
|
},
|
|
|
|
|
|
popup: {
|
|
|
|
|
|
type: "popup",
|
|
|
|
|
|
popupWidth: 600,
|
|
|
|
|
|
popupHeight: 400,
|
|
|
|
|
|
},
|
|
|
|
|
|
search: {
|
|
|
|
|
|
type: "search",
|
|
|
|
|
|
successMessage: "검색을 실행했습니다.",
|
|
|
|
|
|
},
|
|
|
|
|
|
add: {
|
|
|
|
|
|
type: "add",
|
|
|
|
|
|
successMessage: "추가되었습니다.",
|
|
|
|
|
|
},
|
|
|
|
|
|
edit: {
|
|
|
|
|
|
type: "edit",
|
|
|
|
|
|
successMessage: "편집되었습니다.",
|
|
|
|
|
|
},
|
|
|
|
|
|
close: {
|
|
|
|
|
|
type: "close",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|