- {/* 통합 패널 - 좌측 사이드바 제거 후 너비 300px로 확장 */}
- {panelStates.v2?.isOpen && (
-
- )}
-
- {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - GPU 가속 스크롤 적용 */}
-
- {/* Pan 모드 안내 - 제거됨 */}
- {/* 줌 레벨 표시 */}
-
- 🔍 {Math.round(zoomLevel * 100)}%
-
- {/* 🆕 플로우 버튼 그룹 제어 (버튼 선택 시 표시) */}
- {(() => {
- // 선택된 컴포넌트들
- const selectedComps = layout.components.filter((c) => groupState.selectedComponents.includes(c.id));
-
- // 버튼 컴포넌트만 필터링
- const selectedButtons = selectedComps.filter((comp) => areAllButtons([comp]));
-
- // 플로우 그룹에 속한 버튼이 있는지 확인
- const hasFlowGroupButton = selectedButtons.some((btn) => {
- const flowConfig = (btn as any).webTypeConfig?.flowVisibilityConfig;
- return flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId;
- });
-
- // 버튼이 선택되었거나 플로우 그룹 버튼이 있으면 표시
- const shouldShow = selectedButtons.length >= 1 && (selectedButtons.length >= 2 || hasFlowGroupButton);
-
- if (!shouldShow) return null;
-
- return (
-
-
-
-
-
-
-
-
-
{selectedButtons.length}개 버튼 선택됨
-
-
- {/* 그룹 생성 버튼 (2개 이상 선택 시) */}
- {selectedButtons.length >= 2 && (
-
-
-
-
-
-
- 플로우 그룹으로 묶기
-
- )}
-
- {/* 그룹 해제 버튼 (플로우 그룹 버튼이 있으면 항상 표시) */}
- {hasFlowGroupButton && (
-
-
-
-
-
-
-
- 그룹 해제
-
- )}
-
- {/* 상태 표시 */}
- {hasFlowGroupButton &&
✓ 플로우 그룹 버튼
}
-
-
- );
- })()}
- {/* 🆕 활성 레이어 인디케이터 (기본 레이어가 아닌 경우 표시) */}
- {activeLayerId > 1 && (
-
-
-
레이어 {activeLayerId} 편집 중
-
- )}
-
- {/* 줌 적용 시 스크롤 영역 확보를 위한 래퍼 - 중앙 정렬 + contain 최적화 */}
- {(() => {
- // 🆕 조건부 레이어 편집 시 캔버스 크기를 displayRegion에 맞춤
- const activeRegion = activeLayerId > 1 ? layerRegions[activeLayerId] : null;
- const canvasW = activeRegion ? activeRegion.width : screenResolution.width;
- const canvasH = activeRegion ? activeRegion.height : screenResolution.height;
-
- return (
-
- {/* 실제 작업 캔버스 (해상도 크기 또는 조건부 레이어 영역 크기) */}
-
-
{
- if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) {
- setSelectedComponent(null);
- setGroupState((prev) => ({ ...prev, selectedComponents: [] }));
- }
- }}
- onMouseDown={(e) => {
- // Pan 모드가 아닐 때만 다중 선택 시작
- if (e.target === e.currentTarget && !isPanMode) {
- startSelectionDrag(e);
- }
- }}
- onDragOver={(e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = "copy";
- }}
- onDropCapture={(e) => {
- // 캡처 단계에서 드롭 이벤트를 처리하여 자식 요소 드롭도 감지
- e.preventDefault();
- handleDrop(e);
- }}
- >
- {/* 격자 라인 */}
- {gridLines.map((line, index) => (
-
- ))}
-
- {/* 컴포넌트들 */}
- {(() => {
- // 🆕 플로우 버튼 그룹 감지 및 처리
- // visibleComponents를 사용하여 활성 레이어의 컴포넌트만 표시
- const topLevelComponents = visibleComponents.filter((component) => !component.parentId);
-
- // auto-compact 모드의 버튼들을 그룹별로 묶기
- const buttonGroups: Record
= {};
- const processedButtonIds = new Set();
-
- topLevelComponents.forEach((component) => {
- const isButton =
- component.type === "button" ||
- (component.type === "component" &&
- ["button-primary", "button-secondary"].includes((component as any).componentType));
-
- if (isButton) {
- const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as
- | FlowVisibilityConfig
- | undefined;
-
- if (
- flowConfig?.enabled &&
- flowConfig.layoutBehavior === "auto-compact" &&
- flowConfig.groupId
- ) {
- if (!buttonGroups[flowConfig.groupId]) {
- buttonGroups[flowConfig.groupId] = [];
- }
- buttonGroups[flowConfig.groupId].push(component);
- processedButtonIds.add(component.id);
- }
- }
- });
-
- // 그룹에 속하지 않은 일반 컴포넌트들
- const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
-
- // 🔧 렌더링 확인 로그 (디버그 완료 - 주석 처리)
- // console.log("🔄 ScreenDesigner 렌더링:", { componentsCount: regularComponents.length, forceRenderTrigger, timestamp: Date.now() });
-
- return (
- <>
- {/* 일반 컴포넌트들 */}
- {regularComponents.map((component) => {
- const children =
- component.type === "group"
- ? layout.components.filter((child) => child.parentId === component.id)
- : [];
-
- // 드래그 중 시각적 피드백 (다중 선택 지원)
- const isDraggingThis =
- dragState.isDragging && dragState.draggedComponent?.id === component.id;
- const isBeingDragged =
- dragState.isDragging &&
- dragState.draggedComponents.some((dragComp) => dragComp.id === component.id);
-
- let displayComponent = component;
-
- if (isBeingDragged) {
- if (isDraggingThis) {
- // 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트
- displayComponent = {
- ...component,
- position: dragState.currentPosition,
- style: {
- ...component.style,
- opacity: 0.8,
- transform: "scale(1.02)",
- transition: "none",
- zIndex: 50,
- },
- };
- } else {
- // 다른 선택된 컴포넌트들: 상대적 위치로 실시간 업데이트
- const originalComponent = dragState.draggedComponents.find(
- (dragComp) => dragComp.id === component.id,
- );
- if (originalComponent) {
- const deltaX = dragState.currentPosition.x - dragState.originalPosition.x;
- const deltaY = dragState.currentPosition.y - dragState.originalPosition.y;
-
- displayComponent = {
- ...component,
- position: {
- x: originalComponent.position.x + deltaX,
- y: originalComponent.position.y + deltaY,
- z: originalComponent.position.z || 1,
- } as Position,
- style: {
- ...component.style,
- opacity: 0.8,
- transition: "none",
- zIndex: 40, // 주 컴포넌트보다 약간 낮게
- },
- };
- }
- }
- }
-
- // 전역 파일 상태도 key에 포함하여 실시간 리렌더링
- const globalFileState =
- typeof window !== "undefined" ? (window as any).globalFileState || {} : {};
- 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 (
- handleComponentClick(component, e)}
- onDoubleClick={(e) => handleComponentDoubleClick(component, e)}
- onDragStart={(e) => startComponentDrag(component, e)}
- onDragEnd={endDrag}
- selectedScreen={selectedScreen}
- tableName={selectedScreen?.tableName} // 🆕 디자인 모드에서도 옵션 로딩을 위해 전달
- menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
- // onZoneComponentDrop 제거
- onZoneClick={handleZoneClick}
- // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영)
- onConfigChange={(config) => {
- // console.log("📤 테이블 설정 변경을 상세설정에 반영:", config);
-
- // 컴포넌트의 componentConfig 업데이트
- const updatedComponents = layout.components.map((comp) => {
- if (comp.id === component.id) {
- return {
- ...comp,
- componentConfig: {
- ...comp.componentConfig,
- ...config,
- },
- };
- }
- return comp;
- });
-
- const newLayout = {
- ...layout,
- components: updatedComponents,
- };
-
- setLayout(newLayout);
- saveToHistory(newLayout);
-
- console.log("✅ 컴포넌트 설정 업데이트 완료:", {
- componentId: component.id,
- updatedConfig: config,
- });
- }}
- // 🆕 컴포넌트 전체 업데이트 핸들러 (탭 내부 컴포넌트 위치 조정 등)
- onUpdateComponent={(updatedComponent) => {
- const updatedComponents = layout.components.map((comp) =>
- comp.id === updatedComponent.id ? updatedComponent : comp,
- );
-
- const newLayout = {
- ...layout,
- components: updatedComponents,
- };
-
- setLayout(newLayout);
- saveToHistory(newLayout);
- }}
- // 🆕 리사이즈 핸들러 (10px 스냅 적용됨)
- onResize={(componentId, newSize) => {
- setLayout((prevLayout) => {
- const updatedComponents = prevLayout.components.map((comp) =>
- comp.id === componentId ? { ...comp, size: newSize } : comp,
- );
-
- const newLayout = {
- ...prevLayout,
- components: updatedComponents,
- };
-
- // saveToHistory는 별도로 호출 (prevLayout 기반)
- setTimeout(() => saveToHistory(newLayout), 0);
- return newLayout;
- });
- }}
- // 🆕 탭 내부 컴포넌트 선택 핸들러
- onSelectTabComponent={(tabId, compId, comp) =>
- handleSelectTabComponent(component.id, tabId, compId, comp)
- }
- selectedTabComponentId={
- selectedTabComponentInfo?.tabsComponentId === component.id
- ? selectedTabComponentInfo.componentId
- : undefined
- }
- // 🆕 분할 패널 내부 컴포넌트 선택 핸들러
- onSelectPanelComponent={(panelSide, compId, comp) =>
- handleSelectPanelComponent(component.id, panelSide, compId, comp)
- }
- selectedPanelComponentId={
- selectedPanelComponentInfo?.splitPanelId === component.id
- ? selectedPanelComponentInfo.componentId
- : undefined
- }
- >
- {/* 컨테이너, 그룹, 영역, 컴포넌트의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
- {(component.type === "group" ||
- component.type === "container" ||
- component.type === "area" ||
- component.type === "component") &&
- layout.components
- .filter((child) => child.parentId === component.id)
- .map((child) => {
- // 자식 컴포넌트에도 드래그 피드백 적용
- const isChildDraggingThis =
- dragState.isDragging && dragState.draggedComponent?.id === child.id;
- const isChildBeingDragged =
- dragState.isDragging &&
- dragState.draggedComponents.some((dragComp) => dragComp.id === child.id);
-
- let displayChild = child;
-
- if (isChildBeingDragged) {
- if (isChildDraggingThis) {
- // 주 드래그 자식 컴포넌트
- displayChild = {
- ...child,
- position: dragState.currentPosition,
- style: {
- ...child.style,
- opacity: 0.8,
- transform: "scale(1.02)",
- transition: "none",
- zIndex: 50,
- },
- };
- } else {
- // 다른 선택된 자식 컴포넌트들
- const originalChildComponent = dragState.draggedComponents.find(
- (dragComp) => dragComp.id === child.id,
- );
- if (originalChildComponent) {
- const deltaX = dragState.currentPosition.x - dragState.originalPosition.x;
- const deltaY = dragState.currentPosition.y - dragState.originalPosition.y;
-
- displayChild = {
- ...child,
- position: {
- x: originalChildComponent.position.x + deltaX,
- y: originalChildComponent.position.y + deltaY,
- z: originalChildComponent.position.z || 1,
- } as Position,
- style: {
- ...child.style,
- opacity: 0.8,
- transition: "none",
- zIndex: 8888,
- },
- };
- }
- }
- }
-
- // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
- const relativeChildComponent = {
- ...displayChild,
- position: {
- x: displayChild.position.x - component.position.x,
- y: displayChild.position.y - component.position.y,
- z: displayChild.position.z || 1,
- },
- };
-
- return (
- f.objid) || [])}`}
- component={relativeChildComponent}
- isSelected={
- selectedComponent?.id === child.id ||
- groupState.selectedComponents.includes(child.id)
- }
- isDesignMode={true} // 편집 모드로 설정
- onClick={(e) => handleComponentClick(child, e)}
- onDoubleClick={(e) => handleComponentDoubleClick(child, e)}
- onDragStart={(e) => startComponentDrag(child, e)}
- onDragEnd={endDrag}
- selectedScreen={selectedScreen}
- tableName={selectedScreen?.tableName} // 🆕 디자인 모드에서도 옵션 로딩을 위해 전달
- // onZoneComponentDrop 제거
- onZoneClick={handleZoneClick}
- // 설정 변경 핸들러 (자식 컴포넌트용)
- onConfigChange={(config) => {
- // console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config);
- // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
- }}
- // 🆕 자식 컴포넌트 리사이즈 핸들러
- onResize={(componentId, newSize) => {
- setLayout((prevLayout) => {
- const updatedComponents = prevLayout.components.map((comp) =>
- comp.id === componentId ? { ...comp, size: newSize } : comp,
- );
-
- const newLayout = {
- ...prevLayout,
- components: updatedComponents,
- };
-
- setTimeout(() => saveToHistory(newLayout), 0);
- return newLayout;
- });
- }}
- />
- );
- })}
-
- );
- })}
-
- {/* 🆕 플로우 버튼 그룹들 */}
- {Object.entries(buttonGroups).map(([groupId, buttons]) => {
- if (buttons.length === 0) return null;
-
- const firstButton = buttons[0];
- const groupConfig = (firstButton as any).webTypeConfig
- ?.flowVisibilityConfig as FlowVisibilityConfig;
-
- // 🔧 그룹의 위치 및 크기 계산
- // 모든 버튼이 같은 위치(groupX, groupY)에 배치되어 있으므로
- // 첫 번째 버튼의 위치를 그룹 시작점으로 사용
- const direction = groupConfig.groupDirection || "horizontal";
- const gap = groupConfig.groupGap ?? 8;
- const align = groupConfig.groupAlign || "start";
-
- const groupPosition = {
- x: buttons[0].position.x,
- y: buttons[0].position.y,
- z: buttons[0].position.z || 2,
- };
-
- // 버튼들의 실제 크기 계산
- let groupWidth = 0;
- let groupHeight = 0;
-
- if (direction === "horizontal") {
- // 가로 정렬: 모든 버튼의 너비 + 간격
- groupWidth = buttons.reduce((total, button, index) => {
- const buttonWidth = button.size?.width || 100;
- const gapWidth = index < buttons.length - 1 ? gap : 0;
- return total + buttonWidth + gapWidth;
- }, 0);
- groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40));
- } else {
- // 세로 정렬
- groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100));
- groupHeight = buttons.reduce((total, button, index) => {
- const buttonHeight = button.size?.height || 40;
- const gapHeight = index < buttons.length - 1 ? gap : 0;
- return total + buttonHeight + gapHeight;
- }, 0);
- }
-
- // 🆕 그룹 전체가 선택되었는지 확인
- const isGroupSelected = buttons.every(
- (btn) =>
- selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id),
- );
- const hasAnySelected = buttons.some(
- (btn) =>
- selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id),
- );
-
- return (
-
-
{
- // 드래그 피드백
- const isDraggingThis =
- dragState.isDragging && dragState.draggedComponent?.id === button.id;
- const isBeingDragged =
- dragState.isDragging &&
- dragState.draggedComponents.some((dragComp) => dragComp.id === button.id);
-
- let displayButton = button;
-
- if (isBeingDragged) {
- if (isDraggingThis) {
- displayButton = {
- ...button,
- position: dragState.currentPosition,
- style: {
- ...button.style,
- opacity: 0.8,
- transform: "scale(1.02)",
- transition: "none",
- zIndex: 50,
- },
- };
- }
- }
-
- // 🔧 그룹 내부에서는 상대 위치 사용 (wrapper로 처리)
- const relativeButton = {
- ...displayButton,
- position: {
- x: 0,
- y: 0,
- z: displayButton.position.z || 1,
- },
- };
-
- return (
- {
- // 클릭이 아닌 드래그인 경우에만 드래그 시작
- e.preventDefault();
- e.stopPropagation();
-
- const startX = e.clientX;
- const startY = e.clientY;
- let isDragging = false;
- let dragStarted = false;
-
- const handleMouseMove = (moveEvent: MouseEvent) => {
- const deltaX = Math.abs(moveEvent.clientX - startX);
- const deltaY = Math.abs(moveEvent.clientY - startY);
-
- // 5픽셀 이상 움직이면 드래그로 간주
- if ((deltaX > 5 || deltaY > 5) && !dragStarted) {
- isDragging = true;
- dragStarted = true;
-
- // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택
- if (!e.shiftKey) {
- const buttonIds = buttons.map((b) => b.id);
- setGroupState((prev) => ({
- ...prev,
- selectedComponents: buttonIds,
- }));
- }
-
- // 드래그 시작
- startComponentDrag(button, e as any);
- }
- };
-
- const handleMouseUp = () => {
- document.removeEventListener("mousemove", handleMouseMove);
- document.removeEventListener("mouseup", handleMouseUp);
-
- // 드래그가 아니면 클릭으로 처리
- if (!isDragging) {
- // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택
- if (!e.shiftKey) {
- const buttonIds = buttons.map((b) => b.id);
- setGroupState((prev) => ({
- ...prev,
- selectedComponents: buttonIds,
- }));
- }
- handleComponentClick(button, e);
- }
- };
-
- document.addEventListener("mousemove", handleMouseMove);
- document.addEventListener("mouseup", handleMouseUp);
- }}
- onDoubleClick={(e) => {
- e.stopPropagation();
- handleComponentDoubleClick(button, e);
- }}
- className={
- selectedComponent?.id === button.id ||
- groupState.selectedComponents.includes(button.id)
- ? "outline-1 outline-offset-1 outline-blue-400"
- : ""
- }
- >
- {/* 그룹 내부에서는 DynamicComponentRenderer로 직접 렌더링 */}
-
- {}}
- />
-
-
- );
- }}
- />
-
- );
- })}
- >
- );
- })()}
-
- {/* 드래그 선택 영역 */}
- {selectionDrag.isSelecting && (
-
- )}
-
- {/* 빈 캔버스 안내 */}
- {layout.components.length === 0 && (
-
-
-
-
-
-
캔버스가 비어있습니다
-
- 좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요
-
-
-
- 단축키: T(테이블), M(템플릿), P(속성), S(스타일),
- R(격자), D(상세설정), E(해상도)
-
-
- 편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장),
- Ctrl+Z(실행취소), Delete(삭제)
-
-
- ⚠️
- 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다
-
-
-
-
- )}
-
-
-
- ); /* 🔥 줌 래퍼 닫기 */
- })()}
-