캔버스 너비 초과하는 배치 막음

This commit is contained in:
dohyeons 2025-10-14 13:20:17 +09:00
parent ce599fab22
commit 5cd5ad6c49
2 changed files with 31 additions and 81 deletions

View File

@ -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({
) : (
<ChartRenderer
element={element}
data={chartData}
data={chartData || undefined}
width={element.size.width}
height={element.size.height - 45}
/>
@ -381,16 +396,12 @@ export function CanvasElement({
) : element.type === "widget" && element.subtype === "weather" ? (
// 날씨 위젯 렌더링
<div className="widget-interactive-area h-full w-full">
<WeatherWidget city={element.config?.city || "서울"} refreshInterval={600000} />
<WeatherWidget city="서울" refreshInterval={600000} />
</div>
) : element.type === "widget" && element.subtype === "exchange" ? (
// 환율 위젯 렌더링
<div className="widget-interactive-area h-full w-full">
<ExchangeWidget
baseCurrency={element.config?.baseCurrency || "KRW"}
targetCurrency={element.config?.targetCurrency || "USD"}
refreshInterval={600000}
/>
<ExchangeWidget baseCurrency="KRW" targetCurrency="USD" refreshInterval={600000} />
</div>
) : 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<string, any>[];
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
};
}

View File

@ -33,7 +33,7 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
onRemoveElement,
onSelectElement,
onConfigureElement,
backgroundColor = '#f9fafb',
backgroundColor = "#f9fafb",
},
ref,
) => {
@ -72,9 +72,13 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
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);