- Integrated BOM routes into the backend for managing BOM history and versions. - Enhanced the V2BomTreeConfigPanel to include options for history and version table management. - Updated the BomTreeComponent to support viewing BOM data in both tree and level formats, with modals for editing BOM details, viewing history, and managing versions. - Improved user interaction with new buttons for accessing BOM history and version management directly from the BOM tree view.
1075 lines
43 KiB
TypeScript
1075 lines
43 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* BOM 트리 뷰 설정 패널
|
|
*
|
|
* V2BomItemEditorConfigPanel 구조 기반:
|
|
* - 기본 탭: 디테일 테이블 + 엔티티 선택 + 트리 설정
|
|
* - 컬럼 탭: 소스 표시 컬럼 + 디테일 컬럼 + 선택된 컬럼 상세
|
|
*/
|
|
|
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Separator } from "@/components/ui/separator";
|
|
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 {
|
|
Database,
|
|
Link2,
|
|
Trash2,
|
|
GripVertical,
|
|
ArrowRight,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Eye,
|
|
EyeOff,
|
|
Check,
|
|
ChevronsUpDown,
|
|
GitBranch,
|
|
} from "lucide-react";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface TableRelation {
|
|
tableName: string;
|
|
tableLabel: string;
|
|
foreignKeyColumn: string;
|
|
referenceColumn: string;
|
|
}
|
|
|
|
interface ColumnOption {
|
|
columnName: string;
|
|
displayName: string;
|
|
inputType?: string;
|
|
detailSettings?: {
|
|
codeGroup?: string;
|
|
referenceTable?: string;
|
|
referenceColumn?: string;
|
|
displayColumn?: string;
|
|
format?: string;
|
|
};
|
|
}
|
|
|
|
interface EntityColumnOption {
|
|
columnName: string;
|
|
displayName: string;
|
|
referenceTable?: string;
|
|
referenceColumn?: string;
|
|
displayColumn?: string;
|
|
}
|
|
|
|
interface TreeColumnConfig {
|
|
key: string;
|
|
title: string;
|
|
width?: string;
|
|
visible?: boolean;
|
|
hidden?: boolean;
|
|
isSourceDisplay?: boolean;
|
|
}
|
|
|
|
interface BomTreeConfig {
|
|
detailTable?: string;
|
|
foreignKey?: string;
|
|
parentKey?: string;
|
|
|
|
historyTable?: string;
|
|
versionTable?: string;
|
|
|
|
dataSource?: {
|
|
sourceTable?: string;
|
|
foreignKey?: string;
|
|
referenceKey?: string;
|
|
displayColumn?: string;
|
|
};
|
|
|
|
columns: TreeColumnConfig[];
|
|
|
|
features?: {
|
|
showExpandAll?: boolean;
|
|
showHeader?: boolean;
|
|
showQuantity?: boolean;
|
|
showLossRate?: boolean;
|
|
showHistory?: boolean;
|
|
showVersion?: boolean;
|
|
};
|
|
}
|
|
|
|
interface V2BomTreeConfigPanelProps {
|
|
config: BomTreeConfig;
|
|
onChange: (config: BomTreeConfig) => void;
|
|
currentTableName?: string;
|
|
screenTableName?: string;
|
|
}
|
|
|
|
export function V2BomTreeConfigPanel({
|
|
config: propConfig,
|
|
onChange,
|
|
currentTableName: propCurrentTableName,
|
|
screenTableName,
|
|
}: V2BomTreeConfigPanelProps) {
|
|
const currentTableName = screenTableName || propCurrentTableName;
|
|
|
|
const config: BomTreeConfig = useMemo(
|
|
() => ({
|
|
columns: [],
|
|
...propConfig,
|
|
dataSource: { ...propConfig?.dataSource },
|
|
features: {
|
|
showExpandAll: true,
|
|
showHeader: true,
|
|
showQuantity: true,
|
|
showLossRate: true,
|
|
...propConfig?.features,
|
|
},
|
|
}),
|
|
[propConfig],
|
|
);
|
|
|
|
const [detailTableColumns, setDetailTableColumns] = useState<ColumnOption[]>([]);
|
|
const [entityColumns, setEntityColumns] = useState<EntityColumnOption[]>([]);
|
|
const [sourceTableColumns, setSourceTableColumns] = useState<ColumnOption[]>([]);
|
|
const [allTables, setAllTables] = useState<{ tableName: string; displayName: string }[]>([]);
|
|
const [relatedTables, setRelatedTables] = useState<TableRelation[]>([]);
|
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
|
const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
|
|
const [loadingTables, setLoadingTables] = useState(false);
|
|
const [loadingRelations, setLoadingRelations] = useState(false);
|
|
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
|
|
const [expandedColumn, setExpandedColumn] = useState<string | null>(null);
|
|
|
|
const updateConfig = useCallback(
|
|
(updates: Partial<BomTreeConfig>) => {
|
|
onChange({ ...config, ...updates });
|
|
},
|
|
[config, onChange],
|
|
);
|
|
|
|
const updateFeatures = useCallback(
|
|
(field: string, value: any) => {
|
|
updateConfig({ features: { ...config.features, [field]: value } });
|
|
},
|
|
[config.features, updateConfig],
|
|
);
|
|
|
|
// 전체 테이블 목록 로드
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
setLoadingTables(true);
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setAllTables(
|
|
response.data.map((t: any) => ({
|
|
tableName: t.tableName || t.table_name,
|
|
displayName: t.displayName || t.table_label || t.tableName || t.table_name,
|
|
})),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
} finally {
|
|
setLoadingTables(false);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
// 연관 테이블 로드
|
|
useEffect(() => {
|
|
const loadRelatedTables = async () => {
|
|
const baseTable = currentTableName;
|
|
if (!baseTable) {
|
|
setRelatedTables([]);
|
|
return;
|
|
}
|
|
setLoadingRelations(true);
|
|
try {
|
|
const { apiClient } = await import("@/lib/api/client");
|
|
const response = await apiClient.get(
|
|
`/table-management/columns/${baseTable}/referenced-by`,
|
|
);
|
|
if (response.data.success && response.data.data) {
|
|
setRelatedTables(
|
|
response.data.data.map((rel: any) => ({
|
|
tableName: rel.tableName || rel.table_name,
|
|
tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
|
|
foreignKeyColumn: rel.columnName || rel.column_name,
|
|
referenceColumn: rel.referenceColumn || rel.reference_column || "id",
|
|
})),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("연관 테이블 로드 실패:", error);
|
|
setRelatedTables([]);
|
|
} finally {
|
|
setLoadingRelations(false);
|
|
}
|
|
};
|
|
loadRelatedTables();
|
|
}, [currentTableName]);
|
|
|
|
// 디테일 테이블 선택
|
|
const handleDetailTableSelect = useCallback(
|
|
(tableName: string) => {
|
|
const relation = relatedTables.find((r) => r.tableName === tableName);
|
|
updateConfig({
|
|
detailTable: tableName,
|
|
foreignKey: relation?.foreignKeyColumn || config.foreignKey,
|
|
});
|
|
},
|
|
[relatedTables, config.foreignKey, updateConfig],
|
|
);
|
|
|
|
// 디테일 테이블 컬럼 로드
|
|
useEffect(() => {
|
|
const loadColumns = async () => {
|
|
if (!config.detailTable) {
|
|
setDetailTableColumns([]);
|
|
setEntityColumns([]);
|
|
return;
|
|
}
|
|
setLoadingColumns(true);
|
|
try {
|
|
const columnData = await tableTypeApi.getColumns(config.detailTable);
|
|
const cols: ColumnOption[] = [];
|
|
const entityCols: EntityColumnOption[] = [];
|
|
|
|
for (const c of columnData) {
|
|
let detailSettings: any = null;
|
|
if (c.detailSettings) {
|
|
try {
|
|
detailSettings =
|
|
typeof c.detailSettings === "string" ? JSON.parse(c.detailSettings) : c.detailSettings;
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
const col: ColumnOption = {
|
|
columnName: c.columnName || c.column_name,
|
|
displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
|
|
inputType: c.inputType || c.input_type,
|
|
detailSettings: detailSettings
|
|
? {
|
|
codeGroup: detailSettings.codeGroup,
|
|
referenceTable: detailSettings.referenceTable,
|
|
referenceColumn: detailSettings.referenceColumn,
|
|
displayColumn: detailSettings.displayColumn,
|
|
format: detailSettings.format,
|
|
}
|
|
: undefined,
|
|
};
|
|
cols.push(col);
|
|
|
|
if (col.inputType === "entity") {
|
|
const refTable = detailSettings?.referenceTable || c.referenceTable;
|
|
if (refTable) {
|
|
entityCols.push({
|
|
columnName: col.columnName,
|
|
displayName: col.displayName,
|
|
referenceTable: refTable,
|
|
referenceColumn: detailSettings?.referenceColumn || c.referenceColumn || "id",
|
|
displayColumn: detailSettings?.displayColumn || c.displayColumn,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
setDetailTableColumns(cols);
|
|
setEntityColumns(entityCols);
|
|
} catch (error) {
|
|
console.error("컬럼 로드 실패:", error);
|
|
setDetailTableColumns([]);
|
|
setEntityColumns([]);
|
|
} finally {
|
|
setLoadingColumns(false);
|
|
}
|
|
};
|
|
loadColumns();
|
|
}, [config.detailTable]);
|
|
|
|
// 소스(엔티티) 테이블 컬럼 로드
|
|
useEffect(() => {
|
|
const loadSourceColumns = async () => {
|
|
const sourceTable = config.dataSource?.sourceTable;
|
|
if (!sourceTable) {
|
|
setSourceTableColumns([]);
|
|
return;
|
|
}
|
|
setLoadingSourceColumns(true);
|
|
try {
|
|
const columnData = await tableTypeApi.getColumns(sourceTable);
|
|
setSourceTableColumns(
|
|
columnData.map((c: any) => ({
|
|
columnName: c.columnName || c.column_name,
|
|
displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
|
|
inputType: c.inputType || c.input_type,
|
|
})),
|
|
);
|
|
} catch (error) {
|
|
console.error("소스 테이블 컬럼 로드 실패:", error);
|
|
setSourceTableColumns([]);
|
|
} finally {
|
|
setLoadingSourceColumns(false);
|
|
}
|
|
};
|
|
loadSourceColumns();
|
|
}, [config.dataSource?.sourceTable]);
|
|
|
|
// 엔티티 컬럼 선택 시 소스 테이블 자동 설정
|
|
const handleEntityColumnSelect = (columnName: string) => {
|
|
const selectedEntity = entityColumns.find((c) => c.columnName === columnName);
|
|
if (selectedEntity) {
|
|
updateConfig({
|
|
dataSource: {
|
|
...config.dataSource,
|
|
sourceTable: selectedEntity.referenceTable || "",
|
|
foreignKey: selectedEntity.columnName,
|
|
referenceKey: selectedEntity.referenceColumn || "id",
|
|
displayColumn: selectedEntity.displayColumn,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// 컬럼 토글
|
|
const toggleDetailColumn = (column: ColumnOption) => {
|
|
const exists = config.columns.findIndex((c) => c.key === column.columnName && !c.isSourceDisplay);
|
|
if (exists >= 0) {
|
|
updateConfig({ columns: config.columns.filter((c) => c.key !== column.columnName || c.isSourceDisplay) });
|
|
} else {
|
|
const newCol: TreeColumnConfig = {
|
|
key: column.columnName,
|
|
title: column.displayName,
|
|
width: "auto",
|
|
visible: true,
|
|
};
|
|
updateConfig({ columns: [...config.columns, newCol] });
|
|
}
|
|
};
|
|
|
|
const toggleSourceDisplayColumn = (column: ColumnOption) => {
|
|
const exists = config.columns.some((c) => c.key === column.columnName && c.isSourceDisplay);
|
|
if (exists) {
|
|
updateConfig({ columns: config.columns.filter((c) => c.key !== column.columnName) });
|
|
} else {
|
|
const newCol: TreeColumnConfig = {
|
|
key: column.columnName,
|
|
title: column.displayName,
|
|
width: "auto",
|
|
visible: true,
|
|
isSourceDisplay: true,
|
|
};
|
|
updateConfig({ columns: [...config.columns, newCol] });
|
|
}
|
|
};
|
|
|
|
const isColumnAdded = (columnName: string) =>
|
|
config.columns.some((c) => c.key === columnName && !c.isSourceDisplay);
|
|
|
|
const isSourceColumnSelected = (columnName: string) =>
|
|
config.columns.some((c) => c.key === columnName && c.isSourceDisplay);
|
|
|
|
const updateColumnProp = (key: string, field: keyof TreeColumnConfig, value: any) => {
|
|
updateConfig({
|
|
columns: config.columns.map((col) => (col.key === key ? { ...col, [field]: value } : col)),
|
|
});
|
|
};
|
|
|
|
// FK/시스템 컬럼 제외한 표시 가능 컬럼
|
|
const displayableColumns = useMemo(() => {
|
|
const fkColumn = config.dataSource?.foreignKey;
|
|
const systemCols = ["id", "created_at", "updated_at", "created_by", "updated_by", "company_code", "created_date"];
|
|
return detailTableColumns.filter(
|
|
(col) => col.columnName !== fkColumn && col.inputType !== "entity" && !systemCols.includes(col.columnName),
|
|
);
|
|
}, [detailTableColumns, config.dataSource?.foreignKey]);
|
|
|
|
// FK 후보 컬럼
|
|
const fkCandidateColumns = useMemo(() => {
|
|
const systemCols = ["created_at", "updated_at", "created_by", "updated_by", "company_code", "created_date"];
|
|
return detailTableColumns.filter((c) => !systemCols.includes(c.columnName));
|
|
}, [detailTableColumns]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Tabs defaultValue="basic" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="basic" className="text-xs">
|
|
기본
|
|
</TabsTrigger>
|
|
<TabsTrigger value="columns" className="text-xs">
|
|
컬럼
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* ─── 기본 설정 탭 ─── */}
|
|
<TabsContent value="basic" className="mt-4 space-y-4">
|
|
{/* 디테일 테이블 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">디테일 테이블</Label>
|
|
|
|
<div
|
|
className={cn(
|
|
"rounded-lg border p-3",
|
|
config.detailTable ? "border-orange-300 bg-orange-50" : "border-gray-300 bg-gray-50",
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Database
|
|
className={cn("h-4 w-4", config.detailTable ? "text-orange-600" : "text-gray-400")}
|
|
/>
|
|
<div className="flex-1">
|
|
<p className={cn("text-sm font-medium", config.detailTable ? "text-orange-700" : "text-gray-500")}>
|
|
{config.detailTable
|
|
? allTables.find((t) => t.tableName === config.detailTable)?.displayName || config.detailTable
|
|
: "미설정"}
|
|
</p>
|
|
{config.detailTable && config.foreignKey && (
|
|
<p className="mt-0.5 text-[10px] text-orange-600">
|
|
FK: {config.foreignKey} → {currentTableName || "메인 테이블"}.id
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={tableComboboxOpen}
|
|
disabled={loadingTables || loadingRelations}
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{loadingTables ? "로딩 중..." : "디테일 테이블 선택..."}
|
|
<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-60">
|
|
<CommandEmpty className="py-3 text-center text-xs">
|
|
테이블을 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
|
|
{relatedTables.length > 0 && (
|
|
<CommandGroup heading="연관 테이블 (FK 자동 설정)">
|
|
{relatedTables.map((rel) => (
|
|
<CommandItem
|
|
key={rel.tableName}
|
|
value={`${rel.tableName} ${rel.tableLabel}`}
|
|
onSelect={() => {
|
|
handleDetailTableSelect(rel.tableName);
|
|
setTableComboboxOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.detailTable === rel.tableName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<Link2 className="mr-2 h-3 w-3 text-orange-500" />
|
|
<span>{rel.tableLabel}</span>
|
|
<span className="text-muted-foreground ml-1 text-[10px]">
|
|
({rel.foreignKeyColumn})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
)}
|
|
|
|
<CommandGroup heading="전체 테이블">
|
|
{allTables
|
|
.filter((t) => !relatedTables.some((r) => r.tableName === t.tableName))
|
|
.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.tableName} ${table.displayName}`}
|
|
onSelect={() => {
|
|
handleDetailTableSelect(table.tableName);
|
|
setTableComboboxOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.detailTable === table.tableName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<Database className="mr-2 h-3 w-3 text-gray-400" />
|
|
<span>{table.displayName}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 트리 구조 설정 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<GitBranch className="h-4 w-4" />
|
|
<Label className="text-xs font-medium">트리 구조 설정</Label>
|
|
</div>
|
|
<p className="text-muted-foreground text-[10px]">
|
|
메인 FK와 부모-자식 계층 FK를 선택하세요
|
|
</p>
|
|
|
|
{fkCandidateColumns.length > 0 ? (
|
|
<div className="space-y-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">FK 컬럼 (메인 테이블 참조)</Label>
|
|
<Select
|
|
value={config.foreignKey || ""}
|
|
onValueChange={(value) => updateConfig({ foreignKey: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="FK 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{fkCandidateColumns.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="space-y-1">
|
|
<Label className="text-[10px]">부모 키 컬럼 (자기 참조 FK)</Label>
|
|
<Select
|
|
value={config.parentKey || ""}
|
|
onValueChange={(value) => updateConfig({ parentKey: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="부모 키 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{fkCandidateColumns.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>
|
|
) : (
|
|
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
|
<p className="text-[10px] text-gray-500">
|
|
{loadingColumns ? "로딩 중..." : "디테일 테이블을 먼저 선택하세요"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 엔티티 선택 (품목 참조) */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">엔티티 선택 (품목 참조)</Label>
|
|
<p className="text-muted-foreground text-[10px]">
|
|
트리 노드에 표시할 품목 정보의 소스 엔티티
|
|
</p>
|
|
|
|
{entityColumns.length > 0 ? (
|
|
<Select
|
|
value={config.dataSource?.foreignKey || ""}
|
|
onValueChange={handleEntityColumnSelect}
|
|
disabled={!config.detailTable}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="엔티티 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{entityColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
<div className="flex items-center gap-2">
|
|
<Link2 className="h-3 w-3 text-blue-500" />
|
|
<span>{col.displayName}</span>
|
|
<ArrowRight className="h-3 w-3 text-gray-400" />
|
|
<span className="text-gray-500">{col.referenceTable}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
|
<p className="text-[10px] text-gray-500">
|
|
{loadingColumns
|
|
? "로딩 중..."
|
|
: !config.detailTable
|
|
? "디테일 테이블을 먼저 선택하세요"
|
|
: "엔티티 타입 컬럼이 없습니다"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{config.dataSource?.sourceTable && (
|
|
<div className="space-y-1 rounded border border-green-200 bg-green-50 p-2">
|
|
<p className="text-xs font-medium text-green-700">선택된 엔티티</p>
|
|
<div className="text-[10px] text-green-600">
|
|
<p>참조 테이블: {config.dataSource.sourceTable}</p>
|
|
<p>FK 컬럼: {config.dataSource.foreignKey}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 이력/버전 테이블 설정 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">이력/버전 관리</Label>
|
|
<p className="text-muted-foreground text-[10px]">
|
|
BOM 변경 이력과 버전 관리에 사용할 테이블을 선택하세요
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="tree-showHistory"
|
|
checked={config.features?.showHistory ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showHistory", !!checked)}
|
|
/>
|
|
<Label htmlFor="tree-showHistory" className="text-[10px]">이력 관리 사용</Label>
|
|
</div>
|
|
{(config.features?.showHistory ?? true) && (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
disabled={loadingTables}
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{config.historyTable
|
|
? allTables.find((t) => t.tableName === config.historyTable)?.displayName || config.historyTable
|
|
: "이력 테이블 선택..."}
|
|
<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="py-3 text-center text-xs">
|
|
테이블을 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{allTables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.tableName} ${table.displayName}`}
|
|
onSelect={() => updateConfig({ historyTable: table.tableName })}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.historyTable === table.tableName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<Database className="mr-2 h-3 w-3 text-gray-400" />
|
|
<span>{table.displayName}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="tree-showVersion"
|
|
checked={config.features?.showVersion ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showVersion", !!checked)}
|
|
/>
|
|
<Label htmlFor="tree-showVersion" className="text-[10px]">버전 관리 사용</Label>
|
|
</div>
|
|
{(config.features?.showVersion ?? true) && (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
disabled={loadingTables}
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{config.versionTable
|
|
? allTables.find((t) => t.tableName === config.versionTable)?.displayName || config.versionTable
|
|
: "버전 테이블 선택..."}
|
|
<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="py-3 text-center text-xs">
|
|
테이블을 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{allTables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.tableName} ${table.displayName}`}
|
|
onSelect={() => updateConfig({ versionTable: table.tableName })}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.versionTable === table.tableName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<Database className="mr-2 h-3 w-3 text-gray-400" />
|
|
<span>{table.displayName}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
</div>
|
|
</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="tree-showExpandAll"
|
|
checked={config.features?.showExpandAll ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showExpandAll", !!checked)}
|
|
/>
|
|
<label htmlFor="tree-showExpandAll" className="text-xs">
|
|
전체 펼치기/접기
|
|
</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="tree-showHeader"
|
|
checked={config.features?.showHeader ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showHeader", !!checked)}
|
|
/>
|
|
<label htmlFor="tree-showHeader" className="text-xs">
|
|
헤더 정보
|
|
</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="tree-showQuantity"
|
|
checked={config.features?.showQuantity ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showQuantity", !!checked)}
|
|
/>
|
|
<label htmlFor="tree-showQuantity" className="text-xs">
|
|
수량 표시
|
|
</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="tree-showLossRate"
|
|
checked={config.features?.showLossRate ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showLossRate", !!checked)}
|
|
/>
|
|
<label htmlFor="tree-showLossRate" 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-gray-200 bg-gray-50 p-2">
|
|
<p className="text-xs font-medium text-gray-700">{currentTableName}</p>
|
|
<p className="text-[10px] text-gray-500">
|
|
컬럼 {detailTableColumns.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]">
|
|
트리 노드에 표시할 소스/디테일 컬럼을 선택하세요
|
|
</p>
|
|
|
|
{/* 소스 테이블 컬럼 (표시용) */}
|
|
{config.dataSource?.sourceTable && (
|
|
<>
|
|
<div className="mb-1 mt-2 flex items-center gap-1 text-[10px] font-medium text-blue-600">
|
|
<Link2 className="h-3 w-3" />
|
|
소스 테이블 ({config.dataSource.sourceTable}) - 표시용
|
|
</div>
|
|
{loadingSourceColumns ? (
|
|
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
|
) : sourceTableColumns.length === 0 ? (
|
|
<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-blue-200 bg-blue-50/30 p-2">
|
|
{sourceTableColumns.map((column) => (
|
|
<div
|
|
key={`source-${column.columnName}`}
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-blue-100/50",
|
|
isSourceColumnSelected(column.columnName) && "bg-blue-100",
|
|
)}
|
|
onClick={() => toggleSourceDisplayColumn(column)}
|
|
>
|
|
<Checkbox
|
|
checked={isSourceColumnSelected(column.columnName)}
|
|
onCheckedChange={() => toggleSourceDisplayColumn(column)}
|
|
className="pointer-events-none h-3.5 w-3.5"
|
|
/>
|
|
<Link2 className="h-3 w-3 flex-shrink-0 text-blue-500" />
|
|
<span className="truncate text-xs">{column.displayName}</span>
|
|
<span className="ml-auto text-[10px] text-blue-400">표시</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 디테일 테이블 컬럼 */}
|
|
<div className="mb-1 mt-3 flex items-center gap-1 text-[10px] font-medium text-gray-600">
|
|
<Database className="h-3 w-3" />
|
|
디테일 테이블 ({config.detailTable || "미선택"}) - 직접 컬럼
|
|
</div>
|
|
{loadingColumns ? (
|
|
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
|
) : displayableColumns.length === 0 ? (
|
|
<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">
|
|
{displayableColumns.map((column) => (
|
|
<div
|
|
key={`detail-${column.columnName}`}
|
|
className={cn(
|
|
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
|
isColumnAdded(column.columnName) && "bg-primary/10",
|
|
)}
|
|
onClick={() => toggleDetailColumn(column)}
|
|
>
|
|
<Checkbox
|
|
checked={isColumnAdded(column.columnName)}
|
|
onCheckedChange={() => toggleDetailColumn(column)}
|
|
className="pointer-events-none h-3.5 w-3.5"
|
|
/>
|
|
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
|
<span className="truncate text-xs">{column.displayName}</span>
|
|
<span className="ml-auto text-[10px] text-gray-400">{column.inputType}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</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-blue-200 bg-blue-50/50"
|
|
: "border-gray-200 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-gray-200"
|
|
>
|
|
{expandedColumn === col.key ? (
|
|
<ChevronDown className="h-3 w-3 text-gray-500" />
|
|
) : (
|
|
<ChevronRight className="h-3 w-3 text-gray-500" />
|
|
)}
|
|
</button>
|
|
)}
|
|
|
|
{col.isSourceDisplay ? (
|
|
<Link2 className="h-3 w-3 flex-shrink-0 text-blue-500" 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-gray-200",
|
|
col.hidden ? "text-gray-400" : "text-gray-600",
|
|
)}
|
|
title={col.hidden ? "히든" : "표시됨"}
|
|
>
|
|
{col.hidden ? (
|
|
<EyeOff className="h-3 w-3" />
|
|
) : (
|
|
<Eye className="h-3 w-3" />
|
|
)}
|
|
</button>
|
|
)}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
if (col.isSourceDisplay) {
|
|
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
|
} else {
|
|
toggleDetailColumn({ 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-gray-300 bg-gray-50 p-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">컬럼 너비</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>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
V2BomTreeConfigPanel.displayName = "V2BomTreeConfigPanel";
|
|
|
|
export default V2BomTreeConfigPanel;
|