133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
"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"
|
||
className="h-auto w-full justify-start py-1.5 text-left"
|
||
onClick={() => applyTemplate(t.template_id)}
|
||
>
|
||
<span className="truncate">{t.template_name_kor}</span>
|
||
<span className="text-muted-foreground ml-1 shrink-0 text-xs">
|
||
{t.width_mm}×{t.height_mm}
|
||
</span>
|
||
</Button>
|
||
))
|
||
)}
|
||
</div>
|
||
</ScrollArea>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|