[agent-pipeline] pipe-20260311130333-zqic round-1

This commit is contained in:
DDD1542 2026-03-11 22:13:58 +09:00
parent 1d9ed6b36b
commit ae852ed4ad
1 changed files with 350 additions and 294 deletions

View File

@ -11,11 +11,12 @@
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
// Separator 제거 - 팔란티어 스타일 섹션 헤더 사용
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Database,
Link2,
@ -31,7 +32,10 @@ import {
Wand2,
Check,
ChevronsUpDown,
ListTree,
Settings,
Rows3,
Columns3,
MousePointerClick,
} from "lucide-react";
import {
Command,
@ -55,10 +59,7 @@ import { cn } from "@/lib/utils";
import {
V2RepeaterConfig,
RepeaterColumnConfig,
RepeaterEntityJoin,
DEFAULT_REPEATER_CONFIG,
RENDER_MODE_OPTIONS,
MODAL_SIZE_OPTIONS,
} from "@/types/v2-repeater";
// 테이블 엔티티 관계 정보
@ -759,7 +760,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
}, [currentTableColumns, config.dataSource?.foreignKey]);
return (
<div className="space-y-1">
<div className="space-y-4">
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="basic" className="text-xs"></TabsTrigger>
@ -768,63 +769,69 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
</TabsList>
{/* 기본 설정 탭 */}
<TabsContent value="basic" className="mt-4 space-y-1">
{/* 렌더링 모드 */}
<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">RENDER MODE</h4>
<Select
value={config.renderMode}
onValueChange={(value) => {
const newMode = value as any;
const currentMode = config.renderMode;
// 모달 → 인라인 모드로 변경 시: isSourceDisplay 컬럼 제거 및 모달 설정 초기화
if (currentMode === "modal" && newMode === "inline") {
const filteredColumns = config.columns.filter((col) => !col.isSourceDisplay);
updateConfig({
renderMode: newMode,
columns: filteredColumns,
dataSource: {
...config.dataSource,
sourceTable: undefined,
foreignKey: undefined,
referenceKey: undefined,
displayColumn: undefined,
},
modal: {
...config.modal,
searchFields: [],
sourceDisplayColumns: [],
},
});
} else {
updateConfig({ renderMode: newMode });
}
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="모드 선택" />
</SelectTrigger>
<SelectContent>
{RENDER_MODE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<div className="flex flex-col">
<span>{opt.label}</span>
<span className="text-[10px] text-muted-foreground/70">
{opt.value === "inline" && "현재 테이블 컬럼 직접 입력"}
{opt.value === "modal" && "엔티티 선택 후 추가 정보 입력"}
{opt.value === "button" && "버튼으로 관련 화면 열기"}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<TabsContent value="basic" className="mt-4 space-y-4">
{/* 렌더링 모드 - 카드 선택 */}
<div className="space-y-2">
<p className="text-sm font-medium"> ?</p>
<div className="grid grid-cols-3 gap-2">
{[
{ value: "inline", icon: Rows3, title: "직접 입력", description: "테이블 컬럼에 바로 입력해요" },
{ value: "modal", icon: Columns3, title: "모달 선택", description: "엔티티를 검색해서 추가해요" },
{ value: "button", icon: MousePointerClick, title: "버튼 연결", description: "버튼으로 관련 화면을 열어요" },
].map((card) => {
const Icon = card.icon;
const isSelected = config.renderMode === card.value;
return (
<button
key={card.value}
type="button"
onClick={() => {
const newMode = card.value as any;
const currentMode = config.renderMode;
if (currentMode === "modal" && newMode === "inline") {
const filteredColumns = config.columns.filter((col) => !col.isSourceDisplay);
updateConfig({
renderMode: newMode,
columns: filteredColumns,
dataSource: {
...config.dataSource,
sourceTable: undefined,
foreignKey: undefined,
referenceKey: undefined,
displayColumn: undefined,
},
modal: {
...config.modal,
searchFields: [],
sourceDisplayColumns: [],
},
});
} else {
updateConfig({ renderMode: newMode });
}
}}
className={cn(
"flex flex-col items-center justify-center rounded-lg border p-3 text-center transition-all min-h-[80px]",
isSelected
? "border-primary bg-primary/5 ring-1 ring-primary/20"
: "border-border hover:border-primary/50 hover:bg-muted/50"
)}
>
<Icon className="h-5 w-5 mb-1.5 text-primary" />
<span className="text-xs font-medium leading-tight">{card.title}</span>
<span className="text-[10px] text-muted-foreground leading-tight mt-0.5">{card.description}</span>
</button>
);
})}
</div>
</div>
{/* 저장 대상 테이블 */}
<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">SAVE TABLE</h4>
<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>
{/* 현재 선택된 테이블 표시 (기존 테이블 UI와 동일한 스타일) */}
<div className={cn(
@ -1009,32 +1016,33 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
{config.useCustomTable && config.mainTableName && !currentTableName && (
<div className="rounded border border-primary/20 bg-primary/10 p-2">
<p className="text-[10px] text-primary">
모드: 화면 .
모드: 화면
</p>
</div>
)}
{/* 현재 화면 정보 (메인 테이블이 설정된 경우에만 표시) */}
{currentTableName && (
<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">{currentTableName}</p>
<p className="text-[10px] text-muted-foreground mt-0.5">
{currentTableColumns.length} / {entityColumns.length}
</p>
</div>
)}
</div>
{/* 현재 화면 정보 (메인 테이블이 설정된 경우에만 표시) */}
{currentTableName && (
<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">MAIN TABLE</h4>
<div className="rounded border border-border bg-muted p-2">
<p className="text-xs text-foreground font-medium">{currentTableName}</p>
<p className="text-[10px] text-muted-foreground">
{currentTableColumns.length} / {entityColumns.length}
</p>
</div>
</div>
)}
{/* 모달 모드: 엔티티 컬럼 선택 */}
{isModalMode && (
<>
<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 SELECT</h4>
<p className="text-[10px] text-muted-foreground">
(FK만 )
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
<div className="flex items-center gap-2">
<Link2 className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"> ?</span>
</div>
<p className="text-[11px] text-muted-foreground">
FK만
</p>
{entityColumns.length > 0 ? (
@ -1060,222 +1068,253 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
</SelectContent>
</Select>
) : (
<div className="rounded border border-border bg-muted p-2">
<p className="text-[10px] text-muted-foreground">
<div className="rounded-md border-2 border-dashed p-4 text-center">
<p className="text-sm text-muted-foreground">
{loadingColumns
? "로딩 중..."
? "컬럼 정보를 불러오고 있어요..."
: !targetTableForColumns
? "저장 테이블을 먼저 선택세요"
: "엔티티 타입 컬럼이 없습니다"}
? "저장 테이블을 먼저 선택해주세요"
: "엔티티 타입 컬럼이 없어요"}
</p>
</div>
)}
{/* 선택된 엔티티 정보 */}
{config.dataSource?.sourceTable && (
<div className="rounded border border-emerald-200 bg-emerald-50 p-2 space-y-1">
<p className="text-xs text-emerald-700 font-medium"> </p>
<div className="text-[10px] text-emerald-600">
<p> : {config.dataSource.sourceTable}</p>
<p> : {config.dataSource.foreignKey} (FK)</p>
</div>
<div className="rounded-md border bg-background p-3 space-y-1">
<p className="text-xs text-muted-foreground"> </p>
<p className="text-sm font-medium">{config.dataSource.sourceTable}</p>
<p className="text-[11px] text-muted-foreground">
{config.dataSource.foreignKey} FK로
</p>
</div>
)}
</div>
</>
)}
{/* 소스 디테일 자동 조회 설정 */}
<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">SOURCE DETAIL</h4>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground flex items-center gap-1">
<ListTree className="h-3 w-3" />
</span>
<Checkbox
id="enableSourceDetail"
checked={!!config.sourceDetailConfig}
onCheckedChange={(checked) => {
if (checked) {
updateConfig({
sourceDetailConfig: {
tableName: "",
foreignKey: "",
parentKey: "",
},
});
} else {
updateConfig({ sourceDetailConfig: undefined });
}
}}
/>
</div>
<p className="text-[10px] text-muted-foreground">
.
</p>
{config.sourceDetailConfig && (
<div className="space-y-2 rounded border border-violet-200 bg-violet-50 p-2">
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs"
>
{config.sourceDetailConfig.tableName
? (allTables.find(t => t.tableName === config.sourceDetailConfig!.tableName)?.displayName || config.sourceDetailConfig.tableName)
: "테이블 선택..."
}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-48">
<CommandEmpty className="text-xs py-3 text-center"> .</CommandEmpty>
<CommandGroup>
{allTables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.tableName} ${table.displayName}`}
onSelect={() => {
updateConfig({
sourceDetailConfig: {
...config.sourceDetailConfig!,
tableName: table.tableName,
},
});
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", config.sourceDetailConfig!.tableName === table.tableName ? "opacity-100" : "opacity-0")} />
<span>{table.displayName}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* 기능 옵션 - 토스식 Switch + 설명 */}
<div className="rounded-lg border bg-muted/30 p-4 space-y-1">
<span className="text-sm font-medium"> </span>
<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>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[10px]"> FK </Label>
<Input
value={config.sourceDetailConfig.foreignKey || ""}
onChange={(e) =>
updateConfig({
sourceDetailConfig: {
...config.sourceDetailConfig!,
foreignKey: e.target.value,
},
})
}
placeholder="예: order_no"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Input
value={config.sourceDetailConfig.parentKey || ""}
onChange={(e) =>
updateConfig({
sourceDetailConfig: {
...config.sourceDetailConfig!,
parentKey: e.target.value,
},
})
}
placeholder="예: order_no"
className="h-7 text-xs"
/>
</div>
</div>
<p className="text-[10px] text-violet-600">
[{config.sourceDetailConfig.parentKey || "?"}]
{" "}{config.sourceDetailConfig.tableName || "?"}.{config.sourceDetailConfig.foreignKey || "?"}
</p>
<Switch
checked={config.features?.showAddButton ?? true}
onCheckedChange={(checked) => updateFeatures("showAddButton", checked)}
/>
</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.features?.showDeleteButton ?? true}
onCheckedChange={(checked) => updateFeatures("showDeleteButton", checked)}
/>
</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.features?.inlineEdit ?? false}
onCheckedChange={(checked) => updateFeatures("inlineEdit", checked)}
/>
</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.features?.multiSelect ?? true}
onCheckedChange={(checked) => updateFeatures("multiSelect", checked)}
/>
</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.features?.showRowNumber ?? false}
onCheckedChange={(checked) => updateFeatures("showRowNumber", checked)}
/>
</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.features?.selectable ?? false}
onCheckedChange={(checked) => updateFeatures("selectable", checked)}
/>
</div>
</div>
</div>
{/* 기능 옵션 */}
<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">FEATURES</h4>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<Checkbox
id="showAddButton"
checked={config.features?.showAddButton ?? true}
onCheckedChange={(checked) => updateFeatures("showAddButton", !!checked)}
/>
</div>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<Checkbox
id="showDeleteButton"
checked={config.features?.showDeleteButton ?? true}
onCheckedChange={(checked) => updateFeatures("showDeleteButton", !!checked)}
/>
</div>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<Checkbox
id="inlineEdit"
checked={config.features?.inlineEdit ?? false}
onCheckedChange={(checked) => updateFeatures("inlineEdit", !!checked)}
/>
</div>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<Checkbox
id="multiSelect"
checked={config.features?.multiSelect ?? true}
onCheckedChange={(checked) => updateFeatures("multiSelect", !!checked)}
/>
</div>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<Checkbox
id="showRowNumber"
checked={config.features?.showRowNumber ?? false}
onCheckedChange={(checked) => updateFeatures("showRowNumber", !!checked)}
/>
</div>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<Checkbox
id="selectable"
checked={config.features?.selectable ?? false}
onCheckedChange={(checked) => updateFeatures("selectable", !!checked)}
/>
</div>
</div>
{/* 고급 설정 - Collapsible (소스 디테일 등) */}
<Collapsible>
<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="h-4 w-4 text-muted-foreground transition-transform duration-200" />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
{/* 소스 디테일 자동 조회 */}
<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.sourceDetailConfig}
onCheckedChange={(checked) => {
if (checked) {
updateConfig({
sourceDetailConfig: {
tableName: "",
foreignKey: "",
parentKey: "",
},
});
} else {
updateConfig({ sourceDetailConfig: undefined });
}
}}
/>
</div>
{config.sourceDetailConfig && (
<div className="ml-4 border-l-2 border-primary/20 pl-3 space-y-2">
<div className="space-y-1">
<p className="text-xs text-muted-foreground"> </p>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs"
>
{config.sourceDetailConfig.tableName
? (allTables.find(t => t.tableName === config.sourceDetailConfig!.tableName)?.displayName || config.sourceDetailConfig.tableName)
: "테이블 선택..."
}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-48">
<CommandEmpty className="text-xs py-3 text-center"> .</CommandEmpty>
<CommandGroup>
{allTables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.tableName} ${table.displayName}`}
onSelect={() => {
updateConfig({
sourceDetailConfig: {
...config.sourceDetailConfig!,
tableName: table.tableName,
},
});
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", config.sourceDetailConfig!.tableName === table.tableName ? "opacity-100" : "opacity-0")} />
<span>{table.displayName}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<p className="text-xs text-muted-foreground"> FK </p>
<Input
value={config.sourceDetailConfig.foreignKey || ""}
onChange={(e) =>
updateConfig({
sourceDetailConfig: {
...config.sourceDetailConfig!,
foreignKey: e.target.value,
},
})
}
placeholder="예: order_no"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground"> </p>
<Input
value={config.sourceDetailConfig.parentKey || ""}
onChange={(e) =>
updateConfig({
sourceDetailConfig: {
...config.sourceDetailConfig!,
parentKey: e.target.value,
},
})
}
placeholder="예: order_no"
className="h-7 text-xs"
/>
</div>
</div>
<p className="text-[11px] text-muted-foreground">
[{config.sourceDetailConfig.parentKey || "?"}]
{" "}{config.sourceDetailConfig.tableName || "?"}.{config.sourceDetailConfig.foreignKey || "?"}
</p>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
</TabsContent>
{/* 컬럼 설정 탭 */}
<TabsContent value="columns" className="mt-4 space-y-1">
<TabsContent value="columns" className="mt-4 space-y-4">
{/* 통합 컬럼 선택 */}
<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">COLUMN SELECT</h4>
<p className="text-[10px] text-muted-foreground">
{isModalMode
? "표시할 컬럼과 입력 컬럼을 선택하세요. 아이콘으로 표시/입력 구분"
: "입력받을 컬럼을 선택하세요"
<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">
{isModalMode ? "어떤 컬럼을 표시하고 입력받을까요?" : "어떤 컬럼을 입력받을까요?"}
</span>
</div>
<p className="text-[11px] text-muted-foreground">
{isModalMode
? "소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용이에요"
: "체크한 컬럼이 리피터에 입력 필드로 표시돼요"
}
</p>
</p>
{/* 모달 모드: 소스 테이블 컬럼 (표시용) */}
{isModalMode && config.dataSource?.sourceTable && (
@ -1353,11 +1392,11 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
{/* 선택된 컬럼 상세 설정 */}
{config.columns.length > 0 && (
<>
<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">
SELECTED COLUMNS ({config.columns.length})
<span className="ml-2 font-normal normal-case tracking-normal"> </span>
</h4>
<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"> ({config.columns.length})</span>
<span className="text-[11px] text-muted-foreground"> </span>
</div>
<div className="max-h-48 space-y-1 overflow-y-auto">
{config.columns.map((col, index) => (
<div key={col.key} className="space-y-1">
@ -1684,11 +1723,14 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
{/* 계산 규칙 */}
{(isModalMode || isInlineMode) && config.columns.length > 0 && (
<>
<div className="border-b border-border/50 pb-3 mb-3">
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">CALCULATION RULES</h4>
<Button type="button" variant="outline" size="sm" onClick={addCalculationRule} className="h-6 text-xs">
<Calculator className="mr-1 h-3 w-3" />
<div className="flex items-center gap-2">
<Calculator className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"> </span>
</div>
<Button type="button" variant="outline" size="sm" onClick={addCalculationRule} className="h-7 px-2 text-xs">
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
@ -1785,9 +1827,11 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
))}
{calculationRules.length === 0 && (
<p className="text-muted-foreground py-1 text-center text-[10px]">
</p>
<div className="text-center py-4 text-muted-foreground">
<Calculator className="mx-auto mb-2 h-6 w-6 opacity-30" />
<p className="text-xs"> </p>
<p className="text-[10px] mt-0.5"> </p>
</div>
)}
</div>
</div>
@ -1796,22 +1840,34 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
</TabsContent>
{/* Entity 조인 설정 탭 */}
<TabsContent value="entityJoin" className="mt-4 space-y-1">
<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 JOIN</h4>
<p className="text-muted-foreground text-[10px]">
FK
<TabsContent value="entityJoin" className="mt-4 space-y-4">
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
<div className="flex items-center gap-2">
<Link2 className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"> </span>
</div>
<p className="text-[11px] text-muted-foreground">
FK
</p>
{loadingEntityJoins ? (
<p className="text-muted-foreground py-2 text-center text-xs"> ...</p>
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<div className="h-3 w-3 animate-spin rounded-full border-2 border-primary border-t-transparent" />
...
</div>
) : entityJoinData.joinTables.length === 0 ? (
<div className="rounded-md border border-dashed p-4 text-center">
<p className="text-muted-foreground text-xs">
<div className="rounded-md border-2 border-dashed p-4 text-center">
<Link2 className="mx-auto mb-2 h-8 w-8 opacity-30 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{entityJoinTargetTable
? `${entityJoinTargetTable} 테이블에 Entity 조인 가능한 컬럼이 없습니다`
? "조인 가능한 컬럼이 없어요"
: "저장 테이블을 먼저 설정해주세요"}
</p>
{entityJoinTargetTable && (
<p className="text-xs text-muted-foreground mt-0.5">
</p>
)}
</div>
) : (
<div className="space-y-3">
@ -1873,8 +1929,8 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
{/* 현재 설정된 Entity 조인 목록 */}
{config.entityJoins && config.entityJoins.length > 0 && (
<div className="space-y-1">
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">CONFIGURED JOINS</h4>
<div className="space-y-2 border-t pt-3">
<span className="text-xs font-medium"> ({config.entityJoins.length})</span>
<div className="space-y-1">
{config.entityJoins.map((join, idx) => (
<div key={idx} className="flex items-center gap-1 rounded border bg-muted/30 px-2 py-1 text-[10px]">