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

271 lines
10 KiB
TypeScript

"use client";
import { useState, useRef, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Wand2 } from "lucide-react";
interface SignatureGeneratorProps {
onSignatureSelect: (dataUrl: string) => void;
}
// 서명용 손글씨 폰트 목록 (완전한 한글 지원 폰트만 사용)
const SIGNATURE_FONTS = {
korean: [
{ name: "나눔손글씨 붓", style: "'Nanum Brush Script', cursive", weight: 400 },
{ name: "나눔손글씨 펜", style: "'Nanum Pen Script', cursive", weight: 400 },
{ name: "손글씨 (Gaegu)", style: "Gaegu, cursive", weight: 700 },
{ name: "하이멜로디", style: "'Hi Melody', cursive", weight: 400 },
{ name: "감자꽃", style: "'Gamja Flower', cursive", weight: 400 },
{ name: "푸어스토리", style: "'Poor Story', cursive", weight: 400 },
{ name: "도현", style: "'Do Hyeon', sans-serif", weight: 400 },
{ name: "주아", style: "Jua, sans-serif", weight: 400 },
],
english: [
{ name: "Allura (우아한)", style: "Allura, cursive", weight: 400 },
{ name: "Dancing Script (춤추는)", style: "'Dancing Script', cursive", weight: 700 },
{ name: "Great Vibes (멋진)", style: "'Great Vibes', cursive", weight: 400 },
{ name: "Pacifico (파도)", style: "Pacifico, cursive", weight: 400 },
{ name: "Satisfy (만족)", style: "Satisfy, cursive", weight: 400 },
{ name: "Caveat (거친)", style: "Caveat, cursive", weight: 700 },
{ name: "Permanent Marker", style: "'Permanent Marker', cursive", weight: 400 },
{ name: "Shadows Into Light", style: "'Shadows Into Light', cursive", weight: 400 },
{ name: "Kalam (볼드)", style: "Kalam, cursive", weight: 700 },
{ name: "Patrick Hand", style: "'Patrick Hand', cursive", weight: 400 },
{ name: "Indie Flower", style: "'Indie Flower', cursive", weight: 400 },
{ name: "Amatic SC", style: "'Amatic SC', cursive", weight: 700 },
{ name: "Covered By Your Grace", style: "'Covered By Your Grace', cursive", weight: 400 },
],
};
export function SignatureGenerator({ onSignatureSelect }: SignatureGeneratorProps) {
const [language, setLanguage] = useState<"korean" | "english">("korean");
const [name, setName] = useState("");
const [generatedSignatures, setGeneratedSignatures] = useState<string[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [fontsLoaded, setFontsLoaded] = useState(false);
const canvasRefs = useRef<(HTMLCanvasElement | null)[]>([]);
const fonts = SIGNATURE_FONTS[language];
// 컴포넌트 마운트 시 폰트 미리 로드
useEffect(() => {
const loadAllFonts = async () => {
try {
await document.fonts.ready;
// 모든 폰트를 명시적으로 로드
const allFonts = [...SIGNATURE_FONTS.korean, ...SIGNATURE_FONTS.english];
const fontLoadPromises = allFonts.map((font) => document.fonts.load(`${font.weight} 124px ${font.style}`));
await Promise.all(fontLoadPromises);
// 임시 Canvas를 그려서 폰트를 강제로 렌더링 (브라우저가 폰트를 실제로 사용하도록)
const tempCanvas = document.createElement("canvas");
tempCanvas.width = 100;
tempCanvas.height = 100;
const tempCtx = tempCanvas.getContext("2d");
if (tempCtx) {
for (const font of allFonts) {
tempCtx.font = `${font.weight} 124px ${font.style}`;
tempCtx.fillText("테", 0, 50);
tempCtx.fillText("A", 0, 50);
}
}
// 폰트 렌더링 후 대기
await new Promise((resolve) => setTimeout(resolve, 500));
setFontsLoaded(true);
} catch (error) {
console.warn("Font preloading failed:", error);
await new Promise((resolve) => setTimeout(resolve, 1500));
setFontsLoaded(true);
}
};
loadAllFonts();
}, []);
// 서명 생성
const generateSignatures = async () => {
if (!name.trim()) return;
setIsGenerating(true);
// 폰트가 미리 로드될 때까지 대기
if (!fontsLoaded) {
await new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (fontsLoaded) {
clearInterval(checkInterval);
resolve(true);
}
}, 100);
});
}
// 사용자가 입력한 텍스트로 각 폰트의 글리프를 미리 로드
const preloadCanvas = document.createElement("canvas");
preloadCanvas.width = 500;
preloadCanvas.height = 200;
const preloadCtx = preloadCanvas.getContext("2d");
if (preloadCtx) {
for (const font of fonts) {
preloadCtx.font = `${font.weight} 124px ${font.style}`;
preloadCtx.fillText(name, 0, 100);
}
}
// 글리프 로드 대기 (중요: 첫 렌더링 후 폰트가 완전히 로드되도록)
await new Promise((resolve) => setTimeout(resolve, 300));
const newSignatures: string[] = [];
// 동기적으로 하나씩 생성
for (const font of fonts) {
const canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 200;
const ctx = canvas.getContext("2d");
if (ctx) {
// 배경 흰색
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 텍스트 스타일
ctx.fillStyle = "#000000";
let fontSize = 124;
ctx.font = `${font.weight} ${fontSize}px ${font.style}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// 텍스트 너비 측정 및 크기 조정 (캔버스 너비의 90% 이내로)
let textWidth = ctx.measureText(name).width;
const maxWidth = canvas.width * 0.9;
while (textWidth > maxWidth && fontSize > 30) {
fontSize -= 2;
ctx.font = `${font.weight} ${fontSize}px ${font.style}`;
textWidth = ctx.measureText(name).width;
}
// 텍스트 그리기
ctx.fillText(name, canvas.width / 2, canvas.height / 2);
// 데이터 URL로 변환
newSignatures.push(canvas.toDataURL("image/png"));
}
}
setGeneratedSignatures(newSignatures);
setIsGenerating(false);
};
// 서명 선택 (더블클릭)
const handleSignatureDoubleClick = (dataUrl: string) => {
onSignatureSelect(dataUrl);
};
return (
<div className="space-y-3">
{/* 언어 선택 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={language}
onValueChange={(value: "korean" | "english") => {
setLanguage(value);
setName(""); // 언어 변경 시 입력값 초기화
setGeneratedSignatures([]); // 생성된 서명도 초기화
}}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="korean"></SelectItem>
<SelectItem value="english"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 이름 입력 */}
<div className="space-y-2">
<Label className="text-xs">:</Label>
<div className="flex gap-2">
<Input
value={name}
onChange={(e) => {
const input = e.target.value;
// 국문일 때는 한글, 영문일 때는 영문+숫자+공백만 허용
if (language === "korean") {
// 한글만 허용 (자음, 모음, 완성된 글자)
const koreanOnly = input.replace(/[^\u3131-\u3163\uac00-\ud7a3\s]/g, "");
setName(koreanOnly);
} else {
// 영문, 숫자, 공백만 허용
const englishOnly = input.replace(/[^a-zA-Z\s]/g, "");
setName(englishOnly);
}
}}
placeholder={language === "korean" ? "홍길동" : "John Doe"}
maxLength={14}
className="h-8 flex-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
generateSignatures();
}
}}
/>
<Button
type="button"
size="sm"
onClick={generateSignatures}
disabled={!name.trim() || isGenerating || !fontsLoaded}
>
<Wand2 className="mr-1 h-3 w-3" />
{!fontsLoaded ? "폰트 로딩 중..." : isGenerating ? "생성 중..." : "만들기"}
</Button>
</div>
</div>
{/* 생성된 서명 목록 */}
{generatedSignatures.length > 0 && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<p className="text-xs text-gray-500"> </p>
<ScrollArea className="h-[300px] rounded-md border bg-white">
<div className="space-y-2 p-2">
{generatedSignatures.map((signature, index) => (
<Card
key={index}
className="group hover:border-primary cursor-pointer border-2 border-transparent p-3 transition-all"
onDoubleClick={() => handleSignatureDoubleClick(signature)}
>
<div className="flex items-center justify-between">
<img
src={signature}
alt={`서명 ${index + 1}`}
className="h-auto max-h-[45px] w-auto max-w-[280px] object-contain"
/>
<p className="ml-2 text-xs text-gray-400 opacity-0 transition-opacity group-hover:opacity-100">
{fonts[index].name}
</p>
</div>
</Card>
))}
</div>
</ScrollArea>
</div>
)}
</div>
);
}