feat: Implement searchable category combobox in item info page
- Added a new CategoryCombobox component for improved category selection. - Integrated the combobox into the item info form, replacing the previous select component. - Enhanced data handling to support multiple category selections with comma separation. - Updated selling and standard price fields to format numbers correctly. These changes aim to enhance user experience by providing a more intuitive and flexible category selection method.
This commit is contained in:
@@ -33,9 +33,11 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, FileSpreadsheet, Download,
|
||||
Pencil, Copy, Settings2,
|
||||
Pencil, Copy, Settings2, Check, ChevronsUpDown,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -44,6 +46,43 @@ import { useTableSettings } from "@/hooks/useTableSettings";
|
||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// 검색 가능한 카테고리 콤보박스
|
||||
function CategoryCombobox({ options, value, onChange, placeholder }: {
|
||||
options: { code: string; label: string }[];
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selected = options.find((o) => o.code === value);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" aria-expanded={open} className="h-9 w-full justify-between font-normal">
|
||||
<span className="truncate">{selected?.label || <span className="text-muted-foreground">{placeholder}</span>}</span>
|
||||
<ChevronsUpDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="검색..." className="h-8" />
|
||||
<CommandList>
|
||||
<CommandEmpty>검색 결과가 없어요</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{options.map((opt) => (
|
||||
<CommandItem key={opt.code} value={opt.label} onSelect={() => { onChange(opt.code); setOpen(false); }}>
|
||||
<Check className={cn("mr-2 h-3.5 w-3.5", value === opt.code ? "opacity-100" : "opacity-0")} />
|
||||
{opt.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const TABLE_NAME = "item_info";
|
||||
|
||||
const GRID_COLUMNS = [
|
||||
@@ -55,8 +94,8 @@ const GRID_COLUMNS = [
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "material", label: "재질" },
|
||||
{ key: "status", label: "상태" },
|
||||
{ key: "selling_price", label: "판매가격", align: "right" as const },
|
||||
{ key: "standard_price", label: "기준단가", align: "right" as const },
|
||||
{ key: "selling_price", label: "판매가격", align: "right" as const, formatNumber: true },
|
||||
{ key: "standard_price", label: "기준단가", align: "right" as const, formatNumber: true },
|
||||
{ key: "weight", label: "중량", align: "right" as const },
|
||||
{ key: "inventory_unit", label: "재고단위" },
|
||||
{ key: "user_type01", label: "대분류" },
|
||||
@@ -163,6 +202,14 @@ export default function ItemInfoPage() {
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
// 쉼표 구분 다중값 지원
|
||||
if (code.includes(",")) {
|
||||
return code.split(",").map((c) => {
|
||||
const trimmed = c.trim();
|
||||
if (!trimmed || trimmed === "s") return "";
|
||||
return categoryOptions[col]?.find((o) => o.code === trimmed)?.label || trimmed;
|
||||
}).filter(Boolean).join(", ");
|
||||
}
|
||||
return categoryOptions[col]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
const data = raw.map((r: any) => {
|
||||
@@ -361,6 +408,7 @@ export default function ItemInfoPage() {
|
||||
key: col.key,
|
||||
label: col.label,
|
||||
align: col.align as "left" | "center" | "right" | undefined,
|
||||
formatNumber: (col as any).formatNumber,
|
||||
}))}
|
||||
data={ts.groupData(items)}
|
||||
loading={loading}
|
||||
@@ -394,21 +442,12 @@ export default function ItemInfoPage() {
|
||||
{field.required && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
{field.type === "category" ? (
|
||||
<Select
|
||||
<CategoryCombobox
|
||||
options={categoryOptions[field.key] || []}
|
||||
value={formData[field.key] || ""}
|
||||
onValueChange={(v) => setFormData((prev) => ({ ...prev, [field.key]: v }))}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-full">
|
||||
<SelectValue placeholder={`${field.label} 선택`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions[field.key] || []).map((opt) => (
|
||||
<SelectItem key={opt.code} value={opt.code}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
onChange={(v) => setFormData((prev) => ({ ...prev, [field.key]: v }))}
|
||||
placeholder={`${field.label} 선택`}
|
||||
/>
|
||||
) : field.type === "textarea" ? (
|
||||
<Textarea
|
||||
value={formData[field.key] || ""}
|
||||
@@ -416,6 +455,16 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/[^\d.-]/g, "");
|
||||
setFormData((prev) => ({ ...prev, [field.key]: raw }));
|
||||
}}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9 text-right"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
|
||||
Reference in New Issue
Block a user