refactor: Update table column widths and improve pagination settings in purchase and sales order pages

- Changed column width definitions from fixed to minimum widths for better responsiveness in the purchase order and sales order pages.
- Increased the pagination size from 500 to 5000 for supplier and user data fetching to accommodate larger datasets.
- Enhanced item search functionality by including management item filters in server queries, improving data handling and user experience.
- These changes aim to provide a more flexible and user-friendly interface across multiple company implementations.
This commit is contained in:
kjs
2026-04-13 13:38:44 +09:00
parent 4267b42fdf
commit 77b7a0cdbb
14 changed files with 585 additions and 604 deletions
@@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [
];
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]" },
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
{ key: "received_qty", label: "입고수량", width: "min-w-[90px]" },
{ key: "remain_qty", label: "잔량", width: "min-w-[80px]" },
{ key: "unit_price", label: "단가", width: "min-w-[100px]" },
{ key: "amount", label: "금액", width: "min-w-[100px]" },
{ key: "due_date", label: "납기일", width: "min-w-[160px]" },
{ key: "memo", label: "메모", width: "min-w-[120px]" },
];
const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16";
@@ -237,7 +237,7 @@ export default function PurchaseOrderPage() {
);
try {
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
optMap["supplier_code"] = supps.map((s: any) => ({
@@ -247,7 +247,7 @@ export default function PurchaseOrderPage() {
} catch { /* skip */ }
try {
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
optMap["manager"] = users.map((u: any) => ({
@@ -293,7 +293,7 @@ export default function PurchaseOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
@@ -538,7 +538,7 @@ export default function PurchaseOrderPage() {
}
};
// 품목 검색
// 품목 검색 (수주관리와 동일한 서버 페이징 방식)
const searchItems = async (page?: number, size?: number) => {
const p = page ?? itemPage;
const s = size ?? itemPageSize;
@@ -548,25 +548,24 @@ export default function PurchaseOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
const rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -608,7 +607,7 @@ export default function PurchaseOrderPage() {
try {
const itemIds = selected.map((item) => item.item_number || item.id);
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: {
enabled: true,
filters: [
@@ -671,7 +670,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
autoFilter: true,
});
@@ -693,7 +692,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: supplierCode },
{ columnName: "item_id", operator: "in", value: itemCodes },
@@ -1037,7 +1036,7 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<DndContext sensors={modalSensors} collisionDetection={closestCenter} onDragEnd={handleModalDragEnd}>
<Table className="table-fixed">
<Table>
<TableHeader className="sticky top-0 z-10">
<SortableContext items={visibleModalColumns.map((c) => c.key)} strategy={horizontalListSortingStrategy}>
<TableRow className="bg-muted hover:bg-muted">
@@ -1061,12 +1060,12 @@ export default function PurchaseOrderPage() {
{visibleModalColumns.map((col) => {
switch (col.key) {
case "item_code":
return <TableCell key={col.key} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] font-mono whitespace-nowrap"><span title={row.item_code}>{row.item_code}</span></TableCell>;
case "item_name":
return <TableCell key={col.key} className="text-[13px] max-w-[120px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] whitespace-nowrap"><span title={row.item_name}>{row.item_name}</span></TableCell>;
case "supplier":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[150px]">
{isReadOnly ? (
<span className="text-xs">{(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"}</span>
) : (
@@ -1076,7 +1075,7 @@ export default function PurchaseOrderPage() {
updateDetailRow(idx, "supplier_code", v);
updateDetailRow(idx, "supplier_name", name);
}}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectContent>
{(categoryOptions["supplier_code"] || []).map(o => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1092,11 +1091,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[110px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1106,11 +1105,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}</TableCell>;
case "unit_price":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[120px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1118,21 +1117,21 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>;
case "due_date":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[160px]">
{isReadOnly ? (
<span className="text-xs">{row.due_date}</span>
) : (
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" />
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
case "memo":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[140px]">
{isReadOnly ? (
<span className="text-xs">{row.memo}</span>
) : (
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" />
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
@@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
<Table noWrapper className="min-w-max w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-28 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
<TableHead className="min-w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailRows.map((row, idx) => (
<TableRow key={row._id || idx}>
<TableCell className="text-center font-mono text-[13px] text-muted-foreground/50">{idx + 1}</TableCell>
<TableCell className="max-w-[112px]">
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
<TableCell className="whitespace-nowrap">
<span className="font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
</TableCell>
<TableCell className="max-w-[128px]">
<span className="block truncate text-[13px]" title={row.part_name}>{row.part_name}</span>
<TableCell className="whitespace-nowrap">
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.material}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.material}</TableCell>
<TableCell>
<Input
value={row.packing_material || ""}
onChange={(e) => updateDetailRow(idx, "packing_material", e.target.value)}
placeholder="포장재"
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell>
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(categoryOptions["item_unit"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1371,7 +1371,7 @@ export default function SalesOrderPage() {
min="1"
value={row.qty || "1"}
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1380,7 +1380,7 @@ export default function SalesOrderPage() {
min="0"
value={row.pack_qty || "0"}
onChange={(e) => updateDetailRow(idx, "pack_qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1388,10 +1388,10 @@ export default function SalesOrderPage() {
value={formatNumber(row.unit_price || "")}
onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
readOnly={!allowPriceEdit}
className={cn("h-8 text-xs text-right font-mono w-20", !allowPriceEdit && "bg-muted cursor-not-allowed")}
className={cn("h-8 text-xs text-right font-mono w-full", !allowPriceEdit && "bg-muted cursor-not-allowed")}
/>
</TableCell>
<TableCell className="text-right font-mono text-[13px] font-semibold">
<TableCell className="text-right font-mono text-[13px] font-semibold whitespace-nowrap">
{row.amount ? Number(row.amount).toLocaleString() : "0"}
</TableCell>
<TableCell>
@@ -1399,7 +1399,7 @@ export default function SalesOrderPage() {
type="date"
value={row.due_date || ""}
onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)}
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell className="text-center">
@@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [
];
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]" },
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
{ key: "received_qty", label: "입고수량", width: "min-w-[90px]" },
{ key: "remain_qty", label: "잔량", width: "min-w-[80px]" },
{ key: "unit_price", label: "단가", width: "min-w-[100px]" },
{ key: "amount", label: "금액", width: "min-w-[100px]" },
{ key: "due_date", label: "납기일", width: "min-w-[160px]" },
{ key: "memo", label: "메모", width: "min-w-[120px]" },
];
const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16";
@@ -237,7 +237,7 @@ export default function PurchaseOrderPage() {
);
try {
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
optMap["supplier_code"] = supps.map((s: any) => ({
@@ -247,7 +247,7 @@ export default function PurchaseOrderPage() {
} catch { /* skip */ }
try {
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
optMap["manager"] = users.map((u: any) => ({
@@ -293,7 +293,7 @@ export default function PurchaseOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
@@ -538,7 +538,7 @@ export default function PurchaseOrderPage() {
}
};
// 품목 검색
// 품목 검색 (수주관리와 동일한 서버 페이징 방식)
const searchItems = async (page?: number, size?: number) => {
const p = page ?? itemPage;
const s = size ?? itemPageSize;
@@ -548,25 +548,24 @@ export default function PurchaseOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
const rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -608,7 +607,7 @@ export default function PurchaseOrderPage() {
try {
const itemIds = selected.map((item) => item.item_number || item.id);
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: {
enabled: true,
filters: [
@@ -671,7 +670,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
autoFilter: true,
});
@@ -693,7 +692,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: supplierCode },
{ columnName: "item_id", operator: "in", value: itemCodes },
@@ -1037,7 +1036,7 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<DndContext sensors={modalSensors} collisionDetection={closestCenter} onDragEnd={handleModalDragEnd}>
<Table className="table-fixed">
<Table>
<TableHeader className="sticky top-0 z-10">
<SortableContext items={visibleModalColumns.map((c) => c.key)} strategy={horizontalListSortingStrategy}>
<TableRow className="bg-muted hover:bg-muted">
@@ -1061,12 +1060,12 @@ export default function PurchaseOrderPage() {
{visibleModalColumns.map((col) => {
switch (col.key) {
case "item_code":
return <TableCell key={col.key} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] font-mono whitespace-nowrap"><span title={row.item_code}>{row.item_code}</span></TableCell>;
case "item_name":
return <TableCell key={col.key} className="text-[13px] max-w-[120px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] whitespace-nowrap"><span title={row.item_name}>{row.item_name}</span></TableCell>;
case "supplier":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[150px]">
{isReadOnly ? (
<span className="text-xs">{(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"}</span>
) : (
@@ -1076,7 +1075,7 @@ export default function PurchaseOrderPage() {
updateDetailRow(idx, "supplier_code", v);
updateDetailRow(idx, "supplier_name", name);
}}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectContent>
{(categoryOptions["supplier_code"] || []).map(o => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1092,11 +1091,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[110px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1106,11 +1105,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}</TableCell>;
case "unit_price":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[120px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1118,21 +1117,21 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>;
case "due_date":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[160px]">
{isReadOnly ? (
<span className="text-xs">{row.due_date}</span>
) : (
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" />
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
case "memo":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[140px]">
{isReadOnly ? (
<span className="text-xs">{row.memo}</span>
) : (
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" />
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
@@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
<Table noWrapper className="min-w-max w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-28 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
<TableHead className="min-w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailRows.map((row, idx) => (
<TableRow key={row._id || idx}>
<TableCell className="text-center font-mono text-[13px] text-muted-foreground/50">{idx + 1}</TableCell>
<TableCell className="max-w-[112px]">
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
<TableCell className="whitespace-nowrap">
<span className="font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
</TableCell>
<TableCell className="max-w-[128px]">
<span className="block truncate text-[13px]" title={row.part_name}>{row.part_name}</span>
<TableCell className="whitespace-nowrap">
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.material}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.material}</TableCell>
<TableCell>
<Input
value={row.packing_material || ""}
onChange={(e) => updateDetailRow(idx, "packing_material", e.target.value)}
placeholder="포장재"
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell>
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(categoryOptions["item_unit"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1371,7 +1371,7 @@ export default function SalesOrderPage() {
min="1"
value={row.qty || "1"}
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1380,7 +1380,7 @@ export default function SalesOrderPage() {
min="0"
value={row.pack_qty || "0"}
onChange={(e) => updateDetailRow(idx, "pack_qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1388,10 +1388,10 @@ export default function SalesOrderPage() {
value={formatNumber(row.unit_price || "")}
onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
readOnly={!allowPriceEdit}
className={cn("h-8 text-xs text-right font-mono w-20", !allowPriceEdit && "bg-muted cursor-not-allowed")}
className={cn("h-8 text-xs text-right font-mono w-full", !allowPriceEdit && "bg-muted cursor-not-allowed")}
/>
</TableCell>
<TableCell className="text-right font-mono text-[13px] font-semibold">
<TableCell className="text-right font-mono text-[13px] font-semibold whitespace-nowrap">
{row.amount ? Number(row.amount).toLocaleString() : "0"}
</TableCell>
<TableCell>
@@ -1399,7 +1399,7 @@ export default function SalesOrderPage() {
type="date"
value={row.due_date || ""}
onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)}
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell className="text-center">
@@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [
];
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]" },
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
{ key: "received_qty", label: "입고수량", width: "min-w-[90px]" },
{ key: "remain_qty", label: "잔량", width: "min-w-[80px]" },
{ key: "unit_price", label: "단가", width: "min-w-[100px]" },
{ key: "amount", label: "금액", width: "min-w-[100px]" },
{ key: "due_date", label: "납기일", width: "min-w-[160px]" },
{ key: "memo", label: "메모", width: "min-w-[120px]" },
];
const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16";
@@ -237,7 +237,7 @@ export default function PurchaseOrderPage() {
);
try {
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
optMap["supplier_code"] = supps.map((s: any) => ({
@@ -247,7 +247,7 @@ export default function PurchaseOrderPage() {
} catch { /* skip */ }
try {
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
optMap["manager"] = users.map((u: any) => ({
@@ -293,7 +293,7 @@ export default function PurchaseOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
@@ -538,7 +538,7 @@ export default function PurchaseOrderPage() {
}
};
// 품목 검색
// 품목 검색 (수주관리와 동일한 서버 페이징 방식)
const searchItems = async (page?: number, size?: number) => {
const p = page ?? itemPage;
const s = size ?? itemPageSize;
@@ -548,25 +548,24 @@ export default function PurchaseOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
const rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -608,7 +607,7 @@ export default function PurchaseOrderPage() {
try {
const itemIds = selected.map((item) => item.item_number || item.id);
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: {
enabled: true,
filters: [
@@ -671,7 +670,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
autoFilter: true,
});
@@ -693,7 +692,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: supplierCode },
{ columnName: "item_id", operator: "in", value: itemCodes },
@@ -1037,7 +1036,7 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<DndContext sensors={modalSensors} collisionDetection={closestCenter} onDragEnd={handleModalDragEnd}>
<Table className="table-fixed">
<Table>
<TableHeader className="sticky top-0 z-10">
<SortableContext items={visibleModalColumns.map((c) => c.key)} strategy={horizontalListSortingStrategy}>
<TableRow className="bg-muted hover:bg-muted">
@@ -1061,12 +1060,12 @@ export default function PurchaseOrderPage() {
{visibleModalColumns.map((col) => {
switch (col.key) {
case "item_code":
return <TableCell key={col.key} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] font-mono whitespace-nowrap"><span title={row.item_code}>{row.item_code}</span></TableCell>;
case "item_name":
return <TableCell key={col.key} className="text-[13px] max-w-[120px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] whitespace-nowrap"><span title={row.item_name}>{row.item_name}</span></TableCell>;
case "supplier":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[150px]">
{isReadOnly ? (
<span className="text-xs">{(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"}</span>
) : (
@@ -1076,7 +1075,7 @@ export default function PurchaseOrderPage() {
updateDetailRow(idx, "supplier_code", v);
updateDetailRow(idx, "supplier_name", name);
}}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectContent>
{(categoryOptions["supplier_code"] || []).map(o => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1092,11 +1091,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[110px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1106,11 +1105,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}</TableCell>;
case "unit_price":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[120px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1118,21 +1117,21 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>;
case "due_date":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[160px]">
{isReadOnly ? (
<span className="text-xs">{row.due_date}</span>
) : (
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" />
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
case "memo":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[140px]">
{isReadOnly ? (
<span className="text-xs">{row.memo}</span>
) : (
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" />
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
@@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
<Table noWrapper className="min-w-max w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-28 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
<TableHead className="min-w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailRows.map((row, idx) => (
<TableRow key={row._id || idx}>
<TableCell className="text-center font-mono text-[13px] text-muted-foreground/50">{idx + 1}</TableCell>
<TableCell className="max-w-[112px]">
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
<TableCell className="whitespace-nowrap">
<span className="font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
</TableCell>
<TableCell className="max-w-[128px]">
<span className="block truncate text-[13px]" title={row.part_name}>{row.part_name}</span>
<TableCell className="whitespace-nowrap">
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.material}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.material}</TableCell>
<TableCell>
<Input
value={row.packing_material || ""}
onChange={(e) => updateDetailRow(idx, "packing_material", e.target.value)}
placeholder="포장재"
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell>
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(categoryOptions["item_unit"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1371,7 +1371,7 @@ export default function SalesOrderPage() {
min="1"
value={row.qty || "1"}
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1380,7 +1380,7 @@ export default function SalesOrderPage() {
min="0"
value={row.pack_qty || "0"}
onChange={(e) => updateDetailRow(idx, "pack_qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1388,10 +1388,10 @@ export default function SalesOrderPage() {
value={formatNumber(row.unit_price || "")}
onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
readOnly={!allowPriceEdit}
className={cn("h-8 text-xs text-right font-mono w-20", !allowPriceEdit && "bg-muted cursor-not-allowed")}
className={cn("h-8 text-xs text-right font-mono w-full", !allowPriceEdit && "bg-muted cursor-not-allowed")}
/>
</TableCell>
<TableCell className="text-right font-mono text-[13px] font-semibold">
<TableCell className="text-right font-mono text-[13px] font-semibold whitespace-nowrap">
{row.amount ? Number(row.amount).toLocaleString() : "0"}
</TableCell>
<TableCell>
@@ -1399,7 +1399,7 @@ export default function SalesOrderPage() {
type="date"
value={row.due_date || ""}
onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)}
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell className="text-center">
@@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [
];
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]" },
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
{ key: "received_qty", label: "입고수량", width: "min-w-[90px]" },
{ key: "remain_qty", label: "잔량", width: "min-w-[80px]" },
{ key: "unit_price", label: "단가", width: "min-w-[100px]" },
{ key: "amount", label: "금액", width: "min-w-[100px]" },
{ key: "due_date", label: "납기일", width: "min-w-[160px]" },
{ key: "memo", label: "메모", width: "min-w-[120px]" },
];
const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16";
@@ -237,7 +237,7 @@ export default function PurchaseOrderPage() {
);
try {
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
optMap["supplier_code"] = supps.map((s: any) => ({
@@ -247,7 +247,7 @@ export default function PurchaseOrderPage() {
} catch { /* skip */ }
try {
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
optMap["manager"] = users.map((u: any) => ({
@@ -293,7 +293,7 @@ export default function PurchaseOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
@@ -538,7 +538,7 @@ export default function PurchaseOrderPage() {
}
};
// 품목 검색
// 품목 검색 (수주관리와 동일한 서버 페이징 방식)
const searchItems = async (page?: number, size?: number) => {
const p = page ?? itemPage;
const s = size ?? itemPageSize;
@@ -548,25 +548,24 @@ export default function PurchaseOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
const rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -608,7 +607,7 @@ export default function PurchaseOrderPage() {
try {
const itemIds = selected.map((item) => item.item_number || item.id);
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: {
enabled: true,
filters: [
@@ -671,7 +670,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
autoFilter: true,
});
@@ -693,7 +692,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: supplierCode },
{ columnName: "item_id", operator: "in", value: itemCodes },
@@ -1037,7 +1036,7 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<DndContext sensors={modalSensors} collisionDetection={closestCenter} onDragEnd={handleModalDragEnd}>
<Table className="table-fixed">
<Table>
<TableHeader className="sticky top-0 z-10">
<SortableContext items={visibleModalColumns.map((c) => c.key)} strategy={horizontalListSortingStrategy}>
<TableRow className="bg-muted hover:bg-muted">
@@ -1061,12 +1060,12 @@ export default function PurchaseOrderPage() {
{visibleModalColumns.map((col) => {
switch (col.key) {
case "item_code":
return <TableCell key={col.key} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] font-mono whitespace-nowrap"><span title={row.item_code}>{row.item_code}</span></TableCell>;
case "item_name":
return <TableCell key={col.key} className="text-[13px] max-w-[120px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] whitespace-nowrap"><span title={row.item_name}>{row.item_name}</span></TableCell>;
case "supplier":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[150px]">
{isReadOnly ? (
<span className="text-xs">{(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"}</span>
) : (
@@ -1076,7 +1075,7 @@ export default function PurchaseOrderPage() {
updateDetailRow(idx, "supplier_code", v);
updateDetailRow(idx, "supplier_name", name);
}}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectContent>
{(categoryOptions["supplier_code"] || []).map(o => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1092,11 +1091,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[110px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1106,11 +1105,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}</TableCell>;
case "unit_price":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[120px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1118,21 +1117,21 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>;
case "due_date":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[160px]">
{isReadOnly ? (
<span className="text-xs">{row.due_date}</span>
) : (
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" />
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
case "memo":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[140px]">
{isReadOnly ? (
<span className="text-xs">{row.memo}</span>
) : (
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" />
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
@@ -517,42 +517,44 @@ export default function SalesOrderPage() {
}
};
// 삭제 (마스터 + 디테일)
// 삭제 (선택한 디테일 삭제 → 디테일 0건인 마스터 자동 삭제)
const handleDelete = async () => {
if (checkedIds.length === 0) { toast.error("삭제할 수주를 선택해주세요."); return; }
const selectedItems = orders.filter((o) => checkedIds.includes(o.id));
const orderNos = [...new Set(selectedItems.map((o) => o.order_no))];
const ok = await confirm(`${orderNos.length}건의 수주를 삭제하시겠습니까?`, {
if (checkedIds.length === 0) { toast.error("삭제할 항목을 선택해주세요."); return; }
const ok = await confirm(`${checkedIds.length}건의 수주 항목을 삭제하시겠습니까?`, {
description: "삭제된 데이터는 복구할 수 없습니다.",
variant: "destructive",
confirmText: "삭제",
});
if (!ok) return;
try {
// 1. 선택한 디테일 삭제
await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, {
data: checkedIds.map((id) => ({ id })),
});
// 2. 영향받는 수주번호의 잔여 디테일 확인 → 0건이면 마스터도 삭제
const selectedItems = orders.filter((o) => checkedIds.includes(o.id));
const orderNos = [...new Set(selectedItems.map((o) => o.order_no))];
for (const orderNo of orderNos) {
// 디테일 삭제
const detailRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 9999,
dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] },
autoFilter: true,
});
const details = detailRes.data?.data?.data || detailRes.data?.data?.rows || [];
if (details.length > 0) {
await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, {
data: details.map((d: any) => ({ id: d.id })),
});
}
// 마스터 삭제
const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, {
const remainRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 1,
dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] },
autoFilter: true,
});
const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || [];
if (masters.length > 0) {
await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, {
data: masters.map((m: any) => ({ id: m.id })),
const remaining = remainRes.data?.data?.data || remainRes.data?.data?.rows || [];
if (remaining.length === 0) {
// 디테일 0건 → 마스터 삭제
const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, {
page: 1, size: 1,
dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] },
autoFilter: true,
});
const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || [];
if (masters.length > 0) {
await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, {
data: masters.map((m: any) => ({ id: m.id })),
});
}
}
}
toast.success("삭제되었습니다.");
@@ -622,6 +624,15 @@ export default function SalesOrderPage() {
const filters: any[] = [];
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
// 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
const partnerId = masterForm.partner_id;
@@ -630,7 +641,7 @@ export default function SalesOrderPage() {
if (isCustomerPrice && partnerId) {
try {
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
autoFilter: true,
});
@@ -640,31 +651,22 @@ export default function SalesOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
let rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
// 거래처우선일 때 연결된 품목만 표시
// 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터)
if (customerItemIds) {
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
}
// 관리품목 필터 (코드/라벨 혼재 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -1318,44 +1320,44 @@ export default function SalesOrderPage() {
<Table noWrapper className="min-w-max w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-28 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
<TableHead className="min-w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailRows.map((row, idx) => (
<TableRow key={row._id || idx}>
<TableCell className="text-center font-mono text-[13px] text-muted-foreground/50">{idx + 1}</TableCell>
<TableCell className="max-w-[112px]">
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
<TableCell className="whitespace-nowrap">
<span className="font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
</TableCell>
<TableCell className="max-w-[128px]">
<span className="block truncate text-[13px]" title={row.part_name}>{row.part_name}</span>
<TableCell className="whitespace-nowrap">
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.material}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.material}</TableCell>
<TableCell>
<Input
value={row.packing_material || ""}
onChange={(e) => updateDetailRow(idx, "packing_material", e.target.value)}
placeholder="포장재"
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell>
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(categoryOptions["item_unit"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1369,7 +1371,7 @@ export default function SalesOrderPage() {
min="1"
value={row.qty || "1"}
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1378,7 +1380,7 @@ export default function SalesOrderPage() {
min="0"
value={row.pack_qty || "0"}
onChange={(e) => updateDetailRow(idx, "pack_qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1386,10 +1388,10 @@ export default function SalesOrderPage() {
value={formatNumber(row.unit_price || "")}
onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
readOnly={!allowPriceEdit}
className={cn("h-8 text-xs text-right font-mono w-20", !allowPriceEdit && "bg-muted cursor-not-allowed")}
className={cn("h-8 text-xs text-right font-mono w-full", !allowPriceEdit && "bg-muted cursor-not-allowed")}
/>
</TableCell>
<TableCell className="text-right font-mono text-[13px] font-semibold">
<TableCell className="text-right font-mono text-[13px] font-semibold whitespace-nowrap">
{row.amount ? Number(row.amount).toLocaleString() : "0"}
</TableCell>
<TableCell>
@@ -1397,7 +1399,7 @@ export default function SalesOrderPage() {
type="date"
value={row.due_date || ""}
onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)}
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell className="text-center">
@@ -1472,15 +1474,7 @@ export default function SalesOrderPage() {
onKeyDown={(e) => e.key === "Enter" && triggerNewSearch()}
className="h-9 flex-1"
/>
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted text-xs font-medium text-muted-foreground whitespace-nowrap"></div>
<Button size="sm" onClick={triggerNewSearch} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>
@@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [
];
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]" },
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
{ key: "received_qty", label: "입고수량", width: "min-w-[90px]" },
{ key: "remain_qty", label: "잔량", width: "min-w-[80px]" },
{ key: "unit_price", label: "단가", width: "min-w-[100px]" },
{ key: "amount", label: "금액", width: "min-w-[100px]" },
{ key: "due_date", label: "납기일", width: "min-w-[160px]" },
{ key: "memo", label: "메모", width: "min-w-[120px]" },
];
const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16";
@@ -237,7 +237,7 @@ export default function PurchaseOrderPage() {
);
try {
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
optMap["supplier_code"] = supps.map((s: any) => ({
@@ -247,7 +247,7 @@ export default function PurchaseOrderPage() {
} catch { /* skip */ }
try {
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
optMap["manager"] = users.map((u: any) => ({
@@ -293,7 +293,7 @@ export default function PurchaseOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
@@ -538,7 +538,7 @@ export default function PurchaseOrderPage() {
}
};
// 품목 검색
// 품목 검색 (수주관리와 동일한 서버 페이징 방식)
const searchItems = async (page?: number, size?: number) => {
const p = page ?? itemPage;
const s = size ?? itemPageSize;
@@ -548,25 +548,24 @@ export default function PurchaseOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
const rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -608,7 +607,7 @@ export default function PurchaseOrderPage() {
try {
const itemIds = selected.map((item) => item.item_number || item.id);
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: {
enabled: true,
filters: [
@@ -671,7 +670,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
autoFilter: true,
});
@@ -693,7 +692,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: supplierCode },
{ columnName: "item_id", operator: "in", value: itemCodes },
@@ -1037,7 +1036,7 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<DndContext sensors={modalSensors} collisionDetection={closestCenter} onDragEnd={handleModalDragEnd}>
<Table className="table-fixed">
<Table>
<TableHeader className="sticky top-0 z-10">
<SortableContext items={visibleModalColumns.map((c) => c.key)} strategy={horizontalListSortingStrategy}>
<TableRow className="bg-muted hover:bg-muted">
@@ -1061,12 +1060,12 @@ export default function PurchaseOrderPage() {
{visibleModalColumns.map((col) => {
switch (col.key) {
case "item_code":
return <TableCell key={col.key} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] font-mono whitespace-nowrap"><span title={row.item_code}>{row.item_code}</span></TableCell>;
case "item_name":
return <TableCell key={col.key} className="text-[13px] max-w-[120px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] whitespace-nowrap"><span title={row.item_name}>{row.item_name}</span></TableCell>;
case "supplier":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[150px]">
{isReadOnly ? (
<span className="text-xs">{(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"}</span>
) : (
@@ -1076,7 +1075,7 @@ export default function PurchaseOrderPage() {
updateDetailRow(idx, "supplier_code", v);
updateDetailRow(idx, "supplier_name", name);
}}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectContent>
{(categoryOptions["supplier_code"] || []).map(o => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1092,11 +1091,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[110px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1106,11 +1105,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}</TableCell>;
case "unit_price":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[120px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1118,21 +1117,21 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>;
case "due_date":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[160px]">
{isReadOnly ? (
<span className="text-xs">{row.due_date}</span>
) : (
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" />
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
case "memo":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[140px]">
{isReadOnly ? (
<span className="text-xs">{row.memo}</span>
) : (
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" />
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
@@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
<Table noWrapper className="min-w-max w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-28 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
<TableHead className="min-w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailRows.map((row, idx) => (
<TableRow key={row._id || idx}>
<TableCell className="text-center font-mono text-[13px] text-muted-foreground/50">{idx + 1}</TableCell>
<TableCell className="max-w-[112px]">
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
<TableCell className="whitespace-nowrap">
<span className="font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
</TableCell>
<TableCell className="max-w-[128px]">
<span className="block truncate text-[13px]" title={row.part_name}>{row.part_name}</span>
<TableCell className="whitespace-nowrap">
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.material}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.material}</TableCell>
<TableCell>
<Input
value={row.packing_material || ""}
onChange={(e) => updateDetailRow(idx, "packing_material", e.target.value)}
placeholder="포장재"
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell>
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(categoryOptions["item_unit"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1371,7 +1371,7 @@ export default function SalesOrderPage() {
min="1"
value={row.qty || "1"}
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1380,7 +1380,7 @@ export default function SalesOrderPage() {
min="0"
value={row.pack_qty || "0"}
onChange={(e) => updateDetailRow(idx, "pack_qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1388,10 +1388,10 @@ export default function SalesOrderPage() {
value={formatNumber(row.unit_price || "")}
onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
readOnly={!allowPriceEdit}
className={cn("h-8 text-xs text-right font-mono w-20", !allowPriceEdit && "bg-muted cursor-not-allowed")}
className={cn("h-8 text-xs text-right font-mono w-full", !allowPriceEdit && "bg-muted cursor-not-allowed")}
/>
</TableCell>
<TableCell className="text-right font-mono text-[13px] font-semibold">
<TableCell className="text-right font-mono text-[13px] font-semibold whitespace-nowrap">
{row.amount ? Number(row.amount).toLocaleString() : "0"}
</TableCell>
<TableCell>
@@ -1399,7 +1399,7 @@ export default function SalesOrderPage() {
type="date"
value={row.due_date || ""}
onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)}
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell className="text-center">
@@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [
];
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]" },
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
{ key: "received_qty", label: "입고수량", width: "min-w-[90px]" },
{ key: "remain_qty", label: "잔량", width: "min-w-[80px]" },
{ key: "unit_price", label: "단가", width: "min-w-[100px]" },
{ key: "amount", label: "금액", width: "min-w-[100px]" },
{ key: "due_date", label: "납기일", width: "min-w-[160px]" },
{ key: "memo", label: "메모", width: "min-w-[120px]" },
];
const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16";
@@ -237,7 +237,7 @@ export default function PurchaseOrderPage() {
);
try {
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
optMap["supplier_code"] = supps.map((s: any) => ({
@@ -247,7 +247,7 @@ export default function PurchaseOrderPage() {
} catch { /* skip */ }
try {
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
optMap["manager"] = users.map((u: any) => ({
@@ -293,7 +293,7 @@ export default function PurchaseOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
@@ -538,7 +538,7 @@ export default function PurchaseOrderPage() {
}
};
// 품목 검색
// 품목 검색 (수주관리와 동일한 서버 페이징 방식)
const searchItems = async (page?: number, size?: number) => {
const p = page ?? itemPage;
const s = size ?? itemPageSize;
@@ -548,25 +548,24 @@ export default function PurchaseOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
const rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -608,7 +607,7 @@ export default function PurchaseOrderPage() {
try {
const itemIds = selected.map((item) => item.item_number || item.id);
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: {
enabled: true,
filters: [
@@ -671,7 +670,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
autoFilter: true,
});
@@ -693,7 +692,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: supplierCode },
{ columnName: "item_id", operator: "in", value: itemCodes },
@@ -1037,7 +1036,7 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<DndContext sensors={modalSensors} collisionDetection={closestCenter} onDragEnd={handleModalDragEnd}>
<Table className="table-fixed">
<Table>
<TableHeader className="sticky top-0 z-10">
<SortableContext items={visibleModalColumns.map((c) => c.key)} strategy={horizontalListSortingStrategy}>
<TableRow className="bg-muted hover:bg-muted">
@@ -1061,12 +1060,12 @@ export default function PurchaseOrderPage() {
{visibleModalColumns.map((col) => {
switch (col.key) {
case "item_code":
return <TableCell key={col.key} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] font-mono whitespace-nowrap"><span title={row.item_code}>{row.item_code}</span></TableCell>;
case "item_name":
return <TableCell key={col.key} className="text-[13px] max-w-[120px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] whitespace-nowrap"><span title={row.item_name}>{row.item_name}</span></TableCell>;
case "supplier":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[150px]">
{isReadOnly ? (
<span className="text-xs">{(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"}</span>
) : (
@@ -1076,7 +1075,7 @@ export default function PurchaseOrderPage() {
updateDetailRow(idx, "supplier_code", v);
updateDetailRow(idx, "supplier_name", name);
}}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectContent>
{(categoryOptions["supplier_code"] || []).map(o => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1092,11 +1091,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[110px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1106,11 +1105,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}</TableCell>;
case "unit_price":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[120px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1118,21 +1117,21 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>;
case "due_date":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[160px]">
{isReadOnly ? (
<span className="text-xs">{row.due_date}</span>
) : (
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" />
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
case "memo":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[140px]">
{isReadOnly ? (
<span className="text-xs">{row.memo}</span>
) : (
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" />
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
@@ -1320,44 +1320,44 @@ export default function SalesOrderPage() {
<Table noWrapper className="min-w-max w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-28 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
<TableHead className="min-w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailRows.map((row, idx) => (
<TableRow key={row._id || idx}>
<TableCell className="text-center font-mono text-[13px] text-muted-foreground/50">{idx + 1}</TableCell>
<TableCell className="max-w-[112px]">
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
<TableCell className="whitespace-nowrap">
<span className="font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
</TableCell>
<TableCell className="max-w-[128px]">
<span className="block truncate text-[13px]" title={row.part_name}>{row.part_name}</span>
<TableCell className="whitespace-nowrap">
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.material}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.material}</TableCell>
<TableCell>
<Input
value={row.packing_material || ""}
onChange={(e) => updateDetailRow(idx, "packing_material", e.target.value)}
placeholder="포장재"
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell>
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(categoryOptions["item_unit"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1371,7 +1371,7 @@ export default function SalesOrderPage() {
min="1"
value={row.qty || "1"}
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1380,7 +1380,7 @@ export default function SalesOrderPage() {
min="0"
value={row.pack_qty || "0"}
onChange={(e) => updateDetailRow(idx, "pack_qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1388,10 +1388,10 @@ export default function SalesOrderPage() {
value={formatNumber(row.unit_price || "")}
onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
readOnly={!allowPriceEdit}
className={cn("h-8 text-xs text-right font-mono w-20", !allowPriceEdit && "bg-muted cursor-not-allowed")}
className={cn("h-8 text-xs text-right font-mono w-full", !allowPriceEdit && "bg-muted cursor-not-allowed")}
/>
</TableCell>
<TableCell className="text-right font-mono text-[13px] font-semibold">
<TableCell className="text-right font-mono text-[13px] font-semibold whitespace-nowrap">
{row.amount ? Number(row.amount).toLocaleString() : "0"}
</TableCell>
<TableCell>
@@ -1399,7 +1399,7 @@ export default function SalesOrderPage() {
type="date"
value={row.due_date || ""}
onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)}
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell className="text-center">
@@ -88,18 +88,18 @@ const GRID_COLUMNS_CONFIG = [
];
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]" },
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
{ key: "received_qty", label: "입고수량", width: "min-w-[90px]" },
{ key: "remain_qty", label: "잔량", width: "min-w-[80px]" },
{ key: "unit_price", label: "단가", width: "min-w-[100px]" },
{ key: "amount", label: "금액", width: "min-w-[100px]" },
{ key: "due_date", label: "납기일", width: "min-w-[160px]" },
{ key: "memo", label: "메모", width: "min-w-[120px]" },
];
const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16";
@@ -237,7 +237,7 @@ export default function PurchaseOrderPage() {
);
try {
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
optMap["supplier_code"] = supps.map((s: any) => ({
@@ -247,7 +247,7 @@ export default function PurchaseOrderPage() {
} catch { /* skip */ }
try {
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
page: 1, size: 500, autoFilter: true,
page: 1, size: 5000, autoFilter: true,
});
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
optMap["manager"] = users.map((u: any) => ({
@@ -293,7 +293,7 @@ export default function PurchaseOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: detailFilters.length > 0 ? { enabled: true, filters: detailFilters } : undefined,
autoFilter: true,
sort: { columnName: "purchase_no", order: "desc" },
@@ -538,7 +538,7 @@ export default function PurchaseOrderPage() {
}
};
// 품목 검색
// 품목 검색 (수주관리와 동일한 서버 페이징 방식)
const searchItems = async (page?: number, size?: number) => {
const p = page ?? itemPage;
const s = size ?? itemPageSize;
@@ -548,25 +548,24 @@ export default function PurchaseOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
const rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -608,7 +607,7 @@ export default function PurchaseOrderPage() {
try {
const itemIds = selected.map((item) => item.item_number || item.id);
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: {
enabled: true,
filters: [
@@ -671,7 +670,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
autoFilter: true,
});
@@ -693,7 +692,7 @@ export default function PurchaseOrderPage() {
if (itemCodes.length === 0) return;
try {
const res = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: supplierCode },
{ columnName: "item_id", operator: "in", value: itemCodes },
@@ -1037,7 +1036,7 @@ export default function PurchaseOrderPage() {
) : (
<div className="border rounded-lg overflow-x-auto">
<DndContext sensors={modalSensors} collisionDetection={closestCenter} onDragEnd={handleModalDragEnd}>
<Table className="table-fixed">
<Table>
<TableHeader className="sticky top-0 z-10">
<SortableContext items={visibleModalColumns.map((c) => c.key)} strategy={horizontalListSortingStrategy}>
<TableRow className="bg-muted hover:bg-muted">
@@ -1061,12 +1060,12 @@ export default function PurchaseOrderPage() {
{visibleModalColumns.map((col) => {
switch (col.key) {
case "item_code":
return <TableCell key={col.key} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] font-mono whitespace-nowrap"><span title={row.item_code}>{row.item_code}</span></TableCell>;
case "item_name":
return <TableCell key={col.key} className="text-[13px] max-w-[120px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>;
return <TableCell key={col.key} className="text-[13px] whitespace-nowrap"><span title={row.item_name}>{row.item_name}</span></TableCell>;
case "supplier":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[150px]">
{isReadOnly ? (
<span className="text-xs">{(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"}</span>
) : (
@@ -1076,7 +1075,7 @@ export default function PurchaseOrderPage() {
updateDetailRow(idx, "supplier_code", v);
updateDetailRow(idx, "supplier_name", name);
}}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="공급업체" /></SelectTrigger>
<SelectContent>
{(categoryOptions["supplier_code"] || []).map(o => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1092,11 +1091,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[110px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.order_qty || "")} onChange={(e) => updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1106,11 +1105,11 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}</TableCell>;
case "unit_price":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[120px]">
{isReadOnly ? (
<span className="text-xs text-right font-mono block">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</span>
) : (
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" />
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono w-full" />
)}
</TableCell>
);
@@ -1118,21 +1117,21 @@ export default function PurchaseOrderPage() {
return <TableCell key={col.key} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>;
case "due_date":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[160px]">
{isReadOnly ? (
<span className="text-xs">{row.due_date}</span>
) : (
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" />
<Input type="date" value={row.due_date || ""} onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
case "memo":
return (
<TableCell key={col.key}>
<TableCell key={col.key} className="min-w-[140px]">
{isReadOnly ? (
<span className="text-xs">{row.memo}</span>
) : (
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" />
<Input value={row.memo || ""} onChange={(e) => updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs w-full" />
)}
</TableCell>
);
@@ -517,42 +517,44 @@ export default function SalesOrderPage() {
}
};
// 삭제 (마스터 + 디테일)
// 삭제 (선택한 디테일 삭제 → 디테일 0건인 마스터 자동 삭제)
const handleDelete = async () => {
if (checkedIds.length === 0) { toast.error("삭제할 수주를 선택해주세요."); return; }
const selectedItems = orders.filter((o) => checkedIds.includes(o.id));
const orderNos = [...new Set(selectedItems.map((o) => o.order_no))];
const ok = await confirm(`${orderNos.length}건의 수주를 삭제하시겠습니까?`, {
if (checkedIds.length === 0) { toast.error("삭제할 항목을 선택해주세요."); return; }
const ok = await confirm(`${checkedIds.length}건의 수주 항목을 삭제하시겠습니까?`, {
description: "삭제된 데이터는 복구할 수 없습니다.",
variant: "destructive",
confirmText: "삭제",
});
if (!ok) return;
try {
// 1. 선택한 디테일 삭제
await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, {
data: checkedIds.map((id) => ({ id })),
});
// 2. 영향받는 수주번호의 잔여 디테일 확인 → 0건이면 마스터도 삭제
const selectedItems = orders.filter((o) => checkedIds.includes(o.id));
const orderNos = [...new Set(selectedItems.map((o) => o.order_no))];
for (const orderNo of orderNos) {
// 디테일 삭제
const detailRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 9999,
dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] },
autoFilter: true,
});
const details = detailRes.data?.data?.data || detailRes.data?.data?.rows || [];
if (details.length > 0) {
await apiClient.delete(`/table-management/tables/${DETAIL_TABLE}/delete`, {
data: details.map((d: any) => ({ id: d.id })),
});
}
// 마스터 삭제
const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, {
const remainRes = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, {
page: 1, size: 1,
dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] },
autoFilter: true,
});
const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || [];
if (masters.length > 0) {
await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, {
data: masters.map((m: any) => ({ id: m.id })),
const remaining = remainRes.data?.data?.data || remainRes.data?.data?.rows || [];
if (remaining.length === 0) {
// 디테일 0건 → 마스터 삭제
const masterRes = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, {
page: 1, size: 1,
dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] },
autoFilter: true,
});
const masters = masterRes.data?.data?.data || masterRes.data?.data?.rows || [];
if (masters.length > 0) {
await apiClient.delete(`/table-management/tables/${MASTER_TABLE}/delete`, {
data: masters.map((m: any) => ({ id: m.id })),
});
}
}
}
toast.success("삭제되었습니다.");
@@ -622,6 +624,15 @@ export default function SalesOrderPage() {
const filters: any[] = [];
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
// 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
const partnerId = masterForm.partner_id;
@@ -630,7 +641,7 @@ export default function SalesOrderPage() {
if (isCustomerPrice && partnerId) {
try {
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
autoFilter: true,
});
@@ -640,31 +651,22 @@ export default function SalesOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
let rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
// 거래처우선일 때 연결된 품목만 표시
// 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터)
if (customerItemIds) {
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
}
// 관리품목 필터 (코드/라벨 혼재 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -1318,44 +1320,44 @@ export default function SalesOrderPage() {
<Table noWrapper className="min-w-max w-full">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-28 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-20 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-32 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-24 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
<TableHead className="min-w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[110px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">/</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailRows.map((row, idx) => (
<TableRow key={row._id || idx}>
<TableCell className="text-center font-mono text-[13px] text-muted-foreground/50">{idx + 1}</TableCell>
<TableCell className="max-w-[112px]">
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
<TableCell className="whitespace-nowrap">
<span className="font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
</TableCell>
<TableCell className="max-w-[128px]">
<span className="block truncate text-[13px]" title={row.part_name}>{row.part_name}</span>
<TableCell className="whitespace-nowrap">
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.material}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.spec}</TableCell>
<TableCell className="text-[13px] text-muted-foreground whitespace-nowrap">{row.material}</TableCell>
<TableCell>
<Input
value={row.packing_material || ""}
onChange={(e) => updateDetailRow(idx, "packing_material", e.target.value)}
placeholder="포장재"
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell>
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(categoryOptions["item_unit"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
@@ -1369,7 +1371,7 @@ export default function SalesOrderPage() {
min="1"
value={row.qty || "1"}
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1378,7 +1380,7 @@ export default function SalesOrderPage() {
min="0"
value={row.pack_qty || "0"}
onChange={(e) => updateDetailRow(idx, "pack_qty", e.target.value)}
className="h-8 text-xs text-right font-mono w-16"
className="h-8 text-xs text-right font-mono w-full"
/>
</TableCell>
<TableCell>
@@ -1386,10 +1388,10 @@ export default function SalesOrderPage() {
value={formatNumber(row.unit_price || "")}
onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
readOnly={!allowPriceEdit}
className={cn("h-8 text-xs text-right font-mono w-20", !allowPriceEdit && "bg-muted cursor-not-allowed")}
className={cn("h-8 text-xs text-right font-mono w-full", !allowPriceEdit && "bg-muted cursor-not-allowed")}
/>
</TableCell>
<TableCell className="text-right font-mono text-[13px] font-semibold">
<TableCell className="text-right font-mono text-[13px] font-semibold whitespace-nowrap">
{row.amount ? Number(row.amount).toLocaleString() : "0"}
</TableCell>
<TableCell>
@@ -1397,7 +1399,7 @@ export default function SalesOrderPage() {
type="date"
value={row.due_date || ""}
onChange={(e) => updateDetailRow(idx, "due_date", e.target.value)}
className="h-8 text-xs"
className="h-8 text-xs w-full"
/>
</TableCell>
<TableCell className="text-center">
@@ -1472,15 +1474,7 @@ export default function SalesOrderPage() {
onKeyDown={(e) => e.key === "Enter" && triggerNewSearch()}
className="h-9 flex-1"
/>
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted text-xs font-medium text-muted-foreground whitespace-nowrap"></div>
<Button size="sm" onClick={triggerNewSearch} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>