[agent-pipeline] pipe-20260311130333-zqic round-1
This commit is contained in:
parent
1d9ed6b36b
commit
ae852ed4ad
|
|
@ -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]">
|
||||
|
|
|
|||
Loading…
Reference in New Issue