"use client"; /** * FieldChooser 컴포넌트 * 사용 가능한 필드 목록을 표시하고 영역에 배치할 수 있는 모달 */ import React, { useState, useMemo } from "react"; import { cn } from "@/lib/utils"; import { PivotFieldConfig, PivotAreaType, AggregationType, SummaryDisplayMode } from "../types"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Search, Filter, Columns, Rows, BarChart3, GripVertical, Plus, Minus, Type, Hash, Calendar, ToggleLeft, } from "lucide-react"; // ==================== 타입 ==================== interface AvailableField { field: string; caption: string; dataType: "string" | "number" | "date" | "boolean"; isSelected: boolean; currentArea?: PivotAreaType; } interface FieldChooserProps { open: boolean; onOpenChange: (open: boolean) => void; availableFields: AvailableField[]; selectedFields: PivotFieldConfig[]; onFieldsChange: (fields: PivotFieldConfig[]) => void; } // ==================== 영역 설정 ==================== const AREA_OPTIONS: { value: PivotAreaType | "none"; label: string; icon: React.ReactNode; }[] = [ { value: "none", label: "사용 안함", icon: }, { value: "filter", label: "필터", icon: }, { value: "row", label: "행", icon: }, { value: "column", label: "열", icon: }, { value: "data", label: "데이터", icon: }, ]; const SUMMARY_OPTIONS: { value: AggregationType; label: string }[] = [ { value: "sum", label: "합계" }, { value: "count", label: "개수" }, { value: "avg", label: "평균" }, { value: "min", label: "최소" }, { value: "max", label: "최대" }, { value: "countDistinct", label: "고유 개수" }, ]; const DISPLAY_MODE_OPTIONS: { value: SummaryDisplayMode; label: string }[] = [ { value: "absoluteValue", label: "절대값" }, { value: "percentOfRowTotal", label: "행 총계 %" }, { value: "percentOfColumnTotal", label: "열 총계 %" }, { value: "percentOfGrandTotal", label: "전체 총계 %" }, { value: "runningTotalByRow", label: "행 누계" }, { value: "runningTotalByColumn", label: "열 누계" }, { value: "differenceFromPrevious", label: "이전 대비 차이" }, { value: "percentDifferenceFromPrevious", label: "이전 대비 % 차이" }, ]; const DATE_GROUP_OPTIONS: { value: string; label: string }[] = [ { value: "none", label: "그룹 없음" }, { value: "year", label: "년" }, { value: "quarter", label: "분기" }, { value: "month", label: "월" }, { value: "week", label: "주" }, { value: "day", label: "일" }, ]; const DATA_TYPE_ICONS: Record = { string: , number: , date: , boolean: , }; // ==================== 필드 아이템 ==================== interface FieldItemProps { field: AvailableField; config?: PivotFieldConfig; onAreaChange: (area: PivotAreaType | "none") => void; onSummaryChange?: (summary: AggregationType) => void; onDisplayModeChange?: (displayMode: SummaryDisplayMode) => void; } const FieldItem: React.FC = ({ field, config, onAreaChange, onSummaryChange, onDisplayModeChange, }) => { const currentArea = config?.area || "none"; const isSelected = currentArea !== "none"; return (
{/* 데이터 타입 아이콘 */}
{DATA_TYPE_ICONS[field.dataType] || }
{/* 필드명 */}
{field.caption}
{field.field}
{/* 영역 선택 */} {/* 집계 함수 선택 (데이터 영역인 경우) */} {currentArea === "data" && onSummaryChange && ( )} {/* 표시 모드 선택 (데이터 영역인 경우) */} {currentArea === "data" && onDisplayModeChange && ( )}
); }; // ==================== 메인 컴포넌트 ==================== export const FieldChooser: React.FC = ({ open, onOpenChange, availableFields, selectedFields, onFieldsChange, }) => { const [searchQuery, setSearchQuery] = useState(""); const [filterType, setFilterType] = useState<"all" | "selected" | "unselected">( "all" ); // 필터링된 필드 목록 const filteredFields = useMemo(() => { let result = availableFields; // 검색어 필터 if (searchQuery) { const query = searchQuery.toLowerCase(); result = result.filter( (f) => f.caption.toLowerCase().includes(query) || f.field.toLowerCase().includes(query) ); } // 선택 상태 필터 if (filterType === "selected") { result = result.filter((f) => selectedFields.some((sf) => sf.field === f.field && sf.visible !== false) ); } else if (filterType === "unselected") { result = result.filter( (f) => !selectedFields.some( (sf) => sf.field === f.field && sf.visible !== false ) ); } return result; }, [availableFields, selectedFields, searchQuery, filterType]); // 필드 영역 변경 const handleAreaChange = ( field: AvailableField, area: PivotAreaType | "none" ) => { const existingConfig = selectedFields.find((f) => f.field === field.field); if (area === "none") { // 필드 완전 제거 (visible: false 대신 배열에서 제거) if (existingConfig) { const newFields = selectedFields.filter((f) => f.field !== field.field); onFieldsChange(newFields); } } else { // 필드 추가 또는 영역 변경 if (existingConfig) { const newFields = selectedFields.map((f) => f.field === field.field ? { ...f, area, visible: true } : f ); onFieldsChange(newFields); } else { // 새 필드 추가 const newField: PivotFieldConfig = { field: field.field, caption: field.caption, area, dataType: field.dataType, visible: true, summaryType: area === "data" ? "sum" : undefined, areaIndex: selectedFields.filter((f) => f.area === area).length, }; onFieldsChange([...selectedFields, newField]); } } }; // 집계 함수 변경 const handleSummaryChange = ( field: AvailableField, summaryType: AggregationType ) => { const newFields = selectedFields.map((f) => f.field === field.field ? { ...f, summaryType } : f ); onFieldsChange(newFields); }; // 표시 모드 변경 const handleDisplayModeChange = ( field: AvailableField, displayMode: SummaryDisplayMode ) => { const newFields = selectedFields.map((f) => f.field === field.field ? { ...f, summaryDisplayMode: displayMode } : f ); onFieldsChange(newFields); }; // 모든 필드 선택 해제 const handleClearAll = () => { const newFields = selectedFields.map((f) => ({ ...f, visible: false })); onFieldsChange(newFields); }; // 통계 const stats = useMemo(() => { const visible = selectedFields.filter((f) => f.visible !== false); return { total: availableFields.length, selected: visible.length, filter: visible.filter((f) => f.area === "filter").length, row: visible.filter((f) => f.area === "row").length, column: visible.filter((f) => f.area === "column").length, data: visible.filter((f) => f.area === "data").length, }; }, [availableFields, selectedFields]); return ( 필드 선택기 피벗 테이블에 표시할 필드를 선택하고 영역을 지정하세요. {/* 통계 */}
전체: {stats.total} 선택됨: {stats.selected} 필터: {stats.filter} 행: {stats.row} 열: {stats.column} 데이터: {stats.data}
{/* 검색 및 필터 */}
setSearchQuery(e.target.value)} className="pl-9 h-9" />
{/* 필드 목록 */}
{filteredFields.length === 0 ? (
검색 결과가 없습니다.
) : ( filteredFields.map((field) => { const config = selectedFields.find( (f) => f.field === field.field && f.visible !== false ); return ( handleAreaChange(field, area)} onSummaryChange={ config?.area === "data" ? (summary) => handleSummaryChange(field, summary) : undefined } onDisplayModeChange={ config?.area === "data" ? (mode) => handleDisplayModeChange(field, mode) : undefined } /> ); }) )}
{/* 푸터 */}
); }; export default FieldChooser;