docs: v2-timeline-scheduler 구현 완료 및 상태 업데이트
- v2-timeline-scheduler의 구현 상태를 체크리스트에 반영하였으며, 관련 문서화 작업을 완료하였습니다. - 각 구성 요소의 구현 완료 상태를 명시하고, 향후 작업 계획을 업데이트하였습니다. - 타임라인 스케줄러 컴포넌트를 레지스트리에 추가하여 통합하였습니다.
This commit is contained in:
@@ -0,0 +1,629 @@
|
||||
"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) {
|
||||
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 });
|
||||
};
|
||||
|
||||
// 필드 매핑 업데이트
|
||||
const updateFieldMapping = (field: string, value: string) => {
|
||||
updateConfig({
|
||||
fieldMapping: {
|
||||
...config.fieldMapping,
|
||||
[field]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 리소스 필드 매핑 업데이트
|
||||
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={config.fieldMapping?.id || ""}
|
||||
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={config.fieldMapping?.resourceId || ""}
|
||||
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={config.fieldMapping?.title || ""}
|
||||
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={config.fieldMapping?.startDate || ""}
|
||||
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={config.fieldMapping?.endDate || ""}
|
||||
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={config.fieldMapping?.status || "__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;
|
||||
Reference in New Issue
Block a user