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 (
+
+ );
+}
+
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() {
{/* ์ก์
๋ฒํผ ์์ญ */}
-
-
+
+ ํ๋ชฉ ์ถ๊ฐ
+
+ {
+ const item = items.find((i) => i.id === selectedItemId);
+ if (item) openEditModal(item);
+ }}>
์์
ts.setOpen(true)}>
@@ -691,7 +939,7 @@ export default function PurchaseItemPage() {
emptyMessage="๋ฑ๋ก๋ ๊ตฌ๋งคํ๋ชฉ์ด ์์ด์"
selectedId={selectedItemId}
onSelect={(id) => setSelectedItemId(id)}
- onRowDoubleClick={() => openEditItem()}
+ onRowDoubleClick={(row) => openEditModal(row)}
showRowNumber
showPagination
defaultPageSize={20}
@@ -722,9 +970,11 @@ export default function PurchaseItemPage() {
๊ณต๊ธ์
์ฒด๋ณ ๋จ๊ฐ
-
- {supplierItems.length}
-
+ {Object.keys(supplierGroups).length > 0 && (
+
+ {Object.keys(supplierGroups).length}
+
+ )}
- {/* ๊ณต๊ธ์
์ฒด ํ
์ด๋ธ */}
+ {/* ๊ณต๊ธ์
์ฒด ํ
์ด๋ธ (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([]);
- }}
- />
-
- ๊ณต๊ธ์
์ฒด์ฝ๋
- ๊ณต๊ธ์
์ฒด๋ช
- ๊ณต๊ธ์
์ฒดํ๋ฒ
- ๊ณต๊ธ์
์ฒดํ๋ช
- ๊ธฐ์ค๊ฐ
- ๋จ๊ฐ
- ํตํ
+
- )}
+ {
+ 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() {
- {/* โโ ํ๋ชฉ ์์ ๋ชจ๋ฌ โโ */}
-