ERP-node/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputCon...

564 lines
25 KiB
TypeScript

"use client";
import React, { useState, useMemo } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardContent } from "@/components/ui/card";
import { Plus, X } from "lucide-react";
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition } from "./types";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
export interface SelectedItemsDetailInputConfigPanelProps {
config: SelectedItemsDetailInputConfig;
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 원본 테이블 컬럼
targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 대상 테이블 컬럼
allTables?: Array<{ tableName: string; displayName?: string }>;
screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용)
onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백
onTargetTableChange?: (tableName: string) => void; // 🆕 대상 테이블 변경 콜백 (기존 onTableChange 대체)
}
/**
* SelectedItemsDetailInput 설정 패널
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
*/
export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailInputConfigPanelProps> = ({
config,
onChange,
sourceTableColumns = [], // 🆕 원본 테이블 컬럼
targetTableColumns = [], // 🆕 대상 테이블 컬럼
allTables = [],
screenTableName, // 🆕 현재 화면의 테이블명
onSourceTableChange, // 🆕 원본 테이블 변경 콜백
onTargetTableChange, // 🆕 대상 테이블 변경 콜백
}) => {
const [localFields, setLocalFields] = useState<AdditionalFieldDefinition[]>(config.additionalFields || []);
const [displayColumns, setDisplayColumns] = useState<Array<{ name: string; label: string; width?: string }>>(config.displayColumns || []);
const [fieldPopoverOpen, setFieldPopoverOpen] = useState<Record<number, boolean>>({});
// 🆕 원본 테이블 선택 상태
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
// 🆕 대상 테이블 선택 상태 (기존 tableSelectOpen)
const [tableSelectOpen, setTableSelectOpen] = useState(false);
const [tableSearchValue, setTableSearchValue] = useState("");
// 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정
React.useEffect(() => {
if (screenTableName && !config.targetTable) {
console.log("✨ 현재 화면 테이블을 저장 대상 테이블로 자동 설정:", screenTableName);
handleChange("targetTable", screenTableName);
// 컬럼도 자동 로드
if (onTargetTableChange) {
onTargetTableChange(screenTableName);
}
}
}, [screenTableName]); // config.targetTable은 의존성에서 제외 (한 번만 실행)
const handleChange = (key: keyof SelectedItemsDetailInputConfig, value: any) => {
onChange({ [key]: value });
};
const handleFieldsChange = (fields: AdditionalFieldDefinition[]) => {
setLocalFields(fields);
handleChange("additionalFields", fields);
};
const handleDisplayColumnsChange = (columns: Array<{ name: string; label: string; width?: string }>) => {
setDisplayColumns(columns);
handleChange("displayColumns", columns);
};
// 필드 추가
const addField = () => {
const newField: AdditionalFieldDefinition = {
name: `field_${localFields.length + 1}`,
label: `필드 ${localFields.length + 1}`,
type: "text",
};
handleFieldsChange([...localFields, newField]);
};
// 필드 제거
const removeField = (index: number) => {
handleFieldsChange(localFields.filter((_, i) => i !== index));
};
// 필드 수정
const updateField = (index: number, updates: Partial<AdditionalFieldDefinition>) => {
const newFields = [...localFields];
newFields[index] = { ...newFields[index], ...updates };
handleFieldsChange(newFields);
};
// 표시 컬럼 추가
const addDisplayColumn = (columnName: string, columnLabel: string) => {
if (!displayColumns.some(col => col.name === columnName)) {
handleDisplayColumnsChange([...displayColumns, { name: columnName, label: columnLabel }]);
}
};
// 표시 컬럼 제거
const removeDisplayColumn = (columnName: string) => {
handleDisplayColumnsChange(displayColumns.filter((col) => col.name !== columnName));
};
// 🆕 표시 컬럼용: 원본 테이블에서 사용되지 않은 컬럼 목록
const availableColumns = useMemo(() => {
const usedColumns = new Set([...displayColumns.map(c => c.name), ...localFields.map((f) => f.name)]);
return sourceTableColumns.filter((col) => !usedColumns.has(col.columnName));
}, [sourceTableColumns, displayColumns, localFields]);
// 🆕 추가 입력 필드용: 대상 테이블에서 사용되지 않은 컬럼 목록
const availableTargetColumns = useMemo(() => {
const usedColumns = new Set([...displayColumns.map(c => c.name), ...localFields.map((f) => f.name)]);
return targetTableColumns.filter((col) => !usedColumns.has(col.columnName));
}, [targetTableColumns, displayColumns, localFields]);
// 🆕 원본 테이블 필터링
const filteredSourceTables = useMemo(() => {
if (!sourceTableSearchValue) return allTables;
const searchLower = sourceTableSearchValue.toLowerCase();
return allTables.filter(
(table) =>
table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower),
);
}, [allTables, sourceTableSearchValue]);
// 🆕 선택된 원본 테이블 표시명
const selectedSourceTableLabel = useMemo(() => {
if (!config.sourceTable) return "원본 테이블을 선택하세요";
const table = allTables.find((t) => t.tableName === config.sourceTable);
return table ? table.displayName || table.tableName : config.sourceTable;
}, [config.sourceTable, allTables]);
// 대상 테이블 필터링
const filteredTables = useMemo(() => {
if (!tableSearchValue) return allTables;
const searchLower = tableSearchValue.toLowerCase();
return allTables.filter(
(table) =>
table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower),
);
}, [allTables, tableSearchValue]);
// 선택된 대상 테이블 표시명
const selectedTableLabel = useMemo(() => {
if (!config.targetTable) return "저장 대상 테이블을 선택하세요";
const table = allTables.find((t) => t.tableName === config.targetTable);
return table ? table.displayName || table.tableName : config.targetTable;
}, [config.targetTable, allTables]);
return (
<div className="space-y-4">
{/* 데이터 소스 ID */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">
ID <span className="text-primary font-normal">( )</span>
</Label>
<Input
value={config.dataSourceId || ""}
onChange={(e) => handleChange("dataSourceId", e.target.value)}
placeholder="비워두면 URL 파라미터에서 자동 설정"
className="h-7 text-xs sm:h-8 sm:text-sm"
/>
<p className="text-[10px] text-primary font-medium sm:text-xs">
URL (Button이 )
</p>
<p className="text-[10px] text-gray-500 sm:text-xs">
</p>
</div>
{/* 🆕 원본 데이터 테이블 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Popover open={sourceTableSelectOpen} onOpenChange={setSourceTableSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={sourceTableSelectOpen}
className="h-8 w-full justify-between text-xs sm:text-sm"
disabled={allTables.length === 0}
>
{selectedSourceTableLabel}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." value={sourceTableSearchValue} onValueChange={setSourceTableSearchValue} className="h-8 text-xs sm:text-sm" />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-64 overflow-auto">
{filteredSourceTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
handleChange("sourceTable", currentValue);
setSourceTableSelectOpen(false);
setSourceTableSearchValue("");
if (onSourceTableChange) {
onSourceTableChange(currentValue);
}
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
config.sourceTable === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
{table.displayName || table.tableName}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<p className="text-[10px] text-gray-500 sm:text-xs">
(: item_info)
</p>
</div>
{/* 저장 대상 테이블 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Popover open={tableSelectOpen} onOpenChange={setTableSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableSelectOpen}
className="h-7 w-full justify-between text-xs sm:h-8 sm:text-sm"
>
{selectedTableLabel}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput
placeholder="테이블 검색..."
value={tableSearchValue}
onValueChange={setTableSearchValue}
className="text-xs sm:text-sm"
/>
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup className="max-h-48 overflow-auto sm:max-h-64">
{filteredTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
handleChange("targetTable", currentValue);
setTableSelectOpen(false);
setTableSearchValue("");
if (onTargetTableChange) {
onTargetTableChange(currentValue);
}
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
config.targetTable === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
{table.displayName || table.tableName}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<p className="text-[10px] text-gray-500 sm:text-xs"> </p>
</div>
{/* 표시할 원본 데이터 컬럼 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
<div className="space-y-2">
{displayColumns.map((col, index) => (
<div key={index} className="flex items-center gap-2">
<Input value={col.label || col.name} readOnly className="h-7 flex-1 text-xs sm:h-8 sm:text-sm" />
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeDisplayColumn(col.name)}
className="h-6 w-6 text-red-500 hover:bg-red-50 sm:h-7 sm:w-7"
>
<X className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
))}
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 w-full border-dashed text-xs sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs sm:text-sm" />
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup className="max-h-48 overflow-auto sm:max-h-64">
{availableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={() => addDisplayColumn(column.columnName, column.columnLabel || column.columnName)}
className="text-xs sm:text-sm"
>
<div>
<div className="font-medium">{column.columnLabel || column.columnName}</div>
{column.dataType && <div className="text-[10px] text-gray-500">{column.dataType}</div>}
</div>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<p className="text-[10px] text-gray-500 sm:text-xs">
(: 품목코드, )
</p>
</div>
{/* 추가 입력 필드 정의 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
{localFields.map((field, index) => (
<Card key={index} className="border-2">
<CardContent className="space-y-2 pt-3 sm:space-y-3 sm:pt-4">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-gray-700 sm:text-sm"> {index + 1}</span>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeField(index)}
className="h-5 w-5 text-red-500 hover:bg-red-50 sm:h-6 sm:w-6"
>
<X className="h-2 w-2 sm:h-3 sm:w-3" />
</Button>
</div>
<div className="grid grid-cols-2 gap-2 sm:gap-3">
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> ()</Label>
<Popover
open={fieldPopoverOpen[index] || false}
onOpenChange={(open) => setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: open })}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-6 w-full justify-between text-[10px] sm:h-7 sm:text-xs"
>
{field.name || "컬럼 선택"}
<ChevronsUpDown className="ml-1 h-2 w-2 shrink-0 opacity-50 sm:h-3 sm:w-3" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-0 sm:w-[200px]">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-6 text-[10px] sm:h-7 sm:text-xs" />
<CommandEmpty className="text-[10px] sm:text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[150px] overflow-auto sm:max-h-[200px]">
{availableTargetColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={() => {
updateField(index, {
name: column.columnName,
label: column.columnLabel || column.columnName,
inputType: column.inputType || "text", // 🆕 inputType 포함
codeCategory: column.codeCategory, // 🆕 codeCategory 포함
});
setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: false });
}}
className="text-[10px] sm:text-xs"
>
<Check
className={cn(
"mr-1 h-2 w-2 sm:mr-2 sm:h-3 sm:w-3",
field.name === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
<div>
<div className="font-medium">{column.columnLabel}</div>
<div className="text-[9px] text-gray-500">{column.columnName}</div>
</div>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"></Label>
<Input
value={field.label}
onChange={(e) => updateField(index, { label: e.target.value })}
placeholder="필드 라벨"
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2 sm:gap-3">
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> ()</Label>
<Input
value={field.inputType || field.type || "text"}
readOnly
disabled
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs bg-muted"
/>
<p className="text-[9px] text-primary sm:text-[10px]">
</p>
</div>
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs">Placeholder</Label>
<Input
value={field.placeholder || ""}
onChange={(e) => updateField(index, { placeholder: e.target.value })}
placeholder="입력 안내"
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id={`required-${index}`}
checked={field.required ?? false}
onCheckedChange={(checked) => updateField(index, { required: checked as boolean })}
/>
<Label htmlFor={`required-${index}`} className="cursor-pointer text-[10px] font-normal sm:text-xs">
</Label>
</div>
</CardContent>
</Card>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={addField}
className="h-7 w-full border-dashed text-xs sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4" />
</Button>
</div>
{/* 레이아웃 설정 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"></Label>
<Select
value={config.layout || "grid"}
onValueChange={(value) => handleChange("layout", value as "grid" | "card")}
>
<SelectTrigger className="h-7 text-xs sm:h-8 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="grid" className="text-xs sm:text-sm">
(Grid)
</SelectItem>
<SelectItem value="card" className="text-xs sm:text-sm">
(Card)
</SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground sm:text-xs">
{config.layout === "grid" ? "행 단위로 데이터를 표시합니다" : "각 항목을 카드로 표시합니다"}
</p>
</div>
{/* 옵션 */}
<div className="space-y-2 rounded-lg border p-3 sm:p-4">
<div className="flex items-center space-x-2">
<Checkbox
id="show-index"
checked={config.showIndex ?? true}
onCheckedChange={(checked) => handleChange("showIndex", checked as boolean)}
/>
<Label htmlFor="show-index" className="cursor-pointer text-[10px] font-normal sm:text-xs">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="allow-remove"
checked={config.allowRemove ?? false}
onCheckedChange={(checked) => handleChange("allowRemove", checked as boolean)}
/>
<Label htmlFor="allow-remove" className="cursor-pointer text-[10px] font-normal sm:text-xs">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="disabled"
checked={config.disabled ?? false}
onCheckedChange={(checked) => handleChange("disabled", checked as boolean)}
/>
<Label htmlFor="disabled" className="cursor-pointer text-[10px] font-normal sm:text-xs">
( )
</Label>
</div>
</div>
{/* 사용 예시 */}
<div className="rounded-lg bg-blue-50 p-2 text-xs sm:p-3 sm:text-sm">
<p className="mb-1 font-medium text-blue-900">💡 </p>
<ul className="space-y-1 text-[10px] text-blue-700 sm:text-xs">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
);
};
SelectedItemsDetailInputConfigPanel.displayName = "SelectedItemsDetailInputConfigPanel";