[agent-pipeline] pipe-20260311080625-8a1t round-1
This commit is contained in:
parent
615bd8e2bf
commit
ff54e48ede
|
|
@ -2,16 +2,18 @@
|
|||
|
||||
/**
|
||||
* V2Select 설정 패널
|
||||
* 통합 선택 컴포넌트의 세부 설정을 관리합니다.
|
||||
* 토스식 단계별 UX: 소스 카드 선택 -> 소스별 설정 -> 고급 설정(접힘)
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } 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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Trash2, Loader2, Filter } from "lucide-react";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { List, Code, Database, FolderTree, Settings, ChevronDown, Plus, Trash2, Loader2, Filter } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import type { V2SelectFilter } from "@/types/v2-components";
|
||||
|
||||
|
|
@ -52,6 +54,35 @@ const USER_FIELD_OPTIONS = [
|
|||
{ value: "userName", label: "사용자명" },
|
||||
] as const;
|
||||
|
||||
// ─── 데이터 소스 카드 정의 ───
|
||||
const SOURCE_CARDS = [
|
||||
{
|
||||
value: "static",
|
||||
icon: List,
|
||||
title: "직접 입력",
|
||||
description: "옵션을 직접 추가해요",
|
||||
},
|
||||
{
|
||||
value: "code",
|
||||
icon: Code,
|
||||
title: "공통 코드",
|
||||
description: "등록된 공통 코드를 사용해요",
|
||||
},
|
||||
{
|
||||
value: "entity",
|
||||
icon: Database,
|
||||
title: "테이블 데이터",
|
||||
description: "다른 테이블에서 가져와요",
|
||||
entityOnly: true,
|
||||
},
|
||||
{
|
||||
value: "category",
|
||||
icon: FolderTree,
|
||||
title: "카테고리",
|
||||
description: "카테고리로 분류해요",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 필터 조건 설정 서브 컴포넌트
|
||||
*/
|
||||
|
|
@ -108,7 +139,7 @@ const FilterConditionsSection: React.FC<{
|
|||
<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" />
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">DATA FILTER</span>
|
||||
<span className="text-xs font-medium">데이터 필터</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -275,6 +306,8 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
|||
const [filterColumns, setFilterColumns] = useState<ColumnOption[]>([]);
|
||||
const [loadingFilterColumns, setLoadingFilterColumns] = useState(false);
|
||||
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
|
||||
const updateConfig = (field: string, value: any) => {
|
||||
onChange({ ...config, [field]: value });
|
||||
};
|
||||
|
|
@ -422,358 +455,299 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
|||
|
||||
const effectiveSource = isCategoryType ? "category" : config.source || "static";
|
||||
|
||||
// 표시할 소스 카드 결정
|
||||
const visibleCards = useMemo(() => {
|
||||
if (isCategoryType) {
|
||||
return SOURCE_CARDS.filter((c) => c.value === "category");
|
||||
}
|
||||
return SOURCE_CARDS.filter((c) => {
|
||||
if (c.entityOnly && !isEntityType) return false;
|
||||
return true;
|
||||
});
|
||||
}, [isCategoryType, isEntityType]);
|
||||
|
||||
// 카드 그리드 열 수: 3개면 한 줄, 4개면 2x2
|
||||
const gridCols = visibleCards.length <= 3 ? "grid-cols-3" : "grid-cols-2";
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{/* SELECT MODE 섹션 */}
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">SELECT MODE</h4>
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">선택 모드</span>
|
||||
<div className="w-[140px]">
|
||||
<Select value={config.mode || "dropdown"} onValueChange={(value) => updateConfig("mode", value)}>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="모드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dropdown">드롭다운</SelectItem>
|
||||
<SelectItem value="combobox">콤보박스 (검색)</SelectItem>
|
||||
<SelectItem value="radio">라디오 버튼</SelectItem>
|
||||
<SelectItem value="check">체크박스</SelectItem>
|
||||
<SelectItem value="tag">태그 선택</SelectItem>
|
||||
<SelectItem value="tagbox">태그박스 (태그+드롭다운)</SelectItem>
|
||||
<SelectItem value="toggle">토글 스위치</SelectItem>
|
||||
<SelectItem value="swap">스왑 선택</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{/* ─── 1단계: 데이터 소스 선택 ─── */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">이 필드는 어떤 데이터를 선택하나요?</p>
|
||||
<div className={cn("grid gap-2", gridCols)}>
|
||||
{visibleCards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
const isSelected = effectiveSource === card.value;
|
||||
return (
|
||||
<button
|
||||
key={card.value}
|
||||
type="button"
|
||||
onClick={() => updateConfig("source", card.value)}
|
||||
className={cn(
|
||||
"flex flex-col items-start gap-1 rounded-lg border p-3 text-left transition-all w-full",
|
||||
isSelected
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary/20"
|
||||
: "border-border hover:border-primary/50 hover:bg-muted/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">{card.title}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-muted-foreground">{card.description}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DATA SOURCE 섹션 */}
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">DATA SOURCE</h4>
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">데이터 소스</span>
|
||||
<div className="w-[140px]">
|
||||
{isCategoryType ? (
|
||||
<div className="bg-muted flex h-7 items-center rounded-md px-2">
|
||||
<span className="text-[11px] font-medium text-emerald-600">카테고리 (자동)</span>
|
||||
{/* ─── 2단계: 소스별 설정 ─── */}
|
||||
|
||||
{/* 직접 입력 (static) */}
|
||||
{effectiveSource === "static" && (
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">옵션 목록</span>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addOption} className="h-7 px-2 text-xs">
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{options.length > 0 ? (
|
||||
<div className="max-h-40 space-y-1.5 overflow-y-auto">
|
||||
{options.map((option: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Input
|
||||
value={option.value || ""}
|
||||
onChange={(e) => updateOptionValue(index, e.target.value)}
|
||||
placeholder={`옵션 ${index + 1}`}
|
||||
className="h-8 flex-1 text-sm"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeOption(index)}
|
||||
className="text-destructive h-8 w-8 shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<List className="mx-auto mb-2 h-8 w-8 opacity-30" />
|
||||
<p className="text-sm">아직 옵션이 없어요</p>
|
||||
<p className="text-xs">위의 추가 버튼으로 옵션을 만들어보세요</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{options.length > 0 && (
|
||||
<div className="border-t pt-3 mt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">기본 선택값</span>
|
||||
<Select
|
||||
value={config.defaultValue || "_none_"}
|
||||
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[160px] text-sm">
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
<Select value={config.source || "static"} onValueChange={(value) => updateConfig("source", value)}>
|
||||
<SelectTrigger className="h-7 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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CATEGORY 섹션 */}
|
||||
{/* 공통 코드 (code) */}
|
||||
{effectiveSource === "code" && (
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">공통 코드</span>
|
||||
</div>
|
||||
{config.codeGroup ? (
|
||||
<div className="rounded-md border bg-background p-3">
|
||||
<p className="text-xs text-muted-foreground">코드 그룹</p>
|
||||
<p className="mt-0.5 text-sm font-medium">{config.codeGroup}</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">
|
||||
테이블 컬럼에 설정된 코드 그룹이 자동으로 적용돼요
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border-2 border-dashed p-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">코드 그룹이 설정되지 않았어요</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">테이블 타입 관리에서 컬럼의 코드 그룹을 설정해주세요</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 테이블 데이터 (entity) */}
|
||||
{effectiveSource === "entity" && (
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">테이블 데이터</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-background p-3">
|
||||
<p className="text-xs text-muted-foreground">참조 테이블</p>
|
||||
<p className="mt-0.5 text-sm font-medium">{config.entityTable || "미설정"}</p>
|
||||
</div>
|
||||
|
||||
{loadingColumns && (
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
컬럼 목록 로딩 중...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entityColumns.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs text-muted-foreground">실제 저장되는 값</p>
|
||||
<Select
|
||||
value={config.entityValueColumn || ""}
|
||||
onValueChange={(v) => updateConfig("entityValueColumn", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{entityColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs text-muted-foreground">사용자에게 보여지는 텍스트</p>
|
||||
<Select
|
||||
value={config.entityLabelColumn || ""}
|
||||
onValueChange={(v) => updateConfig("entityLabelColumn", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{entityColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
엔티티 선택 시 같은 폼의 관련 필드가 자동으로 채워져요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingColumns && entityColumns.length === 0 && !config.entityTable && (
|
||||
<div className="rounded-md border-2 border-dashed p-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">참조 테이블이 설정되지 않았어요</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">테이블 타입 관리에서 참조 테이블을 설정해주세요</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.entityTable && !loadingColumns && entityColumns.length === 0 && (
|
||||
<p className="text-[10px] text-amber-600">
|
||||
테이블 컬럼을 조회할 수 없습니다. 테이블 타입 관리에서 참조 테이블을 설정해주세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 카테고리 (category) */}
|
||||
{effectiveSource === "category" && (
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">CATEGORY</h4>
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderTree className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">카테고리</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted rounded-md p-2 mb-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="rounded-md border bg-background p-3">
|
||||
<div className="flex gap-6">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-[10px]">테이블</p>
|
||||
<p className="text-xs font-medium">{config.categoryTable || tableName || "-"}</p>
|
||||
<p className="text-xs text-muted-foreground">테이블</p>
|
||||
<p className="text-sm 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>
|
||||
<p className="text-xs text-muted-foreground">컬럼</p>
|
||||
<p className="text-sm font-medium">{config.categoryColumn || columnName || "-"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadingCategoryValues && (
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs py-1">
|
||||
<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="py-1">
|
||||
<span className="text-[10px] text-muted-foreground">값 목록 ({categoryValues.length}개)</span>
|
||||
<div className="bg-muted max-h-32 space-y-0.5 overflow-y-auto rounded-md p-1.5 mt-1">
|
||||
{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>
|
||||
<p className="mb-1.5 text-xs text-muted-foreground">
|
||||
{categoryValues.length}개의 값이 있어요
|
||||
</p>
|
||||
<div className="max-h-28 overflow-y-auto rounded-md border bg-background p-2 space-y-0.5">
|
||||
{categoryValues.map((cv) => (
|
||||
<div key={cv.valueCode} className="flex items-center gap-2 px-1.5 py-0.5 text-xs">
|
||||
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">{cv.valueCode}</span>
|
||||
<span className="truncate">{cv.valueLabel}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">기본값</span>
|
||||
<div className="w-[140px]">
|
||||
<Select
|
||||
value={config.defaultValue || "_none_"}
|
||||
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 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>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">기본 선택값</span>
|
||||
<Select
|
||||
value={config.defaultValue || "_none_"}
|
||||
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[160px] text-sm">
|
||||
<SelectValue placeholder="선택 안함" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_none_">선택 안함</SelectItem>
|
||||
{categoryValues.map((cv) => (
|
||||
<SelectItem key={cv.valueCode} value={cv.valueCode}>
|
||||
{cv.valueLabel}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[10px] mt-0.5">화면 로드 시 자동 선택될 카테고리 값</p>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingCategoryValues && categoryValues.length === 0 && (
|
||||
<p className="text-[10px] text-amber-600 py-1">
|
||||
<p className="text-[10px] text-amber-600">
|
||||
카테고리 값이 없습니다. 테이블 카테고리 관리에서 값을 추가해주세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* STATIC OPTIONS 섹션 */}
|
||||
{effectiveSource === "static" && (
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">STATIC OPTIONS</h4>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={addOption} className="h-6 px-2 text-xs">
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-40 space-y-1.5 overflow-y-auto">
|
||||
{options.map((option: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={option.value || ""}
|
||||
onChange={(e) => updateOptionValue(index, e.target.value)}
|
||||
placeholder={`옵션 ${index + 1}`}
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeOption(index)}
|
||||
className="text-destructive h-7 w-7 shrink-0 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{options.length === 0 && (
|
||||
<p className="text-muted-foreground py-2 text-center text-xs">옵션을 추가해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{options.length > 0 && (
|
||||
<>
|
||||
<div className="flex items-center justify-between py-1.5 mt-2">
|
||||
<span className="text-xs text-muted-foreground">기본값</span>
|
||||
<div className="w-[140px]">
|
||||
<Select
|
||||
value={config.defaultValue || "_none_"}
|
||||
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 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>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[10px] mt-0.5">화면 로드 시 자동 선택될 값</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CODE GROUP 섹션 */}
|
||||
{effectiveSource === "code" && (
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">CODE GROUP</h4>
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">코드 그룹</span>
|
||||
<div className="w-[140px]">
|
||||
{config.codeGroup ? (
|
||||
<span className="text-xs font-medium">{config.codeGroup}</span>
|
||||
) : (
|
||||
<span className="text-[10px] text-amber-600">미설정</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ENTITY 섹션 */}
|
||||
{effectiveSource === "entity" && (
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">ENTITY</h4>
|
||||
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">참조 테이블</span>
|
||||
<div className="w-[140px]">
|
||||
<Input
|
||||
value={config.entityTable || ""}
|
||||
readOnly
|
||||
disabled
|
||||
placeholder="미설정"
|
||||
className="bg-muted h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadingColumns && (
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs py-1">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
컬럼 목록 로딩 중...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 mt-1">
|
||||
<div className="flex-1">
|
||||
<Label className="text-[10px] text-muted-foreground">값 컬럼</Label>
|
||||
{entityColumns.length > 0 ? (
|
||||
<Select
|
||||
value={config.entityValueColumn || ""}
|
||||
onValueChange={(value) => updateConfig("entityValueColumn", value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 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-7 text-xs"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Label className="text-[10px] text-muted-foreground">표시 컬럼</Label>
|
||||
{entityColumns.length > 0 ? (
|
||||
<Select
|
||||
value={config.entityLabelColumn || ""}
|
||||
onValueChange={(value) => updateConfig("entityLabelColumn", value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 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-7 text-xs"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.entityTable && !loadingColumns && entityColumns.length === 0 && (
|
||||
<p className="text-[10px] text-amber-600 mt-1">
|
||||
테이블 컬럼을 조회할 수 없습니다. 테이블 타입 관리에서 참조 테이블을 설정해주세요.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{config.entityTable && entityColumns.length > 0 && (
|
||||
<p className="text-muted-foreground text-[10px] mt-2">
|
||||
같은 폼에 참조 테이블({config.entityTable})의 컬럼이 배치되어 있으면, 엔티티 선택 시 해당 필드가 자동으로
|
||||
채워집니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OPTIONS 섹션 */}
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">OPTIONS</h4>
|
||||
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">다중 선택 허용</span>
|
||||
<Checkbox
|
||||
checked={config.multiple || false}
|
||||
onCheckedChange={(checked) => updateConfig("multiple", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">검색 기능</span>
|
||||
<Checkbox
|
||||
checked={config.searchable || false}
|
||||
onCheckedChange={(checked) => updateConfig("searchable", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">값 초기화 허용</span>
|
||||
<Checkbox
|
||||
checked={config.allowClear !== false}
|
||||
onCheckedChange={(checked) => updateConfig("allowClear", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.multiple && (
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-xs text-muted-foreground">최대 선택 개수</span>
|
||||
<div className="w-[140px]">
|
||||
<Input
|
||||
type="number"
|
||||
value={config.maxSelect ?? ""}
|
||||
onChange={(e) => updateConfig("maxSelect", e.target.value ? Number(e.target.value) : undefined)}
|
||||
placeholder="제한 없음"
|
||||
min="1"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* DATA FILTER 섹션 */}
|
||||
{/* 데이터 필터 (static 제외, filterTargetTable 있을 때만) */}
|
||||
{effectiveSource !== "static" && filterTargetTable && (
|
||||
<div className="border-b border-border/50 pb-3 mb-3">
|
||||
<div className="rounded-lg border bg-muted/30 p-4">
|
||||
<FilterConditionsSection
|
||||
filters={(config.filters as V2SelectFilter[]) || []}
|
||||
columns={filterColumns}
|
||||
|
|
@ -783,6 +757,104 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ─── 3단계: 고급 설정 (기본 접혀있음) ─── */}
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">고급 설정</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
||||
advancedOpen && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
{/* 선택 모드 */}
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs text-muted-foreground">선택 방식</p>
|
||||
<Select value={config.mode || "dropdown"} onValueChange={(v) => updateConfig("mode", v)}>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dropdown">드롭다운</SelectItem>
|
||||
<SelectItem value="combobox">검색 가능 드롭다운</SelectItem>
|
||||
<SelectItem value="radio">라디오 버튼</SelectItem>
|
||||
<SelectItem value="check">체크박스</SelectItem>
|
||||
<Separator className="my-1" />
|
||||
<SelectItem value="tag">태그 선택</SelectItem>
|
||||
<SelectItem value="tagbox">태그박스</SelectItem>
|
||||
<SelectItem value="toggle">토글</SelectItem>
|
||||
<SelectItem value="swap">스왑</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">대부분의 경우 드롭다운이 적합해요</p>
|
||||
</div>
|
||||
|
||||
{/* 토글 옵션들 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">여러 개 선택</p>
|
||||
<p className="text-[11px] text-muted-foreground">한 번에 여러 값을 선택할 수 있어요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.multiple || false}
|
||||
onCheckedChange={(checked) => updateConfig("multiple", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.multiple && (
|
||||
<div className="ml-4 border-l-2 border-primary/20 pl-3">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-muted-foreground">최대 선택 개수</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.maxSelect ?? ""}
|
||||
onChange={(e) => updateConfig("maxSelect", e.target.value ? Number(e.target.value) : undefined)}
|
||||
placeholder="제한 없음"
|
||||
min={1}
|
||||
className="h-7 w-[100px] text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">검색 기능</p>
|
||||
<p className="text-[11px] text-muted-foreground">옵션이 많을 때 검색으로 찾을 수 있어요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.searchable || false}
|
||||
onCheckedChange={(checked) => updateConfig("searchable", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">선택 초기화</p>
|
||||
<p className="text-[11px] text-muted-foreground">선택한 값을 지울 수 있는 X 버튼이 표시돼요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.allowClear !== false}
|
||||
onCheckedChange={(checked) => updateConfig("allowClear", checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue