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

424 lines
20 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import { Loader2, Trash2, Upload, ChevronRight, FileText, Maximize2, Space, Droplet } from "lucide-react";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { reportApi } from "@/lib/api/reportApi";
import { useToast } from "@/hooks/use-toast";
export function PageSettingsTab() {
const { currentPage, currentPageId, updatePageSettings, layoutConfig, updateWatermark } = useReportDesigner();
const [uploadingWatermarkImage, setUploadingWatermarkImage] = useState(false);
const watermarkFileInputRef = useRef<HTMLInputElement>(null);
const [expandedSection, setExpandedSection] = useState<string>("page-info");
const { toast } = useToast();
const handleWatermarkImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast({ title: "오류", description: "이미지 파일만 업로드 가능합니다.", variant: "destructive" });
return;
}
if (file.size > 5 * 1024 * 1024) {
toast({ title: "오류", description: "파일 크기는 5MB 이하여야 합니다.", variant: "destructive" });
return;
}
try {
setUploadingWatermarkImage(true);
const result = await reportApi.uploadImage(file);
if (result.success) {
updateWatermark({ ...layoutConfig.watermark!, imageUrl: result.data.fileUrl });
toast({ title: "성공", description: "워터마크 이미지가 업로드되었습니다." });
}
} catch {
toast({ title: "오류", description: "이미지 업로드 중 오류가 발생했습니다.", variant: "destructive" });
} finally {
setUploadingWatermarkImage(false);
if (watermarkFileInputRef.current) watermarkFileInputRef.current.value = "";
}
};
if (!currentPage || !currentPageId) {
return (
<ScrollArea className="h-full">
<div className="flex h-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
</ScrollArea>
);
}
const toggleSection = (id: string) => {
setExpandedSection(expandedSection === id ? "" : id);
};
return (
<div className="flex h-full flex-col overflow-y-auto bg-white">
{/* 페이지 정보 (아코디언) */}
<div className="border-b-2 border-gray-100">
<button
onClick={() => toggleSection("page-info")}
className={`flex h-12 w-full items-center justify-between px-4 transition-colors ${
expandedSection === "page-info"
? "bg-linear-to-r from-blue-600 to-indigo-600 text-white shadow-sm"
: "bg-white text-gray-900 hover:bg-gray-50"
}`}
>
<div className="flex items-center gap-2">
<FileText className={`h-4 w-4 ${expandedSection === "page-info" ? "" : "text-blue-600"}`} />
<span className="text-sm font-bold"> </span>
</div>
<ChevronRight className={`h-4 w-4 transition-transform ${expandedSection === "page-info" ? "rotate-90" : expandedSection === "page-info" ? "" : "text-gray-400"}`} />
</button>
{expandedSection === "page-info" && (
<div className="space-y-4 border-t border-blue-100 bg-linear-to-b from-blue-50/30 to-white p-4">
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"></Label>
<Input
value={currentPage.page_name}
onChange={(e) => updatePageSettings(currentPageId, { page_name: e.target.value })}
className="h-10 border-2 text-sm focus:border-blue-500"
/>
</div>
</div>
)}
</div>
{/* 페이지 크기 (아코디언) */}
<div className="border-b-2 border-gray-100">
<button
onClick={() => toggleSection("page-size")}
className={`flex h-12 w-full items-center justify-between px-4 transition-colors ${
expandedSection === "page-size"
? "bg-linear-to-r from-blue-600 to-indigo-600 text-white shadow-sm"
: "bg-white text-gray-900 hover:bg-gray-50"
}`}
>
<div className="flex items-center gap-2">
<Maximize2 className={`h-4 w-4 ${expandedSection === "page-size" ? "" : "text-blue-600"}`} />
<span className="text-sm font-bold"> </span>
</div>
<ChevronRight className={`h-4 w-4 transition-transform ${expandedSection === "page-size" ? "rotate-90" : "text-gray-400"}`} />
</button>
{expandedSection === "page-size" && (
<div className="space-y-4 border-t border-gray-100 bg-gray-50/50 p-4">
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"></Label>
<Select
value={currentPage.orientation}
onValueChange={(value: "portrait" | "landscape") => updatePageSettings(currentPageId, { orientation: value })}
>
<SelectTrigger className="h-10 border-2 text-sm focus:border-blue-500">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="portrait"></SelectItem>
<SelectItem value="landscape"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"> (mm)</Label>
<Input
type="number"
min={1}
value={currentPage.width}
onChange={(e) => updatePageSettings(currentPageId, { width: Math.max(1, Number(e.target.value)) })}
className="h-10 border-2 text-sm focus:border-blue-500"
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"> (mm)</Label>
<Input
type="number"
min={1}
value={currentPage.height}
onChange={(e) => updatePageSettings(currentPageId, { height: Math.max(1, Number(e.target.value)) })}
className="h-10 border-2 text-sm focus:border-blue-500"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2 pt-1">
<Button size="sm" variant="outline" className="h-8 text-xs" onClick={() => updatePageSettings(currentPageId, { width: 210, height: 297, orientation: "portrait" })}>
A4
</Button>
<Button size="sm" variant="outline" className="h-8 text-xs" onClick={() => updatePageSettings(currentPageId, { width: 297, height: 210, orientation: "landscape" })}>
A4
</Button>
</div>
</div>
)}
</div>
{/* 여백 설정 (아코디언) */}
<div className="border-b-2 border-gray-100">
<button
onClick={() => toggleSection("margin")}
className={`flex h-12 w-full items-center justify-between px-4 transition-colors ${
expandedSection === "margin"
? "bg-linear-to-r from-blue-600 to-indigo-600 text-white shadow-sm"
: "bg-white text-gray-900 hover:bg-gray-50"
}`}
>
<div className="flex items-center gap-2">
<Space className={`h-4 w-4 ${expandedSection === "margin" ? "" : "text-blue-600"}`} />
<span className="text-sm font-bold"> </span>
</div>
<ChevronRight className={`h-4 w-4 transition-transform ${expandedSection === "margin" ? "rotate-90" : "text-gray-400"}`} />
</button>
{expandedSection === "margin" && (
<div className="space-y-3 border-t border-gray-100 bg-gray-50/50 p-4">
<div className="grid grid-cols-2 gap-3">
{(["top", "bottom", "left", "right"] as const).map((side) => (
<div key={side} className="space-y-2">
<Label className="text-xs font-semibold text-gray-700">
{side === "top" ? "상단 (mm)" : side === "bottom" ? "하단 (mm)" : side === "left" ? "좌측 (mm)" : "우측 (mm)"}
</Label>
<Input
type="number"
min={0}
value={currentPage.margins[side]}
onChange={(e) =>
updatePageSettings(currentPageId, {
margins: { ...currentPage.margins, [side]: Math.max(0, Number(e.target.value)) },
})
}
className="h-10 border-2 text-sm focus:border-blue-500"
/>
</div>
))}
</div>
<div className="grid grid-cols-3 gap-2 pt-1">
{[
{ label: "좁게", value: 10 },
{ label: "보통", value: 20 },
{ label: "넓게", value: 30 },
].map(({ label, value }) => (
<Button
key={label}
size="sm"
variant="outline"
className="h-8 text-xs"
onClick={() => updatePageSettings(currentPageId, { margins: { top: value, bottom: value, left: value, right: value } })}
>
{label}
</Button>
))}
</div>
</div>
)}
</div>
{/* 워터마크 설정 (아코디언) */}
<div className="border-b-2 border-gray-100">
<button
onClick={() => toggleSection("watermark")}
className={`flex h-12 w-full items-center justify-between px-4 transition-colors ${
expandedSection === "watermark"
? "bg-linear-to-r from-blue-600 to-indigo-600 text-white shadow-sm"
: "bg-white text-gray-900 hover:bg-gray-50"
}`}
>
<div className="flex items-center gap-2">
<Droplet className={`h-4 w-4 ${expandedSection === "watermark" ? "" : "text-blue-600"}`} />
<span className="text-sm font-bold"> </span>
</div>
<ChevronRight className={`h-4 w-4 transition-transform ${expandedSection === "watermark" ? "rotate-90" : "text-gray-400"}`} />
</button>
{expandedSection === "watermark" && (
<div className="space-y-4 border-t border-gray-100 bg-gray-50/50 p-4">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold text-gray-700"> </Label>
<Switch
checked={layoutConfig.watermark?.enabled ?? false}
onCheckedChange={(checked) =>
updateWatermark({
...layoutConfig.watermark,
enabled: checked,
type: layoutConfig.watermark?.type ?? "text",
opacity: layoutConfig.watermark?.opacity ?? 0.3,
style: layoutConfig.watermark?.style ?? "diagonal",
})
}
/>
</div>
{layoutConfig.watermark?.enabled && (
<>
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"></Label>
<Select
value={layoutConfig.watermark?.type ?? "text"}
onValueChange={(value: "text" | "image") => updateWatermark({ ...layoutConfig.watermark!, type: value })}
>
<SelectTrigger className="h-10 border-2 text-sm focus:border-blue-500">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="image"></SelectItem>
</SelectContent>
</Select>
</div>
{layoutConfig.watermark?.type === "text" && (
<>
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"></Label>
<Input
value={layoutConfig.watermark?.text ?? ""}
onChange={(e) => updateWatermark({ ...layoutConfig.watermark!, text: e.target.value })}
placeholder="CONFIDENTIAL"
className="h-10 border-2 text-sm focus:border-blue-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"> </Label>
<Input
type="number"
value={layoutConfig.watermark?.fontSize ?? 48}
onChange={(e) => updateWatermark({ ...layoutConfig.watermark!, fontSize: Number(e.target.value) })}
className="h-10 border-2 text-sm focus:border-blue-500"
min={12}
max={200}
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"></Label>
<div className="flex gap-1">
<Input
type="color"
value={layoutConfig.watermark?.fontColor ?? "#cccccc"}
onChange={(e) => updateWatermark({ ...layoutConfig.watermark!, fontColor: e.target.value })}
className="h-10 w-12 cursor-pointer p-1"
/>
<Input
type="text"
value={layoutConfig.watermark?.fontColor ?? "#cccccc"}
onChange={(e) => updateWatermark({ ...layoutConfig.watermark!, fontColor: e.target.value })}
className="h-10 flex-1 border-2 text-sm"
/>
</div>
</div>
</div>
</>
)}
{layoutConfig.watermark?.type === "image" && (
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"> </Label>
<div className="flex gap-2">
<input
ref={watermarkFileInputRef}
type="file"
accept="image/*"
onChange={handleWatermarkImageUpload}
className="hidden"
disabled={uploadingWatermarkImage}
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => watermarkFileInputRef.current?.click()}
disabled={uploadingWatermarkImage}
className="h-10 flex-1 border-2 text-sm"
>
{uploadingWatermarkImage ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Upload className="mr-2 h-4 w-4" />}
{layoutConfig.watermark?.imageUrl ? "이미지 변경" : "이미지 선택"}
</Button>
{layoutConfig.watermark?.imageUrl && (
<Button type="button" variant="ghost" size="sm" className="h-10" onClick={() => updateWatermark({ ...layoutConfig.watermark!, imageUrl: "" })}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
)}
</div>
</div>
)}
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"> </Label>
<Select
value={layoutConfig.watermark?.style ?? "diagonal"}
onValueChange={(value: "diagonal" | "center" | "tile") => updateWatermark({ ...layoutConfig.watermark!, style: value })}
>
<SelectTrigger className="h-10 border-2 text-sm focus:border-blue-500">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="diagonal"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="tile"> ()</SelectItem>
</SelectContent>
</Select>
</div>
{(layoutConfig.watermark?.style === "diagonal" || layoutConfig.watermark?.style === "tile") && (
<div className="space-y-2">
<Label className="text-xs font-semibold text-gray-700"> </Label>
<Input
type="number"
value={layoutConfig.watermark?.rotation ?? -45}
onChange={(e) => updateWatermark({ ...layoutConfig.watermark!, rotation: Number(e.target.value) })}
className="h-10 border-2 text-sm focus:border-blue-500"
min={-180}
max={180}
/>
</div>
)}
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-xs font-semibold text-gray-700"></Label>
<span className="text-xs text-gray-500">{Math.round((layoutConfig.watermark?.opacity ?? 0.3) * 100)}%</span>
</div>
<Slider
value={[(layoutConfig.watermark?.opacity ?? 0.3) * 100]}
onValueChange={(value) => updateWatermark({ ...layoutConfig.watermark!, opacity: value[0] / 100 })}
min={5}
max={100}
step={5}
className="my-2"
/>
</div>
<div className="grid grid-cols-2 gap-2 pt-2">
{[
{ label: "초안", text: "DRAFT", fontSize: 64, fontColor: "#cccccc", style: "diagonal" as const, opacity: 0.2, rotation: -45 },
{ label: "대외비", text: "대외비", fontSize: 64, fontColor: "#ff0000", style: "diagonal" as const, opacity: 0.15, rotation: -45 },
{ label: "샘플", text: "SAMPLE", fontSize: 48, fontColor: "#888888", style: "tile" as const, opacity: 0.1, rotation: -30 },
{ label: "사본", text: "COPY", fontSize: 56, fontColor: "#aaaaaa", style: "center" as const, opacity: 0.25 },
].map(({ label, ...preset }) => (
<Button
key={label}
size="sm"
variant="outline"
className="h-8 text-xs"
onClick={() => updateWatermark({ ...layoutConfig.watermark!, type: "text", ...preset })}
>
{label}
</Button>
))}
</div>
</>
)}
</div>
)}
</div>
</div>
);
}