From c8922e23b0d41fdb9a7f5d593c368285cdfc7460 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 10 Apr 2026 17:31:57 +0900 Subject: [PATCH] 123 --- .../(main)/COMPANY_16/purchase/order/page.tsx | 280 ++++++++++++------ .../COMPANY_16/purchase/supplier/page.tsx | 8 +- .../(main)/COMPANY_16/sales/customer/page.tsx | 8 +- 3 files changed, 202 insertions(+), 94 deletions(-) diff --git a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx index 1bc3bc88..143a84a9 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx @@ -15,9 +15,12 @@ import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Package, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, - Settings2, + Settings2, GripVertical, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { apiClient } from "@/lib/api/client"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; @@ -84,6 +87,49 @@ const GRID_COLUMNS_CONFIG = [ { key: "memo", label: "메모" }, ]; +const MODAL_DETAIL_COLUMNS = [ + { key: "item_code", label: "품번", width: "w-[120px]" }, + { key: "item_name", label: "품명", width: "w-[120px]" }, + { key: "supplier", label: "공급업체", width: "w-[150px]" }, + { key: "spec", label: "규격", width: "w-[80px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "order_qty", label: "발주수량", width: "w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "w-[80px]" }, + { key: "unit_price", label: "단가", width: "w-[100px]" }, + { key: "amount", label: "금액", width: "w-[100px]" }, + { key: "due_date", label: "납기일", width: "w-[160px]" }, + { key: "memo", label: "메모", width: "w-[120px]" }, +]; + +const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; + +function SortableModalHead({ col }: { col: { key: string; label: string; width: string } }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( + +
+ + {col.label} +
+
+ ); +} + export default function PurchaseOrderPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); @@ -121,8 +167,43 @@ export default function PurchaseOrderPage() { // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); + // 모달 품목 테이블 컬럼 순서 (드래그 재정렬) + const [modalColumns, setModalColumns] = useState(MODAL_DETAIL_COLUMNS); + const modalSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); + + useEffect(() => { + const saved = localStorage.getItem(MODAL_COL_ORDER_KEY); + if (saved) { + try { + const order = JSON.parse(saved) as string[]; + const reordered = order.map((key) => MODAL_DETAIL_COLUMNS.find((c) => c.key === key)).filter(Boolean) as typeof MODAL_DETAIL_COLUMNS; + const remaining = MODAL_DETAIL_COLUMNS.filter((c) => !order.includes(c.key)); + setModalColumns([...reordered, ...remaining]); + } catch { /* skip */ } + } + }, []); + + const handleModalDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setModalColumns((prev) => { + const oldIndex = prev.findIndex((c) => c.key === active.id); + const newIndex = prev.findIndex((c) => c.key === over.id); + const next = arrayMove(prev, oldIndex, newIndex); + localStorage.setItem(MODAL_COL_ORDER_KEY, JSON.stringify(next.map((c) => c.key))); + return next; + }); + }; + const isReadOnly = masterForm.status === "입고완료" || masterForm.status === "취소"; + const visibleModalColumns = useMemo(() => { + return modalColumns.filter((col) => { + if (col.key === "supplier" && masterForm.input_mode !== "itemFirst") return false; + return true; + }); + }, [modalColumns, masterForm.input_mode]); + // 카테고리 로드 useEffect(() => { const loadCategories = async () => { @@ -1012,96 +1093,115 @@ export default function PurchaseOrderPage() { ) : (
- - - - {!isReadOnly && } - 품번 - 품명 - {masterForm.input_mode === "itemFirst" && ( - 공급업체 - )} - 규격 - 단위 - 발주수량 - 입고수량 - 잔량 - 단가 - 금액 - 납기일 - 메모 - - - - {detailRows.map((row, idx) => ( - - {!isReadOnly && ( - - - - )} - {row.item_code} - {row.item_name} - {masterForm.input_mode === "itemFirst" && ( - - {isReadOnly ? ( - {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} - ) : ( - - )} - - )} - {row.spec} - {row.unit} - - {isReadOnly ? ( - {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + +
+ + c.key)} strategy={horizontalListSortingStrategy}> + + {!isReadOnly && } + {visibleModalColumns.map((col) => ( + + ))} + + + + + {detailRows.map((row, idx) => ( + + {!isReadOnly && ( + + + )} - - {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"} - {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"} - - {isReadOnly ? ( - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - {row.amount ? Number(row.amount).toLocaleString() : ""} - - {isReadOnly ? ( - {row.due_date} - ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> - )} - - - {isReadOnly ? ( - {row.memo} - ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> - )} - - - ))} - -
+ {visibleModalColumns.map((col) => { + switch (col.key) { + case "item_code": + return {row.item_code}; + case "item_name": + return {row.item_name}; + case "supplier": + return ( + + {isReadOnly ? ( + {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} + ) : ( + + )} + + ); + case "spec": + return {row.spec}; + case "unit": + return {row.unit}; + case "order_qty": + return ( + + {isReadOnly ? ( + {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "received_qty": + return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; + case "remain_qty": + return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; + case "unit_price": + return ( + + {isReadOnly ? ( + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "amount": + return {row.amount ? Number(row.amount).toLocaleString() : ""}; + case "due_date": + return ( + + {isReadOnly ? ( + {row.due_date} + ) : ( + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + )} + + ); + case "memo": + return ( + + {isReadOnly ? ( + {row.memo} + ) : ( + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + )} + + ); + default: + return ; + } + })} + + ))} + + +
)} diff --git a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx index 20c70b57..8f2755e5 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx @@ -971,6 +971,7 @@ export default function SupplierManagementPage() { { columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code }, { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; mappingRows = allMappings @@ -991,7 +992,8 @@ export default function SupplierManagementPage() { { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, }); - const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || []) + .sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || "")); priceRows = allPriceData.map((p: any) => ({ _id: `p_existing_${p.id}`, start_date: p.start_date ? String(p.start_date).split("T")[0] : "", @@ -1043,6 +1045,7 @@ export default function SupplierManagementPage() { { columnName: "supplier_id", operator: "equals", value: selectedSupplier.supplier_code }, { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || []; } catch { /* skip */ } @@ -1092,7 +1095,8 @@ export default function SupplierManagementPage() { { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, }); - existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []; + existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []) + .sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || "")); } catch { /* skip */ } // 단가 upsert diff --git a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx index 30fb95cc..eb3898d9 100644 --- a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx @@ -988,6 +988,7 @@ export default function CustomerManagementPage() { { columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code }, { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; mappingRows = allMappings @@ -1008,7 +1009,8 @@ export default function CustomerManagementPage() { { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, }); - const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || []) + .sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || "")); priceRows = allPriceData.map((p: any) => ({ _id: `p_existing_${p.id}`, start_date: p.start_date ? String(p.start_date).split("T")[0] : "", @@ -1063,6 +1065,7 @@ export default function CustomerManagementPage() { { columnName: "customer_id", operator: "equals", value: selectedCustomer.customer_code }, { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || []; } catch { /* skip */ } @@ -1112,7 +1115,8 @@ export default function CustomerManagementPage() { { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, }); - existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []; + existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []) + .sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || "")); } catch { /* skip */ } // 단가 upsert