"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; originalData?: Record; // 부분 업데이트용 원본 데이터 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 { 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 { 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, 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): 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 { // 제출은 저장과 유사하지만 추가적인 처리가 있을 수 있음 return await this.handleSave(config, context); } /** * 삭제 액션 처리 */ private static async handleDelete(config: ButtonActionConfig, context: ButtonActionContext): Promise { 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): { 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> = { 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", }, };