ERP-node/frontend/components/barcode/designer/BarcodePrintPreviewModal.tsx

234 lines
7.5 KiB
TypeScript
Raw Normal View History

2026-03-04 20:51:00 +09:00
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Download, Printer, Loader2, AlertCircle } from "lucide-react";
import { BarcodeLabelLayout } from "@/types/barcode";
import { generateZPL } from "@/lib/zplGenerator";
import {
printZPLToZebraBLE,
isWebBluetoothSupported,
getUnsupportedMessage,
} from "@/lib/zebraBluetooth";
import {
printZPLToBrowserPrint,
getBrowserPrintHelpMessage,
} from "@/lib/zebraBrowserPrint";
import { useToast } from "@/hooks/use-toast";
import { BarcodeLabelCanvasComponent } from "./BarcodeLabelCanvasComponent";
import { MM_TO_PX } from "@/contexts/BarcodeDesignerContext";
const PREVIEW_MAX_PX = 320;
interface BarcodePrintPreviewModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
layout: BarcodeLabelLayout;
labelName?: string;
}
export function BarcodePrintPreviewModal({
open,
onOpenChange,
layout,
labelName = "라벨",
}: BarcodePrintPreviewModalProps) {
const { toast } = useToast();
const [printing, setPrinting] = useState(false);
const { width_mm, height_mm, components } = layout;
const widthPx = width_mm * MM_TO_PX;
const heightPx = height_mm * MM_TO_PX;
const scale =
widthPx > PREVIEW_MAX_PX || heightPx > PREVIEW_MAX_PX
? Math.min(PREVIEW_MAX_PX / widthPx, PREVIEW_MAX_PX / heightPx)
: 1;
const previewW = Math.round(widthPx * scale);
const previewH = Math.round(heightPx * scale);
const zpl = generateZPL(layout);
const bleSupported = isWebBluetoothSupported();
const unsupportedMsg = getUnsupportedMessage();
const handleDownloadZPL = () => {
const blob = new Blob([zpl], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${labelName}.zpl`;
a.click();
URL.revokeObjectURL(url);
toast({ title: "다운로드", description: "ZPL 파일이 저장되었습니다." });
};
const handlePrintToZebra = async () => {
const canUseBle = bleSupported;
if (!canUseBle) {
// Browser Print만 시도 (스크립트 로드 후 기본 프린터로 전송)
setPrinting(true);
try {
const result = await printZPLToBrowserPrint(zpl);
if (result.success) {
toast({ title: "전송 완료", description: result.message });
onOpenChange(false);
} else {
toast({
title: "출력 실패",
description: result.message,
variant: "destructive",
});
toast({
title: "안내",
description: getBrowserPrintHelpMessage(),
variant: "default",
});
}
} catch (e: unknown) {
toast({
title: "오류",
description: (e as Error).message || "Zebra 출력 중 오류가 발생했습니다.",
variant: "destructive",
});
} finally {
setPrinting(false);
}
return;
}
// Web Bluetooth 지원 시: Browser Print 먼저 시도, 실패하면 BLE로 폴백
setPrinting(true);
try {
const bpResult = await printZPLToBrowserPrint(zpl);
if (bpResult.success) {
toast({ title: "전송 완료", description: bpResult.message });
onOpenChange(false);
return;
}
const bleResult = await printZPLToZebraBLE(zpl);
if (bleResult.success) {
toast({ title: "전송 완료", description: bleResult.message });
onOpenChange(false);
} else {
toast({
title: "출력 실패",
description: bleResult.message,
variant: "destructive",
});
toast({
title: "안내",
description: getBrowserPrintHelpMessage(),
variant: "default",
});
}
} catch (e: unknown) {
toast({
title: "오류",
description: (e as Error).message || "Zebra 출력 중 오류가 발생했습니다.",
variant: "destructive",
});
} finally {
setPrinting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-muted-foreground text-sm">
{width_mm}×{height_mm}mm · {components.length}
</p>
{/* 미리보기 캔버스 (축소) */}
<div className="flex justify-center rounded border bg-gray-100 p-4">
<div
className="relative bg-white shadow"
style={{
width: previewW,
height: previewH,
transformOrigin: "top left",
}}
>
<div
className="pointer-events-none"
style={{
transform: `scale(${scale})`,
transformOrigin: "0 0",
width: widthPx,
height: heightPx,
position: "absolute",
left: 0,
top: 0,
}}
>
{components.map((c) => (
<BarcodeLabelCanvasComponent key={c.id} component={c} />
))}
</div>
</div>
</div>
{!bleSupported && (
<div className="flex gap-2 rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
<AlertCircle className="h-4 w-4 shrink-0" />
<span>
Web Bluetooth . Zebra Browser Print .
{unsupportedMsg && ` ${unsupportedMsg}`}
</span>
</div>
)}
<p className="text-muted-foreground text-xs">
{bleSupported ? (
<>
Zebra Bluetooth LE로 , .
(Chrome/Edge )
{typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent) && (
<> Android에서는 BLE , &apos;ZD421&apos; .</>
)}
</>
) : null}
{typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent) && (
<>
{" "}
&apos;Zebra Browser Print&apos; , · .
</>
)}
</p>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" size="sm" onClick={handleDownloadZPL} className="gap-1">
<Download className="h-4 w-4" />
ZPL
</Button>
<Button
size="sm"
className="gap-1"
onClick={handlePrintToZebra}
disabled={printing}
>
{printing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Printer className="h-4 w-4" />
)}
Zebra
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}