2025-11-05 16:36:32 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
import {
|
2025-12-05 10:46:10 +09:00
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
} from "@/components/ui/dialog";
|
2025-11-05 16:36:32 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
|
|
import { GripVertical, Eye, EyeOff } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
interface ColumnConfig {
|
|
|
|
|
columnName: string;
|
|
|
|
|
label: string;
|
|
|
|
|
visible: boolean;
|
|
|
|
|
width?: number;
|
|
|
|
|
frozen?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TableOptionsModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
columns: ColumnConfig[];
|
|
|
|
|
onSave: (config: {
|
|
|
|
|
columns: ColumnConfig[];
|
|
|
|
|
showGridLines: boolean;
|
|
|
|
|
viewMode: "table" | "card" | "grouped-card";
|
|
|
|
|
}) => void;
|
|
|
|
|
tableName: string;
|
|
|
|
|
userId?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function TableOptionsModal({
|
|
|
|
|
isOpen,
|
|
|
|
|
onClose,
|
|
|
|
|
columns: initialColumns,
|
|
|
|
|
onSave,
|
|
|
|
|
tableName,
|
|
|
|
|
userId = "guest",
|
|
|
|
|
}: TableOptionsModalProps) {
|
|
|
|
|
const [columns, setColumns] = useState<ColumnConfig[]>(initialColumns);
|
|
|
|
|
const [showGridLines, setShowGridLines] = useState(true);
|
|
|
|
|
const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table");
|
|
|
|
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
|
|
|
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
|
|
|
|
|
|
|
|
// localStorage에서 설정 불러오기
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
const storageKey = `table_options_${tableName}_${userId}`;
|
|
|
|
|
const savedConfig = localStorage.getItem(storageKey);
|
2026-03-10 18:30:18 +09:00
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
if (savedConfig) {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(savedConfig);
|
|
|
|
|
setColumns(parsed.columns || initialColumns);
|
|
|
|
|
setShowGridLines(parsed.showGridLines ?? true);
|
|
|
|
|
setViewMode(parsed.viewMode || "table");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("설정 불러오기 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
setColumns(initialColumns);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, tableName, userId, initialColumns]);
|
|
|
|
|
|
|
|
|
|
// 컬럼 표시/숨기기 토글
|
|
|
|
|
const toggleColumnVisibility = (index: number) => {
|
|
|
|
|
const newColumns = [...columns];
|
|
|
|
|
newColumns[index].visible = !newColumns[index].visible;
|
|
|
|
|
setColumns(newColumns);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 컬럼 틀고정 토글
|
|
|
|
|
const toggleColumnFrozen = (index: number) => {
|
|
|
|
|
const newColumns = [...columns];
|
|
|
|
|
newColumns[index].frozen = !newColumns[index].frozen;
|
|
|
|
|
setColumns(newColumns);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 컬럼 너비 변경
|
|
|
|
|
const updateColumnWidth = (index: number, width: number) => {
|
|
|
|
|
const newColumns = [...columns];
|
|
|
|
|
newColumns[index].width = width;
|
|
|
|
|
setColumns(newColumns);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 드래그 앤 드롭 핸들러
|
|
|
|
|
const handleDragStart = (index: number) => {
|
|
|
|
|
setDraggedIndex(index);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragOver = (e: React.DragEvent, index: number) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setDragOverIndex(index);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
|
|
|
|
|
e.preventDefault();
|
2026-03-10 18:30:18 +09:00
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
if (draggedIndex === null || draggedIndex === dropIndex) {
|
|
|
|
|
setDraggedIndex(null);
|
|
|
|
|
setDragOverIndex(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newColumns = [...columns];
|
|
|
|
|
const [draggedColumn] = newColumns.splice(draggedIndex, 1);
|
|
|
|
|
newColumns.splice(dropIndex, 0, draggedColumn);
|
2026-03-10 18:30:18 +09:00
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
setColumns(newColumns);
|
|
|
|
|
setDraggedIndex(null);
|
|
|
|
|
setDragOverIndex(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = () => {
|
|
|
|
|
setDraggedIndex(null);
|
|
|
|
|
setDragOverIndex(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 저장
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
const config = {
|
|
|
|
|
columns,
|
|
|
|
|
showGridLines,
|
|
|
|
|
viewMode,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// localStorage에 저장
|
|
|
|
|
const storageKey = `table_options_${tableName}_${userId}`;
|
|
|
|
|
localStorage.setItem(storageKey, JSON.stringify(config));
|
|
|
|
|
|
|
|
|
|
onSave(config);
|
|
|
|
|
onClose();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 초기화
|
|
|
|
|
const handleReset = () => {
|
|
|
|
|
setColumns(initialColumns);
|
|
|
|
|
setShowGridLines(true);
|
|
|
|
|
setViewMode("table");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-05 10:46:10 +09:00
|
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
|
|
|
<DialogContent className="max-h-[90vh] sm:max-w-[700px]">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="text-base sm:text-lg">테이블 옵션</DialogTitle>
|
|
|
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
|
|
|
컬럼 표시/숨기기, 순서 변경, 틀고정 등을 설정할 수 있습니다.
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
2025-11-05 16:36:32 +09:00
|
|
|
|
2026-03-10 18:30:18 +09:00
|
|
|
<Tabs defaultValue="columns" className="flex flex-1 flex-col overflow-hidden">
|
|
|
|
|
<TabsList className="grid w-full flex-shrink-0 grid-cols-3">
|
|
|
|
|
<TabsTrigger value="columns" className="text-xs sm:text-sm">
|
|
|
|
|
컬럼 설정
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="display" className="text-xs sm:text-sm">
|
|
|
|
|
표시 설정
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="view" className="text-xs sm:text-sm">
|
|
|
|
|
보기 모드
|
|
|
|
|
</TabsTrigger>
|
2025-11-05 16:36:32 +09:00
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
{/* 컬럼 설정 탭 */}
|
2026-03-10 18:30:18 +09:00
|
|
|
<TabsContent value="columns" className="mt-4 flex-1 space-y-3 overflow-y-auto sm:space-y-4">
|
|
|
|
|
<div className="text-muted-foreground mb-2 text-xs sm:text-sm">
|
2025-11-05 16:36:32 +09:00
|
|
|
드래그하여 순서를 변경하거나, 아이콘을 클릭하여 표시/숨기기를 설정하세요.
|
|
|
|
|
</div>
|
2026-03-10 18:30:18 +09:00
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
<div className="space-y-2">
|
2025-11-05 16:36:32 +09:00
|
|
|
{columns.map((column, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={column.columnName}
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={() => handleDragStart(index)}
|
|
|
|
|
onDragOver={(e) => handleDragOver(e, index)}
|
|
|
|
|
onDrop={(e) => handleDrop(e, index)}
|
|
|
|
|
onDragEnd={handleDragEnd}
|
2026-03-10 18:30:18 +09:00
|
|
|
className={`bg-card hover:bg-accent/50 flex cursor-move items-center gap-2 rounded-md border p-2 transition-colors sm:p-3 ${
|
2025-11-05 16:36:32 +09:00
|
|
|
dragOverIndex === index ? "border-primary" : "border-border"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{/* 드래그 핸들 */}
|
2026-03-10 18:30:18 +09:00
|
|
|
<GripVertical className="text-muted-foreground h-4 w-4 flex-shrink-0" />
|
2025-11-05 16:36:32 +09:00
|
|
|
|
|
|
|
|
{/* 컬럼명 */}
|
2026-03-10 18:30:18 +09:00
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="truncate text-xs font-medium sm:text-sm">{column.label}</div>
|
|
|
|
|
<div className="text-muted-foreground truncate text-[10px] sm:text-xs">{column.columnName}</div>
|
2025-11-05 16:36:32 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 너비 설정 */}
|
|
|
|
|
<div className="flex items-center gap-1 sm:gap-2">
|
2026-03-10 18:30:18 +09:00
|
|
|
<Label className="text-[10px] whitespace-nowrap sm:text-xs">너비:</Label>
|
2025-11-05 16:36:32 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
value={column.width || 150}
|
|
|
|
|
onChange={(e) => updateColumnWidth(index, parseInt(e.target.value) || 150)}
|
2026-03-10 18:30:18 +09:00
|
|
|
className="h-7 w-16 text-xs sm:h-8 sm:w-20"
|
2025-11-05 16:36:32 +09:00
|
|
|
min={50}
|
|
|
|
|
max={500}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 틀고정 */}
|
|
|
|
|
<Button
|
|
|
|
|
variant={column.frozen ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => toggleColumnFrozen(index)}
|
|
|
|
|
className="h-7 px-2 text-[10px] sm:h-8 sm:px-3 sm:text-xs"
|
|
|
|
|
>
|
|
|
|
|
{column.frozen ? "고정됨" : "고정"}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* 표시/숨기기 */}
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => toggleColumnVisibility(index)}
|
2026-03-10 18:30:18 +09:00
|
|
|
className="h-7 w-7 flex-shrink-0 sm:h-8 sm:w-8"
|
2025-11-05 16:36:32 +09:00
|
|
|
>
|
|
|
|
|
{column.visible ? (
|
2026-03-10 18:30:18 +09:00
|
|
|
<Eye className="text-primary h-4 w-4" />
|
2025-11-05 16:36:32 +09:00
|
|
|
) : (
|
2026-03-10 18:30:18 +09:00
|
|
|
<EyeOff className="text-muted-foreground h-4 w-4" />
|
2025-11-05 16:36:32 +09:00
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* 표시 설정 탭 */}
|
2026-03-10 18:30:18 +09:00
|
|
|
<TabsContent value="display" className="mt-4 flex-1 space-y-3 overflow-y-auto sm:space-y-4">
|
|
|
|
|
<div className="bg-card flex items-center justify-between rounded-md border p-3 sm:p-4">
|
2025-11-05 16:36:32 +09:00
|
|
|
<div className="space-y-0.5">
|
2026-03-10 18:30:18 +09:00
|
|
|
<Label className="text-xs font-medium sm:text-sm">그리드선 표시</Label>
|
|
|
|
|
<p className="text-muted-foreground text-[10px] sm:text-xs">테이블의 셀 구분선을 표시합니다</p>
|
2025-11-05 16:36:32 +09:00
|
|
|
</div>
|
2026-03-10 18:30:18 +09:00
|
|
|
<Switch checked={showGridLines} onCheckedChange={setShowGridLines} />
|
2025-11-05 16:36:32 +09:00
|
|
|
</div>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* 보기 모드 탭 */}
|
2026-03-10 18:30:18 +09:00
|
|
|
<TabsContent value="view" className="mt-4 flex-1 space-y-3 overflow-y-auto sm:space-y-4">
|
2025-11-05 16:36:32 +09:00
|
|
|
<div className="grid gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
variant={viewMode === "table" ? "default" : "outline"}
|
|
|
|
|
onClick={() => setViewMode("table")}
|
2026-03-10 18:30:18 +09:00
|
|
|
className="h-auto flex-col items-start p-3 text-left sm:p-4"
|
2025-11-05 16:36:32 +09:00
|
|
|
>
|
2026-03-10 18:30:18 +09:00
|
|
|
<div className="text-sm font-semibold sm:text-base">테이블형</div>
|
|
|
|
|
<div className="text-muted-foreground mt-1 text-xs">전통적인 행/열 테이블 형식으로 표시</div>
|
2025-11-05 16:36:32 +09:00
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant={viewMode === "card" ? "default" : "outline"}
|
|
|
|
|
onClick={() => setViewMode("card")}
|
2026-03-10 18:30:18 +09:00
|
|
|
className="h-auto flex-col items-start p-3 text-left sm:p-4"
|
2025-11-05 16:36:32 +09:00
|
|
|
>
|
2026-03-10 18:30:18 +09:00
|
|
|
<div className="text-sm font-semibold sm:text-base">카드형</div>
|
|
|
|
|
<div className="text-muted-foreground mt-1 text-xs">각 항목을 카드로 표시 (가로로 길게)</div>
|
2025-11-05 16:36:32 +09:00
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant={viewMode === "grouped-card" ? "default" : "outline"}
|
|
|
|
|
onClick={() => setViewMode("grouped-card")}
|
2026-03-10 18:30:18 +09:00
|
|
|
className="h-auto flex-col items-start p-3 text-left sm:p-4"
|
2025-11-05 16:36:32 +09:00
|
|
|
>
|
2026-03-10 18:30:18 +09:00
|
|
|
<div className="text-sm font-semibold sm:text-base">그룹화된 카드형</div>
|
|
|
|
|
<div className="text-muted-foreground mt-1 text-xs">그룹별로 카드를 묶어서 표시</div>
|
2025-11-05 16:36:32 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
2026-03-10 18:30:18 +09:00
|
|
|
<DialogFooter className="mt-4 gap-2 sm:gap-0">
|
2025-11-05 16:36:32 +09:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
초기화
|
|
|
|
|
</Button>
|
2026-03-10 18:30:18 +09:00
|
|
|
<Button variant="outline" onClick={onClose} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
2025-11-05 16:36:32 +09:00
|
|
|
취소
|
|
|
|
|
</Button>
|
2026-03-10 18:30:18 +09:00
|
|
|
<Button onClick={handleSave} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
2025-11-05 16:36:32 +09:00
|
|
|
저장
|
|
|
|
|
</Button>
|
2025-12-05 10:46:10 +09:00
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2025-11-05 16:36:32 +09:00
|
|
|
);
|
|
|
|
|
}
|