ERP-node/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx

817 lines
30 KiB
TypeScript
Raw Normal View History

2025-12-19 15:44:38 +09:00
"use client";
/**
* V2Select
2025-12-19 15:44:38 +09:00
* .
*/
import React, { useState, useEffect, useCallback, useMemo } from "react";
2025-12-19 15:44:38 +09:00
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Plus, Trash2, Loader2, Filter } from "lucide-react";
2025-12-19 15:44:38 +09:00
import { apiClient } from "@/lib/api/client";
import type { V2SelectFilter } from "@/types/v2-components";
2025-12-19 15:44:38 +09:00
interface ColumnOption {
columnName: string;
columnLabel: string;
}
interface CategoryValueOption {
valueCode: string;
valueLabel: string;
}
const OPERATOR_OPTIONS = [
{ value: "=", label: "같음 (=)" },
{ value: "!=", label: "다름 (!=)" },
{ value: ">", label: "초과 (>)" },
{ value: "<", label: "미만 (<)" },
{ value: ">=", label: "이상 (>=)" },
{ value: "<=", label: "이하 (<=)" },
{ value: "in", label: "포함 (IN)" },
{ value: "notIn", label: "미포함 (NOT IN)" },
{ value: "like", label: "유사 (LIKE)" },
{ value: "isNull", label: "NULL" },
{ value: "isNotNull", label: "NOT NULL" },
] as const;
const VALUE_TYPE_OPTIONS = [
{ value: "static", label: "고정값" },
{ value: "field", label: "폼 필드 참조" },
{ value: "user", label: "로그인 사용자" },
] as const;
const USER_FIELD_OPTIONS = [
{ value: "companyCode", label: "회사코드" },
{ value: "userId", label: "사용자ID" },
{ value: "deptCode", label: "부서코드" },
{ value: "userName", label: "사용자명" },
] as const;
/**
*
*/
const FilterConditionsSection: React.FC<{
filters: V2SelectFilter[];
columns: ColumnOption[];
loadingColumns: boolean;
targetTable: string;
onFiltersChange: (filters: V2SelectFilter[]) => void;
}> = ({ filters, columns, loadingColumns, targetTable, onFiltersChange }) => {
const addFilter = () => {
onFiltersChange([
...filters,
{ column: "", operator: "=", valueType: "static", value: "" },
]);
};
const updateFilter = (index: number, patch: Partial<V2SelectFilter>) => {
const updated = [...filters];
updated[index] = { ...updated[index], ...patch };
// valueType 변경 시 관련 필드 초기화
if (patch.valueType) {
if (patch.valueType === "static") {
updated[index].fieldRef = undefined;
updated[index].userField = undefined;
} else if (patch.valueType === "field") {
updated[index].value = undefined;
updated[index].userField = undefined;
} else if (patch.valueType === "user") {
updated[index].value = undefined;
updated[index].fieldRef = undefined;
}
}
// isNull/isNotNull 연산자는 값 불필요
if (patch.operator === "isNull" || patch.operator === "isNotNull") {
updated[index].value = undefined;
updated[index].fieldRef = undefined;
updated[index].userField = undefined;
updated[index].valueType = "static";
}
onFiltersChange(updated);
};
const removeFilter = (index: number) => {
onFiltersChange(filters.filter((_, i) => i !== index));
};
const needsValue = (op: string) => op !== "isNull" && op !== "isNotNull";
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Filter className="h-3.5 w-3.5 text-muted-foreground" />
<Label className="text-xs font-medium"> </Label>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={addFilter}
className="h-6 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<p className="text-muted-foreground text-[10px]">
{targetTable}
</p>
{loadingColumns && (
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
)}
{filters.length === 0 && (
<p className="text-muted-foreground py-2 text-center text-xs">
</p>
)}
<div className="space-y-3">
{filters.map((filter, index) => (
<div key={index} className="space-y-1.5 rounded-md border p-2">
{/* 행 1: 컬럼 + 연산자 + 삭제 */}
<div className="flex items-center gap-1.5">
{/* 컬럼 선택 */}
<Select
value={filter.column || ""}
onValueChange={(v) => updateFilter(index, { column: v })}
>
<SelectTrigger className="h-7 flex-1 text-[11px]">
<SelectValue placeholder="컬럼" />
</SelectTrigger>
<SelectContent>
{columns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 연산자 선택 */}
<Select
value={filter.operator || "="}
onValueChange={(v) => updateFilter(index, { operator: v as V2SelectFilter["operator"] })}
>
<SelectTrigger className="h-7 w-[90px] shrink-0 text-[11px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{OPERATOR_OPTIONS.map((op) => (
<SelectItem key={op.value} value={op.value}>
{op.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 삭제 버튼 */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeFilter(index)}
className="text-destructive h-7 w-7 shrink-0 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 행 2: 값 유형 + 값 입력 (isNull/isNotNull 제외) */}
{needsValue(filter.operator) && (
<div className="flex items-center gap-1.5">
{/* 값 유형 */}
<Select
value={filter.valueType || "static"}
onValueChange={(v) => updateFilter(index, { valueType: v as V2SelectFilter["valueType"] })}
>
<SelectTrigger className="h-7 w-[100px] shrink-0 text-[11px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{VALUE_TYPE_OPTIONS.map((vt) => (
<SelectItem key={vt.value} value={vt.value}>
{vt.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 값 입력 영역 */}
{(filter.valueType || "static") === "static" && (
<Input
value={String(filter.value ?? "")}
onChange={(e) => updateFilter(index, { value: e.target.value })}
placeholder={filter.operator === "in" || filter.operator === "notIn" ? "값1, 값2, ..." : "값 입력"}
className="h-7 flex-1 text-[11px]"
/>
)}
{filter.valueType === "field" && (
<Input
value={filter.fieldRef || ""}
onChange={(e) => updateFilter(index, { fieldRef: e.target.value })}
placeholder="참조할 필드명 (columnName)"
className="h-7 flex-1 text-[11px]"
/>
)}
{filter.valueType === "user" && (
<Select
value={filter.userField || ""}
onValueChange={(v) => updateFilter(index, { userField: v as V2SelectFilter["userField"] })}
>
<SelectTrigger className="h-7 flex-1 text-[11px]">
<SelectValue placeholder="사용자 필드" />
</SelectTrigger>
<SelectContent>
{USER_FIELD_OPTIONS.map((uf) => (
<SelectItem key={uf.value} value={uf.value}>
{uf.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
)}
</div>
))}
</div>
</div>
);
};
interface V2SelectConfigPanelProps {
2025-12-19 15:44:38 +09:00
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
/** 컬럼의 inputType (entity/category 타입 확인용) */
2025-12-23 10:49:28 +09:00
inputType?: string;
/** 현재 테이블명 (카테고리 값 조회용) */
tableName?: string;
/** 현재 컬럼명 (카테고리 값 조회용) */
columnName?: string;
2025-12-19 15:44:38 +09:00
}
export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
config,
onChange,
inputType,
tableName,
columnName,
}) => {
2025-12-23 10:49:28 +09:00
const isEntityType = inputType === "entity";
const isCategoryType = inputType === "category";
2025-12-19 15:44:38 +09:00
const [entityColumns, setEntityColumns] = useState<ColumnOption[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
// 카테고리 값 목록
const [categoryValues, setCategoryValues] = useState<CategoryValueOption[]>([]);
const [loadingCategoryValues, setLoadingCategoryValues] = useState(false);
// 필터용 컬럼 목록 (옵션 데이터 소스 테이블의 컬럼)
const [filterColumns, setFilterColumns] = useState<ColumnOption[]>([]);
const [loadingFilterColumns, setLoadingFilterColumns] = useState(false);
2025-12-19 15:44:38 +09:00
const updateConfig = (field: string, value: any) => {
onChange({ ...config, [field]: value });
};
// 필터 대상 테이블 결정
const filterTargetTable = useMemo(() => {
const src = config.source || "static";
if (src === "entity") return config.entityTable;
if (src === "db") return config.table;
if (src === "distinct" || src === "select") return tableName;
return null;
}, [config.source, config.entityTable, config.table, tableName]);
// 필터 대상 테이블의 컬럼 로드
useEffect(() => {
if (!filterTargetTable) {
setFilterColumns([]);
return;
}
const loadFilterColumns = async () => {
setLoadingFilterColumns(true);
try {
const response = await apiClient.get(`/table-management/tables/${filterTargetTable}/columns?size=500`);
const data = response.data.data || response.data;
const columns = data.columns || data || [];
setFilterColumns(
columns.map((col: any) => ({
columnName: col.columnName || col.column_name || col.name,
columnLabel: col.displayName || col.display_name || col.columnLabel || col.column_label || col.columnName || col.column_name || col.name,
}))
);
} catch {
setFilterColumns([]);
} finally {
setLoadingFilterColumns(false);
}
};
loadFilterColumns();
}, [filterTargetTable]);
// 카테고리 타입이면 source를 자동으로 category로 설정
useEffect(() => {
if (isCategoryType && config.source !== "category") {
onChange({ ...config, source: "category" });
}
}, [isCategoryType]);
// 카테고리 값 로드
const loadCategoryValues = useCallback(async (catTable: string, catColumn: string) => {
if (!catTable || !catColumn) {
setCategoryValues([]);
return;
}
setLoadingCategoryValues(true);
try {
const response = await apiClient.get(`/table-categories/${catTable}/${catColumn}/values`);
const data = response.data;
if (data.success && data.data) {
const flattenTree = (items: any[], depth: number = 0): CategoryValueOption[] => {
const result: CategoryValueOption[] = [];
for (const item of items) {
result.push({
valueCode: item.valueCode,
valueLabel: depth > 0 ? `${" ".repeat(depth)}${item.valueLabel}` : item.valueLabel,
});
if (item.children && item.children.length > 0) {
result.push(...flattenTree(item.children, depth + 1));
}
}
return result;
};
setCategoryValues(flattenTree(data.data));
}
} catch (error) {
console.error("카테고리 값 조회 실패:", error);
setCategoryValues([]);
} finally {
setLoadingCategoryValues(false);
}
}, []);
// 카테고리 소스일 때 값 로드
useEffect(() => {
if (config.source === "category") {
const catTable = config.categoryTable || tableName;
const catColumn = config.categoryColumn || columnName;
if (catTable && catColumn) {
loadCategoryValues(catTable, catColumn);
}
}
}, [config.source, config.categoryTable, config.categoryColumn, tableName, columnName, loadCategoryValues]);
2025-12-19 15:44:38 +09:00
// 엔티티 테이블 변경 시 컬럼 목록 조회
const loadEntityColumns = useCallback(async (tblName: string) => {
if (!tblName) {
2025-12-19 15:44:38 +09:00
setEntityColumns([]);
return;
}
setLoadingColumns(true);
try {
const response = await apiClient.get(`/table-management/tables/${tblName}/columns?size=500`);
2025-12-19 15:44:38 +09:00
const data = response.data.data || response.data;
const columns = data.columns || data || [];
2025-12-19 15:44:38 +09:00
const columnOptions: ColumnOption[] = columns.map((col: any) => {
const name = col.columnName || col.column_name || col.name;
const label = col.displayName || col.display_name || col.columnLabel || col.column_label || name;
2025-12-19 15:44:38 +09:00
return {
columnName: name,
columnLabel: label,
};
});
2025-12-19 15:44:38 +09:00
setEntityColumns(columnOptions);
} catch (error) {
console.error("컬럼 목록 조회 실패:", error);
setEntityColumns([]);
} finally {
setLoadingColumns(false);
}
}, []);
useEffect(() => {
if (config.source === "entity" && config.entityTable) {
loadEntityColumns(config.entityTable);
}
}, [config.source, config.entityTable, loadEntityColumns]);
// 정적 옵션 관리
const options = config.options || [];
2025-12-19 15:44:38 +09:00
const addOption = () => {
const newOptions = [...options, { value: "", label: "" }];
updateConfig("options", newOptions);
};
const updateOptionValue = (index: number, value: string) => {
2025-12-19 15:44:38 +09:00
const newOptions = [...options];
newOptions[index] = { ...newOptions[index], value, label: value };
2025-12-19 15:44:38 +09:00
updateConfig("options", newOptions);
};
const removeOption = (index: number) => {
const newOptions = options.filter((_: any, i: number) => i !== index);
updateConfig("options", newOptions);
};
// 현재 source 결정 (카테고리 타입이면 강제 category)
const effectiveSource = isCategoryType ? "category" : config.source || "static";
2025-12-19 15:44:38 +09:00
return (
<div className="space-y-4">
{/* 선택 모드 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select value={config.mode || "dropdown"} onValueChange={(value) => updateConfig("mode", value)}>
2025-12-19 15:44:38 +09:00
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="모드 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="dropdown"></SelectItem>
<SelectItem value="combobox"> ()</SelectItem>
2025-12-19 15:44:38 +09:00
<SelectItem value="radio"> </SelectItem>
<SelectItem value="check"></SelectItem>
<SelectItem value="tag"> </SelectItem>
<SelectItem value="tagbox"> (+)</SelectItem>
2025-12-19 15:44:38 +09:00
<SelectItem value="toggle"> </SelectItem>
<SelectItem value="swap"> </SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
{/* 데이터 소스 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
{isCategoryType ? (
<div className="bg-muted flex h-8 items-center rounded-md px-3">
<span className="text-xs font-medium text-emerald-600"> ( )</span>
</div>
) : (
<Select value={config.source || "static"} onValueChange={(value) => updateConfig("source", value)}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="소스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="static"> </SelectItem>
<SelectItem value="code"> </SelectItem>
<SelectItem value="category"></SelectItem>
{isEntityType && <SelectItem value="entity"></SelectItem>}
</SelectContent>
</Select>
)}
2025-12-19 15:44:38 +09:00
</div>
{/* 카테고리 설정 */}
{effectiveSource === "category" && (
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs font-medium"> </Label>
<div className="bg-muted rounded-md p-2">
<div className="grid grid-cols-2 gap-2">
<div>
<p className="text-muted-foreground text-[10px]"></p>
<p className="text-xs font-medium">{config.categoryTable || tableName || "-"}</p>
</div>
<div>
<p className="text-muted-foreground text-[10px]"></p>
<p className="text-xs font-medium">{config.categoryColumn || columnName || "-"}</p>
</div>
</div>
</div>
</div>
{/* 카테고리 값 로딩 중 */}
{loadingCategoryValues && (
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
)}
{/* 카테고리 값 목록 표시 */}
{categoryValues.length > 0 && (
<div className="space-y-2">
<Label className="text-xs font-medium"> ({categoryValues.length})</Label>
<div className="bg-muted max-h-32 space-y-0.5 overflow-y-auto rounded-md p-1.5">
{categoryValues.map((cv) => (
<div key={cv.valueCode} className="flex items-center gap-2 px-1.5 py-0.5">
<span className="text-muted-foreground shrink-0 font-mono text-[10px]">{cv.valueCode}</span>
<span className="truncate text-xs">{cv.valueLabel}</span>
</div>
))}
</div>
</div>
)}
{/* 기본값 설정 */}
{categoryValues.length > 0 && (
<div className="border-t pt-2">
<Label className="text-xs font-medium"></Label>
<Select
value={config.defaultValue || "_none_"}
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
>
<SelectTrigger className="mt-1 h-8 text-xs">
<SelectValue placeholder="기본값 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_none_"> </SelectItem>
{categoryValues.map((cv) => (
<SelectItem key={cv.valueCode} value={cv.valueCode}>
{cv.valueLabel}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-[10px]"> </p>
</div>
)}
{/* 카테고리 값 없음 안내 */}
{!loadingCategoryValues && categoryValues.length === 0 && (
<p className="text-[10px] text-amber-600">
. .
</p>
)}
</div>
)}
2025-12-19 15:44:38 +09:00
{/* 정적 옵션 관리 */}
{effectiveSource === "static" && (
2025-12-19 15:44:38 +09:00
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium"> </Label>
<Button type="button" variant="ghost" size="sm" onClick={addOption} className="h-6 px-2 text-xs">
<Plus className="mr-1 h-3 w-3" />
2025-12-19 15:44:38 +09:00
</Button>
</div>
<div className="max-h-40 space-y-1.5 overflow-y-auto">
2025-12-19 15:44:38 +09:00
{options.map((option: any, index: number) => (
<div key={index} className="flex items-center gap-1.5">
2025-12-19 15:44:38 +09:00
<Input
value={option.value || ""}
onChange={(e) => updateOptionValue(index, e.target.value)}
placeholder={`옵션 ${index + 1}`}
className="h-7 flex-1 text-xs"
2025-12-19 15:44:38 +09:00
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeOption(index)}
className="text-destructive h-7 w-7 shrink-0 p-0"
2025-12-19 15:44:38 +09:00
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
{options.length === 0 && (
<p className="text-muted-foreground py-2 text-center text-xs"> </p>
2025-12-19 15:44:38 +09:00
)}
</div>
{/* 기본값 설정 */}
{options.length > 0 && (
<div className="mt-3 border-t pt-2">
<Label className="text-xs font-medium"></Label>
<Select
value={config.defaultValue || "_none_"}
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
>
<SelectTrigger className="mt-1 h-8 text-xs">
<SelectValue placeholder="기본값 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_none_"> </SelectItem>
{options.map((option: any, index: number) => (
<SelectItem key={`default-${index}`} value={option.value || `_idx_${index}`}>
{option.label || option.value || `옵션 ${index + 1}`}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-[10px]"> </p>
</div>
)}
2025-12-19 15:44:38 +09:00
</div>
)}
{/* 공통 코드 설정 */}
{effectiveSource === "code" && (
2025-12-23 10:49:28 +09:00
<div className="space-y-1">
2025-12-19 15:44:38 +09:00
<Label className="text-xs font-medium"> </Label>
2025-12-23 10:49:28 +09:00
{config.codeGroup ? (
<p className="text-foreground text-sm font-medium">{config.codeGroup}</p>
2025-12-23 10:49:28 +09:00
) : (
<p className="text-xs text-amber-600"> </p>
2025-12-23 10:49:28 +09:00
)}
2025-12-19 15:44:38 +09:00
</div>
)}
{/* 엔티티(참조 테이블) 설정 */}
{effectiveSource === "entity" && (
2025-12-19 15:44:38 +09:00
<div className="space-y-3">
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Input
value={config.entityTable || ""}
readOnly
disabled
placeholder="테이블 타입 관리에서 설정"
className="bg-muted h-8 text-xs"
2025-12-19 15:44:38 +09:00
/>
<p className="text-muted-foreground text-[10px]">
2025-12-19 15:44:38 +09:00
( )
</p>
</div>
{loadingColumns && (
<div className="text-muted-foreground flex items-center gap-2 text-xs">
2025-12-19 15:44:38 +09:00
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
)}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label className="text-xs font-medium"> ()</Label>
{entityColumns.length > 0 ? (
<Select
value={config.entityValueColumn || ""}
onValueChange={(value) => updateConfig("entityValueColumn", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{entityColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={config.entityValueColumn || ""}
onChange={(e) => updateConfig("entityValueColumn", e.target.value)}
placeholder="id"
className="h-8 text-xs"
/>
)}
<p className="text-muted-foreground text-[10px]"> </p>
2025-12-19 15:44:38 +09:00
</div>
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
{entityColumns.length > 0 ? (
<Select
value={config.entityLabelColumn || ""}
onValueChange={(value) => updateConfig("entityLabelColumn", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{entityColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={config.entityLabelColumn || ""}
onChange={(e) => updateConfig("entityLabelColumn", e.target.value)}
placeholder="name"
className="h-8 text-xs"
/>
)}
<p className="text-muted-foreground text-[10px]"> </p>
2025-12-19 15:44:38 +09:00
</div>
</div>
{config.entityTable && !loadingColumns && entityColumns.length === 0 && (
<p className="text-[10px] text-amber-600">
. .
</p>
)}
{config.entityTable && entityColumns.length > 0 && (
<div className="border-t pt-3">
<p className="text-muted-foreground text-[10px]">
({config.entityTable}) ,
.
</p>
</div>
)}
2025-12-19 15:44:38 +09:00
</div>
)}
<Separator />
{/* 추가 옵션 */}
<div className="space-y-3">
<Label className="text-xs font-medium"> </Label>
2025-12-19 15:44:38 +09:00
<div className="flex items-center space-x-2">
<Checkbox
id="multiple"
checked={config.multiple || false}
onCheckedChange={(checked) => updateConfig("multiple", checked)}
/>
<label htmlFor="multiple" className="text-xs">
</label>
2025-12-19 15:44:38 +09:00
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="searchable"
checked={config.searchable || false}
onCheckedChange={(checked) => updateConfig("searchable", checked)}
/>
<label htmlFor="searchable" className="text-xs">
</label>
2025-12-19 15:44:38 +09:00
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="allowClear"
checked={config.allowClear !== false}
onCheckedChange={(checked) => updateConfig("allowClear", checked)}
/>
<label htmlFor="allowClear" className="text-xs">
</label>
2025-12-19 15:44:38 +09:00
</div>
</div>
{/* 다중 선택 시 최대 개수 */}
{config.multiple && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Input
type="number"
value={config.maxSelect ?? ""}
onChange={(e) => updateConfig("maxSelect", e.target.value ? Number(e.target.value) : undefined)}
placeholder="제한 없음"
min="1"
className="h-8 text-xs"
/>
</div>
)}
{/* 데이터 필터 조건 - static 소스 외 모든 소스에서 사용 */}
{effectiveSource !== "static" && filterTargetTable && (
<>
<Separator />
<FilterConditionsSection
filters={(config.filters as V2SelectFilter[]) || []}
columns={filterColumns}
loadingColumns={loadingFilterColumns}
targetTable={filterTargetTable}
onFiltersChange={(filters) => updateConfig("filters", filters)}
/>
</>
)}
2025-12-19 15:44:38 +09:00
</div>
);
};
V2SelectConfigPanel.displayName = "V2SelectConfigPanel";
2025-12-19 15:44:38 +09:00
export default V2SelectConfigPanel;