diff --git a/backend-node/src/controllers/entitySearchController.ts b/backend-node/src/controllers/entitySearchController.ts index 2b944e2b..5046d8bb 100644 --- a/backend-node/src/controllers/entitySearchController.ts +++ b/backend-node/src/controllers/entitySearchController.ts @@ -17,6 +17,15 @@ export async function searchEntity(req: Request, res: Response) { limit = "20", } = req.query; + // tableName 유효성 검증 + if (!tableName || tableName === "undefined" || tableName === "null") { + logger.warn("엔티티 검색 실패: 테이블명이 없음", { tableName }); + return res.status(400).json({ + success: false, + message: "테이블명이 지정되지 않았습니다. 컴포넌트 설정에서 sourceTable을 확인해주세요.", + }); + } + // 멀티테넌시 const companyCode = req.user!.companyCode; diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 8c922da1..087444b7 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -16,6 +16,7 @@ import { ComponentData } from "@/types/screen"; import { toast } from "sonner"; import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; +import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; interface ScreenModalState { isOpen: boolean; @@ -55,11 +56,11 @@ export const ScreenModal: React.FC = ({ className }) => { // 폼 데이터 상태 추가 const [formData, setFormData] = useState>({}); - + // 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록) const continuousModeRef = useRef(false); const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음) - + // localStorage에서 연속 모드 상태 복원 useEffect(() => { const savedMode = localStorage.getItem("screenModal_continuousMode"); @@ -121,7 +122,7 @@ export const ScreenModal: React.FC = ({ className }) => { useEffect(() => { const handleOpenModal = (event: CustomEvent) => { const { screenId, title, description, size, urlParams } = event.detail; - + // 🆕 URL 파라미터가 있으면 현재 URL에 추가 if (urlParams && typeof window !== "undefined") { const currentUrl = new URL(window.location.href); @@ -132,7 +133,7 @@ export const ScreenModal: React.FC = ({ className }) => { window.history.pushState({}, "", currentUrl.toString()); console.log("✅ URL 파라미터 추가:", urlParams); } - + setModalState({ isOpen: true, screenId, @@ -151,7 +152,7 @@ export const ScreenModal: React.FC = ({ className }) => { window.history.pushState({}, "", currentUrl.toString()); console.log("🧹 URL 파라미터 제거"); } - + setModalState({ isOpen: false, screenId: null, @@ -172,14 +173,14 @@ export const ScreenModal: React.FC = ({ className }) => { // console.log("💾 저장 성공 이벤트 수신"); // console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode); // console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode")); - + if (isContinuousMode) { // 연속 모드: 폼만 초기화하고 모달은 유지 // console.log("✅ 연속 모드 활성화 - 폼만 초기화"); - + // 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨) setFormData({}); - + toast.success("저장되었습니다. 계속 입력하세요."); } else { // 일반 모드: 모달 닫기 @@ -226,7 +227,7 @@ export const ScreenModal: React.FC = ({ className }) => { // 화면 관리에서 설정한 해상도 사용 (우선순위) const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution; - + let dimensions; if (screenResolution && screenResolution.width && screenResolution.height) { // 화면 관리에서 설정한 해상도 사용 @@ -242,7 +243,7 @@ export const ScreenModal: React.FC = ({ className }) => { dimensions = calculateScreenDimensions(components); console.log("⚠️ 자동 계산된 크기 사용:", dimensions); } - + setScreenDimensions(dimensions); setScreenData({ @@ -302,17 +303,17 @@ export const ScreenModal: React.FC = ({ className }) => { }; const modalStyle = getModalStyle(); - + // 안정적인 modalId를 상태로 저장 (모달이 닫혀도 유지) const [persistedModalId, setPersistedModalId] = useState(undefined); - + // modalId 생성 및 업데이트 useEffect(() => { // 모달이 열려있고 screenId가 있을 때만 업데이트 if (!modalState.isOpen) return; - + let newModalId: string | undefined; - + // 1순위: screenId (가장 안정적) if (modalState.screenId) { newModalId = `screen-modal-${modalState.screenId}`; @@ -350,11 +351,17 @@ export const ScreenModal: React.FC = ({ className }) => { // result: newModalId, // }); } - + if (newModalId) { setPersistedModalId(newModalId); } - }, [modalState.isOpen, modalState.screenId, modalState.title, screenData?.screenInfo?.tableName, screenData?.screenInfo?.screenName]); + }, [ + modalState.isOpen, + modalState.screenId, + modalState.title, + screenData?.screenInfo?.tableName, + screenData?.screenInfo?.screenName, + ]); return ( @@ -397,7 +404,7 @@ export const ScreenModal: React.FC = ({ className }) => { ) : screenData ? (
= ({ className }) => { const offsetY = screenDimensions?.offsetY || 0; // offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시) - const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : { - ...component, - position: { - ...component.position, - x: parseFloat(component.position?.x?.toString() || "0") - offsetX, - y: parseFloat(component.position?.y?.toString() || "0") - offsetY, - }, - }; + const adjustedComponent = + offsetX === 0 && offsetY === 0 + ? component + : { + ...component, + position: { + ...component.position, + x: parseFloat(component.position?.x?.toString() || "0") - offsetX, + y: parseFloat(component.position?.y?.toString() || "0") - offsetY, + }, + }; return ( = ({ className }) => { // console.log("🔄 연속 모드 변경:", isChecked); }} /> -
diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index b488de72..93872289 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -272,19 +272,15 @@ export const RealtimePreviewDynamic: React.FC = ({ right: undefined, }; - // 디버깅: 크기 정보 로그 - if (component.id && isSelected) { - console.log("📐 RealtimePreview baseStyle:", { - componentId: component.id, - componentType: (component as any).componentType || component.type, - sizeWidth: size?.width, - sizeHeight: size?.height, - styleWidth: componentStyle?.width, - styleHeight: componentStyle?.height, - baseStyleWidth: baseStyle.width, - baseStyleHeight: baseStyle.height, - }); - } + // 크기 정보는 필요시에만 디버깅 (개발 중 문제 발생 시 주석 해제) + // if (component.id && isSelected) { + // console.log("📐 RealtimePreview baseStyle:", { + // componentId: component.id, + // componentType: (component as any).componentType || component.type, + // sizeWidth: size?.width, + // sizeHeight: size?.height, + // }); + // } // 🔍 DOM 렌더링 후 실제 크기 측정 const innerDivRef = React.useRef(null); diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index 6a1c4d7f..81e1b2a9 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -883,10 +883,12 @@ export const DetailSettingsPanel: React.FC = ({ // 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤 const ConfigPanelWrapper = () => { - const config = currentConfig.config || definition.defaultConfig || {}; + // Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장 + const config = currentConfig || definition.defaultProps?.componentConfig || {}; const handleConfigChange = (newConfig: any) => { - onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig); + // componentConfig 전체를 업데이트 + onUpdateProperty(selectedComponent.id, "componentConfig", newConfig); }; return ( @@ -895,7 +897,7 @@ export const DetailSettingsPanel: React.FC = ({

{definition.name} 설정

- + ); }; diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index f4942953..6c77b4f1 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -8,6 +8,7 @@ import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react"; import { @@ -266,7 +267,12 @@ export const UnifiedPropertiesPanel: React.FC = ({ const renderComponentConfigPanel = () => { if (!selectedComponent) return null; - const componentType = selectedComponent.componentConfig?.type || selectedComponent.type; + // 🎯 Section Card, Section Paper 등 신규 컴포넌트는 componentType에서 감지 + const componentType = + selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등) + selectedComponent.componentConfig?.type || + selectedComponent.componentConfig?.id || + selectedComponent.type; const handleUpdateProperty = (path: string, value: any) => { onUpdateProperty(selectedComponent.id, path, value); @@ -276,10 +282,15 @@ export const UnifiedPropertiesPanel: React.FC = ({ onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig); }; - // 🆕 ComponentRegistry에서 ConfigPanel 가져오기 - const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id; + // 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도 + const componentId = + selectedComponent.componentType || // ⭐ section-card 등 + selectedComponent.componentConfig?.type || + selectedComponent.componentConfig?.id; + if (componentId) { const definition = ComponentRegistry.getComponent(componentId); + if (definition?.configPanel) { const ConfigPanelComponent = definition.configPanel; const currentConfig = selectedComponent.componentConfig || {}; @@ -293,10 +304,12 @@ export const UnifiedPropertiesPanel: React.FC = ({ // 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤 const ConfigPanelWrapper = () => { - const config = currentConfig.config || definition.defaultConfig || {}; + // Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장 + const config = currentConfig || definition.defaultProps?.componentConfig || {}; const handleConfigChange = (newConfig: any) => { - onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig); + // componentConfig 전체를 업데이트 + onUpdateProperty(selectedComponent.id, "componentConfig", newConfig); }; return ( @@ -305,18 +318,19 @@ export const UnifiedPropertiesPanel: React.FC = ({

{definition.name} 설정

- + ); }; return ; } else { - console.warn("⚠️ ConfigPanel 없음:", { + console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", { componentId, definitionName: definition?.name, hasDefinition: !!definition, }); + // ConfigPanel이 없으면 아래 switch case로 넘어감 } } @@ -363,6 +377,280 @@ export const UnifiedPropertiesPanel: React.FC = ({ case "badge-status": return ; + case "section-card": + return ( +
+
+

Section Card 설정

+

+ 제목과 테두리가 있는 명확한 그룹화 컨테이너 +

+
+ + {/* 헤더 표시 */} +
+ { + handleUpdateProperty(selectedComponent.id, "componentConfig.showHeader", checked); + }} + /> + +
+ + {/* 제목 */} + {selectedComponent.componentConfig?.showHeader !== false && ( +
+ + { + handleUpdateProperty(selectedComponent.id, "componentConfig.title", e.target.value); + }} + placeholder="섹션 제목 입력" + className="h-9 text-xs" + /> +
+ )} + + {/* 설명 */} + {selectedComponent.componentConfig?.showHeader !== false && ( +
+ +