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