원본승격 완료, 차트 위젯은 보류
This commit is contained in:
@@ -152,7 +152,7 @@ import { ClockWidget } from "./widgets/ClockWidget";
|
||||
import { CalendarWidget } from "./widgets/CalendarWidget";
|
||||
// 기사 관리 위젯 임포트
|
||||
import { DriverManagementWidget } from "./widgets/DriverManagementWidget";
|
||||
import { ListWidget } from "./widgets/ListWidget";
|
||||
// import { ListWidget } from "./widgets/ListWidget"; // (구버전 - 주석 처리: 2025-10-28, list-v2로 대체)
|
||||
import { X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -892,28 +892,28 @@ export function CanvasElement({
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<MapTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "map-test-v2" ? (
|
||||
// 🧪 테스트용 지도 위젯 V2 (다중 데이터 소스)
|
||||
) : element.type === "widget" && element.subtype === "map-summary-v2" ? (
|
||||
// 지도 위젯 (다중 데이터 소스) - 승격 완료
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<MapTestWidgetV2 element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "chart-test" ? (
|
||||
// 🧪 테스트용 차트 위젯 (다중 데이터 소스)
|
||||
) : element.type === "widget" && element.subtype === "chart" ? (
|
||||
// 차트 위젯 (다중 데이터 소스) - 승격 완료
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<ChartTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "list-test" ? (
|
||||
// 🧪 테스트용 리스트 위젯 (다중 데이터 소스)
|
||||
) : element.type === "widget" && element.subtype === "list-v2" ? (
|
||||
// 리스트 위젯 (다중 데이터 소스) - 승격 완료
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<ListTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "custom-metric-test" ? (
|
||||
// 🧪 통계 카드 (다중 데이터 소스)
|
||||
) : element.type === "widget" && element.subtype === "custom-metric-v2" ? (
|
||||
// 통계 카드 위젯 (다중 데이터 소스) - 승격 완료
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<CustomMetricTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "risk-alert-test" ? (
|
||||
// 🧪 테스트용 리스크/알림 위젯 (다중 데이터 소스)
|
||||
) : element.type === "widget" && element.subtype === "risk-alert-v2" ? (
|
||||
// 리스크/알림 위젯 (다중 데이터 소스) - 승격 완료
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<RiskAlertTestWidget element={element} />
|
||||
</div>
|
||||
@@ -1013,11 +1013,11 @@ export function CanvasElement({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "list" ? (
|
||||
// 리스트 위젯 렌더링
|
||||
<div className="h-full w-full">
|
||||
<ListWidget element={element} />
|
||||
</div>
|
||||
// ) : element.type === "widget" && element.subtype === "list" ? (
|
||||
// // 리스트 위젯 렌더링 (구버전 - 주석 처리: 2025-10-28, list-v2로 대체)
|
||||
// <div className="h-full w-full">
|
||||
// <ListWidget element={element} />
|
||||
// </div>
|
||||
) : element.type === "widget" && element.subtype === "yard-management-3d" ? (
|
||||
// 야드 관리 3D 위젯 렌더링
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
|
||||
@@ -181,23 +181,15 @@ export function DashboardTopMenu({
|
||||
<SelectValue placeholder="위젯 추가" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[99999]">
|
||||
<SelectGroup>
|
||||
<SelectLabel>🧪 테스트 위젯 (다중 데이터 소스)</SelectLabel>
|
||||
<SelectItem value="map-test-v2">🧪 지도 테스트 V2</SelectItem>
|
||||
<SelectItem value="chart-test">🧪 차트 테스트</SelectItem>
|
||||
<SelectItem value="list-test">🧪 리스트 테스트</SelectItem>
|
||||
<SelectItem value="custom-metric-test">통계 카드</SelectItem>
|
||||
{/* <SelectItem value="status-summary-test">🧪 상태 요약 테스트</SelectItem> */}
|
||||
<SelectItem value="risk-alert-test">🧪 리스크/알림 테스트</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>데이터 위젯</SelectLabel>
|
||||
<SelectItem value="list">리스트 위젯</SelectItem>
|
||||
<SelectItem value="custom-metric">사용자 커스텀 카드</SelectItem>
|
||||
<SelectItem value="map-summary-v2">지도</SelectItem>
|
||||
{/* <SelectItem value="chart">차트</SelectItem> */}
|
||||
<SelectItem value="list-v2">리스트</SelectItem>
|
||||
<SelectItem value="custom-metric-v2">통계 카드</SelectItem>
|
||||
<SelectItem value="risk-alert-v2">리스크/알림</SelectItem>
|
||||
<SelectItem value="yard-management-3d">야드 관리 3D</SelectItem>
|
||||
{/* <SelectItem value="transport-stats">커스텀 통계 카드</SelectItem> */}
|
||||
<SelectItem value="map-summary">커스텀 지도 카드</SelectItem>
|
||||
{/* <SelectItem value="map-test">🧪 지도 테스트 (REST API)</SelectItem> */}
|
||||
{/* <SelectItem value="status-summary">커스텀 상태 카드</SelectItem> */}
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
@@ -211,7 +203,7 @@ export function DashboardTopMenu({
|
||||
<SelectItem value="todo">일정관리 위젯</SelectItem>
|
||||
{/* <SelectItem value="booking-alert">예약 알림</SelectItem> */}
|
||||
<SelectItem value="document">문서</SelectItem>
|
||||
<SelectItem value="risk-alert">리스크 알림</SelectItem>
|
||||
{/* <SelectItem value="risk-alert">리스크 알림</SelectItem> */}
|
||||
</SelectGroup>
|
||||
{/* 범용 위젯으로 대체 가능하여 주석처리 */}
|
||||
{/* <SelectGroup>
|
||||
|
||||
@@ -154,11 +154,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
|
||||
// 다중 데이터 소스 위젯 체크
|
||||
const isMultiDS =
|
||||
element.subtype === "map-test-v2" ||
|
||||
element.subtype === "chart-test" ||
|
||||
element.subtype === "list-test" ||
|
||||
element.subtype === "custom-metric-test" ||
|
||||
element.subtype === "risk-alert-test";
|
||||
element.subtype === "map-summary-v2" ||
|
||||
element.subtype === "chart" ||
|
||||
element.subtype === "list-v2" ||
|
||||
element.subtype === "custom-metric-v2" ||
|
||||
element.subtype === "risk-alert-v2";
|
||||
|
||||
const updatedElement: DashboardElement = {
|
||||
...element,
|
||||
@@ -252,14 +252,14 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
element.type === "widget" &&
|
||||
(element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget);
|
||||
|
||||
// 다중 데이터 소스 테스트 위젯
|
||||
// 다중 데이터 소스 위젯
|
||||
const isMultiDataSourceWidget =
|
||||
element.subtype === "map-test-v2" ||
|
||||
element.subtype === "chart-test" ||
|
||||
element.subtype === "list-test" ||
|
||||
element.subtype === "custom-metric-test" ||
|
||||
element.subtype === "map-summary-v2" ||
|
||||
element.subtype === "chart" ||
|
||||
element.subtype === "list-v2" ||
|
||||
element.subtype === "custom-metric-v2" ||
|
||||
element.subtype === "status-summary-test" ||
|
||||
element.subtype === "risk-alert-test";
|
||||
element.subtype === "risk-alert-v2";
|
||||
|
||||
// 저장 가능 여부 확인
|
||||
const isPieChart = element.subtype === "pie" || element.subtype === "donut";
|
||||
@@ -370,8 +370,8 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 지도 테스트 V2: 타일맵 URL 설정 */}
|
||||
{element.subtype === "map-test-v2" && (
|
||||
{/* 지도 위젯: 타일맵 URL 설정 */}
|
||||
{element.subtype === "map-summary-v2" && (
|
||||
<div className="rounded-lg bg-white shadow-sm">
|
||||
<details className="group">
|
||||
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50">
|
||||
@@ -401,8 +401,8 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 차트 테스트: 차트 설정 */}
|
||||
{element.subtype === "chart-test" && (
|
||||
{/* 차트 위젯: 차트 설정 */}
|
||||
{element.subtype === "chart" && (
|
||||
<div className="rounded-lg bg-white shadow-sm">
|
||||
<details className="group" open>
|
||||
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50">
|
||||
|
||||
@@ -22,14 +22,19 @@ export type ElementSubtype =
|
||||
| "vehicle-status"
|
||||
| "vehicle-list" // (구버전 - 호환용)
|
||||
| "vehicle-map" // (구버전 - 호환용)
|
||||
| "map-summary" // 범용 지도 카드 (통합)
|
||||
// | "map-summary" // (구버전 - 주석 처리: 2025-10-28, map-summary-v2로 대체)
|
||||
// | "map-test" // 🧪 지도 테스트 위젯 (REST API 지원) - V2로 대체
|
||||
| "map-test-v2" // 🧪 지도 테스트 V2 (다중 데이터 소스)
|
||||
| "chart-test" // 🧪 차트 테스트 (다중 데이터 소스)
|
||||
| "list-test" // 🧪 리스트 테스트 (다중 데이터 소스)
|
||||
| "custom-metric-test" // 🧪 통계 카드 (다중 데이터 소스)
|
||||
| "map-summary-v2" // 지도 위젯 (다중 데이터 소스) - 승격 완료
|
||||
// | "map-test-v2" // (테스트 버전 - 주석 처리: 2025-10-28, map-summary-v2로 승격)
|
||||
| "chart" // 차트 위젯 (다중 데이터 소스) - 승격 완료
|
||||
// | "chart-test" // (테스트 버전 - 주석 처리: 2025-10-28, chart로 승격)
|
||||
| "list-v2" // 리스트 위젯 (다중 데이터 소스) - 승격 완료
|
||||
// | "list-test" // (테스트 버전 - 주석 처리: 2025-10-28, list-v2로 승격)
|
||||
| "custom-metric-v2" // 통계 카드 위젯 (다중 데이터 소스) - 승격 완료
|
||||
// | "custom-metric-test" // (테스트 버전 - 주석 처리: 2025-10-28, custom-metric-v2로 승격)
|
||||
// | "status-summary-test" // 🧪 상태 요약 테스트 (CustomMetricTest로 대체 가능)
|
||||
| "risk-alert-test" // 🧪 리스크/알림 테스트 (다중 데이터 소스)
|
||||
| "risk-alert-v2" // 리스크/알림 위젯 (다중 데이터 소스) - 승격 완료
|
||||
// | "risk-alert-test" // (테스트 버전 - 주석 처리: 2025-10-28, risk-alert-v2로 승격)
|
||||
| "delivery-status"
|
||||
| "status-summary" // 범용 상태 카드 (통합)
|
||||
// | "list-summary" // 범용 목록 카드 (다른 분 작업 중 - 임시 주석)
|
||||
@@ -37,17 +42,17 @@ export type ElementSubtype =
|
||||
| "delivery-today-stats" // (구버전 - 호환용)
|
||||
| "cargo-list" // (구버전 - 호환용)
|
||||
| "customer-issues" // (구버전 - 호환용)
|
||||
| "risk-alert"
|
||||
// | "risk-alert" // (구버전 - 주석 처리: 2025-10-28, risk-alert-v2로 대체)
|
||||
| "driver-management" // (구버전 - 호환용)
|
||||
| "todo"
|
||||
| "booking-alert"
|
||||
| "maintenance"
|
||||
| "document"
|
||||
| "list"
|
||||
// | "list" // (구버전 - 주석 처리: 2025-10-28, list-v2로 대체)
|
||||
| "yard-management-3d" // 야드 관리 3D 위젯
|
||||
| "work-history" // 작업 이력 위젯
|
||||
| "transport-stats" // 커스텀 통계 카드 위젯
|
||||
| "custom-metric"; // 사용자 커스텀 카드 위젯
|
||||
| "transport-stats"; // 커스텀 통계 카드 위젯
|
||||
// | "custom-metric"; // (구버전 - 주석 처리: 2025-10-28, custom-metric-v2로 대체)
|
||||
|
||||
// 차트 분류
|
||||
export type ChartCategory = "axis-based" | "circular";
|
||||
|
||||
@@ -1,340 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement, QueryResult, ListColumn } from "../types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface ListWidgetProps {
|
||||
element: DashboardElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스트 위젯 컴포넌트
|
||||
* - DB 쿼리 또는 REST API로 데이터 가져오기
|
||||
* - 테이블 형태로 데이터 표시
|
||||
* - 페이지네이션, 정렬, 검색 기능
|
||||
/*
|
||||
* ⚠️ DEPRECATED - 이 위젯은 더 이상 사용되지 않습니다.
|
||||
*
|
||||
* 이 파일은 2025-10-28에 주석 처리되었습니다.
|
||||
* 새로운 버전: ListTestWidget.tsx (subtype: list-v2)
|
||||
*
|
||||
* 변경 이유:
|
||||
* - 다중 데이터 소스 지원 (REST API + Database 혼합)
|
||||
* - 컬럼 매핑 기능 추가
|
||||
* - 자동 새로고침 간격 설정 가능
|
||||
* - 테이블/카드 뷰 전환
|
||||
* - 페이지네이션 개선
|
||||
*
|
||||
* 이 파일은 복구를 위해 보관 중이며,
|
||||
* 향후 문제 발생 시 참고용으로 사용될 수 있습니다.
|
||||
*
|
||||
* 롤백 방법:
|
||||
* 1. 이 파일의 주석 제거
|
||||
* 2. types.ts에서 "list" 활성화
|
||||
* 3. "list-v2" 주석 처리
|
||||
*/
|
||||
export function ListWidget({ element }: ListWidgetProps) {
|
||||
const [data, setData] = useState<QueryResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const config = element.listConfig || {
|
||||
columnMode: "auto",
|
||||
viewMode: "table",
|
||||
columns: [],
|
||||
pageSize: 10,
|
||||
enablePagination: true,
|
||||
showHeader: true,
|
||||
stripedRows: true,
|
||||
compactMode: false,
|
||||
cardColumns: 3,
|
||||
};
|
||||
|
||||
// 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (!element.dataSource || (!element.dataSource.query && !element.dataSource.endpoint)) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let queryResult: QueryResult;
|
||||
|
||||
// REST API vs Database 분기
|
||||
if (element.dataSource.type === "api" && element.dataSource.endpoint) {
|
||||
// REST API - 백엔드 프록시를 통한 호출
|
||||
const params = new URLSearchParams();
|
||||
if (element.dataSource.queryParams) {
|
||||
Object.entries(element.dataSource.queryParams).forEach(([key, value]) => {
|
||||
if (key && value) {
|
||||
params.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: element.dataSource.endpoint,
|
||||
method: "GET",
|
||||
headers: element.dataSource.headers || {},
|
||||
queryParams: Object.fromEntries(params),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "외부 API 호출 실패");
|
||||
}
|
||||
|
||||
const apiData = result.data;
|
||||
|
||||
// JSON Path 처리
|
||||
let processedData = apiData;
|
||||
if (element.dataSource.jsonPath) {
|
||||
const paths = element.dataSource.jsonPath.split(".");
|
||||
for (const path of paths) {
|
||||
if (processedData && typeof processedData === "object" && path in processedData) {
|
||||
processedData = processedData[path];
|
||||
} else {
|
||||
throw new Error(`JSON Path "${element.dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = Array.isArray(processedData) ? processedData : [processedData];
|
||||
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
||||
|
||||
queryResult = {
|
||||
columns,
|
||||
rows,
|
||||
totalRows: rows.length,
|
||||
executionTime: 0,
|
||||
};
|
||||
} else if (element.dataSource.query) {
|
||||
// Database (현재 DB 또는 외부 DB)
|
||||
if (element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId) {
|
||||
// 외부 DB
|
||||
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
||||
const externalResult = await ExternalDbConnectionAPI.executeQuery(
|
||||
parseInt(element.dataSource.externalConnectionId),
|
||||
element.dataSource.query,
|
||||
);
|
||||
if (!externalResult.success || !externalResult.data) {
|
||||
throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패");
|
||||
}
|
||||
|
||||
const resultData = externalResult.data as unknown as {
|
||||
columns: string[];
|
||||
rows: Record<string, unknown>[];
|
||||
rowCount: number;
|
||||
};
|
||||
queryResult = {
|
||||
columns: resultData.columns,
|
||||
rows: resultData.rows,
|
||||
totalRows: resultData.rowCount,
|
||||
executionTime: 0,
|
||||
};
|
||||
} else {
|
||||
// 현재 DB
|
||||
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||
const result = await dashboardApi.executeQuery(element.dataSource.query);
|
||||
queryResult = {
|
||||
columns: result.columns,
|
||||
rows: result.rows,
|
||||
totalRows: result.rowCount,
|
||||
executionTime: 0,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw new Error("데이터 소스가 올바르게 설정되지 않았습니다");
|
||||
}
|
||||
|
||||
setData(queryResult);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
|
||||
// 자동 새로고침 설정
|
||||
const refreshInterval = element.dataSource?.refreshInterval;
|
||||
if (refreshInterval && refreshInterval > 0) {
|
||||
const interval = setInterval(loadData, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [element.dataSource]);
|
||||
|
||||
// 로딩 중
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" />
|
||||
<div className="text-sm text-gray-600">데이터 로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 에러
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-2xl">⚠️</div>
|
||||
<div className="text-sm font-medium text-red-600">오류 발생</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 데이터 없음
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 p-4">
|
||||
<div className="text-center">
|
||||
<div className="mt-1 text-xs text-gray-500">데이터와 컬럼을 설정해주세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 컬럼 설정이 없으면 자동으로 모든 컬럼 표시
|
||||
const displayColumns: ListColumn[] =
|
||||
config.columns.length > 0
|
||||
? config.columns
|
||||
: data.columns.map((col) => ({
|
||||
id: col,
|
||||
label: col,
|
||||
field: col,
|
||||
visible: true,
|
||||
align: "left" as const,
|
||||
}));
|
||||
|
||||
// 페이지네이션
|
||||
const totalPages = Math.ceil(data.rows.length / config.pageSize);
|
||||
const startIdx = (currentPage - 1) * config.pageSize;
|
||||
const endIdx = startIdx + config.pageSize;
|
||||
const paginatedRows = config.enablePagination ? data.rows.slice(startIdx, endIdx) : data.rows;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col gap-3 p-4">
|
||||
{/* 테이블 뷰 */}
|
||||
{config.viewMode === "table" && (
|
||||
<div className={`flex-1 overflow-auto rounded-lg border ${config.compactMode ? "text-xs" : "text-sm"}`}>
|
||||
<Table>
|
||||
{config.showHeader && (
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{displayColumns
|
||||
.filter((col) => col.visible)
|
||||
.map((col) => (
|
||||
<TableHead
|
||||
key={col.id}
|
||||
className={col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}
|
||||
style={{ width: col.width ? `${col.width}px` : undefined }}
|
||||
>
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
)}
|
||||
<TableBody>
|
||||
{paginatedRows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={displayColumns.filter((col) => col.visible).length}
|
||||
className="text-center text-gray-500"
|
||||
>
|
||||
데이터가 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
paginatedRows.map((row, idx) => (
|
||||
<TableRow key={idx} className={config.stripedRows && idx % 2 === 1 ? "bg-muted/50" : ""}>
|
||||
{displayColumns
|
||||
.filter((col) => col.visible)
|
||||
.map((col) => (
|
||||
<TableCell
|
||||
key={col.id}
|
||||
className={col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}
|
||||
>
|
||||
{String(row[col.field] ?? "")}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 카드 뷰 */}
|
||||
{config.viewMode === "card" && (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{paginatedRows.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">데이터가 없습니다</div>
|
||||
) : (
|
||||
<div
|
||||
className={`grid gap-4 ${config.compactMode ? "text-xs" : "text-sm"}`}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${config.cardColumns || 3}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{paginatedRows.map((row, idx) => (
|
||||
<Card key={idx} className="p-4 transition-shadow hover:shadow-md">
|
||||
<div className="space-y-2">
|
||||
{displayColumns
|
||||
.filter((col) => col.visible)
|
||||
.map((col) => (
|
||||
<div key={col.id}>
|
||||
<div className="text-xs font-medium text-gray-500">{col.label}</div>
|
||||
<div
|
||||
className={`font-medium text-gray-900 ${col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}`}
|
||||
>
|
||||
{String(row[col.field] ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{config.enablePagination && totalPages > 1 && (
|
||||
<div className="flex shrink-0 items-center justify-between border-t pt-3 text-sm">
|
||||
<div className="text-gray-600">
|
||||
{startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<span className="text-gray-700">{currentPage}</span>
|
||||
<span className="text-gray-400">/</span>
|
||||
<span className="text-gray-500">{totalPages}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// "use client";
|
||||
//
|
||||
// ... (전체 코드 주석 처리됨)
|
||||
|
||||
Reference in New Issue
Block a user