Merge pull request '대시보드 뷰어에 반응형 적용' (#116) from feat/dashboard into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/116
This commit is contained in:
commit
45d00e10e7
|
|
@ -161,13 +161,24 @@ interface DashboardViewerProps {
|
|||
*/
|
||||
export function DashboardViewer({
|
||||
elements,
|
||||
dashboardId,
|
||||
refreshInterval,
|
||||
backgroundColor = "#f9fafb",
|
||||
resolution = "fhd",
|
||||
}: DashboardViewerProps) {
|
||||
const [elementData, setElementData] = useState<Record<string, QueryResult>>({});
|
||||
const [loadingElements, setLoadingElements] = useState<Set<string>>(new Set());
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
// 화면 크기 감지
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 1024); // 1024px (lg) 미만은 모바일/태블릿
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
// 캔버스 설정 계산
|
||||
const canvasConfig = useMemo(() => {
|
||||
|
|
@ -269,6 +280,21 @@ export function DashboardViewer({
|
|||
return () => clearInterval(interval);
|
||||
}, [refreshInterval, loadAllData]);
|
||||
|
||||
// 모바일에서 요소를 자연스러운 읽기 순서로 정렬 (왼쪽→오른쪽, 위→아래)
|
||||
const sortedElements = useMemo(() => {
|
||||
if (!isMobile) return elements;
|
||||
|
||||
return [...elements].sort((a, b) => {
|
||||
// Y 좌표 차이가 50px 이상이면 Y 우선 (같은 행으로 간주 안함)
|
||||
const yDiff = a.position.y - b.position.y;
|
||||
if (Math.abs(yDiff) > 50) {
|
||||
return yDiff;
|
||||
}
|
||||
// 같은 행이면 X 좌표로 정렬
|
||||
return a.position.x - b.position.x;
|
||||
});
|
||||
}, [elements, isMobile]);
|
||||
|
||||
// 요소가 없는 경우
|
||||
if (elements.length === 0) {
|
||||
return (
|
||||
|
|
@ -284,30 +310,47 @@ export function DashboardViewer({
|
|||
|
||||
return (
|
||||
<DashboardProvider>
|
||||
{/* 스크롤 가능한 컨테이너 */}
|
||||
<div className="flex min-h-screen items-start justify-center bg-gray-100 p-8">
|
||||
{/* 고정 크기 캔버스 (편집 화면과 동일한 레이아웃) */}
|
||||
<div
|
||||
className="relative rounded-lg"
|
||||
style={{
|
||||
width: `${canvasConfig.width}px`,
|
||||
minHeight: `${canvasConfig.height}px`,
|
||||
height: `${canvasHeight}px`,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
>
|
||||
{/* 대시보드 요소들 */}
|
||||
{elements.map((element) => (
|
||||
<ViewerElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
data={elementData[element.id]}
|
||||
isLoading={loadingElements.has(element.id)}
|
||||
onRefresh={() => loadElementData(element)}
|
||||
/>
|
||||
))}
|
||||
{isMobile ? (
|
||||
// 모바일/태블릿: 세로 스택 레이아웃
|
||||
<div className="min-h-screen bg-gray-100 p-4" style={{ backgroundColor }}>
|
||||
<div className="mx-auto max-w-3xl space-y-4">
|
||||
{sortedElements.map((element) => (
|
||||
<ViewerElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
data={elementData[element.id]}
|
||||
isLoading={loadingElements.has(element.id)}
|
||||
onRefresh={() => loadElementData(element)}
|
||||
isMobile={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 데스크톱: 기존 고정 캔버스 레이아웃
|
||||
<div className="flex min-h-screen items-start justify-center bg-gray-100 p-8">
|
||||
<div
|
||||
className="relative rounded-lg"
|
||||
style={{
|
||||
width: `${canvasConfig.width}px`,
|
||||
minHeight: `${canvasConfig.height}px`,
|
||||
height: `${canvasHeight}px`,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
>
|
||||
{sortedElements.map((element) => (
|
||||
<ViewerElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
data={elementData[element.id]}
|
||||
isLoading={loadingElements.has(element.id)}
|
||||
onRefresh={() => loadElementData(element)}
|
||||
isMobile={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DashboardProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -317,14 +360,61 @@ interface ViewerElementProps {
|
|||
data?: QueryResult;
|
||||
isLoading: boolean;
|
||||
onRefresh: () => void;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 뷰어 요소 컴포넌트
|
||||
*/
|
||||
function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementProps) {
|
||||
function ViewerElement({ element, data, isLoading, onRefresh, isMobile }: ViewerElementProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
if (isMobile) {
|
||||
// 모바일/태블릿: 세로 스택 카드 스타일
|
||||
return (
|
||||
<div
|
||||
className="relative overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm"
|
||||
style={{ minHeight: "300px" }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{element.showHeader !== false && (
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<h3 className="text-sm font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className="text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
title="새로고침"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border border-gray-400 border-t-transparent" />
|
||||
) : (
|
||||
"🔄"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className={element.showHeader !== false ? "p-4" : "p-4"} style={{ minHeight: "250px" }}>
|
||||
{element.type === "chart" ? (
|
||||
<ChartRenderer element={element} data={data} width={undefined} height={250} />
|
||||
) : (
|
||||
renderWidget(element)
|
||||
)}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-white">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<div className="text-sm text-gray-600">업데이트 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 데스크톱: 기존 absolute positioning
|
||||
return (
|
||||
<div
|
||||
className="absolute overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm"
|
||||
|
|
@ -337,16 +427,13 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
|||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* 헤더 (showHeader가 false가 아닐 때만 표시) */}
|
||||
{element.showHeader !== false && (
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<h3 className="text-sm font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
||||
|
||||
{/* 새로고침 버튼 (항상 렌더링하되 opacity로 제어) */}
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className={`hover:text-muted-foreground text-gray-400 transition-opacity disabled:opacity-50 ${
|
||||
className={`text-gray-400 transition-opacity hover:text-gray-600 disabled:opacity-50 ${
|
||||
isHovered ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
title="새로고침"
|
||||
|
|
@ -359,8 +446,6 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 내용 */}
|
||||
<div className={element.showHeader !== false ? "h-[calc(100%-57px)]" : "h-full"}>
|
||||
{element.type === "chart" ? (
|
||||
<ChartRenderer element={element} data={data} width={element.size.width} height={element.size.height - 57} />
|
||||
|
|
@ -368,13 +453,11 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
|||
renderWidget(element)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 로딩 오버레이 */}
|
||||
{isLoading && (
|
||||
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-white">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<div className="text-muted-foreground text-sm">업데이트 중...</div>
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<div className="text-sm text-gray-600">업데이트 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue