From 32139beebc8be23ebdc7bcc08b15e50abf4082d6 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 4 Feb 2026 18:01:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Docker=20=EB=B0=8F=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Docker Compose 설정에서 Node.js 메모리 제한을 8192MB로 증가시키고, Next.js telemetry를 비활성화하여 성능을 개선하였습니다. - Next.js 구성에서 메모리 사용량 최적화를 위한 webpackMemoryOptimizations를 활성화하였습니다. - ScreenModal 컴포넌트에서 overflow 속성을 조정하여 라벨이 잘리지 않도록 개선하였습니다. - InteractiveScreenViewerDynamic 컴포넌트에서 라벨 표시 여부를 확인하는 로직을 추가하여 사용자 경험을 향상시켰습니다. - RealtimePreviewDynamic 컴포넌트에서 라벨 표시 및 디버깅 로그를 추가하여 렌더링 과정을 추적할 수 있도록 하였습니다. - ImprovedButtonControlConfigPanel에서 controlMode 설정을 추가하여 플로우 제어 기능을 개선하였습니다. - V2PropertiesPanel에서 라벨 텍스트 및 표시 상태 업데이트 로직을 개선하여 일관성을 높였습니다. - DynamicComponentRenderer에서 라벨 표시 로직을 개선하여 사용자 정의 스타일을 보다 효과적으로 적용할 수 있도록 하였습니다. - layoutV2Converter에서 webTypeConfig를 병합하여 버튼 제어 기능과 플로우 가시성을 보존하였습니다. --- docker/dev/docker-compose.frontend.mac.yml | 3 +- frontend/components/common/ScreenModal.tsx | 4 +- .../screen/InteractiveScreenViewerDynamic.tsx | 19 ++- .../components/screen/RealtimePreview.tsx | 16 +- .../screen/RealtimePreviewDynamic.tsx | 7 +- frontend/components/screen/ScreenDesigner.tsx | 146 +++++++----------- .../ImprovedButtonControlConfigPanel.tsx | 2 + .../screen/panels/V2PropertiesPanel.tsx | 21 ++- frontend/components/v2/V2Input.tsx | 6 +- .../lib/registry/DynamicComponentRenderer.tsx | 17 +- .../ButtonPrimaryComponent.tsx | 19 +-- .../components/v2-input/V2InputRenderer.tsx | 6 +- .../lib/utils/improvedButtonActionExecutor.ts | 118 ++++++++++++-- frontend/lib/utils/layoutV2Converter.ts | 38 ++++- frontend/next.config.mjs | 3 +- 15 files changed, 295 insertions(+), 130 deletions(-) diff --git a/docker/dev/docker-compose.frontend.mac.yml b/docker/dev/docker-compose.frontend.mac.yml index 23bcb654..6428d481 100644 --- a/docker/dev/docker-compose.frontend.mac.yml +++ b/docker/dev/docker-compose.frontend.mac.yml @@ -9,7 +9,8 @@ services: - "9771:3000" environment: - NEXT_PUBLIC_API_URL=http://localhost:8080/api - - NODE_OPTIONS=--max-old-space-size=4096 + - NODE_OPTIONS=--max-old-space-size=8192 + - NEXT_TELEMETRY_DISABLED=1 volumes: - ../../frontend:/app - /app/node_modules diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 62685175..38aebadc 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -603,7 +603,7 @@ export const ScreenModal: React.FC = ({ className }) => {
{loading ? (
@@ -620,6 +620,8 @@ export const ScreenModal: React.FC = ({ className }) => { style={{ width: `${screenDimensions?.width || 800}px`, height: `${screenDimensions?.height || 600}px`, + // 🆕 라벨이 위로 튀어나갈 수 있도록 overflow visible 설정 + overflow: "visible", }} > {(() => { diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 8dc5da89..d8e26377 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -1062,22 +1062,35 @@ export const InteractiveScreenViewerDynamic: React.FC 0 ? "visible" : undefined, }; return ( <>
- {/* 라벨 숨김 - 모달에서는 라벨을 표시하지 않음 */} - {/* 위젯 렌더링 */} + {/* 위젯 렌더링 (라벨은 V2Input 내부에서 absolute로 표시됨) */} {renderInteractiveWidget(component)}
diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index b58a6a1f..5a786616 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -119,6 +119,9 @@ const WidgetRenderer: React.FC<{ tableDisplayData?: any[]; [key: string]: any; }> = ({ component, isDesignMode = false, sortBy, sortOrder, tableDisplayData, ...restProps }) => { + // 🔧 무조건 로그 (렌더링 확인용) + console.log("📦 WidgetRenderer 렌더링:", component.id, "labelDisplay:", component.style?.labelDisplay); + // 위젯 컴포넌트가 아닌 경우 빈 div 반환 if (!isWidgetComponent(component)) { return
위젯이 아닙니다
; @@ -127,9 +130,6 @@ const WidgetRenderer: React.FC<{ const widget = component; const { widgetType, label, placeholder, required, readonly, columnName, style } = widget; - // 디버깅: 실제 widgetType 값 확인 - // console.log("RealtimePreviewDynamic - widgetType:", widgetType, "columnName:", columnName); - // 사용자가 테두리를 설정했는지 확인 const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border); @@ -246,8 +246,17 @@ export const RealtimePreviewDynamic: React.FC = ({ tableDisplayData, // 🆕 화면 표시 데이터 ...restProps }) => { + // 🔧 무조건 로그 - 파일 반영 테스트용 (2024-TEST) + console.log("🔷🔷🔷 RealtimePreview 2024:", component.id); + const { user } = useAuth(); const { type, id, position, size, style = {} } = component; + + // 🔧 v2 컴포넌트 렌더링 추적 + if (id?.includes("v2-")) { + console.log("🔷 RealtimePreview 렌더:", id, "type:", type, "labelDisplay:", style?.labelDisplay); + } + const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0); const [actualHeight, setActualHeight] = useState(null); const contentRef = React.useRef(null); @@ -741,6 +750,7 @@ export const RealtimePreviewDynamic: React.FC = ({ {/* 컴포넌트 타입 - 레지스트리 기반 렌더링 (Section Paper, Section Card 등) */} {type === "component" && (() => { + console.log("📦 DynamicComponentRenderer 렌더링:", component.id, "labelDisplay:", component.style?.labelDisplay); const { DynamicComponentRenderer } = require("@/lib/registry/DynamicComponentRenderer"); return ( = ({ />
- {/* 선택된 컴포넌트 정보 표시 */} + {/* 선택된 컴포넌트 정보 표시 - 🔧 오른쪽으로 이동 (라벨과 겹치지 않도록) */} {isSelected && ( -
+
{type === "widget" && (
{getWidgetIcon((component as WidgetComponent).widgetType)} @@ -685,7 +685,8 @@ const RealtimePreviewDynamicComponent: React.FC = ({ ); }; -// React.memo로 래핑하여 불필요한 리렌더링 방지 +// 🔧 arePropsEqual 제거 - 기본 React.memo 사용 (디버깅용) +// component 객체가 새로 생성되면 자동으로 리렌더링됨 export const RealtimePreviewDynamic = React.memo(RealtimePreviewDynamicComponent); // displayName 설정 (디버깅용) diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 192bd16c..389e8366 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -472,14 +472,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 이미 배치된 컬럼 목록 계산 const placedColumns = useMemo(() => { const placed = new Set(); + // 🔧 화면의 메인 테이블명을 fallback으로 사용 + const screenTableName = selectedScreen?.tableName; const collectColumns = (components: ComponentData[]) => { components.forEach((comp) => { const anyComp = comp as any; - // widget 타입 또는 component 타입 (새로운 시스템)에서 tableName과 columnName 확인 - if ((comp.type === "widget" || comp.type === "component") && anyComp.tableName && anyComp.columnName) { - const key = `${anyComp.tableName}.${anyComp.columnName}`; + // 🔧 tableName과 columnName을 여러 위치에서 찾기 (최상위, componentConfig, 또는 화면 테이블명) + const tableName = anyComp.tableName || anyComp.componentConfig?.tableName || screenTableName; + const columnName = anyComp.columnName || anyComp.componentConfig?.columnName; + + // widget 타입 또는 component 타입에서 columnName 확인 (tableName은 화면 테이블명으로 fallback) + if ((comp.type === "widget" || comp.type === "component") && tableName && columnName) { + const key = `${tableName}.${columnName}`; placed.add(key); } @@ -492,7 +498,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU collectColumns(layout.components); return placed; - }, [layout.components]); + }, [layout.components, selectedScreen?.tableName]); // 히스토리에 저장 const saveToHistory = useCallback( @@ -770,6 +776,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const finalKey = pathParts[pathParts.length - 1]; current[finalKey] = value; + // 🔧 style 관련 업데이트 디버그 로그 + if (path.includes("style") || path.includes("labelDisplay")) { + console.log("🎨 style 업데이트 제대로 렌더링된거다 내가바꿈:", { + componentId: comp.id, + path, + value, + updatedStyle: newComp.style, + pathIncludesLabelDisplay: path.includes("labelDisplay"), + }); + } + + // 🆕 labelDisplay 변경 시 강제 리렌더링 트리거 (조건문 밖으로 이동) + if (path === "style.labelDisplay") { + console.log("⏰⏰⏰ labelDisplay 변경 감지! forceRenderTrigger 실행 예정"); + } + // 🆕 size 변경 시 style도 함께 업데이트 (파란 테두리와 실제 크기 동기화) if (path === "size.width" || path === "size.height" || path === "size") { // 🔧 style 객체를 새로 복사하여 불변성 유지 @@ -1787,97 +1809,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const buttonComponents = layoutWithResolution.components.filter( (c: any) => c.componentType?.startsWith("button") || c.type === "button" || c.type === "button-primary", ); - console.log("💾 저장 시작:", { - screenId: selectedScreen.screenId, - componentsCount: layoutWithResolution.components.length, - gridSettings: layoutWithResolution.gridSettings, - screenResolution: layoutWithResolution.screenResolution, - buttonComponents: buttonComponents.map((c: any) => ({ - id: c.id, - type: c.type, - componentType: c.componentType, - text: c.componentConfig?.text, - actionType: c.componentConfig?.action?.type, - fullAction: c.componentConfig?.action, - })), - }); - - // 🔍 디버그: 분할 패널 내부의 탭 및 컴포넌트 설정 확인 - const splitPanels = layoutWithResolution.components.filter( - (c: any) => c.componentType === "v2-split-panel-layout" || c.componentType === "split-panel-layout" - ); - splitPanels.forEach((sp: any) => { - console.log("🔍 [저장] 분할 패널 설정:", { - id: sp.id, - leftPanel: sp.componentConfig?.leftPanel, - rightPanel: sp.componentConfig?.rightPanel, - }); - // 🆕 분할 패널 내 모든 컴포넌트의 componentConfig 로그 - const rightComponents = sp.componentConfig?.rightPanel?.components || []; - console.log("🔍 [저장] 오른쪽 패널 컴포넌트들:", rightComponents.map((c: any) => ({ - id: c.id, - componentType: c.componentType, - hasComponentConfig: !!c.componentConfig, - componentConfig: JSON.parse(JSON.stringify(c.componentConfig || {})), - }))); - // 왼쪽 패널의 탭 컴포넌트 확인 - const leftTabs = sp.componentConfig?.leftPanel?.components?.filter( - (c: any) => c.componentType === "v2-tabs-widget" - ); - leftTabs?.forEach((tabWidget: any) => { - console.log("🔍 [저장] 왼쪽 패널 탭 위젯 전체 componentConfig:", { - tabWidgetId: tabWidget.id, - fullComponentConfig: JSON.parse(JSON.stringify(tabWidget.componentConfig || {})), - }); - console.log("🔍 [저장] 왼쪽 패널 탭 내부 컴포넌트:", { - tabId: tabWidget.id, - tabs: tabWidget.componentConfig?.tabs?.map((t: any) => ({ - id: t.id, - label: t.label, - componentsCount: t.components?.length || 0, - components: t.components, - })), - }); - }); - // 오른쪽 패널의 탭 컴포넌트 확인 - const rightTabs = sp.componentConfig?.rightPanel?.components?.filter( - (c: any) => c.componentType === "v2-tabs-widget" - ); - rightTabs?.forEach((tabWidget: any) => { - console.log("🔍 [저장] 오른쪽 패널 탭 위젯 전체 componentConfig:", { - tabWidgetId: tabWidget.id, - fullComponentConfig: JSON.parse(JSON.stringify(tabWidget.componentConfig || {})), - }); - console.log("🔍 [저장] 오른쪽 패널 탭 내부 컴포넌트:", { - tabId: tabWidget.id, - tabs: tabWidget.componentConfig?.tabs?.map((t: any) => ({ - id: t.id, - label: t.label, - componentsCount: t.components?.length || 0, - components: t.components, - })), - }); - }); - }); + // 💾 저장 로그 (디버그 완료 - 간소화) + // console.log("💾 저장 시작:", { screenId: selectedScreen.screenId, componentsCount: layoutWithResolution.components.length }); + // 분할 패널 디버그 로그 (주석 처리) // V2 API 사용 여부에 따라 분기 if (USE_V2_API) { + // 🔧 V2 레이아웃 저장 (디버그 로그 주석 처리) const v2Layout = convertLegacyToV2(layoutWithResolution); - console.log("📦 V2 변환 결과 (분할 패널 overrides):", v2Layout.components - .filter((c: any) => c.url?.includes("split-panel")) - .map((c: any) => ({ - id: c.id, - url: c.url, - overrides: c.overrides, - })) - ); await screenApi.saveLayoutV2(selectedScreen.screenId, v2Layout); - console.log("📦 V2 레이아웃 저장:", v2Layout.components.length, "개 컴포넌트"); + // console.log("📦 V2 레이아웃 저장:", v2Layout.components.length, "개 컴포넌트"); } else { await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); } - console.log("✅ 저장 성공! 메뉴 할당 모달 열기"); + // console.log("✅ 저장 성공!"); toast.success("화면이 저장되었습니다."); // 저장 성공 후 부모에게 화면 정보 업데이트 알림 (테이블명 즉시 반영) @@ -3084,7 +3030,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }, webTypeConfig: getDefaultWebTypeConfig(component.webType), style: { - labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 + labelDisplay: true, // 🆕 라벨 기본 표시 (사용자가 끄고 싶으면 체크 해제) labelFontSize: "14px", labelColor: "#212121", labelFontWeight: "500", @@ -3750,7 +3696,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU entityJoinColumn: column.entityJoinColumn, }), style: { - labelDisplay: false, // 라벨 숨김 + labelDisplay: true, // 🆕 라벨 기본 표시 labelFontSize: "12px", labelColor: "#212121", labelFontWeight: "500", @@ -3816,7 +3762,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU entityJoinColumn: column.entityJoinColumn, }), style: { - labelDisplay: false, // 라벨 숨김 + labelDisplay: true, // 🆕 라벨 기본 표시 labelFontSize: "14px", labelColor: "#000000", // 순수한 검정 labelFontWeight: "500", @@ -5452,6 +5398,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU ); } + // 🔧 ScreenDesigner 렌더링 확인 (디버그 완료 - 주석 처리) + // console.log("🏠 ScreenDesigner 렌더!", Date.now()); + return ( @@ -6163,6 +6112,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 그룹에 속하지 않은 일반 컴포넌트들 const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + // 🔧 렌더링 확인 로그 (디버그 완료 - 주석 처리) + // console.log("🔄 ScreenDesigner 렌더링:", { componentsCount: regularComponents.length, forceRenderTrigger, timestamp: Date.now() }); + return ( <> {/* 일반 컴포넌트들 */} @@ -6228,11 +6180,23 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const globalFiles = globalFileState[component.id] || []; const componentFiles = (component as any).uploadedFiles || []; const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; + // 🆕 style 변경 시 리렌더링을 위한 key 추가 + const styleKey = component.style?.labelDisplay !== undefined ? `label-${component.style.labelDisplay}` : ""; + const fullKey = `${component.id}-${fileStateKey}-${styleKey}-${(component as any).lastFileUpdate || 0}-${forceRenderTrigger}`; + + // 🔧 v2-input 계열 컴포넌트 key 변경 로그 (디버그 완료 - 주석 처리) + // if (component.id.includes("v2-") || component.widgetType?.includes("v2-")) { console.log("🔑 RealtimePreview key:", { id: component.id, styleKey, labelDisplay: component.style?.labelDisplay, forceRenderTrigger, fullKey }); } + + // 🆕 labelDisplay 변경 시 새 객체로 강제 변경 감지 + const componentWithLabel = { + ...displayComponent, + _labelDisplayKey: component.style?.labelDisplay, + }; return ( = ({
{ handleUpdate("style.labelText", e.target.value); handleUpdate("label", e.target.value); // label도 함께 업데이트 @@ -861,8 +861,23 @@ export const V2PropertiesPanel: React.FC = ({
handleUpdate("style.labelDisplay", checked)} + checked={selectedComponent.style?.labelDisplay === true || selectedComponent.labelDisplay === true} + onCheckedChange={(checked) => { + const boolValue = checked === true; + // 🔧 "필수"처럼 직접 경로로 업데이트! (style 객체 전체 덮어쓰기 방지) + handleUpdate("style.labelDisplay", boolValue); + handleUpdate("labelDisplay", boolValue); + // labelText도 설정 (처음 켤 때 라벨 텍스트가 없을 수 있음) + if (boolValue && !selectedComponent.style?.labelText) { + const labelValue = + selectedComponent.label || + selectedComponent.componentConfig?.label || + ""; + if (labelValue) { + handleUpdate("style.labelText", labelValue); + } + } + }} className="h-4 w-4" /> diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index 85083174..ac4bc33c 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -802,7 +802,9 @@ export const V2Input = forwardRef((props, ref) => }; // 라벨이 표시될 때 입력 필드가 차지할 높이 계산 - const showLabel = label && style?.labelDisplay !== false; + // 🔧 label prop이 없어도 style.labelText에서 가져올 수 있도록 수정 + const actualLabel = label || style?.labelText; + const showLabel = actualLabel && style?.labelDisplay === true; // size에서 우선 가져오고, 없으면 style에서 가져옴 const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; @@ -836,7 +838,7 @@ export const V2Input = forwardRef((props, ref) => }} className="text-sm font-medium whitespace-nowrap" > - {label} + {actualLabel} {required && *} )} diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 9571abef..4fe888bf 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -513,6 +513,18 @@ export const DynamicComponentRenderer: React.FC = componentType === "modal-repeater-table" || componentType === "v2-input"; + // 🆕 v2-input 등의 라벨 표시 로직 (labelDisplay가 true일 때만 라벨 표시) + const labelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay; + const effectiveLabel = labelDisplay === true + ? (component.style?.labelText || (component as any).label || component.componentConfig?.label) + : undefined; + + // 🔧 순서 중요! finalStyle 먼저, component.style 나중에 (커스텀 속성이 CSS 속성을 덮어써야 함) + const mergedStyle = { + ...finalStyle, // CSS 속성 (width, height 등) - 먼저! + ...component.style, // 원본 style (labelDisplay, labelText 등) - 나중에! (덮어씀) + }; + const rendererProps = { component, isSelected, @@ -521,11 +533,14 @@ export const DynamicComponentRenderer: React.FC = onDragEnd, size: component.size || newComponent.defaultSize, position: component.position, - style: finalStyle, // size를 포함한 최종 style config: component.componentConfig, componentConfig: component.componentConfig, // componentConfig의 모든 속성을 props로 spread (tableName, displayField 등) ...(component.componentConfig || {}), + // 🔧 style은 맨 마지막에! (componentConfig.style이 있어도 mergedStyle이 우선) + style: mergedStyle, + // 🆕 라벨 표시 (labelDisplay가 true일 때만) + label: effectiveLabel, // 🆕 V2 레이아웃에서 overrides에서 복원된 상위 레벨 속성들도 전달 inputType: (component as any).inputType || component.componentConfig?.inputType, columnName: (component as any).columnName || component.componentConfig?.columnName, diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index 83a7771d..d6ed349f 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -555,13 +555,10 @@ export const ButtonPrimaryComponent: React.FC = ({ } // 스타일 계산 - // height: 100%로 부모(RealtimePreviewDynamic의 내부 div)의 높이를 따라감 - // width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어) + // 🔧 사용자가 설정한 크기가 있으면 그대로 사용 const componentStyle: React.CSSProperties = { ...component.style, ...style, - width: "100%", - height: "100%", }; // 디자인 모드 스타일 (border 속성 분리하여 충돌 방지) @@ -1289,19 +1286,23 @@ export const ButtonPrimaryComponent: React.FC = ({ componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading; // 공통 버튼 스타일 - // 🔧 component.style에서 background/backgroundColor 충돌 방지 + // 🔧 component.style에서 background/backgroundColor 충돌 방지 (width/height는 허용) const userStyle = component.style ? Object.fromEntries( Object.entries(component.style).filter( - ([key]) => !["width", "height", "background", "backgroundColor"].includes(key), + ([key]) => !["background", "backgroundColor"].includes(key), ), ) : {}; + // 🔧 사용자가 설정한 크기 우선 사용, 없으면 100% + const buttonWidth = component.size?.width ? `${component.size.width}px` : (style?.width || "100%"); + const buttonHeight = component.size?.height ? `${component.size.height}px` : (style?.height || "100%"); + const buttonElementStyle: React.CSSProperties = { - width: "100%", - height: "100%", - minHeight: "40px", + width: buttonWidth, + height: buttonHeight, + minHeight: "32px", // 🔧 최소 높이를 32px로 줄임 border: "none", borderRadius: "0.5rem", backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor, diff --git a/frontend/lib/registry/components/v2-input/V2InputRenderer.tsx b/frontend/lib/registry/components/v2-input/V2InputRenderer.tsx index 52a230fa..e67a8399 100644 --- a/frontend/lib/registry/components/v2-input/V2InputRenderer.tsx +++ b/frontend/lib/registry/components/v2-input/V2InputRenderer.tsx @@ -31,9 +31,11 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer { }; // 라벨: style.labelText 우선, 없으면 component.label 사용 - // style.labelDisplay가 false면 라벨 숨김 + // 🔧 style.labelDisplay를 먼저 체크 (속성 패널에서 style 객체로 업데이트하므로) const style = component.style || {}; - const effectiveLabel = style.labelDisplay === false ? undefined : (style.labelText || component.label); + const labelDisplay = style.labelDisplay ?? (component as any).labelDisplay; + // labelDisplay: true → 라벨 표시, false → 숨김, undefined → 기존 동작 유지(숨김) + const effectiveLabel = labelDisplay === true ? (style.labelText || component.label) : undefined; return ( ; }; + // 🆕 플로우 기반 제어 설정 + flowConfig?: { + flowId: number; + flowName: string; + executionTiming: "before" | "after" | "replace"; + contextData?: Record; + }; } interface ExecutionPlan { @@ -163,15 +170,22 @@ export class ImprovedButtonActionExecutor { return plan; } - // enableDataflowControl 체크를 제거하고 dataflowConfig만 있으면 실행 + // 🔧 controlMode가 없으면 flowConfig/relationshipConfig 존재 여부로 자동 판단 + const effectiveControlMode = dataflowConfig.controlMode + || (dataflowConfig.flowConfig ? "flow" : null) + || (dataflowConfig.relationshipConfig ? "relationship" : null) + || "none"; + console.log("📋 실행 계획 생성:", { controlMode: dataflowConfig.controlMode, + effectiveControlMode, + hasFlowConfig: !!dataflowConfig.flowConfig, hasRelationshipConfig: !!dataflowConfig.relationshipConfig, enableDataflowControl: buttonConfig.enableDataflowControl, }); - // 관계 기반 제어만 지원 - if (dataflowConfig.controlMode === "relationship" && dataflowConfig.relationshipConfig) { + // 관계 기반 제어 + if (effectiveControlMode === "relationship" && dataflowConfig.relationshipConfig) { const control: ControlConfig = { type: "relationship", relationshipConfig: dataflowConfig.relationshipConfig, @@ -191,11 +205,34 @@ export class ImprovedButtonActionExecutor { } } + // 🆕 플로우 기반 제어 + if (effectiveControlMode === "flow" && dataflowConfig.flowConfig) { + const control: ControlConfig = { + type: "flow", + flowConfig: dataflowConfig.flowConfig, + }; + + console.log("📋 플로우 제어 설정:", dataflowConfig.flowConfig); + + switch (dataflowConfig.flowConfig.executionTiming) { + case "before": + plan.beforeControls.push(control); + break; + case "after": + plan.afterControls.push(control); + break; + case "replace": + plan.afterControls.push(control); + plan.hasReplaceControl = true; + break; + } + } + return plan; } /** - * 🔥 제어 실행 (관계 또는 외부호출) + * 🔥 제어 실행 (관계 또는 플로우) */ private static async executeControls( controls: ControlConfig[], @@ -206,8 +243,16 @@ export class ImprovedButtonActionExecutor { for (const control of controls) { try { - // 관계 실행만 지원 - const result = await this.executeRelationship(control.relationshipConfig, formData, context); + let result: ExecutionResult; + + // 🆕 제어 타입에 따라 분기 처리 + if (control.type === "flow" && control.flowConfig) { + result = await this.executeFlow(control.flowConfig, formData, context); + } else if (control.type === "relationship" && control.relationshipConfig) { + result = await this.executeRelationship(control.relationshipConfig, formData, context); + } else { + throw new Error(`지원하지 않는 제어 타입: ${control.type}`); + } results.push(result); @@ -215,7 +260,7 @@ export class ImprovedButtonActionExecutor { if (!result.success) { throw new Error(result.message); } - } catch (error) { + } catch (error: any) { console.error(`제어 실행 실패 (${control.type}):`, error); results.push({ success: false, @@ -230,6 +275,61 @@ export class ImprovedButtonActionExecutor { return results; } + /** + * 🆕 플로우 실행 + */ + private static async executeFlow( + config: { + flowId: number; + flowName: string; + executionTiming: "before" | "after" | "replace"; + contextData?: Record; + }, + formData: Record, + context: ButtonExecutionContext, + ): Promise { + const startTime = Date.now(); + + try { + console.log(`🔄 플로우 실행 시작: ${config.flowName} (ID: ${config.flowId})`); + + // 플로우 실행 API 호출 + const response = await apiClient.post(`/api/dataflow/node-flows/${config.flowId}/execute`, { + formData, + contextData: config.contextData || {}, + selectedRows: context.selectedRows || [], + flowSelectedData: context.flowSelectedData || [], + screenId: context.screenId, + companyCode: context.companyCode, + userId: context.userId, + }); + + const executionTime = Date.now() - startTime; + + if (response.data?.success) { + console.log(`✅ 플로우 실행 성공: ${config.flowName}`, response.data); + return { + success: true, + message: `플로우 "${config.flowName}" 실행 완료`, + executionTime, + data: response.data, + }; + } else { + throw new Error(response.data?.message || "플로우 실행 실패"); + } + } catch (error: any) { + const executionTime = Date.now() - startTime; + console.error(`❌ 플로우 실행 실패: ${config.flowName}`, error); + + return { + success: false, + message: `플로우 "${config.flowName}" 실행 실패: ${error.message}`, + executionTime, + error: error.message, + }; + } + } + /** * 🔥 관계 실행 */ diff --git a/frontend/lib/utils/layoutV2Converter.ts b/frontend/lib/utils/layoutV2Converter.ts index 32360a73..b8485487 100644 --- a/frontend/lib/utils/layoutV2Converter.ts +++ b/frontend/lib/utils/layoutV2Converter.ts @@ -191,6 +191,8 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData | autoFill: overrides.autoFill, // 🆕 style 설정 복원 (라벨 텍스트, 라벨 스타일 등) style: overrides.style || {}, + // 🔧 webTypeConfig 복원 (버튼 제어기능, 플로우 가시성 등) + webTypeConfig: overrides.webTypeConfig || {}, // 기존 구조 호환을 위한 추가 필드 parentId: null, gridColumns: 12, @@ -245,13 +247,47 @@ export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 { if (comp.autoFill) topLevelProps.autoFill = comp.autoFill; // 🆕 style 설정 저장 (라벨 텍스트, 라벨 스타일 등) if (comp.style && Object.keys(comp.style).length > 0) topLevelProps.style = comp.style; + // 🔧 webTypeConfig 저장 (버튼 제어기능, 플로우 가시성 등) + if (comp.webTypeConfig && Object.keys(comp.webTypeConfig).length > 0) { + topLevelProps.webTypeConfig = comp.webTypeConfig; + // 🔍 디버그: webTypeConfig 저장 확인 + if (comp.webTypeConfig.dataflowConfig || comp.webTypeConfig.enableDataflowControl) { + console.log("💾 webTypeConfig 저장:", { + componentId: comp.id, + enableDataflowControl: comp.webTypeConfig.enableDataflowControl, + dataflowConfig: comp.webTypeConfig.dataflowConfig, + }); + } + } // 현재 설정에서 차이값만 추출 const fullConfig = comp.componentConfig || {}; const configOverrides = extractCustomConfig(fullConfig, defaults); + // 🔧 디버그: style 저장 확인 (주석 처리) + // if (comp.style?.labelDisplay !== undefined || configOverrides.style?.labelDisplay !== undefined) { console.log("💾 저장 시 style 변환:", { componentId: comp.id, "comp.style": comp.style, "configOverrides.style": configOverrides.style, "topLevelProps.style": topLevelProps.style }); } + // 상위 레벨 속성과 componentConfig 병합 - const overrides = { ...topLevelProps, ...configOverrides }; + // 🔧 style은 양쪽을 병합하되 comp.style(topLevelProps.style)을 우선시 + const mergedStyle = { + ...(configOverrides.style || {}), + ...(topLevelProps.style || {}), + }; + + // 🔧 webTypeConfig도 병합 (topLevelProps가 우선, dataflowConfig 등 보존) + const mergedWebTypeConfig = { + ...(configOverrides.webTypeConfig || {}), + ...(topLevelProps.webTypeConfig || {}), + }; + + const overrides = { + ...topLevelProps, + ...configOverrides, + // 🆕 병합된 style 사용 (comp.style 값이 최종 우선) + ...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}), + // 🆕 병합된 webTypeConfig 사용 (comp.webTypeConfig가 최종 우선) + ...(Object.keys(mergedWebTypeConfig).length > 0 ? { webTypeConfig: mergedWebTypeConfig } : {}), + }; return { id: comp.id, diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index ca804adc..2e23bc81 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -15,7 +15,8 @@ const nextConfig = { // 실험적 기능 활성화 experimental: { - outputFileTracingRoot: undefined, + // 메모리 사용량 최적화 (Next.js 15+) + webpackMemoryOptimizations: true, }, // API 프록시 설정 - 백엔드로 요청 전달