"use client"; /** * TableSettingsModal -- 하드코딩 페이지용 테이블 설정 모달 (3탭) * * 탭 1: 컬럼 설정 -- 컬럼 표시/숨김, 드래그 순서 변경, 너비(px) 설정, 틀고정 * 탭 2: 필터 설정 -- 필터 활성/비활성, 필터 타입(텍스트/선택/날짜), 너비(%) 설정, 그룹별 합산 * 탭 3: 그룹 설정 -- 그룹핑 컬럼 선택 * * 설정값은 localStorage에 저장되며, onSave 콜백으로 부모 컴포넌트에 전달 * DynamicSearchFilter, DataGrid와 함께 사용 */ import React, { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { GripVertical, Settings2, SlidersHorizontal, Layers, RotateCcw, Lock } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; // ===== 타입 ===== export interface ColumnSetting { columnName: string; displayName: string; visible: boolean; width: number; } export interface FilterSetting { columnName: string; displayName: string; enabled: boolean; filterType: "text" | "select" | "date"; width: number; } export interface GroupSetting { columnName: string; displayName: string; enabled: boolean; } export interface TableSettings { columns: ColumnSetting[]; filters: FilterSetting[]; groups: GroupSetting[]; frozenCount: number; groupSumEnabled: boolean; } export interface TableSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; /** 테이블명 (web-types API 호출용) */ tableName: string; /** localStorage 키 분리용 고유 ID */ settingsId: string; /** 저장 시 콜백 */ onSave?: (settings: TableSettings) => void; /** 초기 탭 */ initialTab?: "columns" | "filters" | "groups"; } // ===== 상수 ===== const FILTER_TYPE_OPTIONS = [ { value: "text", label: "텍스트" }, { value: "select", label: "선택" }, { value: "date", label: "날짜" }, ]; const AUTO_COLS = ["id", "created_date", "updated_date", "writer", "company_code"]; // ===== 유틸 ===== function getStorageKey(settingsId: string) { return `table_settings_${settingsId}`; } /** localStorage에서 저장된 설정 로드 (외부에서도 사용 가능) */ export function loadTableSettings(settingsId: string): TableSettings | null { try { const raw = localStorage.getItem(getStorageKey(settingsId)); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } /** 저장된 컬럼 순서/설정을 API 컬럼과 병합 */ function mergeColumns(fresh: ColumnSetting[], saved: ColumnSetting[]): ColumnSetting[] { const savedMap = new Map(saved.map((s) => [s.columnName, s])); const ordered: ColumnSetting[] = []; // 저장된 순서대로 for (const s of saved) { const f = fresh.find((c) => c.columnName === s.columnName); if (f) ordered.push({ ...f, visible: s.visible, width: s.width }); } // 새로 추가된 컬럼은 맨 뒤에 for (const f of fresh) { if (!savedMap.has(f.columnName)) ordered.push(f); } return ordered; } // ===== Sortable Column Row (탭 1) ===== function SortableColumnRow({ col, onToggleVisible, onWidthChange, }: { col: ColumnSetting & { _idx: number }; onToggleVisible: (idx: number) => void; onWidthChange: (idx: number, width: number) => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: col.columnName }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
{/* 드래그 핸들 */} {/* 표시 체크박스 */} onToggleVisible(col._idx)} /> {/* 표시 토글 (Switch) */} onToggleVisible(col._idx)} className="shrink-0" /> {/* 컬럼명 + 기술명 */}
{col.displayName}
{col.columnName}
{/* 너비 입력 */}
너비: onWidthChange(col._idx, Number(e.target.value) || 100)} className="h-8 w-[70px] text-xs text-center" min={50} max={500} />
); } // ===== TableSettingsModal ===== export function TableSettingsModal({ open, onOpenChange, tableName, settingsId, onSave, initialTab = "columns", }: TableSettingsModalProps) { const [activeTab, setActiveTab] = useState(initialTab); const [loading, setLoading] = useState(false); // 임시 설정 (모달 내에서만 수정, 저장 시 반영) const [tempColumns, setTempColumns] = useState([]); const [tempFilters, setTempFilters] = useState([]); const [tempGroups, setTempGroups] = useState([]); const [tempFrozenCount, setTempFrozenCount] = useState(0); const [tempGroupSum, setTempGroupSum] = useState(false); // 원본 컬럼 (초기화용) const [defaultColumns, setDefaultColumns] = useState([]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) ); // 모달 열릴 때 데이터 로드 useEffect(() => { if (!open) return; setActiveTab(initialTab); loadData(); }, [open]); // eslint-disable-line react-hooks/exhaustive-deps const loadData = async () => { setLoading(true); try { const res = await apiClient.get(`/table-management/tables/${tableName}/web-types`); const types: any[] = res.data?.data || []; // 기본 컬럼 설정 생성 const freshColumns: ColumnSetting[] = types .filter((t) => !AUTO_COLS.includes(t.columnName)) .map((t) => ({ columnName: t.columnName, displayName: t.displayName || t.columnLabel || t.columnName, visible: true, width: 120, })); // 기본 필터 설정 생성 const freshFilters: FilterSetting[] = freshColumns.map((c) => { const wt = types.find((t) => t.columnName === c.columnName); let filterType: "text" | "select" | "date" = "text"; if (wt?.inputType === "category" || wt?.inputType === "select") filterType = "select"; else if (wt?.inputType === "date" || wt?.inputType === "datetime") filterType = "date"; return { columnName: c.columnName, displayName: c.displayName, enabled: false, filterType, width: 25, }; }); // 기본 그룹 설정 생성 const freshGroups: GroupSetting[] = freshColumns.map((c) => ({ columnName: c.columnName, displayName: c.displayName, enabled: false, })); setDefaultColumns(freshColumns); // localStorage에서 저장된 설정 복원 const saved = loadTableSettings(settingsId); if (saved) { setTempColumns(mergeColumns(freshColumns, saved.columns)); setTempFilters(freshFilters.map((f) => { const s = saved.filters?.find((sf) => sf.columnName === f.columnName); return s ? { ...f, enabled: s.enabled, filterType: s.filterType, width: s.width } : f; })); setTempGroups(freshGroups.map((g) => { const s = saved.groups?.find((sg) => sg.columnName === g.columnName); return s ? { ...g, enabled: s.enabled } : g; })); setTempFrozenCount(saved.frozenCount || 0); setTempGroupSum(saved.groupSumEnabled || false); } else { setTempColumns(freshColumns); setTempFilters(freshFilters); setTempGroups(freshGroups); setTempFrozenCount(0); setTempGroupSum(false); } } catch (err) { console.error("테이블 설정 로드 실패:", err); } finally { setLoading(false); } }; // 저장 const handleSave = () => { const settings: TableSettings = { columns: tempColumns, filters: tempFilters, groups: tempGroups, frozenCount: tempFrozenCount, groupSumEnabled: tempGroupSum, }; localStorage.setItem(getStorageKey(settingsId), JSON.stringify(settings)); onSave?.(settings); onOpenChange(false); }; // 컬럼 설정 초기화 const handleResetColumns = () => { setTempColumns(defaultColumns.map((c) => ({ ...c }))); setTempFrozenCount(0); }; // ===== 컬럼 설정 핸들러 ===== const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; setTempColumns((prev) => { const oldIdx = prev.findIndex((c) => c.columnName === active.id); const newIdx = prev.findIndex((c) => c.columnName === over.id); return arrayMove(prev, oldIdx, newIdx); }); }; const toggleColumnVisible = (idx: number) => { setTempColumns((prev) => { const next = [...prev]; next[idx] = { ...next[idx], visible: !next[idx].visible }; return next; }); }; const changeColumnWidth = (idx: number, width: number) => { setTempColumns((prev) => { const next = [...prev]; next[idx] = { ...next[idx], width }; return next; }); }; // ===== 필터 설정 핸들러 ===== const allFiltersEnabled = tempFilters.length > 0 && tempFilters.every((f) => f.enabled); const toggleFilterAll = (checked: boolean) => { setTempFilters((prev) => prev.map((f) => ({ ...f, enabled: checked }))); }; const toggleFilter = (idx: number) => { setTempFilters((prev) => { const next = [...prev]; next[idx] = { ...next[idx], enabled: !next[idx].enabled }; return next; }); }; const changeFilterType = (idx: number, filterType: "text" | "select" | "date") => { setTempFilters((prev) => { const next = [...prev]; next[idx] = { ...next[idx], filterType }; return next; }); }; const changeFilterWidth = (idx: number, width: number) => { setTempFilters((prev) => { const next = [...prev]; next[idx] = { ...next[idx], width }; return next; }); }; // ===== 그룹 설정 핸들러 ===== const toggleGroup = (idx: number) => { setTempGroups((prev) => { const next = [...prev]; next[idx] = { ...next[idx], enabled: !next[idx].enabled }; return next; }); }; const visibleCount = tempColumns.filter((c) => c.visible).length; return ( 테이블 설정 테이블의 컬럼, 필터, 그룹화를 설정합니다 {loading ? (
로딩 중...
) : ( setActiveTab(v as typeof activeTab)} className="flex-1 flex flex-col min-h-0"> 컬럼 설정 필터 설정 그룹 설정 {/* ===== 탭 1: 컬럼 설정 ===== */} {/* 헤더: 표시 수 / 틀고정 / 초기화 */}
{visibleCount}/{tempColumns.length}개 컬럼 표시 중
틀고정: setTempFrozenCount( Math.min(Math.max(0, Number(e.target.value) || 0), tempColumns.length) ) } className="h-7 w-[50px] text-xs text-center" min={0} max={tempColumns.length} /> 개 컬럼
{/* 컬럼 목록 (드래그 순서 변경 가능) */} c.columnName)} strategy={verticalListSortingStrategy} >
{tempColumns.map((col, idx) => ( ))}
{/* ===== 탭 2: 필터 설정 ===== */} {/* 전체 선택 */}
toggleFilterAll(!allFiltersEnabled)} > 전체 선택
{/* 필터 목록 */}
{tempFilters.map((filter, idx) => (
toggleFilter(idx)} />
{filter.displayName}
changeFilterWidth(idx, Number(e.target.value) || 25)} className="h-8 w-[55px] text-xs text-center" min={10} max={100} /> %
))}
{/* 그룹별 합산 토글 */}
그룹별 합산
같은 값끼리 그룹핑하여 합산
{/* ===== 탭 3: 그룹 설정 ===== */}
사용 가능한 컬럼
{tempGroups.map((group, idx) => (
toggleGroup(idx)} >
{group.displayName}
{group.columnName}
))}
)}
); }