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

754 lines
23 KiB
TypeScript

"use client";
import { toast } from "sonner";
import { screenApi } from "@/lib/api/screen";
import { DynamicFormApi } from "@/lib/api/dynamicForm";
/**
* 버튼 액션 타입 정의
*/
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;
}
/**
* 버튼 액션 실행 컨텍스트
*/
export interface ButtonActionContext {
formData: Record<string, any>;
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
screenId?: number;
tableName?: string;
onFormDataChange?: (fieldName: string, value: any) => void;
onClose?: () => void;
onRefresh?: () => void;
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
}
/**
* 버튼 액션 실행기
*/
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);
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);
const isUpdate = primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== "";
console.log("💾 저장 모드 판단 (DB 기반):", {
tableName,
formData,
primaryKeys,
primaryKeyValue,
isUpdate: isUpdate ? "UPDATE" : "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 처리
console.log("🆕 INSERT 모드로 저장:", { formData });
saveResult = await DynamicFormApi.saveFormData({
screenId,
tableName,
data: formData,
});
}
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; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
}
}
/**
* 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;
}
/**
* 제출 액션 처리
*/
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 } = context;
try {
// 다중 선택된 행이 있는 경우 (테이블에서 체크박스로 선택)
if (selectedRowsData && selectedRowsData.length > 0) {
console.log(`다중 삭제 액션 실행: ${selectedRowsData.length}개 항목`, selectedRowsData);
// 각 선택된 항목을 삭제
for (const rowData of selectedRowsData) {
// 더 포괄적인 ID 찾기 (테이블 구조에 따라 다양한 필드명 시도)
const deleteId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK;
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를 찾을 수 없습니다. 사용 가능한 필드: ${Object.keys(rowData).join(", ")}`);
}
}
console.log(`✅ 다중 삭제 성공: ${selectedRowsData.length}개 항목`);
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 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",
},
});
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 {
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);
}
return true;
}
/**
* 편집 폼 열기 (단일 항목)
*/
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;
}
/**
* 닫기 액션 처리
*/
private static handleClose(config: ButtonActionConfig, context: ButtonActionContext): boolean {
console.log("닫기 액션 실행:", context);
context.onClose?.();
return true;
}
/**
* 폼 데이터 유효성 검사
*/
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",
},
};