ERP-node/frontend/components/screen/config-panels/TabsConfigPanel.tsx

441 lines
16 KiB
TypeScript
Raw Normal View History

2025-11-24 17:24:47 +09:00
"use client";
import React, { useEffect, useState } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Plus,
X,
GripVertical,
ChevronDown,
ChevronRight,
Trash2,
Move,
} from "lucide-react";
2025-11-24 17:24:47 +09:00
import { cn } from "@/lib/utils";
import type { TabItem, TabInlineComponent } from "@/types/screen-management";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
2025-11-24 17:24:47 +09:00
interface TabsConfigPanelProps {
config: any;
onChange: (config: any) => void;
}
export function TabsConfigPanel({ config, onChange }: TabsConfigPanelProps) {
const [localTabs, setLocalTabs] = useState<TabItem[]>(config.tabs || []);
const [expandedTabs, setExpandedTabs] = useState<Set<string>>(new Set());
const [isUserEditing, setIsUserEditing] = useState(false);
2025-11-24 17:24:47 +09:00
useEffect(() => {
if (!isUserEditing) {
setLocalTabs(config.tabs || []);
}
}, [config.tabs, isUserEditing]);
2025-11-24 17:24:47 +09:00
// 탭 확장/축소 토글
const toggleTabExpand = (tabId: string) => {
setExpandedTabs((prev) => {
const newSet = new Set(prev);
if (newSet.has(tabId)) {
newSet.delete(tabId);
} else {
newSet.add(tabId);
}
return newSet;
});
};
2025-11-24 17:24:47 +09:00
// 탭 추가
const handleAddTab = () => {
const newTab: TabItem = {
id: `tab-${Date.now()}`,
label: `새 탭 ${localTabs.length + 1}`,
order: localTabs.length,
disabled: false,
components: [],
2025-11-24 17:24:47 +09:00
};
const updatedTabs = [...localTabs, newTab];
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
// 새 탭 자동 확장
setExpandedTabs((prev) => new Set([...prev, newTab.id]));
2025-11-24 17:24:47 +09:00
};
// 탭 제거
const handleRemoveTab = (tabId: string) => {
const updatedTabs = localTabs.filter((tab) => tab.id !== tabId);
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 라벨 변경 (입력 중)
2025-11-24 17:24:47 +09:00
const handleLabelChange = (tabId: string, label: string) => {
setIsUserEditing(true);
const updatedTabs = localTabs.map((tab) =>
tab.id === tabId ? { ...tab, label } : tab
);
2025-11-24 17:24:47 +09:00
setLocalTabs(updatedTabs);
};
// 탭 라벨 변경 완료
const handleLabelBlur = () => {
setIsUserEditing(false);
onChange({ ...config, tabs: localTabs });
2025-11-24 17:24:47 +09:00
};
// 탭 비활성화 토글
const handleDisabledToggle = (tabId: string, disabled: boolean) => {
const updatedTabs = localTabs.map((tab) =>
tab.id === tabId ? { ...tab, disabled } : tab
);
2025-11-24 17:24:47 +09:00
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 순서 변경
const handleMoveTab = (tabId: string, direction: "up" | "down") => {
const index = localTabs.findIndex((tab) => tab.id === tabId);
if (
(direction === "up" && index === 0) ||
(direction === "down" && index === localTabs.length - 1)
) {
return;
}
const newTabs = [...localTabs];
const targetIndex = direction === "up" ? index - 1 : index + 1;
[newTabs[index], newTabs[targetIndex]] = [
newTabs[targetIndex],
newTabs[index],
];
2025-11-24 17:24:47 +09:00
const updatedTabs = newTabs.map((tab, idx) => ({ ...tab, order: idx }));
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 컴포넌트 제거
const handleRemoveComponent = (tabId: string, componentId: string) => {
const updatedTabs = localTabs.map((tab) => {
if (tab.id === tabId) {
return {
...tab,
components: (tab.components || []).filter(
(comp) => comp.id !== componentId
),
};
}
return tab;
});
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 컴포넌트 위치 변경
const handleComponentPositionChange = (
tabId: string,
componentId: string,
field: "x" | "y" | "width" | "height",
value: number
) => {
const updatedTabs = localTabs.map((tab) => {
if (tab.id === tabId) {
return {
...tab,
components: (tab.components || []).map((comp) => {
if (comp.id === componentId) {
if (field === "x" || field === "y") {
return {
...comp,
position: { ...comp.position, [field]: value },
};
} else {
return {
...comp,
size: { ...comp.size, [field]: value },
};
}
}
return comp;
}),
};
}
return tab;
});
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
2025-11-24 17:24:47 +09:00
return (
<div className="space-y-6 p-4">
<div>
<h3 className="mb-4 text-sm font-semibold"> </h3>
<div className="space-y-4">
{/* 탭 방향 */}
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={config.orientation || "horizontal"}
onValueChange={(value: "horizontal" | "vertical") =>
onChange({ ...config, orientation: value })
}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="horizontal"></SelectItem>
<SelectItem value="vertical"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 탭 스타일 */}
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={config.variant || "default"}
onValueChange={(value: "default" | "pills" | "underline") =>
onChange({ ...config, variant: value })
}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"></SelectItem>
<SelectItem value="pills"></SelectItem>
<SelectItem value="underline"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 선택 상태 유지 */}
<div className="flex items-center justify-between">
<div>
<Label className="text-xs sm:text-sm"> </Label>
<p className="text-muted-foreground text-[10px] sm:text-xs">
</p>
</div>
<Switch
checked={config.persistSelection || false}
onCheckedChange={(checked) =>
onChange({ ...config, persistSelection: checked })
}
2025-11-24 17:24:47 +09:00
/>
</div>
{/* 탭 닫기 버튼 */}
<div className="flex items-center justify-between">
<div>
<Label className="text-xs sm:text-sm"> </Label>
<p className="text-muted-foreground text-[10px] sm:text-xs">
</p>
</div>
<Switch
checked={config.allowCloseable || false}
onCheckedChange={(checked) =>
onChange({ ...config, allowCloseable: checked })
}
2025-11-24 17:24:47 +09:00
/>
</div>
</div>
</div>
<div>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold"> </h3>
<Button
onClick={handleAddTab}
size="sm"
variant="outline"
className="h-8 text-xs sm:h-9 sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
{localTabs.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-muted-foreground mt-1 text-xs">
</p>
</div>
) : (
<div className="space-y-3">
{localTabs.map((tab, index) => (
<Collapsible
2025-11-24 17:24:47 +09:00
key={tab.id}
open={expandedTabs.has(tab.id)}
onOpenChange={() => toggleTabExpand(tab.id)}
2025-11-24 17:24:47 +09:00
>
<div className="rounded-lg border bg-card shadow-sm">
{/* 탭 헤더 */}
<div className="flex items-center justify-between p-3">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 cursor-grab text-muted-foreground" />
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
{expandedTabs.has(tab.id) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</CollapsibleTrigger>
<span className="text-xs font-medium">
{tab.label || `${index + 1}`}
</span>
{tab.components && tab.components.length > 0 && (
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] text-primary">
{tab.components.length}
</span>
)}
</div>
<div className="flex items-center gap-1">
<Button
onClick={() => handleMoveTab(tab.id, "up")}
disabled={index === 0}
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
>
</Button>
<Button
onClick={() => handleMoveTab(tab.id, "down")}
disabled={index === localTabs.length - 1}
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
>
</Button>
<Button
onClick={() => handleRemoveTab(tab.id)}
size="sm"
variant="ghost"
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
>
<X className="h-3 w-3" />
</Button>
</div>
2025-11-24 17:24:47 +09:00
</div>
{/* 탭 컨텐츠 */}
<CollapsibleContent>
<div className="space-y-4 border-t p-3">
{/* 탭 라벨 */}
<div>
<Label className="text-xs"> </Label>
<Input
value={tab.label}
onChange={(e) =>
handleLabelChange(tab.id, e.target.value)
}
onBlur={handleLabelBlur}
placeholder="탭 이름"
className="h-8 text-xs sm:h-9 sm:text-sm"
/>
</div>
2025-11-24 17:24:47 +09:00
{/* 비활성화 */}
<div className="flex items-center justify-between">
<Label className="text-xs"></Label>
<Switch
checked={tab.disabled || false}
onCheckedChange={(checked) =>
handleDisabledToggle(tab.id, checked)
}
/>
2025-11-24 17:24:47 +09:00
</div>
{/* 컴포넌트 목록 */}
<div>
<Label className="mb-2 block text-xs">
</Label>
{!tab.components || tab.components.length === 0 ? (
<div className="rounded-md border border-dashed bg-muted/30 p-4 text-center">
<Move className="mx-auto mb-2 h-6 w-6 text-muted-foreground" />
<p className="text-muted-foreground text-xs">
</p>
</div>
) : (
<div className="space-y-2">
{tab.components.map((comp: TabInlineComponent) => (
<div
key={comp.id}
className="flex items-center justify-between rounded-md border bg-background p-2"
>
<div className="flex-1">
<p className="text-xs font-medium">
{comp.label || comp.componentType}
</p>
<p className="text-muted-foreground text-[10px]">
{comp.componentType} | : ({comp.position?.x || 0},{" "}
{comp.position?.y || 0}) | : {comp.size?.width || 0}x
{comp.size?.height || 0}
</p>
</div>
<Button
onClick={() =>
handleRemoveComponent(tab.id, comp.id)
}
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
</CollapsibleContent>
2025-11-24 17:24:47 +09:00
</div>
</Collapsible>
2025-11-24 17:24:47 +09:00
))}
</div>
)}
</div>
{/* 사용 안내 */}
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
<h4 className="mb-1 text-xs font-semibold text-blue-900">
</h4>
<ol className="list-inside list-decimal space-y-1 text-[10px] text-blue-800">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ol>
</div>
</div>
2025-11-24 17:24:47 +09:00
);
}