[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,17 +769,25 @@ 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: "엔티티를 검색해서 추가해요" },
{ 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; const currentMode = config.renderMode;
// 모달 → 인라인 모드로 변경 시: isSourceDisplay 컬럼 제거 및 모달 설정 초기화
if (currentMode === "modal" && newMode === "inline") { if (currentMode === "modal" && newMode === "inline") {
const filteredColumns = config.columns.filter((col) => !col.isSourceDisplay); const filteredColumns = config.columns.filter((col) => !col.isSourceDisplay);
updateConfig({ updateConfig({
@ -801,30 +810,28 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
updateConfig({ renderMode: newMode }); 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"
)}
> >
<SelectTrigger className="h-7 text-xs"> <Icon className="h-5 w-5 mb-1.5 text-primary" />
<SelectValue placeholder="모드 선택" /> <span className="text-xs font-medium leading-tight">{card.title}</span>
</SelectTrigger> <span className="text-[10px] text-muted-foreground leading-tight mt-0.5">{card.description}</span>
<SelectContent> </button>
{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> </div>
</SelectItem>
))}
</SelectContent>
</Select>
</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> </p>
</div> </div>
)} )}
</div>
{/* 현재 화면 정보 (메인 테이블이 설정된 경우에만 표시) */} {/* 현재 화면 정보 (메인 테이블이 설정된 경우에만 표시) */}
{currentTableName && ( {currentTableName && (
<div className="border-b border-border/50 pb-3 mb-3"> <div className="rounded-md border bg-background p-3">
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">MAIN TABLE</h4> <p className="text-xs text-muted-foreground"> </p>
<div className="rounded border border-border bg-muted p-2"> <p className="mt-0.5 text-sm font-medium">{currentTableName}</p>
<p className="text-xs text-foreground font-medium">{currentTableName}</p> <p className="text-[10px] text-muted-foreground mt-0.5">
<p className="text-[10px] text-muted-foreground">
{currentTableColumns.length} / {entityColumns.length} {currentTableColumns.length} / {entityColumns.length}
</p> </p>
</div> </div>
</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,41 +1068,121 @@ 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 </div>
id="enableSourceDetail" <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>
{/* 고급 설정 - 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} checked={!!config.sourceDetailConfig}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
if (checked) { if (checked) {
@ -1111,14 +1199,11 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
}} }}
/> />
</div> </div>
<p className="text-[10px] text-muted-foreground">
.
</p>
{config.sourceDetailConfig && ( {config.sourceDetailConfig && (
<div className="space-y-2 rounded border border-violet-200 bg-violet-50 p-2"> <div className="ml-4 border-l-2 border-primary/20 pl-3 space-y-2">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> </Label> <p className="text-xs text-muted-foreground"> </p>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@ -1170,7 +1255,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> FK </Label> <p className="text-xs text-muted-foreground"> FK </p>
<Input <Input
value={config.sourceDetailConfig.foreignKey || ""} value={config.sourceDetailConfig.foreignKey || ""}
onChange={(e) => onChange={(e) =>
@ -1186,7 +1271,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> </Label> <p className="text-xs text-muted-foreground"> </p>
<Input <Input
value={config.sourceDetailConfig.parentKey || ""} value={config.sourceDetailConfig.parentKey || ""}
onChange={(e) => onChange={(e) =>
@ -1203,77 +1288,31 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
</div> </div>
</div> </div>
<p className="text-[10px] text-violet-600"> <p className="text-[11px] text-muted-foreground">
[{config.sourceDetailConfig.parentKey || "?"}] [{config.sourceDetailConfig.parentKey || "?"}]
{" "}{config.sourceDetailConfig.tableName || "?"}.{config.sourceDetailConfig.foreignKey || "?"} {" "}{config.sourceDetailConfig.tableName || "?"}.{config.sourceDetailConfig.foreignKey || "?"}
</p> </p>
</div> </div>
)} )}
</div> </div>
</CollapsibleContent>
{/* 기능 옵션 */} </Collapsible>
<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>
</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" />
<span className="text-sm font-medium">
{isModalMode ? "어떤 컬럼을 표시하고 입력받을까요?" : "어떤 컬럼을 입력받을까요?"}
</span>
</div>
<p className="text-[11px] text-muted-foreground">
{isModalMode {isModalMode
? "표시할 컬럼과 입력 컬럼을 선택하세요. 아이콘으로 표시/입력 구분" ? "소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용이에요"
: "입력받을 컬럼을 선택하세요" : "체크한 컬럼이 리피터에 입력 필드로 표시돼요"
} }
</p> </p>
@ -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]">