499 lines
13 KiB
TypeScript
499 lines
13 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>;
|
|
screenId?: number;
|
|
tableName?: string;
|
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
|
onClose?: () => void;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
/**
|
|
* 버튼 액션 실행기
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 저장 액션 처리
|
|
*/
|
|
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
|
const { formData, 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) {
|
|
// 기본 테이블 저장 로직
|
|
console.log("테이블 저장:", { tableName, formData, screenId });
|
|
|
|
// 실제 저장 API 호출
|
|
const 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; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 제출 액션 처리
|
|
*/
|
|
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 } = context;
|
|
|
|
try {
|
|
if (tableName && screenId && formData.id) {
|
|
console.log("데이터 삭제:", { tableName, screenId, id: formData.id });
|
|
|
|
// 실제 삭제 API 호출
|
|
const deleteResult = await DynamicFormApi.deleteFormData(formData.id);
|
|
|
|
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 {
|
|
console.log("편집 액션 실행:", context);
|
|
// 편집 로직 구현 (예: 편집 모드로 전환)
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 닫기 액션 처리
|
|
*/
|
|
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",
|
|
},
|
|
};
|