김주석씨
This commit is contained in:
@@ -543,6 +543,9 @@ export async function getItems(req: AuthenticatedRequest, res: Response) {
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, item_number, item_name, size AS spec, material, unit,
|
||||
COALESCE(width::text, '') AS width,
|
||||
COALESCE(height::text, '') AS height,
|
||||
COALESCE(thickness::text, '') AS thickness,
|
||||
COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price
|
||||
FROM item_info
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
|
||||
@@ -991,6 +991,9 @@ export async function getItems(req: AuthenticatedRequest, res: Response) {
|
||||
const dataResult = await pool.query(
|
||||
`SELECT
|
||||
id, item_number, item_name, size AS spec, material, unit,
|
||||
COALESCE(width::text, '') AS width,
|
||||
COALESCE(height::text, '') AS height,
|
||||
COALESCE(thickness::text, '') AS thickness,
|
||||
COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price
|
||||
FROM item_info
|
||||
WHERE ${whereClause}
|
||||
|
||||
@@ -333,7 +333,7 @@ export async function getShipmentPlanSource(req: AuthenticatedRequest, res: Resp
|
||||
LEFT JOIN sales_order_detail d ON sp.detail_id = d.id AND sp.company_code = d.company_code
|
||||
LEFT JOIN sales_order_mng m ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
SELECT item_name, width, height, thickness FROM item_info
|
||||
WHERE item_number = COALESCE(d.part_code, m.part_code) AND company_code = sp.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
@@ -353,6 +353,9 @@ export async function getShipmentPlanSource(req: AuthenticatedRequest, res: Resp
|
||||
COALESCE(d.part_code, m.part_code, '') AS item_code,
|
||||
COALESCE(i.item_name, d.part_name, m.part_name, COALESCE(d.part_code, m.part_code, '')) AS item_name,
|
||||
COALESCE(d.spec, m.spec, '') AS spec,
|
||||
COALESCE(i.width::text, '') AS width,
|
||||
COALESCE(i.height::text, '') AS height,
|
||||
COALESCE(i.thickness::text, '') AS thickness,
|
||||
COALESCE(m.material, '') AS material,
|
||||
COALESCE(c.customer_name, '') AS customer_name,
|
||||
COALESCE(m.partner_id, d.delivery_partner_code, '') AS partner_code,
|
||||
@@ -400,7 +403,7 @@ export async function getSalesOrderSource(req: AuthenticatedRequest, res: Respon
|
||||
FROM sales_order_detail d
|
||||
LEFT JOIN sales_order_mng m ON d.order_no = m.order_no AND d.company_code = m.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
SELECT item_name, width, height, thickness FROM item_info
|
||||
WHERE item_number = d.part_code AND company_code = d.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
@@ -418,6 +421,9 @@ export async function getSalesOrderSource(req: AuthenticatedRequest, res: Respon
|
||||
d.id, d.order_no, d.part_code AS item_code,
|
||||
COALESCE(i.item_name, d.part_name, d.part_code) AS item_name,
|
||||
COALESCE(d.spec, '') AS spec, COALESCE(m.material, '') AS material,
|
||||
COALESCE(i.width::text, '') AS width,
|
||||
COALESCE(i.height::text, '') AS height,
|
||||
COALESCE(i.thickness::text, '') AS thickness,
|
||||
COALESCE(NULLIF(d.qty,'')::numeric, 0) AS qty,
|
||||
COALESCE(NULLIF(d.balance_qty,'')::numeric, 0) AS balance_qty,
|
||||
COALESCE(c.customer_name, COALESCE(d.delivery_partner_code, m.partner_id, '')) AS customer_name,
|
||||
@@ -465,7 +471,10 @@ export async function getItemSource(req: AuthenticatedRequest, res: Response) {
|
||||
const query = `
|
||||
SELECT
|
||||
item_number AS item_code, item_name,
|
||||
COALESCE(size, '') AS spec, COALESCE(material, '') AS material
|
||||
COALESCE(size, '') AS spec, COALESCE(material, '') AS material,
|
||||
COALESCE(width::text, '') AS width,
|
||||
COALESCE(height::text, '') AS height,
|
||||
COALESCE(thickness::text, '') AS thickness
|
||||
FROM item_info
|
||||
WHERE ${whereClause}
|
||||
ORDER BY item_name
|
||||
|
||||
@@ -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 }>>({});
|
||||
const [itemMap, setItemMap] = useState<Record<string, { item_name: string; unit: string; width: string; height: string; thickness: string }>>({});
|
||||
const [warehouseMap, setWarehouseMap] = useState<Record<string, string>>({});
|
||||
const [userMap, setUserMap] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -125,10 +125,16 @@ 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 }> = {};
|
||||
const map: Record<string, { item_name: string; unit: string; width: string; height: string; thickness: 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 };
|
||||
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 || "",
|
||||
};
|
||||
}
|
||||
setItemMap(map);
|
||||
} catch { /* skip */ }
|
||||
@@ -341,6 +347,9 @@ 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>
|
||||
@@ -361,6 +370,7 @@ 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>
|
||||
@@ -404,6 +414,9 @@ 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -86,6 +86,9 @@ 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: "단가" },
|
||||
@@ -95,8 +98,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({
|
||||
@@ -658,6 +661,9 @@ export default function ReceivingPage() {
|
||||
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,
|
||||
@@ -684,6 +690,9 @@ export default function ReceivingPage() {
|
||||
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,
|
||||
@@ -710,6 +719,9 @@ export default function ReceivingPage() {
|
||||
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,
|
||||
@@ -939,6 +951,9 @@ 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" }} />
|
||||
@@ -1050,6 +1065,9 @@ 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>
|
||||
@@ -1421,7 +1439,7 @@ export default function ReceivingPage() {
|
||||
<TableCell className="p-2 text-center">
|
||||
{idx + 1}
|
||||
</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}
|
||||
@@ -1430,6 +1448,13 @@ 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]">
|
||||
@@ -1595,13 +1620,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">
|
||||
@@ -1679,13 +1711,20 @@ function SourceShipmentTable({
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={sh.partner_id}>{sh.partner_id}</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={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">
|
||||
@@ -1758,6 +1797,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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -42,6 +42,9 @@ const formatNum = (v: any) => (v == null || v === "" ? "-" : Number(v).toLocaleS
|
||||
const GRID_COLUMNS_CONFIG = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "width", label: "가로" },
|
||||
{ key: "height", label: "세로" },
|
||||
{ key: "thickness", label: "두께" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
@@ -124,6 +127,9 @@ export default function SubcontractorItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" },
|
||||
size: { width: "w-[90px]", render: (v) => v || "-" },
|
||||
width: { width: "w-[70px]", align: "right", render: (v) => v || "-" },
|
||||
height: { width: "w-[70px]", align: "right", render: (v) => v || "-" },
|
||||
thickness: { width: "w-[70px]", align: "right", render: (v) => v || "-" },
|
||||
unit: { width: "w-[60px]", render: (v) => v || "-" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
selling_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: "기준단가/구매단가" },
|
||||
|
||||
@@ -50,6 +50,9 @@ const FLAT_COLUMNS = [
|
||||
{ key: "order_date", label: "수주일", source: "master" },
|
||||
{ key: "part_code", label: "품번", source: "detail" },
|
||||
{ key: "part_name", label: "품명", source: "detail" },
|
||||
{ key: "width", label: "가로", source: "detail" },
|
||||
{ key: "height", label: "세로", source: "detail" },
|
||||
{ key: "thickness", label: "두께", source: "detail" },
|
||||
{ key: "spec", label: "규격", source: "detail" },
|
||||
{ key: "unit", label: "단위", source: "detail" },
|
||||
{ key: "qty", label: "수량", source: "detail" },
|
||||
@@ -66,8 +69,8 @@ const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail");
|
||||
// 필터용 전체 키
|
||||
const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label }));
|
||||
|
||||
// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(14) = 15
|
||||
const TOTAL_COLS = 15;
|
||||
// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(17) = 18
|
||||
const TOTAL_COLS = 18;
|
||||
|
||||
// 헤더 필터 Popover
|
||||
function HeaderFilterPopover({
|
||||
@@ -363,6 +366,9 @@ export default function SalesOrderPage() {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
width: row.width || item?.width || "",
|
||||
height: row.height || item?.height || "",
|
||||
thickness: row.thickness || item?.thickness || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
@@ -827,6 +833,9 @@ export default function SalesOrderPage() {
|
||||
part_code: itemCode,
|
||||
part_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
width: item.width || "",
|
||||
height: item.height || "",
|
||||
thickness: item.thickness || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
@@ -1022,7 +1031,10 @@ export default function SalesOrderPage() {
|
||||
<col style={{ width: "100px" }} />
|
||||
<col style={{ width: "120px" }} />
|
||||
<col style={{ width: "140px" }} />
|
||||
<col style={{ width: "80px" }} />
|
||||
<col style={{ width: "70px" }} />
|
||||
<col style={{ width: "70px" }} />
|
||||
<col style={{ width: "70px" }} />
|
||||
<col style={{ width: "100px" }} />
|
||||
<col style={{ width: "70px" }} />
|
||||
<col style={{ width: "80px" }} />
|
||||
<col style={{ width: "80px" }} />
|
||||
@@ -1138,6 +1150,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell className="whitespace-nowrap text-[13px]">{row.order_date || ""}</TableCell>
|
||||
<TableCell className="font-mono text-[13px]">{row.part_code}</TableCell>
|
||||
<TableCell className="text-[13px] max-w-[150px]"><span className="block truncate" title={row.part_name}>{row.part_name}</span></TableCell>
|
||||
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{row.width || "-"}</TableCell>
|
||||
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{row.height || "-"}</TableCell>
|
||||
<TableCell className="text-right font-mono text-[13px] text-muted-foreground">{row.thickness || "-"}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
|
||||
<TableCell className="text-[13px]">{row.unit}</TableCell>
|
||||
<TableCell className="text-right font-mono text-[13px]">{row.qty ? Number(row.qty).toLocaleString() : ""}</TableCell>
|
||||
@@ -1497,6 +1512,9 @@ export default function SalesOrderPage() {
|
||||
<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-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">가로</TableHead>
|
||||
<TableHead className="min-w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">세로</TableHead>
|
||||
<TableHead className="min-w-[70px] text-right 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>
|
||||
@@ -1519,6 +1537,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<span className="text-[13px]" title={row.part_name}>{row.part_name}</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px] text-right font-mono text-muted-foreground whitespace-nowrap">{row.width || "-"}</TableCell>
|
||||
<TableCell className="text-[13px] text-right font-mono text-muted-foreground whitespace-nowrap">{row.height || "-"}</TableCell>
|
||||
<TableCell className="text-[13px] text-right font-mono text-muted-foreground whitespace-nowrap">{row.thickness || "-"}</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>
|
||||
@@ -1674,6 +1695,9 @@ export default function SalesOrderPage() {
|
||||
</TableHead>
|
||||
<TableHead className="w-32 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-16 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">가로</TableHead>
|
||||
<TableHead className="w-16 text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">세로</TableHead>
|
||||
<TableHead className="w-16 text-right 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-24 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">재질</TableHead>
|
||||
<TableHead className="w-16 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단위</TableHead>
|
||||
@@ -1682,7 +1706,7 @@ export default function SalesOrderPage() {
|
||||
<TableBody>
|
||||
{itemSearchResults.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="py-8 text-center text-muted-foreground">검색 결과가 없어요</TableCell>
|
||||
<TableCell colSpan={9} className="py-8 text-center text-muted-foreground">검색 결과가 없어요</TableCell>
|
||||
</TableRow>
|
||||
) : itemSearchResults.map((item) => (
|
||||
<TableRow
|
||||
@@ -1703,6 +1727,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell className="max-w-[150px]">
|
||||
<span className="block truncate text-sm" 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}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -37,6 +37,9 @@ const GRID_COLUMNS = [
|
||||
{ key: "status", label: "상태" },
|
||||
{ key: "item_code", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "width", label: "가로" },
|
||||
{ key: "height", label: "세로" },
|
||||
{ key: "thickness", label: "두께" },
|
||||
{ key: "qty", label: "수량" },
|
||||
{ key: "source_type", label: "소스" },
|
||||
{ key: "remark", label: "비고" },
|
||||
@@ -76,6 +79,9 @@ interface SelectedItem {
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
spec: string;
|
||||
width: string;
|
||||
height: string;
|
||||
thickness: string;
|
||||
material: string;
|
||||
customer: string;
|
||||
planQty: number;
|
||||
@@ -248,6 +254,9 @@ export default function ShippingOrderPage() {
|
||||
itemCode: it.item_code || "",
|
||||
itemName: it.item_name || "",
|
||||
spec: it.spec || "",
|
||||
width: it.width || "",
|
||||
height: it.height || "",
|
||||
thickness: it.thickness || "",
|
||||
material: it.material || "",
|
||||
customer: order.customer_name || "",
|
||||
planQty: Number(it.plan_qty || 0),
|
||||
@@ -311,6 +320,9 @@ export default function ShippingOrderPage() {
|
||||
itemCode: item.item_code || "",
|
||||
itemName: item.item_name || "",
|
||||
spec: item.spec || "",
|
||||
width: item.width || "",
|
||||
height: item.height || "",
|
||||
thickness: item.thickness || "",
|
||||
material: item.material || "",
|
||||
customer: item.customer_name || "",
|
||||
planQty: Number(item.plan_qty || item.qty || item.balance_qty || 0),
|
||||
@@ -628,6 +640,9 @@ export default function ShippingOrderPage() {
|
||||
<TableHead className="w-[40px] text-center 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="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-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
|
||||
@@ -657,6 +672,9 @@ export default function ShippingOrderPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_code || "-"}</TableCell>
|
||||
<TableCell className="text-sm font-medium">{item.item_name || "-"}</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] text-muted-foreground">{item.spec || "-"}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.customer_name || "-"}</TableCell>
|
||||
<TableCell className="text-right text-[13px]">
|
||||
@@ -837,6 +855,9 @@ export default function ShippingOrderPage() {
|
||||
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">소스</TableHead>
|
||||
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>
|
||||
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>
|
||||
<TableHead className="w-[50px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">가로</TableHead>
|
||||
<TableHead className="w-[50px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">세로</TableHead>
|
||||
<TableHead className="w-[50px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">두께</TableHead>
|
||||
<TableHead className="w-[90px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출하수량</TableHead>
|
||||
<TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획수량</TableHead>
|
||||
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">삭제</TableHead>
|
||||
@@ -854,6 +875,9 @@ export default function ShippingOrderPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">{item.itemCode}</TableCell>
|
||||
<TableCell className="text-sm font-medium">{item.itemName}</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-center">
|
||||
<Input
|
||||
type="number"
|
||||
|
||||
@@ -26,6 +26,9 @@ const GRID_COLUMNS = [
|
||||
{ key: "customer_name", label: "거래처" },
|
||||
{ key: "part_code", label: "품목코드" },
|
||||
{ key: "part_name", label: "품목명" },
|
||||
{ key: "width", label: "가로" },
|
||||
{ key: "height", label: "세로" },
|
||||
{ key: "thickness", label: "두께" },
|
||||
{ key: "order_qty", label: "수주수량" },
|
||||
{ key: "plan_qty", label: "계획수량" },
|
||||
{ key: "plan_date", label: "계획일" },
|
||||
@@ -242,6 +245,9 @@ export default function ShippingPlanPage() {
|
||||
{ key: "customer_name", label: "거래처", render: (val: any) => <span className="text-sm">{val || "-"}</span> },
|
||||
{ key: "part_code", label: "품목코드", render: (val: any) => <span className="text-muted-foreground text-[13px]">{val || "-"}</span> },
|
||||
{ key: "part_name", label: "품목명", render: (val: any) => <span className="font-medium text-sm">{val || "-"}</span> },
|
||||
{ key: "width", label: "가로", align: "right" as const, render: (val: any) => <span className="font-mono text-[13px]">{val || "-"}</span> },
|
||||
{ key: "height", label: "세로", align: "right" as const, render: (val: any) => <span className="font-mono text-[13px]">{val || "-"}</span> },
|
||||
{ key: "thickness", label: "두께", align: "right" as const, render: (val: any) => <span className="font-mono text-[13px]">{val || "-"}</span> },
|
||||
{ key: "order_qty", label: "수주수량", align: "right" as const, formatNumber: true },
|
||||
{ key: "plan_qty", label: "계획수량", align: "right" as const, render: (val: any) => <span className="font-semibold text-primary text-sm">{formatNumber(val)}</span> },
|
||||
{ key: "plan_date", label: "계획일", align: "center" as const, render: (val: any) => <span className="text-sm">{formatDate(val)}</span> },
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { PdfUpload } from "@/components/common/PdfUpload";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
|
||||
@@ -145,6 +146,7 @@ const GRID_COLUMNS = [
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "material", label: "재질" },
|
||||
{ key: "mold_number", label: "금형번호" },
|
||||
{ key: "status", label: "상태" },
|
||||
{ key: "selling_price", label: "판매가격", align: "right" as const, formatNumber: true },
|
||||
{ key: "standard_price", label: "기준단가", align: "right" as const, formatNumber: true },
|
||||
@@ -153,6 +155,9 @@ const GRID_COLUMNS = [
|
||||
{ key: "user_type01", label: "대분류" },
|
||||
{ key: "user_type02", label: "중분류" },
|
||||
{ key: "lead_time", label: "생산 리드타임(일)", align: "right" as const },
|
||||
{ key: "use_insert", label: "인서트" },
|
||||
{ key: "use_packaging", label: "포장" },
|
||||
{ key: "drawing_path", label: "도면" },
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
@@ -163,6 +168,7 @@ const FORM_FIELDS = [
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "mold_number", label: "금형번호", type: "text" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
@@ -174,6 +180,9 @@ const FORM_FIELDS = [
|
||||
{ key: "user_type01", label: "대분류", type: "category" },
|
||||
{ key: "user_type02", label: "중분류", type: "category" },
|
||||
{ key: "lead_time", label: "생산 리드타임(일)", type: "text", placeholder: "숫자 입력 (예: 7)" },
|
||||
{ key: "use_insert", label: "인서트 사용", type: "category" },
|
||||
{ key: "use_packaging", label: "포장 사용", type: "category" },
|
||||
{ key: "drawing_path", label: "도면 (PDF)", type: "pdf" },
|
||||
{ key: "image", label: "품목 이미지", type: "image" },
|
||||
{ key: "meno", label: "메모", type: "textarea" },
|
||||
];
|
||||
@@ -181,6 +190,7 @@ const FORM_FIELDS = [
|
||||
const CATEGORY_COLUMNS = [
|
||||
"division", "type", "unit", "material", "status",
|
||||
"inventory_unit", "currency_code", "user_type01", "user_type02",
|
||||
"use_insert", "use_packaging",
|
||||
];
|
||||
|
||||
export default function ItemInfoPage() {
|
||||
@@ -737,7 +747,7 @@ export default function ItemInfoPage() {
|
||||
{FORM_FIELDS.map((field) => (
|
||||
<div
|
||||
key={field.key}
|
||||
className={cn("space-y-1.5", (field.type === "textarea" || field.type === "image") && "col-span-2")}
|
||||
className={cn("space-y-1.5", (field.type === "textarea" || field.type === "image" || field.type === "pdf") && "col-span-2")}
|
||||
>
|
||||
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{field.label}
|
||||
@@ -752,6 +762,15 @@ export default function ItemInfoPage() {
|
||||
columnName={field.key}
|
||||
height="h-32"
|
||||
/>
|
||||
) : field.type === "pdf" ? (
|
||||
<PdfUpload
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(v) => setFormData((prev) => ({ ...prev, [field.key]: v }))}
|
||||
tableName={TABLE_NAME}
|
||||
recordId={formData.id || ""}
|
||||
columnName={field.key}
|
||||
height="h-32"
|
||||
/>
|
||||
) : field.type === "multi-category" ? (
|
||||
<MultiCategoryCombobox
|
||||
options={categoryOptions[field.key] || []}
|
||||
|
||||
148
frontend/components/common/PdfUpload.tsx
Normal file
148
frontend/components/common/PdfUpload.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Upload, X, Loader2, FileText, ExternalLink } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface PdfUploadProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
tableName?: string;
|
||||
recordId?: string;
|
||||
columnName?: string;
|
||||
height?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PdfUpload({
|
||||
value, onChange, tableName, recordId, columnName,
|
||||
height = "h-40", disabled = false, className,
|
||||
}: PdfUploadProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const apiBase = typeof window !== "undefined"
|
||||
? (process.env.NEXT_PUBLIC_API_URL || "").replace(/\/api\/?$/, "")
|
||||
: "";
|
||||
const pdfUrl = value
|
||||
? (value.startsWith("http") || value.startsWith("/"))
|
||||
? value
|
||||
: `${apiBase}/api/files/preview/${value}`
|
||||
: null;
|
||||
|
||||
const handleUpload = useCallback(async (file: File) => {
|
||||
const isPdf = file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
|
||||
if (!isPdf) {
|
||||
toast.error("PDF 파일만 업로드 가능합니다.");
|
||||
return;
|
||||
}
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
toast.error("파일 크기는 20MB 이하만 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("files", file);
|
||||
formData.append("docType", "PDF");
|
||||
formData.append("docTypeName", "도면PDF");
|
||||
if (tableName) formData.append("linkedTable", tableName);
|
||||
if (recordId) formData.append("recordId", recordId);
|
||||
if (columnName) formData.append("columnName", columnName);
|
||||
if (tableName && recordId) {
|
||||
formData.append("autoLink", "true");
|
||||
if (columnName) formData.append("isVirtualFileColumn", "true");
|
||||
}
|
||||
|
||||
const res = await apiClient.post("/files/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
|
||||
if (res.data?.success && (res.data.files?.length > 0 || res.data.data?.length > 0)) {
|
||||
const uploaded = res.data.files?.[0] || res.data.data?.[0];
|
||||
const objid = uploaded.objid;
|
||||
onChange?.(objid);
|
||||
toast.success("PDF가 업로드되었습니다.");
|
||||
} else {
|
||||
toast.error(res.data?.message || "업로드에 실패했습니다.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("PDF 업로드 실패:", err);
|
||||
toast.error(err.response?.data?.message || "업로드에 실패했습니다.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}, [tableName, recordId, columnName, onChange]);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) handleUpload(file);
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) handleUpload(file);
|
||||
};
|
||||
|
||||
const handleRemove = () => onChange?.("");
|
||||
|
||||
const handleOpen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (pdfUrl) window.open(pdfUrl, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"border-2 border-dashed rounded-lg flex flex-col items-center justify-center cursor-pointer transition-colors overflow-hidden",
|
||||
height,
|
||||
dragOver ? "border-primary bg-primary/5" : pdfUrl ? "border-primary/40 bg-muted/20" : "border-muted-foreground/25 hover:border-primary hover:bg-muted/50",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
onClick={() => !disabled && !uploading && fileRef.current?.click()}
|
||||
onDragOver={(e) => { e.preventDefault(); if (!disabled) setDragOver(true); }}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={!disabled ? handleDrop : undefined}
|
||||
>
|
||||
{uploading ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">업로드 중...</span>
|
||||
</div>
|
||||
) : pdfUrl ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<FileText className="h-10 w-10 text-primary" />
|
||||
<span className="text-xs text-foreground font-medium">PDF 업로드됨</span>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={handleOpen}>
|
||||
<ExternalLink className="h-3 w-3 mr-1" /> 새 탭에서 열기
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Upload className="h-8 w-8 text-muted-foreground/50" />
|
||||
<span className="text-xs text-muted-foreground">PDF 클릭 또는 드래그하여 업로드</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pdfUrl && !disabled && (
|
||||
<Button variant="destructive" size="sm" className="absolute top-1 right-1 h-6 w-6 p-0 rounded-full"
|
||||
onClick={(e) => { e.stopPropagation(); handleRemove(); }}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<input ref={fileRef} type="file" accept="application/pdf,.pdf" onChange={handleFileChange} className="hidden" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user