4156 lines
201 KiB
TypeScript
4156 lines
201 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useMemo, useCallback } 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 { apiClient } from "@/lib/api/client";
|
|
|
|
// 타입 import
|
|
import {
|
|
FormSectionConfig,
|
|
TableSectionConfig,
|
|
TableColumnConfig,
|
|
TablePreFilter,
|
|
TableModalFilter,
|
|
TableCalculationRule,
|
|
ConditionalCalculationRule,
|
|
ConditionalCalculationConfig,
|
|
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>
|
|
);
|
|
|
|
// 계산 규칙 편집 컴포넌트 (조건부 계산 지원)
|
|
interface CalculationRuleEditorProps {
|
|
calc: TableCalculationRule;
|
|
index: number;
|
|
columns: TableColumnConfig[];
|
|
sourceTableName?: string; // 소스 테이블명 추가
|
|
onUpdate: (updates: Partial<TableCalculationRule>) => void;
|
|
onRemove: () => void;
|
|
}
|
|
|
|
const CalculationRuleEditor: React.FC<CalculationRuleEditorProps> = ({
|
|
calc,
|
|
index,
|
|
columns,
|
|
sourceTableName,
|
|
onUpdate,
|
|
onRemove,
|
|
}) => {
|
|
const [categoryOptions, setCategoryOptions] = useState<{ value: string; label: string }[]>([]);
|
|
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
const [categoryColumns, setCategoryColumns] = useState<Record<string, boolean>>({});
|
|
|
|
// 조건부 계산 활성화 여부
|
|
const isConditionalEnabled = calc.conditionalCalculation?.enabled ?? false;
|
|
|
|
// 소스 테이블의 카테고리 컬럼 정보 로드
|
|
useEffect(() => {
|
|
const loadCategoryColumns = async () => {
|
|
if (!sourceTableName) {
|
|
setCategoryColumns({});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { getCategoryColumns } = await import("@/lib/api/tableCategoryValue");
|
|
const result = await getCategoryColumns(sourceTableName);
|
|
|
|
if (result && result.success && Array.isArray(result.data)) {
|
|
const categoryMap: Record<string, boolean> = {};
|
|
result.data.forEach((col: any) => {
|
|
// API 응답은 camelCase (columnName)
|
|
const colName = col.columnName || col.column_name;
|
|
if (colName) {
|
|
categoryMap[colName] = true;
|
|
}
|
|
});
|
|
setCategoryColumns(categoryMap);
|
|
}
|
|
} catch (error) {
|
|
console.error("카테고리 컬럼 조회 실패:", error);
|
|
}
|
|
};
|
|
|
|
loadCategoryColumns();
|
|
}, [sourceTableName]);
|
|
|
|
// 조건 필드가 선택되었을 때 옵션 로드 (테이블 타입 관리의 카테고리 기준)
|
|
useEffect(() => {
|
|
const loadConditionOptions = async () => {
|
|
if (!isConditionalEnabled || !calc.conditionalCalculation?.conditionField) {
|
|
setCategoryOptions([]);
|
|
return;
|
|
}
|
|
|
|
const conditionField = calc.conditionalCalculation.conditionField;
|
|
|
|
// 소스 필드(sourceField)가 있으면 해당 필드명 사용, 없으면 field명 사용
|
|
const selectedColumn = columns.find((col) => col.field === conditionField);
|
|
const actualFieldName = selectedColumn?.sourceField || conditionField;
|
|
|
|
// 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인
|
|
if (sourceTableName && categoryColumns[actualFieldName]) {
|
|
try {
|
|
setLoadingOptions(true);
|
|
const { getCategoryValues } = await import("@/lib/api/tableCategoryValue");
|
|
const result = await getCategoryValues(sourceTableName, actualFieldName, false);
|
|
if (result && result.success && Array.isArray(result.data)) {
|
|
const options = result.data.map((item: any) => ({
|
|
// API 응답은 camelCase (valueCode, valueLabel)
|
|
value: item.valueCode || item.value_code || item.value,
|
|
label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value,
|
|
}));
|
|
setCategoryOptions(options);
|
|
} else {
|
|
setCategoryOptions([]);
|
|
}
|
|
} catch (error) {
|
|
console.error("카테고리 값 로드 실패:", error);
|
|
setCategoryOptions([]);
|
|
} finally {
|
|
setLoadingOptions(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 카테고리 키가 직접 설정된 경우 (저장된 값)
|
|
const categoryKey = calc.conditionalCalculation?.conditionFieldCategoryKey;
|
|
if (categoryKey) {
|
|
try {
|
|
setLoadingOptions(true);
|
|
const [tableName, columnName] = categoryKey.split(".");
|
|
if (tableName && columnName) {
|
|
const { getCategoryValues } = await import("@/lib/api/tableCategoryValue");
|
|
const result = await getCategoryValues(tableName, columnName, false);
|
|
if (result && result.success && Array.isArray(result.data)) {
|
|
setCategoryOptions(
|
|
result.data.map((item: any) => ({
|
|
// API 응답은 camelCase (valueCode, valueLabel)
|
|
value: item.valueCode || item.value_code || item.value,
|
|
label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value,
|
|
}))
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("카테고리 옵션 로드 실패:", error);
|
|
} finally {
|
|
setLoadingOptions(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 그 외 타입은 옵션 없음 (직접 입력)
|
|
setCategoryOptions([]);
|
|
};
|
|
|
|
loadConditionOptions();
|
|
}, [isConditionalEnabled, calc.conditionalCalculation?.conditionField, calc.conditionalCalculation?.conditionFieldCategoryKey, columns, sourceTableName, categoryColumns]);
|
|
|
|
// 조건부 계산 토글
|
|
const toggleConditionalCalculation = (enabled: boolean) => {
|
|
onUpdate({
|
|
conditionalCalculation: enabled
|
|
? {
|
|
enabled: true,
|
|
conditionField: "",
|
|
conditionFieldType: "static",
|
|
rules: [],
|
|
defaultFormula: calc.formula || "",
|
|
}
|
|
: undefined,
|
|
});
|
|
};
|
|
|
|
// 조건 필드 변경
|
|
const updateConditionField = (field: string) => {
|
|
const selectedColumn = columns.find((col) => col.field === field);
|
|
const actualFieldName = selectedColumn?.sourceField || field;
|
|
|
|
// 컬럼의 타입과 옵션 확인 (테이블 타입 관리의 카테고리 기준)
|
|
let conditionFieldType: "static" | "code" | "table" = "static";
|
|
let conditionFieldCategoryKey = "";
|
|
|
|
// 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인
|
|
if (sourceTableName && categoryColumns[actualFieldName]) {
|
|
conditionFieldType = "code";
|
|
conditionFieldCategoryKey = `${sourceTableName}.${actualFieldName}`;
|
|
}
|
|
|
|
onUpdate({
|
|
conditionalCalculation: {
|
|
...calc.conditionalCalculation!,
|
|
conditionField: field,
|
|
conditionFieldType,
|
|
conditionFieldCategoryKey,
|
|
rules: [], // 필드 변경 시 규칙 초기화
|
|
},
|
|
});
|
|
};
|
|
|
|
// 조건 규칙 추가
|
|
const addConditionRule = () => {
|
|
const newRule: ConditionalCalculationRule = {
|
|
conditionValue: "",
|
|
formula: calc.formula || "",
|
|
};
|
|
onUpdate({
|
|
conditionalCalculation: {
|
|
...calc.conditionalCalculation!,
|
|
rules: [...(calc.conditionalCalculation?.rules || []), newRule],
|
|
},
|
|
});
|
|
};
|
|
|
|
// 조건 규칙 업데이트
|
|
const updateConditionRule = (ruleIndex: number, updates: Partial<ConditionalCalculationRule>) => {
|
|
const newRules = [...(calc.conditionalCalculation?.rules || [])];
|
|
newRules[ruleIndex] = { ...newRules[ruleIndex], ...updates };
|
|
onUpdate({
|
|
conditionalCalculation: {
|
|
...calc.conditionalCalculation!,
|
|
rules: newRules,
|
|
},
|
|
});
|
|
};
|
|
|
|
// 조건 규칙 삭제
|
|
const removeConditionRule = (ruleIndex: number) => {
|
|
onUpdate({
|
|
conditionalCalculation: {
|
|
...calc.conditionalCalculation!,
|
|
rules: (calc.conditionalCalculation?.rules || []).filter((_, i) => i !== ruleIndex),
|
|
},
|
|
});
|
|
};
|
|
|
|
// 기본 계산식 업데이트
|
|
const updateDefaultFormula = (formula: string) => {
|
|
onUpdate({
|
|
conditionalCalculation: {
|
|
...calc.conditionalCalculation!,
|
|
defaultFormula: formula,
|
|
},
|
|
});
|
|
};
|
|
|
|
// 조건 필드로 사용 가능한 컬럼 (모든 컬럼)
|
|
const availableColumns = columns.filter((col) => col.field);
|
|
|
|
return (
|
|
<div className="border rounded-lg p-3 bg-muted/30 space-y-3">
|
|
{/* 기본 계산 규칙 */}
|
|
<div className="flex items-center gap-2">
|
|
<Select
|
|
value={calc.resultField || ""}
|
|
onValueChange={(value) => onUpdate({ resultField: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs w-[150px]">
|
|
<SelectValue placeholder="결과 필드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columns.length === 0 ? (
|
|
<SelectItem value="__no_columns__" disabled>
|
|
컬럼 설정에서 먼저 컬럼을 추가하세요
|
|
</SelectItem>
|
|
) : (
|
|
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) => onUpdate({ formula: e.target.value })}
|
|
placeholder="수식 (예: qty * unit_price)"
|
|
className="h-8 text-xs flex-1"
|
|
disabled={isConditionalEnabled}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={onRemove}
|
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 조건부 계산 토글 */}
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
id={`conditional-calc-${index}`}
|
|
checked={isConditionalEnabled}
|
|
onCheckedChange={toggleConditionalCalculation}
|
|
className="scale-75"
|
|
/>
|
|
<Label htmlFor={`conditional-calc-${index}`} className="text-xs cursor-pointer">
|
|
조건부 계산 활성화
|
|
</Label>
|
|
{availableColumns.length === 0 && !isConditionalEnabled && (
|
|
<span className="text-[10px] text-muted-foreground ml-2">
|
|
(컬럼 설정에서 먼저 컬럼을 추가하세요)
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* 조건부 계산 설정 */}
|
|
{isConditionalEnabled && (
|
|
<div className="border-t pt-3 space-y-3">
|
|
{/* 조건 필드 선택 */}
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs w-[80px] shrink-0">조건 필드:</Label>
|
|
<Select
|
|
value={calc.conditionalCalculation?.conditionField || ""}
|
|
onValueChange={updateConditionField}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs flex-1">
|
|
<SelectValue placeholder="조건 기준 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableColumns.length === 0 ? (
|
|
<SelectItem value="__no_columns__" disabled>
|
|
컬럼이 없습니다
|
|
</SelectItem>
|
|
) : (
|
|
availableColumns.map((col, idx) => {
|
|
// 소스 필드명으로 카테고리 여부 확인
|
|
const actualFieldName = col.sourceField || col.field;
|
|
const isCategoryColumn = categoryColumns[actualFieldName];
|
|
return (
|
|
<SelectItem key={col.field || `col_${idx}`} value={col.field}>
|
|
{col.label || col.field} {isCategoryColumn ? "(카테고리)" : `(${col.type})`}
|
|
</SelectItem>
|
|
);
|
|
})
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 조건별 계산식 목록 */}
|
|
{calc.conditionalCalculation?.conditionField && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs">조건별 계산식:</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addConditionRule}
|
|
className="h-6 text-[10px] px-2"
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
조건 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{(calc.conditionalCalculation?.rules || []).map((rule, ruleIndex) => (
|
|
<div key={ruleIndex} className="flex items-center gap-2 bg-background rounded p-2">
|
|
{/* 조건값 선택 */}
|
|
{categoryOptions.length > 0 ? (
|
|
<Select
|
|
value={rule.conditionValue}
|
|
onValueChange={(value) =>
|
|
updateConditionRule(ruleIndex, { conditionValue: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs w-[120px]">
|
|
<SelectValue placeholder="조건값" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categoryOptions.map((opt, optIdx) => (
|
|
<SelectItem key={`${opt.value}_${optIdx}`} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<Input
|
|
value={rule.conditionValue}
|
|
onChange={(e) =>
|
|
updateConditionRule(ruleIndex, { conditionValue: e.target.value })
|
|
}
|
|
placeholder="조건값"
|
|
className="h-7 text-xs w-[120px]"
|
|
/>
|
|
)}
|
|
<span className="text-xs text-muted-foreground">→</span>
|
|
<Input
|
|
value={rule.formula}
|
|
onChange={(e) =>
|
|
updateConditionRule(ruleIndex, { formula: e.target.value })
|
|
}
|
|
placeholder="계산식"
|
|
className="h-7 text-xs flex-1"
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeConditionRule(ruleIndex)}
|
|
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
|
|
{/* 기본 계산식 */}
|
|
<div className="flex items-center gap-2 bg-background/50 rounded p-2 border-dashed border">
|
|
<span className="text-xs text-muted-foreground w-[120px] text-center">
|
|
(기본값)
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">→</span>
|
|
<Input
|
|
value={calc.conditionalCalculation?.defaultFormula || ""}
|
|
onChange={(e) => updateDefaultFormula(e.target.value)}
|
|
placeholder="기본 계산식 (조건 미해당 시)"
|
|
className="h-7 text-xs flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{loadingOptions && (
|
|
<p className="text-[10px] text-muted-foreground">옵션 로딩 중...</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 옵션 소스 설정 컴포넌트 (검색 가능한 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; // 소스 테이블명
|
|
externalTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 외부 데이터 테이블 컬럼
|
|
externalTableName?: string; // 외부 데이터 테이블명
|
|
externalDataEnabled?: boolean; // 외부 데이터 소스 활성화 여부
|
|
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,
|
|
externalTableColumns,
|
|
externalTableName,
|
|
externalDataEnabled,
|
|
tables,
|
|
tableColumns,
|
|
sections,
|
|
formFields,
|
|
tableConfig,
|
|
availableParentFields = [],
|
|
onLoadTableColumns,
|
|
onUpdate,
|
|
onMoveUp,
|
|
onMoveDown,
|
|
onRemove,
|
|
}: ColumnSettingItemProps) {
|
|
const [fieldSearchOpen, setFieldSearchOpen] = useState(false);
|
|
const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false);
|
|
const [externalFieldSearchOpen, setExternalFieldSearchOpen] = 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>
|
|
|
|
{/* 외부 필드 - Combobox (외부 데이터에서 가져올 컬럼) */}
|
|
{externalDataEnabled && externalTableName && (
|
|
<div>
|
|
<Label className="text-xs">외부 필드</Label>
|
|
<Popover open={externalFieldSearchOpen} onOpenChange={setExternalFieldSearchOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={externalFieldSearchOpen}
|
|
className="h-8 w-full justify-between text-xs mt-1"
|
|
disabled={externalTableColumns.length === 0}
|
|
>
|
|
<span className="truncate">
|
|
{col.externalField || "(필드명과 동일)"}
|
|
</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({ externalField: undefined });
|
|
setExternalFieldSearchOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
!col.externalField ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<span className="text-muted-foreground">(필드명과 동일)</span>
|
|
</CommandItem>
|
|
{/* 외부 테이블 컬럼 목록 */}
|
|
{externalTableColumns.map((extCol) => (
|
|
<CommandItem
|
|
key={extCol.column_name}
|
|
value={extCol.column_name}
|
|
onSelect={() => {
|
|
onUpdate({ externalField: extCol.column_name });
|
|
setExternalFieldSearchOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.externalField === extCol.column_name ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col flex-1 min-w-0">
|
|
<span className="font-medium truncate">{extCol.column_name}</span>
|
|
{extCol.comment && (
|
|
<span className="text-[10px] text-muted-foreground truncate">
|
|
{extCol.comment}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
외부 데이터({externalTableName})에서 이 컬럼에 매핑할 필드
|
|
</p>
|
|
</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="UI에서 숨기지만 데이터는 유지됩니다">
|
|
<Switch
|
|
checked={col.hidden ?? false}
|
|
onCheckedChange={(checked) => onUpdate({ hidden: 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; input_type?: 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 [externalTableSearchOpen, setExternalTableSearchOpen] = useState(false);
|
|
|
|
// 활성 탭
|
|
const [activeTab, setActiveTab] = useState("source");
|
|
|
|
// 사전 필터 카테고리 옵션 캐시 (컬럼명 -> 옵션 배열)
|
|
const [preFilterCategoryOptions, setPreFilterCategoryOptions] = useState<
|
|
Record<string, { value: string; label: string }[]>
|
|
>({});
|
|
|
|
// 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 loadCategoryOptions = useCallback(async (columnName: string) => {
|
|
if (!tableConfig.source.tableName || !columnName) return;
|
|
|
|
// 이미 로드된 경우 스킵
|
|
if (preFilterCategoryOptions[columnName]) return;
|
|
|
|
try {
|
|
const response = await apiClient.get(
|
|
`/table-categories/${tableConfig.source.tableName}/${columnName}/values`
|
|
);
|
|
if (response.data?.success && response.data?.data) {
|
|
const options = response.data.data.map((item: any) => ({
|
|
// value는 DB에 저장된 실제 값(valueCode)을 사용해야 필터링이 정상 작동
|
|
value: item.valueCode || item.value_code || item.valueLabel || item.value_label || "",
|
|
// label은 사용자에게 보여질 라벨
|
|
label: item.valueLabel || item.value_label || item.valueCode || item.value_code || "",
|
|
}));
|
|
setPreFilterCategoryOptions((prev) => ({
|
|
...prev,
|
|
[columnName]: options,
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error(`카테고리 옵션 로드 실패 (${columnName}):`, error);
|
|
}
|
|
}, [tableConfig.source.tableName, preFilterCategoryOptions]);
|
|
|
|
// 사전 필터에서 선택된 카테고리 컬럼들의 옵션 자동 로드
|
|
useEffect(() => {
|
|
const preFilters = tableConfig.filters?.preFilters || [];
|
|
preFilters.forEach((filter) => {
|
|
if (filter.column) {
|
|
const col = sourceTableColumns.find((c) => c.column_name === filter.column);
|
|
if (col && col.input_type === "category") {
|
|
loadCategoryOptions(filter.column);
|
|
}
|
|
}
|
|
});
|
|
}, [tableConfig.filters?.preFilters, sourceTableColumns, loadCategoryOptions]);
|
|
|
|
// 저장 테이블의 컬럼 목록
|
|
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 updateExternalDataSource = (updates: Partial<NonNullable<TableSectionConfig["externalDataSource"]>>) => {
|
|
updateTableConfig({
|
|
externalDataSource: { ...tableConfig.externalDataSource, enabled: false, tableName: "", ...updates },
|
|
});
|
|
};
|
|
|
|
// 외부 데이터 소스 테이블 컬럼 목록
|
|
const externalTableColumns = useMemo(() => {
|
|
return tableColumns[tableConfig.externalDataSource?.tableName || ""] || [];
|
|
}, [tableColumns, tableConfig.externalDataSource?.tableName]);
|
|
|
|
// 외부 데이터 소스 테이블 변경 시 컬럼 로드
|
|
useEffect(() => {
|
|
if (tableConfig.externalDataSource?.enabled && tableConfig.externalDataSource?.tableName) {
|
|
onLoadTableColumns(tableConfig.externalDataSource.tableName);
|
|
}
|
|
}, [tableConfig.externalDataSource?.enabled, tableConfig.externalDataSource?.tableName, onLoadTableColumns]);
|
|
|
|
// 저장 함수
|
|
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>
|
|
|
|
{/* 외부 데이터 소스 설정 */}
|
|
<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.externalDataSource?.enabled ?? false}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
updateExternalDataSource({ enabled: true, tableName: "" });
|
|
} else {
|
|
updateTableConfig({ externalDataSource: undefined });
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{tableConfig.externalDataSource?.enabled && (
|
|
<div className="space-y-3 pt-2">
|
|
<div>
|
|
<Label className="text-xs font-medium mb-1.5 block">외부 데이터 테이블</Label>
|
|
<Popover open={externalTableSearchOpen} onOpenChange={setExternalTableSearchOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={externalTableSearchOpen}
|
|
className="h-9 w-full justify-between text-sm"
|
|
>
|
|
{tableConfig.externalDataSource?.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={() => {
|
|
updateExternalDataSource({ enabled: true, tableName: table.table_name });
|
|
onLoadTableColumns(table.table_name);
|
|
setExternalTableSearchOpen(false);
|
|
}}
|
|
className="text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
tableConfig.externalDataSource?.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>
|
|
<HelpText>이전 화면에서 전달받을 데이터의 원본 테이블을 선택하세요. (예: 수주상세 데이터를 전달받는 경우 sales_order_detail)</HelpText>
|
|
</div>
|
|
|
|
{tableConfig.externalDataSource?.tableName && externalTableColumns.length > 0 && (
|
|
<div className="bg-muted/30 rounded-lg p-3">
|
|
<p className="text-xs text-muted-foreground">
|
|
선택한 테이블 컬럼: {externalTableColumns.length}개
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground mt-1">
|
|
"컬럼 설정" 탭에서 각 컬럼의 "외부 필드"를 설정하여 전달받은 데이터의 컬럼을 매핑하세요.
|
|
</p>
|
|
</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}
|
|
externalTableColumns={externalTableColumns}
|
|
externalTableName={tableConfig.externalDataSource?.tableName}
|
|
externalDataEnabled={tableConfig.externalDataSource?.enabled}
|
|
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) => {
|
|
// 선택된 컬럼의 정보 조회
|
|
const selectedColumn = filter.column
|
|
? sourceTableColumns.find((c) => c.column_name === filter.column)
|
|
: null;
|
|
const isCategory = selectedColumn?.input_type === "category";
|
|
const categoryOptions = isCategory && filter.column
|
|
? preFilterCategoryOptions[filter.column] || []
|
|
: [];
|
|
|
|
return (
|
|
<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, value: "" }); // 컬럼 변경 시 값 초기화
|
|
// 카테고리 컬럼인 경우 옵션 로드
|
|
const col = sourceTableColumns.find((c) => c.column_name === value);
|
|
if (col && col.input_type === "category") {
|
|
loadCategoryOptions(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.comment || 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>
|
|
|
|
{/* 카테고리 컬럼인 경우 Select Box로 값 선택 */}
|
|
{isCategory ? (
|
|
<Select
|
|
value={filter.value || undefined}
|
|
onValueChange={(value) => updatePreFilter(index, { value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs flex-1">
|
|
<SelectValue placeholder="값 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categoryOptions.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="space-y-2 p-3 bg-muted/30 rounded-lg">
|
|
<Label className="text-xs font-medium">표시할 버튼 선택</Label>
|
|
<p className="text-[10px] text-muted-foreground mb-2">
|
|
두 버튼을 동시에 표시할 수 있습니다.
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={tableConfig.uiConfig?.showSearchButton ?? true}
|
|
onCheckedChange={(checked) => updateUiConfig({ showSearchButton: checked })}
|
|
className="scale-75"
|
|
/>
|
|
<div>
|
|
<span className="text-xs font-medium">검색 버튼</span>
|
|
<p className="text-[10px] text-muted-foreground">기존 데이터에서 선택</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={tableConfig.uiConfig?.showAddRowButton ?? false}
|
|
onCheckedChange={(checked) => updateUiConfig({ showAddRowButton: checked })}
|
|
className="scale-75"
|
|
/>
|
|
<div>
|
|
<span className="text-xs font-medium">행 추가 버튼</span>
|
|
<p className="text-[10px] text-muted-foreground">빈 행 직접 입력</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* 검색 버튼 텍스트 */}
|
|
<div>
|
|
<Label className="text-xs">검색 버튼 텍스트</Label>
|
|
<Input
|
|
value={tableConfig.uiConfig?.searchButtonText || ""}
|
|
onChange={(e) => updateUiConfig({ searchButtonText: e.target.value })}
|
|
placeholder="품목 검색"
|
|
className="h-8 text-xs mt-1"
|
|
disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)}
|
|
/>
|
|
</div>
|
|
{/* 행 추가 버튼 텍스트 */}
|
|
<div>
|
|
<Label className="text-xs">행 추가 버튼 텍스트</Label>
|
|
<Input
|
|
value={tableConfig.uiConfig?.addRowButtonText || ""}
|
|
onChange={(e) => updateUiConfig({ addRowButtonText: e.target.value })}
|
|
placeholder="직접 입력"
|
|
className="h-8 text-xs mt-1"
|
|
disabled={!tableConfig.uiConfig?.showAddRowButton}
|
|
/>
|
|
</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?.showSearchButton ?? true)}
|
|
/>
|
|
</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?.showSearchButton ?? true)}
|
|
/>
|
|
<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) => (
|
|
<CalculationRuleEditor
|
|
key={index}
|
|
calc={calc}
|
|
index={index}
|
|
columns={tableConfig.columns || []}
|
|
sourceTableName={tableConfig.source?.tableName}
|
|
onUpdate={(updates) => updateCalculation(index, updates)}
|
|
onRemove={() => removeCalculation(index)}
|
|
/>
|
|
))}
|
|
</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>
|
|
);
|
|
}
|