180 lines
5.7 KiB
TypeScript
180 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { ArrowLeft, Save, Loader2, Download, Printer } from "lucide-react";
|
|
import { useBarcodeDesigner } from "@/contexts/BarcodeDesignerContext";
|
|
import { barcodeApi } from "@/lib/api/barcodeApi";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { generateZPL } from "@/lib/zplGenerator";
|
|
import { BarcodePrintPreviewModal } from "./BarcodePrintPreviewModal";
|
|
|
|
export function BarcodeDesignerToolbar() {
|
|
const router = useRouter();
|
|
const { toast } = useToast();
|
|
const {
|
|
labelId,
|
|
labelMaster,
|
|
widthMm,
|
|
heightMm,
|
|
components,
|
|
saveLayout,
|
|
isSaving,
|
|
} = useBarcodeDesigner();
|
|
|
|
const handleDownloadZPL = () => {
|
|
const layout = { width_mm: widthMm, height_mm: heightMm, components };
|
|
const zpl = generateZPL(layout);
|
|
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 = (labelMaster?.label_name_kor || "label") + ".zpl";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
toast({ title: "다운로드", description: "ZPL 파일이 다운로드되었습니다. Zebra 프린터/유틸에서 사용하세요." });
|
|
};
|
|
|
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
|
const [printPreviewOpen, setPrintPreviewOpen] = useState(false);
|
|
const [newLabelName, setNewLabelName] = useState("");
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
const handleSave = async () => {
|
|
if (labelId !== "new") {
|
|
await saveLayout();
|
|
return;
|
|
}
|
|
setSaveDialogOpen(true);
|
|
};
|
|
|
|
const handleCreateAndSave = async () => {
|
|
const name = newLabelName.trim();
|
|
if (!name) {
|
|
toast({
|
|
title: "입력 필요",
|
|
description: "라벨명을 입력하세요.",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
setCreating(true);
|
|
try {
|
|
const createRes = await barcodeApi.createLabel({
|
|
labelNameKor: name,
|
|
});
|
|
if (!createRes.success || !createRes.data?.labelId) throw new Error(createRes.message || "생성 실패");
|
|
const newId = createRes.data.labelId;
|
|
|
|
await barcodeApi.saveLayout(newId, {
|
|
width_mm: widthMm,
|
|
height_mm: heightMm,
|
|
components: components.map((c, i) => ({ ...c, zIndex: i })),
|
|
});
|
|
|
|
toast({ title: "저장됨", description: "라벨이 생성되었습니다." });
|
|
setSaveDialogOpen(false);
|
|
setNewLabelName("");
|
|
router.push(`/admin/screenMng/barcodeList/designer/${newId}`);
|
|
} catch (e: any) {
|
|
toast({
|
|
title: "저장 실패",
|
|
description: e.message || "라벨 생성에 실패했습니다.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center justify-between border-b bg-white px-4 py-2">
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="gap-1"
|
|
onClick={() => router.push("/admin/screenMng/barcodeList")}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
목록
|
|
</Button>
|
|
<span className="text-muted-foreground text-sm">
|
|
{labelId === "new" ? "새 라벨" : labelMaster?.label_name_kor || "바코드 라벨 디자이너"}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="gap-1"
|
|
onClick={() => setPrintPreviewOpen(true)}
|
|
>
|
|
<Printer className="h-4 w-4" />
|
|
인쇄 미리보기
|
|
</Button>
|
|
<Button size="sm" variant="outline" className="gap-1" onClick={handleDownloadZPL}>
|
|
<Download className="h-4 w-4" />
|
|
ZPL 다운로드
|
|
</Button>
|
|
<Button size="sm" className="gap-1" onClick={handleSave} disabled={isSaving || creating}>
|
|
{(isSaving || creating) ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Save className="h-4 w-4" />
|
|
)}
|
|
저장
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<BarcodePrintPreviewModal
|
|
open={printPreviewOpen}
|
|
onOpenChange={setPrintPreviewOpen}
|
|
layout={{
|
|
width_mm: widthMm,
|
|
height_mm: heightMm,
|
|
components: components.map((c, i) => ({ ...c, zIndex: i })),
|
|
}}
|
|
labelName={labelMaster?.label_name_kor || "라벨"}
|
|
/>
|
|
|
|
<Dialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>새 라벨 저장</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-2 py-2">
|
|
<Label>라벨명 (한글)</Label>
|
|
<Input
|
|
value={newLabelName}
|
|
onChange={(e) => setNewLabelName(e.target.value)}
|
|
placeholder="예: 품목 바코드 라벨"
|
|
/>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setSaveDialogOpen(false)}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleCreateAndSave} disabled={creating}>
|
|
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|