2025-12-19 15:44:38 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* UnifiedSelect 설정 패널
|
|
|
|
|
* 통합 선택 컴포넌트의 세부 설정을 관리합니다.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
|
|
|
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 } from "lucide-react";
|
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
|
|
|
|
|
|
interface ColumnOption {
|
|
|
|
|
columnName: string;
|
|
|
|
|
columnLabel: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface UnifiedSelectConfigPanelProps {
|
|
|
|
|
config: Record<string, any>;
|
|
|
|
|
onChange: (config: Record<string, any>) => void;
|
2025-12-23 10:49:28 +09:00
|
|
|
/** 컬럼의 inputType (entity 타입인 경우에만 엔티티 소스 표시) */
|
|
|
|
|
inputType?: string;
|
2025-12-19 15:44:38 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const UnifiedSelectConfigPanel: React.FC<UnifiedSelectConfigPanelProps> = ({
|
|
|
|
|
config,
|
|
|
|
|
onChange,
|
2025-12-23 10:49:28 +09:00
|
|
|
inputType,
|
2025-12-19 15:44:38 +09:00
|
|
|
}) => {
|
2025-12-23 10:49:28 +09:00
|
|
|
// 엔티티 타입인지 확인
|
|
|
|
|
const isEntityType = inputType === "entity";
|
2025-12-19 15:44:38 +09:00
|
|
|
// 엔티티 테이블의 컬럼 목록
|
|
|
|
|
const [entityColumns, setEntityColumns] = useState<ColumnOption[]>([]);
|
|
|
|
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 설정 업데이트 핸들러
|
|
|
|
|
const updateConfig = (field: string, value: any) => {
|
|
|
|
|
onChange({ ...config, [field]: value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 엔티티 테이블 변경 시 컬럼 목록 조회
|
|
|
|
|
const loadEntityColumns = useCallback(async (tableName: string) => {
|
|
|
|
|
if (!tableName) {
|
|
|
|
|
setEntityColumns([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLoadingColumns(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=500`);
|
|
|
|
|
const data = response.data.data || response.data;
|
|
|
|
|
const columns = data.columns || data || [];
|
|
|
|
|
|
|
|
|
|
const columnOptions: ColumnOption[] = columns.map((col: any) => {
|
|
|
|
|
const name = col.columnName || col.column_name || col.name;
|
|
|
|
|
// displayName 우선 사용
|
|
|
|
|
const label = col.displayName || col.display_name || col.columnLabel || col.column_label || name;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
columnName: name,
|
|
|
|
|
columnLabel: label,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 || [];
|
|
|
|
|
|
|
|
|
|
const addOption = () => {
|
|
|
|
|
const newOptions = [...options, { value: "", label: "" }];
|
|
|
|
|
updateConfig("options", newOptions);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateOption = (index: number, field: "value" | "label", value: string) => {
|
|
|
|
|
const newOptions = [...options];
|
|
|
|
|
newOptions[index] = { ...newOptions[index], [field]: value };
|
|
|
|
|
updateConfig("options", newOptions);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeOption = (index: number) => {
|
|
|
|
|
const newOptions = options.filter((_: any, i: number) => i !== index);
|
|
|
|
|
updateConfig("options", newOptions);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
<SelectValue placeholder="모드 선택" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="dropdown">드롭다운</SelectItem>
|
|
|
|
|
<SelectItem value="radio">라디오 버튼</SelectItem>
|
|
|
|
|
<SelectItem value="check">체크박스</SelectItem>
|
|
|
|
|
<SelectItem value="tag">태그 선택</SelectItem>
|
|
|
|
|
<SelectItem value="toggle">토글 스위치</SelectItem>
|
|
|
|
|
<SelectItem value="swap">스왑 선택</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* 데이터 소스 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs font-medium">데이터 소스</Label>
|
|
|
|
|
<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>
|
2025-12-23 10:49:28 +09:00
|
|
|
{/* 엔티티 타입일 때만 엔티티 옵션 표시 */}
|
|
|
|
|
{isEntityType && <SelectItem value="entity">엔티티</SelectItem>}
|
2025-12-19 15:44:38 +09:00
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 정적 옵션 관리 */}
|
|
|
|
|
{config.source === "static" && (
|
|
|
|
|
<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="h-3 w-3 mr-1" />
|
|
|
|
|
추가
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2 max-h-40 overflow-y-auto">
|
|
|
|
|
{options.map((option: any, index: number) => (
|
|
|
|
|
<div key={index} className="flex items-center gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
value={option.value || ""}
|
|
|
|
|
onChange={(e) => updateOption(index, "value", e.target.value)}
|
|
|
|
|
placeholder="값"
|
|
|
|
|
className="h-7 text-xs flex-1"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={option.label || ""}
|
|
|
|
|
onChange={(e) => updateOption(index, "label", e.target.value)}
|
|
|
|
|
placeholder="표시 텍스트"
|
|
|
|
|
className="h-7 text-xs flex-1"
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => removeOption(index)}
|
|
|
|
|
className="h-7 w-7 p-0 text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
{options.length === 0 && (
|
|
|
|
|
<p className="text-xs text-muted-foreground text-center py-2">
|
|
|
|
|
옵션을 추가해주세요
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-23 10:49:28 +09:00
|
|
|
{/* 공통 코드 설정 - 테이블 타입 관리에서 설정되므로 정보만 표시 */}
|
2025-12-19 15:44:38 +09:00
|
|
|
{config.source === "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-sm font-medium text-foreground">{config.codeGroup}</p>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-xs text-amber-600">
|
|
|
|
|
테이블 타입 관리에서 코드 그룹을 설정해주세요
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2025-12-19 15:44:38 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 엔티티(참조 테이블) 설정 */}
|
|
|
|
|
{config.source === "entity" && (
|
|
|
|
|
<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="h-8 text-xs bg-muted"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-[10px] text-muted-foreground">
|
|
|
|
|
조인할 테이블명 (테이블 타입 관리에서 설정된 경우 자동 입력됨)
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 컬럼 로딩 중 표시 */}
|
|
|
|
|
{loadingColumns && (
|
|
|
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
|
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
|
|
|
컬럼 목록 로딩 중...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 컬럼 선택 - 테이블이 설정되어 있고 컬럼 목록이 있는 경우 Select로 표시 */}
|
|
|
|
|
<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-[10px] text-muted-foreground">저장될 값</p>
|
|
|
|
|
</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-[10px] text-muted-foreground">화면에 표시될 값</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 컬럼이 없는 경우 안내 */}
|
|
|
|
|
{config.entityTable && !loadingColumns && entityColumns.length === 0 && (
|
|
|
|
|
<p className="text-[10px] text-amber-600">
|
|
|
|
|
테이블 컬럼을 조회할 수 없습니다. 테이블 타입 관리에서 참조 테이블을 설정해주세요.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* 추가 옵션 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<Label className="text-xs font-medium">추가 옵션</Label>
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
</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>
|
|
|
|
|
</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>
|
|
|
|
|
</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>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UnifiedSelectConfigPanel.displayName = "UnifiedSelectConfigPanel";
|
|
|
|
|
|
|
|
|
|
export default UnifiedSelectConfigPanel;
|