ERP-node/frontend/components/v2/config-panels/V2RackStructureConfigPanel.tsx

268 lines
10 KiB
TypeScript

"use client";
/**
* V2 렉 구조 설정 패널
* 토스식 단계별 UX: 필드 매핑 -> 제한 설정 -> UI 설정(접힘)
*/
import React, { useState, useEffect, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Badge } from "@/components/ui/badge";
import { Database, SlidersHorizontal, Settings, ChevronDown, CheckCircle2, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
import type { RackStructureComponentConfig, FieldMapping } from "@/lib/registry/components/v2-rack-structure/types";
interface V2RackStructureConfigPanelProps {
config: RackStructureComponentConfig;
onChange: (config: RackStructureComponentConfig) => void;
tables?: Array<{
tableName: string;
tableLabel?: string;
columns: Array<{
columnName: string;
columnLabel?: string;
dataType?: string;
}>;
}>;
}
export const V2RackStructureConfigPanel: React.FC<V2RackStructureConfigPanelProps> = ({
config,
onChange,
tables = [],
}) => {
const [availableColumns, setAvailableColumns] = useState<
Array<{ value: string; label: string }>
>([]);
const [advancedOpen, setAdvancedOpen] = useState(false);
useEffect(() => {
const columns: Array<{ value: string; label: string }> = [];
tables.forEach((table) => {
table.columns.forEach((col) => {
columns.push({
value: col.columnName,
label: col.columnLabel || col.columnName,
});
});
});
setAvailableColumns(columns);
}, [tables]);
const handleChange = (key: keyof RackStructureComponentConfig, value: any) => {
onChange({ ...config, [key]: value });
};
const handleFieldMappingChange = (field: keyof FieldMapping, value: string) => {
const currentMapping = config.fieldMapping || {};
onChange({
...config,
fieldMapping: {
...currentMapping,
[field]: value === "__none__" ? undefined : value,
},
});
};
const fieldMapping = config.fieldMapping || {};
const fieldMappingItems: Array<{
key: keyof FieldMapping;
label: string;
description: string;
}> = [
{ key: "warehouseCodeField", label: "창고 코드", description: "창고를 식별하는 코드 필드예요" },
{ key: "warehouseNameField", label: "창고명", description: "창고 이름을 표시하는 필드예요" },
{ key: "floorField", label: "층", description: "몇 층인지 나타내는 필드예요" },
{ key: "zoneField", label: "구역", description: "구역 정보를 가져올 필드예요" },
{ key: "locationTypeField", label: "위치 유형", description: "위치의 유형(선반, 바닥 등)을 나타내요" },
{ key: "statusField", label: "사용 여부", description: "사용/미사용 상태를 나타내는 필드예요" },
];
const mappedCount = useMemo(
() => fieldMappingItems.filter((item) => fieldMapping[item.key]).length,
[fieldMapping]
);
return (
<div className="space-y-4">
{/* ─── 1단계: 필드 매핑 ─── */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-primary" />
<p className="text-sm font-medium"> </p>
<Badge variant="secondary" className="ml-auto text-[10px] px-1.5 py-0">
{mappedCount}/{fieldMappingItems.length}
</Badge>
</div>
<p className="text-[11px] text-muted-foreground pl-6">
</p>
</div>
<div className="space-y-1.5">
{fieldMappingItems.map((item) => {
const isMapped = !!fieldMapping[item.key];
return (
<div
key={item.key}
className={cn(
"flex items-center gap-3 rounded-lg border px-3 py-2 transition-colors",
isMapped ? "border-primary/30 bg-primary/5" : "bg-muted/30"
)}
>
{isMapped ? (
<CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-primary" />
) : (
<Circle className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40" />
)}
<div className="min-w-0 flex-1">
<p className="text-xs font-medium truncate">{item.label}</p>
<p className="text-[10px] text-muted-foreground truncate">{item.description}</p>
</div>
<Select
value={fieldMapping[item.key] || "__none__"}
onValueChange={(v) => handleFieldMappingChange(item.key, v)}
>
<SelectTrigger className="h-7 w-[120px] shrink-0 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> </SelectItem>
{availableColumns.map((col) => (
<SelectItem key={col.value} value={col.value}>
{col.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
})}
</div>
{/* ─── 2단계: 제한 설정 ─── */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<SlidersHorizontal className="h-4 w-4 text-primary" />
<p className="text-sm font-medium"> </p>
</div>
<p className="text-[11px] text-muted-foreground pl-6">
</p>
</div>
<div className="grid grid-cols-3 gap-2">
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
<p className="text-[10px] text-muted-foreground"> </p>
<Input
type="number"
min={1}
max={20}
value={config.maxConditions || 10}
onChange={(e) => handleChange("maxConditions", parseInt(e.target.value) || 10)}
className="h-7 text-xs text-center"
/>
</div>
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
<p className="text-[10px] text-muted-foreground"> </p>
<Input
type="number"
min={1}
max={999}
value={config.maxRows || 99}
onChange={(e) => handleChange("maxRows", parseInt(e.target.value) || 99)}
className="h-7 text-xs text-center"
/>
</div>
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
<p className="text-[10px] text-muted-foreground"> </p>
<Input
type="number"
min={1}
max={99}
value={config.maxLevels || 20}
onChange={(e) => handleChange("maxLevels", parseInt(e.target.value) || 20)}
className="h-7 text-xs text-center"
/>
</div>
</div>
{/* ─── 3단계: 고급 설정 (Collapsible) ─── */}
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<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">UI </span>
</div>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200",
advancedOpen && "rotate-180"
)}
/>
</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 className="min-w-0 flex-1 mr-3">
<p className="text-xs font-medium">릿 </p>
<p className="text-[10px] text-muted-foreground"> 릿 / </p>
</div>
<Switch
checked={config.showTemplates ?? true}
onCheckedChange={(checked) => handleChange("showTemplates", checked)}
/>
</div>
<div className="flex items-center justify-between py-1">
<div className="min-w-0 flex-1 mr-3">
<p className="text-xs font-medium"></p>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<Switch
checked={config.showPreview ?? true}
onCheckedChange={(checked) => handleChange("showPreview", checked)}
/>
</div>
<div className="flex items-center justify-between py-1">
<div className="min-w-0 flex-1 mr-3">
<p className="text-xs font-medium"> </p>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<Switch
checked={config.showStatistics ?? true}
onCheckedChange={(checked) => handleChange("showStatistics", checked)}
/>
</div>
<div className="flex items-center justify-between py-1">
<div className="min-w-0 flex-1 mr-3">
<p className="text-xs font-medium"> </p>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<Switch
checked={config.readonly ?? false}
onCheckedChange={(checked) => handleChange("readonly", checked)}
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
};
V2RackStructureConfigPanel.displayName = "V2RackStructureConfigPanel";
export default V2RackStructureConfigPanel;