From 5cd5ad6c49ded17a39f19d018e14a59d8c8c4f10 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 13:20:17 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=20=EB=84=88?= =?UTF-8?q?=EB=B9=84=20=EC=B4=88=EA=B3=BC=ED=95=98=EB=8A=94=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EB=A7=89=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 104 +++++------------- .../admin/dashboard/DashboardCanvas.tsx | 8 +- 2 files changed, 31 insertions(+), 81 deletions(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 3decd573..43ce5163 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -131,9 +131,13 @@ export function CanvasElement({ const deltaY = e.clientY - dragStart.y; // 임시 위치 계산 (스냅 안 됨) - const rawX = Math.max(0, dragStart.elementX + deltaX); + let rawX = Math.max(0, dragStart.elementX + deltaX); const rawY = Math.max(0, dragStart.elementY + deltaY); + // X 좌표가 캔버스 너비를 벗어나지 않도록 제한 + const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + rawX = Math.min(rawX, maxX); + setTempPosition({ x: rawX, y: rawY }); } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; @@ -173,21 +177,29 @@ export function CanvasElement({ break; } + // 가로 너비가 캔버스를 벗어나지 않도록 제한 + const maxWidth = GRID_CONFIG.CANVAS_WIDTH - newX; + newWidth = Math.min(newWidth, maxWidth); + // 임시 크기/위치 저장 (스냅 안 됨) setTempPosition({ x: Math.max(0, newX), y: Math.max(0, newY) }); setTempSize({ width: newWidth, height: newHeight }); } }, - [isDragging, isResizing, dragStart, resizeStart], + [isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype], ); // 마우스 업 처리 (그리드 스냅 적용) const handleMouseUp = useCallback(() => { if (isDragging && tempPosition) { // 드래그 종료 시 그리드에 스냅 (동적 셀 크기 사용) - const snappedX = snapToGrid(tempPosition.x, cellSize); + let snappedX = snapToGrid(tempPosition.x, cellSize); const snappedY = snapToGrid(tempPosition.y, cellSize); + // X 좌표가 캔버스 너비를 벗어나지 않도록 최종 제한 + const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + snappedX = Math.min(snappedX, maxX); + onUpdate(element.id, { position: { x: snappedX, y: snappedY }, }); @@ -199,9 +211,13 @@ export function CanvasElement({ // 리사이즈 종료 시 그리드에 스냅 (동적 셀 크기 사용) const snappedX = snapToGrid(tempPosition.x, cellSize); const snappedY = snapToGrid(tempPosition.y, cellSize); - const snappedWidth = snapSizeToGrid(tempSize.width, 2, cellSize); + let snappedWidth = snapSizeToGrid(tempSize.width, 2, cellSize); const snappedHeight = snapSizeToGrid(tempSize.height, 2, cellSize); + // 가로 너비가 캔버스를 벗어나지 않도록 최종 제한 + const maxWidth = GRID_CONFIG.CANVAS_WIDTH - snappedX; + snappedWidth = Math.min(snappedWidth, maxWidth); + onUpdate(element.id, { position: { x: snappedX, y: snappedY }, size: { width: snappedWidth, height: snappedHeight }, @@ -213,7 +229,7 @@ export function CanvasElement({ setIsDragging(false); setIsResizing(false); - }, [isDragging, isResizing, tempPosition, tempSize, element.id, onUpdate, cellSize]); + }, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize]); // 전역 마우스 이벤트 등록 React.useEffect(() => { @@ -251,12 +267,11 @@ export function CanvasElement({ executionTime: 0, }); } catch (error) { - // console.error('❌ 데이터 로딩 오류:', error); setChartData(null); } finally { setIsLoadingData(false); } - }, [element.dataSource?.query, element.type, element.subtype]); + }, [element.dataSource?.query, element.type]); // 컴포넌트 마운트 시 및 쿼리 변경 시 데이터 로딩 useEffect(() => { @@ -372,7 +387,7 @@ export function CanvasElement({ ) : ( @@ -381,16 +396,12 @@ export function CanvasElement({ ) : element.type === "widget" && element.subtype === "weather" ? ( // 날씨 위젯 렌더링
- +
) : element.type === "widget" && element.subtype === "exchange" ? ( // 환율 위젯 렌더링
- +
) : element.type === "widget" && element.subtype === "clock" ? ( // 시계 위젯 렌더링 @@ -487,68 +498,3 @@ function ResizeHandle({ position, onMouseDown }: ResizeHandleProps) { /> ); } - -/** - * 샘플 데이터 생성 함수 (실제 API 호출 대신 사용) - */ -function generateSampleData(query: string, chartType: string): QueryResult { - // 쿼리에서 키워드 추출하여 적절한 샘플 데이터 생성 - const isMonthly = query.toLowerCase().includes("month"); - const isSales = query.toLowerCase().includes("sales") || query.toLowerCase().includes("매출"); - const isUsers = query.toLowerCase().includes("users") || query.toLowerCase().includes("사용자"); - const isProducts = query.toLowerCase().includes("product") || query.toLowerCase().includes("상품"); - - let columns: string[]; - let rows: Record[]; - - if (isMonthly && isSales) { - // 월별 매출 데이터 - columns = ["month", "sales", "order_count"]; - rows = [ - { month: "2024-01", sales: 1200000, order_count: 45 }, - { month: "2024-02", sales: 1350000, order_count: 52 }, - { month: "2024-03", sales: 1180000, order_count: 41 }, - { month: "2024-04", sales: 1420000, order_count: 58 }, - { month: "2024-05", sales: 1680000, order_count: 67 }, - { month: "2024-06", sales: 1540000, order_count: 61 }, - ]; - } else if (isUsers) { - // 사용자 가입 추이 - columns = ["week", "new_users"]; - rows = [ - { week: "2024-W10", new_users: 23 }, - { week: "2024-W11", new_users: 31 }, - { week: "2024-W12", new_users: 28 }, - { week: "2024-W13", new_users: 35 }, - { week: "2024-W14", new_users: 42 }, - { week: "2024-W15", new_users: 38 }, - ]; - } else if (isProducts) { - // 상품별 판매량 - columns = ["product_name", "total_sold", "revenue"]; - rows = [ - { product_name: "스마트폰", total_sold: 156, revenue: 234000000 }, - { product_name: "노트북", total_sold: 89, revenue: 178000000 }, - { product_name: "태블릿", total_sold: 134, revenue: 67000000 }, - { product_name: "이어폰", total_sold: 267, revenue: 26700000 }, - { product_name: "스마트워치", total_sold: 98, revenue: 49000000 }, - ]; - } else { - // 기본 샘플 데이터 - columns = ["category", "value", "count"]; - rows = [ - { category: "A", value: 100, count: 10 }, - { category: "B", value: 150, count: 15 }, - { category: "C", value: 120, count: 12 }, - { category: "D", value: 180, count: 18 }, - { category: "E", value: 90, count: 9 }, - ]; - } - - return { - columns, - rows, - totalRows: rows.length, - executionTime: Math.floor(Math.random() * 100) + 50, // 50-150ms - }; -} diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx index d8b7007e..1a4ec333 100644 --- a/frontend/components/admin/dashboard/DashboardCanvas.tsx +++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx @@ -33,7 +33,7 @@ export const DashboardCanvas = forwardRef( onRemoveElement, onSelectElement, onConfigureElement, - backgroundColor = '#f9fafb', + backgroundColor = "#f9fafb", }, ref, ) => { @@ -72,9 +72,13 @@ export const DashboardCanvas = forwardRef( const rawY = e.clientY - rect.top + (ref.current?.scrollTop || 0); // 그리드에 스냅 (고정 셀 크기 사용) - const snappedX = snapToGrid(rawX, GRID_CONFIG.CELL_SIZE); + let snappedX = snapToGrid(rawX, GRID_CONFIG.CELL_SIZE); const snappedY = snapToGrid(rawY, GRID_CONFIG.CELL_SIZE); + // X 좌표가 캔버스 너비를 벗어나지 않도록 제한 + const maxX = GRID_CONFIG.CANVAS_WIDTH - GRID_CONFIG.CELL_SIZE * 2; // 최소 2칸 너비 보장 + snappedX = Math.max(0, Math.min(snappedX, maxX)); + onCreateElement(dragData.type, dragData.subtype, snappedX, snappedY); } catch (error) { // console.error('드롭 데이터 파싱 오류:', error);