리스트에 카드 뷰 모드 추가

This commit is contained in:
dohyeons 2025-10-15 11:29:53 +09:00
parent f7fc0debe5
commit 31fcceef20
4 changed files with 150 additions and 62 deletions

View File

@ -213,12 +213,14 @@ export interface ChartDataset {
// 리스트 위젯 설정
export interface ListWidgetConfig {
columnMode: "auto" | "manual"; // 컬럼 설정 방식 (자동 or 수동)
viewMode: "table" | "card"; // 뷰 모드 (테이블 or 카드) (기본: table)
columns: ListColumn[]; // 컬럼 정의
pageSize: number; // 페이지당 행 수 (기본: 10)
enablePagination: boolean; // 페이지네이션 활성화 (기본: true)
showHeader: boolean; // 헤더 표시 (기본: true)
stripedRows: boolean; // 줄무늬 행 (기본: true)
showHeader: boolean; // 헤더 표시 (기본: true, 테이블 모드에만 적용)
stripedRows: boolean; // 줄무늬 행 (기본: true, 테이블 모드에만 적용)
compactMode: boolean; // 압축 모드 (기본: false)
cardColumns: number; // 카드 뷰 컬럼 수 (기본: 3)
}
// 리스트 컬럼

View File

@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react";
import { DashboardElement, QueryResult, ListWidgetConfig } 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;
@ -24,12 +25,14 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
const config = element.listConfig || {
columnMode: "auto",
viewMode: "table",
columns: [],
pageSize: 10,
enablePagination: true,
showHeader: true,
stripedRows: true,
compactMode: false,
cardColumns: 3,
};
// 데이터 로드
@ -209,55 +212,92 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
<h3 className="text-sm font-semibold text-gray-700">{element.title}</h3>
</div>
{/* 테이블 */}
<div className={`flex-1 overflow-auto rounded-lg border ${config.compactMode ? "text-xs" : "text-sm"}`}>
<Table>
{config.showHeader && (
<TableHeader>
<TableRow>
{config.columns
.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={config.columns.filter((col) => col.visible).length}
className="text-center text-gray-500"
>
</TableCell>
</TableRow>
) : (
paginatedRows.map((row, idx) => (
<TableRow key={idx} className={config.stripedRows ? undefined : ""}>
{/* 테이블 뷰 */}
{config.viewMode === "table" && (
<div className={`flex-1 overflow-auto rounded-lg border ${config.compactMode ? "text-xs" : "text-sm"}`}>
<Table>
{config.showHeader && (
<TableHeader>
<TableRow>
{config.columns
.filter((col) => col.visible)
.map((col) => (
<TableCell
<TableHead
key={col.id}
className={col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}
style={{ width: col.width ? `${col.width}px` : undefined }}
>
{String(row[col.field] ?? "")}
</TableCell>
{col.label}
</TableHead>
))}
</TableRow>
))
</TableHeader>
)}
</TableBody>
</Table>
</div>
<TableBody>
{paginatedRows.length === 0 ? (
<TableRow>
<TableCell
colSpan={config.columns.filter((col) => col.visible).length}
className="text-center text-gray-500"
>
</TableCell>
</TableRow>
) : (
paginatedRows.map((row, idx) => (
<TableRow key={idx} className={config.stripedRows ? undefined : ""}>
{config.columns
.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">
{config.columns
.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 && (

View File

@ -36,12 +36,14 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
const [listConfig, setListConfig] = useState<ListWidgetConfig>(
element.listConfig || {
columnMode: "auto",
viewMode: "table",
columns: [],
pageSize: 10,
enablePagination: true,
showHeader: true,
stripedRows: true,
compactMode: false,
cardColumns: 3,
},
);
@ -64,6 +66,7 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
// 현재 스텝은 1로 초기화
setCurrentStep(1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, element.id]); // element.id가 변경될 때만 재실행
// 데이터 소스 타입 변경

View File

@ -7,6 +7,7 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Input } from "@/components/ui/input";
interface ListTableOptionsProps {
config: ListWidgetConfig;
@ -26,6 +27,28 @@ export function ListTableOptions({ config, onChange }: ListTableOptionsProps) {
</div>
<div className="space-y-6">
{/* 뷰 모드 */}
<div>
<Label className="mb-2 block text-sm font-medium"> </Label>
<RadioGroup
value={config.viewMode}
onValueChange={(value: "table" | "card") => onChange({ viewMode: value })}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="table" id="table" />
<Label htmlFor="table" className="cursor-pointer font-normal">
📊 ()
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="card" id="card" />
<Label htmlFor="card" className="cursor-pointer font-normal">
🗂
</Label>
</div>
</RadioGroup>
</div>
{/* 컬럼 모드 */}
<div>
<Label className="mb-2 block text-sm font-medium"> </Label>
@ -48,6 +71,22 @@ export function ListTableOptions({ config, onChange }: ListTableOptionsProps) {
</RadioGroup>
</div>
{/* 카드 뷰 컬럼 수 */}
{config.viewMode === "card" && (
<div>
<Label className="mb-2 block text-sm font-medium"> </Label>
<Input
type="number"
min="1"
max="6"
value={config.cardColumns || 3}
onChange={(e) => onChange({ cardColumns: parseInt(e.target.value) || 3 })}
className="w-full"
/>
<p className="mt-1 text-xs text-gray-500"> (1-6)</p>
</div>
)}
{/* 페이지 크기 */}
<div>
<Label className="mb-2 block text-sm font-medium"> </Label>
@ -86,26 +125,30 @@ export function ListTableOptions({ config, onChange }: ListTableOptionsProps) {
<div className="space-y-3">
<Label className="text-sm font-medium"></Label>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Checkbox
id="showHeader"
checked={config.showHeader}
onCheckedChange={(checked) => onChange({ showHeader: checked as boolean })}
/>
<Label htmlFor="showHeader" className="cursor-pointer font-normal">
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="stripedRows"
checked={config.stripedRows}
onCheckedChange={(checked) => onChange({ stripedRows: checked as boolean })}
/>
<Label htmlFor="stripedRows" className="cursor-pointer font-normal">
</Label>
</div>
{config.viewMode === "table" && (
<>
<div className="flex items-center gap-2">
<Checkbox
id="showHeader"
checked={config.showHeader}
onCheckedChange={(checked) => onChange({ showHeader: checked as boolean })}
/>
<Label htmlFor="showHeader" className="cursor-pointer font-normal">
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="stripedRows"
checked={config.stripedRows}
onCheckedChange={(checked) => onChange({ stripedRows: checked as boolean })}
/>
<Label htmlFor="stripedRows" className="cursor-pointer font-normal">
</Label>
</div>
</>
)}
<div className="flex items-center gap-2">
<Checkbox
id="compactMode"