feat: Docker 및 컴포넌트 최적화
- Docker Compose 설정에서 Node.js 메모리 제한을 8192MB로 증가시키고, Next.js telemetry를 비활성화하여 성능을 개선하였습니다. - Next.js 구성에서 메모리 사용량 최적화를 위한 webpackMemoryOptimizations를 활성화하였습니다. - ScreenModal 컴포넌트에서 overflow 속성을 조정하여 라벨이 잘리지 않도록 개선하였습니다. - InteractiveScreenViewerDynamic 컴포넌트에서 라벨 표시 여부를 확인하는 로직을 추가하여 사용자 경험을 향상시켰습니다. - RealtimePreviewDynamic 컴포넌트에서 라벨 표시 및 디버깅 로그를 추가하여 렌더링 과정을 추적할 수 있도록 하였습니다. - ImprovedButtonControlConfigPanel에서 controlMode 설정을 추가하여 플로우 제어 기능을 개선하였습니다. - V2PropertiesPanel에서 라벨 텍스트 및 표시 상태 업데이트 로직을 개선하여 일관성을 높였습니다. - DynamicComponentRenderer에서 라벨 표시 로직을 개선하여 사용자 정의 스타일을 보다 효과적으로 적용할 수 있도록 하였습니다. - layoutV2Converter에서 webTypeConfig를 병합하여 버튼 제어 기능과 플로우 가시성을 보존하였습니다.
This commit is contained in:
parent
593209e26e
commit
32139beebc
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -603,7 +603,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
</DialogHeader>
|
||||
|
||||
<div
|
||||
className="flex-1 overflow-hidden flex items-center justify-center"
|
||||
className="flex-1 overflow-auto flex items-start justify-center pt-6"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
|
|
@ -620,6 +620,8 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
style={{
|
||||
width: `${screenDimensions?.width || 800}px`,
|
||||
height: `${screenDimensions?.height || 600}px`,
|
||||
// 🆕 라벨이 위로 튀어나갈 수 있도록 overflow visible 설정
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
|
|
|
|||
|
|
@ -1062,22 +1062,35 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
// TableSearchWidget의 경우 높이를 자동으로 설정
|
||||
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
||||
|
||||
// 🆕 라벨 표시 여부 확인 (V2 입력 컴포넌트)
|
||||
// labelDisplay가 false가 아니고, labelText 또는 label이 있으면 라벨 표시
|
||||
const isV2InputComponent = type === "v2-input" || type === "v2-select" || type === "v2-date";
|
||||
const hasVisibleLabel = isV2InputComponent &&
|
||||
style?.labelDisplay !== false &&
|
||||
(style?.labelText || (component as any).label);
|
||||
|
||||
// 라벨이 있는 경우 상단 여백 계산 (라벨 폰트크기 + 여백)
|
||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||
const labelOffset = hasVisibleLabel ? (labelFontSize + labelMarginBottom + 2) : 0;
|
||||
|
||||
const componentStyle = {
|
||||
position: "absolute" as const,
|
||||
left: position?.x || 0,
|
||||
top: position?.y || 0,
|
||||
top: position?.y || 0, // 원래 위치 유지 (음수로 가면 overflow-hidden에 잘림)
|
||||
zIndex: position?.z || 1,
|
||||
...styleWithoutSize, // width/height 제외한 스타일만 먼저 적용
|
||||
width: size?.width || 200, // size의 픽셀 값이 최종 우선순위
|
||||
height: isTableSearchWidget ? "auto" : size?.height || 10,
|
||||
minHeight: isTableSearchWidget ? "48px" : undefined,
|
||||
// 🆕 라벨이 있으면 overflow visible로 설정하여 라벨이 잘리지 않게 함
|
||||
overflow: labelOffset > 0 ? "visible" : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute" style={componentStyle}>
|
||||
{/* 라벨 숨김 - 모달에서는 라벨을 표시하지 않음 */}
|
||||
{/* 위젯 렌더링 */}
|
||||
{/* 위젯 렌더링 (라벨은 V2Input 내부에서 absolute로 표시됨) */}
|
||||
{renderInteractiveWidget(component)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <div className="text-xs text-gray-500">위젯이 아닙니다</div>;
|
||||
|
|
@ -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<RealtimePreviewProps> = ({
|
|||
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<number | null>(null);
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
|
|
@ -741,6 +750,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
{/* 컴포넌트 타입 - 레지스트리 기반 렌더링 (Section Paper, Section Card 등) */}
|
||||
{type === "component" &&
|
||||
(() => {
|
||||
console.log("📦 DynamicComponentRenderer 렌더링:", component.id, "labelDisplay:", component.style?.labelDisplay);
|
||||
const { DynamicComponentRenderer } = require("@/lib/registry/DynamicComponentRenderer");
|
||||
return (
|
||||
<DynamicComponentRenderer
|
||||
|
|
|
|||
|
|
@ -644,9 +644,9 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 선택된 컴포넌트 정보 표시 */}
|
||||
{/* 선택된 컴포넌트 정보 표시 - 🔧 오른쪽으로 이동 (라벨과 겹치지 않도록) */}
|
||||
{isSelected && (
|
||||
<div className="bg-primary text-primary-foreground absolute -top-7 left-0 rounded-md px-2.5 py-1 text-xs font-medium shadow-sm">
|
||||
<div className="bg-primary text-primary-foreground absolute -top-7 right-0 rounded-md px-2.5 py-1 text-xs font-medium shadow-sm">
|
||||
{type === "widget" && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{getWidgetIcon((component as WidgetComponent).widgetType)}
|
||||
|
|
@ -685,7 +685,8 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// React.memo로 래핑하여 불필요한 리렌더링 방지
|
||||
// 🔧 arePropsEqual 제거 - 기본 React.memo 사용 (디버깅용)
|
||||
// component 객체가 새로 생성되면 자동으로 리렌더링됨
|
||||
export const RealtimePreviewDynamic = React.memo(RealtimePreviewDynamicComponent);
|
||||
|
||||
// displayName 설정 (디버깅용)
|
||||
|
|
|
|||
|
|
@ -472,14 +472,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
// 이미 배치된 컬럼 목록 계산
|
||||
const placedColumns = useMemo(() => {
|
||||
const placed = new Set<string>();
|
||||
// 🔧 화면의 메인 테이블명을 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 (
|
||||
<ScreenPreviewProvider isPreviewMode={false}>
|
||||
<TableOptionsProvider>
|
||||
|
|
@ -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 (
|
||||
<RealtimePreview
|
||||
key={`${component.id}-${fileStateKey}-${(component as any).lastFileUpdate || 0}-${forceRenderTrigger}`}
|
||||
component={displayComponent}
|
||||
key={fullKey}
|
||||
component={componentWithLabel}
|
||||
isSelected={
|
||||
selectedComponent?.id === component.id ||
|
||||
groupState.selectedComponents.includes(component.id)
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlCon
|
|||
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig", {
|
||||
...dataflowConfig,
|
||||
// 🔧 controlMode 설정 (플로우 제어가 있으면 "flow", 없으면 "none")
|
||||
controlMode: firstValidControl ? "flow" : "none",
|
||||
// 기존 형식 (하위 호환성)
|
||||
selectedDiagramId: firstValidControl?.flowId || null,
|
||||
selectedRelationshipId: null,
|
||||
|
|
|
|||
|
|
@ -822,7 +822,7 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
|
|||
<div className="space-y-1">
|
||||
<Label className="text-xs">라벨 텍스트</Label>
|
||||
<Input
|
||||
value={selectedComponent.style?.labelText !== undefined ? selectedComponent.style.labelText : (selectedComponent.label || "")}
|
||||
value={selectedComponent.style?.labelText !== undefined ? selectedComponent.style.labelText : (selectedComponent.label || selectedComponent.componentConfig?.label || "")}
|
||||
onChange={(e) => {
|
||||
handleUpdate("style.labelText", e.target.value);
|
||||
handleUpdate("label", e.target.value); // label도 함께 업데이트
|
||||
|
|
@ -861,8 +861,23 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
|
|||
</div>
|
||||
<div className="flex items-center space-x-2 pt-5">
|
||||
<Checkbox
|
||||
checked={selectedComponent.style?.labelDisplay !== false}
|
||||
onCheckedChange={(checked) => 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"
|
||||
/>
|
||||
<Label className="text-xs">표시</Label>
|
||||
|
|
|
|||
|
|
@ -802,7 +802,9 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((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<HTMLDivElement, V2InputProps>((props, ref) =>
|
|||
}}
|
||||
className="text-sm font-medium whitespace-nowrap"
|
||||
>
|
||||
{label}
|
||||
{actualLabel}
|
||||
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||
</Label>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -513,6 +513,18 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
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<DynamicComponentRendererProps> =
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -555,13 +555,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
}
|
||||
|
||||
// 스타일 계산
|
||||
// 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<ButtonPrimaryComponentProps> = ({
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<V2Input
|
||||
|
|
|
|||
|
|
@ -43,13 +43,20 @@ export interface ButtonExecutionResult {
|
|||
}
|
||||
|
||||
interface ControlConfig {
|
||||
type: "relationship";
|
||||
relationshipConfig: {
|
||||
type: "relationship" | "flow";
|
||||
relationshipConfig?: {
|
||||
relationshipId: string;
|
||||
relationshipName: string;
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
contextData?: Record<string, any>;
|
||||
};
|
||||
// 🆕 플로우 기반 제어 설정
|
||||
flowConfig?: {
|
||||
flowId: number;
|
||||
flowName: string;
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
contextData?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
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<string, any>;
|
||||
},
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 관계 실행
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ const nextConfig = {
|
|||
|
||||
// 실험적 기능 활성화
|
||||
experimental: {
|
||||
outputFileTracingRoot: undefined,
|
||||
// 메모리 사용량 최적화 (Next.js 15+)
|
||||
webpackMemoryOptimizations: true,
|
||||
},
|
||||
|
||||
// API 프록시 설정 - 백엔드로 요청 전달
|
||||
|
|
|
|||
Loading…
Reference in New Issue