234 lines
7.5 KiB
TypeScript
234 lines
7.5 KiB
TypeScript
"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 기기가 모두 표시되므로, 'ZD421' 등 프린터 이름을 골라 주세요.</>
|
||
)}
|
||
</>
|
||
) : null}
|
||
{typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent) && (
|
||
<>
|
||
{" "}
|
||
목록에 프린터가 안 나오면 지브라 공식 'Zebra Browser Print' 앱을 설치한 뒤, 앱에서 프린터 검색·기본 설정 후 이 사이트를 허용하면 출력할 수 있습니다.
|
||
</>
|
||
)}
|
||
</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>
|
||
);
|
||
}
|