1292 lines
59 KiB
TypeScript
1292 lines
59 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useMemo } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Check, ChevronsUpDown, Filter, Table as TableIcon } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
// 타입 import
|
|
import {
|
|
FormSectionConfig,
|
|
TableSectionConfig,
|
|
TableColumnConfig,
|
|
TablePreFilter,
|
|
TableModalFilter,
|
|
TableCalculationRule,
|
|
TABLE_COLUMN_TYPE_OPTIONS,
|
|
FILTER_OPERATOR_OPTIONS,
|
|
MODAL_FILTER_TYPE_OPTIONS,
|
|
} from "../types";
|
|
|
|
import {
|
|
defaultTableSectionConfig,
|
|
defaultTableColumnConfig,
|
|
defaultPreFilterConfig,
|
|
defaultModalFilterConfig,
|
|
defaultCalculationRuleConfig,
|
|
generateTableColumnId,
|
|
generateFilterId,
|
|
} from "../config";
|
|
|
|
// 도움말 텍스트 컴포넌트
|
|
const HelpText = ({ children }: { children: React.ReactNode }) => (
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
|
|
);
|
|
|
|
// 컬럼 설정 아이템 컴포넌트
|
|
interface ColumnSettingItemProps {
|
|
col: TableColumnConfig;
|
|
index: number;
|
|
totalCount: number;
|
|
saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[];
|
|
displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록
|
|
sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 소스 테이블 컬럼
|
|
onUpdate: (updates: Partial<TableColumnConfig>) => void;
|
|
onMoveUp: () => void;
|
|
onMoveDown: () => void;
|
|
onRemove: () => void;
|
|
}
|
|
|
|
function ColumnSettingItem({
|
|
col,
|
|
index,
|
|
totalCount,
|
|
saveTableColumns,
|
|
displayColumns,
|
|
sourceTableColumns,
|
|
onUpdate,
|
|
onMoveUp,
|
|
onMoveDown,
|
|
onRemove,
|
|
}: ColumnSettingItemProps) {
|
|
const [fieldSearchOpen, setFieldSearchOpen] = useState(false);
|
|
const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="border rounded-lg p-3 space-y-3 bg-card">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">{col.label || col.field || `컬럼 ${index + 1}`}</span>
|
|
<Badge variant="secondary" className="text-xs">
|
|
{TABLE_COLUMN_TYPE_OPTIONS.find((t) => t.value === col.type)?.label || col.type}
|
|
</Badge>
|
|
{col.calculated && <Badge variant="outline" className="text-xs">계산</Badge>}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Button size="sm" variant="ghost" onClick={onMoveUp} disabled={index === 0} className="h-7 w-7 p-0">
|
|
<ChevronUp className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onClick={onMoveDown} disabled={index === totalCount - 1} className="h-7 w-7 p-0">
|
|
<ChevronDown className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onClick={onRemove} className="h-7 w-7 p-0 text-destructive hover:text-destructive">
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-5 gap-3">
|
|
{/* 필드명 - Combobox (저장할 컬럼) */}
|
|
<div>
|
|
<Label className="text-xs">필드명 (저장)</Label>
|
|
<Popover open={fieldSearchOpen} onOpenChange={setFieldSearchOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={fieldSearchOpen}
|
|
className="h-8 w-full justify-between text-xs mt-1"
|
|
>
|
|
<span className="truncate">
|
|
{col.field || "필드 선택..."}
|
|
</span>
|
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 w-[250px]" align="start">
|
|
<Command>
|
|
<CommandInput placeholder="필드 검색..." className="text-xs" />
|
|
<CommandList className="max-h-[200px]">
|
|
<CommandEmpty className="text-xs py-4 text-center">
|
|
필드를 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{saveTableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.column_name}
|
|
value={column.column_name}
|
|
onSelect={() => {
|
|
onUpdate({
|
|
field: column.column_name,
|
|
// 라벨이 비어있으면 comment로 자동 설정
|
|
...((!col.label || col.label.startsWith("컬럼 ")) && column.comment ? { label: column.comment } : {})
|
|
});
|
|
setFieldSearchOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.field === column.column_name ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col flex-1 min-w-0">
|
|
<span className="font-medium truncate">{column.column_name}</span>
|
|
<span className="text-[10px] text-muted-foreground truncate">
|
|
{column.comment || column.data_type}
|
|
</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 소스 필드 - Combobox (검색 모달에서 가져올 컬럼) */}
|
|
<div>
|
|
<Label className="text-xs">소스 필드</Label>
|
|
<Popover open={sourceFieldSearchOpen} onOpenChange={setSourceFieldSearchOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={sourceFieldSearchOpen}
|
|
className="h-8 w-full justify-between text-xs mt-1"
|
|
disabled={displayColumns.length === 0}
|
|
>
|
|
<span className="truncate">
|
|
{col.sourceField || "(필드명과 동일)"}
|
|
</span>
|
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 w-[250px]" align="start">
|
|
<Command>
|
|
<CommandInput placeholder="소스 필드 검색..." className="text-xs" />
|
|
<CommandList className="max-h-[200px]">
|
|
<CommandEmpty className="text-xs py-4 text-center">
|
|
소스 필드를 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{/* 필드명과 동일 옵션 */}
|
|
<CommandItem
|
|
value="__same_as_field__"
|
|
onSelect={() => {
|
|
onUpdate({ sourceField: undefined });
|
|
setSourceFieldSearchOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
!col.sourceField ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<span className="text-muted-foreground">(필드명과 동일)</span>
|
|
</CommandItem>
|
|
{/* 표시 컬럼 목록 */}
|
|
{displayColumns.map((colName) => {
|
|
const colInfo = sourceTableColumns.find((c) => c.column_name === colName);
|
|
return (
|
|
<CommandItem
|
|
key={colName}
|
|
value={colName}
|
|
onSelect={() => {
|
|
onUpdate({ sourceField: colName });
|
|
setSourceFieldSearchOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.sourceField === colName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col flex-1 min-w-0">
|
|
<span className="font-medium truncate">{colName}</span>
|
|
{colInfo?.comment && (
|
|
<span className="text-[10px] text-muted-foreground truncate">
|
|
{colInfo.comment}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 라벨 */}
|
|
<div>
|
|
<Label className="text-xs">라벨</Label>
|
|
<Input
|
|
value={col.label}
|
|
onChange={(e) => onUpdate({ label: e.target.value })}
|
|
placeholder="표시 라벨"
|
|
className="h-8 text-xs mt-1"
|
|
/>
|
|
</div>
|
|
|
|
{/* 타입 */}
|
|
<div>
|
|
<Label className="text-xs">타입</Label>
|
|
<Select value={col.type} onValueChange={(value: any) => onUpdate({ type: value })}>
|
|
<SelectTrigger className="h-8 text-xs mt-1">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TABLE_COLUMN_TYPE_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 너비 */}
|
|
<div>
|
|
<Label className="text-xs">너비</Label>
|
|
<Input
|
|
value={col.width || ""}
|
|
onChange={(e) => onUpdate({ width: e.target.value })}
|
|
placeholder="150px"
|
|
className="h-8 text-xs mt-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 옵션 스위치 */}
|
|
<div className="flex items-center gap-4">
|
|
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
|
<Switch
|
|
checked={col.editable ?? true}
|
|
onCheckedChange={(checked) => onUpdate({ editable: checked })}
|
|
className="scale-75"
|
|
/>
|
|
<span>편집 가능</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
|
<Switch
|
|
checked={col.calculated ?? false}
|
|
onCheckedChange={(checked) => onUpdate({ calculated: checked })}
|
|
className="scale-75"
|
|
/>
|
|
<span>계산 필드</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
|
<Switch
|
|
checked={col.required ?? false}
|
|
onCheckedChange={(checked) => onUpdate({ required: checked })}
|
|
className="scale-75"
|
|
/>
|
|
<span>필수</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface TableSectionSettingsModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
section: FormSectionConfig;
|
|
onSave: (updates: Partial<FormSectionConfig>) => void;
|
|
tables: { table_name: string; comment?: string }[];
|
|
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>;
|
|
onLoadTableColumns: (tableName: string) => void;
|
|
// 카테고리 목록 (table_column_category_values에서 가져옴)
|
|
categoryList?: { tableName: string; columnName: string; displayName?: string }[];
|
|
onLoadCategoryList?: () => void;
|
|
}
|
|
|
|
export function TableSectionSettingsModal({
|
|
open,
|
|
onOpenChange,
|
|
section,
|
|
onSave,
|
|
tables,
|
|
tableColumns,
|
|
onLoadTableColumns,
|
|
categoryList = [],
|
|
onLoadCategoryList,
|
|
}: TableSectionSettingsModalProps) {
|
|
// 로컬 상태
|
|
const [title, setTitle] = useState(section.title);
|
|
const [description, setDescription] = useState(section.description || "");
|
|
const [tableConfig, setTableConfig] = useState<TableSectionConfig>(
|
|
section.tableConfig || { ...defaultTableSectionConfig }
|
|
);
|
|
|
|
// 테이블 검색 Combobox 상태
|
|
const [tableSearchOpen, setTableSearchOpen] = useState(false);
|
|
const [saveTableSearchOpen, setSaveTableSearchOpen] = useState(false);
|
|
|
|
// 활성 탭
|
|
const [activeTab, setActiveTab] = useState("source");
|
|
|
|
// open이 변경될 때마다 데이터 동기화
|
|
useEffect(() => {
|
|
if (open) {
|
|
setTitle(section.title);
|
|
setDescription(section.description || "");
|
|
setTableConfig(section.tableConfig || { ...defaultTableSectionConfig });
|
|
}
|
|
}, [open, section]);
|
|
|
|
// 소스 테이블 변경 시 컬럼 로드
|
|
useEffect(() => {
|
|
if (tableConfig.source.tableName) {
|
|
onLoadTableColumns(tableConfig.source.tableName);
|
|
}
|
|
}, [tableConfig.source.tableName, onLoadTableColumns]);
|
|
|
|
// 저장 테이블 변경 시 컬럼 로드
|
|
useEffect(() => {
|
|
if (tableConfig.saveConfig?.targetTable) {
|
|
onLoadTableColumns(tableConfig.saveConfig.targetTable);
|
|
}
|
|
}, [tableConfig.saveConfig?.targetTable, onLoadTableColumns]);
|
|
|
|
// 소스 테이블의 컬럼 목록
|
|
const sourceTableColumns = useMemo(() => {
|
|
return tableColumns[tableConfig.source.tableName] || [];
|
|
}, [tableColumns, tableConfig.source.tableName]);
|
|
|
|
// 저장 테이블의 컬럼 목록
|
|
const saveTableColumns = useMemo(() => {
|
|
// 저장 테이블이 지정되어 있으면 해당 테이블의 컬럼, 아니면 소스 테이블의 컬럼 사용
|
|
const targetTable = tableConfig.saveConfig?.targetTable;
|
|
if (targetTable) {
|
|
return tableColumns[targetTable] || [];
|
|
}
|
|
return sourceTableColumns;
|
|
}, [tableColumns, tableConfig.saveConfig?.targetTable, sourceTableColumns]);
|
|
|
|
// 설정 업데이트 함수
|
|
const updateTableConfig = (updates: Partial<TableSectionConfig>) => {
|
|
setTableConfig((prev) => ({ ...prev, ...updates }));
|
|
};
|
|
|
|
const updateSource = (updates: Partial<TableSectionConfig["source"]>) => {
|
|
updateTableConfig({
|
|
source: { ...tableConfig.source, ...updates },
|
|
});
|
|
};
|
|
|
|
const updateFilters = (updates: Partial<TableSectionConfig["filters"]>) => {
|
|
updateTableConfig({
|
|
filters: { ...tableConfig.filters, ...updates },
|
|
});
|
|
};
|
|
|
|
const updateUiConfig = (updates: Partial<NonNullable<TableSectionConfig["uiConfig"]>>) => {
|
|
updateTableConfig({
|
|
uiConfig: { ...tableConfig.uiConfig, ...updates },
|
|
});
|
|
};
|
|
|
|
const updateSaveConfig = (updates: Partial<NonNullable<TableSectionConfig["saveConfig"]>>) => {
|
|
updateTableConfig({
|
|
saveConfig: { ...tableConfig.saveConfig, ...updates },
|
|
});
|
|
};
|
|
|
|
// 저장 함수
|
|
const handleSave = () => {
|
|
onSave({
|
|
title,
|
|
description,
|
|
tableConfig,
|
|
});
|
|
onOpenChange(false);
|
|
};
|
|
|
|
// 컬럼 추가
|
|
const addColumn = () => {
|
|
const newColumn: TableColumnConfig = {
|
|
...defaultTableColumnConfig,
|
|
field: `column_${(tableConfig.columns || []).length + 1}`,
|
|
label: `컬럼 ${(tableConfig.columns || []).length + 1}`,
|
|
};
|
|
updateTableConfig({
|
|
columns: [...(tableConfig.columns || []), newColumn],
|
|
});
|
|
};
|
|
|
|
// 컬럼 삭제
|
|
const removeColumn = (index: number) => {
|
|
updateTableConfig({
|
|
columns: (tableConfig.columns || []).filter((_, i) => i !== index),
|
|
});
|
|
};
|
|
|
|
// 컬럼 업데이트
|
|
const updateColumn = (index: number, updates: Partial<TableColumnConfig>) => {
|
|
updateTableConfig({
|
|
columns: (tableConfig.columns || []).map((col, i) =>
|
|
i === index ? { ...col, ...updates } : col
|
|
),
|
|
});
|
|
};
|
|
|
|
// 컬럼 이동
|
|
const moveColumn = (index: number, direction: "up" | "down") => {
|
|
const columns = [...(tableConfig.columns || [])];
|
|
if (direction === "up" && index > 0) {
|
|
[columns[index - 1], columns[index]] = [columns[index], columns[index - 1]];
|
|
} else if (direction === "down" && index < columns.length - 1) {
|
|
[columns[index], columns[index + 1]] = [columns[index + 1], columns[index]];
|
|
}
|
|
updateTableConfig({ columns });
|
|
};
|
|
|
|
// 사전 필터 추가
|
|
const addPreFilter = () => {
|
|
const newFilter: TablePreFilter = { ...defaultPreFilterConfig };
|
|
updateFilters({
|
|
preFilters: [...(tableConfig.filters?.preFilters || []), newFilter],
|
|
});
|
|
};
|
|
|
|
// 사전 필터 삭제
|
|
const removePreFilter = (index: number) => {
|
|
updateFilters({
|
|
preFilters: (tableConfig.filters?.preFilters || []).filter((_, i) => i !== index),
|
|
});
|
|
};
|
|
|
|
// 사전 필터 업데이트
|
|
const updatePreFilter = (index: number, updates: Partial<TablePreFilter>) => {
|
|
updateFilters({
|
|
preFilters: (tableConfig.filters?.preFilters || []).map((f, i) =>
|
|
i === index ? { ...f, ...updates } : f
|
|
),
|
|
});
|
|
};
|
|
|
|
// 모달 필터 추가
|
|
const addModalFilter = () => {
|
|
const newFilter: TableModalFilter = { ...defaultModalFilterConfig };
|
|
updateFilters({
|
|
modalFilters: [...(tableConfig.filters?.modalFilters || []), newFilter],
|
|
});
|
|
};
|
|
|
|
// 모달 필터 삭제
|
|
const removeModalFilter = (index: number) => {
|
|
updateFilters({
|
|
modalFilters: (tableConfig.filters?.modalFilters || []).filter((_, i) => i !== index),
|
|
});
|
|
};
|
|
|
|
// 모달 필터 업데이트
|
|
const updateModalFilter = (index: number, updates: Partial<TableModalFilter>) => {
|
|
updateFilters({
|
|
modalFilters: (tableConfig.filters?.modalFilters || []).map((f, i) =>
|
|
i === index ? { ...f, ...updates } : f
|
|
),
|
|
});
|
|
};
|
|
|
|
// 계산 규칙 추가
|
|
const addCalculation = () => {
|
|
const newCalc: TableCalculationRule = { ...defaultCalculationRuleConfig };
|
|
updateTableConfig({
|
|
calculations: [...(tableConfig.calculations || []), newCalc],
|
|
});
|
|
};
|
|
|
|
// 계산 규칙 삭제
|
|
const removeCalculation = (index: number) => {
|
|
updateTableConfig({
|
|
calculations: (tableConfig.calculations || []).filter((_, i) => i !== index),
|
|
});
|
|
};
|
|
|
|
// 계산 규칙 업데이트
|
|
const updateCalculation = (index: number, updates: Partial<TableCalculationRule>) => {
|
|
updateTableConfig({
|
|
calculations: (tableConfig.calculations || []).map((c, i) =>
|
|
i === index ? { ...c, ...updates } : c
|
|
),
|
|
});
|
|
};
|
|
|
|
// 표시 컬럼 토글
|
|
const toggleDisplayColumn = (columnName: string) => {
|
|
const current = tableConfig.source.displayColumns || [];
|
|
if (current.includes(columnName)) {
|
|
updateSource({
|
|
displayColumns: current.filter((c) => c !== columnName),
|
|
});
|
|
} else {
|
|
updateSource({
|
|
displayColumns: [...current, columnName],
|
|
});
|
|
}
|
|
};
|
|
|
|
// 검색 컬럼 토글
|
|
const toggleSearchColumn = (columnName: string) => {
|
|
const current = tableConfig.source.searchColumns || [];
|
|
if (current.includes(columnName)) {
|
|
updateSource({
|
|
searchColumns: current.filter((c) => c !== columnName),
|
|
});
|
|
} else {
|
|
updateSource({
|
|
searchColumns: [...current, columnName],
|
|
});
|
|
}
|
|
};
|
|
|
|
// 표시 컬럼 순서 변경
|
|
const moveDisplayColumn = (index: number, direction: "up" | "down") => {
|
|
const columns = [...(tableConfig.source.displayColumns || [])];
|
|
if (direction === "up" && index > 0) {
|
|
[columns[index - 1], columns[index]] = [columns[index], columns[index - 1]];
|
|
} else if (direction === "down" && index < columns.length - 1) {
|
|
[columns[index], columns[index + 1]] = [columns[index + 1], columns[index]];
|
|
}
|
|
updateSource({ displayColumns: columns });
|
|
};
|
|
|
|
// 표시 컬럼 삭제 (순서 편집 영역에서)
|
|
const removeDisplayColumn = (columnName: string) => {
|
|
updateSource({
|
|
displayColumns: (tableConfig.source.displayColumns || []).filter((c) => c !== columnName),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[900px] max-h-[90vh] flex flex-col">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">테이블 섹션 설정</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
테이블 형식의 데이터를 표시하고 편집하는 섹션을 설정합니다.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
<ScrollArea className="h-[calc(90vh-180px)]">
|
|
<div className="space-y-4 p-1">
|
|
{/* 기본 정보 */}
|
|
<div className="space-y-3 border rounded-lg p-4">
|
|
<h4 className="text-sm font-medium">기본 정보</h4>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs font-medium mb-1.5 block">섹션 제목</Label>
|
|
<Input
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="예: 품목 목록"
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs font-medium mb-1.5 block">설명 (선택)</Label>
|
|
<Input
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="섹션에 대한 설명"
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 탭 구성 */}
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
<TabsList className="w-full grid grid-cols-4">
|
|
<TabsTrigger key="source" value="source" className="text-xs">테이블 설정</TabsTrigger>
|
|
<TabsTrigger key="columns" value="columns" className="text-xs">컬럼 설정</TabsTrigger>
|
|
<TabsTrigger key="filters" value="filters" className="text-xs">검색 설정</TabsTrigger>
|
|
<TabsTrigger key="advanced" value="advanced" className="text-xs">고급 설정</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 테이블 설정 탭 */}
|
|
<TabsContent key="source-content" value="source" className="mt-4 space-y-4">
|
|
{/* 소스 테이블 설정 */}
|
|
<div className="space-y-3 border rounded-lg p-4">
|
|
<h4 className="text-sm font-medium">검색용 소스 테이블</h4>
|
|
<p className="text-xs text-muted-foreground -mt-1">검색 모달에서 데이터를 가져올 테이블입니다.</p>
|
|
|
|
<div>
|
|
<Label className="text-xs font-medium mb-1.5 block">테이블 선택</Label>
|
|
<Popover open={tableSearchOpen} onOpenChange={setTableSearchOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={tableSearchOpen}
|
|
className="h-9 w-full justify-between text-sm"
|
|
>
|
|
{tableConfig.source.tableName || "테이블 선택..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 w-full min-w-[400px]" align="start">
|
|
<Command>
|
|
<CommandInput placeholder="테이블 검색..." className="text-sm" />
|
|
<CommandList className="max-h-[300px]">
|
|
<CommandEmpty className="text-sm py-6 text-center">
|
|
테이블을 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.table_name}
|
|
value={table.table_name}
|
|
onSelect={() => {
|
|
updateSource({ tableName: table.table_name });
|
|
setTableSearchOpen(false);
|
|
}}
|
|
className="text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
tableConfig.source.tableName === table.table_name ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{table.table_name}</span>
|
|
{table.comment && (
|
|
<span className="text-xs text-muted-foreground">{table.comment}</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 저장 테이블 설정 */}
|
|
<div className="space-y-3 border rounded-lg p-4">
|
|
<h4 className="text-sm font-medium">저장용 테이블</h4>
|
|
<p className="text-xs text-muted-foreground -mt-1">테이블 섹션 데이터를 저장할 테이블입니다. 미설정 시 메인 테이블에 저장됩니다.</p>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">저장 테이블</Label>
|
|
<Popover open={saveTableSearchOpen} onOpenChange={setSaveTableSearchOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={saveTableSearchOpen}
|
|
className="h-9 w-full justify-between text-sm mt-1"
|
|
>
|
|
{tableConfig.saveConfig?.targetTable || "(메인 테이블과 동일)"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 w-full min-w-[300px]" align="start">
|
|
<Command>
|
|
<CommandInput placeholder="테이블 검색..." className="text-sm" />
|
|
<CommandList className="max-h-[200px]">
|
|
<CommandEmpty className="text-sm py-4 text-center">
|
|
테이블을 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
<CommandItem
|
|
value=""
|
|
onSelect={() => {
|
|
updateSaveConfig({ targetTable: undefined });
|
|
setSaveTableSearchOpen(false);
|
|
}}
|
|
className="text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
!tableConfig.saveConfig?.targetTable ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<span className="text-muted-foreground">(메인 테이블과 동일)</span>
|
|
</CommandItem>
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.table_name}
|
|
value={table.table_name}
|
|
onSelect={() => {
|
|
updateSaveConfig({ targetTable: table.table_name });
|
|
// 선택 즉시 컬럼 로드 요청
|
|
onLoadTableColumns(table.table_name);
|
|
setSaveTableSearchOpen(false);
|
|
}}
|
|
className="text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
tableConfig.saveConfig?.targetTable === table.table_name ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{table.table_name}</span>
|
|
{table.comment && (
|
|
<span className="text-xs text-muted-foreground">{table.comment}</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">중복 체크 필드</Label>
|
|
<Input
|
|
value={tableConfig.saveConfig?.uniqueField || ""}
|
|
onChange={(e) => updateSaveConfig({ uniqueField: e.target.value || undefined })}
|
|
placeholder="예: item_id"
|
|
className="h-9 text-sm mt-1"
|
|
/>
|
|
<HelpText>동일 값이 있으면 추가하지 않습니다.</HelpText>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 컬럼 설정 탭 */}
|
|
<TabsContent key="columns-content" value="columns" className="mt-4 space-y-4">
|
|
{/* 안내 메시지 */}
|
|
{saveTableColumns.length === 0 && !tableConfig.saveConfig?.targetTable && !tableConfig.source.tableName && (
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
|
<p className="text-xs text-amber-800">
|
|
"테이블 설정" 탭에서 저장 테이블을 먼저 선택해주세요. 선택한 테이블의 컬럼을 여기서 설정할 수 있습니다.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 테이블은 선택했지만 컬럼이 아직 로드되지 않은 경우 */}
|
|
{saveTableColumns.length === 0 && (tableConfig.saveConfig?.targetTable || tableConfig.source.tableName) && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
<p className="text-xs text-blue-800">
|
|
테이블 "{tableConfig.saveConfig?.targetTable || tableConfig.source.tableName}" 의 컬럼을 불러오는 중입니다...
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<Label className="text-sm font-medium">테이블 컬럼</Label>
|
|
{saveTableColumns.length > 0 && (
|
|
<p className="text-xs text-muted-foreground">
|
|
사용 가능한 컬럼: {saveTableColumns.length}개 ({tableConfig.saveConfig?.targetTable || tableConfig.source.tableName || "테이블 미선택"})
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={addColumn} className="h-8 text-xs">
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{(tableConfig.columns || []).length === 0 ? (
|
|
<div className="text-center py-8 border border-dashed rounded-lg bg-muted/20">
|
|
<TableIcon className="h-8 w-8 mx-auto mb-2 text-muted-foreground/50" />
|
|
<p className="text-sm text-muted-foreground">컬럼이 없습니다</p>
|
|
<p className="text-xs text-muted-foreground mt-1">"컬럼 추가" 버튼으로 추가하세요</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{(tableConfig.columns || []).map((col, index) => (
|
|
<ColumnSettingItem
|
|
key={index}
|
|
col={col}
|
|
index={index}
|
|
totalCount={(tableConfig.columns || []).length}
|
|
saveTableColumns={saveTableColumns}
|
|
displayColumns={tableConfig.source.displayColumns || []}
|
|
sourceTableColumns={sourceTableColumns}
|
|
onUpdate={(updates) => updateColumn(index, updates)}
|
|
onMoveUp={() => moveColumn(index, "up")}
|
|
onMoveDown={() => moveColumn(index, "down")}
|
|
onRemove={() => removeColumn(index)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{/* 검색 설정 탭 */}
|
|
<TabsContent key="filters-content" value="filters" className="mt-4 space-y-4">
|
|
{/* 표시 컬럼 / 검색 컬럼 설정 */}
|
|
<div className="space-y-4 border rounded-lg p-4">
|
|
<h4 className="text-sm font-medium">검색 모달 컬럼 설정</h4>
|
|
<p className="text-xs text-muted-foreground -mt-2">검색 모달에서 보여줄 컬럼과 검색 대상 컬럼을 설정합니다.</p>
|
|
|
|
{/* 소스 테이블 미선택 시 안내 */}
|
|
{!tableConfig.source.tableName && (
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
|
<p className="text-xs text-amber-800">
|
|
"테이블 설정" 탭에서 검색용 소스 테이블을 먼저 선택해주세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 표시 컬럼 선택 */}
|
|
{sourceTableColumns.length > 0 && (
|
|
<div>
|
|
<Label className="text-xs font-medium mb-1.5 block">표시 컬럼 (모달 테이블에 보여줄 컬럼)</Label>
|
|
<div className="border rounded-lg p-3 max-h-[150px] overflow-y-auto">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{sourceTableColumns.map((col) => (
|
|
<label
|
|
key={col.column_name}
|
|
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 p-1.5 rounded"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={(tableConfig.source.displayColumns || []).includes(col.column_name)}
|
|
onChange={() => toggleDisplayColumn(col.column_name)}
|
|
className="rounded"
|
|
/>
|
|
<span className="flex-1 truncate">{col.column_name}</span>
|
|
{col.comment && (
|
|
<span className="text-xs text-muted-foreground truncate max-w-[100px]">
|
|
{col.comment}
|
|
</span>
|
|
)}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<HelpText>선택된 컬럼: {(tableConfig.source.displayColumns || []).length}개</HelpText>
|
|
|
|
{/* 선택된 컬럼 순서 편집 */}
|
|
{(tableConfig.source.displayColumns || []).length > 0 && (
|
|
<div className="mt-3">
|
|
<Label className="text-xs font-medium mb-1.5 block text-muted-foreground">컬럼 순서 편집</Label>
|
|
<div className="border rounded-lg p-2 space-y-1 bg-muted/30">
|
|
{(tableConfig.source.displayColumns || []).map((colName, index) => {
|
|
const colInfo = sourceTableColumns.find((c) => c.column_name === colName);
|
|
return (
|
|
<div
|
|
key={colName}
|
|
className="flex items-center gap-2 bg-background rounded px-2 py-1.5"
|
|
>
|
|
<GripVertical className="h-3.5 w-3.5 text-muted-foreground" />
|
|
<span className="text-xs font-medium flex-1">{colName}</span>
|
|
{colInfo?.comment && (
|
|
<span className="text-[10px] text-muted-foreground truncate max-w-[80px]">
|
|
{colInfo.comment}
|
|
</span>
|
|
)}
|
|
<div className="flex items-center gap-0.5">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => moveDisplayColumn(index, "up")}
|
|
disabled={index === 0}
|
|
className="h-6 w-6 p-0"
|
|
>
|
|
<ChevronUp className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => moveDisplayColumn(index, "down")}
|
|
disabled={index === (tableConfig.source.displayColumns || []).length - 1}
|
|
className="h-6 w-6 p-0"
|
|
>
|
|
<ChevronDown className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeDisplayColumn(colName)}
|
|
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 검색 컬럼 선택 */}
|
|
{sourceTableColumns.length > 0 && (
|
|
<div>
|
|
<Label className="text-xs font-medium mb-1.5 block">검색 컬럼 (검색어 입력 시 조회 대상 컬럼)</Label>
|
|
<div className="border rounded-lg p-3 max-h-[150px] overflow-y-auto">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{sourceTableColumns.map((col) => (
|
|
<label
|
|
key={col.column_name}
|
|
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 p-1.5 rounded"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={(tableConfig.source.searchColumns || []).includes(col.column_name)}
|
|
onChange={() => toggleSearchColumn(col.column_name)}
|
|
className="rounded"
|
|
/>
|
|
<span className="flex-1 truncate">{col.column_name}</span>
|
|
{col.comment && (
|
|
<span className="text-xs text-muted-foreground truncate max-w-[100px]">
|
|
{col.comment}
|
|
</span>
|
|
)}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<HelpText>검색 컬럼: {(tableConfig.source.searchColumns || []).length}개</HelpText>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 사전 필터 */}
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<Label className="text-sm font-medium">사전 필터</Label>
|
|
<p className="text-xs text-muted-foreground">항상 적용되는 필터 조건입니다.</p>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={addPreFilter} className="h-8 text-xs">
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{(tableConfig.filters?.preFilters || []).map((filter, index) => (
|
|
<div key={index} className="flex items-center gap-2 border rounded-lg p-2 bg-card">
|
|
<Select
|
|
value={filter.column || undefined}
|
|
onValueChange={(value) => updatePreFilter(index, { column: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs w-[150px]">
|
|
<SelectValue placeholder="컬럼" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceTableColumns
|
|
.filter((col) => col.column_name)
|
|
.map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name}>
|
|
{col.column_name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select
|
|
value={filter.operator || undefined}
|
|
onValueChange={(value: any) => updatePreFilter(index, { operator: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs w-[100px]">
|
|
<SelectValue placeholder="연산자" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{FILTER_OPERATOR_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Input
|
|
value={filter.value || ""}
|
|
onChange={(e) => updatePreFilter(index, { value: e.target.value })}
|
|
placeholder="값"
|
|
className="h-8 text-xs flex-1"
|
|
/>
|
|
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removePreFilter(index)}
|
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 모달 필터 */}
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<Label className="text-sm font-medium">모달 필터</Label>
|
|
<p className="text-xs text-muted-foreground">사용자가 선택할 수 있는 필터입니다.</p>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={addModalFilter} className="h-8 text-xs">
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{(tableConfig.filters?.modalFilters || []).map((filter, index) => (
|
|
<div key={index} className="border rounded-lg p-3 space-y-2 bg-card">
|
|
<div className="flex items-center gap-2">
|
|
{/* 컬럼 선택 */}
|
|
<Select
|
|
value={filter.column || undefined}
|
|
onValueChange={(value) => {
|
|
// 컬럼 선택 시 자동으로 categoryRef 설정
|
|
updateModalFilter(index, {
|
|
column: value,
|
|
categoryRef: tableConfig.source.tableName ? {
|
|
tableName: tableConfig.source.tableName,
|
|
columnName: value,
|
|
} : undefined,
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs w-[130px]">
|
|
<SelectValue placeholder="컬럼" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceTableColumns
|
|
.filter((col) => col.column_name)
|
|
.map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name}>
|
|
{col.column_name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 라벨 */}
|
|
<Input
|
|
value={filter.label || ""}
|
|
onChange={(e) => updateModalFilter(index, { label: e.target.value })}
|
|
placeholder="라벨"
|
|
className="h-8 text-xs w-[100px]"
|
|
/>
|
|
|
|
{/* 타입 */}
|
|
<Select
|
|
value={filter.type || undefined}
|
|
onValueChange={(value: any) => updateModalFilter(index, { type: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs w-[100px]">
|
|
<SelectValue placeholder="타입" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{MODAL_FILTER_TYPE_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 카테고리 선택 (타입이 category일 때만 표시) */}
|
|
{filter.type === "category" && (
|
|
<Select
|
|
value={filter.categoryRef ? `${filter.categoryRef.tableName}.${filter.categoryRef.columnName}` : undefined}
|
|
onValueChange={(value) => {
|
|
const [tableName, columnName] = value.split(".");
|
|
updateModalFilter(index, {
|
|
categoryRef: { tableName, columnName }
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs w-[180px]">
|
|
<SelectValue placeholder="카테고리 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{/* 현재 소스 테이블의 컬럼 기반 카테고리 */}
|
|
{filter.column && tableConfig.source.tableName && (
|
|
<SelectItem
|
|
value={`${tableConfig.source.tableName}.${filter.column}`}
|
|
>
|
|
{tableConfig.source.tableName}.{filter.column}
|
|
</SelectItem>
|
|
)}
|
|
{/* 카테고리 목록에서 추가 */}
|
|
{categoryList
|
|
.filter((cat) =>
|
|
// 이미 위에서 추가한 항목 제외
|
|
!(cat.tableName === tableConfig.source.tableName && cat.columnName === filter.column)
|
|
)
|
|
.map((cat) => (
|
|
<SelectItem
|
|
key={`${cat.tableName}.${cat.columnName}`}
|
|
value={`${cat.tableName}.${cat.columnName}`}
|
|
>
|
|
{cat.displayName || `${cat.tableName}.${cat.columnName}`}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
|
|
{/* 삭제 버튼 */}
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeModalFilter(index)}
|
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 고급 설정 탭 */}
|
|
<TabsContent key="advanced-content" value="advanced" className="mt-4 space-y-4">
|
|
{/* UI 설정 */}
|
|
<div className="space-y-3 border rounded-lg p-4">
|
|
<h4 className="text-sm font-medium">UI 설정</h4>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">추가 버튼 텍스트</Label>
|
|
<Input
|
|
value={tableConfig.uiConfig?.addButtonText || ""}
|
|
onChange={(e) => updateUiConfig({ addButtonText: e.target.value })}
|
|
placeholder="항목 검색"
|
|
className="h-8 text-xs mt-1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">모달 제목</Label>
|
|
<Input
|
|
value={tableConfig.uiConfig?.modalTitle || ""}
|
|
onChange={(e) => updateUiConfig({ modalTitle: e.target.value })}
|
|
placeholder="항목 검색 및 선택"
|
|
className="h-8 text-xs mt-1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">테이블 최대 높이</Label>
|
|
<Input
|
|
value={tableConfig.uiConfig?.maxHeight || ""}
|
|
onChange={(e) => updateUiConfig({ maxHeight: e.target.value })}
|
|
placeholder="400px"
|
|
className="h-8 text-xs mt-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
|
<Switch
|
|
checked={tableConfig.uiConfig?.multiSelect ?? true}
|
|
onCheckedChange={(checked) => updateUiConfig({ multiSelect: checked })}
|
|
className="scale-75"
|
|
/>
|
|
<span>다중 선택 허용</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 계산 규칙 */}
|
|
<div className="space-y-3 border rounded-lg p-4">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h4 className="text-sm font-medium">계산 규칙</h4>
|
|
<p className="text-xs text-muted-foreground">다른 컬럼 값을 기반으로 자동 계산합니다.</p>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={addCalculation} className="h-8 text-xs">
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{(tableConfig.calculations || []).map((calc, index) => (
|
|
<div key={index} className="flex items-center gap-2 border rounded-lg p-2 bg-muted/30">
|
|
<Select
|
|
value={calc.resultField || ""}
|
|
onValueChange={(value) => updateCalculation(index, { resultField: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs w-[150px]">
|
|
<SelectValue placeholder="결과 필드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(tableConfig.columns || []).length === 0 ? (
|
|
<SelectItem value="__no_columns__" disabled>
|
|
컬럼 설정에서 먼저 컬럼을 추가하세요
|
|
</SelectItem>
|
|
) : (
|
|
(tableConfig.columns || []).map((col) => (
|
|
<SelectItem key={col.field} value={col.field}>
|
|
{col.label || col.field}
|
|
</SelectItem>
|
|
))
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
<span className="text-xs text-muted-foreground">=</span>
|
|
<Input
|
|
value={calc.formula}
|
|
onChange={(e) => updateCalculation(index, { formula: e.target.value })}
|
|
placeholder="수식 (예: quantity * unit_price)"
|
|
className="h-8 text-xs flex-1"
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeCalculation(index)}
|
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button key="cancel" variant="outline" onClick={() => onOpenChange(false)} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
|
취소
|
|
</Button>
|
|
<Button key="save" onClick={handleSave} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|