From 4996dd55620cebc051bb2c15f138c72234586e07 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 23 Oct 2025 13:15:52 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 32 +++++++ .../screen/InteractiveScreenViewerDynamic.tsx | 8 ++ .../screen/RealtimePreviewDynamic.tsx | 20 ++++ .../config-panels/ButtonConfigPanel.tsx | 22 ++--- .../button-primary/ButtonPrimaryComponent.tsx | 94 ++++++++++++++++--- frontend/lib/utils/buttonActions.ts | 73 +------------- 6 files changed, 156 insertions(+), 93 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 7c15afff..12231373 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -24,6 +24,12 @@ export default function ScreenViewPage() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [formData, setFormData] = useState>({}); + // 테이블에서 선택된 행 데이터 (버튼 액션에 전달) + const [selectedRowsData, setSelectedRowsData] = useState([]); + + // 테이블 새로고침을 위한 키 (값이 변경되면 테이블이 리렌더링됨) + const [tableRefreshKey, setTableRefreshKey] = useState(0); + // 편집 모달 상태 const [editModalOpen, setEditModalOpen] = useState(false); const [editModalConfig, setEditModalConfig] = useState<{ @@ -172,6 +178,19 @@ export default function ScreenViewPage() { isSelected={false} isDesignMode={false} onClick={() => {}} + screenId={screenId} + tableName={screen?.tableName} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("🔍 화면에서 선택된 행 데이터:", selectedData); + setSelectedRowsData(selectedData); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + console.log("🔄 테이블 새로고침 요청됨"); + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // 선택 해제 + }} > {/* 자식 컴포넌트들 */} {(component.type === "group" || component.type === "container" || component.type === "area") && @@ -195,6 +214,19 @@ export default function ScreenViewPage() { isSelected={false} isDesignMode={false} onClick={() => {}} + screenId={screenId} + tableName={screen?.tableName} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); + setSelectedRowsData(selectedData); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + console.log("🔄 테이블 새로고침 요청됨 (자식)"); + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // 선택 해제 + }} /> ); })} diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index d0eacf74..2765e57e 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -48,6 +48,9 @@ export const InteractiveScreenViewerDynamic: React.FC>({}); const [dateValues, setDateValues] = useState>({}); + // 테이블에서 선택된 행 데이터 (버튼 액션에 전달) + const [selectedRowsData, setSelectedRowsData] = useState([]); + // 팝업 화면 상태 const [popupScreen, setPopupScreen] = useState<{ screenId: number; @@ -186,6 +189,11 @@ export const InteractiveScreenViewerDynamic: React.FC { + console.log("🔍 테이블에서 선택된 행 데이터:", selectedData); + setSelectedRowsData(selectedData); + }} onRefresh={() => { console.log("🔄 버튼에서 테이블 새로고침 요청됨"); // 테이블 컴포넌트는 자체적으로 loadData 호출 diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index b42340fb..93580cb6 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -34,6 +34,14 @@ interface RealtimePreviewProps { onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 onConfigChange?: (config: any) => void; // 설정 변경 핸들러 + + // 버튼 액션을 위한 props + screenId?: number; + tableName?: string; + selectedRowsData?: any[]; + onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; + refreshKey?: number; + onRefresh?: () => void; } // 동적 위젯 타입 아이콘 (레지스트리에서 조회) @@ -77,6 +85,12 @@ export const RealtimePreviewDynamic: React.FC = ({ onZoneComponentDrop, onZoneClick, onConfigChange, + screenId, + tableName, + selectedRowsData, + onSelectedRowsChange, + refreshKey, + onRefresh, }) => { const { id, type, position, size, style: componentStyle } = component; @@ -178,6 +192,12 @@ export const RealtimePreviewDynamic: React.FC = ({ onZoneComponentDrop={onZoneComponentDrop} onZoneClick={onZoneClick} onConfigChange={onConfigChange} + screenId={screenId} + tableName={tableName} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={onSelectedRowsChange} + refreshKey={refreshKey} + onRefresh={onRefresh} /> diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index dac32163..a7949431 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -66,12 +66,18 @@ export const ButtonConfigPanel: React.FC = ({ component, // eslint-disable-next-line react-hooks/exhaustive-deps }, [component.id]); - // 화면 목록 가져오기 + // 화면 목록 가져오기 (전체 목록) useEffect(() => { const fetchScreens = async () => { try { setScreensLoading(true); - const response = await apiClient.get("/screen-management/screens"); + // 전체 목록을 가져오기 위해 size를 큰 값으로 설정 + const response = await apiClient.get("/screen-management/screens", { + params: { + page: 1, + size: 9999, // 매우 큰 값으로 설정하여 전체 목록 가져오기 + }, + }); if (response.data.success && Array.isArray(response.data.data)) { const screenList = response.data.data.map((screen: any) => ({ @@ -194,17 +200,11 @@ export const ButtonConfigPanel: React.FC = ({ component, 저장 - 취소 삭제 - 수정 - 추가 - 검색 - 초기화 - 제출 - 닫기 - 모달 열기 + 편집 페이지 이동 - 제어 (조건 체크만) + 모달 열기 + 제어 흐름 diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 3fa26bd1..e371f6bc 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -64,6 +64,15 @@ export const ButtonPrimaryComponent: React.FC = ({ selectedRowsData, ...props }) => { + console.log("🔵 ButtonPrimaryComponent 렌더링, 받은 props:", { + componentId: component.id, + hasSelectedRowsData: !!selectedRowsData, + selectedRowsDataLength: selectedRowsData?.length, + selectedRowsData, + tableName, + screenId, + }); + // 확인 다이얼로그 상태 const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [pendingAction, setPendingAction] = useState<{ @@ -204,7 +213,7 @@ export const ButtonPrimaryComponent: React.FC = ({ } // 확인 다이얼로그가 필요한 액션 타입들 - const confirmationRequiredActions: ButtonActionType[] = ["save", "submit", "delete"]; + const confirmationRequiredActions: ButtonActionType[] = ["save", "delete"]; // 실제 액션 실행 함수 const executeAction = async (actionConfig: any, context: ButtonActionContext) => { @@ -221,8 +230,9 @@ export const ButtonPrimaryComponent: React.FC = ({ // 추가 안전장치: 모든 로딩 토스트 제거 toast.dismiss(); - // edit 액션을 제외하고만 로딩 토스트 표시 - if (actionConfig.type !== "edit") { + // UI 전환 액션(edit, modal, navigate)을 제외하고만 로딩 토스트 표시 + const silentActions = ["edit", "modal", "navigate"]; + if (!silentActions.includes(actionConfig.type)) { console.log("📱 로딩 토스트 표시 시작"); currentLoadingToastRef.current = toast.loading( actionConfig.type === "save" @@ -237,9 +247,16 @@ export const ButtonPrimaryComponent: React.FC = ({ }, ); console.log("📱 로딩 토스트 ID:", currentLoadingToastRef.current); + } else { + console.log("🔕 UI 전환 액션은 로딩 토스트 표시 안함:", actionConfig.type); } console.log("⚡ ButtonActionExecutor.executeAction 호출 시작"); + console.log("🔍 actionConfig 확인:", { + type: actionConfig.type, + successMessage: actionConfig.successMessage, + errorMessage: actionConfig.errorMessage, + }); const success = await ButtonActionExecutor.executeAction(actionConfig, context); console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success); @@ -252,37 +269,70 @@ export const ButtonPrimaryComponent: React.FC = ({ // 실패한 경우 오류 처리 if (!success) { + // UI 전환 액션(edit, modal, navigate)은 에러도 조용히 처리 + const silentActions = ["edit", "modal", "navigate"]; + if (silentActions.includes(actionConfig.type)) { + console.log("🔕 UI 전환 액션 실패지만 에러 토스트 표시 안함:", actionConfig.type); + return; + } + console.log("❌ 액션 실패, 오류 토스트 표시"); - const errorMessage = - actionConfig.errorMessage || - (actionConfig.type === "save" + // 기본 에러 메시지 결정 + const defaultErrorMessage = + actionConfig.type === "save" ? "저장 중 오류가 발생했습니다." : actionConfig.type === "delete" ? "삭제 중 오류가 발생했습니다." : actionConfig.type === "submit" ? "제출 중 오류가 발생했습니다." - : "처리 중 오류가 발생했습니다."); + : "처리 중 오류가 발생했습니다."; + + // 커스텀 메시지 사용 조건: + // 1. 커스텀 메시지가 있고 + // 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우) + const useCustomMessage = + actionConfig.errorMessage && + (actionConfig.type === "save" || !actionConfig.errorMessage.includes("저장")); + + const errorMessage = useCustomMessage ? actionConfig.errorMessage : defaultErrorMessage; + + console.log("🔍 에러 메시지 결정:", { + actionType: actionConfig.type, + customMessage: actionConfig.errorMessage, + useCustom: useCustomMessage, + finalMessage: errorMessage + }); + toast.error(errorMessage); return; } // 성공한 경우에만 성공 토스트 표시 - // edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요) - if (actionConfig.type !== "edit") { - const successMessage = - actionConfig.successMessage || - (actionConfig.type === "save" + // edit, modal, navigate 액션은 조용히 처리 (UI 전환만 하므로 토스트 불필요) + if (actionConfig.type !== "edit" && actionConfig.type !== "modal" && actionConfig.type !== "navigate") { + // 기본 성공 메시지 결정 + const defaultSuccessMessage = + actionConfig.type === "save" ? "저장되었습니다." : actionConfig.type === "delete" ? "삭제되었습니다." : actionConfig.type === "submit" ? "제출되었습니다." - : "완료되었습니다."); + : "완료되었습니다."; + + // 커스텀 메시지 사용 조건: + // 1. 커스텀 메시지가 있고 + // 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우) + const useCustomMessage = + actionConfig.successMessage && + (actionConfig.type === "save" || !actionConfig.successMessage.includes("저장")); + + const successMessage = useCustomMessage ? actionConfig.successMessage : defaultSuccessMessage; console.log("🎉 성공 토스트 표시:", successMessage); toast.success(successMessage); } else { - console.log("🔕 edit 액션은 조용히 처리 (토스트 없음)"); + console.log("🔕 UI 전환 액션은 조용히 처리 (토스트 없음):", actionConfig.type); } console.log("✅ 버튼 액션 실행 성공:", actionConfig.type); @@ -357,6 +407,13 @@ export const ButtonPrimaryComponent: React.FC = ({ requiresConfirmation: confirmationRequiredActions.includes(processedConfig.action.type), }); + // 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단 + if (processedConfig.action.type === "delete" && (!selectedRowsData || selectedRowsData.length === 0)) { + console.log("⚠️ 삭제할 데이터가 선택되지 않았습니다."); + toast.warning("삭제할 항목을 먼저 선택해주세요."); + return; + } + const context: ButtonActionContext = { formData: formData || {}, originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가 @@ -370,6 +427,15 @@ export const ButtonPrimaryComponent: React.FC = ({ selectedRowsData, }; + console.log("🔍 버튼 액션 실행 전 context 확인:", { + hasSelectedRowsData: !!selectedRowsData, + selectedRowsDataLength: selectedRowsData?.length, + selectedRowsData, + tableName, + screenId, + formData, + }); + // 확인이 필요한 액션인지 확인 if (confirmationRequiredActions.includes(processedConfig.action.type)) { console.log("📋 확인 다이얼로그 표시 중..."); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index e978ce3b..abacc70b 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -11,18 +11,11 @@ import type { ExtendedControlContext } from "@/types/control-management"; */ export type ButtonActionType = | "save" // 저장 - | "cancel" // 취소 | "delete" // 삭제 | "edit" // 편집 - | "add" // 추가 - | "search" // 검색 - | "reset" // 초기화 - | "submit" // 제출 - | "close" // 닫기 - | "popup" // 팝업 열기 | "navigate" // 페이지 이동 | "modal" // 모달 열기 - | "newWindow"; // 새 창 열기 + | "control"; // 제어 흐름 /** * 버튼 액션 설정 @@ -92,42 +85,18 @@ export class ButtonActionExecutor { 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); - case "control": return this.handleControl(config, context); @@ -515,9 +484,9 @@ export class ButtonActionExecutor { }); window.dispatchEvent(modalEvent); - toast.success("모달 화면이 열렸습니다."); + // 모달 열기는 조용히 처리 (토스트 불필요) } else { - toast.error("모달로 열 화면이 지정되지 않았습니다."); + console.error("모달로 열 화면이 지정되지 않았습니다."); return false; } @@ -1421,26 +1390,12 @@ export const DEFAULT_BUTTON_ACTIONS: Record