바코드 기능 커밋밋
This commit is contained in:
179
frontend/components/barcode/designer/BarcodeDesignerToolbar.tsx
Normal file
179
frontend/components/barcode/designer/BarcodeDesignerToolbar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user