ERP-node/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx

3461 lines
171 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, Search } from "lucide-react";
import { cn } from "@/lib/utils";
// 타입 import
import {
FormSectionConfig,
TableSectionConfig,
TableColumnConfig,
TablePreFilter,
TableModalFilter,
TableCalculationRule,
LookupOption,
LookupCondition,
ConditionalTableOption,
TABLE_COLUMN_TYPE_OPTIONS,
FILTER_OPERATOR_OPTIONS,
MODAL_FILTER_TYPE_OPTIONS,
LOOKUP_TYPE_OPTIONS,
LOOKUP_CONDITION_SOURCE_OPTIONS,
CONDITIONAL_TABLE_TRIGGER_OPTIONS,
} from "../types";
import {
defaultTableSectionConfig,
defaultTableColumnConfig,
defaultPreFilterConfig,
defaultModalFilterConfig,
defaultCalculationRuleConfig,
defaultConditionalTableConfig,
generateTableColumnId,
generateFilterId,
generateConditionalOptionId,
} from "../config";
// 도움말 텍스트 컴포넌트
const HelpText = ({ children }: { children: React.ReactNode }) => (
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
);
// 옵션 소스 설정 컴포넌트 (검색 가능한 Combobox)
interface OptionSourceConfigProps {
optionSource: {
enabled: boolean;
tableName: string;
valueColumn: string;
labelColumn: string;
filterCondition?: string;
};
tables: { table_name: string; comment?: string }[];
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>;
onUpdate: (updates: Partial<OptionSourceConfigProps["optionSource"]>) => void;
}
const OptionSourceConfig: React.FC<OptionSourceConfigProps> = ({
optionSource,
tables,
tableColumns,
onUpdate,
}) => {
const [tableOpen, setTableOpen] = useState(false);
const [valueColumnOpen, setValueColumnOpen] = useState(false);
// 선택된 테이블의 컬럼 목록
const selectedTableColumns = useMemo(() => {
return tableColumns[optionSource.tableName] || [];
}, [tableColumns, optionSource.tableName]);
return (
<div className="grid grid-cols-3 gap-2 pl-6">
{/* 테이블 선택 Combobox */}
<div>
<Label className="text-[10px]"></Label>
<Popover open={tableOpen} onOpenChange={setTableOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableOpen}
className="mt-1 h-7 w-full justify-between text-xs"
>
<span className="truncate">
{optionSource.tableName
? tables.find((t) => t.table_name === optionSource.tableName)?.comment || optionSource.tableName
: "테이블 선택"}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색 (영문/한글)..." className="h-8 text-xs" />
<CommandList className="max-h-[200px]">
<CommandEmpty className="py-3 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.table_name}
value={`${table.table_name} ${table.comment || ""}`}
onSelect={() => {
onUpdate({
tableName: table.table_name,
valueColumn: "", // 테이블 변경 시 컬럼 초기화
labelColumn: "",
});
setTableOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
optionSource.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-[10px] text-muted-foreground">{table.comment}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 참조할 값 컬럼 선택 Combobox */}
<div>
<Label className="text-[10px]"> </Label>
<Popover open={valueColumnOpen} onOpenChange={setValueColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={valueColumnOpen}
disabled={!optionSource.tableName}
className="mt-1 h-7 w-full justify-between text-xs"
>
<span className="truncate">
{optionSource.valueColumn
? selectedTableColumns.find((c) => c.column_name === optionSource.valueColumn)?.comment
? `${optionSource.valueColumn} (${selectedTableColumns.find((c) => c.column_name === optionSource.valueColumn)?.comment})`
: optionSource.valueColumn
: "컬럼 선택"}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색 (영문/한글)..." className="h-8 text-xs" />
<CommandList className="max-h-[200px]">
<CommandEmpty className="py-3 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
{selectedTableColumns.map((column) => (
<CommandItem
key={column.column_name}
value={`${column.column_name} ${column.comment || ""}`}
onSelect={() => {
onUpdate({ valueColumn: column.column_name });
setValueColumnOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
optionSource.valueColumn === column.column_name ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{column.column_name}</span>
{column.comment && (
<span className="text-[10px] text-muted-foreground">{column.comment}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 출력할 값 컬럼 선택 Combobox */}
<div>
<Label className="text-[10px]"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={!optionSource.tableName}
className="mt-1 h-7 w-full justify-between text-xs"
>
<span className="truncate">
{optionSource.labelColumn
? selectedTableColumns.find((c) => c.column_name === optionSource.labelColumn)?.comment
? `${optionSource.labelColumn} (${selectedTableColumns.find((c) => c.column_name === optionSource.labelColumn)?.comment})`
: optionSource.labelColumn
: "(참조할 값과 동일)"}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색 (영문/한글)..." className="h-8 text-xs" />
<CommandList className="max-h-[200px]">
<CommandEmpty className="py-3 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
{/* 값 컬럼 사용 옵션 */}
<CommandItem
value="__use_value__"
onSelect={() => onUpdate({ labelColumn: "" })}
className="text-xs text-muted-foreground"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!optionSource.labelColumn ? "opacity-100" : "opacity-0"
)}
/>
( )
</CommandItem>
{selectedTableColumns.map((column) => (
<CommandItem
key={column.column_name}
value={`${column.column_name} ${column.comment || ""}`}
onSelect={() => onUpdate({ labelColumn: column.column_name })}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
optionSource.labelColumn === column.column_name ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{column.column_name}</span>
{column.comment && (
<span className="text-[10px] text-muted-foreground">{column.comment}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="mt-0.5 text-[9px] text-muted-foreground">
</p>
</div>
</div>
);
};
// 부모 화면에서 전달 가능한 필드 타입
interface AvailableParentField {
name: string; // 필드명 (columnName)
label: string; // 표시 라벨
sourceComponent?: string; // 출처 컴포넌트
sourceTable?: string; // 출처 테이블명
}
// 컬럼 설정 아이템 컴포넌트
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 }[]; // 소스 테이블 컬럼
sourceTableName: string; // 소스 테이블명
tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>; // 테이블별 컬럼
sections: { id: string; title: string }[]; // 섹션 목록
formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록
tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용
availableParentFields?: AvailableParentField[]; // 부모 화면에서 전달 가능한 필드 목록
onLoadTableColumns: (tableName: string) => void;
onUpdate: (updates: Partial<TableColumnConfig>) => void;
onMoveUp: () => void;
onMoveDown: () => void;
onRemove: () => void;
}
function ColumnSettingItem({
col,
index,
totalCount,
saveTableColumns,
displayColumns,
sourceTableColumns,
sourceTableName,
tables,
tableColumns,
sections,
formFields,
tableConfig,
availableParentFields = [],
onLoadTableColumns,
onUpdate,
onMoveUp,
onMoveDown,
onRemove,
}: ColumnSettingItemProps) {
const [fieldSearchOpen, setFieldSearchOpen] = useState(false);
const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false);
const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false);
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
// 조회 옵션 추가
const addLookupOption = () => {
const newOption: LookupOption = {
id: `lookup_${Date.now()}`,
label: `조회 옵션 ${(col.lookup?.options || []).length + 1}`,
type: "sameTable",
tableName: sourceTableName,
valueColumn: "",
conditions: [],
isDefault: (col.lookup?.options || []).length === 0,
};
onUpdate({
lookup: {
enabled: true,
options: [...(col.lookup?.options || []), newOption],
},
});
};
// 조회 옵션 삭제
const removeLookupOption = (optIndex: number) => {
const newOptions = (col.lookup?.options || []).filter((_, i) => i !== optIndex);
if (newOptions.length > 0 && !newOptions.some((opt) => opt.isDefault)) {
newOptions[0].isDefault = true;
}
onUpdate({
lookup: {
enabled: col.lookup?.enabled ?? false,
options: newOptions,
},
});
};
// 조회 옵션 업데이트
const updateLookupOption = (optIndex: number, updates: Partial<LookupOption>) => {
onUpdate({
lookup: {
enabled: col.lookup?.enabled ?? false,
options: (col.lookup?.options || []).map((opt, i) =>
i === optIndex ? { ...opt, ...updates } : opt
),
},
});
};
// 조회 조건 추가
const addLookupCondition = (optIndex: number) => {
const option = col.lookup?.options?.[optIndex];
if (!option) return;
const newCondition: LookupCondition = {
sourceType: "currentRow",
sourceField: "",
targetColumn: "",
};
updateLookupOption(optIndex, {
conditions: [...(option.conditions || []), newCondition],
});
};
// 조회 조건 삭제
const removeLookupCondition = (optIndex: number, condIndex: number) => {
const option = col.lookup?.options?.[optIndex];
if (!option) return;
updateLookupOption(optIndex, {
conditions: option.conditions.filter((_, i) => i !== condIndex),
});
};
// 조회 조건 업데이트
const updateLookupCondition = (optIndex: number, condIndex: number, updates: Partial<LookupCondition>) => {
const option = col.lookup?.options?.[optIndex];
if (!option) return;
updateLookupOption(optIndex, {
conditions: option.conditions.map((c, i) =>
i === condIndex ? { ...c, ...updates } : c
),
});
};
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={cn(
"h-8 w-full justify-between text-xs mt-1",
!col.field && "text-muted-foreground"
)}
>
<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>
{/* 선택 안 함 옵션 */}
<CommandItem
key="__none__"
value="__none__"
onSelect={() => {
onUpdate({ field: "" });
setFieldSearchOpen(false);
}}
className="text-xs text-muted-foreground"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!col.field ? "opacity-100" : "opacity-0"
)}
/>
<span className="italic">( - )</span>
</CommandItem>
{/* 실제 컬럼 목록 */}
{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>
<label className="flex items-center gap-2 text-xs cursor-pointer" title="부모 화면에서 전달받은 값을 모든 행에 적용">
<Switch
checked={col.receiveFromParent ?? false}
onCheckedChange={(checked) => onUpdate({ receiveFromParent: checked })}
className="scale-75"
/>
<span className="text-blue-600"></span>
</label>
<label className="flex items-center gap-2 text-xs cursor-pointer">
<Switch
checked={col.lookup?.enabled ?? false}
onCheckedChange={(checked) => {
if (checked) {
onUpdate({ lookup: { enabled: true, options: [] } });
} else {
onUpdate({ lookup: undefined });
}
}}
className="scale-75"
/>
<span className="flex items-center gap-1">
<Search className="h-3 w-3" />
</span>
</label>
{/* 날짜 타입일 때만 일괄 적용 옵션 표시 */}
{col.type === "date" && (
<label className="flex items-center gap-2 text-xs cursor-pointer" title="첫 번째 날짜 입력 시 모든 행에 동일하게 적용">
<Switch
checked={col.batchApply ?? false}
onCheckedChange={(checked) => onUpdate({ batchApply: checked })}
className="scale-75"
/>
<span> </span>
</label>
)}
</div>
{/* 부모에서 값 받기 설정 (부모값 ON일 때만 표시) */}
{col.receiveFromParent && (
<div className="border-t pt-3 mt-3 space-y-2">
<Label className="text-xs font-medium text-blue-600"> </Label>
<p className="text-[10px] text-muted-foreground">
. .
</p>
{availableParentFields.length > 0 ? (
<Popover open={parentFieldSearchOpen} onOpenChange={setParentFieldSearchOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={parentFieldSearchOpen}
className="h-8 w-full justify-between text-xs"
>
<span className="truncate">
{col.parentFieldName
? availableParentFields.find(f => f.name === col.parentFieldName)?.label || col.parentFieldName
: `(기본: ${col.field})`}
</span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[300px]" align="start">
<Command>
<CommandInput placeholder="부모 필드 검색..." className="text-xs" />
<CommandList className="max-h-[250px]">
<CommandEmpty className="text-xs py-4 text-center">
.
</CommandEmpty>
<CommandGroup>
{/* 기본값 (필드명과 동일) */}
<CommandItem
value="__same_as_field__"
onSelect={() => {
onUpdate({ parentFieldName: undefined });
setParentFieldSearchOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!col.parentFieldName ? "opacity-100" : "opacity-0"
)}
/>
<span className="text-muted-foreground">(: {col.field})</span>
</CommandItem>
{/* 부모 필드 목록 */}
{availableParentFields.map((pf) => (
<CommandItem
key={pf.name}
value={`${pf.name} ${pf.label}`}
onSelect={() => {
onUpdate({ parentFieldName: pf.name });
setParentFieldSearchOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.parentFieldName === pf.name ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col flex-1 min-w-0">
<span className="font-medium truncate">{pf.label || pf.name}</span>
{pf.sourceComponent && (
<span className="text-[10px] text-muted-foreground truncate">
{pf.sourceComponent}{pf.sourceTable && ` (${pf.sourceTable})`}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<div className="space-y-1">
<Input
value={col.parentFieldName || ""}
onChange={(e) => onUpdate({ parentFieldName: e.target.value })}
placeholder={col.field}
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
"{col.field}" .
</p>
</div>
)}
</div>
)}
{/* 조회 설정 (조회 ON일 때만 표시) */}
{col.lookup?.enabled && (
<div className="border-t pt-3 mt-3 space-y-3">
<div className="flex justify-between items-center">
<Label className="text-xs font-medium text-blue-600"> </Label>
<Button size="sm" variant="outline" onClick={addLookupOption} className="h-6 text-xs px-2">
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{(col.lookup?.options || []).length === 0 ? (
<p className="text-xs text-muted-foreground text-center py-2 border border-dashed rounded bg-muted/20">
"옵션 추가" .
</p>
) : (
<div className="space-y-3">
{(col.lookup?.options || []).map((option, optIndex) => (
<div key={option.id} className="border rounded-lg p-3 space-y-3 bg-blue-50/30">
{/* 옵션 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs font-medium">{option.label || `옵션 ${optIndex + 1}`}</span>
{option.isDefault && (
<Badge variant="secondary" className="text-[10px] h-4"></Badge>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeLookupOption(optIndex)}
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 기본 설정 - 첫 번째 줄: 옵션명, 표시 라벨 */}
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[10px]"></Label>
<Input
value={option.label}
onChange={(e) => updateLookupOption(optIndex, { label: e.target.value })}
placeholder="예: 기준단가"
className="h-7 text-xs mt-0.5"
/>
</div>
<div>
<Label className="text-[10px]"> ( )</Label>
<Input
value={option.displayLabel || ""}
onChange={(e) => updateLookupOption(optIndex, { displayLabel: e.target.value })}
placeholder={`예: 단가 (${option.label || "옵션명"})`}
className="h-7 text-xs mt-0.5"
/>
<p className="text-[9px] text-muted-foreground mt-0.5">
</p>
</div>
</div>
{/* 기본 설정 - 두 번째 줄: 조회 유형, 테이블, 가져올 컬럼 */}
<div className="grid grid-cols-3 gap-2">
<div>
<Label className="text-[10px]"> </Label>
<Select
value={option.type}
onValueChange={(value: "sameTable" | "relatedTable" | "combinedLookup") => {
const newTableName = value === "sameTable" ? sourceTableName : "";
updateLookupOption(optIndex, { type: value, tableName: newTableName, conditions: [] });
if (newTableName) onLoadTableColumns(newTableName);
}}
>
<SelectTrigger className="h-7 text-xs mt-0.5">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LOOKUP_TYPE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value} className="text-xs">
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> </Label>
{option.type === "sameTable" ? (
<Input value={sourceTableName} disabled className="h-7 text-xs mt-0.5 bg-muted" />
) : (
<Popover
open={lookupTableOpenMap[option.id]}
onOpenChange={(open) => setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: open }))}
>
<PopoverTrigger asChild>
<Button variant="outline" className="h-7 w-full justify-between text-xs mt-0.5">
{option.tableName || "선택..."}
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[200px]" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-[150px]">
<CommandEmpty className="text-xs py-2 text-center"></CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.table_name}
value={table.table_name}
onSelect={() => {
updateLookupOption(optIndex, { tableName: table.table_name });
onLoadTableColumns(table.table_name);
setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: false }));
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", option.tableName === table.table_name ? "opacity-100" : "opacity-0")} />
{table.table_name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)}
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={option.valueColumn}
onValueChange={(value) => updateLookupOption(optIndex, { valueColumn: value })}
>
<SelectTrigger className="h-7 text-xs mt-0.5">
<SelectValue placeholder="선택..." />
</SelectTrigger>
<SelectContent>
{(tableColumns[option.tableName] || []).map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
{c.column_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 기본 옵션 & 조회 조건 */}
<div className="flex items-center justify-between">
<label className="flex items-center gap-1.5 text-xs cursor-pointer">
<Switch
checked={option.isDefault ?? false}
onCheckedChange={(checked) => {
if (checked) {
onUpdate({
lookup: {
enabled: true,
options: (col.lookup?.options || []).map((o, i) => ({ ...o, isDefault: i === optIndex })),
},
});
} else {
updateLookupOption(optIndex, { isDefault: false });
}
}}
className="scale-[0.6]"
/>
<span> </span>
</label>
<Button size="sm" variant="ghost" onClick={() => addLookupCondition(optIndex)} className="h-6 text-xs px-2">
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{/* 조회 조건 목록 */}
{(option.conditions || []).length > 0 && (
<div className="space-y-2">
{option.conditions.map((cond, condIndex) => (
<div key={condIndex} className="border rounded bg-white p-2 space-y-2">
{/* 기본 조건 행 */}
<div className="flex items-center gap-1.5">
<Select
value={cond.sourceType}
onValueChange={(value: "currentRow" | "sourceTable" | "sectionField" | "externalTable") =>
updateLookupCondition(optIndex, condIndex, {
sourceType: value,
sourceField: "",
sectionId: undefined,
transform: undefined,
externalLookup: value === "externalTable" ? {
tableName: "",
matchColumn: "",
matchSourceType: "sourceTable",
matchSourceField: "",
resultColumn: "",
} : undefined,
})
}
>
<SelectTrigger className="h-6 text-[10px] w-[90px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LOOKUP_CONDITION_SOURCE_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value} className="text-xs">{o.label}</SelectItem>
))}
</SelectContent>
</Select>
{/* 다른 섹션 선택 시 - 섹션 드롭다운 */}
{cond.sourceType === "sectionField" && (
<Select
value={cond.sectionId || ""}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { sectionId: value, sourceField: "" })}
>
<SelectTrigger className="h-6 text-[10px] w-[70px]">
<SelectValue placeholder="섹션" />
</SelectTrigger>
<SelectContent>
{sections.map((s) => (
<SelectItem key={s.id} value={s.id} className="text-xs">{s.title}</SelectItem>
))}
</SelectContent>
</Select>
)}
{/* 현재 행 / 소스 테이블 / 다른 섹션 - 필드 선택 */}
{cond.sourceType !== "externalTable" && (
<div className="space-y-0.5">
<Select
value={cond.sourceField}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { sourceField: value })}
>
<SelectTrigger className="h-6 text-[10px] w-[110px]">
<SelectValue placeholder="필드" />
</SelectTrigger>
<SelectContent>
{cond.sourceType === "currentRow" ? (
// 현재 행: 테이블에 설정된 컬럼 필드 표시
<>
<div className="px-2 py-1 text-[10px] font-medium text-green-600 bg-green-50 border-b">
</div>
{tableConfig?.columns?.map((c) => (
<SelectItem key={c.field} value={c.field} className="text-xs">
{c.label} ({c.field})
</SelectItem>
))}
</>
) : cond.sourceType === "sourceTable" ? (
// 소스 테이블: 원본 테이블의 컬럼 표시
<>
<div className="px-2 py-1 text-[10px] font-medium text-orange-600 bg-orange-50 border-b">
{sourceTableName}
</div>
{sourceTableColumns.map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
{c.column_name}
</SelectItem>
))}
</>
) : (
// 다른 섹션: 폼 필드 표시
<>
{cond.sectionId && (
<div className="px-2 py-1 text-[10px] font-medium text-purple-600 bg-purple-50 border-b">
{sections.find(s => s.id === cond.sectionId)?.title || cond.sectionId}
</div>
)}
{formFields
.filter((f) => !cond.sectionId || f.sectionId === cond.sectionId)
.map((f) => (
<SelectItem key={f.columnName} value={f.columnName} className="text-xs">{f.label}</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
{cond.sourceField && (
<p className="text-[9px] text-muted-foreground truncate">
{cond.sourceType === "currentRow"
? `rowData.${cond.sourceField}`
: cond.sourceType === "sourceTable"
? `${sourceTableName}.${cond.sourceField}`
: `formData.${cond.sourceField}`
}
</p>
)}
</div>
)}
{/* 현재 행 / 소스 테이블 / 다른 섹션일 때 = 기호와 조회 컬럼 */}
{cond.sourceType !== "externalTable" && (
<>
<span className="text-[10px] text-muted-foreground">=</span>
<div className="flex-1 space-y-0.5">
<Select
value={cond.targetColumn}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { targetColumn: value })}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="조회 컬럼" />
</SelectTrigger>
<SelectContent>
{option.tableName && (
<div className="px-2 py-1 text-[10px] font-medium text-blue-600 bg-blue-50 border-b">
{option.tableName}
</div>
)}
{(tableColumns[option.tableName] || []).map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
))}
</SelectContent>
</Select>
{cond.targetColumn && option.tableName && (
<p className="text-[9px] text-muted-foreground truncate">
{option.tableName}.{cond.targetColumn}
</p>
)}
</div>
</>
)}
<Button
size="sm"
variant="ghost"
onClick={() => removeLookupCondition(optIndex, condIndex)}
className="h-6 w-6 p-0 text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 외부 테이블 조회 설정 */}
{cond.sourceType === "externalTable" && cond.externalLookup && (
<div className="pl-2 border-l-2 border-orange-200 space-y-2">
<p className="text-[10px] text-orange-600 font-medium"> </p>
{/* 1행: 조회 테이블 선택 */}
<div className="grid grid-cols-3 gap-1.5">
<div>
<p className="text-[9px] text-muted-foreground mb-0.5"> </p>
<Popover
open={lookupTableOpenMap[`ext_${optIndex}_${condIndex}`] || false}
onOpenChange={(open) => setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: open }))}
>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-6 w-full justify-between text-[10px]">
{cond.externalLookup.tableName || "테이블 선택"}
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[180px]" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="text-xs py-2 text-center"> .</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.table_name}
value={table.table_name}
onSelect={() => {
onLoadTableColumns(table.table_name);
updateLookupCondition(optIndex, condIndex, {
externalLookup: { ...cond.externalLookup!, tableName: table.table_name, matchColumn: "", resultColumn: "" }
});
setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: false }));
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", cond.externalLookup?.tableName === table.table_name ? "opacity-100" : "opacity-0")} />
{table.table_name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div>
<p className="text-[9px] text-muted-foreground mb-0.5"> </p>
<Select
value={cond.externalLookup.matchColumn}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
externalLookup: { ...cond.externalLookup!, matchColumn: value }
})}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="컬럼" />
</SelectTrigger>
<SelectContent>
{(tableColumns[cond.externalLookup.tableName] || []).map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<p className="text-[9px] text-muted-foreground mb-0.5"> </p>
<Select
value={cond.externalLookup.resultColumn}
onValueChange={(value) => {
updateLookupCondition(optIndex, condIndex, {
externalLookup: { ...cond.externalLookup!, resultColumn: value },
sourceField: value // sourceField에도 저장
});
}}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="컬럼" />
</SelectTrigger>
<SelectContent>
{(tableColumns[cond.externalLookup.tableName] || []).map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 2행: 비교 값 출처 */}
<div className="p-1.5 bg-orange-50/50 rounded">
<p className="text-[9px] text-muted-foreground mb-1"> ( )</p>
<div className="flex items-center gap-1.5">
<Select
value={cond.externalLookup.matchSourceType}
onValueChange={(value: "currentRow" | "sourceTable" | "sectionField") => updateLookupCondition(optIndex, condIndex, {
externalLookup: { ...cond.externalLookup!, matchSourceType: value, matchSourceField: "", matchSectionId: undefined }
})}
>
<SelectTrigger className="h-5 text-[9px] w-[80px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="currentRow" className="text-xs"> </SelectItem>
<SelectItem value="sourceTable" className="text-xs"> </SelectItem>
<SelectItem value="sectionField" className="text-xs"> </SelectItem>
</SelectContent>
</Select>
{cond.externalLookup.matchSourceType === "sectionField" && (
<Select
value={cond.externalLookup.matchSectionId || ""}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
externalLookup: { ...cond.externalLookup!, matchSectionId: value, matchSourceField: "" }
})}
>
<SelectTrigger className="h-5 text-[9px] w-[60px]">
<SelectValue placeholder="섹션" />
</SelectTrigger>
<SelectContent>
{sections.map((s) => (
<SelectItem key={s.id} value={s.id} className="text-xs">{s.title}</SelectItem>
))}
</SelectContent>
</Select>
)}
<Select
value={cond.externalLookup.matchSourceField}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
externalLookup: { ...cond.externalLookup!, matchSourceField: value }
})}
>
<SelectTrigger className="h-5 text-[9px] flex-1">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{cond.externalLookup.matchSourceType === "currentRow" ? (
<>
<div className="px-2 py-1 text-[9px] font-medium text-green-600 bg-green-50 border-b">
</div>
{tableConfig?.columns?.map((c) => (
<SelectItem key={c.field} value={c.field} className="text-xs">{c.label} ({c.field})</SelectItem>
))}
</>
) : cond.externalLookup.matchSourceType === "sourceTable" ? (
<>
<div className="px-2 py-1 text-[9px] font-medium text-orange-600 bg-orange-50 border-b">
{sourceTableName}
</div>
{sourceTableColumns.map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
))}
</>
) : (
<>
{cond.externalLookup.matchSectionId && (
<div className="px-2 py-1 text-[9px] font-medium text-purple-600 bg-purple-50 border-b">
{sections.find(s => s.id === cond.externalLookup?.matchSectionId)?.title}
</div>
)}
{formFields
.filter((f) => !cond.externalLookup?.matchSectionId || f.sectionId === cond.externalLookup?.matchSectionId)
.map((f) => (
<SelectItem key={f.columnName} value={f.columnName} className="text-xs">{f.label}</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
</div>
{/* 3행: 최종 조회 컬럼 */}
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-muted-foreground"> ( )</span>
<span className="text-[10px] text-muted-foreground">=</span>
<Select
value={cond.targetColumn}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { targetColumn: value })}
>
<SelectTrigger className="h-5 text-[9px] flex-1">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{option.tableName && (
<div className="px-2 py-1 text-[9px] font-medium text-blue-600 bg-blue-50 border-b">
{option.tableName}
</div>
)}
{(tableColumns[option.tableName] || []).map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 설명 텍스트 */}
{cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && cond.targetColumn && (
<p className="text-[9px] text-orange-600 bg-orange-100/50 rounded px-1.5 py-0.5">
{cond.externalLookup.tableName} {cond.externalLookup.matchColumn} = ( ) {" "}
{cond.externalLookup.resultColumn} {option.tableName}.{cond.targetColumn}
</p>
)}
</div>
)}
{/* 값 변환 설정 (다른 섹션일 때만 표시) */}
{cond.sourceType === "sectionField" && (
<div className="pl-2 border-l-2 border-blue-200">
<label className="flex items-center gap-1.5 text-[10px] cursor-pointer">
<Switch
checked={cond.transform?.enabled ?? false}
onCheckedChange={(checked) => {
if (checked) {
updateLookupCondition(optIndex, condIndex, {
transform: { enabled: true, tableName: "", matchColumn: "", resultColumn: "" }
});
} else {
updateLookupCondition(optIndex, condIndex, { transform: undefined });
}
}}
className="scale-[0.5]"
/>
<span className="text-blue-600 font-medium"> </span>
<span className="text-muted-foreground">( )</span>
</label>
{cond.transform?.enabled && (
<div className="mt-1.5 p-2 bg-blue-50/50 rounded space-y-1.5">
<div className="grid grid-cols-3 gap-1.5">
<div>
<Label className="text-[9px] text-muted-foreground"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="h-6 w-full justify-between text-[10px]">
{cond.transform.tableName || "선택..."}
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[180px]" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-[150px]">
<CommandEmpty className="text-xs py-2 text-center"></CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.table_name}
value={table.table_name}
onSelect={() => {
updateLookupCondition(optIndex, condIndex, {
transform: { ...cond.transform!, tableName: table.table_name, matchColumn: "", resultColumn: "" }
});
onLoadTableColumns(table.table_name);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", cond.transform?.tableName === table.table_name ? "opacity-100" : "opacity-0")} />
{table.table_name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div>
<Label className="text-[9px] text-muted-foreground"> </Label>
<Select
value={cond.transform.matchColumn}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
transform: { ...cond.transform!, matchColumn: value }
})}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="선택..." />
</SelectTrigger>
<SelectContent>
{(tableColumns[cond.transform.tableName] || []).map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[9px] text-muted-foreground"> </Label>
<Select
value={cond.transform.resultColumn}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
transform: { ...cond.transform!, resultColumn: value }
})}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="선택..." />
</SelectTrigger>
<SelectContent>
{(tableColumns[cond.transform.tableName] || []).map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{cond.transform.tableName && cond.transform.matchColumn && cond.transform.resultColumn && (
<p className="text-[9px] text-blue-600 bg-blue-100/50 rounded px-1.5 py-0.5">
{cond.transform.tableName} {cond.transform.matchColumn} = {cond.transform.resultColumn}
</p>
)}
</div>
)}
</div>
)}
</div>
))}
</div>
)}
{/* 조회 유형 설명 */}
<p className="text-[10px] text-muted-foreground bg-muted/50 rounded p-1.5">
{option.type === "sameTable" && "동일 테이블: 검색 모달에서 선택한 행의 다른 컬럼 값"}
{option.type === "relatedTable" && "연관 테이블: 현재 행 데이터로 다른 테이블 조회"}
{option.type === "combinedLookup" && "복합 조건: 다른 섹션 필드 + 현재 행 조합 조회"}
</p>
</div>
))}
</div>
)}
</div>
)}
{/* 동적 Select 옵션 (소스 테이블 필터링이 활성화되고, 타입이 select일 때만 표시) */}
{col.type === "select" && tableConfig.conditionalTable?.sourceFilter?.enabled && (
<div className="border-t pt-3 mt-3 space-y-3">
<div className="flex items-center justify-between">
<div>
<Label className="text-xs font-medium text-green-600"> </Label>
<p className="text-[10px] text-muted-foreground">
. .
</p>
</div>
<Switch
checked={col.dynamicSelectOptions?.enabled ?? false}
onCheckedChange={(checked) => {
onUpdate({
dynamicSelectOptions: checked
? {
enabled: true,
sourceField: "",
distinct: true,
}
: undefined,
});
}}
className="scale-75"
/>
</div>
{col.dynamicSelectOptions?.enabled && (
<div className="space-y-3 pl-2 border-l-2 border-green-500/30">
{/* 소스 컬럼 선택 */}
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-[10px]"> ( )</Label>
<p className="text-[9px] text-muted-foreground mb-1">
</p>
{sourceTableColumns.length > 0 ? (
<Select
value={col.dynamicSelectOptions.sourceField || ""}
onValueChange={(value) => {
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
sourceField: value,
// 라벨 필드가 비어있으면 소스 필드와 동일하게 설정
labelField: col.dynamicSelectOptions?.labelField || value,
},
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{sourceTableColumns.map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
{c.column_name} {c.comment && `(${c.comment})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={col.dynamicSelectOptions.sourceField || ""}
onChange={(e) => {
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
sourceField: e.target.value,
},
});
}}
placeholder="inspection_item"
className="h-7 text-xs"
/>
)}
</div>
<div>
<Label className="text-[10px]"> ()</Label>
<p className="text-[9px] text-muted-foreground mb-1">
( )
</p>
{sourceTableColumns.length > 0 ? (
<Select
value={col.dynamicSelectOptions.labelField || "__same_as_source__"}
onValueChange={(value) => {
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
labelField: value === "__same_as_source__" ? "" : value,
},
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="(소스 컬럼과 동일)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__same_as_source__" className="text-xs text-muted-foreground">
( )
</SelectItem>
{sourceTableColumns.map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
{c.column_name} {c.comment && `(${c.comment})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={col.dynamicSelectOptions.labelField || ""}
onChange={(e) => {
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
labelField: e.target.value,
},
});
}}
placeholder="(비워두면 소스 컬럼과 동일)"
className="h-7 text-xs"
/>
)}
</div>
</div>
{/* 행 선택 모드 */}
<div className="space-y-2">
<label className="flex items-center gap-2 text-xs cursor-pointer">
<Switch
checked={col.dynamicSelectOptions.rowSelectionMode?.enabled ?? false}
onCheckedChange={(checked) => {
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
rowSelectionMode: checked
? {
enabled: true,
autoFillColumns: [],
}
: undefined,
},
});
}}
className="scale-75"
/>
<span> ( )</span>
</label>
<p className="text-[9px] text-muted-foreground pl-6">
.
</p>
{col.dynamicSelectOptions.rowSelectionMode?.enabled && (
<div className="pl-6 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[10px]"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const currentMappings = col.dynamicSelectOptions?.rowSelectionMode?.autoFillColumns || [];
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
rowSelectionMode: {
...col.dynamicSelectOptions!.rowSelectionMode!,
autoFillColumns: [...currentMappings, { sourceColumn: "", targetField: "" }],
},
},
});
}}
className="h-6 text-[10px] px-2"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || []).length === 0 ? (
<p className="text-[10px] text-muted-foreground text-center py-2 border border-dashed rounded bg-muted/20">
"매핑 추가" .
</p>
) : (
<div className="space-y-2">
{(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || []).map((mapping, mappingIndex) => (
<div key={mappingIndex} className="flex items-center gap-2 p-2 border rounded bg-green-50/30">
{/* 소스 컬럼 */}
<div className="flex-1">
<Label className="text-[9px] text-muted-foreground"> </Label>
{sourceTableColumns.length > 0 ? (
<Select
value={mapping.sourceColumn}
onValueChange={(value) => {
const newMappings = [...(col.dynamicSelectOptions?.rowSelectionMode?.autoFillColumns || [])];
newMappings[mappingIndex] = { ...newMappings[mappingIndex], sourceColumn: value };
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
rowSelectionMode: {
...col.dynamicSelectOptions!.rowSelectionMode!,
autoFillColumns: newMappings,
},
},
});
}}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="소스 컬럼" />
</SelectTrigger>
<SelectContent>
{sourceTableColumns.map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
{c.column_name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={mapping.sourceColumn}
onChange={(e) => {
const newMappings = [...(col.dynamicSelectOptions?.rowSelectionMode?.autoFillColumns || [])];
newMappings[mappingIndex] = { ...newMappings[mappingIndex], sourceColumn: e.target.value };
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
rowSelectionMode: {
...col.dynamicSelectOptions!.rowSelectionMode!,
autoFillColumns: newMappings,
},
},
});
}}
placeholder="소스 컬럼"
className="h-6 text-[10px]"
/>
)}
</div>
<span className="text-muted-foreground text-xs"></span>
{/* 타겟 필드 */}
<div className="flex-1">
<Label className="text-[9px] text-muted-foreground"> </Label>
<Select
value={mapping.targetField}
onValueChange={(value) => {
const newMappings = [...(col.dynamicSelectOptions?.rowSelectionMode?.autoFillColumns || [])];
newMappings[mappingIndex] = { ...newMappings[mappingIndex], targetField: value };
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
rowSelectionMode: {
...col.dynamicSelectOptions!.rowSelectionMode!,
autoFillColumns: newMappings,
},
},
});
}}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="타겟 필드" />
</SelectTrigger>
<SelectContent>
{(tableConfig.columns || [])
.filter((c) => c.field !== col.field) // 현재 컬럼 제외
.map((c) => (
<SelectItem key={c.field} value={c.field} className="text-xs">
{c.label || c.field}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 삭제 버튼 */}
<Button
size="sm"
variant="ghost"
onClick={() => {
const newMappings = (col.dynamicSelectOptions?.rowSelectionMode?.autoFillColumns || []).filter(
(_, i) => i !== mappingIndex
);
onUpdate({
dynamicSelectOptions: {
...col.dynamicSelectOptions!,
rowSelectionMode: {
...col.dynamicSelectOptions!.rowSelectionMode!,
autoFillColumns: newMappings,
},
},
});
}}
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
{/* 매핑 설명 */}
{(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || []).length > 0 && (
<div className="text-[9px] text-green-600 bg-green-100/50 rounded p-1.5">
{col.label || col.field} :{" "}
{(col.dynamicSelectOptions.rowSelectionMode?.autoFillColumns || [])
.filter((m) => m.sourceColumn && m.targetField)
.map((m) => {
const targetCol = tableConfig.columns?.find((c) => c.field === m.targetField);
return `${m.sourceColumn}${targetCol?.label || m.targetField}`;
})
.join(", ")}
</div>
)}
</div>
)}
</div>
{/* 설정 요약 */}
{col.dynamicSelectOptions.sourceField && (
<div className="text-[9px] text-green-600 bg-green-100/50 rounded p-1.5">
{sourceTableName}.{col.dynamicSelectOptions.sourceField}
{tableConfig.conditionalTable?.sourceFilter?.filterColumn && (
<> (: {tableConfig.conditionalTable.sourceFilter.filterColumn} = )</>
)}
</div>
)}
</div>
)}
</div>
)}
{/* ============================================ */}
{/* 저장 설정 섹션 */}
{/* ============================================ */}
<div className="space-y-2 border-t pt-3">
<Label className="text-xs font-semibold flex items-center gap-2">
</Label>
<p className="text-[10px] text-muted-foreground">
DB에 .
</p>
{/* 저장 여부 라디오 버튼 */}
<div className="space-y-2 pl-2">
{/* 저장함 옵션 */}
<label className="flex items-start gap-3 p-2 border rounded-lg cursor-pointer hover:bg-muted/50 transition-colors">
<input
type="radio"
name={`saveConfig_${col.field}`}
checked={col.saveConfig?.saveToTarget !== false}
onChange={() => {
onUpdate({
saveConfig: {
saveToTarget: true,
},
});
}}
className="mt-0.5"
/>
<div className="flex-1">
<span className="text-xs font-medium"> ()</span>
<p className="text-[10px] text-muted-foreground mt-0.5">
/ DB에 .
</p>
</div>
</label>
{/* 저장 안 함 옵션 */}
<label className="flex items-start gap-3 p-2 border rounded-lg cursor-pointer hover:bg-muted/50 transition-colors">
<input
type="radio"
name={`saveConfig_${col.field}`}
checked={col.saveConfig?.saveToTarget === false}
onChange={() => {
onUpdate({
saveConfig: {
saveToTarget: false,
referenceDisplay: {
referenceIdField: "",
sourceColumn: "",
},
},
});
}}
className="mt-0.5"
/>
<div className="flex-1">
<span className="text-xs font-medium"> - </span>
<p className="text-[10px] text-muted-foreground mt-0.5">
ID로 .
</p>
</div>
</label>
</div>
{/* 참조 설정 패널 (저장 안 함 선택 시) */}
{col.saveConfig?.saveToTarget === false && (
<div className="ml-6 p-3 border-2 border-dashed border-amber-300 rounded-lg bg-amber-50/50 space-y-3">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-amber-600" />
<span className="text-xs font-semibold text-amber-700"> </span>
</div>
{/* Step 1: ID 컬럼 선택 */}
<div className="space-y-1">
<Label className="text-[10px] font-medium">
1. ID ?
</Label>
<Select
value={col.saveConfig?.referenceDisplay?.referenceIdField || ""}
onValueChange={(value) => {
onUpdate({
saveConfig: {
...col.saveConfig!,
referenceDisplay: {
...col.saveConfig!.referenceDisplay!,
referenceIdField: value,
},
},
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="ID 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{(tableConfig.columns || [])
.filter((c) => c.field !== col.field) // 현재 컬럼 제외
.map((c) => (
<SelectItem key={c.field} value={c.field} className="text-xs">
{c.label || c.field}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[9px] text-muted-foreground">
ID로 .
</p>
</div>
{/* Step 2: 소스 컬럼 선택 */}
<div className="space-y-1">
<Label className="text-[10px] font-medium">
2. ?
</Label>
{sourceTableColumns.length > 0 ? (
<Select
value={col.saveConfig?.referenceDisplay?.sourceColumn || ""}
onValueChange={(value) => {
onUpdate({
saveConfig: {
...col.saveConfig!,
referenceDisplay: {
...col.saveConfig!.referenceDisplay!,
sourceColumn: value,
},
},
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="소스 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{sourceTableColumns.map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
{c.column_name} {c.comment && `(${c.comment})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={col.saveConfig?.referenceDisplay?.sourceColumn || ""}
onChange={(e) => {
onUpdate({
saveConfig: {
...col.saveConfig!,
referenceDisplay: {
...col.saveConfig!.referenceDisplay!,
sourceColumn: e.target.value,
},
},
});
}}
placeholder="소스 컬럼명 입력"
className="h-7 text-xs"
/>
)}
<p className="text-[9px] text-muted-foreground">
.
</p>
</div>
{/* 설정 요약 */}
{col.saveConfig?.referenceDisplay?.referenceIdField && col.saveConfig?.referenceDisplay?.sourceColumn && (
<div className="text-[10px] text-amber-700 bg-amber-100 rounded p-2 mt-2">
<strong> :</strong>
<br />
- ({col.label || col.field}) .
<br />
- <strong>{col.saveConfig.referenceDisplay.referenceIdField}</strong>{" "}
<strong>{sourceTableName}</strong> {" "}
<strong>{col.saveConfig.referenceDisplay.sourceColumn}</strong> .
</div>
)}
</div>
)}
</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;
// 전체 섹션 목록 (다른 섹션 필드 참조용)
allSections?: FormSectionConfig[];
// 부모 화면에서 전달 가능한 필드 목록
availableParentFields?: AvailableParentField[];
}
export function TableSectionSettingsModal({
open,
onOpenChange,
section,
onSave,
tables,
tableColumns,
onLoadTableColumns,
categoryList = [],
onLoadCategoryList,
allSections = [],
availableParentFields = [],
}: 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]);
// 조회 설정에 있는 테이블들의 컬럼 로드 (모달 열릴 때)
useEffect(() => {
if (open && tableConfig.columns) {
const tablesToLoad = new Set<string>();
// 각 컬럼의 lookup 설정에서 테이블 수집
tableConfig.columns.forEach((col) => {
if (col.lookup?.enabled && col.lookup.options) {
col.lookup.options.forEach((option) => {
// 조회 테이블
if (option.tableName) {
tablesToLoad.add(option.tableName);
}
// 변환 테이블
option.conditions?.forEach((cond) => {
if (cond.transform?.enabled && cond.transform.tableName) {
tablesToLoad.add(cond.transform.tableName);
}
});
});
}
});
// 수집된 테이블들의 컬럼 로드
tablesToLoad.forEach((tableName) => {
if (!tableColumns[tableName]) {
onLoadTableColumns(tableName);
}
});
}
}, [open, tableConfig.columns, tableColumns, 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 otherSections = useMemo(() => {
return allSections
.filter((s) => s.id !== section.id && s.type !== "table")
.map((s) => ({ id: s.id, title: s.title }));
}, [allSections, section.id]);
// 다른 섹션의 필드 목록
const otherSectionFields = useMemo(() => {
const fields: { columnName: string; label: string; sectionId: string }[] = [];
allSections
.filter((s) => s.id !== section.id && s.type !== "table")
.forEach((s) => {
(s.fields || []).forEach((f) => {
fields.push({
columnName: f.columnName,
label: f.label,
sectionId: s.id,
});
});
});
return fields;
}, [allSections, section.id]);
// 설정 업데이트 함수
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}
sourceTableName={tableConfig.source.tableName}
tables={tables}
tableColumns={tableColumns}
sections={otherSections}
formFields={otherSectionFields}
tableConfig={tableConfig}
availableParentFields={availableParentFields}
onLoadTableColumns={onLoadTableColumns}
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>
<Select
value={tableConfig.uiConfig?.addButtonType || "search"}
onValueChange={(value) => updateUiConfig({ addButtonType: value as "search" | "addRow" })}
>
<SelectTrigger className="h-8 text-xs mt-1">
<SelectValue placeholder="버튼 동작 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="search">
<div className="flex flex-col">
<span> </span>
<span className="text-[10px] text-muted-foreground"> </span>
</div>
</SelectItem>
<SelectItem value="addRow">
<div className="flex flex-col">
<span> </span>
<span className="text-[10px] text-muted-foreground"> </span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
value={tableConfig.uiConfig?.addButtonText || ""}
onChange={(e) => updateUiConfig({ addButtonText: e.target.value })}
placeholder={tableConfig.uiConfig?.addButtonType === "addRow" ? "항목 추가" : "항목 검색"}
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"
disabled={tableConfig.uiConfig?.addButtonType === "addRow"}
/>
{tableConfig.uiConfig?.addButtonType === "addRow" && (
<p className="text-[10px] text-muted-foreground mt-0.5"> </p>
)}
</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"
disabled={tableConfig.uiConfig?.addButtonType === "addRow"}
/>
<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 || [])
.filter((col) => col.field) // 빈 필드명 제외
.map((col, idx) => (
<SelectItem key={col.field || `col_${idx}`} 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>
{/* 조건부 테이블 설정 */}
<div className="space-y-3 border rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium"> </h4>
<p className="text-xs text-muted-foreground">
( ) .
</p>
</div>
<Switch
checked={tableConfig.conditionalTable?.enabled ?? false}
onCheckedChange={(checked) => {
setTableConfig({
...tableConfig,
conditionalTable: checked
? { ...defaultConditionalTableConfig, enabled: true }
: { ...defaultConditionalTableConfig, enabled: false },
});
}}
className="scale-75"
/>
</div>
{tableConfig.conditionalTable?.enabled && (
<div className="space-y-4 pt-2">
{/* 트리거 유형 및 조건 컬럼 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs"> </Label>
<Select
value={tableConfig.conditionalTable.triggerType || "checkbox"}
onValueChange={(value: "checkbox" | "dropdown" | "tabs") => {
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
triggerType: value,
},
});
}}
>
<SelectTrigger className="h-8 text-xs mt-1">
<SelectValue placeholder="트리거 유형 선택" />
</SelectTrigger>
<SelectContent>
{CONDITIONAL_TABLE_TRIGGER_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText>
체크박스: 다중 / 드롭다운: 단일 / : 모든
</HelpText>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={tableConfig.conditionalTable.conditionColumn || ""}
onValueChange={(value) => {
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
conditionColumn: value,
},
});
}}
>
<SelectTrigger className="h-8 text-xs mt-1">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{saveTableColumns.map((col) => (
<SelectItem key={col.column_name} value={col.column_name}>
{col.comment || col.column_name}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> .</HelpText>
</div>
</div>
{/* 조건 옵션 목록 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
const newOption: ConditionalTableOption = {
id: generateConditionalOptionId(),
value: "",
label: "",
};
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
options: [...(tableConfig.conditionalTable?.options || []), newOption],
},
});
}}
className="h-7 text-xs"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
</div>
{/* 옵션 목록 */}
<div className="space-y-2">
{(tableConfig.conditionalTable?.options || []).map((option, index) => (
<div key={option.id} className="flex items-center gap-2 border rounded-lg p-2 bg-muted/30">
<Input
value={option.value}
onChange={(e) => {
const newOptions = [...(tableConfig.conditionalTable?.options || [])];
newOptions[index] = { ...newOptions[index], value: e.target.value };
// label이 비어있으면 value와 동일하게 설정
if (!newOptions[index].label) {
newOptions[index].label = e.target.value;
}
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
options: newOptions,
},
});
}}
placeholder="저장 값 (예: 입고검사)"
className="h-8 text-xs flex-1"
/>
<Input
value={option.label}
onChange={(e) => {
const newOptions = [...(tableConfig.conditionalTable?.options || [])];
newOptions[index] = { ...newOptions[index], label: e.target.value };
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
options: newOptions,
},
});
}}
placeholder="표시 라벨 (예: 입고검사)"
className="h-8 text-xs flex-1"
/>
<Button
size="sm"
variant="ghost"
onClick={() => {
const newOptions = (tableConfig.conditionalTable?.options || []).filter(
(_, i) => i !== index
);
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
options: newOptions,
},
});
}}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
))}
{(tableConfig.conditionalTable?.options || []).length === 0 && (
<div className="text-center py-4 text-xs text-muted-foreground border border-dashed rounded-lg">
. (: 입고검사, , )
</div>
)}
</div>
</div>
{/* 테이블에서 옵션 로드 설정 */}
<div className="space-y-2 border-t pt-3">
<div className="flex items-center gap-2">
<Switch
checked={tableConfig.conditionalTable?.optionSource?.enabled ?? false}
onCheckedChange={(checked) => {
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
optionSource: {
...tableConfig.conditionalTable?.optionSource,
enabled: checked,
tableName: tableConfig.conditionalTable?.optionSource?.tableName || "",
valueColumn: tableConfig.conditionalTable?.optionSource?.valueColumn || "",
labelColumn: tableConfig.conditionalTable?.optionSource?.labelColumn || "",
},
},
});
}}
className="scale-75"
/>
<Label className="text-xs"> </Label>
</div>
{tableConfig.conditionalTable?.optionSource?.enabled && (
<OptionSourceConfig
optionSource={tableConfig.conditionalTable.optionSource}
tables={tables}
tableColumns={tableColumns}
onUpdate={(updates) => {
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
optionSource: {
...tableConfig.conditionalTable?.optionSource!,
...updates,
},
},
});
}}
/>
)}
</div>
{/* 소스 테이블 필터링 설정 */}
<div className="space-y-2 border-t pt-3">
<div className="flex items-center gap-2">
<Switch
checked={tableConfig.conditionalTable?.sourceFilter?.enabled ?? false}
onCheckedChange={(checked) => {
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
sourceFilter: {
enabled: checked,
filterColumn: tableConfig.conditionalTable?.sourceFilter?.filterColumn || "",
},
},
});
}}
className="scale-75"
/>
<div>
<Label className="text-xs"> </Label>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
</div>
{tableConfig.conditionalTable?.sourceFilter?.enabled && (
<div className="pl-6">
<Label className="text-[10px]"> </Label>
<p className="text-[10px] text-muted-foreground mb-1">
({tableConfig.source?.tableName || "미설정"})
</p>
{sourceTableColumns.length > 0 ? (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs font-normal"
>
{tableConfig.conditionalTable.sourceFilter.filterColumn
? (() => {
const col = sourceTableColumns.find(
(c) => c.column_name === tableConfig.conditionalTable?.sourceFilter?.filterColumn
);
return col
? `${col.column_name} (${col.comment || col.column_name})`
: tableConfig.conditionalTable.sourceFilter.filterColumn;
})()
: "컬럼 선택..."}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs text-center">
.
</CommandEmpty>
<CommandGroup>
{sourceTableColumns.map((col) => (
<CommandItem
key={col.column_name}
value={`${col.column_name} ${col.comment || ""}`}
onSelect={() => {
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
sourceFilter: {
...tableConfig.conditionalTable?.sourceFilter!,
filterColumn: col.column_name,
},
},
});
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
tableConfig.conditionalTable?.sourceFilter?.filterColumn === col.column_name
? "opacity-100"
: "opacity-0"
)}
/>
<span className="font-medium">{col.column_name}</span>
{col.comment && (
<span className="ml-1 text-muted-foreground">({col.comment})</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
value={tableConfig.conditionalTable.sourceFilter.filterColumn || ""}
onChange={(e) => {
setTableConfig({
...tableConfig,
conditionalTable: {
...tableConfig.conditionalTable!,
sourceFilter: {
...tableConfig.conditionalTable?.sourceFilter!,
filterColumn: e.target.value,
},
},
});
}}
placeholder="inspection_type"
className="h-7 text-xs"
/>
)}
<p className="text-[10px] text-muted-foreground mt-1">
: 검사유형 "입고검사" inspection_type = '입고검사'
</p>
</div>
)}
{/* 사용 가이드 */}
<div className="mt-4 p-3 bg-blue-50/50 border border-blue-200/50 rounded-lg space-y-2">
<p className="text-xs font-medium text-blue-700"> </p>
<div className="text-[10px] text-blue-600 space-y-1.5">
<p className="font-medium">1. :</p>
<ul className="list-disc pl-4 space-y-0.5">
<li><span className="font-medium"> </span>: </li>
<li><span className="font-medium"> </span>: </li>
</ul>
<p className="font-medium mt-2">2. :</p>
<ul className="list-disc pl-4 space-y-0.5">
<li> <span className="font-medium">"선택(드롭다운)"</span> </li>
<li><span className="font-medium">"동적 드롭다운 옵션"</span> </li>
<li> </li>
<li><span className="font-medium">"행 선택 모드"</span> </li>
</ul>
<p className="font-medium mt-2">3. ():</p>
<ul className="list-disc pl-4 space-y-0.5">
<li>"입고검사" </li>
<li>"항목 추가" </li>
<li>"검사항목" inspection_type='입고검사' </li>
<li> , ( )</li>
</ul>
</div>
</div>
</div>
</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>
);
}