ERP-node/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPane...

664 lines
25 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { tableTypeApi } from "@/lib/api/screen";
import { TimelineSchedulerConfig } from "./types";
import { zoomLevelOptions, statusOptions } from "./config";
interface TimelineSchedulerConfigPanelProps {
config: TimelineSchedulerConfig;
onChange: (config: Partial<TimelineSchedulerConfig>) => void;
}
interface TableInfo {
tableName: string;
displayName: string;
}
interface ColumnInfo {
columnName: string;
displayName: string;
}
export function TimelineSchedulerConfigPanel({
config,
onChange,
}: TimelineSchedulerConfigPanelProps) {
// 🐛 디버깅: 받은 config 출력
console.log("🐛 [TimelineSchedulerConfigPanel] config:", {
selectedTable: config.selectedTable,
fieldMapping: config.fieldMapping,
fieldMappingKeys: config.fieldMapping ? Object.keys(config.fieldMapping) : [],
});
const [tables, setTables] = useState<TableInfo[]>([]);
const [tableColumns, setTableColumns] = useState<ColumnInfo[]>([]);
const [resourceColumns, setResourceColumns] = useState<ColumnInfo[]>([]);
const [loading, setLoading] = useState(false);
const [tableSelectOpen, setTableSelectOpen] = useState(false);
const [resourceTableSelectOpen, setResourceTableSelectOpen] = useState(false);
// 테이블 목록 로드
useEffect(() => {
const loadTables = async () => {
setLoading(true);
try {
const tableList = await tableTypeApi.getTables();
if (Array.isArray(tableList)) {
setTables(
tableList.map((t: any) => ({
tableName: t.table_name || t.tableName,
displayName: t.display_name || t.displayName || t.table_name || t.tableName,
}))
);
}
} catch (err) {
console.error("테이블 목록 로드 오류:", err);
} finally {
setLoading(false);
}
};
loadTables();
}, []);
// 스케줄 테이블 컬럼 로드
useEffect(() => {
const loadColumns = async () => {
if (!config.selectedTable) {
setTableColumns([]);
return;
}
try {
const columns = await tableTypeApi.getColumns(config.selectedTable);
if (Array.isArray(columns)) {
setTableColumns(
columns.map((col: any) => ({
columnName: col.column_name || col.columnName,
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
}))
);
}
} catch (err) {
console.error("컬럼 로드 오류:", err);
setTableColumns([]);
}
};
loadColumns();
}, [config.selectedTable]);
// 리소스 테이블 컬럼 로드
useEffect(() => {
const loadResourceColumns = async () => {
if (!config.resourceTable) {
setResourceColumns([]);
return;
}
try {
const columns = await tableTypeApi.getColumns(config.resourceTable);
if (Array.isArray(columns)) {
setResourceColumns(
columns.map((col: any) => ({
columnName: col.column_name || col.columnName,
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
}))
);
}
} catch (err) {
console.error("리소스 컬럼 로드 오류:", err);
setResourceColumns([]);
}
};
loadResourceColumns();
}, [config.resourceTable]);
// 설정 업데이트 헬퍼
const updateConfig = (updates: Partial<TimelineSchedulerConfig>) => {
onChange({ ...config, ...updates });
};
// 🆕 이전 형식(idField)과 새 형식(id) 모두 지원하는 헬퍼 함수
const getFieldMappingValue = (newKey: string, oldKey: string): string => {
const mapping = config.fieldMapping as Record<string, any> | undefined;
if (!mapping) return "";
return mapping[newKey] || mapping[oldKey] || "";
};
// 필드 매핑 업데이트 (새 형식으로 저장하고, 이전 형식 키 삭제)
const updateFieldMapping = (field: string, value: string) => {
const currentMapping = { ...config.fieldMapping } as Record<string, any>;
// 이전 형식 키 매핑
const oldKeyMap: Record<string, string> = {
id: "idField",
resourceId: "resourceIdField",
title: "titleField",
startDate: "startDateField",
endDate: "endDateField",
status: "statusField",
progress: "progressField",
color: "colorField",
};
// 새 형식으로 저장
currentMapping[field] = value;
// 이전 형식 키가 있으면 삭제
const oldKey = oldKeyMap[field];
if (oldKey && currentMapping[oldKey]) {
delete currentMapping[oldKey];
}
updateConfig({
fieldMapping: currentMapping,
});
};
// 리소스 필드 매핑 업데이트
const updateResourceFieldMapping = (field: string, value: string) => {
updateConfig({
resourceFieldMapping: {
...config.resourceFieldMapping,
id: config.resourceFieldMapping?.id || "id",
name: config.resourceFieldMapping?.name || "name",
[field]: value,
},
});
};
return (
<div className="space-y-4 p-4">
<Accordion type="multiple" defaultValue={["table", "mapping", "display"]}>
{/* 테이블 설정 */}
<AccordionItem value="table">
<AccordionTrigger className="text-sm font-medium">
</AccordionTrigger>
<AccordionContent className="space-y-3 pt-2">
{/* 스케줄 테이블 선택 */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Popover open={tableSelectOpen} onOpenChange={setTableSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableSelectOpen}
className="h-8 w-full justify-between text-xs"
disabled={loading}
>
{loading ? (
<span className="flex items-center gap-2">
<Loader2 className="h-3 w-3 animate-spin" />
...
</span>
) : config.selectedTable ? (
tables.find((t) => t.tableName === config.selectedTable)
?.displayName || config.selectedTable
) : (
"테이블 선택..."
)}
<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
filter={(value, search) => {
const lowerSearch = search.toLowerCase();
if (value.toLowerCase().includes(lowerSearch)) {
return 1;
}
return 0;
}}
>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="text-xs">
.
</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.displayName} ${table.tableName}`}
onSelect={() => {
updateConfig({ selectedTable: table.tableName });
setTableSelectOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.selectedTable === table.tableName
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{table.displayName}</span>
<span className="text-[10px] text-muted-foreground">
{table.tableName}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 리소스 테이블 선택 */}
<div className="space-y-1">
<Label className="text-xs"> (/)</Label>
<Popover
open={resourceTableSelectOpen}
onOpenChange={setResourceTableSelectOpen}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={resourceTableSelectOpen}
className="h-8 w-full justify-between text-xs"
disabled={loading}
>
{config.resourceTable ? (
tables.find((t) => t.tableName === config.resourceTable)
?.displayName || config.resourceTable
) : (
"리소스 테이블 선택..."
)}
<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
filter={(value, search) => {
const lowerSearch = search.toLowerCase();
if (value.toLowerCase().includes(lowerSearch)) {
return 1;
}
return 0;
}}
>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="text-xs">
.
</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.displayName} ${table.tableName}`}
onSelect={() => {
updateConfig({ resourceTable: table.tableName });
setResourceTableSelectOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.resourceTable === table.tableName
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{table.displayName}</span>
<span className="text-[10px] text-muted-foreground">
{table.tableName}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</AccordionContent>
</AccordionItem>
{/* 필드 매핑 */}
<AccordionItem value="mapping">
<AccordionTrigger className="text-sm font-medium">
</AccordionTrigger>
<AccordionContent className="space-y-3 pt-2">
{/* 스케줄 필드 매핑 */}
{config.selectedTable && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<div className="grid grid-cols-2 gap-2">
{/* ID 필드 */}
<div className="space-y-1">
<Label className="text-[10px]">ID</Label>
<Select
value={getFieldMappingValue("id", "idField")}
onValueChange={(v) => updateFieldMapping("id", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 리소스 ID 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"> ID</Label>
<Select
value={getFieldMappingValue("resourceId", "resourceIdField")}
onValueChange={(v) => updateFieldMapping("resourceId", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 제목 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={getFieldMappingValue("title", "titleField")}
onValueChange={(v) => updateFieldMapping("title", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 시작일 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={getFieldMappingValue("startDate", "startDateField")}
onValueChange={(v) => updateFieldMapping("startDate", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 종료일 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={getFieldMappingValue("endDate", "endDateField")}
onValueChange={(v) => updateFieldMapping("endDate", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 상태 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"> ()</Label>
<Select
value={getFieldMappingValue("status", "statusField") || "__none__"}
onValueChange={(v) => updateFieldMapping("status", v === "__none__" ? "" : v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"></SelectItem>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
)}
{/* 리소스 필드 매핑 */}
{config.resourceTable && (
<div className="space-y-2 mt-3">
<Label className="text-xs font-medium"> </Label>
<div className="grid grid-cols-2 gap-2">
{/* ID 필드 */}
<div className="space-y-1">
<Label className="text-[10px]">ID</Label>
<Select
value={config.resourceFieldMapping?.id || ""}
onValueChange={(v) => updateResourceFieldMapping("id", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{resourceColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 이름 필드 */}
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={config.resourceFieldMapping?.name || ""}
onValueChange={(v) => updateResourceFieldMapping("name", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{resourceColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
)}
</AccordionContent>
</AccordionItem>
{/* 표시 설정 */}
<AccordionItem value="display">
<AccordionTrigger className="text-sm font-medium">
</AccordionTrigger>
<AccordionContent className="space-y-3 pt-2">
{/* 기본 줌 레벨 */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
value={config.defaultZoomLevel || "day"}
onValueChange={(v) =>
updateConfig({ defaultZoomLevel: v as any })
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{zoomLevelOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 높이 */}
<div className="space-y-1">
<Label className="text-xs"> (px)</Label>
<Input
type="number"
value={config.height || 500}
onChange={(e) =>
updateConfig({ height: parseInt(e.target.value) || 500 })
}
className="h-8 text-xs"
/>
</div>
{/* 행 높이 */}
<div className="space-y-1">
<Label className="text-xs"> (px)</Label>
<Input
type="number"
value={config.rowHeight || 50}
onChange={(e) =>
updateConfig({ rowHeight: parseInt(e.target.value) || 50 })
}
className="h-8 text-xs"
/>
</div>
{/* 토글 스위치들 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.editable ?? true}
onCheckedChange={(v) => updateConfig({ editable: v })}
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.draggable ?? true}
onCheckedChange={(v) => updateConfig({ draggable: v })}
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-xs"></Label>
<Switch
checked={config.resizable ?? true}
onCheckedChange={(v) => updateConfig({ resizable: v })}
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.showTodayLine ?? true}
onCheckedChange={(v) => updateConfig({ showTodayLine: v })}
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.showProgress ?? true}
onCheckedChange={(v) => updateConfig({ showProgress: v })}
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.showToolbar ?? true}
onCheckedChange={(v) => updateConfig({ showToolbar: v })}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}
export default TimelineSchedulerConfigPanel;