김주석씨

This commit is contained in:
DDD1542
2026-04-15 11:46:05 +09:00
parent 4d55aebe61
commit b7082b7799
16 changed files with 423 additions and 24 deletions

View File

@@ -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 ")}

View File

@@ -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}

View File

@@ -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

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 }>>({});
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>

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

@@ -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>

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

@@ -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 },

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

@@ -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}

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 },

View File

@@ -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"

View File

@@ -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> },

View File

@@ -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] || []}

View 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>
);
}