diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 7c15afff..a2c0e05e 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -21,9 +21,15 @@ export default function ScreenViewPage() { const [layout, setLayout] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // 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,24 @@ 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([]); // 선택 해제 + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + console.log("📝 폼 데이터 변경:", fieldName, "=", value); + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} > {/* 자식 컴포넌트들 */} {(component.type === "group" || component.type === "container" || component.type === "area") && @@ -195,6 +219,24 @@ 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([]); // 선택 해제 + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value); + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} /> ); })} diff --git a/frontend/components/admin/MenuFormModal.tsx b/frontend/components/admin/MenuFormModal.tsx index ad9edfc4..e0230c8a 100644 --- a/frontend/components/admin/MenuFormModal.tsx +++ b/frontend/components/admin/MenuFormModal.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react"; import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu"; import { companyAPI } from "@/lib/api/company"; -import { screenApi } from "@/lib/api/screen"; +import { screenApi, menuScreenApi } from "@/lib/api/screen"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -598,6 +598,48 @@ export const MenuFormModal: React.FC = ({ } if (response.success) { + // 화면 할당이 있는 경우 추가 처리 + if (urlType === "screen" && selectedScreen) { + try { + // menuId는 response에서 반환되거나 기존 menuId 사용 + const targetMenuId = menuId || response.data?.objid; + const menuObjid = parseInt(targetMenuId?.toString() || "0"); + + if (menuObjid > 0) { + console.log("📋 화면-메뉴 관계 테이블 업데이트 시작:", { + screenId: selectedScreen.screenId, + menuObjid, + }); + + // 1. 기존 할당된 화면들 먼저 조회 + try { + const existingScreens = await menuScreenApi.getScreensByMenu(menuObjid); + console.log("📋 기존 할당된 화면:", existingScreens.length, "개"); + + // 2. 기존 화면들 모두 제거 + for (const existingScreen of existingScreens) { + try { + await menuScreenApi.unassignScreenFromMenu(existingScreen.screenId, menuObjid); + console.log(`✅ 기존 화면 제거 완료: ${existingScreen.screenName}`); + } catch (unassignError) { + console.warn(`⚠️ 기존 화면 제거 실패: ${existingScreen.screenName}`, unassignError); + } + } + } catch (getError) { + console.warn("⚠️ 기존 화면 조회 실패 (계속 진행):", getError); + } + + // 3. 새 화면 할당 + await menuScreenApi.assignScreenToMenu(selectedScreen.screenId, menuObjid); + console.log("✅ 새 화면 할당 완료"); + } + } catch (assignError) { + console.error("❌ 화면-메뉴 관계 테이블 할당 실패:", assignError); + // 할당 실패는 경고만 하고 메뉴 저장은 성공으로 처리 + toast.warning("메뉴는 저장되었으나 화면 할당에 실패했습니다."); + } + } + toast.success(response.message); onSuccess(); onClose(); 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/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index e7e95578..e0d6f978 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -222,6 +222,66 @@ export const RealtimePreviewDynamic: React.FC = ({ const { user } = useAuth(); const { type, id, position, size, style = {} } = component; const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0); + const [actualHeight, setActualHeight] = useState(null); + const contentRef = React.useRef(null); + + // 플로우 위젯의 실제 높이 측정 + useEffect(() => { + const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget"); + + if (isFlowWidget && contentRef.current) { + const measureHeight = () => { + if (contentRef.current) { + // getBoundingClientRect()로 실제 렌더링된 높이 측정 + const rect = contentRef.current.getBoundingClientRect(); + const measured = rect.height; + + // scrollHeight도 함께 확인하여 더 큰 값 사용 + const scrollHeight = contentRef.current.scrollHeight; + const rawHeight = Math.max(measured, scrollHeight); + + // 40px 단위로 올림 + const finalHeight = Math.ceil(rawHeight / 40) * 40; + + if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) { + setActualHeight(finalHeight); + } + } + }; + + // 초기 측정 (렌더링 완료 후) + const initialTimer = setTimeout(() => { + measureHeight(); + }, 100); + + // 추가 측정 (데이터 로딩 완료 대기) + const delayedTimer = setTimeout(() => { + measureHeight(); + }, 500); + + // 스텝 클릭 등으로 높이가 변경될 때를 위한 추가 측정 + const extendedTimer = setTimeout(() => { + measureHeight(); + }, 1000); + + // ResizeObserver로 크기 변화 감지 (스텝 클릭 시 데이터 테이블 펼쳐짐) + const resizeObserver = new ResizeObserver(() => { + // 약간의 지연을 두고 측정 (DOM 업데이트 완료 대기) + setTimeout(() => { + measureHeight(); + }, 100); + }); + + resizeObserver.observe(contentRef.current); + + return () => { + clearTimeout(initialTimer); + clearTimeout(delayedTimer); + clearTimeout(extendedTimer); + resizeObserver.disconnect(); + }; + } + }, [type, id]); // 전역 파일 상태 변경 감지 (해당 컴포넌트만) useEffect(() => { @@ -314,12 +374,20 @@ export const RealtimePreviewDynamic: React.FC = ({ }, [component.id, fileUpdateTrigger]); // 컴포넌트 스타일 계산 + const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget"); + + // 높이 결정 로직 + let finalHeight = size?.height || 40; + if (isFlowWidget && actualHeight) { + finalHeight = actualHeight; + } + const componentStyle = { position: "absolute" as const, left: position?.x || 0, top: position?.y || 0, width: size?.width || 200, - height: size?.height || 40, + height: finalHeight, zIndex: position?.z || 1, ...style, }; @@ -358,7 +426,10 @@ export const RealtimePreviewDynamic: React.FC = ({ onDragEnd={handleDragEnd} > {/* 컴포넌트 타입별 렌더링 */} -
+
{/* 영역 타입 */} {type === "area" && renderArea(component, children)} @@ -422,7 +493,7 @@ export const RealtimePreviewDynamic: React.FC = ({ console.log("🔍 RealtimePreview 최종 flowComponent:", flowComponent); return ( -
+
); diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index b42340fb..67e802f0 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -34,6 +34,18 @@ 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; + + // 폼 데이터 관련 props + formData?: Record; + onFormDataChange?: (fieldName: string, value: any) => void; } // 동적 위젯 타입 아이콘 (레지스트리에서 조회) @@ -77,14 +89,101 @@ export const RealtimePreviewDynamic: React.FC = ({ onZoneComponentDrop, onZoneClick, onConfigChange, + screenId, + tableName, + selectedRowsData, + onSelectedRowsChange, + refreshKey, + onRefresh, + formData, + onFormDataChange, }) => { + const [actualHeight, setActualHeight] = React.useState(null); + const contentRef = React.useRef(null); + const lastUpdatedHeight = React.useRef(null); + + // 플로우 위젯의 실제 높이 측정 + React.useEffect(() => { + const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget"; + + if (isFlowWidget && contentRef.current) { + const measureHeight = () => { + if (contentRef.current) { + // getBoundingClientRect()로 실제 렌더링된 높이 측정 + const rect = contentRef.current.getBoundingClientRect(); + const measured = rect.height; + + // scrollHeight도 함께 확인하여 더 큰 값 사용 + const scrollHeight = contentRef.current.scrollHeight; + const rawHeight = Math.max(measured, scrollHeight); + + // 40px 단위로 올림 + const finalHeight = Math.ceil(rawHeight / 40) * 40; + + if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) { + setActualHeight(finalHeight); + + // 컴포넌트의 실제 size.height도 업데이트 (중복 업데이트 방지) + if (onConfigChange && finalHeight !== lastUpdatedHeight.current && finalHeight !== component.size?.height) { + lastUpdatedHeight.current = finalHeight; + console.log("🔄 플로우 위젯 높이 업데이트 이벤트 발송:", { + componentId: component.id, + oldHeight: component.size?.height, + newHeight: finalHeight, + }); + // size는 별도 속성이므로 직접 업데이트 + const event = new CustomEvent('updateComponentSize', { + detail: { + componentId: component.id, + height: finalHeight + } + }); + window.dispatchEvent(event); + } + } + } + }; + + // 초기 측정 (렌더링 완료 후) + const initialTimer = setTimeout(() => { + measureHeight(); + }, 100); + + // 추가 측정 (데이터 로딩 완료 대기) + const delayedTimer = setTimeout(() => { + measureHeight(); + }, 500); + + // 스텝 클릭 등으로 높이가 변경될 때를 위한 추가 측정 + const extendedTimer = setTimeout(() => { + measureHeight(); + }, 1000); + + // ResizeObserver로 크기 변화 감지 (스텝 클릭 시 데이터 테이블 펼쳐짐) + const resizeObserver = new ResizeObserver(() => { + // 약간의 지연을 두고 측정 (DOM 업데이트 완료 대기) + setTimeout(() => { + measureHeight(); + }, 100); + }); + + resizeObserver.observe(contentRef.current); + + return () => { + clearTimeout(initialTimer); + clearTimeout(delayedTimer); + clearTimeout(extendedTimer); + resizeObserver.disconnect(); + }; + } + }, [component.type, component.id, actualHeight, component.size?.height, onConfigChange]); const { id, type, position, size, style: componentStyle } = component; // 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래) const selectionStyle = isSelected ? { outline: "2px solid rgb(59, 130, 246)", - outlineOffset: "2px", + outlineOffset: "0px", // 스크롤 방지를 위해 0으로 설정 zIndex: 20, } : {}; @@ -106,6 +205,12 @@ export const RealtimePreviewDynamic: React.FC = ({ }; const getHeight = () => { + // 플로우 위젯의 경우 측정된 높이 사용 + const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget"; + if (isFlowWidget && actualHeight) { + return `${actualHeight}px`; + } + // 1순위: style.height가 있으면 우선 사용 if (componentStyle?.height) { return componentStyle.height; @@ -161,7 +266,8 @@ export const RealtimePreviewDynamic: React.FC = ({ > {/* 동적 컴포넌트 렌더링 */}
@@ -178,6 +284,14 @@ export const RealtimePreviewDynamic: React.FC = ({ onZoneComponentDrop={onZoneComponentDrop} onZoneClick={onZoneClick} onConfigChange={onConfigChange} + screenId={screenId} + tableName={tableName} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={onSelectedRowsChange} + refreshKey={refreshKey} + onRefresh={onRefresh} + formData={formData} + onFormDataChange={onFormDataChange} />
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index ba8931f5..7fc457d5 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -2906,9 +2906,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const relativeMouseX = event.clientX - rect.left; const relativeMouseY = event.clientY - rect.top; + // 컴포넌트 크기 가져오기 + const draggedComp = layout.components.find((c) => c.id === dragState.draggedComponent.id); + const componentWidth = draggedComp?.size?.width || 100; + const componentHeight = draggedComp?.size?.height || 40; + + // 경계 제한 적용 + const rawX = relativeMouseX - dragState.grabOffset.x; + const rawY = relativeMouseY - dragState.grabOffset.y; + const newPosition = { - x: relativeMouseX - dragState.grabOffset.x, - y: relativeMouseY - dragState.grabOffset.y, + x: Math.max(0, Math.min(rawX, screenResolution.width - componentWidth)), + y: Math.max(0, Math.min(rawY, screenResolution.height - componentHeight)), z: (dragState.draggedComponent.position as Position).z || 1, }; @@ -3002,6 +3011,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD z: originalComponent.position.z || 1, }; + // 캔버스 경계 제한 (컴포넌트가 화면 밖으로 나가지 않도록) + const componentWidth = comp.size?.width || 100; + const componentHeight = comp.size?.height || 40; + + // 최소 위치: 0, 최대 위치: 캔버스 크기 - 컴포넌트 크기 + newPosition.x = Math.max(0, Math.min(newPosition.x, screenResolution.width - componentWidth)); + newPosition.y = Math.max(0, Math.min(newPosition.y, screenResolution.height - componentHeight)); + // 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용 if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) { const { columnWidth } = gridInfo; @@ -3895,6 +3912,73 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD selectedScreen, ]); + // 플로우 위젯 높이 자동 업데이트 이벤트 리스너 + useEffect(() => { + const handleComponentSizeUpdate = (event: CustomEvent) => { + const { componentId, height } = event.detail; + + console.log("📥 ScreenDesigner에서 높이 업데이트 이벤트 수신:", { + componentId, + height, + }); + + // 해당 컴포넌트 찾기 + const targetComponent = layout.components.find((c) => c.id === componentId); + if (!targetComponent) { + console.log("⚠️ 컴포넌트를 찾을 수 없음:", componentId); + return; + } + + // 이미 같은 높이면 업데이트 안함 + if (targetComponent.size?.height === height) { + console.log("ℹ️ 이미 같은 높이:", height); + return; + } + + console.log("✅ 컴포넌트 높이 업데이트 중:", { + componentId, + oldHeight: targetComponent.size?.height, + newHeight: height, + }); + + // 컴포넌트 높이 업데이트 + const updatedComponents = layout.components.map((comp) => { + if (comp.id === componentId) { + return { + ...comp, + size: { + ...comp.size, + width: comp.size?.width || 100, + height: height, + }, + }; + } + return comp; + }); + + const newLayout = { + ...layout, + components: updatedComponents, + }; + + setLayout(newLayout); + + // 선택된 컴포넌트도 업데이트 + if (selectedComponent?.id === componentId) { + const updatedComponent = updatedComponents.find((c) => c.id === componentId); + if (updatedComponent) { + setSelectedComponent(updatedComponent); + console.log("✅ 선택된 컴포넌트도 업데이트됨"); + } + } + }; + + window.addEventListener("updateComponentSize", handleComponentSizeUpdate as EventListener); + return () => { + window.removeEventListener("updateComponentSize", handleComponentSizeUpdate as EventListener); + }; + }, [layout, selectedComponent]); + if (!selectedScreen) { return (
@@ -4007,20 +4091,23 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD minHeight: Math.max(screenResolution.height, 800) * zoomLevel, }} > - {/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 + 줌 적용 */} + {/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */}
{ if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) { setSelectedComponent(null); 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/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index 57964650..0d6c3fda 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -9,7 +9,6 @@ import { Separator } from "@/components/ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react"; import { ComponentData, @@ -96,13 +95,35 @@ export const UnifiedPropertiesPanel: React.FC = ({ } }, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]); - // 컴포넌트가 선택되지 않았을 때 + // 컴포넌트가 선택되지 않았을 때도 해상도 설정은 표시 if (!selectedComponent) { return ( -
- -

컴포넌트를 선택하여

-

속성을 편집하세요

+
+ {/* 해상도 설정만 표시 */} +
+
+ {currentResolution && onResolutionChange && ( +
+
+ +

해상도 설정

+
+ +
+ )} + + {/* 안내 메시지 */} + +
+ +

컴포넌트를 선택하여

+

속성을 편집하세요

+
+
+
); } @@ -340,26 +361,12 @@ export const UnifiedPropertiesPanel: React.FC = ({ {/* 옵션 */} -
-
- handleUpdate("visible", checked)} - /> - -
-
- handleUpdate("disabled", checked)} - /> - -
+
{widget.required !== undefined && (
handleUpdate("required", checked)} + checked={widget.required === true || selectedComponent.componentConfig?.required === true} + onCheckedChange={(checked) => handleUpdate("componentConfig.required", checked)} />
@@ -367,8 +374,8 @@ export const UnifiedPropertiesPanel: React.FC = ({ {widget.readonly !== undefined && (
handleUpdate("readonly", checked)} + checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true} + onCheckedChange={(checked) => handleUpdate("componentConfig.readonly", checked)} />
@@ -524,8 +531,10 @@ export const UnifiedPropertiesPanel: React.FC = ({ tables={tables} onChange={(newConfig) => { console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig); - // 전체 componentConfig를 업데이트 - handleUpdate("componentConfig", newConfig); + // 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지 + Object.entries(newConfig).forEach(([key, value]) => { + handleUpdate(`componentConfig.${key}`, value); + }); }} />
@@ -603,47 +612,39 @@ export const UnifiedPropertiesPanel: React.FC = ({ )}
- {/* 탭 컨텐츠 */} - - - - 편집 - - - - 스타일 & 해상도 - - - - {/* 속성 탭 */} - -
- {/* 기본 설정 */} - {renderBasicTab()} - - {/* 상세 설정 통합 */} - - {renderDetailTab()} -
-
- - {/* 스타일 & 해상도 탭 */} - -
- {/* 해상도 설정 */} - {currentResolution && onResolutionChange && ( -
+ {/* 통합 컨텐츠 (탭 제거) */} +
+
+ {/* 해상도 설정 - 항상 맨 위에 표시 */} + {currentResolution && onResolutionChange && ( + <> +
+
+ +

해상도 설정

+
- )} + + + )} - {/* 스타일 설정 */} - {selectedComponent ? ( -
-
+ {/* 기본 설정 */} + {renderBasicTab()} + + {/* 상세 설정 */} + + {renderDetailTab()} + + {/* 스타일 설정 */} + {selectedComponent && ( + <> + +
+

컴포넌트 스타일

@@ -658,14 +659,10 @@ export const UnifiedPropertiesPanel: React.FC = ({ }} />
- ) : ( -
- 컴포넌트를 선택하여 스타일을 편집하세요 -
- )} -
- - + + )} +
+
); }; diff --git a/frontend/components/screen/panels/webtype-configs/CodeTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/CodeTypeConfigPanel.tsx index cbab910a..2f735751 100644 --- a/frontend/components/screen/panels/webtype-configs/CodeTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/CodeTypeConfigPanel.tsx @@ -174,52 +174,55 @@ export const CodeTypeConfigPanel: React.FC = ({ config
- {/* 라인 넘버 표시 */} -
- - updateConfig("lineNumbers", !!checked)} - /> -
+ {/* 옵션 - 가로 배치 */} +
+ {/* 라인 넘버 표시 */} +
+ + updateConfig("lineNumbers", !!checked)} + /> +
- {/* 단어 줄바꿈 */} -
- - updateConfig("wordWrap", !!checked)} - /> -
+ {/* 단어 줄바꿈 */} +
+ + updateConfig("wordWrap", !!checked)} + /> +
- {/* 읽기 전용 */} -
- - updateConfig("readOnly", !!checked)} - /> -
+ {/* 읽기 전용 */} +
+ + updateConfig("readOnly", !!checked)} + /> +
- {/* 자동 포맷팅 */} -
- - updateConfig("autoFormat", !!checked)} - /> + {/* 자동 포맷팅 */} +
+ + updateConfig("autoFormat", !!checked)} + /> +
{/* 플레이스홀더 */} diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index f941a908..adc07129 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -240,6 +240,19 @@ export const DynamicComponentRenderer: React.FC = // component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리) const { height: _height, ...styleWithoutHeight } = component.style || {}; + // 숨김 값 추출 (디버깅) + const hiddenValue = component.hidden || component.componentConfig?.hidden; + if (hiddenValue) { + console.log("🔍 DynamicComponentRenderer hidden 체크:", { + componentId: component.id, + componentType, + componentHidden: component.hidden, + componentConfigHidden: component.componentConfig?.hidden, + finalHiddenValue: hiddenValue, + isDesignMode: props.isDesignMode, + }); + } + const rendererProps = { component, isSelected, @@ -253,8 +266,8 @@ export const DynamicComponentRenderer: React.FC = componentConfig: component.componentConfig, value: currentValue, // formData에서 추출한 현재 값 전달 // 새로운 기능들 전달 - autoGeneration: component.autoGeneration, - hidden: component.hidden, + autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration, + hidden: hiddenValue, // React 전용 props들은 직접 전달 (DOM에 전달되지 않음) isInteractive, formData, 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/registry/components/text-display/TextDisplayComponent.tsx b/frontend/lib/registry/components/text-display/TextDisplayComponent.tsx index dc4f15da..a1e96a78 100644 --- a/frontend/lib/registry/components/text-display/TextDisplayComponent.tsx +++ b/frontend/lib/registry/components/text-display/TextDisplayComponent.tsx @@ -3,6 +3,7 @@ import React from "react"; import { ComponentRendererProps } from "../../types"; import { TextDisplayConfig } from "./types"; +import { filterDOMProps } from "@/lib/utils/domPropsFilter"; export interface TextDisplayComponentProps extends ComponentRendererProps { // 추가 props가 필요한 경우 여기에 정의 @@ -53,20 +54,7 @@ export const TextDisplayComponent: React.FC = ({ .join(" "); // DOM props 필터링 (React 관련 props 제거) - const { - component: _component, - isDesignMode: _isDesignMode, - isSelected: _isSelected, - isInteractive: _isInteractive, - screenId: _screenId, - tableName: _tableName, - onRefresh: _onRefresh, - onClose: _onClose, - formData: _formData, - onFormDataChange: _onFormDataChange, - componentConfig: _componentConfig, - ...domProps - } = props; + const domProps = filterDOMProps(props); // 텍스트 스타일 계산 const textStyle: React.CSSProperties = { diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 185110d8..7c963f6e 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -51,6 +51,19 @@ export const TextInputComponent: React.FC = ({ // 숨김 상태 (props에서 전달받은 값 우선 사용) const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false; + // 디버깅: 컴포넌트 설정 확인 + console.log("👻 텍스트 입력 컴포넌트 상태:", { + componentId: component.id, + label: component.label, + isHidden, + componentConfig: componentConfig, + readonly: componentConfig.readonly, + disabled: componentConfig.disabled, + required: componentConfig.required, + isDesignMode, + willRender: !(isHidden && !isDesignMode), + }); + // 자동생성된 값 상태 const [autoGeneratedValue, setAutoGeneratedValue] = useState(""); @@ -134,18 +147,22 @@ export const TextInputComponent: React.FC = ({ } }, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]); + // 실제 화면에서 숨김 처리된 컴포넌트는 렌더링하지 않음 + if (isHidden && !isDesignMode) { + return null; + } + // 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외) const componentStyle: React.CSSProperties = { width: "100%", height: "100%", ...component.style, ...style, - // 숨김 기능: 디자인 모드에서는 연하게, 실제 화면에서는 완전히 숨김 - ...(isHidden && { - opacity: isDesignMode ? 0.4 : 0, - backgroundColor: isDesignMode ? "#f3f4f6" : "transparent", - pointerEvents: isDesignMode ? "auto" : "none", - display: isDesignMode ? "block" : "none", + // 숨김 기능: 편집 모드에서만 연하게 표시 + ...(isHidden && isDesignMode && { + opacity: 0.4, + backgroundColor: "#f3f4f6", + pointerEvents: "auto", }), }; @@ -315,7 +332,7 @@ export const TextInputComponent: React.FC = ({ // 이메일 타입 전용 UI if (webType === "email") { return ( -
+
{/* 라벨 렌더링 */} {component.label && component.style?.labelDisplay !== false && (