772 lines
32 KiB
TypeScript
772 lines
32 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||
|
|
import { Separator } from "@/components/ui/separator";
|
||
|
|
import { TableListConfig, ColumnConfig } from "./types";
|
||
|
|
import {
|
||
|
|
Plus,
|
||
|
|
Trash2,
|
||
|
|
ArrowUp,
|
||
|
|
ArrowDown,
|
||
|
|
Eye,
|
||
|
|
EyeOff,
|
||
|
|
Settings,
|
||
|
|
Columns,
|
||
|
|
Filter,
|
||
|
|
Palette,
|
||
|
|
MousePointer,
|
||
|
|
} from "lucide-react";
|
||
|
|
|
||
|
|
export interface TableListConfigPanelProps {
|
||
|
|
config: TableListConfig;
|
||
|
|
onChange: (config: Partial<TableListConfig>) => void;
|
||
|
|
screenTableName?: string; // 화면에 연결된 테이블명
|
||
|
|
tableColumns?: any[]; // 테이블 컬럼 정보
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* TableList 설정 패널
|
||
|
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||
|
|
*/
|
||
|
|
export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||
|
|
config,
|
||
|
|
onChange,
|
||
|
|
screenTableName,
|
||
|
|
tableColumns,
|
||
|
|
}) => {
|
||
|
|
console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns });
|
||
|
|
|
||
|
|
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
||
|
|
const [loadingTables, setLoadingTables] = useState(false);
|
||
|
|
const [availableColumns, setAvailableColumns] = useState<
|
||
|
|
Array<{ columnName: string; dataType: string; label?: string }>
|
||
|
|
>([]);
|
||
|
|
|
||
|
|
// 화면 테이블명이 있으면 자동으로 설정
|
||
|
|
useEffect(() => {
|
||
|
|
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
|
||
|
|
console.log("🔄 화면 테이블명 자동 설정:", screenTableName);
|
||
|
|
onChange({ selectedTable: screenTableName });
|
||
|
|
}
|
||
|
|
}, [screenTableName, config.selectedTable, onChange]);
|
||
|
|
|
||
|
|
// 테이블 목록 가져오기
|
||
|
|
useEffect(() => {
|
||
|
|
const fetchTables = async () => {
|
||
|
|
setLoadingTables(true);
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/tables");
|
||
|
|
if (response.ok) {
|
||
|
|
const result = await response.json();
|
||
|
|
if (result.success && result.data) {
|
||
|
|
setAvailableTables(
|
||
|
|
result.data.map((table: any) => ({
|
||
|
|
tableName: table.tableName,
|
||
|
|
displayName: table.displayName || table.tableName,
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("테이블 목록 가져오기 실패:", error);
|
||
|
|
} finally {
|
||
|
|
setLoadingTables(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
fetchTables();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 선택된 테이블의 컬럼 목록 설정 (tableColumns prop 우선 사용)
|
||
|
|
useEffect(() => {
|
||
|
|
console.log("🔍 useEffect 실행됨 - tableColumns:", tableColumns, "length:", tableColumns?.length);
|
||
|
|
if (tableColumns && tableColumns.length > 0) {
|
||
|
|
// tableColumns prop이 있으면 사용
|
||
|
|
console.log("🔧 tableColumns prop 사용:", tableColumns);
|
||
|
|
console.log("🔧 첫 번째 컬럼 상세:", tableColumns[0]);
|
||
|
|
const mappedColumns = tableColumns.map((column: any) => ({
|
||
|
|
columnName: column.columnName || column.name,
|
||
|
|
dataType: column.dataType || column.type || "text",
|
||
|
|
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
|
||
|
|
}));
|
||
|
|
console.log("🏷️ availableColumns 설정됨:", mappedColumns);
|
||
|
|
console.log("🏷️ 첫 번째 mappedColumn:", mappedColumns[0]);
|
||
|
|
setAvailableColumns(mappedColumns);
|
||
|
|
} else if (config.selectedTable || screenTableName) {
|
||
|
|
// API에서 컬럼 정보 가져오기
|
||
|
|
const fetchColumns = async () => {
|
||
|
|
const tableName = config.selectedTable || screenTableName;
|
||
|
|
if (!tableName) {
|
||
|
|
setAvailableColumns([]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log("🔧 API에서 컬럼 정보 가져오기:", tableName);
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/tables/${tableName}/columns`);
|
||
|
|
if (response.ok) {
|
||
|
|
const result = await response.json();
|
||
|
|
if (result.success && result.data) {
|
||
|
|
console.log("🔧 API 응답 컬럼 데이터:", result.data);
|
||
|
|
setAvailableColumns(
|
||
|
|
result.data.map((col: any) => ({
|
||
|
|
columnName: col.columnName,
|
||
|
|
dataType: col.dataType,
|
||
|
|
label: col.displayName || col.columnName,
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("컬럼 목록 가져오기 실패:", error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
fetchColumns();
|
||
|
|
} else {
|
||
|
|
setAvailableColumns([]);
|
||
|
|
}
|
||
|
|
}, [config.selectedTable, screenTableName, tableColumns]);
|
||
|
|
|
||
|
|
const handleChange = (key: keyof TableListConfig, value: any) => {
|
||
|
|
onChange({ [key]: value });
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
|
||
|
|
const parentValue = config[parentKey] as any;
|
||
|
|
onChange({
|
||
|
|
[parentKey]: {
|
||
|
|
...parentValue,
|
||
|
|
[childKey]: value,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 컬럼 추가
|
||
|
|
const addColumn = (columnName: string) => {
|
||
|
|
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
|
||
|
|
if (existingColumn) return;
|
||
|
|
|
||
|
|
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
|
||
|
|
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
|
||
|
|
|
||
|
|
// 라벨명 우선 사용, 없으면 컬럼명 사용
|
||
|
|
const displayName = columnInfo?.label || columnInfo?.displayName || columnName;
|
||
|
|
|
||
|
|
const newColumn: ColumnConfig = {
|
||
|
|
columnName,
|
||
|
|
displayName,
|
||
|
|
visible: true,
|
||
|
|
sortable: true,
|
||
|
|
searchable: true,
|
||
|
|
align: "left",
|
||
|
|
format: "text",
|
||
|
|
order: config.columns?.length || 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
handleChange("columns", [...(config.columns || []), newColumn]);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 컬럼 제거
|
||
|
|
const removeColumn = (columnName: string) => {
|
||
|
|
const updatedColumns = config.columns?.filter((col) => col.columnName !== columnName) || [];
|
||
|
|
handleChange("columns", updatedColumns);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 컬럼 업데이트
|
||
|
|
const updateColumn = (columnName: string, updates: Partial<ColumnConfig>) => {
|
||
|
|
const updatedColumns =
|
||
|
|
config.columns?.map((col) => (col.columnName === columnName ? { ...col, ...updates } : col)) || [];
|
||
|
|
handleChange("columns", updatedColumns);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 컬럼 순서 변경
|
||
|
|
const moveColumn = (columnName: string, direction: "up" | "down") => {
|
||
|
|
const columns = [...(config.columns || [])];
|
||
|
|
const index = columns.findIndex((col) => col.columnName === columnName);
|
||
|
|
|
||
|
|
if (index === -1) return;
|
||
|
|
|
||
|
|
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
||
|
|
if (targetIndex < 0 || targetIndex >= columns.length) return;
|
||
|
|
|
||
|
|
[columns[index], columns[targetIndex]] = [columns[targetIndex], columns[index]];
|
||
|
|
|
||
|
|
// order 값 재정렬
|
||
|
|
columns.forEach((col, idx) => {
|
||
|
|
col.order = idx;
|
||
|
|
});
|
||
|
|
|
||
|
|
handleChange("columns", columns);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div className="text-sm font-medium">테이블 리스트 설정</div>
|
||
|
|
|
||
|
|
<Tabs defaultValue="basic" className="w-full">
|
||
|
|
<TabsList className="grid w-full grid-cols-5">
|
||
|
|
<TabsTrigger value="basic" className="flex items-center gap-1">
|
||
|
|
<Settings className="h-3 w-3" />
|
||
|
|
기본
|
||
|
|
</TabsTrigger>
|
||
|
|
<TabsTrigger value="columns" className="flex items-center gap-1">
|
||
|
|
<Columns className="h-3 w-3" />
|
||
|
|
컬럼
|
||
|
|
</TabsTrigger>
|
||
|
|
<TabsTrigger value="filter" className="flex items-center gap-1">
|
||
|
|
<Filter className="h-3 w-3" />
|
||
|
|
필터
|
||
|
|
</TabsTrigger>
|
||
|
|
<TabsTrigger value="actions" className="flex items-center gap-1">
|
||
|
|
<MousePointer className="h-3 w-3" />
|
||
|
|
액션
|
||
|
|
</TabsTrigger>
|
||
|
|
<TabsTrigger value="style" className="flex items-center gap-1">
|
||
|
|
<Palette className="h-3 w-3" />
|
||
|
|
스타일
|
||
|
|
</TabsTrigger>
|
||
|
|
</TabsList>
|
||
|
|
|
||
|
|
{/* 기본 설정 탭 */}
|
||
|
|
<TabsContent value="basic" className="space-y-4">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">연결된 테이블</CardTitle>
|
||
|
|
<CardDescription>화면에 연결된 테이블 정보가 자동으로 매핑됩니다</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>현재 연결된 테이블</Label>
|
||
|
|
<div className="rounded-md bg-gray-50 p-3">
|
||
|
|
<div className="text-sm font-medium">
|
||
|
|
{screenTableName ? (
|
||
|
|
<span className="text-blue-600">{screenTableName}</span>
|
||
|
|
) : (
|
||
|
|
<span className="text-gray-500">테이블이 연결되지 않았습니다</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{screenTableName && (
|
||
|
|
<div className="mt-1 text-xs text-gray-500">화면 설정에서 자동으로 연결된 테이블입니다</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="title">제목</Label>
|
||
|
|
<Input
|
||
|
|
id="title"
|
||
|
|
value={config.title || ""}
|
||
|
|
onChange={(e) => handleChange("title", e.target.value)}
|
||
|
|
placeholder="테이블 제목 (선택사항)"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">표시 설정</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showHeader"
|
||
|
|
checked={config.showHeader}
|
||
|
|
onCheckedChange={(checked) => handleChange("showHeader", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showHeader">헤더 표시</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showFooter"
|
||
|
|
checked={config.showFooter}
|
||
|
|
onCheckedChange={(checked) => handleChange("showFooter", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showFooter">푸터 표시</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="autoLoad"
|
||
|
|
checked={config.autoLoad}
|
||
|
|
onCheckedChange={(checked) => handleChange("autoLoad", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="autoLoad">자동 데이터 로드</Label>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">높이 설정</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>높이 모드</Label>
|
||
|
|
<Select
|
||
|
|
value={config.height}
|
||
|
|
onValueChange={(value: "auto" | "fixed" | "viewport") => handleChange("height", value)}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="auto">자동</SelectItem>
|
||
|
|
<SelectItem value="fixed">고정</SelectItem>
|
||
|
|
<SelectItem value="viewport">화면 높이</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{config.height === "fixed" && (
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="fixedHeight">고정 높이 (px)</Label>
|
||
|
|
<Input
|
||
|
|
id="fixedHeight"
|
||
|
|
type="number"
|
||
|
|
value={config.fixedHeight || 400}
|
||
|
|
onChange={(e) => handleChange("fixedHeight", parseInt(e.target.value) || 400)}
|
||
|
|
min={200}
|
||
|
|
max={1000}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">페이지네이션</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="paginationEnabled"
|
||
|
|
checked={config.pagination?.enabled}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("pagination", "enabled", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="paginationEnabled">페이지네이션 사용</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{config.pagination?.enabled && (
|
||
|
|
<>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="pageSize">페이지 크기</Label>
|
||
|
|
<Select
|
||
|
|
value={config.pagination?.pageSize?.toString() || "20"}
|
||
|
|
onValueChange={(value) => handleNestedChange("pagination", "pageSize", parseInt(value))}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="10">10개씩</SelectItem>
|
||
|
|
<SelectItem value="20">20개씩</SelectItem>
|
||
|
|
<SelectItem value="50">50개씩</SelectItem>
|
||
|
|
<SelectItem value="100">100개씩</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showSizeSelector"
|
||
|
|
checked={config.pagination?.showSizeSelector}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("pagination", "showSizeSelector", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showSizeSelector">페이지 크기 선택기 표시</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showPageInfo"
|
||
|
|
checked={config.pagination?.showPageInfo}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("pagination", "showPageInfo", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showPageInfo">페이지 정보 표시</Label>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* 컬럼 설정 탭 */}
|
||
|
|
<TabsContent value="columns" className="space-y-4">
|
||
|
|
{!screenTableName ? (
|
||
|
|
<Card>
|
||
|
|
<CardContent className="pt-6">
|
||
|
|
<div className="text-center text-gray-500">
|
||
|
|
<p>테이블이 연결되지 않았습니다.</p>
|
||
|
|
<p className="text-sm">화면에 테이블을 연결한 후 컬럼을 설정할 수 있습니다.</p>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">컬럼 추가 - {screenTableName}</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
{availableColumns.length > 0
|
||
|
|
? `${availableColumns.length}개의 사용 가능한 컬럼에서 선택하세요`
|
||
|
|
: "컬럼 정보를 불러오는 중..."}
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{availableColumns.length > 0 ? (
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{availableColumns
|
||
|
|
.filter((col) => !config.columns?.find((c) => c.columnName === col.columnName))
|
||
|
|
.map((column) => (
|
||
|
|
<Button
|
||
|
|
key={column.columnName}
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => addColumn(column.columnName)}
|
||
|
|
className="flex items-center gap-1"
|
||
|
|
>
|
||
|
|
<Plus className="h-3 w-3" />
|
||
|
|
{column.label || column.columnName}
|
||
|
|
<Badge variant="secondary" className="text-xs">
|
||
|
|
{column.dataType}
|
||
|
|
</Badge>
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="py-4 text-center text-gray-500">
|
||
|
|
<p>컬럼 정보를 불러오는 중입니다...</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{screenTableName && (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">컬럼 설정</CardTitle>
|
||
|
|
<CardDescription>선택된 컬럼들의 표시 옵션을 설정하세요</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<ScrollArea className="h-64">
|
||
|
|
<div className="space-y-3">
|
||
|
|
{config.columns?.map((column, index) => (
|
||
|
|
<div key={column.columnName} className="space-y-3 rounded-lg border p-3">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
checked={column.visible}
|
||
|
|
onCheckedChange={(checked) =>
|
||
|
|
updateColumn(column.columnName, { visible: checked as boolean })
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
<span className="font-medium">
|
||
|
|
{availableColumns.find((col) => col.columnName === column.columnName)?.label ||
|
||
|
|
column.displayName ||
|
||
|
|
column.columnName}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => moveColumn(column.columnName, "up")}
|
||
|
|
disabled={index === 0}
|
||
|
|
>
|
||
|
|
<ArrowUp className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => moveColumn(column.columnName, "down")}
|
||
|
|
disabled={index === (config.columns?.length || 0) - 1}
|
||
|
|
>
|
||
|
|
<ArrowDown className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => removeColumn(column.columnName)}
|
||
|
|
className="text-red-500 hover:text-red-600"
|
||
|
|
>
|
||
|
|
<Trash2 className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{column.visible && (
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label className="text-xs">표시명</Label>
|
||
|
|
<Input
|
||
|
|
value={
|
||
|
|
availableColumns.find((col) => col.columnName === column.columnName)?.label ||
|
||
|
|
column.displayName ||
|
||
|
|
column.columnName
|
||
|
|
}
|
||
|
|
onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })}
|
||
|
|
className="h-8"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label className="text-xs">정렬</Label>
|
||
|
|
<Select
|
||
|
|
value={column.align}
|
||
|
|
onValueChange={(value: "left" | "center" | "right") =>
|
||
|
|
updateColumn(column.columnName, { align: value })
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="h-8">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
||
|
|
<SelectItem value="center">가운데</SelectItem>
|
||
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label className="text-xs">형식</Label>
|
||
|
|
<Select
|
||
|
|
value={column.format}
|
||
|
|
onValueChange={(value: "text" | "number" | "date" | "currency" | "boolean") =>
|
||
|
|
updateColumn(column.columnName, { format: value })
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="h-8">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="text">텍스트</SelectItem>
|
||
|
|
<SelectItem value="number">숫자</SelectItem>
|
||
|
|
<SelectItem value="date">날짜</SelectItem>
|
||
|
|
<SelectItem value="currency">통화</SelectItem>
|
||
|
|
<SelectItem value="boolean">불린</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label className="text-xs">너비 (px)</Label>
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
value={column.width || ""}
|
||
|
|
onChange={(e) =>
|
||
|
|
updateColumn(column.columnName, {
|
||
|
|
width: e.target.value ? parseInt(e.target.value) : undefined,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
placeholder="자동"
|
||
|
|
className="h-8"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-4">
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<Checkbox
|
||
|
|
checked={column.sortable}
|
||
|
|
onCheckedChange={(checked) =>
|
||
|
|
updateColumn(column.columnName, { sortable: checked as boolean })
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
<Label className="text-xs">정렬 가능</Label>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<Checkbox
|
||
|
|
checked={column.searchable}
|
||
|
|
onCheckedChange={(checked) =>
|
||
|
|
updateColumn(column.columnName, { searchable: checked as boolean })
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
<Label className="text-xs">검색 가능</Label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</ScrollArea>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* 필터 설정 탭 */}
|
||
|
|
<TabsContent value="filter" className="space-y-4">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">검색 및 필터</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="filterEnabled"
|
||
|
|
checked={config.filter?.enabled}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("filter", "enabled", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="filterEnabled">필터 기능 사용</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{config.filter?.enabled && (
|
||
|
|
<>
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="quickSearch"
|
||
|
|
checked={config.filter?.quickSearch}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("filter", "quickSearch", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="quickSearch">빠른 검색</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{config.filter?.quickSearch && (
|
||
|
|
<div className="ml-6 flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showColumnSelector"
|
||
|
|
checked={config.filter?.showColumnSelector}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("filter", "showColumnSelector", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showColumnSelector">검색 컬럼 선택기 표시</Label>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="advancedFilter"
|
||
|
|
checked={config.filter?.advancedFilter}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("filter", "advancedFilter", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="advancedFilter">고급 필터</Label>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* 액션 설정 탭 */}
|
||
|
|
<TabsContent value="actions" className="space-y-4">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">행 액션</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showActions"
|
||
|
|
checked={config.actions?.showActions}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("actions", "showActions", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showActions">행 액션 버튼 표시</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="bulkActions"
|
||
|
|
checked={config.actions?.bulkActions}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("actions", "bulkActions", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="bulkActions">일괄 액션 사용</Label>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* 스타일 설정 탭 */}
|
||
|
|
<TabsContent value="style" className="space-y-4">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">테이블 스타일</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>테마</Label>
|
||
|
|
<Select
|
||
|
|
value={config.tableStyle?.theme}
|
||
|
|
onValueChange={(value: "default" | "striped" | "bordered" | "minimal") =>
|
||
|
|
handleNestedChange("tableStyle", "theme", value)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="default">기본</SelectItem>
|
||
|
|
<SelectItem value="striped">줄무늬</SelectItem>
|
||
|
|
<SelectItem value="bordered">테두리</SelectItem>
|
||
|
|
<SelectItem value="minimal">미니멀</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>행 높이</Label>
|
||
|
|
<Select
|
||
|
|
value={config.tableStyle?.rowHeight}
|
||
|
|
onValueChange={(value: "compact" | "normal" | "comfortable") =>
|
||
|
|
handleNestedChange("tableStyle", "rowHeight", value)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="compact">좁음</SelectItem>
|
||
|
|
<SelectItem value="normal">보통</SelectItem>
|
||
|
|
<SelectItem value="comfortable">넓음</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="alternateRows"
|
||
|
|
checked={config.tableStyle?.alternateRows}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("tableStyle", "alternateRows", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="alternateRows">교대로 행 색상 변경</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="hoverEffect"
|
||
|
|
checked={config.tableStyle?.hoverEffect}
|
||
|
|
onCheckedChange={(checked) => handleNestedChange("tableStyle", "hoverEffect", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="hoverEffect">마우스 오버 효과</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="stickyHeader"
|
||
|
|
checked={config.stickyHeader}
|
||
|
|
onCheckedChange={(checked) => handleChange("stickyHeader", checked)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="stickyHeader">헤더 고정</Label>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
</Tabs>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|