diff --git a/backend-node/src/services/DashboardService.ts b/backend-node/src/services/DashboardService.ts index f8816555..c7650df2 100644 --- a/backend-node/src/services/DashboardService.ts +++ b/backend-node/src/services/DashboardService.ts @@ -61,8 +61,9 @@ export class DashboardService { id, dashboard_id, element_type, element_subtype, position_x, position_y, width, height, title, custom_title, show_header, content, data_source_config, chart_config, + list_config, yard_config, display_order, created_at, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) `, [ elementId, @@ -79,6 +80,8 @@ export class DashboardService { element.content || null, JSON.stringify(element.dataSource || {}), JSON.stringify(element.chartConfig || {}), + JSON.stringify(element.listConfig || null), + JSON.stringify(element.yardConfig || null), i, now, now, @@ -342,6 +345,16 @@ export class DashboardService { content: row.content, dataSource: JSON.parse(row.data_source_config || "{}"), chartConfig: JSON.parse(row.chart_config || "{}"), + listConfig: row.list_config + ? typeof row.list_config === "string" + ? JSON.parse(row.list_config) + : row.list_config + : undefined, + yardConfig: row.yard_config + ? typeof row.yard_config === "string" + ? JSON.parse(row.yard_config) + : row.yard_config + : undefined, }) ); @@ -465,8 +478,9 @@ export class DashboardService { id, dashboard_id, element_type, element_subtype, position_x, position_y, width, height, title, custom_title, show_header, content, data_source_config, chart_config, + list_config, yard_config, display_order, created_at, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) `, [ elementId, @@ -483,6 +497,8 @@ export class DashboardService { element.content || null, JSON.stringify(element.dataSource || {}), JSON.stringify(element.chartConfig || {}), + JSON.stringify(element.listConfig || null), + JSON.stringify(element.yardConfig || null), i, now, now, diff --git a/backend-node/src/types/dashboard.ts b/backend-node/src/types/dashboard.ts index 789adda3..b03acbff 100644 --- a/backend-node/src/types/dashboard.ts +++ b/backend-node/src/types/dashboard.ts @@ -35,6 +35,16 @@ export interface DashboardElement { title?: string; showLegend?: boolean; }; + listConfig?: { + columns?: any[]; + pagination?: any; + viewMode?: string; + cardColumns?: number; + }; + yardConfig?: { + layoutId: number; + layoutName?: string; + }; } export interface Dashboard { diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index d06e8f98..eb1e679d 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -351,7 +351,7 @@ export function CanvasElement({ if (isResizing && tempPosition && tempSize) { // tempPosition과 tempSize는 이미 리사이즈 중에 마그네틱 스냅 적용됨 // 다시 스냅하지 않고 그대로 사용! - let finalX = tempPosition.x; + const finalX = tempPosition.x; const finalY = tempPosition.y; let finalWidth = tempSize.width; const finalHeight = tempSize.height; diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx index eecf12ec..45d2cf3c 100644 --- a/frontend/components/admin/dashboard/DashboardCanvas.tsx +++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx @@ -51,6 +51,12 @@ export const DashboardCanvas = forwardRef( // 충돌 방지 기능이 포함된 업데이트 핸들러 const handleUpdateWithCollisionDetection = useCallback( (id: string, updates: Partial) => { + // position이나 size가 아닌 다른 속성 업데이트는 충돌 감지 없이 바로 처리 + if (!updates.position && !updates.size) { + onUpdateElement(id, updates); + return; + } + // 업데이트할 요소 찾기 const elementIndex = elements.findIndex((el) => el.id === id); if (elementIndex === -1) { @@ -58,9 +64,38 @@ export const DashboardCanvas = forwardRef( return; } + // position이나 size와 다른 속성이 함께 있으면 분리해서 처리 + const positionSizeUpdates: any = {}; + const otherUpdates: any = {}; + + Object.keys(updates).forEach((key) => { + if (key === "position" || key === "size") { + positionSizeUpdates[key] = (updates as any)[key]; + } else { + otherUpdates[key] = (updates as any)[key]; + } + }); + + // 다른 속성들은 먼저 바로 업데이트 + if (Object.keys(otherUpdates).length > 0) { + onUpdateElement(id, otherUpdates); + } + + // position/size가 없으면 여기서 종료 + if (Object.keys(positionSizeUpdates).length === 0) { + return; + } + // 임시로 업데이트된 요소 배열 생성 const updatedElements = elements.map((el) => - el.id === id ? { ...el, ...updates, position: updates.position || el.position, size: updates.size || el.size } : el + el.id === id + ? { + ...el, + ...positionSizeUpdates, + position: positionSizeUpdates.position || el.position, + size: positionSizeUpdates.size || el.size, + } + : el, ); // 서브 그리드 크기 계산 (cellSize / 3) @@ -85,7 +120,7 @@ export const DashboardCanvas = forwardRef( } }); }, - [elements, onUpdateElement, cellSize, canvasWidth] + [elements, onUpdateElement, cellSize, canvasWidth], ); // 드래그 오버 처리 @@ -124,20 +159,17 @@ export const DashboardCanvas = forwardRef( const subGridSize = Math.floor(cellSize / 3); const gridSize = cellSize + 5; // GAP 포함한 실제 그리드 크기 const magneticThreshold = 15; - + // X 좌표 스냅 const nearestGridX = Math.round(rawX / gridSize) * gridSize; const distToGridX = Math.abs(rawX - nearestGridX); - let snappedX = distToGridX <= magneticThreshold - ? nearestGridX - : Math.round(rawX / subGridSize) * subGridSize; - + let snappedX = distToGridX <= magneticThreshold ? nearestGridX : Math.round(rawX / subGridSize) * subGridSize; + // Y 좌표 스냅 const nearestGridY = Math.round(rawY / gridSize) * gridSize; const distToGridY = Math.abs(rawY - nearestGridY); - const snappedY = distToGridY <= magneticThreshold - ? nearestGridY - : Math.round(rawY / subGridSize) * subGridSize; + const snappedY = + distToGridY <= magneticThreshold ? nearestGridY : Math.round(rawY / subGridSize) * subGridSize; // X 좌표가 캔버스 너비를 벗어나지 않도록 제한 const maxX = canvasWidth - cellSize * 2; // 최소 2칸 너비 보장 diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 6f127191..7f4dc32f 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -355,6 +355,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D dataSource: el.dataSource, chartConfig: el.chartConfig, listConfig: el.listConfig, + yardConfig: el.yardConfig, })); let savedDashboard; diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx index c36608ee..b9995e26 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DCanvas.tsx @@ -130,7 +130,14 @@ function MaterialBox({ const handlePointerDown = (e: any) => { e.stopPropagation(); - if (isSelected && onDrag && meshRef.current) { + + // 뷰어 모드(onDrag 없음)에서는 클릭만 처리 + if (!onDrag) { + return; + } + + // 편집 모드에서 선택되었고 드래그 가능한 경우 + if (isSelected && meshRef.current) { // 드래그 시작 시점의 자재 위치 저장 (숫자로 변환) dragStartPos.current = { x: Number(placement.position_x), @@ -161,11 +168,17 @@ function MaterialBox({ e.stopPropagation(); e.nativeEvent?.stopPropagation(); e.nativeEvent?.stopImmediatePropagation(); + console.log("3D Box clicked:", placement.material_name); onClick(); }} onPointerDown={handlePointerDown} onPointerOver={() => { - gl.domElement.style.cursor = isSelected ? "grab" : "pointer"; + // 뷰어 모드(onDrag 없음)에서는 기본 커서, 편집 모드에서는 grab 커서 + if (onDrag) { + gl.domElement.style.cursor = isSelected ? "grab" : "pointer"; + } else { + gl.domElement.style.cursor = "pointer"; + } }} onPointerOut={() => { if (!isDragging) { diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx index 2955ef6d..1e4b133b 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/Yard3DViewer.tsx @@ -34,6 +34,17 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + // 선택 변경 로그 + const handlePlacementClick = (placement: YardPlacement | null) => { + console.log("Yard3DViewer - Placement clicked:", placement?.material_name); + setSelectedPlacement(placement); + }; + + // 선택 상태 변경 감지 + useEffect(() => { + console.log("selectedPlacement changed:", selectedPlacement?.material_name); + }, [selectedPlacement]); + // 배치 데이터 로드 useEffect(() => { const loadPlacements = async () => { @@ -91,62 +102,42 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) { } return ( -
+
{/* 3D 캔버스 */} -
- -
+ - {/* 선택된 자재 정보 패널 (우측) */} + {/* 선택된 자재 정보 패널 (오버레이) */} {selectedPlacement && ( -
-
-

자재 정보

+
+
+

자재 정보

+
-
+
- -
{selectedPlacement.material_code}
+ +
{selectedPlacement.material_name}
- -
{selectedPlacement.material_name}
-
- -
- -
+ +
{selectedPlacement.quantity} {selectedPlacement.unit}
- -
- -
- ({selectedPlacement.position_x.toFixed(1)}, {selectedPlacement.position_y.toFixed(1)},{" "} - {selectedPlacement.position_z.toFixed(1)}) -
-
- -
- -
- {selectedPlacement.size_x} × {selectedPlacement.size_y} × {selectedPlacement.size_z} -
-
- - {selectedPlacement.memo && ( -
- -
{selectedPlacement.memo}
-
- )}
)}