diff --git a/frontend/app/(main)/COMPANY_30/sales/order/page.tsx b/frontend/app/(main)/COMPANY_30/sales/order/page.tsx index 3a7df3f0..7eca3b76 100644 --- a/frontend/app/(main)/COMPANY_30/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_30/sales/order/page.tsx @@ -71,7 +71,8 @@ const LEFT_COLUMNS: DataGridColumn[] = [ // 우측: 품목 상세 (가로/세로/두께/면적 포함) const RIGHT_COLUMNS: DataGridColumn[] = [ - { key: "division", label: "구분", width: "w-[70px]" }, + { key: "division", label: "관리품목", width: "w-[90px]" }, + { key: "type", label: "품목구분", width: "w-[90px]" }, { key: "part_name", label: "품명", minWidth: "min-w-[120px]" }, { key: "spec", label: "규격", width: "w-[100px]" }, { key: "width", label: "가로", width: "w-[65px]", formatNumber: true, align: "right" }, @@ -91,7 +92,8 @@ const RIGHT_COLUMNS: DataGridColumn[] = [ // 모달 품목 테이블 컬럼 (드래그 재정렬 + resize) type ModalCol = { key: string; label: string; width: number }; const MODAL_DETAIL_COLUMNS: ModalCol[] = [ - { key: "division", label: "구분", width: 100 }, + { key: "division", label: "관리품목", width: 110 }, + { key: "type", label: "품목구분", width: 110 }, { key: "part_name", label: "품명", width: 170 }, { key: "spec", label: "규격", width: 110 }, { key: "width", label: "가로", width: 90 }, @@ -106,6 +108,34 @@ const MODAL_DETAIL_COLUMNS: ModalCol[] = [ ]; const MODAL_COL_KEY = "c30_sales_order_modal_col"; +// sales_order_detail 행에 item_info의 division/type을 붙여줌 (detail 테이블엔 없고 item_info에만 있음) +async function enrichDetailsWithItemInfo>(rows: T[]): Promise { + const codes = [...new Set(rows.map((r) => r.part_code).filter(Boolean) as string[])]; + if (codes.length === 0) return rows; + try { + const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: codes.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: codes }] }, + autoFilter: true, + }); + const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || []; + const map: Record = {}; + for (const i of items) { + map[i.item_number] = { division: i.division || "", type: i.type || "" }; + } + return rows.map((r) => { + const info = r.part_code ? map[r.part_code] : undefined; + return { + ...r, + division: r.division || info?.division || "", + type: r.type || info?.type || "", + }; + }); + } catch { + return rows; + } +} + function SortableModalHead({ col, onResize }: { col: ModalCol; onResize: (key: string, w: number) => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); const [resizing, setResizing] = useState(false); @@ -208,12 +238,18 @@ export default function ChunganSalesOrderPage() { if (!saved) return; const parsed = JSON.parse(saved) as { key: string; width: number }[]; const byKey = new Map(parsed.map((c) => [c.key, c.width])); - const ordered = parsed - .map((p) => MODAL_DETAIL_COLUMNS.find((c) => c.key === p.key)) - .filter(Boolean) as ModalCol[]; - const missing = MODAL_DETAIL_COLUMNS.filter((c) => !byKey.has(c.key)); - const merged = [...ordered, ...missing].map((c) => ({ ...c, width: byKey.get(c.key) ?? c.width })); - setModalColumns(merged); + const savedKeys = new Set(parsed.map((p) => p.key)); + const defaultKeys = new Set(MODAL_DETAIL_COLUMNS.map((c) => c.key)); + // 저장된 키셋이 기본 정의와 정확히 일치할 때만 저장된 순서 사용, + // 그렇지 않으면 (새 컬럼 추가 등) 기본 정의 순서를 따르고 너비만 복원 + if (savedKeys.size === defaultKeys.size && [...savedKeys].every((k) => defaultKeys.has(k))) { + const ordered = parsed + .map((p) => MODAL_DETAIL_COLUMNS.find((c) => c.key === p.key)) + .filter(Boolean) as ModalCol[]; + setModalColumns(ordered.map((c) => ({ ...c, width: byKey.get(c.key) ?? c.width }))); + } else { + setModalColumns(MODAL_DETAIL_COLUMNS.map((c) => ({ ...c, width: byKey.get(c.key) ?? c.width }))); + } } catch { /* skip */ } }, []); @@ -353,6 +389,7 @@ export default function ChunganSalesOrderPage() { allRows = dRes.data?.data?.data || dRes.data?.data?.rows || []; } catch { /* skip */ } } + allRows = await enrichDetailsWithItemInfo(allRows); setAllDetails(allRows); const masterMap: Record = {}; @@ -458,6 +495,7 @@ export default function ChunganSalesOrderPage() { .map((d) => ({ ...d, division: categoryOptions["item_division"]?.find((o) => o.code === d.division)?.label || d.division || "", + type: categoryOptions["item_type"]?.find((o) => o.code === d.type)?.label || d.type || "", })); setDetailItems(items); }, [selectedOrderNo, allDetails, categoryOptions]); @@ -500,7 +538,8 @@ export default function ChunganSalesOrderPage() { dataFilter: { enabled: true, filters: [{ columnName: "order_no", operator: "equals", value: orderNo }] }, autoFilter: true, }); - const detailData = detailRes.data?.data?.data || detailRes.data?.data?.rows || []; + const rawDetail = detailRes.data?.data?.data || detailRes.data?.data?.rows || []; + const detailData = await enrichDetailsWithItemInfo(rawDetail); setMasterForm(masterData || {}); setModalDetailRows(detailData.map((d: any, i: number) => ({ @@ -508,6 +547,7 @@ export default function ChunganSalesOrderPage() { _id: d.id || `row_${i}`, _fromItemInfo: !!d.part_code, _divisionLabel: categoryOptions["item_division"]?.find((o: any) => o.code === d.division)?.label || d.division || "", + _typeLabel: categoryOptions["item_type"]?.find((o: any) => o.code === d.type)?.label || d.type || "", }))); setIsEditMode(true); setIsModalOpen(true); @@ -565,29 +605,24 @@ export default function ChunganSalesOrderPage() { } }; - // 품목 자동 등록 (item_info에 없으면 등록) + // 품목 자동 등록 (품목검색으로 가져온 기존 품목은 skip, 행추가한 건 무조건 신규 INSERT) const autoRegisterItems = async (rows: any[]) => { for (const row of rows) { - if (row.part_code || !row.part_name) continue; + if (row._fromItemInfo || row.part_code) continue; + if (!row.part_name) continue; try { - // item_info에서 품명으로 검색 - const searchRes = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, { - page: 1, size: 1, - dataFilter: { enabled: true, filters: [{ columnName: "item_name", operator: "equals", value: row.part_name }] }, - autoFilter: true, - }); - const found = (searchRes.data?.data?.data || searchRes.data?.data?.rows || [])[0]; - if (found) { - row.part_code = found.item_number; - continue; - } - // 없으면 자동 등록 await apiClient.post(`/table-management/tables/${ITEM_TABLE}/add`, { + id: crypto.randomUUID(), item_name: row.part_name, + division: row.division || "", + type: row.type || "", size: row.spec || "", unit: row.unit || "", + width: row.width || "", + height: row.height || "", + thickness: row.thickness || "", }); - // 등록 후 재조회하여 item_number 획득 + // 방금 등록된 레코드의 item_number 획득 (동명 중복이 있을 수 있으므로 최신 1건) const reSearch = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, { page: 1, size: 1, dataFilter: { enabled: true, filters: [{ columnName: "item_name", operator: "equals", value: row.part_name }] }, @@ -651,7 +686,7 @@ export default function ChunganSalesOrderPage() { for (let i = 0; i < modalDetailRows.length; i++) { const row = modalDetailRows[i]; - const { _id, _fromItemInfo, _divisionLabel, id: rowId, created_date: _cd, updated_date: _ud, writer: _w, company_code: _cc, ...detailFields } = row; + const { _id, _fromItemInfo, _divisionLabel, _typeLabel, division: _div, type: _typ, id: rowId, created_date: _cd, updated_date: _ud, writer: _w, company_code: _cc, ...detailFields } = row; await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/add`, { id: crypto.randomUUID(), ...detailFields, @@ -731,7 +766,10 @@ export default function ChunganSalesOrderPage() { setModalDetailRows((prev) => [...prev, { _id: `new_${Date.now()}_${Math.random()}`, _fromItemInfo: false, - part_code: "", part_name: "", spec: "", division: "", _divisionLabel: "", unit: "㎡", + part_code: "", part_name: "", spec: "", + division: "", _divisionLabel: "", + type: "", _typeLabel: "", + unit: "㎡", width: "", height: "", thickness: "", area: "", qty: "", unit_price: "", amount: "", due_date: "", memo: "", @@ -785,12 +823,23 @@ export default function ChunganSalesOrderPage() { {row._divisionLabel || "-"} ) : ( ); + case "type": + return row._fromItemInfo ? ( + {row._typeLabel || "-"} + ) : ( + + ); case "part_name": return row._fromItemInfo ? ( {row.part_name || "-"}