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" />
+
+
+ {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