diff --git a/.playwright-mcp/page-2026-04-09T02-32-06-365Z.yml b/.playwright-mcp/page-2026-04-09T02-32-06-365Z.yml new file mode 100644 index 00000000..4312b38d --- /dev/null +++ b/.playwright-mcp/page-2026-04-09T02-32-06-365Z.yml @@ -0,0 +1 @@ +- generic [ref=e2]: "{\"success\": false, \"error\": \"Invalid endpoint\"}" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-09T02-32-30-815Z.yml b/.playwright-mcp/page-2026-04-09T02-32-30-815Z.yml new file mode 100644 index 00000000..a5e9fe42 --- /dev/null +++ b/.playwright-mcp/page-2026-04-09T02-32-30-815Z.yml @@ -0,0 +1,27 @@ +- generic [ref=e3]: + - generic [ref=e5]: + - generic [ref=e6]: + - heading "๐Ÿ“ฆ ํŒ๋งคํ’ˆ๋ชฉ ๋ชฉ๋ก" [level=3] [ref=e7] + - generic [ref=e8]: + - text: ์ด + - strong [ref=e9]: "0" + - text: ๊ฐœ + - combobox [ref=e10]: + - option "โš™๏ธ Group by" [selected] + - option "ํ†ตํ™”" + - option "๋‹จ์œ„" + - option "์ƒํƒœ" + - generic [ref=e11]: + - generic [ref=e12] [cursor=pointer]: + - checkbox "๋ฏธ์‚ฌ์šฉ ํฌํ•จ" [ref=e13] + - generic [ref=e14]: ๋ฏธ์‚ฌ์šฉ ํฌํ•จ + - button "โž• ํ’ˆ๋ชฉ ์ถ”๊ฐ€" [ref=e15] + - button "โœ๏ธ ์ˆ˜์ •" [disabled] [ref=e16] + - button "โธ๏ธ ์‚ฌ์šฉ/๋ฏธ์‚ฌ์šฉ" [disabled] [ref=e17] + - generic [ref=e20]: + - generic [ref=e21]: + - heading "๐Ÿข ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ์ •๋ณด" [level=3] [ref=e22] + - button "โž• ๊ฑฐ๋ž˜์ฒ˜ ์ถ”๊ฐ€" [disabled] [ref=e23] + - generic [ref=e25]: + - generic [ref=e26]: ๐Ÿ“ญ + - generic [ref=e27]: ์™ผ์ชฝ์—์„œ ํ’ˆ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š” \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-09T02-35-09-102Z.yml b/.playwright-mcp/page-2026-04-09T02-35-09-102Z.yml new file mode 100644 index 00000000..a5e9fe42 --- /dev/null +++ b/.playwright-mcp/page-2026-04-09T02-35-09-102Z.yml @@ -0,0 +1,27 @@ +- generic [ref=e3]: + - generic [ref=e5]: + - generic [ref=e6]: + - heading "๐Ÿ“ฆ ํŒ๋งคํ’ˆ๋ชฉ ๋ชฉ๋ก" [level=3] [ref=e7] + - generic [ref=e8]: + - text: ์ด + - strong [ref=e9]: "0" + - text: ๊ฐœ + - combobox [ref=e10]: + - option "โš™๏ธ Group by" [selected] + - option "ํ†ตํ™”" + - option "๋‹จ์œ„" + - option "์ƒํƒœ" + - generic [ref=e11]: + - generic [ref=e12] [cursor=pointer]: + - checkbox "๋ฏธ์‚ฌ์šฉ ํฌํ•จ" [ref=e13] + - generic [ref=e14]: ๋ฏธ์‚ฌ์šฉ ํฌํ•จ + - button "โž• ํ’ˆ๋ชฉ ์ถ”๊ฐ€" [ref=e15] + - button "โœ๏ธ ์ˆ˜์ •" [disabled] [ref=e16] + - button "โธ๏ธ ์‚ฌ์šฉ/๋ฏธ์‚ฌ์šฉ" [disabled] [ref=e17] + - generic [ref=e20]: + - generic [ref=e21]: + - heading "๐Ÿข ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ์ •๋ณด" [level=3] [ref=e22] + - button "โž• ๊ฑฐ๋ž˜์ฒ˜ ์ถ”๊ฐ€" [disabled] [ref=e23] + - generic [ref=e25]: + - generic [ref=e26]: ๐Ÿ“ญ + - generic [ref=e27]: ์™ผ์ชฝ์—์„œ ํ’ˆ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š” \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-09T02-35-16-606Z.yml b/.playwright-mcp/page-2026-04-09T02-35-16-606Z.yml new file mode 100644 index 00000000..48102bd2 --- /dev/null +++ b/.playwright-mcp/page-2026-04-09T02-35-16-606Z.yml @@ -0,0 +1,26 @@ +- generic [ref=e3]: + - generic [ref=e5]: + - generic [ref=e6]: + - heading "๐Ÿข ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก" [level=3] [ref=e7] + - generic [ref=e8]: + - text: ์ด + - strong [ref=e9]: "0" + - text: ๊ฐœ + - combobox [ref=e10] [cursor=pointer]: + - option "โš™๏ธ Group by" [selected] + - option "๊ฑฐ๋ž˜ ์œ ํ˜•" + - option "์ƒํƒœ" + - generic [ref=e11]: + - generic [ref=e12] [cursor=pointer]: + - checkbox "๋ฏธ์‚ฌ์šฉ ํฌํ•จ" [ref=e13] + - generic [ref=e14]: ๋ฏธ์‚ฌ์šฉ ํฌํ•จ + - button "โž• ๊ฑฐ๋ž˜์ฒ˜ ๋“ฑ๋ก" [ref=e15] + - button "โœ๏ธ ์ˆ˜์ •" [disabled] [ref=e16] + - button "โธ๏ธ ์‚ฌ์šฉ/๋ฏธ์‚ฌ์šฉ" [disabled] [ref=e17] + - generic [ref=e20]: + - generic [ref=e21]: + - heading "๐Ÿ“ฆ ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ํ’ˆ๋ชฉ ์ •๋ณด" [level=3] [ref=e22] + - button "โž• ํ’ˆ๋ชฉ ์ถ”๊ฐ€" [disabled] [ref=e23] + - generic [ref=e25]: + - generic [ref=e26]: ๐Ÿ“ญ + - generic [ref=e27]: ์™ผ์ชฝ์—์„œ ๊ฑฐ๋ž˜์ฒ˜๋ฅผ ์„ ํƒํ•˜์„ธ์š” \ No newline at end of file diff --git a/customer-snapshot.md b/customer-snapshot.md new file mode 100644 index 00000000..48102bd2 --- /dev/null +++ b/customer-snapshot.md @@ -0,0 +1,26 @@ +- generic [ref=e3]: + - generic [ref=e5]: + - generic [ref=e6]: + - heading "๐Ÿข ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก" [level=3] [ref=e7] + - generic [ref=e8]: + - text: ์ด + - strong [ref=e9]: "0" + - text: ๊ฐœ + - combobox [ref=e10] [cursor=pointer]: + - option "โš™๏ธ Group by" [selected] + - option "๊ฑฐ๋ž˜ ์œ ํ˜•" + - option "์ƒํƒœ" + - generic [ref=e11]: + - generic [ref=e12] [cursor=pointer]: + - checkbox "๋ฏธ์‚ฌ์šฉ ํฌํ•จ" [ref=e13] + - generic [ref=e14]: ๋ฏธ์‚ฌ์šฉ ํฌํ•จ + - button "โž• ๊ฑฐ๋ž˜์ฒ˜ ๋“ฑ๋ก" [ref=e15] + - button "โœ๏ธ ์ˆ˜์ •" [disabled] [ref=e16] + - button "โธ๏ธ ์‚ฌ์šฉ/๋ฏธ์‚ฌ์šฉ" [disabled] [ref=e17] + - generic [ref=e20]: + - generic [ref=e21]: + - heading "๐Ÿ“ฆ ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ํ’ˆ๋ชฉ ์ •๋ณด" [level=3] [ref=e22] + - button "โž• ํ’ˆ๋ชฉ ์ถ”๊ฐ€" [disabled] [ref=e23] + - generic [ref=e25]: + - generic [ref=e26]: ๐Ÿ“ญ + - generic [ref=e27]: ์™ผ์ชฝ์—์„œ ๊ฑฐ๋ž˜์ฒ˜๋ฅผ ์„ ํƒํ•˜์„ธ์š” \ No newline at end of file diff --git a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx index 7f211a88..9141103c 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx @@ -13,19 +13,32 @@ import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Badge } from "@/components/ui/badge"; -import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Search, X, Settings2, Package } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { ImageUpload } from "@/components/common/ImageUpload"; +import { + Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Search, X, Settings2, Package, + ChevronRight, ChevronDown, Coins, GripVertical, Check, ChevronsUpDown, +} from "lucide-react"; +import { + DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, +} from "@dnd-kit/core"; +import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; -import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; +import { MultiTableExcelUploadModal } from "@/components/common/MultiTableExcelUploadModal"; +import { autoDetectMultiTableConfig, TableChainConfig } from "@/lib/api/multiTableExcel"; import { exportToExcel } from "@/lib/utils/excelExport"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { useTableSettings } from "@/hooks/useTableSettings"; @@ -36,6 +49,121 @@ const ITEM_TABLE = "item_info"; const MAPPING_TABLE = "supplier_item_mapping"; const SUPPLIER_TABLE = "supplier_mng"; +// ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝค๋ณด๋ฐ•์Šค +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 ( + + + + + + + + + ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด์š” + + {options.map((opt) => ( + { onChange(opt.code); setOpen(false); }}> + + {opt.label} + + ))} + + + + + + ); +} + +// ๋‹ค์ค‘ ์„ ํƒ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝค๋ณด๋ฐ•์Šค +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด์š” + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + +const FORM_FIELDS = [ + { key: "item_number", label: "ํ’ˆ๋ชฉ์ฝ”๋“œ", type: "text", required: true, disabled: true, placeholder: "์ž๋™ ์ฑ„๋ฒˆ" }, + { key: "item_name", label: "ํ’ˆ๋ช…", type: "text", required: true }, + { key: "division", label: "๊ด€๋ฆฌํ’ˆ๋ชฉ", type: "multi-category" }, + { key: "type", label: "ํ’ˆ๋ชฉ๊ตฌ๋ถ„", type: "category" }, + { key: "size", label: "๊ทœ๊ฒฉ", type: "text" }, + { key: "unit", label: "๋‹จ์œ„", type: "category" }, + { key: "material", label: "์žฌ์งˆ", type: "category" }, + { key: "status", label: "์ƒํƒœ", type: "category" }, + { key: "weight", label: "์ค‘๋Ÿ‰", type: "text", placeholder: "์ˆซ์ž ์ž…๋ ฅ (์˜ˆ: 3.5)" }, + { key: "volum", label: "๋ถ€ํ”ผ", type: "text", placeholder: "์ˆซ์ž ์ž…๋ ฅ (์˜ˆ: 100)" }, + { key: "specific_gravity", label: "๋น„์ค‘", type: "text", placeholder: "์ˆซ์ž ์ž…๋ ฅ (์˜ˆ: 7.85)" }, + { key: "inventory_unit", label: "์žฌ๊ณ ๋‹จ์œ„", type: "category" }, + { key: "selling_price", label: "ํŒ๋งค๊ฐ€๊ฒฉ", type: "text" }, + { key: "standard_price", label: "๊ธฐ์ค€๋‹จ๊ฐ€", type: "text" }, + { key: "currency_code", label: "ํ†ตํ™”", type: "category" }, + { key: "user_type01", label: "๋Œ€๋ถ„๋ฅ˜", type: "category" }, + { key: "user_type02", label: "์ค‘๋ถ„๋ฅ˜", type: "category" }, + { key: "lead_time", label: "์ƒ์‚ฐ ๋ฆฌ๋“œํƒ€์ž„(์ผ)", type: "text", placeholder: "์ˆซ์ž ์ž…๋ ฅ (์˜ˆ: 7)" }, + { key: "image", label: "ํ’ˆ๋ชฉ ์ด๋ฏธ์ง€", type: "image" }, + { key: "meno", label: "๋ฉ”๋ชจ", type: "textarea" }, +] as const; + +const CATEGORY_COLUMNS = [ + "division", "type", "unit", "material", "status", + "inventory_unit", "currency_code", "user_type01", "user_type02", +]; + // ์ˆซ์ž ํฌ๋งท ํ—ฌํผ const formatNum = (val: any): string => { if (val === null || val === undefined || val === "") return ""; @@ -54,24 +182,51 @@ const ITEM_GRID_COLUMNS = [ { key: "status", label: "์ƒํƒœ" }, ]; +function SortableMappingRow({ id, children }: { id: string; children: React.ReactNode }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( +
+
+ +
+ {children} +
+ ); +} + export default function PurchaseItemPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent, isConfirmOpenRef } = useConfirmDialog(); const ts = useTableSettings("c16-purchase-item", ITEM_TABLE, ITEM_GRID_COLUMNS); + const dndSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })); // ์ขŒ์ธก: ํ’ˆ๋ชฉ const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [itemLoading, setItemLoading] = useState(false); const [itemCount, setItemCount] = useState(0); const [selectedItemId, setSelectedItemId] = useState(null); + // ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ๋ชจ๋‹ฌ (item-info ์Šคํƒ€์ผ) + const [isModalOpen, setIsModalOpen] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + const [editId, setEditId] = useState(null); + const [formData, setFormData] = useState>({}); + // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ (DynamicSearchFilter์—์„œ ๊ด€๋ฆฌ) const [searchFilters, setSearchFilters] = useState([]); // ์šฐ์ธก: ๊ณต๊ธ‰์—…์ฒด const [supplierItems, setSupplierItems] = useState([]); + const [supplierGroups, setSupplierGroups] = useState>({}); const [supplierLoading, setSupplierLoading] = useState(false); const [supplierCheckedIds, setSupplierCheckedIds] = useState([]); + const [expandedItems, setExpandedItems] = useState>(new Set()); + const [collapsedPriceCards, setCollapsedPriceCards] = useState>(new Set()); // ์นดํ…Œ๊ณ ๋ฆฌ const [categoryOptions, setCategoryOptions] = useState>({}); @@ -84,13 +239,12 @@ export default function PurchaseItemPage() { const [suppSearchLoading, setSuppSearchLoading] = useState(false); const [suppCheckedIds, setSuppCheckedIds] = useState>(new Set()); - // ํ’ˆ๋ชฉ ์ˆ˜์ • ๋ชจ๋‹ฌ - const [editItemOpen, setEditItemOpen] = useState(false); - const [editItemForm, setEditItemForm] = useState>({}); const [saving, setSaving] = useState(false); // ์—‘์…€ const [excelUploadOpen, setExcelUploadOpen] = useState(false); + const [excelChainConfig, setExcelChainConfig] = useState(null); + const [excelDetecting, setExcelDetecting] = useState(false); // ๊ณต๊ธ‰์—…์ฒด ์ƒ์„ธ ์ž…๋ ฅ ๋ชจ๋‹ฌ (๊ณต๊ธ‰์—…์ฒด ํ’ˆ๋ฒˆ/ํ’ˆ๋ช… + ๋‹จ๊ฐ€) @@ -118,12 +272,14 @@ export default function PurchaseItemPage() { } return result; }; - for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) { - try { - const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`); - if (res.data?.success) optMap[col] = flatten(res.data.data || []); - } catch { /* skip */ } - } + await Promise.all( + CATEGORY_COLUMNS.map(async (col) => { + try { + const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`); + if (res.data?.success && res.data.data?.length > 0) optMap[col] = flatten(res.data.data); + } catch { /* skip */ } + }) + ); setCategoryOptions(optMap); // ๋‹จ๊ฐ€ ์นดํ…Œ๊ณ ๋ฆฌ @@ -164,10 +320,10 @@ export default function PurchaseItemPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; - for (const col of CATS) { + for (const col of CATEGORY_COLUMNS) { if (converted[col]) converted[col] = resolve(col, converted[col]); } return converted; @@ -184,12 +340,79 @@ export default function PurchaseItemPage() { useEffect(() => { fetchItems(); }, [fetchItems]); + // ์ฑ„๋ฒˆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + const loadNumberingPreview = async () => { + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${ITEM_TABLE}/item_number`); + const rule = ruleRes.data?.data; + if (rule?.ruleId) { + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} }); + return previewRes.data?.data?.generatedCode || ""; + } + } catch { /* ์ฑ„๋ฒˆ ๊ทœ์น™ ์—†์œผ๋ฉด ๋ฌด์‹œ */ } + return ""; + }; + + // ๋“ฑ๋ก ๋ชจ๋‹ฌ ์—ด๊ธฐ + const openRegisterModal = async () => { + setFormData({}); + setIsEditMode(false); + setEditId(null); + setIsModalOpen(true); + const code = await loadNumberingPreview(); + if (code) setFormData((prev) => ({ ...prev, item_number: code })); + }; + + // ์ˆ˜์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ + const openEditModal = (item: any) => { + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); + setIsEditMode(true); + setEditId(item.id); + setIsModalOpen(true); + }; + + // ์ €์žฅ (๋“ฑ๋ก ๋˜๋Š” ์ˆ˜์ •) + const handleSave = async () => { + if (!formData.item_name) { + toast.error("ํ’ˆ๋ช…์€ ํ•„์ˆ˜ ์ž…๋ ฅ์ด์—์š”."); + return; + } + setSaving(true); + try { + if (isEditMode && editId) { + const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData; + await apiClient.put(`/table-management/tables/${ITEM_TABLE}/edit`, { + originalData: { id: editId }, + updatedData: updateFields, + }); + toast.success("์ˆ˜์ •๋˜์—ˆ์–ด์š”."); + } else { + const { id, created_date, updated_date, ...insertFields } = formData; + await apiClient.post(`/table-management/tables/${ITEM_TABLE}/add`, { id: crypto.randomUUID(), ...insertFields }); + toast.success("๋“ฑ๋ก๋˜์—ˆ์–ด์š”."); + } + setIsModalOpen(false); + fetchItems(); + } catch (err: any) { + console.error("์ €์žฅ ์‹คํŒจ:", err); + toast.error(err.response?.data?.message || "์ €์žฅ์— ์‹คํŒจํ–ˆ์–ด์š”."); + } finally { + setSaving(false); + } + }; + // ์„ ํƒ๋œ ํ’ˆ๋ชฉ const selectedItem = items.find((i) => i.id === selectedItemId); // ์šฐ์ธก: ๊ณต๊ธ‰์—…์ฒด ๋ชฉ๋ก ์กฐํšŒ useEffect(() => { - if (!selectedItem?.item_number) { setSupplierItems([]); setSupplierCheckedIds([]); return; } + if (!selectedItem?.item_number) { + setSupplierItems([]); + setSupplierGroups({}); + setSupplierCheckedIds([]); + return; + } setSupplierCheckedIds([]); const itemKey = selectedItem.item_number; const fetchSupplierItems = async () => { @@ -234,36 +457,57 @@ export default function PurchaseItemPage() { } catch { /* skip */ } } - // 4. ๊ณต๊ธ‰์—…์ฒด๋ณ„ ์ค‘๋ณต ์ œ๊ฑฐ + ์˜ค๋Š˜ ๋‚ ์งœ ๊ธฐ์ค€ ๋‹จ๊ฐ€ ๋งค์นญ + // 4. ๊ณต๊ธ‰์—…์ฒด๋ณ„ ๊ทธ๋ฃนํ•‘ โ€” master: ์ฒซ ๋งคํ•‘ + ํ˜„์žฌ ๋‹จ๊ฐ€, details: ์ „์ฒด ๋‹จ๊ฐ€ ๋ฆฌ์ŠคํŠธ const priceResolve = (col: string, code: string) => { if (!code) return ""; return priceCategoryOptions[col]?.find((o: any) => o.code === code)?.label || code; }; const today = new Date().toISOString().split("T")[0]; const seenCustIds = new Set(); - const sortedMappings = [...mappings].sort((a: any, b: any) => (a.supplier_id || "").localeCompare(b.supplier_id || "")); + const grouped: Record = {}; + const flatItems: any[] = []; - setSupplierItems(sortedMappings.map((m: any) => { + for (const m of mappings) { const custKey = m.supplier_id || ""; - const isFirstOfGroup = !seenCustIds.has(custKey); - if (custKey) seenCustIds.add(custKey); + if (seenCustIds.has(custKey)) continue; // ๊ณต๊ธ‰์—…์ฒด๋‹น ์ฒซ ๋งคํ•‘๋งŒ ๋งˆ์Šคํ„ฐ + seenCustIds.add(custKey); - const custPriceList = allPrices.filter((p: any) => p.supplier_id === custKey); + const custInfo = custMap[custKey] || {}; + const custPriceList = allPrices + .filter((p: any) => p.supplier_id === custKey) + .sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || "")); const todayPrice = custPriceList.find((p: any) => (!p.start_date || p.start_date <= today) && (!p.end_date || p.end_date >= today) ) || custPriceList[0] || {}; - return { + const masterRow = { ...m, - supplier_code: isFirstOfGroup ? custKey : "", - supplier_name: isFirstOfGroup ? (custMap[custKey]?.supplier_name || "") : "", + supplier_code: custKey, + supplier_name: custInfo.supplier_name || "", supplier_item_code: m.supplier_item_code || "", supplier_item_name: m.supplier_item_name || "", + base_price_type: priceResolve("base_price_type", todayPrice.base_price_type || ""), base_price: todayPrice.base_price || "", + discount_type: priceResolve("discount_type", todayPrice.discount_type || ""), + discount_value: todayPrice.discount_value || "", calculated_price: todayPrice.calculated_price || todayPrice.unit_price || "", currency_code: priceResolve("currency_code", todayPrice.currency_code || ""), }; - })); + + // ๋‹จ๊ฐ€ ๋ฆฌ์ŠคํŠธ (๋ผ๋ฒจ ๋ณ€ํ™˜) + const priceDetails = custPriceList.map((p: any) => ({ + ...p, + base_price_type_label: priceResolve("base_price_type", p.base_price_type || ""), + discount_type_label: priceResolve("discount_type", p.discount_type || ""), + currency_label: priceResolve("currency_code", p.currency_code || ""), + is_current: (!p.start_date || p.start_date <= today) && (!p.end_date || p.end_date >= today), + })); + + grouped[custKey] = { master: masterRow, details: priceDetails }; + flatItems.push(masterRow); + } + setSupplierGroups(grouped); + setSupplierItems(flatItems); } catch (err) { console.error("๊ณต๊ธ‰์—…์ฒด ์กฐํšŒ ์‹คํŒจ:", err); } finally { @@ -329,6 +573,17 @@ export default function PurchaseItemPage() { })); }; + const handleMappingDragEnd = (custKey: string, event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setSuppMappings((prev) => { + const arr = [...(prev[custKey] || [])]; + const oldIdx = arr.findIndex((r) => r._id === active.id); + const newIdx = arr.findIndex((r) => r._id === over.id); + return { ...prev, [custKey]: arrayMove(arr, oldIdx, newIdx) }; + }); + }; + const updateMappingRow = (custKey: string, rowId: string, field: string, value: string) => { setSuppMappings((prev) => ({ ...prev, @@ -539,34 +794,6 @@ export default function PurchaseItemPage() { } }; - // ํ’ˆ๋ชฉ ์ˆ˜์ • - const openEditItem = () => { - if (!selectedItem) return; - setEditItemForm({ ...selectedItem }); - setEditItemOpen(true); - }; - - const handleEditSave = async () => { - if (!editItemForm.id) return; - setSaving(true); - try { - await apiClient.put(`/table-management/tables/${ITEM_TABLE}/edit`, { - originalData: { id: editItemForm.id }, - updatedData: { - standard_price: editItemForm.standard_price || null, - currency_code: editItemForm.currency_code || null, - }, - }); - toast.success("์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); - setEditItemOpen(false); - fetchItems(); - } catch (err: any) { - toast.error(err.response?.data?.message || "์ˆ˜์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); - } finally { - setSaving(false); - } - }; - // ์šฐ์ธก: ๊ณต๊ธ‰์—…์ฒด ๋งคํ•‘ ์‚ญ์ œ const handleSupplierMappingDelete = async () => { if (supplierCheckedIds.length === 0) return; @@ -647,8 +874,23 @@ export default function PurchaseItemPage() { {/* ์•ก์…˜ ๋ฒ„ํŠผ ์˜์—ญ */}
-
- +
- {/* ๊ณต๊ธ‰์—…์ฒด ํ…Œ์ด๋ธ” */} + {/* ๊ณต๊ธ‰์—…์ฒด ํ…Œ์ด๋ธ” (expandable rows) */}
- {supplierLoading ? ( -
- -
- ) : supplierItems.length === 0 ? ( -
- ๋“ฑ๋ก๋œ ๊ณต๊ธ‰์—…์ฒด๊ฐ€ ์—†์–ด์š” -
- ) : ( - - - - - 0 && supplierCheckedIds.length === supplierItems.length} - onChange={(e) => { - if (e.target.checked) setSupplierCheckedIds(supplierItems.map((c) => c.id)); - else setSupplierCheckedIds([]); - }} - /> - - ๊ณต๊ธ‰์—…์ฒด์ฝ”๋“œ - ๊ณต๊ธ‰์—…์ฒด๋ช… - ๊ณต๊ธ‰์—…์ฒดํ’ˆ๋ฒˆ - ๊ณต๊ธ‰์—…์ฒดํ’ˆ๋ช… - ๊ธฐ์ค€๊ฐ€ - ๋‹จ๊ฐ€ - ํ†ตํ™” +
+ + + + 0 && supplierCheckedIds.length === supplierItems.length} + onChange={(e) => { + if (e.target.checked) setSupplierCheckedIds(supplierItems.map((c) => c.id)); + else setSupplierCheckedIds([]); + }} + /> + + ๊ณต๊ธ‰์—…์ฒด์ฝ”๋“œ + ๊ณต๊ธ‰์—…์ฒด๋ช… + ๊ณต๊ธ‰์—…์ฒดํ’ˆ๋ฒˆ + ๊ณต๊ธ‰์—…์ฒดํ’ˆ๋ช… + ๊ธฐ์ค€๊ฐ€ + ๋‹จ๊ฐ€ + ํ†ตํ™” + + + + {supplierLoading ? ( + + + + - - - {supplierItems.map((row) => ( - openEditSupp(row)} - > - { - e.stopPropagation(); - setSupplierCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); + ) : Object.keys(supplierGroups).length === 0 ? ( + + + ๋“ฑ๋ก๋œ ๊ณต๊ธ‰์—…์ฒด๊ฐ€ ์—†์–ด์š” + + + ) : Object.entries(supplierGroups).map(([custKey, group]) => { + const isExpanded = expandedItems.has(custKey); + const m = group.master; + const isChecked = supplierCheckedIds.includes(m.id); + return ( + + {/* ๋งˆ์Šคํ„ฐ ํ–‰ */} + { + setExpandedItems((prev) => { + const next = new Set(prev); + if (next.has(custKey)) next.delete(custKey); else next.add(custKey); + return next; + }); }} + onDoubleClick={() => openEditSupp(m)} > - - - {row.supplier_code} - {row.supplier_name} - {row.supplier_item_code} - {row.supplier_item_name} - - {row.base_price ? Number(row.base_price).toLocaleString() : ""} - - - {row.calculated_price ? Number(row.calculated_price).toLocaleString() : ""} - - {row.currency_code} - - ))} - -
- )} + { + e.stopPropagation(); + setSupplierCheckedIds((prev) => + prev.includes(m.id) ? prev.filter((id) => id !== m.id) : [...prev, m.id] + ); + }} + > + + + +
+ {isExpanded + ? + : + } + {m.supplier_code} +
+
+ {m.supplier_name} + {m.supplier_item_code} + {m.supplier_item_name} + + {m.base_price ? Number(m.base_price).toLocaleString() : ""} + + + {m.calculated_price ? Number(m.calculated_price).toLocaleString() : ""} + + {m.currency_code} + + + {/* ํ˜„์žฌ ๋‹จ๊ฐ€ ์นด๋“œ (ํŽผ์ณค์„ ๋•Œ) */} + {isExpanded && (() => { + const cp = group.details.find((p) => p.is_current) || group.details[0]; + if (!cp) return ( + + ๋“ฑ๋ก๋œ ๋‹จ๊ฐ€๊ฐ€ ์—†์–ด์š” + + ); + return ( + + +
+ {/* ์นด๋“œ ํ—ค๋” */} +
+
+ + ์ ์šฉ ๋‹จ๊ฐ€ + ํ˜„์žฌ +
+ {group.details.length > 1 && ( + ์ „์ฒด {group.details.length}๊ฑด ์ค‘ + )} +
+ {/* ์นด๋“œ ๋‚ด์šฉ */} +
+
+ ๊ธฐ๊ฐ„ + + {cp.start_date ? String(cp.start_date).split("T")[0] : "โ€”"} ~ {cp.end_date ? String(cp.end_date).split("T")[0] : "โ€”"} + +
+
+ ๊ธฐ์ค€์œ ํ˜• + {cp.base_price_type_label || "-"} +
+
+ ๊ธฐ์ค€๊ฐ€ + {cp.base_price ? Number(cp.base_price).toLocaleString() : "-"} +
+
+ ํ• ์ธ์œ ํ˜• + {cp.discount_type_label && cp.discount_type_label !== "ํ• ์ธ์—†์Œ" ? cp.discount_type_label : "-"} +
+
+ ํ• ์ธ๊ฐ’ + {cp.discount_value ? Number(cp.discount_value).toLocaleString() : "-"} +
+
+ ๋‹จ์ˆ˜์ฒ˜๋ฆฌ + + {cp.rounding_unit_value + ? (priceCategoryOptions["rounding_unit_value"]?.find((o) => o.code === cp.rounding_unit_value)?.label || cp.rounding_unit_value) + : "-"} + +
+ โ†’ +
+ ๊ณ„์‚ฐ๋‹จ๊ฐ€ + + {(cp.calculated_price || cp.unit_price) ? Number(cp.calculated_price || cp.unit_price).toLocaleString() : "-"} + {cp.currency_label} + +
+
+
+
+
+ ); + })()} + + ); + })} + +
)} @@ -823,73 +1167,88 @@ export default function PurchaseItemPage() {
- {/* โ”€โ”€ ํ’ˆ๋ชฉ ์ˆ˜์ • ๋ชจ๋‹ฌ โ”€โ”€ */} - - - - ๊ตฌ๋งคํ’ˆ๋ชฉ ์ˆ˜์ • + {/* โ”€โ”€ ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ๋ชจ๋‹ฌ โ”€โ”€ */} + + + + {isEditMode ? "ํ’ˆ๋ชฉ ์ˆ˜์ •" : "ํ’ˆ๋ชฉ ๋“ฑ๋ก"} - {editItemForm.item_number || ""} โ€” {editItemForm.item_name || ""} + {isEditMode ? "ํ’ˆ๋ชฉ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ด์š”." : "์ƒˆ๋กœ์šด ํ’ˆ๋ชฉ์„ ๋“ฑ๋กํ•ด์š”."} -
- {/* ํ’ˆ๋ชฉ ๊ธฐ๋ณธ์ •๋ณด (์ฝ๊ธฐ ์ „์šฉ) */} - {[ - { key: "item_number", label: "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, - { key: "item_name", label: "ํ’ˆ๋ช…" }, - { key: "size", label: "๊ทœ๊ฒฉ" }, - { key: "unit", label: "๋‹จ์œ„" }, - { key: "material", label: "์žฌ์งˆ" }, - { key: "status", label: "์ƒํƒœ" }, - ].map((f) => ( -
- - -
- ))} -
- - {/* ๊ตฌ๋งค ์„ค์ • (์ˆ˜์ • ๊ฐ€๋Šฅ) */} -
- - setEditItemForm((p) => ({ ...p, standard_price: e.target.value }))} - placeholder="๊ตฌ๋งค๋‹จ๊ฐ€๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”" - className="h-9" - /> -
-
- - setEditItemForm((p) => ({ ...p, standard_price: e.target.value }))} - placeholder="๊ธฐ์ค€๋‹จ๊ฐ€๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”" - className="h-9" - /> -
-
- - +
+
+ {FORM_FIELDS.map((field) => ( +
+ + {field.type === "image" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + tableName={ITEM_TABLE} + recordId={formData.id || ""} + columnName={field.key} + height="h-32" + /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} ์„ ํƒ`} + /> + ) : field.type === "category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} ์„ ํƒ`} + /> + ) : field.type === "textarea" ? ( +