diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index bcf9959c..1bb04e97 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -1119,6 +1119,12 @@ export const InteractiveScreenViewerDynamic: React.FC { const compType = (component as any).componentType || ""; const isSplitLine = type === "component" && compType === "v2-split-line"; @@ -1194,9 +1200,17 @@ export const InteractiveScreenViewerDynamic: React.FC { + const { borderWidth: _bw, borderColor: _bc, borderStyle: _bs, border: _b, borderRadius: _br, ...rest } = safeStyleWithoutSize; + return rest; + })() + : safeStyleWithoutSize; + const componentStyle = { position: "absolute" as const, - ...safeStyleWithoutSize, + ...cleanedStyle, // left/top은 반드시 마지막에 (styleWithoutSize가 덮어쓰지 못하게) left: adjustedX, top: position?.y || 0, @@ -1267,11 +1281,7 @@ export const InteractiveScreenViewerDynamic: React.FC
{needsExternalLabel ? ( -
- {externalLabelComponent} -
- {renderInteractiveWidget(componentToRender)} + isHorizLabel ? ( +
+ +
+ {renderInteractiveWidget(componentToRender)} +
-
+ ) : ( +
+ {externalLabelComponent} +
+ {renderInteractiveWidget(componentToRender)} +
+
+ ) ) : ( renderInteractiveWidget(componentToRender) )} diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index b95506d9..dcca4d0d 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -548,10 +548,23 @@ const RealtimePreviewDynamicComponent: React.FC = ({ const origWidth = size?.width || 100; const isSplitShrunk = splitAdjustedWidth !== null && splitAdjustedWidth < origWidth; + // v2 수평 라벨 컴포넌트: position wrapper에서 border 제거 (DynamicComponentRenderer가 내부에서 처리) + const isV2HorizLabel = !!( + componentStyle && + (componentStyle.labelDisplay === true || componentStyle.labelDisplay === "true") && + (componentStyle.labelPosition === "left" || componentStyle.labelPosition === "right") + ); + const safeComponentStyle = isV2HorizLabel + ? (() => { + const { borderWidth, borderColor, borderStyle, border, borderRadius, ...rest } = componentStyle as any; + return rest; + })() + : componentStyle; + const baseStyle = { left: `${adjustedPositionX}px`, top: `${position.y}px`, - ...componentStyle, + ...safeComponentStyle, width: splitAdjustedWidth !== null ? `${splitAdjustedWidth}px` : displayWidth, height: displayHeight, zIndex: component.type === "layout" ? 1 : position.z || 2, diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index ed98561c..50c4bee4 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -371,15 +371,18 @@ export const DynamicComponentRenderer: React.FC = try { const { V2SelectRenderer } = require("@/lib/registry/components/v2-select/V2SelectRenderer"); const fieldName = columnName || component.id; - const currentValue = props.formData?.[fieldName] || ""; - const handleChange = (value: any) => { - if (props.onFormDataChange) { - props.onFormDataChange(fieldName, value); - } - }; - - // V2SelectRenderer용 컴포넌트 데이터 구성 + // 수평 라벨 감지 + const catLabelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay; + const catLabelPosition = component.style?.labelPosition; + const catLabelText = (catLabelDisplay === true || catLabelDisplay === "true") + ? (component.style?.labelText || (component as any).label || component.componentConfig?.label) + : undefined; + const catNeedsExternalHorizLabel = !!( + catLabelText && + (catLabelPosition === "left" || catLabelPosition === "right") + ); + const selectComponent = { ...component, componentConfig: { @@ -395,6 +398,24 @@ export const DynamicComponentRenderer: React.FC = webType: "category", }; + const catStyle = catNeedsExternalHorizLabel + ? { + ...(component as any).style, + labelDisplay: false, + labelPosition: "top" as const, + width: "100%", + height: "100%", + borderWidth: undefined, + borderColor: undefined, + borderStyle: undefined, + border: undefined, + borderRadius: undefined, + } + : (component as any).style; + const catSize = catNeedsExternalHorizLabel + ? { ...(component as any).size, width: undefined, height: undefined } + : (component as any).size; + const rendererProps = { component: selectComponent, formData: props.formData, @@ -402,12 +423,47 @@ export const DynamicComponentRenderer: React.FC = isDesignMode: props.isDesignMode, isInteractive: props.isInteractive ?? !props.isDesignMode, tableName, - style: (component as any).style, - size: (component as any).size, + style: catStyle, + size: catSize, }; const rendererInstance = new V2SelectRenderer(rendererProps); - return rendererInstance.render(); + const renderedCatSelect = rendererInstance.render(); + + if (catNeedsExternalHorizLabel) { + const labelGap = component.style?.labelGap || "8px"; + const labelFontSize = component.style?.labelFontSize || "14px"; + const labelColor = component.style?.labelColor || "#64748b"; + const labelFontWeight = component.style?.labelFontWeight || "500"; + const isRequired = component.required || (component as any).required; + const isLeft = catLabelPosition === "left"; + return ( +
+ +
+ {renderedCatSelect} +
+
+ ); + } + return renderedCatSelect; } catch (error) { console.error("❌ V2SelectRenderer 로드 실패:", error); } @@ -625,12 +681,33 @@ export const DynamicComponentRenderer: React.FC = ? (component.style?.labelText || (component as any).label || component.componentConfig?.label) : undefined; + // 🔧 수평 라벨(left/right) 감지 → 외부 flex 컨테이너에서 라벨 처리 + const labelPosition = component.style?.labelPosition; + const isV2Component = componentType?.startsWith("v2-"); + const needsExternalHorizLabel = !!( + isV2Component && + effectiveLabel && + (labelPosition === "left" || labelPosition === "right") + ); + // 🔧 순서 중요! component.style 먼저, CSS 크기 속성은 size 기반으로 덮어씀 const mergedStyle = { ...component.style, // 원본 style (labelDisplay, labelText 등) - 먼저! // CSS 크기 속성은 size에서 계산한 값으로 명시적 덮어쓰기 (우선순위 최고) width: finalStyle.width, height: finalStyle.height, + // 수평 라벨 → V2 컴포넌트에는 라벨 비활성화 (외부에서 처리) + ...(needsExternalHorizLabel ? { + labelDisplay: false, + labelPosition: "top" as const, + width: "100%", + height: "100%", + borderWidth: undefined, + borderColor: undefined, + borderStyle: undefined, + border: undefined, + borderRadius: undefined, + } : {}), }; // 컬럼 메타데이터 기반 componentConfig 병합 (DB 최신 설정 우선) @@ -649,7 +726,9 @@ export const DynamicComponentRenderer: React.FC = onClick, onDragStart, onDragEnd, - size: component.size || newComponent.defaultSize, + size: needsExternalHorizLabel + ? { ...(component.size || newComponent.defaultSize), width: undefined, height: undefined } + : (component.size || newComponent.defaultSize), position: component.position, config: mergedComponentConfig, componentConfig: mergedComponentConfig, @@ -657,8 +736,8 @@ export const DynamicComponentRenderer: React.FC = ...(mergedComponentConfig || {}), // 🔧 style은 맨 마지막에! (componentConfig.style이 있어도 mergedStyle이 우선) style: mergedStyle, - // 🆕 라벨 표시 (labelDisplay가 true일 때만) - label: effectiveLabel, + // 수평 라벨 → 외부에서 처리하므로 label 전달 안 함 + label: needsExternalHorizLabel ? undefined : effectiveLabel, // 🆕 V2 레이아웃에서 overrides에서 복원된 상위 레벨 속성들도 전달 (DB 메타데이터 우선) inputType: (baseColumnName && columnMetaCache[screenTableName || ""]?.[baseColumnName]?.input_type) || (component as any).inputType || mergedComponentConfig?.inputType, columnName: (component as any).columnName || component.componentConfig?.columnName, @@ -759,16 +838,51 @@ export const DynamicComponentRenderer: React.FC = NewComponentRenderer.prototype && NewComponentRenderer.prototype.render; + let renderedElement: React.ReactElement; if (isClass) { - // 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속) const rendererInstance = new NewComponentRenderer(rendererProps); - return rendererInstance.render(); + renderedElement = rendererInstance.render(); } else { - // 함수형 컴포넌트 - // refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제 - - return ; + renderedElement = ; } + + // 수평 라벨 → 라벨을 컴포넌트 영역 바깥에 absolute 배치, 입력은 100% 채움 + if (needsExternalHorizLabel) { + const labelGap = component.style?.labelGap || "8px"; + const labelFontSize = component.style?.labelFontSize || "14px"; + const labelColor = component.style?.labelColor || "#64748b"; + const labelFontWeight = component.style?.labelFontWeight || "500"; + const isRequired = component.required || (component as any).required; + const isLeft = labelPosition === "left"; + + return ( +
+ +
+ {renderedElement} +
+
+ ); + } + + return renderedElement; } } catch (error) { console.error(`❌ 새 컴포넌트 렌더링 실패 (${componentType}):`, error);