2025-10-02 13:44:16 +09:00
|
|
|
"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>
|
|
|
|
|
|
|
|
|
|
{/* 페이지 목록 */}
|
2025-10-02 14:13:11 +09:00
|
|
|
<div className="flex-1 overflow-hidden">
|
|
|
|
|
<ScrollArea className="h-full p-2">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{layoutConfig.pages
|
|
|
|
|
.sort((a, b) => a.page_order - b.page_order)
|
|
|
|
|
.map((page, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={page.page_id}
|
|
|
|
|
className={`group relative cursor-pointer rounded-md border p-2 transition-all ${
|
|
|
|
|
page.page_id === currentPageId
|
|
|
|
|
? "border-primary bg-primary/10"
|
|
|
|
|
: "border-border hover:border-primary/50 hover:bg-accent/50"
|
|
|
|
|
} ${draggedIndex === index ? "opacity-50" : ""}`}
|
|
|
|
|
onClick={() => selectPage(page.page_id)}
|
|
|
|
|
onDragOver={(e) => handleDragOver(e, index)}
|
|
|
|
|
onDrop={(e) => handleDrop(e, index)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{/* 드래그 핸들 */}
|
|
|
|
|
<div
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
handleDragStart(index);
|
|
|
|
|
}}
|
|
|
|
|
onDragEnd={handleDragEnd}
|
|
|
|
|
className="text-muted-foreground cursor-grab opacity-0 transition-opacity group-hover:opacity-100 active:cursor-grabbing"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<GripVertical className="h-3 w-3" />
|
2025-10-02 13:44:16 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-10-02 14:13:11 +09:00
|
|
|
{/* 페이지 정보 */}
|
2025-10-02 13:44:16 +09:00
|
|
|
<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();
|
|
|
|
|
}}
|
2025-10-02 14:13:11 +09:00
|
|
|
className="h-6 text-xs"
|
2025-10-02 13:44:16 +09:00
|
|
|
autoFocus
|
|
|
|
|
/>
|
2025-10-02 14:13:11 +09:00
|
|
|
<Button size="sm" variant="ghost" className="h-5 w-5 p-0" onClick={handleSaveEdit}>
|
2025-10-02 13:44:16 +09:00
|
|
|
<Check className="h-3 w-3" />
|
|
|
|
|
</Button>
|
2025-10-02 14:13:11 +09:00
|
|
|
<Button size="sm" variant="ghost" className="h-5 w-5 p-0" onClick={handleCancelEdit}>
|
2025-10-02 13:44:16 +09:00
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2025-10-02 14:13:11 +09:00
|
|
|
<div className="truncate text-xs font-medium">{page.page_name}</div>
|
2025-10-02 13:44:16 +09:00
|
|
|
)}
|
2025-10-02 14:13:11 +09:00
|
|
|
<div className="text-muted-foreground text-[10px]">
|
|
|
|
|
{page.width}x{page.height}mm • {page.components.length}개
|
2025-10-02 13:44:16 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 액션 메뉴 */}
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="ghost"
|
2025-10-02 14:13:11 +09:00
|
|
|
className="h-5 w-5 p-0 opacity-0 transition-opacity group-hover:opacity-100"
|
2025-10-02 13:44:16 +09:00
|
|
|
>
|
|
|
|
|
<span className="sr-only">메뉴</span>
|
2025-10-02 14:13:11 +09:00
|
|
|
<span className="text-sm leading-none">⋮</span>
|
2025-10-02 13:44:16 +09:00
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="end">
|
2025-10-02 14:13:11 +09:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
handleStartEdit(page.page_id, page.page_name);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Edit2 className="mr-2 h-3 w-3" />
|
|
|
|
|
이름 변경
|
|
|
|
|
</DropdownMenuItem>
|
2025-10-02 13:44:16 +09:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
duplicatePage(page.page_id);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-10-02 14:13:11 +09:00
|
|
|
<Copy className="mr-2 h-3 w-3" />
|
2025-10-02 13:44:16 +09:00
|
|
|
복제
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
deletePage(page.page_id);
|
|
|
|
|
}}
|
|
|
|
|
disabled={layoutConfig.pages.length <= 1}
|
|
|
|
|
className="text-destructive focus:text-destructive"
|
|
|
|
|
>
|
2025-10-02 14:13:11 +09:00
|
|
|
<Trash2 className="mr-2 h-3 w-3" />
|
2025-10-02 13:44:16 +09:00
|
|
|
삭제
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-02 14:13:11 +09:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
</div>
|
2025-10-02 13:44:16 +09:00
|
|
|
|
|
|
|
|
{/* 푸터 */}
|
|
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
}
|