ERP-node/frontend/components/report/designer/PageListPanel.tsx

241 lines
9.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { Plus, Copy, Trash2, GripVertical, Edit2, Check, X } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function PageListPanel() {
const {
layoutConfig,
currentPageId,
addPage,
deletePage,
duplicatePage,
reorderPages,
selectPage,
updatePageSettings,
} = useReportDesigner();
const [editingPageId, setEditingPageId] = useState<string | null>(null);
const [editingName, setEditingName] = useState("");
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const handleStartEdit = (pageId: string, currentName: string) => {
setEditingPageId(pageId);
setEditingName(currentName);
};
const handleSaveEdit = () => {
if (editingPageId && editingName.trim()) {
updatePageSettings(editingPageId, { page_name: editingName.trim() });
}
setEditingPageId(null);
setEditingName("");
};
const handleCancelEdit = () => {
setEditingPageId(null);
setEditingName("");
};
const handleDragStart = (index: number) => {
setDraggedIndex(index);
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggedIndex === null || draggedIndex === index) return;
// 실시간으로 순서 변경하지 않고, drop 시에만 변경
};
const handleDrop = (e: React.DragEvent, targetIndex: number) => {
e.preventDefault();
if (draggedIndex === null) return;
const sourceIndex = draggedIndex;
if (sourceIndex !== targetIndex) {
reorderPages(sourceIndex, targetIndex);
}
setDraggedIndex(null);
};
const handleDragEnd = () => {
setDraggedIndex(null);
};
return (
<div className="bg-background flex h-full w-64 flex-col border-r">
{/* 헤더 */}
<div className="flex items-center justify-between border-b p-3">
<h3 className="text-sm font-semibold"> </h3>
<Button size="sm" variant="ghost" onClick={() => addPage()}>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 페이지 목록 */}
<ScrollArea className="flex-1 p-2">
<div className="space-y-2">
{layoutConfig.pages
.sort((a, b) => a.page_order - b.page_order)
.map((page, index) => (
<Card
key={page.page_id}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
onDragEnd={handleDragEnd}
className={`group relative cursor-pointer transition-all hover:shadow-md ${
page.page_id === currentPageId
? "border-primary bg-primary/5 ring-primary/20 ring-2"
: "border-border hover:border-primary/50"
} ${draggedIndex === index ? "opacity-50" : ""}`}
onClick={() => selectPage(page.page_id)}
>
<div className="p-3">
{/* 드래그 핸들 & 페이지 정보 */}
<div className="flex items-start gap-2">
<div className="text-muted-foreground cursor-grab pt-1 opacity-0 transition-opacity group-hover:opacity-100">
<GripVertical className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
{/* 페이지 이름 편집 */}
{editingPageId === page.page_id ? (
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<Input
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveEdit();
if (e.key === "Escape") handleCancelEdit();
}}
className="h-6 text-sm"
autoFocus
/>
<Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={handleSaveEdit}>
<Check className="h-3 w-3" />
</Button>
<Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={handleCancelEdit}>
<X className="h-3 w-3" />
</Button>
</div>
) : (
<div className="flex items-center gap-2">
<span className="truncate text-sm font-medium">{page.page_name}</span>
<Button
size="sm"
variant="ghost"
className="h-5 w-5 p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
handleStartEdit(page.page_id, page.page_name);
}}
>
<Edit2 className="h-3 w-3" />
</Button>
</div>
)}
{/* 페이지 정보 */}
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
<span>
{page.width} x {page.height}mm
</span>
<span></span>
<span>{page.components.length} </span>
</div>
</div>
{/* 액션 메뉴 */}
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0 opacity-0 transition-opacity group-hover:opacity-100"
>
<span className="sr-only"></span>
<span className="text-xl leading-none"></span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
duplicatePage(page.page_id);
}}
>
<Copy className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
deletePage(page.page_id);
}}
disabled={layoutConfig.pages.length <= 1}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* 썸네일 (간단한 미리보기) */}
<div className="mt-2 aspect-[210/297] overflow-hidden rounded border bg-white">
<div className="relative h-full w-full origin-top-left scale-[0.15]">
<div
className="absolute inset-0 bg-white"
style={{
width: `${page.width * 3.7795}px`,
height: `${page.height * 3.7795}px`,
}}
>
{/* 간단한 컴포넌트 표시 */}
{page.components.slice(0, 10).map((comp) => (
<div
key={comp.id}
className="border-primary/20 bg-primary/5 absolute border"
style={{
left: `${comp.x}px`,
top: `${comp.y}px`,
width: `${comp.width}px`,
height: `${comp.height}px`,
}}
/>
))}
</div>
</div>
</div>
</div>
</Card>
))}
</div>
</ScrollArea>
{/* 푸터 */}
<div className="border-t p-2">
<Button size="sm" variant="outline" className="w-full" onClick={() => addPage()}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
);
}