Merge branch 'gbpark-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node
This commit is contained in:
@@ -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<T extends Record<string, any>>(rows: T[]): Promise<T[]> {
|
||||
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<string, { division: string; type: string }> = {};
|
||||
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<string, any> = {};
|
||||
@@ -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() {
|
||||
<span className="text-sm px-2">{row._divisionLabel || "-"}</span>
|
||||
) : (
|
||||
<Select value={row.division || ""} onValueChange={(v) => updateDetailRow(idx, "division", v)}>
|
||||
<SelectTrigger className="h-8 text-sm"><SelectValue placeholder="구분" /></SelectTrigger>
|
||||
<SelectTrigger className="h-8 text-sm"><SelectValue placeholder="관리품목" /></SelectTrigger>
|
||||
<SelectContent position="popper" sideOffset={4}>
|
||||
{(categoryOptions["item_division"] || []).map((o) => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
case "type":
|
||||
return row._fromItemInfo ? (
|
||||
<span className="text-sm px-2">{row._typeLabel || "-"}</span>
|
||||
) : (
|
||||
<Select value={row.type || ""} onValueChange={(v) => updateDetailRow(idx, "type", v)}>
|
||||
<SelectTrigger className="h-8 text-sm"><SelectValue placeholder="품목구분" /></SelectTrigger>
|
||||
<SelectContent position="popper" sideOffset={4}>
|
||||
{(categoryOptions["item_type"] || []).map((o) => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
case "part_name":
|
||||
return row._fromItemInfo ? (
|
||||
<span className="text-sm px-2">{row.part_name || "-"}</span>
|
||||
|
||||
Reference in New Issue
Block a user