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

This commit is contained in:
DDD1542 2026-03-11 22:20:59 +09:00
parent ae852ed4ad
commit d2c8f5f8f5
1 changed files with 254 additions and 264 deletions

View File

@ -3,13 +3,12 @@
/**
* BOM
*
* V2RepeaterConfigPanel :
* - : 저장 + + +
* UX:
* - : 저장 ()
* - : 소스 + +
*/
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
@ -17,7 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/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";
@ -102,27 +101,18 @@ interface BomColumnConfig {
}
interface BomItemEditorConfig {
// 저장 테이블 설정 (리피터 패턴)
useCustomTable?: boolean;
mainTableName?: string;
foreignKeyColumn?: string;
foreignKeySourceColumn?: string;
// 트리 구조 설정
parentKeyColumn?: string;
// 엔티티 (품목 참조) 설정
dataSource?: {
sourceTable?: string;
foreignKey?: string;
referenceKey?: string;
displayColumn?: string;
};
// 컬럼 설정
columns: BomColumnConfig[];
// 기능 옵션
features?: {
showAddButton?: boolean;
showDeleteButton?: boolean;
@ -472,7 +462,6 @@ export function V2BomItemEditorConfigPanel({
});
};
// FK 컬럼 제외한 입력 가능 컬럼
const inputableColumns = useMemo(() => {
const fkColumn = config.dataSource?.foreignKey;
return currentTableColumns.filter(
@ -495,9 +484,12 @@ export function V2BomItemEditorConfigPanel({
{/* ─── 기본 설정 탭 ─── */}
<TabsContent value="basic" className="mt-4 space-y-4">
{/* 저장 대상 테이블 (리피터 동일) */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
{/* 저장 대상 테이블 */}
<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">BOM ?</span>
</div>
<div
className={cn(
@ -539,7 +531,7 @@ export function V2BomItemEditorConfigPanel({
</div>
</div>
{/* 테이블 Combobox (리피터 동일) */}
{/* 테이블 Combobox */}
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
<PopoverTrigger asChild>
<Button
@ -651,19 +643,18 @@ export function V2BomItemEditorConfigPanel({
</PopoverContent>
</Popover>
{/* FK 직접 입력 (연관 없는 테이블 선택 시) */}
{/* FK 직접 입력 */}
{config.useCustomTable &&
config.mainTableName &&
currentTableName &&
!relatedTables.some((r) => r.tableName === config.mainTableName) && (
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-2">
<p className="text-[10px] text-amber-700">
({currentTableName}) . FK
.
({currentTableName}) . FK .
</p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[10px]">FK ( )</Label>
<p className="text-xs text-muted-foreground">FK ( )</p>
<Input
value={config.foreignKeyColumn || ""}
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
@ -672,7 +663,7 @@ export function V2BomItemEditorConfigPanel({
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]">PK ( )</Label>
<p className="text-xs text-muted-foreground">PK ( )</p>
<Input
value={config.foreignKeySourceColumn || "id"}
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
@ -683,72 +674,85 @@ export function V2BomItemEditorConfigPanel({
</div>
</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>
<Separator />
{/* 트리 구조 설정 (BOM 전용) */}
<div className="space-y-2">
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4" />
<Label className="text-xs font-medium"> </Label>
<GitBranch className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"> ?</span>
</div>
<p className="text-muted-foreground text-[10px]">
FK
<p className="text-[11px] text-muted-foreground">
FK -
</p>
{currentTableColumns.length > 0 ? (
<Select
value={config.parentKeyColumn || ""}
onValueChange={(value) => updateConfig({ parentKeyColumn: value })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="부모 키 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{currentTableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span>{col.displayName}</span>
{col.displayName !== col.columnName && (
<span className="text-muted-foreground text-[10px]">
({col.columnName})
</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<div>
<p className="mb-1.5 text-xs text-muted-foreground"> </p>
<Select
value={config.parentKeyColumn || ""}
onValueChange={(value) => updateConfig({ parentKeyColumn: value })}
>
<SelectTrigger className="h-8 text-sm">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{currentTableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span>{col.displayName}</span>
{col.displayName !== col.columnName && (
<span className="text-muted-foreground text-[10px]">
({col.columnName})
</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : (
<div className="rounded border border-border bg-muted p-2">
<p className="text-[10px] text-muted-foreground">
{loadingColumns ? "로딩 중..." : "저장 테이블을 먼저 선택하세요"}
<div className="rounded-md border-2 border-dashed p-4 text-center">
<GitBranch className="mx-auto mb-2 h-8 w-8 opacity-30 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{loadingColumns ? "컬럼 정보를 불러오고 있어요..." : "저장 테이블을 먼저 선택해주세요"}
</p>
</div>
)}
{/* 최대 깊이 */}
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input
type="number"
min={1}
max={10}
value={config.features?.maxDepth ?? 3}
onChange={(e) => updateFeatures("maxDepth", parseInt(e.target.value) || 3)}
className="h-7 w-20 text-xs"
className="h-7 w-[80px] text-xs"
/>
</div>
</div>
<Separator />
{/* 엔티티 선택 (리피터 모달 모드와 동일) */}
<div className="space-y-2">
<Label className="text-xs font-medium"> ( )</Label>
<p className="text-muted-foreground text-[10px]">
(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 ? (
@ -757,7 +761,7 @@ export function V2BomItemEditorConfigPanel({
onValueChange={handleEntityColumnSelect}
disabled={!targetTableForColumns}
>
<SelectTrigger className="h-8 text-xs">
<SelectTrigger className="h-8 text-sm">
<SelectValue placeholder="엔티티 컬럼 선택" />
</SelectTrigger>
<SelectContent>
@ -774,100 +778,86 @@ export function V2BomItemEditorConfigPanel({
</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="space-y-1 rounded border border-emerald-200 bg-emerald-50 p-2">
<p className="text-xs font-medium text-emerald-700"> </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>
<Separator />
{/* 기능 옵션 */}
<div className="space-y-3">
<Label className="text-xs font-medium"> </Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<Checkbox
id="bom-showAddButton"
{/* 기능 옵션 - 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>
<Switch
checked={config.features?.showAddButton ?? true}
onCheckedChange={(checked) => updateFeatures("showAddButton", !!checked)}
onCheckedChange={(checked) => updateFeatures("showAddButton", checked)}
/>
<label htmlFor="bom-showAddButton" className="text-xs">
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="bom-showDeleteButton"
<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)}
onCheckedChange={(checked) => updateFeatures("showDeleteButton", checked)}
/>
<label htmlFor="bom-showDeleteButton" className="text-xs">
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="bom-inlineEdit"
<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)}
onCheckedChange={(checked) => updateFeatures("inlineEdit", checked)}
/>
<label htmlFor="bom-inlineEdit" className="text-xs">
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="bom-showRowNumber"
<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)}
onCheckedChange={(checked) => updateFeatures("showRowNumber", checked)}
/>
<label htmlFor="bom-showRowNumber" className="text-xs">
</label>
</div>
</div>
</div>
{/* 메인 화면 테이블 참고 */}
{currentTableName && (
<>
<Separator />
<div className="space-y-2">
<Label className="text-xs font-medium"> ()</Label>
<div className="rounded border border-border bg-muted p-2">
<p className="text-xs font-medium text-foreground">{currentTableName}</p>
<p className="text-[10px] text-muted-foreground">
{currentTableColumns.length} / {entityColumns.length}
</p>
</div>
</div>
</>
)}
</TabsContent>
{/* ─── 컬럼 설정 탭 (리피터 동일 패턴) ─── */}
{/* ─── 컬럼 설정 탭 ─── */}
<TabsContent value="columns" className="mt-4 space-y-4">
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<p className="text-muted-foreground text-[10px]">
{/* 통합 컬럼 선택 */}
<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>
<p className="text-[11px] text-muted-foreground">
,
</p>
{/* 소스 테이블 컬럼 (표시용) */}
@ -880,7 +870,7 @@ export function V2BomItemEditorConfigPanel({
{loadingSourceColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p>
) : sourceTableColumns.length === 0 ? (
<p className="text-muted-foreground py-2 text-xs"> </p>
<p className="text-muted-foreground py-2 text-xs"> </p>
) : (
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2">
{sourceTableColumns.map((column) => (
@ -915,7 +905,7 @@ export function V2BomItemEditorConfigPanel({
{loadingColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p>
) : inputableColumns.length === 0 ? (
<p className="text-muted-foreground py-2 text-xs"> </p>
<p className="text-muted-foreground py-2 text-xs"> </p>
) : (
<div className="max-h-36 space-y-0.5 overflow-y-auto rounded-md border p-2">
{inputableColumns.map((column) => (
@ -941,141 +931,141 @@ export function V2BomItemEditorConfigPanel({
)}
</div>
{/* 선택된 컬럼 상세 (리피터 동일 패턴) */}
{/* 선택된 컬럼 상세 */}
{config.columns.length > 0 && (
<>
<Separator />
<div className="space-y-2">
<Label className="text-xs font-medium">
({config.columns.length})
<span className="text-muted-foreground ml-2 font-normal"> </span>
</Label>
<div className="max-h-48 space-y-1 overflow-y-auto">
{config.columns.map((col, index) => (
<div key={col.key} className="space-y-1">
<div
className={cn(
"flex items-center gap-2 rounded-md border p-2",
col.isSourceDisplay
? "border-primary/20 bg-primary/10/50"
: "border-border bg-muted/30",
col.hidden && "opacity-50",
)}
draggable
onDragStart={(e) => e.dataTransfer.setData("columnIndex", String(index))}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const fromIndex = parseInt(e.dataTransfer.getData("columnIndex"), 10);
if (fromIndex !== index) {
const newColumns = [...config.columns];
const [movedCol] = newColumns.splice(fromIndex, 1);
newColumns.splice(index, 0, movedCol);
updateConfig({ columns: newColumns });
<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">
<div
className={cn(
"flex items-center gap-2 rounded-md border p-2",
col.isSourceDisplay
? "border-primary/20 bg-primary/10/50"
: "border-border bg-muted/30",
col.hidden && "opacity-50",
)}
draggable
onDragStart={(e) => e.dataTransfer.setData("columnIndex", String(index))}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const fromIndex = parseInt(e.dataTransfer.getData("columnIndex"), 10);
if (fromIndex !== index) {
const newColumns = [...config.columns];
const [movedCol] = newColumns.splice(fromIndex, 1);
newColumns.splice(index, 0, movedCol);
updateConfig({ columns: newColumns });
}
}}
>
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab flex-shrink-0" />
{!col.isSourceDisplay && (
<button
type="button"
onClick={() =>
setExpandedColumn(expandedColumn === col.key ? null : col.key)
}
className="rounded p-0.5 hover:bg-muted/80"
>
{expandedColumn === col.key ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
</button>
)}
{col.isSourceDisplay ? (
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
) : (
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
)}
<Input
value={col.title}
onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
placeholder="제목"
className="h-6 flex-1 text-xs"
/>
{!col.isSourceDisplay && (
<button
type="button"
onClick={() => updateColumnProp(col.key, "hidden", !col.hidden)}
className={cn(
"rounded p-1 hover:bg-muted/80",
col.hidden ? "text-muted-foreground/70" : "text-muted-foreground",
)}
title={col.hidden ? "히든 (저장만 됨)" : "표시됨"}
>
{col.hidden ? (
<EyeOff className="h-3 w-3" />
) : (
<Eye className="h-3 w-3" />
)}
</button>
)}
{!col.isSourceDisplay && (
<button
type="button"
onClick={() => updateColumnProp(col.key, "editable", !(col.editable ?? true))}
className={cn(
"shrink-0 rounded px-1.5 py-0.5 text-[9px] font-medium transition-colors",
(col.editable ?? true)
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: "bg-muted text-muted-foreground dark:bg-foreground/90 dark:text-muted-foreground/70"
)}
title={(col.editable ?? true) ? "편집 가능" : "읽기 전용"}
>
{(col.editable ?? true) ? "편집" : "읽기"}
</button>
)}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
if (col.isSourceDisplay) {
toggleSourceDisplayColumn({
columnName: col.key,
displayName: col.title,
});
} else {
toggleInputColumn({ columnName: col.key, displayName: col.title });
}
}}
className="text-destructive h-6 w-6 p-0"
>
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab flex-shrink-0" />
{!col.isSourceDisplay && (
<button
type="button"
onClick={() =>
setExpandedColumn(expandedColumn === col.key ? null : col.key)
}
className="rounded p-0.5 hover:bg-muted/80"
>
{expandedColumn === col.key ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
</button>
)}
{col.isSourceDisplay ? (
<Link2
className="h-3 w-3 flex-shrink-0 text-primary"
title="소스 표시 (읽기 전용)"
/>
) : (
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
)}
<Input
value={col.title}
onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
placeholder="제목"
className="h-6 flex-1 text-xs"
/>
{!col.isSourceDisplay && (
<button
type="button"
onClick={() => updateColumnProp(col.key, "hidden", !col.hidden)}
className={cn(
"rounded p-1 hover:bg-muted/80",
col.hidden ? "text-muted-foreground/70" : "text-muted-foreground",
)}
title={col.hidden ? "히든 (저장만 됨)" : "표시됨"}
>
{col.hidden ? (
<EyeOff className="h-3 w-3" />
) : (
<Eye className="h-3 w-3" />
)}
</button>
)}
{!col.isSourceDisplay && (
<Checkbox
checked={col.editable ?? true}
onCheckedChange={(checked) =>
updateColumnProp(col.key, "editable", !!checked)
}
title="편집 가능"
/>
)}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
if (col.isSourceDisplay) {
toggleSourceDisplayColumn({
columnName: col.key,
displayName: col.title,
});
} else {
toggleInputColumn({ columnName: col.key, displayName: col.title });
}
}}
className="text-destructive h-6 w-6 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 확장 상세 */}
{!col.isSourceDisplay && expandedColumn === col.key && (
<div className="ml-6 space-y-2 rounded-md border border-dashed border-input bg-muted p-2">
<div className="space-y-1">
<Label className="text-[10px] text-muted-foreground"> </Label>
<Input
value={col.width || "auto"}
onChange={(e) => updateColumnProp(col.key, "width", e.target.value)}
placeholder="auto, 100px, 20%"
className="h-6 text-xs"
/>
</div>
</div>
)}
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
{/* 확장 상세 */}
{!col.isSourceDisplay && expandedColumn === col.key && (
<div className="ml-6 space-y-2 rounded-md border border-dashed border-input bg-muted p-2">
<div className="space-y-1">
<p className="text-[10px] text-muted-foreground"> </p>
<Input
value={col.width || "auto"}
onChange={(e) => updateColumnProp(col.key, "width", e.target.value)}
placeholder="auto, 100px, 20%"
className="h-6 text-xs"
/>
</div>
</div>
)}
</div>
))}
</div>
</>
</div>
)}
</TabsContent>
</Tabs>