229 lines
5.5 KiB
Markdown
229 lines
5.5 KiB
Markdown
|
|
# 대시보드 그리드 시스템
|
||
|
|
|
||
|
|
## 개요
|
||
|
|
|
||
|
|
대시보드 캔버스는 **12 컬럼 그리드 시스템**을 사용하여 요소를 정렬하고 배치합니다.
|
||
|
|
모든 요소는 드래그 또는 리사이즈 종료 시 자동으로 그리드에 스냅됩니다.
|
||
|
|
|
||
|
|
## 그리드 설정
|
||
|
|
|
||
|
|
### 기본 설정 (`gridUtils.ts`)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
GRID_CONFIG = {
|
||
|
|
COLUMNS: 12, // 12 컬럼
|
||
|
|
CELL_SIZE: 60, // 60px x 60px 정사각형 셀
|
||
|
|
GAP: 8, // 셀 간격 8px
|
||
|
|
SNAP_THRESHOLD: 15, // 스냅 임계값 15px
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 실제 그리드 크기
|
||
|
|
|
||
|
|
- **셀 크기 (gap 포함)**: 68px (60px + 8px)
|
||
|
|
- **전체 캔버스 너비**: 808px (12 \* 68px - 8px)
|
||
|
|
- **셀 비율**: 1:1 (정사각형)
|
||
|
|
|
||
|
|
## 스냅 기능
|
||
|
|
|
||
|
|
### 1. 위치 스냅
|
||
|
|
|
||
|
|
요소를 드래그하여 이동할 때:
|
||
|
|
|
||
|
|
- **드래그 중**: 자유롭게 이동 (그리드 무시)
|
||
|
|
- **드래그 종료**: 가장 가까운 그리드 포인트에 자동 스냅
|
||
|
|
- **스냅 계산**: `Math.round(value / 68) * 68`
|
||
|
|
|
||
|
|
### 2. 크기 스냅
|
||
|
|
|
||
|
|
요소의 크기를 조절할 때:
|
||
|
|
|
||
|
|
- **리사이즈 중**: 자유롭게 크기 조절
|
||
|
|
- **리사이즈 종료**: 그리드 단위로 스냅
|
||
|
|
- **최소 크기**: 2셀 x 2셀 (136px x 136px)
|
||
|
|
|
||
|
|
### 3. 드롭 스냅
|
||
|
|
|
||
|
|
사이드바에서 새 요소를 드래그 앤 드롭할 때:
|
||
|
|
|
||
|
|
- 드롭 위치가 자동으로 가장 가까운 그리드 포인트에 스냅
|
||
|
|
- 기본 크기:
|
||
|
|
- 차트: 4 x 3 셀 (264px x 196px)
|
||
|
|
- 위젯: 2 x 2 셀 (136px x 136px)
|
||
|
|
|
||
|
|
## 시각적 피드백
|
||
|
|
|
||
|
|
### 그리드 배경
|
||
|
|
|
||
|
|
캔버스 배경에 그리드 라인이 표시됩니다:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
backgroundImage: `
|
||
|
|
linear-gradient(rgba(59, 130, 246, 0.1) 1px, transparent 1px),
|
||
|
|
linear-gradient(90deg, rgba(59, 130, 246, 0.1) 1px, transparent 1px)
|
||
|
|
`,
|
||
|
|
backgroundSize: '68px 68px'
|
||
|
|
```
|
||
|
|
|
||
|
|
### 요소 테두리
|
||
|
|
|
||
|
|
- **선택 안 됨**: 회색 테두리
|
||
|
|
- **선택됨**: 파란색 테두리 + 리사이즈 핸들 표시
|
||
|
|
- **드래그/리사이즈 중**: 트랜지션 비활성화 (부드러운 움직임)
|
||
|
|
|
||
|
|
## 사용 예시
|
||
|
|
|
||
|
|
### 기본 사용
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { snapToGrid, snapSizeToGrid } from "./gridUtils";
|
||
|
|
|
||
|
|
// 위치 스냅
|
||
|
|
const snappedX = snapToGrid(123); // 136 (가장 가까운 그리드)
|
||
|
|
const snappedY = snapToGrid(45); // 68
|
||
|
|
|
||
|
|
// 크기 스냅
|
||
|
|
const snappedWidth = snapSizeToGrid(250); // 264 (4셀)
|
||
|
|
const snappedHeight = snapSizeToGrid(180); // 196 (3셀)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 경계 체크와 함께
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { snapBoundsToGrid } from "./gridUtils";
|
||
|
|
|
||
|
|
const snapped = snapBoundsToGrid(
|
||
|
|
{
|
||
|
|
position: { x: 123, y: 45 },
|
||
|
|
size: { width: 250, height: 180 },
|
||
|
|
},
|
||
|
|
canvasWidth,
|
||
|
|
canvasHeight,
|
||
|
|
);
|
||
|
|
|
||
|
|
// 결과:
|
||
|
|
// {
|
||
|
|
// position: { x: 136, y: 68 },
|
||
|
|
// size: { width: 264, height: 196 }
|
||
|
|
// }
|
||
|
|
```
|
||
|
|
|
||
|
|
## 그리드 인덱스
|
||
|
|
|
||
|
|
### 좌표 → 인덱스
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { getGridIndex } from "./gridUtils";
|
||
|
|
|
||
|
|
const colIndex = getGridIndex(150); // 2 (3번째 컬럼)
|
||
|
|
const rowIndex = getGridIndex(100); // 1 (2번째 행)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 인덱스 → 좌표
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { gridIndexToCoordinate } from "./gridUtils";
|
||
|
|
|
||
|
|
const x = gridIndexToCoordinate(0); // 0 (1번째 컬럼)
|
||
|
|
const y = gridIndexToCoordinate(1); // 68 (2번째 행)
|
||
|
|
const z = gridIndexToCoordinate(11); // 748 (12번째 컬럼)
|
||
|
|
```
|
||
|
|
|
||
|
|
## 레이아웃 권장사항
|
||
|
|
|
||
|
|
### 일반 차트
|
||
|
|
|
||
|
|
- **권장 크기**: 4 x 3 셀 (264px x 196px)
|
||
|
|
- **최소 크기**: 2 x 2 셀 (136px x 136px)
|
||
|
|
- **최대 크기**: 12 x 8 셀 (808px x 536px)
|
||
|
|
|
||
|
|
### 작은 위젯
|
||
|
|
|
||
|
|
- **권장 크기**: 2 x 2 셀 (136px x 136px)
|
||
|
|
- **최소 크기**: 2 x 2 셀
|
||
|
|
- **최대 크기**: 4 x 4 셀 (264px x 264px)
|
||
|
|
|
||
|
|
### 큰 차트/대시보드
|
||
|
|
|
||
|
|
- **권장 크기**: 6 x 4 셀 (400px x 264px)
|
||
|
|
- **풀 너비**: 12 셀 (808px)
|
||
|
|
|
||
|
|
## 커스터마이징
|
||
|
|
|
||
|
|
### 그리드 크기 변경
|
||
|
|
|
||
|
|
`gridUtils.ts`의 `GRID_CONFIG`를 수정:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export const GRID_CONFIG = {
|
||
|
|
COLUMNS: 12,
|
||
|
|
CELL_SIZE: 80, // 60 → 80 (셀 크기 증가)
|
||
|
|
GAP: 16, // 8 → 16 (간격 증가)
|
||
|
|
SNAP_THRESHOLD: 20, // 15 → 20 (스냅 범위 증가)
|
||
|
|
} as const;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 스냅 비활성화
|
||
|
|
|
||
|
|
특정 요소에서 스냅을 비활성화하려면:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 드래그 종료 시 스냅하지 않고 그냥 업데이트
|
||
|
|
onUpdate(element.id, {
|
||
|
|
position: { x: rawX, y: rawY }, // snapToGrid 호출 안 함
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## 성능 최적화
|
||
|
|
|
||
|
|
### 트랜지션 제어
|
||
|
|
|
||
|
|
드래그/리사이즈 중에는 CSS 트랜지션을 비활성화:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
className={`
|
||
|
|
${(isDragging || isResizing) ? 'transition-none' : 'transition-all duration-150'}
|
||
|
|
`}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 임시 상태 사용
|
||
|
|
|
||
|
|
마우스 이동 중에는 임시 위치/크기만 업데이트하고,
|
||
|
|
마우스 업 시에만 실제 스냅된 값으로 업데이트:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 드래그 중
|
||
|
|
setTempPosition({ x: rawX, y: rawY });
|
||
|
|
|
||
|
|
// 드래그 종료
|
||
|
|
const snapped = snapToGrid(tempPosition.x);
|
||
|
|
onUpdate(element.id, { position: { x: snapped, y: snapped } });
|
||
|
|
```
|
||
|
|
|
||
|
|
## 문제 해결
|
||
|
|
|
||
|
|
### 요소가 스냅되지 않는 경우
|
||
|
|
|
||
|
|
1. `snapToGrid` 함수가 호출되는지 확인
|
||
|
|
2. `SNAP_THRESHOLD` 값 확인 (너무 작으면 스냅 안 됨)
|
||
|
|
3. 임시 상태가 제대로 초기화되는지 확인
|
||
|
|
|
||
|
|
### 그리드가 보이지 않는 경우
|
||
|
|
|
||
|
|
1. 캔버스의 `backgroundImage` 스타일 확인
|
||
|
|
2. `getCellWithGap()` 반환값 확인
|
||
|
|
3. 브라우저 개발자 도구에서 배경 스타일 검사
|
||
|
|
|
||
|
|
### 성능 문제
|
||
|
|
|
||
|
|
1. 트랜지션이 비활성화되었는지 확인
|
||
|
|
2. 불필요한 리렌더링 방지 (React.memo 사용)
|
||
|
|
3. 마우스 이벤트 리스너가 제대로 제거되는지 확인
|
||
|
|
|
||
|
|
## 참고 자료
|
||
|
|
|
||
|
|
- **그리드 유틸리티**: `gridUtils.ts`
|
||
|
|
- **캔버스 컴포넌트**: `DashboardCanvas.tsx`
|
||
|
|
- **요소 컴포넌트**: `CanvasElement.tsx`
|
||
|
|
- **디자이너**: `DashboardDesigner.tsx`
|