136 lines
3.6 KiB
TypeScript
136 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Eraser, Pen } from "lucide-react";
|
|
|
|
interface SignaturePadProps {
|
|
onSignatureChange: (dataUrl: string) => void;
|
|
initialSignature?: string;
|
|
}
|
|
|
|
export function SignaturePad({ onSignatureChange, initialSignature }: SignaturePadProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [isDrawing, setIsDrawing] = useState(false);
|
|
const [hasDrawn, setHasDrawn] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
// 초기 서명이 있으면 로드
|
|
if (initialSignature) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.drawImage(img, 0, 0);
|
|
setHasDrawn(true);
|
|
};
|
|
img.src = initialSignature;
|
|
} else {
|
|
// 캔버스 초기화 (흰색 배경)
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
}, [initialSignature]);
|
|
|
|
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
setIsDrawing(true);
|
|
setHasDrawn(true);
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, y);
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
ctx.strokeStyle = "#000000";
|
|
ctx.lineWidth = 2;
|
|
};
|
|
|
|
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
if (!isDrawing) return;
|
|
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
ctx.lineTo(x, y);
|
|
ctx.stroke();
|
|
};
|
|
|
|
const stopDrawing = () => {
|
|
if (!isDrawing) return;
|
|
|
|
setIsDrawing(false);
|
|
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
// 서명 이미지를 Base64 데이터로 변환하여 콜백 호출
|
|
const dataUrl = canvas.toDataURL("image/png");
|
|
onSignatureChange(dataUrl);
|
|
};
|
|
|
|
const clearSignature = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
// 캔버스 클리어 (흰색 배경)
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
setHasDrawn(false);
|
|
onSignatureChange("");
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Card className="overflow-hidden border-2 border-dashed border-gray-300 bg-white p-2">
|
|
<canvas
|
|
ref={canvasRef}
|
|
width={300}
|
|
height={150}
|
|
onMouseDown={startDrawing}
|
|
onMouseMove={draw}
|
|
onMouseUp={stopDrawing}
|
|
onMouseLeave={stopDrawing}
|
|
className="cursor-crosshair touch-none"
|
|
style={{ width: "100%", height: "auto" }}
|
|
/>
|
|
</Card>
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-xs text-gray-500">
|
|
<Pen className="mr-1 inline h-3 w-3" />
|
|
마우스로 서명해주세요
|
|
</p>
|
|
<Button type="button" variant="outline" size="sm" onClick={clearSignature} disabled={!hasDrawn}>
|
|
<Eraser className="mr-1 h-3 w-3" />
|
|
지우기
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|