2026-03-04 20:51:00 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useMemo } from "react";
|
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
|
|
|
|
import { Loader2, Search } from "lucide-react";
|
|
|
|
|
|
import { useBarcodeDesigner } from "@/contexts/BarcodeDesignerContext";
|
|
|
|
|
|
import { barcodeApi, BarcodeLabelTemplate } from "@/lib/api/barcodeApi";
|
|
|
|
|
|
|
|
|
|
|
|
type Category = "all" | "basic" | "zebra";
|
|
|
|
|
|
|
|
|
|
|
|
export function BarcodeTemplatePalette() {
|
|
|
|
|
|
const { applyTemplate } = useBarcodeDesigner();
|
|
|
|
|
|
const [templates, setTemplates] = useState<BarcodeLabelTemplate[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [category, setCategory] = useState<Category>("all");
|
|
|
|
|
|
const [searchText, setSearchText] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await barcodeApi.getTemplates();
|
|
|
|
|
|
if (res.success && res.data) setTemplates(res.data);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setTemplates([]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const filtered = useMemo(() => {
|
|
|
|
|
|
let list = templates;
|
|
|
|
|
|
if (category === "basic") {
|
|
|
|
|
|
list = list.filter((t) => t.template_id.startsWith("TMPL_"));
|
|
|
|
|
|
} else if (category === "zebra") {
|
|
|
|
|
|
list = list.filter((t) => t.template_id.startsWith("ZJ"));
|
|
|
|
|
|
}
|
|
|
|
|
|
const q = searchText.trim().toLowerCase();
|
|
|
|
|
|
if (q) {
|
|
|
|
|
|
list = list.filter(
|
|
|
|
|
|
(t) =>
|
|
|
|
|
|
t.template_id.toLowerCase().includes(q) ||
|
|
|
|
|
|
(t.template_name_kor && t.template_name_kor.toLowerCase().includes(q)) ||
|
|
|
|
|
|
(t.template_name_eng && t.template_name_eng.toLowerCase().includes(q))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return list;
|
|
|
|
|
|
}, [templates, category, searchText]);
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
|
<CardTitle className="text-sm">라벨 규격</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="flex justify-center py-4">
|
|
|
|
|
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
|
<CardTitle className="text-sm">라벨 규격</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-2">
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Search className="text-muted-foreground absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="코드·이름으로 찾기"
|
|
|
|
|
|
value={searchText}
|
|
|
|
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
|
|
|
|
className="h-8 pl-8 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex gap-1">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={category === "all" ? "secondary" : "ghost"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="flex-1 text-xs"
|
|
|
|
|
|
onClick={() => setCategory("all")}
|
|
|
|
|
|
>
|
|
|
|
|
|
전체
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={category === "basic" ? "secondary" : "ghost"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="flex-1 text-xs"
|
|
|
|
|
|
onClick={() => setCategory("basic")}
|
|
|
|
|
|
>
|
|
|
|
|
|
기본
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={category === "zebra" ? "secondary" : "ghost"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="flex-1 text-xs"
|
|
|
|
|
|
onClick={() => setCategory("zebra")}
|
|
|
|
|
|
>
|
|
|
|
|
|
제트라벨
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ScrollArea className="h-[280px] pr-2">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
{filtered.length === 0 ? (
|
|
|
|
|
|
<p className="text-muted-foreground py-2 text-center text-xs">검색 결과 없음</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
filtered.map((t) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={t.template_id}
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
2026-03-05 19:08:08 +09:00
|
|
|
|
className="h-auto w-full justify-start px-2 py-1.5 text-left"
|
2026-03-04 20:51:00 +09:00
|
|
|
|
onClick={() => applyTemplate(t.template_id)}
|
|
|
|
|
|
>
|
2026-03-05 19:08:08 +09:00
|
|
|
|
<span className="block break-words text-left text-xs leading-tight">
|
|
|
|
|
|
{t.template_name_kor}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-muted-foreground mt-0.5 block text-[10px]">
|
2026-03-04 20:51:00 +09:00
|
|
|
|
{t.width_mm}×{t.height_mm}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|