V2 컴포넌트 규칙 추가 및 기존 컴포넌트 자동 등록 개선: 화면 컴포넌트 개발 가이드에 V2 컴포넌트 사용 규칙을 명시하고, ComponentsPanel에서 수동으로 추가하던 table-list 컴포넌트를 자동 등록으로 변경하여 관리 효율성을 높였습니다. 또한, V2 컴포넌트 목록과 수정/개발 시 규칙을 추가하여 일관된 개발 환경을 조성하였습니다.
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Virtual Scroll 훅
|
||||
* 대용량 피벗 데이터의 가상 스크롤 처리
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
||||
|
||||
// ==================== 타입 ====================
|
||||
|
||||
export interface VirtualScrollOptions {
|
||||
itemCount: number; // 전체 아이템 수
|
||||
itemHeight: number; // 각 아이템 높이 (px)
|
||||
containerHeight: number; // 컨테이너 높이 (px)
|
||||
overscan?: number; // 버퍼 아이템 수 (기본: 5)
|
||||
}
|
||||
|
||||
export interface VirtualScrollResult {
|
||||
// 현재 보여야 할 아이템 범위
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
|
||||
// 가상 스크롤 관련 값
|
||||
totalHeight: number; // 전체 높이
|
||||
offsetTop: number; // 상단 오프셋
|
||||
|
||||
// 보여지는 아이템 목록
|
||||
visibleItems: number[];
|
||||
|
||||
// 이벤트 핸들러
|
||||
onScroll: (scrollTop: number) => void;
|
||||
|
||||
// 컨테이너 ref
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ==================== 훅 ====================
|
||||
|
||||
export function useVirtualScroll(options: VirtualScrollOptions): VirtualScrollResult {
|
||||
const {
|
||||
itemCount,
|
||||
itemHeight,
|
||||
containerHeight,
|
||||
overscan = 5,
|
||||
} = options;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
|
||||
// 보이는 아이템 수
|
||||
const visibleCount = Math.ceil(containerHeight / itemHeight);
|
||||
|
||||
// 시작/끝 인덱스 계산
|
||||
const { startIndex, endIndex } = useMemo(() => {
|
||||
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
|
||||
const end = Math.min(
|
||||
itemCount - 1,
|
||||
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
|
||||
);
|
||||
return { startIndex: start, endIndex: end };
|
||||
}, [scrollTop, itemHeight, containerHeight, itemCount, overscan]);
|
||||
|
||||
// 전체 높이
|
||||
const totalHeight = itemCount * itemHeight;
|
||||
|
||||
// 상단 오프셋
|
||||
const offsetTop = startIndex * itemHeight;
|
||||
|
||||
// 보이는 아이템 인덱스 배열
|
||||
const visibleItems = useMemo(() => {
|
||||
const items: number[] = [];
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
items.push(i);
|
||||
}
|
||||
return items;
|
||||
}, [startIndex, endIndex]);
|
||||
|
||||
// 스크롤 핸들러
|
||||
const onScroll = useCallback((newScrollTop: number) => {
|
||||
setScrollTop(newScrollTop);
|
||||
}, []);
|
||||
|
||||
// 스크롤 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
setScrollTop(container.scrollTop);
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalHeight,
|
||||
offsetTop,
|
||||
visibleItems,
|
||||
onScroll,
|
||||
containerRef,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 열 가상 스크롤 ====================
|
||||
|
||||
export interface VirtualColumnScrollOptions {
|
||||
columnCount: number; // 전체 열 수
|
||||
columnWidth: number; // 각 열 너비 (px)
|
||||
containerWidth: number; // 컨테이너 너비 (px)
|
||||
overscan?: number;
|
||||
}
|
||||
|
||||
export interface VirtualColumnScrollResult {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
totalWidth: number;
|
||||
offsetLeft: number;
|
||||
visibleColumns: number[];
|
||||
onScroll: (scrollLeft: number) => void;
|
||||
}
|
||||
|
||||
export function useVirtualColumnScroll(
|
||||
options: VirtualColumnScrollOptions
|
||||
): VirtualColumnScrollResult {
|
||||
const {
|
||||
columnCount,
|
||||
columnWidth,
|
||||
containerWidth,
|
||||
overscan = 3,
|
||||
} = options;
|
||||
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
|
||||
const { startIndex, endIndex } = useMemo(() => {
|
||||
const start = Math.max(0, Math.floor(scrollLeft / columnWidth) - overscan);
|
||||
const end = Math.min(
|
||||
columnCount - 1,
|
||||
Math.ceil((scrollLeft + containerWidth) / columnWidth) + overscan
|
||||
);
|
||||
return { startIndex: start, endIndex: end };
|
||||
}, [scrollLeft, columnWidth, containerWidth, columnCount, overscan]);
|
||||
|
||||
const totalWidth = columnCount * columnWidth;
|
||||
const offsetLeft = startIndex * columnWidth;
|
||||
|
||||
const visibleColumns = useMemo(() => {
|
||||
const cols: number[] = [];
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
cols.push(i);
|
||||
}
|
||||
return cols;
|
||||
}, [startIndex, endIndex]);
|
||||
|
||||
const onScroll = useCallback((newScrollLeft: number) => {
|
||||
setScrollLeft(newScrollLeft);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalWidth,
|
||||
offsetLeft,
|
||||
visibleColumns,
|
||||
onScroll,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 2D 가상 스크롤 (행 + 열) ====================
|
||||
|
||||
export interface Virtual2DScrollOptions {
|
||||
rowCount: number;
|
||||
columnCount: number;
|
||||
rowHeight: number;
|
||||
columnWidth: number;
|
||||
containerHeight: number;
|
||||
containerWidth: number;
|
||||
rowOverscan?: number;
|
||||
columnOverscan?: number;
|
||||
}
|
||||
|
||||
export interface Virtual2DScrollResult {
|
||||
// 행 범위
|
||||
rowStartIndex: number;
|
||||
rowEndIndex: number;
|
||||
totalHeight: number;
|
||||
offsetTop: number;
|
||||
visibleRows: number[];
|
||||
|
||||
// 열 범위
|
||||
columnStartIndex: number;
|
||||
columnEndIndex: number;
|
||||
totalWidth: number;
|
||||
offsetLeft: number;
|
||||
visibleColumns: number[];
|
||||
|
||||
// 스크롤 핸들러
|
||||
onScroll: (scrollTop: number, scrollLeft: number) => void;
|
||||
|
||||
// 컨테이너 ref
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function useVirtual2DScroll(
|
||||
options: Virtual2DScrollOptions
|
||||
): Virtual2DScrollResult {
|
||||
const {
|
||||
rowCount,
|
||||
columnCount,
|
||||
rowHeight,
|
||||
columnWidth,
|
||||
containerHeight,
|
||||
containerWidth,
|
||||
rowOverscan = 5,
|
||||
columnOverscan = 3,
|
||||
} = options;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
|
||||
// 행 계산
|
||||
const { rowStartIndex, rowEndIndex, visibleRows } = useMemo(() => {
|
||||
const start = Math.max(0, Math.floor(scrollTop / rowHeight) - rowOverscan);
|
||||
const end = Math.min(
|
||||
rowCount - 1,
|
||||
Math.ceil((scrollTop + containerHeight) / rowHeight) + rowOverscan
|
||||
);
|
||||
|
||||
const rows: number[] = [];
|
||||
for (let i = start; i <= end; i++) {
|
||||
rows.push(i);
|
||||
}
|
||||
|
||||
return {
|
||||
rowStartIndex: start,
|
||||
rowEndIndex: end,
|
||||
visibleRows: rows,
|
||||
};
|
||||
}, [scrollTop, rowHeight, containerHeight, rowCount, rowOverscan]);
|
||||
|
||||
// 열 계산
|
||||
const { columnStartIndex, columnEndIndex, visibleColumns } = useMemo(() => {
|
||||
const start = Math.max(0, Math.floor(scrollLeft / columnWidth) - columnOverscan);
|
||||
const end = Math.min(
|
||||
columnCount - 1,
|
||||
Math.ceil((scrollLeft + containerWidth) / columnWidth) + columnOverscan
|
||||
);
|
||||
|
||||
const cols: number[] = [];
|
||||
for (let i = start; i <= end; i++) {
|
||||
cols.push(i);
|
||||
}
|
||||
|
||||
return {
|
||||
columnStartIndex: start,
|
||||
columnEndIndex: end,
|
||||
visibleColumns: cols,
|
||||
};
|
||||
}, [scrollLeft, columnWidth, containerWidth, columnCount, columnOverscan]);
|
||||
|
||||
const totalHeight = rowCount * rowHeight;
|
||||
const totalWidth = columnCount * columnWidth;
|
||||
const offsetTop = rowStartIndex * rowHeight;
|
||||
const offsetLeft = columnStartIndex * columnWidth;
|
||||
|
||||
const onScroll = useCallback((newScrollTop: number, newScrollLeft: number) => {
|
||||
setScrollTop(newScrollTop);
|
||||
setScrollLeft(newScrollLeft);
|
||||
}, []);
|
||||
|
||||
// 스크롤 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
setScrollTop(container.scrollTop);
|
||||
setScrollLeft(container.scrollLeft);
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
rowStartIndex,
|
||||
rowEndIndex,
|
||||
totalHeight,
|
||||
offsetTop,
|
||||
visibleRows,
|
||||
columnStartIndex,
|
||||
columnEndIndex,
|
||||
totalWidth,
|
||||
offsetLeft,
|
||||
visibleColumns,
|
||||
onScroll,
|
||||
containerRef,
|
||||
};
|
||||
}
|
||||
|
||||
export default useVirtualScroll;
|
||||
|
||||
Reference in New Issue
Block a user