Files
wace_rps/frontend/components/common/PartSelect.tsx
T
hjjeong 66e2a63dfa 영업관리 G1 수주확정→프로젝트 자동생성 + 견적요청등록 wace 1:1 이식
- G1: salesOrderMgmtService.updateStatus 트랜잭션화 + project_mgmt 자동생성 (project_no 채번/Machine 분기/contract_item 라인별 INSERT)
- 수주확정 다이얼로그(상태 select 팝업) + 수주취소 다이얼로그(라인별 cancel_qty)·POST /sales/order-mgmt/:id/cancel-qty 신설
- 견적요청등록 폼: estimate_template 분리, 헤더 8(주문유형/국내해외/고객사/유무상/접수일/견적환종/견적환율/결재여부) + 라인 8(제품구분/품번/품명/S/N/견적수량/요청납기/반납사유/고객요청사항) wace 운영 화면과 1:1
- S/N 관리 다이얼로그(테이블+연속번호생성), PartSelect/CommCodeSelect/CustomerSelect 셀렉트박스 + ✕(선택해제), 수주확정된 행 라인 추가/삭제 차단
- DataGrid 체크박스 모드 (영업번호 No → 체크박스, 행 어디 클릭이나 단일 선택)
- 식별자 정합성: contract_mgmt.customer_objid를 customer_mng.customer_code 기반(C_xxxx)으로 통일, contract_no 채번 {YY}C-{NNNN} 운영 패턴 일치, contract_item.quantity ::integer 캐스트

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:48:25 +09:00

99 lines
2.9 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { apiClient } from "@/lib/api/client";
/**
* 품번/품명 자동완성 셀렉트
*
* wace_plm orderMgmtList의 select2-part AJAX 패턴을 단순화한 형태.
* - item_info 전체를 한 번 캐시 (id 기준 단일 소스)
* - mode='partNo': 라벨로 item_number 표시
* - mode='partName': 라벨로 item_name 표시
* - 선택값(value)은 양쪽 모두 item_info.id (= part_objid)
*/
interface PartRow {
id: string;
item_number?: string;
item_name?: string;
}
interface PartSelectProps {
mode: "partNo" | "partName";
/** item_info.id (part_objid) */
value: string;
/** 옵션 선택 시 part_objid + (선택사항) 마스터 정보(item_number/item_name) 전달 */
onValueChange: (partObjId: string, row?: { item_number?: string; item_name?: string }) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}
let cachedRows: PartRow[] | null = null;
let inflight: Promise<PartRow[]> | null = null;
const fetchParts = async (): Promise<PartRow[]> => {
if (cachedRows) return cachedRows;
if (inflight) return inflight;
inflight = (async () => {
// 영업관리 4개 메뉴 공통 endpoint — wace 이식 8179건 + COMPANY_16 데이터.
const res = await apiClient.get("/sales/parts");
const rows = (res.data?.data ?? []) as any[];
cachedRows = rows
.filter((r) => r.id != null)
.map((r) => ({
id: String(r.id),
item_number: r.item_number ?? "",
item_name: r.item_name ?? "",
}));
return cachedRows!;
})();
try {
return await inflight;
} finally {
inflight = null;
}
};
const toOptions = (rows: PartRow[], mode: PartSelectProps["mode"]): SmartSelectOption[] =>
rows
.filter((r) => mode === "partNo" ? r.item_number : r.item_name)
.map((r) => ({
code: r.id,
label: String(mode === "partNo" ? r.item_number : r.item_name),
}));
export function PartSelect({
mode, value, onValueChange,
placeholder = mode === "partNo" ? "품번 선택" : "품명 선택",
disabled, className,
}: PartSelectProps) {
const [options, setOptions] = useState<SmartSelectOption[]>(
cachedRows ? toOptions(cachedRows, mode) : [],
);
useEffect(() => {
let alive = true;
fetchParts()
.then((rows) => { if (alive) setOptions(toOptions(rows, mode)); })
.catch(() => {});
return () => { alive = false; };
}, [mode]);
return (
<SmartSelect
options={options}
value={value}
onValueChange={(v) => {
const row = cachedRows?.find((r) => r.id === v);
onValueChange(v, row ? { item_number: row.item_number, item_name: row.item_name } : undefined);
}}
placeholder={placeholder}
disabled={disabled}
className={className}
/>
);
}