ERP-node/frontend/components/report/designer/properties/SignatureProperties.tsx

286 lines
11 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
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 { Loader2, PenLine, Upload } from "lucide-react";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { SignatureGenerator } from "../SignatureGenerator";
import { reportApi } from "@/lib/api/reportApi";
import { useToast } from "@/hooks/use-toast";
import type { ComponentConfig } from "@/types/report";
interface Props {
component: ComponentConfig;
/** 우측 패널: "style" | 모달: "data" | 미전달: 전체 표시 (하위 호환) */
section?: "style" | "data";
}
export function SignatureProperties({ component, section }: Props) {
const { updateComponent } = useReportDesigner();
const [signatureMethod, setSignatureMethod] = useState<"upload" | "generate">("upload");
const [uploadingImage, setUploadingImage] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const { toast } = useToast();
const showStyle = !section || section === "style";
const showData = !section || section === "data";
const handleImageUpload = 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 > 10 * 1024 * 1024) {
toast({ title: "오류", description: "파일 크기는 10MB 이하여야 합니다.", variant: "destructive" });
return;
}
try {
setUploadingImage(true);
const result = await reportApi.uploadImage(file);
if (result.success) {
updateComponent(component.id, { imageUrl: result.data.fileUrl });
toast({ title: "성공", description: "이미지가 업로드되었습니다." });
}
} catch {
toast({ title: "오류", description: "이미지 업로드 중 오류가 발생했습니다.", variant: "destructive" });
} finally {
setUploadingImage(false);
if (fileInputRef.current) fileInputRef.current.value = "";
}
};
return (
<>
{/* 서명란 스타일 — 우측 패널(section="style")에서 표시 */}
{showStyle && (
<div className="mt-4 space-y-3 rounded-xl border border-indigo-200 bg-indigo-50/50 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-indigo-700">
<PenLine className="h-4 w-4" />
{component.type === "signature" ? "서명란 스타일" : "도장란 스타일"}
</div>
{/* 맞춤 방식 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={component.objectFit || "contain"}
onValueChange={(value) =>
updateComponent(component.id, { objectFit: value as "contain" | "cover" | "fill" | "none" })
}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="contain"> ( )</SelectItem>
<SelectItem value="cover"> ()</SelectItem>
<SelectItem value="fill"></SelectItem>
<SelectItem value="none"> </SelectItem>
</SelectContent>
</Select>
</div>
{/* 레이블 표시 */}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="showLabel"
checked={component.showLabel !== false}
onChange={(e) => updateComponent(component.id, { showLabel: e.target.checked })}
className="h-4 w-4"
/>
<Label htmlFor="showLabel" className="text-xs">
</Label>
</div>
{/* 레이블 텍스트 */}
{component.showLabel !== false && (
<>
<div>
<Label className="text-xs"> </Label>
<Input
type="text"
value={component.labelText || (component.type === "signature" ? "서명:" : "(인)")}
onChange={(e) => updateComponent(component.id, { labelText: e.target.value })}
className="h-9"
/>
</div>
{/* 레이블 위치 (서명란만) */}
{component.type === "signature" && (
<div>
<Label className="text-xs"> </Label>
<Select
value={component.labelPosition || "left"}
onValueChange={(value) =>
updateComponent(component.id, {
labelPosition: value as "top" | "left" | "bottom" | "right",
})
}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="top"></SelectItem>
<SelectItem value="left"></SelectItem>
<SelectItem value="bottom"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
)}
</>
)}
</div>
)}
{/* 서명 입력 — 모달(section="data")에서 표시 */}
{showData && (
<div className="mt-4 space-y-3 rounded-xl border border-indigo-200 bg-indigo-50/50 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-indigo-700">
<PenLine className="h-4 w-4" />
{component.type === "signature" ? "서명 입력" : "도장 이미지"}
</div>
{component.type === "signature" ? (
<>
{/* 서명 방식 선택 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={signatureMethod}
onValueChange={(value: "upload" | "generate") => setSignatureMethod(value)}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="upload"> </SelectItem>
<SelectItem value="generate"> </SelectItem>
</SelectContent>
</Select>
</div>
{/* 이미지 업로드 */}
{signatureMethod === "upload" && (
<div className="mt-3 space-y-2">
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
disabled={uploadingImage}
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={uploadingImage}
className="flex-1"
>
{uploadingImage ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
{component.imageUrl ? "파일 변경" : "파일 선택"}
</>
)}
</Button>
</div>
<p className="text-xs text-gray-500">JPG, PNG, GIF, WEBP ( 10MB)</p>
{component.imageUrl && !component.imageUrl.startsWith("data:") && (
<p className="truncate text-xs text-indigo-600">: {component.imageUrl}</p>
)}
</div>
)}
{/* 서명 만들기 */}
{signatureMethod === "generate" && (
<div className="mt-3">
<SignatureGenerator
onSignatureSelect={(dataUrl) => {
updateComponent(component.id, { imageUrl: dataUrl });
toast({ title: "성공", description: "서명이 적용되었습니다." });
}}
/>
</div>
)}
</>
) : (
// 도장란
<>
<div>
<Label className="text-xs"> </Label>
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
disabled={uploadingImage}
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={uploadingImage}
className="flex-1"
>
{uploadingImage ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
{component.imageUrl ? "파일 변경" : "파일 선택"}
</>
)}
</Button>
</div>
<p className="mt-1 text-xs text-gray-500">JPG, PNG, GIF, WEBP ( 10MB)</p>
{component.imageUrl && !component.imageUrl.startsWith("data:") && (
<p className="mt-2 truncate text-xs text-indigo-600">: {component.imageUrl}</p>
)}
</div>
{/* 이름 입력 (도장란만) */}
<div>
<Label className="text-xs"></Label>
<Input
type="text"
value={component.personName || ""}
onChange={(e) => updateComponent(component.id, { personName: e.target.value })}
placeholder="예: 홍길동"
className="h-9"
/>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
</>
)}
</div>
)}
</>
);
}