feat: Implement duplicate inbound registration check and enhance receiving page

- Added a check in the receiving controller to prevent duplicate inbound registrations based on the inbound number, ensuring idempotency.
- Updated the receiving page to maintain selected items across different inbound types, improving user experience.
- Enhanced the item mapping logic to utilize inventory unit codes, ensuring accurate data representation.
- Adjusted the layout to include a new column for inbound type in the receiving table, providing better visibility of item classifications.
This commit is contained in:
kjs
2026-04-16 11:31:41 +09:00
parent d3491a79bb
commit 0e09b9e686
20 changed files with 243 additions and 160 deletions

View File

@@ -198,6 +198,17 @@ export async function create(req: AuthenticatedRequest, res: Response) {
}
const insertedDetails: any[] = [];
// 기존 디테일이 있으면 스킵 (멱등성 — 같은 inbound_number로 2번 호출 방지)
const existingDetails = await client.query(
`SELECT COUNT(*) AS cnt FROM inbound_detail WHERE company_code = $1 AND inbound_id = $2`,
[companyCode, inboundNumber]
);
if (parseInt(existingDetails.rows[0].cnt, 10) > 0) {
await client.query("COMMIT");
client.release();
return res.json({ success: true, data: [], message: "이미 등록된 입고입니다." });
}
// 2. 디테일 INSERT (inbound_detail) + 재고/발주 업데이트
for (let i = 0; i < items.length; i++) {
const item = items[i];

View File

@@ -104,10 +104,10 @@ export default function InboundOutboundPage() {
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
if (itemCodes.length > 0) {
try {
// 단위 카테고리 코드→라벨 매핑 로드
// 재고단위 카테고리 코드→라벨 매핑 로드
let unitLabelMap: Record<string, string> = {};
try {
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
const catRes = await apiClient.get("/table-categories/item_info/inventory_unit/values");
if (catRes.data?.success && catRes.data.data?.length > 0) {
const flatten = (vals: any[]) => {
for (const v of vals) {
@@ -127,7 +127,7 @@ export default function InboundOutboundPage() {
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
const map: Record<string, { item_name: string; unit: string }> = {};
for (const i of items) {
const rawUnit = i.unit || "";
const rawUnit = i.inventory_unit || "";
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
}
setItemMap(map);

View File

@@ -54,6 +54,7 @@ import {
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
@@ -388,7 +389,7 @@ export default function ReceivingPage() {
const flatRows = useMemo(() => {
return data.map((row) => ({
...row,
inbound_type: resolveInboundType(row.inbound_type),
inbound_type: resolveInboundType((row as any).detail_inbound_type || row.inbound_type),
source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "",
}));
}, [data]);
@@ -595,7 +596,7 @@ export default function ReceivingPage() {
setSelectedItems(
grouped.map((g) => ({
key: g.id,
inbound_type: g.inbound_type || "",
inbound_type: (g as any).detail_inbound_type || g.inbound_type || "",
reference_number: g.reference_number || "",
supplier_code: (g as any).supplier_code || "",
supplier_name: g.supplier_name || "",
@@ -635,7 +636,7 @@ export default function ReceivingPage() {
setPurchaseOrders([]);
setShipments([]);
setItems([]);
setSelectedItems([]);
// 선택 품목은 유지 (여러 유형 혼합 가능)
setSourcePage(1);
setSourceTotalCount(0);
loadSourceData(type, undefined, 1);
@@ -651,7 +652,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "구매입고",
inbound_type: modalInboundType,
reference_number: po.purchase_no,
supplier_code: po.supplier_code,
supplier_name: po.supplier_name,
@@ -677,7 +678,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "반품입고",
inbound_type: modalInboundType,
reference_number: sh.instruction_no,
supplier_code: "",
supplier_name: sh.partner_id,
@@ -695,15 +696,15 @@ export default function ReceivingPage() {
]);
};
// 품목 추가
// 품목 추가 (현재 선택된 입고유형 사용)
const addItem = (item: ItemSource) => {
const key = `item-${item.id}`;
const key = `item-${item.id}-${modalInboundType}`;
if (selectedItems.some((s) => s.key === key)) return;
setSelectedItems((prev) => [
...prev,
{
key,
inbound_type: "기타입고",
inbound_type: modalInboundType,
reference_number: item.item_number,
supplier_code: "",
supplier_name: "",
@@ -1009,11 +1010,11 @@ export default function ReceivingPage() {
</TableCell>
</TableRow>
) : (
paginatedRows.map((row) => {
paginatedRows.map((row, idx) => {
const isChecked = checkedIds.includes(row.id);
return (
<TableRow
key={row.id}
key={`${row.id}-${idx}`}
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isChecked ? "border-l-primary bg-primary/5" : "hover:bg-accent/50"
@@ -1401,6 +1402,7 @@ export default function ReceivingPage() {
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[70px] p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] p-2 text-right">
@@ -1421,6 +1423,9 @@ export default function ReceivingPage() {
<TableCell className="p-2 text-center">
{idx + 1}
</TableCell>
<TableCell className="p-2">
<Badge variant="outline" className="text-[10px]">{item.inbound_type || modalInboundType}</Badge>
</TableCell>
<TableCell className="max-w-[180px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={item.item_name}>

View File

@@ -104,10 +104,10 @@ export default function InboundOutboundPage() {
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
if (itemCodes.length > 0) {
try {
// 단위 카테고리 코드→라벨 매핑 로드
// 재고단위 카테고리 코드→라벨 매핑 로드
let unitLabelMap: Record<string, string> = {};
try {
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
const catRes = await apiClient.get("/table-categories/item_info/inventory_unit/values");
if (catRes.data?.success && catRes.data.data?.length > 0) {
const flatten = (vals: any[]) => {
for (const v of vals) {
@@ -127,7 +127,7 @@ export default function InboundOutboundPage() {
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
const map: Record<string, { item_name: string; unit: string }> = {};
for (const i of items) {
const rawUnit = i.unit || "";
const rawUnit = i.inventory_unit || "";
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
}
setItemMap(map);

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -54,8 +54,8 @@ import {
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
@@ -284,7 +284,6 @@ export default function ReceivingPage() {
const [modalMemo, setModalMemo] = useState("");
const [selectedItems, setSelectedItems] = useState<SelectedSourceItem[]>([]);
const [saving, setSaving] = useState(false);
const savingLockRef = useRef(false);
// 수정 모드
const [editMode, setEditMode] = useState(false);
@@ -390,7 +389,7 @@ export default function ReceivingPage() {
const flatRows = useMemo(() => {
return data.map((row) => ({
...row,
inbound_type: resolveInboundType(row.inbound_type),
inbound_type: resolveInboundType((row as any).detail_inbound_type || row.inbound_type),
source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "",
}));
}, [data]);
@@ -597,7 +596,7 @@ export default function ReceivingPage() {
setSelectedItems(
grouped.map((g) => ({
key: g.id,
inbound_type: g.inbound_type || "",
inbound_type: (g as any).detail_inbound_type || g.inbound_type || "",
reference_number: g.reference_number || "",
supplier_code: (g as any).supplier_code || "",
supplier_name: g.supplier_name || "",
@@ -637,7 +636,7 @@ export default function ReceivingPage() {
setPurchaseOrders([]);
setShipments([]);
setItems([]);
setSelectedItems([]);
// 선택 품목은 유지 (여러 유형 혼합 가능)
setSourcePage(1);
setSourceTotalCount(0);
loadSourceData(type, undefined, 1);
@@ -653,7 +652,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "구매입고",
inbound_type: modalInboundType,
reference_number: po.purchase_no,
supplier_code: po.supplier_code,
supplier_name: po.supplier_name,
@@ -679,7 +678,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "반품입고",
inbound_type: modalInboundType,
reference_number: sh.instruction_no,
supplier_code: "",
supplier_name: sh.partner_id,
@@ -697,15 +696,15 @@ export default function ReceivingPage() {
]);
};
// 품목 추가
// 품목 추가 (현재 선택된 입고유형 사용)
const addItem = (item: ItemSource) => {
const key = `item-${item.id}`;
const key = `item-${item.id}-${modalInboundType}`;
if (selectedItems.some((s) => s.key === key)) return;
setSelectedItems((prev) => [
...prev,
{
key,
inbound_type: "기타입고",
inbound_type: modalInboundType,
reference_number: item.item_number,
supplier_code: "",
supplier_name: "",
@@ -752,7 +751,6 @@ export default function ReceivingPage() {
// 저장
const handleSave = async () => {
if (savingLockRef.current) return;
if (selectedItems.length === 0) {
alert("입고할 품목을 선택해주세요.");
return;
@@ -772,7 +770,6 @@ export default function ReceivingPage() {
toast.error("창고를 선택해주세요.");
return;
}
savingLockRef.current = true;
setSaving(true);
try {
if (editMode) {
@@ -868,7 +865,6 @@ export default function ReceivingPage() {
toast.error(msg);
} finally {
setSaving(false);
savingLockRef.current = false;
}
};
@@ -1014,12 +1010,11 @@ export default function ReceivingPage() {
</TableCell>
</TableRow>
) : (
paginatedRows.map((row) => {
paginatedRows.map((row, idx) => {
const isChecked = checkedIds.includes(row.id);
const rowKey = (row as any).detail_id ? `${row.id}-${(row as any).detail_id}` : row.id;
return (
<TableRow
key={rowKey}
key={`${row.id}-${idx}`}
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isChecked ? "border-l-primary bg-primary/5" : "hover:bg-accent/50"
@@ -1407,6 +1402,7 @@ export default function ReceivingPage() {
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[70px] p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] p-2 text-right">
@@ -1427,6 +1423,9 @@ export default function ReceivingPage() {
<TableCell className="p-2 text-center">
{idx + 1}
</TableCell>
<TableCell className="p-2">
<Badge variant="outline" className="text-[10px]">{item.inbound_type || modalInboundType}</Badge>
</TableCell>
<TableCell className="max-w-[180px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={item.item_name}>

View File

@@ -62,6 +62,9 @@ const SUPPLIER_GRID_COLUMNS = [
{ key: "contact_person", label: "담당자" },
{ key: "contact_phone", label: "전화번호" },
{ key: "email", label: "이메일" },
{ key: "bank_name", label: "은행" },
{ key: "account_number", label: "계좌번호" },
{ key: "remark", label: "비고" },
{ key: "business_number", label: "사업자번호" },
{ key: "address", label: "주소" },
{ key: "status", label: "상태" },
@@ -1291,6 +1294,9 @@ export default function SupplierManagementPage() {
email: { width: "w-[160px]" },
business_number: { width: "w-[120px]" },
address: { minWidth: "min-w-[150px]" },
bank_name: { width: "w-[100px]" },
account_number: { width: "w-[140px]" },
remark: { minWidth: "min-w-[120px]" },
status: {
width: "w-[70px]",
render: (val: any) =>

View File

@@ -62,6 +62,9 @@ const CUSTOMER_GRID_COLUMNS = [
{ key: "contact_person", label: "담당자" },
{ key: "contact_phone", label: "전화번호" },
{ key: "email", label: "이메일" },
{ key: "bank_name", label: "은행" },
{ key: "account_number", label: "계좌번호" },
{ key: "remark", label: "비고" },
{ key: "business_number", label: "사업자번호" },
{ key: "address", label: "주소" },
{ key: "status", label: "상태" },
@@ -1311,6 +1314,9 @@ export default function CustomerManagementPage() {
email: { width: "w-[160px]" },
business_number: { width: "w-[120px]" },
address: { minWidth: "min-w-[150px]" },
bank_name: { width: "w-[100px]" },
account_number: { width: "w-[140px]" },
remark: { minWidth: "min-w-[120px]" },
status: {
width: "w-[70px]",
render: (val: any) =>

View File

@@ -104,10 +104,10 @@ export default function InboundOutboundPage() {
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
if (itemCodes.length > 0) {
try {
// 단위 카테고리 코드→라벨 매핑 로드
// 재고단위 카테고리 코드→라벨 매핑 로드
let unitLabelMap: Record<string, string> = {};
try {
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
const catRes = await apiClient.get("/table-categories/item_info/inventory_unit/values");
if (catRes.data?.success && catRes.data.data?.length > 0) {
const flatten = (vals: any[]) => {
for (const v of vals) {
@@ -127,7 +127,7 @@ export default function InboundOutboundPage() {
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
const map: Record<string, { item_name: string; unit: string }> = {};
for (const i of items) {
const rawUnit = i.unit || "";
const rawUnit = i.inventory_unit || "";
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
}
setItemMap(map);

View File

@@ -54,6 +54,7 @@ import {
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
@@ -388,7 +389,7 @@ export default function ReceivingPage() {
const flatRows = useMemo(() => {
return data.map((row) => ({
...row,
inbound_type: resolveInboundType(row.inbound_type),
inbound_type: resolveInboundType((row as any).detail_inbound_type || row.inbound_type),
source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "",
}));
}, [data]);
@@ -595,7 +596,7 @@ export default function ReceivingPage() {
setSelectedItems(
grouped.map((g) => ({
key: g.id,
inbound_type: g.inbound_type || "",
inbound_type: (g as any).detail_inbound_type || g.inbound_type || "",
reference_number: g.reference_number || "",
supplier_code: (g as any).supplier_code || "",
supplier_name: g.supplier_name || "",
@@ -635,7 +636,7 @@ export default function ReceivingPage() {
setPurchaseOrders([]);
setShipments([]);
setItems([]);
setSelectedItems([]);
// 선택 품목은 유지 (여러 유형 혼합 가능)
setSourcePage(1);
setSourceTotalCount(0);
loadSourceData(type, undefined, 1);
@@ -651,7 +652,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "구매입고",
inbound_type: modalInboundType,
reference_number: po.purchase_no,
supplier_code: po.supplier_code,
supplier_name: po.supplier_name,
@@ -677,7 +678,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "반품입고",
inbound_type: modalInboundType,
reference_number: sh.instruction_no,
supplier_code: "",
supplier_name: sh.partner_id,
@@ -695,15 +696,15 @@ export default function ReceivingPage() {
]);
};
// 품목 추가
// 품목 추가 (현재 선택된 입고유형 사용)
const addItem = (item: ItemSource) => {
const key = `item-${item.id}`;
const key = `item-${item.id}-${modalInboundType}`;
if (selectedItems.some((s) => s.key === key)) return;
setSelectedItems((prev) => [
...prev,
{
key,
inbound_type: "기타입고",
inbound_type: modalInboundType,
reference_number: item.item_number,
supplier_code: "",
supplier_name: "",
@@ -1009,11 +1010,11 @@ export default function ReceivingPage() {
</TableCell>
</TableRow>
) : (
paginatedRows.map((row) => {
paginatedRows.map((row, idx) => {
const isChecked = checkedIds.includes(row.id);
return (
<TableRow
key={row.id}
key={`${row.id}-${idx}`}
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isChecked ? "border-l-primary bg-primary/5" : "hover:bg-accent/50"
@@ -1401,6 +1402,7 @@ export default function ReceivingPage() {
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[70px] p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] p-2 text-right">
@@ -1421,6 +1423,9 @@ export default function ReceivingPage() {
<TableCell className="p-2 text-center">
{idx + 1}
</TableCell>
<TableCell className="p-2">
<Badge variant="outline" className="text-[10px]">{item.inbound_type || modalInboundType}</Badge>
</TableCell>
<TableCell className="max-w-[180px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={item.item_name}>

View File

@@ -75,7 +75,7 @@ export default function InboundOutboundPage() {
const [checkedIds, setCheckedIds] = useState<Set<string>>(new Set());
// 품목명/단위 캐시
const [itemMap, setItemMap] = useState<Record<string, { item_name: string; unit: string; width: string; height: string; thickness: string }>>({});
const [itemMap, setItemMap] = useState<Record<string, { item_name: string; unit: string }>>({});
const [warehouseMap, setWarehouseMap] = useState<Record<string, string>>({});
const [userMap, setUserMap] = useState<Record<string, string>>({});
@@ -104,10 +104,10 @@ export default function InboundOutboundPage() {
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
if (itemCodes.length > 0) {
try {
// 단위 카테고리 코드→라벨 매핑 로드
// 재고단위 카테고리 코드→라벨 매핑 로드
let unitLabelMap: Record<string, string> = {};
try {
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
const catRes = await apiClient.get("/table-categories/item_info/inventory_unit/values");
if (catRes.data?.success && catRes.data.data?.length > 0) {
const flatten = (vals: any[]) => {
for (const v of vals) {
@@ -125,16 +125,10 @@ export default function InboundOutboundPage() {
autoFilter: true,
});
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
const map: Record<string, { item_name: string; unit: string; width: string; height: string; thickness: string }> = {};
const map: Record<string, { item_name: string; unit: string }> = {};
for (const i of items) {
const rawUnit = i.unit || "";
if (!map[i.item_number]) map[i.item_number] = {
item_name: i.item_name || "",
unit: unitLabelMap[rawUnit] || rawUnit,
width: i.width || "",
height: i.height || "",
thickness: i.thickness || "",
};
const rawUnit = i.inventory_unit || "";
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
}
setItemMap(map);
} catch { /* skip */ }
@@ -347,9 +341,6 @@ export default function InboundOutboundPage() {
<TableHead className="w-[80px] text-center text-[11px]"></TableHead>
<TableHead className="w-[110px] text-[11px]"></TableHead>
<TableHead className="w-[160px] text-[11px]"></TableHead>
<TableHead className="w-[60px] text-right text-[11px]"></TableHead>
<TableHead className="w-[60px] text-right text-[11px]"></TableHead>
<TableHead className="w-[60px] text-right text-[11px]"></TableHead>
<TableHead className="w-[80px] text-right text-[11px]"></TableHead>
<TableHead className="w-[50px] text-center text-[11px]"></TableHead>
<TableHead className="w-[110px] text-[11px]"></TableHead>
@@ -370,7 +361,6 @@ export default function InboundOutboundPage() {
<Badge variant="outline" className="text-[10px]">{row._count}</Badge>
</div>
</TableCell>
<TableCell colSpan={3} />
<TableCell className="text-right font-mono font-bold text-primary text-[13px]">
{fmtNum(row._totalQty)}
</TableCell>
@@ -414,9 +404,6 @@ export default function InboundOutboundPage() {
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
<TableCell className="text-[12px] font-mono">{row.item_code || "-"}</TableCell>
<TableCell className="text-[13px]">{info?.item_name || "-"}</TableCell>
<TableCell className="text-right text-[12px] font-mono text-muted-foreground">{info?.width || "-"}</TableCell>
<TableCell className="text-right text-[12px] font-mono text-muted-foreground">{info?.height || "-"}</TableCell>
<TableCell className="text-right text-[12px] font-mono text-muted-foreground">{info?.thickness || "-"}</TableCell>
<TableCell className={cn("text-right font-mono font-semibold text-[13px]", isIn ? "text-emerald-600" : "text-amber-600")}>
{isIn ? "+" : ""}{fmtNum(qty)}
</TableCell>

View File

@@ -54,6 +54,7 @@ import {
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
@@ -86,9 +87,6 @@ const GRID_COLUMNS = [
{ key: "supplier_name", label: "공급처" },
{ key: "item_number", label: "품목코드" },
{ key: "item_name", label: "품목명" },
{ key: "width", label: "가로" },
{ key: "height", label: "세로" },
{ key: "thickness", label: "두께" },
{ key: "spec", label: "규격" },
{ key: "inbound_qty", label: "입고수량" },
{ key: "unit_price", label: "단가" },
@@ -98,8 +96,8 @@ const GRID_COLUMNS = [
{ key: "remark", label: "비고" },
];
// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(18) = 19
const TOTAL_COLS = 19;
// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16
const TOTAL_COLS = 16;
// 헤더 필터 Popover
function HeaderFilterPopover({
@@ -391,7 +389,7 @@ export default function ReceivingPage() {
const flatRows = useMemo(() => {
return data.map((row) => ({
...row,
inbound_type: resolveInboundType(row.inbound_type),
inbound_type: resolveInboundType((row as any).detail_inbound_type || row.inbound_type),
source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "",
}));
}, [data]);
@@ -598,7 +596,7 @@ export default function ReceivingPage() {
setSelectedItems(
grouped.map((g) => ({
key: g.id,
inbound_type: g.inbound_type || "",
inbound_type: (g as any).detail_inbound_type || g.inbound_type || "",
reference_number: g.reference_number || "",
supplier_code: (g as any).supplier_code || "",
supplier_name: g.supplier_name || "",
@@ -638,7 +636,7 @@ export default function ReceivingPage() {
setPurchaseOrders([]);
setShipments([]);
setItems([]);
setSelectedItems([]);
// 선택 품목은 유지 (여러 유형 혼합 가능)
setSourcePage(1);
setSourceTotalCount(0);
loadSourceData(type, undefined, 1);
@@ -654,16 +652,13 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "구매입고",
inbound_type: modalInboundType,
reference_number: po.purchase_no,
supplier_code: po.supplier_code,
supplier_name: po.supplier_name,
item_number: po.item_code,
item_name: po.item_name,
spec: po.spec || "",
width: (po as any).width || "",
height: (po as any).height || "",
thickness: (po as any).thickness || "",
material: po.material || "",
unit: "EA",
inbound_qty: po.remain_qty,
@@ -683,16 +678,13 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "반품입고",
inbound_type: modalInboundType,
reference_number: sh.instruction_no,
supplier_code: "",
supplier_name: sh.partner_id,
item_number: sh.item_code,
item_name: sh.item_name,
spec: sh.spec || "",
width: (sh as any).width || "",
height: (sh as any).height || "",
thickness: (sh as any).thickness || "",
material: sh.material || "",
unit: "EA",
inbound_qty: sh.ship_qty,
@@ -704,24 +696,21 @@ export default function ReceivingPage() {
]);
};
// 품목 추가
// 품목 추가 (현재 선택된 입고유형 사용)
const addItem = (item: ItemSource) => {
const key = `item-${item.id}`;
const key = `item-${item.id}-${modalInboundType}`;
if (selectedItems.some((s) => s.key === key)) return;
setSelectedItems((prev) => [
...prev,
{
key,
inbound_type: "기타입고",
inbound_type: modalInboundType,
reference_number: item.item_number,
supplier_code: "",
supplier_name: "",
item_number: item.item_number,
item_name: item.item_name,
spec: item.spec || "",
width: (item as any).width || "",
height: (item as any).height || "",
thickness: (item as any).thickness || "",
material: item.material || "",
unit: item.inventory_unit || "EA",
inbound_qty: 0,
@@ -951,9 +940,6 @@ export default function ReceivingPage() {
<col style={{ width: "110px" }} />
<col style={{ width: "100px" }} />
<col style={{ width: "140px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "90px" }} />
<col style={{ width: "80px" }} />
<col style={{ width: "80px" }} />
@@ -1024,11 +1010,11 @@ export default function ReceivingPage() {
</TableCell>
</TableRow>
) : (
paginatedRows.map((row) => {
paginatedRows.map((row, idx) => {
const isChecked = checkedIds.includes(row.id);
return (
<TableRow
key={row.id}
key={`${row.id}-${idx}`}
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isChecked ? "border-l-primary bg-primary/5" : "hover:bg-accent/50"
@@ -1065,9 +1051,6 @@ export default function ReceivingPage() {
<TableCell className="text-[13px] truncate max-w-[110px]"><span className="block truncate">{row.supplier_name || ""}</span></TableCell>
<TableCell className="font-mono text-[13px]">{row.item_number || ""}</TableCell>
<TableCell className="text-[13px] max-w-[140px]"><span className="block truncate" title={row.item_name || ""}>{row.item_name || ""}</span></TableCell>
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{(row as any).width || "-"}</TableCell>
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{(row as any).height || "-"}</TableCell>
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{(row as any).thickness || "-"}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec || ""}</TableCell>
<TableCell className="text-right font-mono text-[13px]">{row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}</TableCell>
<TableCell className="text-right font-mono text-[13px]">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</TableCell>
@@ -1419,6 +1402,7 @@ export default function ReceivingPage() {
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[70px] p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] p-2 text-right">
@@ -1439,7 +1423,10 @@ export default function ReceivingPage() {
<TableCell className="p-2 text-center">
{idx + 1}
</TableCell>
<TableCell className="max-w-[220px] p-2">
<TableCell className="p-2">
<Badge variant="outline" className="text-[10px]">{item.inbound_type || modalInboundType}</Badge>
</TableCell>
<TableCell className="max-w-[180px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={item.item_name}>
{item.item_name}
@@ -1448,13 +1435,6 @@ export default function ReceivingPage() {
{item.item_number}
{item.spec ? ` | ${item.spec}` : ""}
</span>
{((item as any).width || (item as any).height || (item as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(item as any).width && `W ${(item as any).width}`}
{(item as any).height && ` × H ${(item as any).height}`}
{(item as any).thickness && ` × T ${(item as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="p-2 text-[11px]">
@@ -1620,20 +1600,13 @@ function SourcePurchaseOrderTable({
</TableCell>
<TableCell className="max-w-[120px] truncate p-2 font-medium" title={po.purchase_no}>{po.purchase_no}</TableCell>
<TableCell className="max-w-[120px] truncate p-2" title={po.supplier_name}>{po.supplier_name}</TableCell>
<TableCell className="max-w-[220px] p-2">
<TableCell className="max-w-[200px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={po.item_name}>{po.item_name}</span>
<span className="text-muted-foreground truncate text-[10px]" title={`${po.item_code}${po.spec ? ` | ${po.spec}` : ""}`}>
{po.item_code}
{po.spec ? ` | ${po.spec}` : ""}
</span>
{((po as any).width || (po as any).height || (po as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(po as any).width && `W ${(po as any).width}`}
{(po as any).height && ` × H ${(po as any).height}`}
{(po as any).thickness && ` × T ${(po as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="p-2 text-right">
@@ -1711,20 +1684,13 @@ function SourceShipmentTable({
: "-"}
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={sh.partner_id}>{sh.partner_id}</TableCell>
<TableCell className="max-w-[220px] p-2">
<TableCell className="max-w-[200px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={sh.item_name}>{sh.item_name}</span>
<span className="text-muted-foreground truncate text-[10px]" title={`${sh.item_code}${sh.spec ? ` | ${sh.spec}` : ""}`}>
{sh.item_code}
{sh.spec ? ` | ${sh.spec}` : ""}
</span>
{((sh as any).width || (sh as any).height || (sh as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(sh as any).width && `W ${(sh as any).width}`}
{(sh as any).height && ` × H ${(sh as any).height}`}
{(sh as any).thickness && ` × T ${(sh as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="p-2 text-right font-semibold">
@@ -1797,13 +1763,6 @@ function SourceItemTable({
<span className="text-muted-foreground truncate text-[10px]" title={item.item_number}>
{item.item_number}
</span>
{((item as any).width || (item as any).height || (item as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(item as any).width && `W ${(item as any).width}`}
{(item as any).height && ` × H ${(item as any).height}`}
{(item as any).thickness && ` × T ${(item as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>

View File

@@ -104,10 +104,10 @@ export default function InboundOutboundPage() {
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
if (itemCodes.length > 0) {
try {
// 단위 카테고리 코드→라벨 매핑 로드
// 재고단위 카테고리 코드→라벨 매핑 로드
let unitLabelMap: Record<string, string> = {};
try {
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
const catRes = await apiClient.get("/table-categories/item_info/inventory_unit/values");
if (catRes.data?.success && catRes.data.data?.length > 0) {
const flatten = (vals: any[]) => {
for (const v of vals) {
@@ -127,7 +127,7 @@ export default function InboundOutboundPage() {
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
const map: Record<string, { item_name: string; unit: string }> = {};
for (const i of items) {
const rawUnit = i.unit || "";
const rawUnit = i.inventory_unit || "";
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
}
setItemMap(map);

View File

@@ -54,6 +54,7 @@ import {
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
@@ -388,7 +389,7 @@ export default function ReceivingPage() {
const flatRows = useMemo(() => {
return data.map((row) => ({
...row,
inbound_type: resolveInboundType(row.inbound_type),
inbound_type: resolveInboundType((row as any).detail_inbound_type || row.inbound_type),
source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "",
}));
}, [data]);
@@ -595,7 +596,7 @@ export default function ReceivingPage() {
setSelectedItems(
grouped.map((g) => ({
key: g.id,
inbound_type: g.inbound_type || "",
inbound_type: (g as any).detail_inbound_type || g.inbound_type || "",
reference_number: g.reference_number || "",
supplier_code: (g as any).supplier_code || "",
supplier_name: g.supplier_name || "",
@@ -635,7 +636,7 @@ export default function ReceivingPage() {
setPurchaseOrders([]);
setShipments([]);
setItems([]);
setSelectedItems([]);
// 선택 품목은 유지 (여러 유형 혼합 가능)
setSourcePage(1);
setSourceTotalCount(0);
loadSourceData(type, undefined, 1);
@@ -651,7 +652,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "구매입고",
inbound_type: modalInboundType,
reference_number: po.purchase_no,
supplier_code: po.supplier_code,
supplier_name: po.supplier_name,
@@ -677,7 +678,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "반품입고",
inbound_type: modalInboundType,
reference_number: sh.instruction_no,
supplier_code: "",
supplier_name: sh.partner_id,
@@ -695,15 +696,15 @@ export default function ReceivingPage() {
]);
};
// 품목 추가
// 품목 추가 (현재 선택된 입고유형 사용)
const addItem = (item: ItemSource) => {
const key = `item-${item.id}`;
const key = `item-${item.id}-${modalInboundType}`;
if (selectedItems.some((s) => s.key === key)) return;
setSelectedItems((prev) => [
...prev,
{
key,
inbound_type: "기타입고",
inbound_type: modalInboundType,
reference_number: item.item_number,
supplier_code: "",
supplier_name: "",
@@ -1009,11 +1010,11 @@ export default function ReceivingPage() {
</TableCell>
</TableRow>
) : (
paginatedRows.map((row) => {
paginatedRows.map((row, idx) => {
const isChecked = checkedIds.includes(row.id);
return (
<TableRow
key={row.id}
key={`${row.id}-${idx}`}
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isChecked ? "border-l-primary bg-primary/5" : "hover:bg-accent/50"
@@ -1401,6 +1402,7 @@ export default function ReceivingPage() {
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[70px] p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] p-2 text-right">
@@ -1421,6 +1423,9 @@ export default function ReceivingPage() {
<TableCell className="p-2 text-center">
{idx + 1}
</TableCell>
<TableCell className="p-2">
<Badge variant="outline" className="text-[10px]">{item.inbound_type || modalInboundType}</Badge>
</TableCell>
<TableCell className="max-w-[180px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={item.item_name}>

View File

@@ -104,10 +104,10 @@ export default function InboundOutboundPage() {
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
if (itemCodes.length > 0) {
try {
// 단위 카테고리 코드→라벨 매핑 로드
// 재고단위 카테고리 코드→라벨 매핑 로드
let unitLabelMap: Record<string, string> = {};
try {
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
const catRes = await apiClient.get("/table-categories/item_info/inventory_unit/values");
if (catRes.data?.success && catRes.data.data?.length > 0) {
const flatten = (vals: any[]) => {
for (const v of vals) {
@@ -127,7 +127,7 @@ export default function InboundOutboundPage() {
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
const map: Record<string, { item_name: string; unit: string }> = {};
for (const i of items) {
const rawUnit = i.unit || "";
const rawUnit = i.inventory_unit || "";
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
}
setItemMap(map);

View File

@@ -106,6 +106,9 @@ const GRID_COLUMNS = [
{ key: "customer_name", label: "거래처" },
{ key: "item_number", label: "품목코드" },
{ key: "item_name", label: "품목명" },
{ key: "width", label: "가로" },
{ key: "height", label: "세로" },
{ key: "thickness", label: "두께" },
{ key: "spec", label: "규격" },
{ key: "outbound_qty", label: "출고수량" },
{ key: "unit_price", label: "단가" },
@@ -115,8 +118,8 @@ const GRID_COLUMNS = [
{ key: "remark", label: "비고" },
];
// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(15) = 16
const TOTAL_COLS = 16;
// 총 컬럼 수: 체크박스(1) + GRID_COLUMNS(18) = 19
const TOTAL_COLS = 19;
// 헤더 필터 Popover
function HeaderFilterPopover({
@@ -626,6 +629,9 @@ export default function OutboundPage() {
item_number: si.item_code,
item_name: si.item_name,
spec: si.spec || "",
width: (si as any).width || "",
height: (si as any).height || "",
thickness: (si as any).thickness || "",
material: si.material || "",
unit: "EA",
outbound_qty: si.remain_qty,
@@ -652,6 +658,9 @@ export default function OutboundPage() {
item_number: po.item_code,
item_name: po.item_name,
spec: po.spec || "",
width: (po as any).width || "",
height: (po as any).height || "",
thickness: (po as any).thickness || "",
material: po.material || "",
unit: "EA",
outbound_qty: po.received_qty,
@@ -678,6 +687,9 @@ export default function OutboundPage() {
item_number: item.item_number,
item_name: item.item_name,
spec: item.spec || "",
width: (item as any).width || "",
height: (item as any).height || "",
thickness: (item as any).thickness || "",
material: item.material || "",
unit: item.inventory_unit || "EA",
outbound_qty: 0,
@@ -896,6 +908,9 @@ export default function OutboundPage() {
<col style={{ width: "110px" }} />
<col style={{ width: "100px" }} />
<col style={{ width: "140px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "90px" }} />
<col style={{ width: "80px" }} />
<col style={{ width: "80px" }} />
@@ -1007,6 +1022,9 @@ export default function OutboundPage() {
<TableCell className="text-[13px] truncate max-w-[110px]"><span className="block truncate">{row.customer_name || ""}</span></TableCell>
<TableCell className="font-mono text-[13px]">{row.item_number || ""}</TableCell>
<TableCell className="text-[13px] max-w-[140px]"><span className="block truncate" title={row.item_name || ""}>{row.item_name || ""}</span></TableCell>
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{(row as any).width || "-"}</TableCell>
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{(row as any).height || "-"}</TableCell>
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{(row as any).thickness || "-"}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{row.spec || ""}</TableCell>
<TableCell className="text-right font-mono text-[13px]">{row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}</TableCell>
<TableCell className="text-right font-mono text-[13px]">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</TableCell>
@@ -1372,7 +1390,7 @@ export default function OutboundPage() {
{resolveCat("outbound_type", item.outbound_type) || "-"}
</Badge>
</TableCell>
<TableCell className="max-w-[180px] p-2">
<TableCell className="max-w-[220px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={item.item_name}>
{item.item_name}
@@ -1381,6 +1399,13 @@ export default function OutboundPage() {
{item.item_number}
{item.spec ? ` | ${item.spec}` : ""}
</span>
{((item as any).width || (item as any).height || (item as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(item as any).width && `W ${(item as any).width}`}
{(item as any).height && ` × H ${(item as any).height}`}
{(item as any).thickness && ` × T ${(item as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="p-2 text-[11px]">{item.reference_number}</TableCell>
@@ -1535,13 +1560,20 @@ function SourceShipmentInstructionTable({
? new Date(si.instruction_date).toLocaleDateString("ko-KR")
: "-"}
</TableCell>
<TableCell className="max-w-[200px] p-2">
<TableCell className="max-w-[220px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={si.item_name}>{si.item_name}</span>
<span className="text-muted-foreground truncate text-[10px]" title={`${si.item_code}${si.spec ? ` | ${si.spec}` : ""}`}>
{si.item_code}
{si.spec ? ` | ${si.spec}` : ""}
</span>
{((si as any).width || (si as any).height || (si as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(si as any).width && `W ${(si as any).width}`}
{(si as any).height && ` × H ${(si as any).height}`}
{(si as any).thickness && ` × T ${(si as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="p-2 text-right">
@@ -1612,13 +1644,20 @@ function SourcePurchaseOrderTable({
</TableCell>
<TableCell className="max-w-[120px] truncate p-2 font-medium" title={po.purchase_no}>{po.purchase_no}</TableCell>
<TableCell className="max-w-[120px] truncate p-2" title={po.supplier_name}>{po.supplier_name}</TableCell>
<TableCell className="max-w-[200px] p-2">
<TableCell className="max-w-[220px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={po.item_name}>{po.item_name}</span>
<span className="text-muted-foreground truncate text-[10px]" title={`${po.item_code}${po.spec ? ` | ${po.spec}` : ""}`}>
{po.item_code}
{po.spec ? ` | ${po.spec}` : ""}
</span>
{((po as any).width || (po as any).height || (po as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(po as any).width && `W ${(po as any).width}`}
{(po as any).height && ` × H ${(po as any).height}`}
{(po as any).thickness && ` × T ${(po as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="p-2 text-right">
@@ -1692,6 +1731,13 @@ function SourceItemTable({
<span className="text-muted-foreground truncate text-[10px]" title={item.item_number}>
{item.item_number}
</span>
{((item as any).width || (item as any).height || (item as any).thickness) && (
<span className="text-muted-foreground truncate text-[10px]">
{(item as any).width && `W ${(item as any).width}`}
{(item as any).height && ` × H ${(item as any).height}`}
{(item as any).thickness && ` × T ${(item as any).thickness}`}
</span>
)}
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>

View File

@@ -54,6 +54,7 @@ import {
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { useTableSettings } from "@/hooks/useTableSettings";
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
@@ -388,7 +389,7 @@ export default function ReceivingPage() {
const flatRows = useMemo(() => {
return data.map((row) => ({
...row,
inbound_type: resolveInboundType(row.inbound_type),
inbound_type: resolveInboundType((row as any).detail_inbound_type || row.inbound_type),
source_type: row.source_table ? (SOURCE_TABLE_LABEL[row.source_table] || row.source_table) : (row as any).source_type || "",
}));
}, [data]);
@@ -595,7 +596,7 @@ export default function ReceivingPage() {
setSelectedItems(
grouped.map((g) => ({
key: g.id,
inbound_type: g.inbound_type || "",
inbound_type: (g as any).detail_inbound_type || g.inbound_type || "",
reference_number: g.reference_number || "",
supplier_code: (g as any).supplier_code || "",
supplier_name: g.supplier_name || "",
@@ -635,7 +636,7 @@ export default function ReceivingPage() {
setPurchaseOrders([]);
setShipments([]);
setItems([]);
setSelectedItems([]);
// 선택 품목은 유지 (여러 유형 혼합 가능)
setSourcePage(1);
setSourceTotalCount(0);
loadSourceData(type, undefined, 1);
@@ -651,7 +652,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "구매입고",
inbound_type: modalInboundType,
reference_number: po.purchase_no,
supplier_code: po.supplier_code,
supplier_name: po.supplier_name,
@@ -677,7 +678,7 @@ export default function ReceivingPage() {
...prev,
{
key,
inbound_type: "반품입고",
inbound_type: modalInboundType,
reference_number: sh.instruction_no,
supplier_code: "",
supplier_name: sh.partner_id,
@@ -695,15 +696,15 @@ export default function ReceivingPage() {
]);
};
// 품목 추가
// 품목 추가 (현재 선택된 입고유형 사용)
const addItem = (item: ItemSource) => {
const key = `item-${item.id}`;
const key = `item-${item.id}-${modalInboundType}`;
if (selectedItems.some((s) => s.key === key)) return;
setSelectedItems((prev) => [
...prev,
{
key,
inbound_type: "기타입고",
inbound_type: modalInboundType,
reference_number: item.item_number,
supplier_code: "",
supplier_name: "",
@@ -1009,11 +1010,11 @@ export default function ReceivingPage() {
</TableCell>
</TableRow>
) : (
paginatedRows.map((row) => {
paginatedRows.map((row, idx) => {
const isChecked = checkedIds.includes(row.id);
return (
<TableRow
key={row.id}
key={`${row.id}-${idx}`}
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isChecked ? "border-l-primary bg-primary/5" : "hover:bg-accent/50"
@@ -1401,6 +1402,7 @@ export default function ReceivingPage() {
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[30px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
<TableHead className="w-[70px] p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] p-2 text-right">
@@ -1421,6 +1423,9 @@ export default function ReceivingPage() {
<TableCell className="p-2 text-center">
{idx + 1}
</TableCell>
<TableCell className="p-2">
<Badge variant="outline" className="text-[10px]">{item.inbound_type || modalInboundType}</Badge>
</TableCell>
<TableCell className="max-w-[180px] p-2">
<div className="flex flex-col">
<span className="truncate font-medium" title={item.item_name}>

View File

@@ -142,6 +142,9 @@ const GRID_COLUMNS = [
{ key: "image", label: "이미지", type: "image" },
{ key: "division", label: "관리품목" },
{ key: "type", label: "품목구분" },
{ key: "width", label: "가로", align: "right" as const },
{ key: "height", label: "세로", align: "right" as const },
{ key: "thickness", label: "두께", align: "right" as const },
{ key: "size", label: "규격" },
{ key: "unit", label: "단위" },
{ key: "material", label: "재질" },
@@ -160,6 +163,9 @@ const FORM_FIELDS = [
{ key: "item_name", label: "품명", type: "text", required: true },
{ key: "division", label: "관리품목", type: "multi-category" },
{ key: "type", label: "품목구분", type: "category" },
{ key: "width", label: "가로", type: "text", placeholder: "숫자 입력 (예: 1000)" },
{ key: "height", label: "세로", type: "text", placeholder: "숫자 입력 (예: 2000)" },
{ key: "thickness", label: "두께", type: "text", placeholder: "숫자 입력 (예: 10)" },
{ key: "size", label: "규격", type: "text" },
{ key: "unit", label: "단위", type: "category" },
{ key: "material", label: "재질", type: "category" },
@@ -383,8 +389,12 @@ export default function ItemInfoPage() {
}
return categoryOptions[col]?.find((o) => o.code === code)?.label || code;
};
setRawItems(raw);
const data = raw.map((r: any) => {
// item_number 내림차순 정렬 (최근 품목이 위로, 자연 정렬)
const sortedRaw = [...raw].sort((a: any, b: any) =>
String(b.item_number || "").localeCompare(String(a.item_number || ""), undefined, { numeric: true, sensitivity: "base" })
);
setRawItems(sortedRaw);
const data = sortedRaw.map((r: any) => {
const converted = { ...r };
for (const col of CATEGORY_COLUMNS) {
if (converted[col]) converted[col] = resolve(col, converted[col]);

View File

@@ -76,6 +76,9 @@ const GRID_COLUMNS_CONFIG = [
{ key: "supplier_name", label: "공급업체" },
{ key: "item_code", label: "품번" },
{ key: "item_name", label: "품명" },
{ key: "width", label: "가로" },
{ key: "height", label: "세로" },
{ key: "thickness", label: "두께" },
{ key: "spec", label: "규격" },
{ key: "order_qty", label: "발주수량" },
{ key: "received_qty", label: "입고수량" },
@@ -91,6 +94,9 @@ const MODAL_DETAIL_COLUMNS = [
{ key: "item_code", label: "품번", width: "min-w-[120px]" },
{ key: "item_name", label: "품명", width: "min-w-[150px]" },
{ key: "supplier", label: "공급업체", width: "min-w-[150px]" },
{ key: "width", label: "가로", width: "min-w-[70px]" },
{ key: "height", label: "세로", width: "min-w-[70px]" },
{ key: "thickness", label: "두께", width: "min-w-[70px]" },
{ key: "spec", label: "규격", width: "min-w-[80px]" },
{ key: "unit", label: "단위", width: "min-w-[90px]" },
{ key: "order_qty", label: "발주수량", width: "min-w-[90px]" },
@@ -351,6 +357,9 @@ export default function PurchaseOrderPage() {
...row,
item_name: row.item_name || item?.item_name || "",
spec: row.spec || item?.size || "",
width: row.width || item?.width || "",
height: row.height || item?.height || "",
thickness: row.thickness || item?.thickness || "",
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
status: master?.status || "",
supplier_name: master?.supplier_name || "",
@@ -641,6 +650,9 @@ export default function PurchaseOrderPage() {
item_code: itemCode,
item_name: item.item_name,
spec: item.size || "",
width: item.width || "",
height: item.height || "",
thickness: item.thickness || "",
material: getCategoryLabel("item_material", item.material) || item.material || "",
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
order_qty: "",
@@ -1087,6 +1099,12 @@ export default function PurchaseOrderPage() {
);
case "spec":
return <TableCell key={col.key} className="text-[13px] text-muted-foreground">{row.spec}</TableCell>;
case "width":
return <TableCell key={col.key} className="text-[13px] text-right font-mono text-muted-foreground">{row.width || "-"}</TableCell>;
case "height":
return <TableCell key={col.key} className="text-[13px] text-right font-mono text-muted-foreground">{row.height || "-"}</TableCell>;
case "thickness":
return <TableCell key={col.key} className="text-[13px] text-right font-mono text-muted-foreground">{row.thickness || "-"}</TableCell>;
case "unit":
return <TableCell key={col.key} className="text-[13px]">{row.unit}</TableCell>;
case "order_qty":
@@ -1224,6 +1242,9 @@ export default function PurchaseOrderPage() {
</TableHead>
<TableHead className="w-[130px] 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="w-[60px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
@@ -1231,7 +1252,7 @@ export default function PurchaseOrderPage() {
</TableHeader>
<TableBody>
{itemSearchResults.length === 0 ? (
<TableRow><TableCell colSpan={6} className="text-center text-muted-foreground py-8"> </TableCell></TableRow>
<TableRow><TableCell colSpan={9} className="text-center text-muted-foreground py-8"> </TableCell></TableRow>
) : itemSearchResults.map((item) => (
<TableRow key={item.id} className={cn("cursor-pointer", itemSelectedMap.has(item.id) && "bg-primary/5")}
onClick={() => setItemSelectedMap((prev) => {
@@ -1244,6 +1265,9 @@ export default function PurchaseOrderPage() {
</TableCell>
<TableCell className="text-[13px] max-w-[130px]"><span className="block truncate" title={item.item_number}>{item.item_number}</span></TableCell>
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
<TableCell className="text-right text-[13px] font-mono">{item.width || "-"}</TableCell>
<TableCell className="text-right text-[13px] font-mono">{item.height || "-"}</TableCell>
<TableCell className="text-right text-[13px] font-mono">{item.thickness || "-"}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>

View File

@@ -141,6 +141,9 @@ const FORM_FIELDS = [
{ key: "item_name", label: "품명", type: "text", required: true },
{ key: "division", label: "관리품목", type: "multi-category" },
{ key: "type", label: "품목구분", type: "category" },
{ key: "width", label: "가로", type: "text", placeholder: "숫자 입력 (예: 1000)" },
{ key: "height", label: "세로", type: "text", placeholder: "숫자 입력 (예: 2000)" },
{ key: "thickness", label: "두께", type: "text", placeholder: "숫자 입력 (예: 10)" },
{ key: "size", label: "규격", type: "text" },
{ key: "inventory_unit", label: "단위", type: "category" },
{ key: "material", label: "재질", type: "category" },
@@ -173,6 +176,9 @@ const formatNum = (val: any): string => {
const ITEM_GRID_COLUMNS = [
{ key: "item_number", label: "품번" },
{ key: "item_name", label: "품명" },
{ key: "width", label: "가로", align: "right" as const },
{ key: "height", label: "세로", align: "right" as const },
{ key: "thickness", label: "두께", align: "right" as const },
{ key: "size", label: "규격" },
{ key: "inventory_unit", label: "단위" },
{ key: "standard_price", label: "기준단가/구매단가" },

View File

@@ -148,6 +148,9 @@ const formatNum = (val: any): string => {
const ITEM_GRID_COLUMNS = [
{ key: "item_number", label: "품번" },
{ key: "item_name", label: "품명" },
{ key: "width", label: "가로", align: "right" as const },
{ key: "height", label: "세로", align: "right" as const },
{ key: "thickness", label: "두께", align: "right" as const },
{ key: "size", label: "규격" },
{ key: "inventory_unit", label: "단위" },
{ key: "standard_price", label: "기준단가" },
@@ -161,6 +164,9 @@ const FORM_FIELDS = [
{ key: "item_name", label: "품명", type: "text", required: true },
{ key: "division", label: "관리품목", type: "multi-category" },
{ key: "type", label: "품목구분", type: "category" },
{ key: "width", label: "가로", type: "text", placeholder: "숫자 입력 (예: 1000)" },
{ key: "height", label: "세로", type: "text", placeholder: "숫자 입력 (예: 2000)" },
{ key: "thickness", label: "두께", type: "text", placeholder: "숫자 입력 (예: 10)" },
{ key: "size", label: "규격", type: "text" },
{ key: "inventory_unit", label: "단위", type: "category" },
{ key: "material", label: "재질", type: "category" },
@@ -1169,6 +1175,9 @@ export default function SalesItemPage() {
const itemColumns: EDataTableColumn[] = [
{ key: "item_number", label: "품번", width: "w-[110px]" },
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
{ key: "width", label: "가로", width: "w-[70px]", align: "right" },
{ key: "height", label: "세로", width: "w-[70px]", align: "right" },
{ key: "thickness", label: "두께", width: "w-[70px]", align: "right" },
{ key: "size", label: "규격", width: "w-[80px]" },
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },