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

234 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}