- Added new filter and linking settings section to the V2TimelineSchedulerConfigPanel, allowing users to manage static filters and linked filters more effectively. - Introduced view mode options to switch between different display modes in the timeline scheduler. - Updated the configuration types and added new toolbar action settings to support custom actions in the timeline toolbar. - Enhanced the overall user experience by providing more flexible filtering and display options. These updates aim to improve the functionality and usability of the timeline scheduler within the ERP system, enabling better data management and visualization. Made-with: Cursor
1769 lines
81 KiB
TypeScript
1769 lines
81 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* V2 타임라인 스케줄러 설정 패널
|
|
* 토스식 단계별 UX: 스케줄 데이터 설정 -> 소스 데이터 설정 -> 리소스 설정 -> 표시 설정(접힘) -> 고급 설정(접힘)
|
|
*/
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Settings, ChevronDown, Check, ChevronsUpDown, Database, Users, Layers, Filter, Link, Zap, Trash2, Plus, GripVertical } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
|
import type { TimelineSchedulerConfig, ScheduleType, SourceDataConfig, ResourceFieldMapping, FieldMapping, ZoomLevel, ToolbarAction } from "@/lib/registry/components/v2-timeline-scheduler/types";
|
|
import { zoomLevelOptions, scheduleTypeOptions, viewModeOptions, dataSourceOptions, toolbarIconOptions } from "@/lib/registry/components/v2-timeline-scheduler/config";
|
|
|
|
interface V2TimelineSchedulerConfigPanelProps {
|
|
config: TimelineSchedulerConfig;
|
|
onChange: (config: Partial<TimelineSchedulerConfig>) => void;
|
|
}
|
|
|
|
interface TableInfo {
|
|
tableName: string;
|
|
displayName: string;
|
|
}
|
|
|
|
interface ColumnInfo {
|
|
columnName: string;
|
|
displayName: string;
|
|
}
|
|
|
|
export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigPanelProps> = ({
|
|
config,
|
|
onChange,
|
|
}) => {
|
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
|
const [sourceColumns, setSourceColumns] = useState<ColumnInfo[]>([]);
|
|
const [resourceColumns, setResourceColumns] = useState<ColumnInfo[]>([]);
|
|
const [scheduleColumns, setScheduleColumns] = useState<ColumnInfo[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [sourceTableOpen, setSourceTableOpen] = useState(false);
|
|
const [resourceTableOpen, setResourceTableOpen] = useState(false);
|
|
const [customTableOpen, setCustomTableOpen] = useState(false);
|
|
const [scheduleDataOpen, setScheduleDataOpen] = useState(true);
|
|
const [filterLinkOpen, setFilterLinkOpen] = useState(false);
|
|
const [sourceDataOpen, setSourceDataOpen] = useState(true);
|
|
const [resourceOpen, setResourceOpen] = useState(true);
|
|
const [displayOpen, setDisplayOpen] = useState(false);
|
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
const [actionsOpen, setActionsOpen] = useState(false);
|
|
const [newFilterKey, setNewFilterKey] = useState("");
|
|
const [newFilterValue, setNewFilterValue] = useState("");
|
|
const [linkedFilterTableOpen, setLinkedFilterTableOpen] = useState(false);
|
|
const [expandedActionId, setExpandedActionId] = useState<string | null>(null);
|
|
|
|
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 loadSourceColumns = async () => {
|
|
if (!config.sourceConfig?.tableName) {
|
|
setSourceColumns([]);
|
|
return;
|
|
}
|
|
try {
|
|
const columns = await tableTypeApi.getColumns(config.sourceConfig.tableName);
|
|
if (Array.isArray(columns)) {
|
|
setSourceColumns(
|
|
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);
|
|
setSourceColumns([]);
|
|
}
|
|
};
|
|
loadSourceColumns();
|
|
}, [config.sourceConfig?.tableName]);
|
|
|
|
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]);
|
|
|
|
// 커스텀 테이블 또는 schedule_mng 컬럼 로드
|
|
useEffect(() => {
|
|
const loadScheduleColumns = async () => {
|
|
const tableName = config.useCustomTable && config.customTableName
|
|
? config.customTableName
|
|
: "schedule_mng";
|
|
try {
|
|
const columns = await tableTypeApi.getColumns(tableName);
|
|
if (Array.isArray(columns)) {
|
|
setScheduleColumns(
|
|
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);
|
|
setScheduleColumns([]);
|
|
}
|
|
};
|
|
loadScheduleColumns();
|
|
}, [config.useCustomTable, config.customTableName]);
|
|
|
|
const updateConfig = (updates: Partial<TimelineSchedulerConfig>) => {
|
|
onChange({ ...config, ...updates });
|
|
};
|
|
|
|
const updateSourceConfig = (updates: Partial<SourceDataConfig>) => {
|
|
updateConfig({
|
|
sourceConfig: {
|
|
...config.sourceConfig,
|
|
...updates,
|
|
},
|
|
});
|
|
};
|
|
|
|
const updateFieldMapping = (field: string, value: string) => {
|
|
updateConfig({
|
|
fieldMapping: {
|
|
...config.fieldMapping,
|
|
id: config.fieldMapping?.id || "id",
|
|
resourceId: config.fieldMapping?.resourceId || "resource_id",
|
|
title: config.fieldMapping?.title || "title",
|
|
startDate: config.fieldMapping?.startDate || "start_date",
|
|
endDate: config.fieldMapping?.endDate || "end_date",
|
|
[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">
|
|
{/* ─── 1단계: 스케줄 데이터 테이블 설정 ─── */}
|
|
<Collapsible open={scheduleDataOpen} onOpenChange={setScheduleDataOpen}>
|
|
<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">
|
|
<Layers className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">스케줄 데이터 테이블</span>
|
|
<Badge variant="secondary" className="text-[10px] h-5">8개 필드</Badge>
|
|
</div>
|
|
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", scheduleDataOpen && "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">
|
|
<span className="text-xs text-muted-foreground truncate">스케줄 타입</span>
|
|
<Select
|
|
value={config.scheduleType || "PRODUCTION"}
|
|
onValueChange={(v) => updateConfig({ scheduleType: v as ScheduleType })}
|
|
>
|
|
<SelectTrigger className="h-7 w-[140px] text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleTypeOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 뷰 모드 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-xs text-muted-foreground truncate">표시 모드</p>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
{viewModeOptions.find((o) => o.value === (config.viewMode || "resource"))?.description}
|
|
</p>
|
|
</div>
|
|
<Select
|
|
value={config.viewMode || "resource"}
|
|
onValueChange={(v) => updateConfig({ viewMode: v as any })}
|
|
>
|
|
<SelectTrigger className="h-7 w-[140px] text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{viewModeOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 커스텀 테이블 사용 여부 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">커스텀 테이블 사용</p>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">OFF: schedule_mng 사용</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.useCustomTable ?? false}
|
|
onCheckedChange={(v) => updateConfig({ useCustomTable: v })}
|
|
/>
|
|
</div>
|
|
|
|
{/* 커스텀 테이블 선택 (Combobox) */}
|
|
{config.useCustomTable && (
|
|
<div className="space-y-1">
|
|
<span className="text-xs text-muted-foreground">커스텀 테이블명</span>
|
|
<Popover open={customTableOpen} onOpenChange={setCustomTableOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={customTableOpen}
|
|
className="h-8 w-full justify-between text-xs"
|
|
disabled={loading}
|
|
>
|
|
{config.customTableName
|
|
? tables.find((t) => t.tableName === config.customTableName)?.displayName ||
|
|
config.customTableName
|
|
: "커스텀 테이블 선택..."}
|
|
<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) => {
|
|
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 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({ customTableName: table.tableName, selectedTable: table.tableName });
|
|
setCustomTableOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.customTableName === table.tableName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span>{table.displayName}</span>
|
|
<span className="text-muted-foreground text-[10px]">{table.tableName}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
)}
|
|
|
|
{/* 디자인 모드 표시용 테이블명 (selectedTable) */}
|
|
{!config.useCustomTable && (
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">디자인 모드 표시 테이블</span>
|
|
<Input
|
|
value={config.selectedTable || "schedule_mng"}
|
|
onChange={(e) => updateConfig({ selectedTable: e.target.value })}
|
|
placeholder="schedule_mng"
|
|
className="h-7 w-[140px] text-xs"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 스케줄 필드 매핑 */}
|
|
<div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1">
|
|
<p className="text-xs font-medium text-primary truncate">스케줄 필드 매핑</p>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">ID 필드 *</span>
|
|
<Select
|
|
value={config.fieldMapping?.id || "schedule_id"}
|
|
onValueChange={(v) => updateFieldMapping("id", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">리소스 ID 필드 *</span>
|
|
<Select
|
|
value={config.fieldMapping?.resourceId || "resource_id"}
|
|
onValueChange={(v) => updateFieldMapping("resourceId", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">제목 필드 *</span>
|
|
<Select
|
|
value={config.fieldMapping?.title || "schedule_name"}
|
|
onValueChange={(v) => updateFieldMapping("title", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">시작일 필드 *</span>
|
|
<Select
|
|
value={config.fieldMapping?.startDate || "start_date"}
|
|
onValueChange={(v) => updateFieldMapping("startDate", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">종료일 필드 *</span>
|
|
<Select
|
|
value={config.fieldMapping?.endDate || "end_date"}
|
|
onValueChange={(v) => updateFieldMapping("endDate", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">상태 필드</span>
|
|
<Select
|
|
value={config.fieldMapping?.status || ""}
|
|
onValueChange={(v) => updateFieldMapping("status", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">진행률 필드</span>
|
|
<Select
|
|
value={config.fieldMapping?.progress || ""}
|
|
onValueChange={(v) => updateFieldMapping("progress", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">색상 필드</span>
|
|
<Select
|
|
value={config.fieldMapping?.color || ""}
|
|
onValueChange={(v) => updateFieldMapping("color", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{scheduleColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* ─── 필터 & 연동 설정 ─── */}
|
|
<Collapsible open={filterLinkOpen} onOpenChange={setFilterLinkOpen}>
|
|
<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">
|
|
<Filter className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">필터 & 연동 설정</span>
|
|
<Badge variant="secondary" className="text-[10px] h-5">
|
|
{Object.keys(config.staticFilters || {}).length + (config.linkedFilter ? 1 : 0)}개
|
|
</Badge>
|
|
</div>
|
|
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", filterLinkOpen && "rotate-180")} />
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="rounded-b-lg border border-t-0 p-4 space-y-4">
|
|
{/* 정적 필터 */}
|
|
<div className="space-y-2">
|
|
<p className="text-xs font-medium text-primary">정적 필터 (staticFilters)</p>
|
|
<p className="text-[10px] text-muted-foreground">데이터 조회 시 항상 적용되는 고정 필터 조건</p>
|
|
|
|
{Object.entries(config.staticFilters || {}).map(([key, value]) => (
|
|
<div key={key} className="flex items-center gap-2">
|
|
<Input value={key} disabled className="h-7 flex-1 text-xs bg-muted/30" />
|
|
<span className="text-xs text-muted-foreground">=</span>
|
|
<Input value={value} disabled className="h-7 flex-1 text-xs bg-muted/30" />
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const updated = { ...config.staticFilters };
|
|
delete updated[key];
|
|
updateConfig({ staticFilters: Object.keys(updated).length > 0 ? updated : undefined });
|
|
}}
|
|
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
value={newFilterKey}
|
|
onChange={(e) => setNewFilterKey(e.target.value)}
|
|
placeholder="필드명 (예: product_type)"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
<span className="text-xs text-muted-foreground">=</span>
|
|
<Input
|
|
value={newFilterValue}
|
|
onChange={(e) => setNewFilterValue(e.target.value)}
|
|
placeholder="값 (예: 완제품)"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
if (!newFilterKey.trim()) return;
|
|
updateConfig({
|
|
staticFilters: {
|
|
...(config.staticFilters || {}),
|
|
[newFilterKey.trim()]: newFilterValue.trim(),
|
|
},
|
|
});
|
|
setNewFilterKey("");
|
|
setNewFilterValue("");
|
|
}}
|
|
disabled={!newFilterKey.trim()}
|
|
className="h-7 w-7 p-0"
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 구분선 */}
|
|
<div className="border-t" />
|
|
|
|
{/* 연결 필터 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs font-medium text-primary flex items-center gap-1">
|
|
<Link className="h-3 w-3" />
|
|
연결 필터 (linkedFilter)
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">다른 컴포넌트 선택에 따라 데이터를 필터링</p>
|
|
</div>
|
|
<Switch
|
|
checked={!!config.linkedFilter}
|
|
onCheckedChange={(v) => {
|
|
if (v) {
|
|
updateConfig({
|
|
linkedFilter: {
|
|
sourceField: "",
|
|
targetField: "",
|
|
showEmptyWhenNoSelection: true,
|
|
emptyMessage: "좌측 목록에서 항목을 선택하세요",
|
|
},
|
|
});
|
|
} else {
|
|
updateConfig({ linkedFilter: undefined });
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{config.linkedFilter && (
|
|
<div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1">
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<span className="text-xs text-muted-foreground">소스 테이블명</span>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">선택 이벤트의 tableName 매칭</p>
|
|
</div>
|
|
<Popover open={linkedFilterTableOpen} onOpenChange={setLinkedFilterTableOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
className="h-7 w-[140px] justify-between text-xs"
|
|
disabled={loading}
|
|
>
|
|
{config.linkedFilter.sourceTableName || "선택..."}
|
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 w-[200px]" align="end">
|
|
<Command filter={(value, search) => value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0}>
|
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs p-2">없음</CommandEmpty>
|
|
<CommandGroup>
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.displayName} ${table.tableName}`}
|
|
onSelect={() => {
|
|
updateConfig({
|
|
linkedFilter: { ...config.linkedFilter!, sourceTableName: table.tableName },
|
|
});
|
|
setLinkedFilterTableOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check className={cn("mr-2 h-3 w-3", config.linkedFilter?.sourceTableName === table.tableName ? "opacity-100" : "opacity-0")} />
|
|
{table.displayName}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground">소스 필드 (sourceField) *</span>
|
|
<Input
|
|
value={config.linkedFilter.sourceField || ""}
|
|
onChange={(e) => updateConfig({ linkedFilter: { ...config.linkedFilter!, sourceField: e.target.value } })}
|
|
placeholder="예: part_code"
|
|
className="h-7 w-[140px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground">타겟 필드 (targetField) *</span>
|
|
<Input
|
|
value={config.linkedFilter.targetField || ""}
|
|
onChange={(e) => updateConfig({ linkedFilter: { ...config.linkedFilter!, targetField: e.target.value } })}
|
|
placeholder="예: item_code"
|
|
className="h-7 w-[140px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground">빈 상태 메시지</span>
|
|
<Input
|
|
value={config.linkedFilter.emptyMessage || ""}
|
|
onChange={(e) => updateConfig({ linkedFilter: { ...config.linkedFilter!, emptyMessage: e.target.value } })}
|
|
placeholder="선택 안내 문구"
|
|
className="h-7 w-[180px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground">선택 없을 때 빈 화면</span>
|
|
<Switch
|
|
checked={config.linkedFilter.showEmptyWhenNoSelection ?? true}
|
|
onCheckedChange={(v) => updateConfig({ linkedFilter: { ...config.linkedFilter!, showEmptyWhenNoSelection: v } })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* ─── 2단계: 소스 데이터 설정 ─── */}
|
|
<Collapsible open={sourceDataOpen} onOpenChange={setSourceDataOpen}>
|
|
<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">
|
|
<Database className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">소스 데이터 설정</span>
|
|
<Badge variant="secondary" className="text-[10px] h-5">
|
|
{config.sourceConfig?.tableName ? "4개 필드" : "미설정"}
|
|
</Badge>
|
|
</div>
|
|
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", sourceDataOpen && "rotate-180")} />
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
|
{/* 소스 테이블 Combobox */}
|
|
<div className="space-y-1">
|
|
<span className="text-xs text-muted-foreground truncate">소스 테이블 (수주/작업요청 등)</span>
|
|
<Popover open={sourceTableOpen} onOpenChange={setSourceTableOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={sourceTableOpen}
|
|
className="h-8 w-full justify-between text-xs"
|
|
disabled={loading}
|
|
>
|
|
{config.sourceConfig?.tableName
|
|
? tables.find((t) => t.tableName === config.sourceConfig?.tableName)?.displayName ||
|
|
config.sourceConfig.tableName
|
|
: "소스 테이블 선택..."}
|
|
<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) => {
|
|
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 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={() => {
|
|
updateSourceConfig({ tableName: table.tableName });
|
|
setSourceTableOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.sourceConfig?.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span>{table.displayName}</span>
|
|
<span className="text-muted-foreground text-[10px]">{table.tableName}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 소스 필드 매핑 (테이블 선택 시) */}
|
|
{config.sourceConfig?.tableName && (
|
|
<div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1">
|
|
<p className="text-xs font-medium text-primary truncate">필드 매핑</p>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<span className="text-xs text-muted-foreground truncate">기준일 (마감일/납기일) *</span>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">스케줄 종료일로 사용돼요</p>
|
|
</div>
|
|
<Select
|
|
value={config.sourceConfig?.dueDateField || ""}
|
|
onValueChange={(v) => updateSourceConfig({ dueDateField: v })}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="필수 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">수량 필드</span>
|
|
<Select
|
|
value={config.sourceConfig?.quantityField || ""}
|
|
onValueChange={(v) => updateSourceConfig({ quantityField: v })}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">그룹 필드 (품목코드)</span>
|
|
<Select
|
|
value={config.sourceConfig?.groupByField || ""}
|
|
onValueChange={(v) => updateSourceConfig({ groupByField: v })}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">그룹명 필드 (품목명)</span>
|
|
<Select
|
|
value={config.sourceConfig?.groupNameField || ""}
|
|
onValueChange={(v) => updateSourceConfig({ groupNameField: v })}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* ─── 3단계: 리소스 설정 ─── */}
|
|
<Collapsible open={resourceOpen} onOpenChange={setResourceOpen}>
|
|
<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">
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">리소스 설정 (설비/작업자)</span>
|
|
<Badge variant="secondary" className="text-[10px] h-5">
|
|
{config.resourceTable ? "3개 필드" : "미설정"}
|
|
</Badge>
|
|
</div>
|
|
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", resourceOpen && "rotate-180")} />
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
|
{/* 리소스 테이블 Combobox */}
|
|
<div className="space-y-1">
|
|
<span className="text-xs text-muted-foreground truncate">리소스 테이블</span>
|
|
<Popover open={resourceTableOpen} onOpenChange={setResourceTableOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={resourceTableOpen}
|
|
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) => {
|
|
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 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 });
|
|
setResourceTableOpen(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-muted-foreground text-[10px]">{table.tableName}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 리소스 필드 매핑 */}
|
|
{config.resourceTable && (
|
|
<div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1">
|
|
<p className="text-xs font-medium text-primary truncate">리소스 필드</p>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">ID 필드</span>
|
|
<Select
|
|
value={config.resourceFieldMapping?.id || ""}
|
|
onValueChange={(v) => updateResourceFieldMapping("id", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{resourceColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">이름 필드</span>
|
|
<Select
|
|
value={config.resourceFieldMapping?.name || ""}
|
|
onValueChange={(v) => updateResourceFieldMapping("name", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{resourceColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">그룹 필드</span>
|
|
<Select
|
|
value={config.resourceFieldMapping?.group || ""}
|
|
onValueChange={(v) => updateResourceFieldMapping("group", v)}
|
|
>
|
|
<SelectTrigger className="h-7 w-[130px] text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{resourceColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* ─── 4단계: 표시 설정 (Collapsible) ─── */}
|
|
<Collapsible open={displayOpen} onOpenChange={setDisplayOpen}>
|
|
<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">표시 설정</span>
|
|
<Badge variant="secondary" className="text-[10px] h-5">18개</Badge>
|
|
</div>
|
|
<ChevronDown
|
|
className={cn(
|
|
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
|
displayOpen && "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">
|
|
<span className="text-xs text-muted-foreground truncate">기본 줌 레벨</span>
|
|
<Select
|
|
value={config.defaultZoomLevel || "day"}
|
|
onValueChange={(v) => updateConfig({ defaultZoomLevel: v as ZoomLevel })}
|
|
>
|
|
<SelectTrigger className="h-7 w-[100px] text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{zoomLevelOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 초기 표시 날짜 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<span className="text-xs text-muted-foreground truncate">초기 표시 날짜</span>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">비워두면 오늘 기준</p>
|
|
</div>
|
|
<Input
|
|
type="date"
|
|
value={config.initialDate || ""}
|
|
onChange={(e) => updateConfig({ initialDate: e.target.value || undefined })}
|
|
className="h-7 w-[140px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 높이 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">높이 (px)</span>
|
|
<Input
|
|
type="number"
|
|
value={config.height || 500}
|
|
onChange={(e) => updateConfig({ height: parseInt(e.target.value) || 500 })}
|
|
className="h-7 w-[100px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 최대 높이 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">최대 높이 (px)</span>
|
|
<Input
|
|
type="number"
|
|
value={typeof config.maxHeight === "number" ? config.maxHeight : ""}
|
|
onChange={(e) => {
|
|
const val = e.target.value ? parseInt(e.target.value) : undefined;
|
|
updateConfig({ maxHeight: val });
|
|
}}
|
|
placeholder="제한 없음"
|
|
className="h-7 w-[100px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 행 높이 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">행 높이 (px)</span>
|
|
<Input
|
|
type="number"
|
|
value={config.rowHeight || 50}
|
|
onChange={(e) => updateConfig({ rowHeight: parseInt(e.target.value) || 50 })}
|
|
className="h-7 w-[100px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 헤더 높이 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">헤더 높이 (px)</span>
|
|
<Input
|
|
type="number"
|
|
value={config.headerHeight || 60}
|
|
onChange={(e) => updateConfig({ headerHeight: parseInt(e.target.value) || 60 })}
|
|
className="h-7 w-[100px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 리소스 컬럼 너비 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-xs text-muted-foreground truncate">리소스 컬럼 너비 (px)</span>
|
|
<Input
|
|
type="number"
|
|
value={config.resourceColumnWidth || 150}
|
|
onChange={(e) => updateConfig({ resourceColumnWidth: parseInt(e.target.value) || 150 })}
|
|
className="h-7 w-[100px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 셀 너비 (줌 레벨별) */}
|
|
<div className="space-y-2 pt-1">
|
|
<span className="text-xs text-muted-foreground truncate">셀 너비 (줌 레벨별, px)</span>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">일</span>
|
|
<Input
|
|
type="number"
|
|
value={config.cellWidth?.day || 60}
|
|
onChange={(e) => updateConfig({ cellWidth: { ...config.cellWidth, day: parseInt(e.target.value) || 60 } })}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">주</span>
|
|
<Input
|
|
type="number"
|
|
value={config.cellWidth?.week || 120}
|
|
onChange={(e) => updateConfig({ cellWidth: { ...config.cellWidth, week: parseInt(e.target.value) || 120 } })}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">월</span>
|
|
<Input
|
|
type="number"
|
|
value={config.cellWidth?.month || 40}
|
|
onChange={(e) => updateConfig({ cellWidth: { ...config.cellWidth, month: parseInt(e.target.value) || 40 } })}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Switch 토글들 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">편집 가능</p>
|
|
<p className="text-[11px] text-muted-foreground">스케줄을 직접 수정할 수 있어요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.editable ?? true}
|
|
onCheckedChange={(v) => updateConfig({ editable: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">드래그 이동</p>
|
|
<p className="text-[11px] text-muted-foreground">스케줄을 드래그해서 날짜를 변경해요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.draggable ?? true}
|
|
onCheckedChange={(v) => updateConfig({ draggable: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">리사이즈</p>
|
|
<p className="text-[11px] text-muted-foreground">스케줄의 기간을 늘이거나 줄여요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.resizable ?? true}
|
|
onCheckedChange={(v) => updateConfig({ resizable: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">오늘 표시선</p>
|
|
<p className="text-[11px] text-muted-foreground">오늘 날짜에 빨간 세로선을 표시해요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showTodayLine ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showTodayLine: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">진행률 표시</p>
|
|
<p className="text-[11px] text-muted-foreground">각 스케줄의 진행률 바를 보여줘요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showProgress ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showProgress: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">충돌 표시</p>
|
|
<p className="text-[11px] text-muted-foreground">스케줄 겹침 시 경고를 표시해요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showConflicts ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showConflicts: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">툴바 표시</p>
|
|
<p className="text-[11px] text-muted-foreground">상단의 네비게이션/줌 컨트롤을 보여줘요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showToolbar ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showToolbar: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">줌 컨트롤 표시</p>
|
|
<p className="text-[11px] text-muted-foreground">줌 레벨 변경 버튼을 보여줘요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showZoomControls ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showZoomControls: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">네비게이션 표시</p>
|
|
<p className="text-[11px] text-muted-foreground">이전/다음/오늘 버튼을 보여줘요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showNavigation ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showNavigation: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">추가 버튼 표시</p>
|
|
<p className="text-[11px] text-muted-foreground">스케줄 추가 버튼을 보여줘요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showAddButton ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showAddButton: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">범례 표시</p>
|
|
<p className="text-[11px] text-muted-foreground">상태별 색상 범례를 보여줘요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.showLegend ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showLegend: v })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* ─── 5단계: 고급 설정 - 상태별 색상 (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">상태별 색상</span>
|
|
<Badge variant="secondary" className="text-[10px] h-5">5개</Badge>
|
|
</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">
|
|
{[
|
|
{ key: "planned", label: "계획됨", defaultColor: "#3b82f6" },
|
|
{ key: "in_progress", label: "진행중", defaultColor: "#f59e0b" },
|
|
{ key: "completed", label: "완료", defaultColor: "#10b981" },
|
|
{ key: "delayed", label: "지연", defaultColor: "#ef4444" },
|
|
{ key: "cancelled", label: "취소", defaultColor: "#6b7280" },
|
|
].map((status) => (
|
|
<div key={status.key} className="flex items-center justify-between py-1">
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className="h-3 w-3 rounded-full"
|
|
style={{
|
|
backgroundColor:
|
|
(config.statusColors as any)?.[status.key] || status.defaultColor,
|
|
}}
|
|
/>
|
|
<span className="text-xs text-muted-foreground">{status.label}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<input
|
|
type="color"
|
|
value={(config.statusColors as any)?.[status.key] || status.defaultColor}
|
|
onChange={(e) =>
|
|
updateConfig({
|
|
statusColors: {
|
|
...config.statusColors,
|
|
[status.key]: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
className="h-7 w-7 cursor-pointer rounded border"
|
|
/>
|
|
<Input
|
|
value={(config.statusColors as any)?.[status.key] || status.defaultColor}
|
|
onChange={(e) =>
|
|
updateConfig({
|
|
statusColors: {
|
|
...config.statusColors,
|
|
[status.key]: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
className="h-7 w-[80px] text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
{/* ─── 6단계: 툴바 액션 설정 ─── */}
|
|
<Collapsible open={actionsOpen} onOpenChange={setActionsOpen}>
|
|
<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">
|
|
<Zap className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">툴바 액션</span>
|
|
<Badge variant="secondary" className="text-[10px] h-5">
|
|
{(config.toolbarActions || []).length}개
|
|
</Badge>
|
|
</div>
|
|
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", actionsOpen && "rotate-180")} />
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
|
<p className="text-[10px] text-muted-foreground">
|
|
툴바에 커스텀 버튼을 추가하여 API 호출 (미리보기 → 확인 → 적용) 워크플로우를 구성해요
|
|
</p>
|
|
|
|
{/* 기존 액션 목록 */}
|
|
{(config.toolbarActions || []).map((action, index) => (
|
|
<Collapsible
|
|
key={action.id}
|
|
open={expandedActionId === action.id}
|
|
onOpenChange={(open) => setExpandedActionId(open ? action.id : null)}
|
|
>
|
|
<div className="rounded-lg border">
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex w-full items-center justify-between px-3 py-2 text-left hover:bg-muted/30"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<GripVertical className="h-3 w-3 text-muted-foreground/50" />
|
|
<div className={cn("h-3 w-3 rounded-sm", action.color?.split(" ")[0] || "bg-primary")} />
|
|
<span className="text-xs font-medium">{action.label || "새 액션"}</span>
|
|
<Badge variant="outline" className="text-[9px] h-4">
|
|
{action.dataSource === "linkedSelection" ? "연결선택" : "스케줄"}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
const updated = (config.toolbarActions || []).filter((_, i) => i !== index);
|
|
updateConfig({ toolbarActions: updated.length > 0 ? updated : undefined });
|
|
}}
|
|
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
<ChevronDown className={cn("h-3 w-3 text-muted-foreground transition-transform", expandedActionId === action.id && "rotate-180")} />
|
|
</div>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="border-t px-3 py-3 space-y-2.5">
|
|
{/* 기본 설정 */}
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">버튼명</span>
|
|
<Input
|
|
value={action.label}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], label: e.target.value };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="w-[110px]">
|
|
<span className="text-[10px] text-muted-foreground">아이콘</span>
|
|
<Select
|
|
value={action.icon || "Zap"}
|
|
onValueChange={(v) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], icon: v as any };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{toolbarIconOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-[10px] text-muted-foreground">버튼 색상 (Tailwind 클래스)</span>
|
|
<Input
|
|
value={action.color || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], color: e.target.value };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="예: bg-emerald-600 hover:bg-emerald-700"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* API 설정 */}
|
|
<div className="border-t pt-2">
|
|
<p className="text-[10px] font-medium text-primary mb-1.5">API 설정</p>
|
|
<div className="space-y-1.5">
|
|
<div>
|
|
<span className="text-[10px] text-muted-foreground">미리보기 API *</span>
|
|
<Input
|
|
value={action.previewApi}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], previewApi: e.target.value };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="/production/generate-schedule/preview"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<span className="text-[10px] text-muted-foreground">적용 API *</span>
|
|
<Input
|
|
value={action.applyApi}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], applyApi: e.target.value };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="/production/generate-schedule"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 다이얼로그 설정 */}
|
|
<div className="border-t pt-2">
|
|
<p className="text-[10px] font-medium text-primary mb-1.5">다이얼로그</p>
|
|
<div className="space-y-1.5">
|
|
<div>
|
|
<span className="text-[10px] text-muted-foreground">제목</span>
|
|
<Input
|
|
value={action.dialogTitle || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], dialogTitle: e.target.value };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="자동 생성"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<span className="text-[10px] text-muted-foreground">설명</span>
|
|
<Input
|
|
value={action.dialogDescription || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], dialogDescription: e.target.value };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="미리보기 후 확인하여 적용합니다"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 데이터 소스 설정 */}
|
|
<div className="border-t pt-2">
|
|
<p className="text-[10px] font-medium text-primary mb-1.5">데이터 소스</p>
|
|
<div className="space-y-1.5">
|
|
<div>
|
|
<span className="text-[10px] text-muted-foreground">데이터 소스 유형 *</span>
|
|
<Select
|
|
value={action.dataSource}
|
|
onValueChange={(v) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], dataSource: v as any };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{dataSourceOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
<div>
|
|
<span>{opt.label}</span>
|
|
<span className="ml-1 text-[10px] text-muted-foreground">({opt.description})</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{action.dataSource === "linkedSelection" && (
|
|
<div className="ml-2 border-l-2 border-blue-200 pl-2 space-y-1.5">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">그룹 필드</span>
|
|
<Input
|
|
value={action.payloadConfig?.groupByField || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, groupByField: e.target.value || undefined } };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="linkedFilter.sourceField 사용"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">수량 필드</span>
|
|
<Input
|
|
value={action.payloadConfig?.quantityField || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, quantityField: e.target.value || undefined } };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="balance_qty"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">기준일 필드</span>
|
|
<Input
|
|
value={action.payloadConfig?.dueDateField || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, dueDateField: e.target.value || undefined } };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="due_date"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">표시명 필드</span>
|
|
<Input
|
|
value={action.payloadConfig?.nameField || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, nameField: e.target.value || undefined } };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="part_name"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{action.dataSource === "currentSchedules" && (
|
|
<div className="ml-2 border-l-2 border-amber-200 pl-2 space-y-1.5">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">필터 필드</span>
|
|
<Input
|
|
value={action.payloadConfig?.scheduleFilterField || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, scheduleFilterField: e.target.value || undefined } };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="product_type"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<span className="text-[10px] text-muted-foreground">필터 값</span>
|
|
<Input
|
|
value={action.payloadConfig?.scheduleFilterValue || ""}
|
|
onChange={(e) => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, scheduleFilterValue: e.target.value || undefined } };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
placeholder="완제품"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 표시 조건 */}
|
|
<div className="border-t pt-2">
|
|
<p className="text-[10px] font-medium text-primary mb-1.5">표시 조건 (showWhen)</p>
|
|
<p className="text-[9px] text-muted-foreground mb-1">staticFilters 값과 비교하여 일치할 때만 버튼 표시</p>
|
|
{Object.entries(action.showWhen || {}).map(([key, value]) => (
|
|
<div key={key} className="flex items-center gap-1 mb-1">
|
|
<Input value={key} disabled className="h-6 flex-1 text-[10px] bg-muted/30" />
|
|
<span className="text-[10px]">=</span>
|
|
<Input value={value} disabled className="h-6 flex-1 text-[10px] bg-muted/30" />
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const updated = [...(config.toolbarActions || [])];
|
|
const newShowWhen = { ...updated[index].showWhen };
|
|
delete newShowWhen[key];
|
|
updated[index] = { ...updated[index], showWhen: Object.keys(newShowWhen).length > 0 ? newShowWhen : undefined };
|
|
updateConfig({ toolbarActions: updated });
|
|
}}
|
|
className="h-6 w-6 p-0 text-destructive"
|
|
>
|
|
<Trash2 className="h-2.5 w-2.5" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<div className="flex items-center gap-1">
|
|
<Input
|
|
id={`showWhen-key-${index}`}
|
|
placeholder="필드명"
|
|
className="h-6 flex-1 text-[10px]"
|
|
/>
|
|
<span className="text-[10px]">=</span>
|
|
<Input
|
|
id={`showWhen-val-${index}`}
|
|
placeholder="값"
|
|
className="h-6 flex-1 text-[10px]"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const keyEl = document.getElementById(`showWhen-key-${index}`) as HTMLInputElement;
|
|
const valEl = document.getElementById(`showWhen-val-${index}`) as HTMLInputElement;
|
|
if (!keyEl?.value?.trim()) return;
|
|
const updated = [...(config.toolbarActions || [])];
|
|
updated[index] = {
|
|
...updated[index],
|
|
showWhen: { ...(updated[index].showWhen || {}), [keyEl.value.trim()]: valEl?.value?.trim() || "" },
|
|
};
|
|
updateConfig({ toolbarActions: updated });
|
|
keyEl.value = "";
|
|
if (valEl) valEl.value = "";
|
|
}}
|
|
className="h-6 w-6 p-0"
|
|
>
|
|
<Plus className="h-2.5 w-2.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</div>
|
|
</Collapsible>
|
|
))}
|
|
|
|
{/* 액션 추가 버튼 */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const newAction: ToolbarAction = {
|
|
id: `action_${Date.now()}`,
|
|
label: "새 액션",
|
|
icon: "Zap",
|
|
color: "bg-primary hover:bg-primary/90",
|
|
previewApi: "",
|
|
applyApi: "",
|
|
dataSource: "linkedSelection",
|
|
};
|
|
updateConfig({
|
|
toolbarActions: [...(config.toolbarActions || []), newAction],
|
|
});
|
|
setExpandedActionId(newAction.id);
|
|
}}
|
|
className="w-full h-8 text-xs gap-1"
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
액션 추가
|
|
</Button>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
V2TimelineSchedulerConfigPanel.displayName = "V2TimelineSchedulerConfigPanel";
|
|
|
|
export default V2TimelineSchedulerConfigPanel;
|