페이지 목록 디자인 변경
This commit is contained in:
parent
c23d372bcd
commit
27e33e27d1
|
|
@ -86,34 +86,40 @@ export function PageListPanel() {
|
|||
</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 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" />
|
||||
</div>
|
||||
|
||||
{/* 페이지 정보 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* 페이지 이름 편집 */}
|
||||
{editingPageId === page.page_id ? (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
|
|
@ -123,40 +129,21 @@ export function PageListPanel() {
|
|||
if (e.key === "Enter") handleSaveEdit();
|
||||
if (e.key === "Escape") handleCancelEdit();
|
||||
}}
|
||||
className="h-6 text-sm"
|
||||
className="h-6 text-xs"
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={handleSaveEdit}>
|
||||
<Button size="sm" variant="ghost" className="h-5 w-5 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}>
|
||||
<Button size="sm" variant="ghost" className="h-5 w-5 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="truncate text-xs font-medium">{page.page_name}</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 className="text-muted-foreground text-[10px]">
|
||||
{page.width}x{page.height}mm • {page.components.length}개
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -166,20 +153,29 @@ export function PageListPanel() {
|
|||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="h-5 w-5 p-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<span className="sr-only">메뉴</span>
|
||||
<span className="text-xl leading-none">⋮</span>
|
||||
<span className="text-sm leading-none">⋮</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStartEdit(page.page_id, page.page_name);
|
||||
}}
|
||||
>
|
||||
<Edit2 className="mr-2 h-3 w-3" />
|
||||
이름 변경
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
duplicatePage(page.page_id);
|
||||
}}
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
<Copy className="mr-2 h-3 w-3" />
|
||||
복제
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
|
|
@ -190,44 +186,17 @@ export function PageListPanel() {
|
|||
disabled={layoutConfig.pages.length <= 1}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<Trash2 className="mr-2 h-3 w-3" />
|
||||
삭제
|
||||
</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>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="border-t p-2">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useCallback, ReactNode, useEffect } from "react";
|
||||
import { createContext, useContext, useState, useCallback, ReactNode, useEffect, useRef } from "react";
|
||||
import { ComponentConfig, ReportDetail, ReportLayout, ReportPage, ReportLayoutConfig } from "@/types/report";
|
||||
import { reportApi } from "@/lib/api/reportApi";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
|
@ -165,23 +165,48 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
|||
// 현재 페이지의 컴포넌트 (읽기 전용)
|
||||
const components = currentPage?.components || [];
|
||||
|
||||
// currentPageId를 ref로 저장하여 클로저 문제 해결
|
||||
const currentPageIdRef = useRef<string | null>(currentPageId);
|
||||
useEffect(() => {
|
||||
currentPageIdRef.current = currentPageId;
|
||||
}, [currentPageId]);
|
||||
|
||||
// 현재 페이지의 컴포넌트를 업데이트하는 헬퍼 함수
|
||||
const setComponents = useCallback(
|
||||
(updater: ComponentConfig[] | ((prev: ComponentConfig[]) => ComponentConfig[])) => {
|
||||
if (!currentPageId) return;
|
||||
setLayoutConfig((prev) => {
|
||||
const pageId = currentPageIdRef.current;
|
||||
if (!pageId) {
|
||||
console.error("❌ currentPageId가 없음");
|
||||
return prev;
|
||||
}
|
||||
|
||||
setLayoutConfig((prev) => ({
|
||||
pages: prev.pages.map((page) =>
|
||||
page.page_id === currentPageId
|
||||
? {
|
||||
...page,
|
||||
components: typeof updater === "function" ? updater(page.components) : updater,
|
||||
}
|
||||
: page,
|
||||
),
|
||||
}));
|
||||
// 현재 선택된 페이지 찾기
|
||||
const currentPageIndex = prev.pages.findIndex((p) => p.page_id === pageId);
|
||||
if (currentPageIndex === -1) {
|
||||
console.error("❌ 페이지를 찾을 수 없음:", pageId);
|
||||
return prev;
|
||||
}
|
||||
|
||||
const currentPageData = prev.pages[currentPageIndex];
|
||||
const newComponents = typeof updater === "function" ? updater(currentPageData.components) : updater;
|
||||
|
||||
const newPages = [...prev.pages];
|
||||
newPages[currentPageIndex] = {
|
||||
...currentPageData,
|
||||
components: newComponents,
|
||||
};
|
||||
|
||||
console.log("✅ 컴포넌트 업데이트:", {
|
||||
pageId,
|
||||
before: currentPageData.components.length,
|
||||
after: newComponents.length,
|
||||
});
|
||||
|
||||
return { pages: newPages };
|
||||
});
|
||||
},
|
||||
[currentPageId],
|
||||
[], // ref를 사용하므로 의존성 배열 비움
|
||||
);
|
||||
|
||||
// 레이아웃 도구 설정
|
||||
|
|
|
|||
Loading…
Reference in New Issue