/** * 외주발주관리 API 클라이언트 (TASK:ERP-019) * 백엔드 라우트: /api/outsource-purchase * * 컨트롤러: * GET / 목록 * GET /auto-processes 공정 자동표기 * GET /:id 상세 * POST / 등록 * PUT /:id 수정 * DELETE /:id 삭제 * POST /release-request 사급자재 출고요청 */ import { apiClient } from "@/lib/api/client"; // ───────────────────────────────────────────────────────────────────────────── // 타입 // ───────────────────────────────────────────────────────────────────────────── export type OPOSourceType = "수주" | "작업지시" | "품목정보" | "생산계획"; export type OPOStatus = "등록" | "진행중" | "완료" | "취소" | "출고요청"; export type ReleaseStatus = "대기" | "요청" | "완료"; export interface OPOMaterial { id?: string; item_code?: string; item_name?: string; qty?: number | string; unit?: string; release_status?: ReleaseStatus; release_date?: string | null; outbound_id?: string | null; } export interface OPOProcess { id?: string; seq: number; process_code?: string; process_name?: string; /** subcontractor_mng.id — 그룹핑·출고요청 키 */ vendor_id?: string; /** 표시·하위호환용. 그룹핑 키로 사용하지 말 것 (사람이 바꿀 수 있음) */ vendor_code?: string; vendor_name?: string; material_needed?: boolean; remark?: string; materials?: OPOMaterial[]; /** 라우팅에 매핑된 외주사 후보 (드롭다운을 이 목록으로 제한) */ vendor_options?: { id: string; code: string; name: string }[]; /** item_routing_detail.id (사급자재 자동 채움 호출에 사용) */ routing_detail_id?: string; } export interface OPOMaster { id?: string; company_code?: string; order_no?: string; source_type: string; source_no?: string; item_code?: string; item_name?: string; spec?: string; material?: string; quantity?: number | string; order_date?: string | null; due_date?: string | null; manager?: string; memo?: string; status?: OPOStatus; writer?: string; created_date?: string; updated_date?: string; // 목록 집계 필드 process_count?: number; vendor_unassigned?: number; material_count?: number; material_pending?: number; first_process_name?: string; first_vendor_name?: string; } export interface OPODetail extends OPOMaster { processes: OPOProcess[]; } export interface OPOInputPayload { order_no?: string; source_type: string; source_no?: string; item_code?: string; item_name?: string; spec?: string; material?: string; quantity?: number | string; order_date?: string | null; due_date?: string | null; manager?: string; memo?: string; status?: OPOStatus; processes?: OPOProcess[]; } export interface ListParams { keyword?: string; status?: string; source_type?: string; date_from?: string; date_to?: string; page?: number; size?: number; sort?: string; } export interface ListResponse { rows: OPOMaster[]; total: number; page: number; size: number; } export interface AutoProcessRouting { id: string; seq_no: number | string; process_code: string; process_name?: string; work_type?: string; execution_type?: string; outsource_supplier?: string; standard_time?: string | number; /** 작업지시 라우팅(work_order_process)에서 채워지는 필드 */ wo_id?: string; plan_qty?: string | number; routing_detail_id?: string; result_status?: string; } export interface AutoProcessResponse { /** 데이터 출처 — 'work_order' (작업지시 라우팅) | 'item_routing' (품목 라우팅 폴백) | 'none' */ source?: "work_order" | "item_routing" | "none"; routing: AutoProcessRouting[]; candidates: AutoProcessRouting[]; } export interface OutsourceableWorkOrderRow { id: string; work_instruction_no: string; qty?: string | number; status?: string; progress_status?: string; item_id?: string; start_date?: string; end_date?: string; created_date?: string; /** 외주발주 가능한 첫 공정명 */ next_process_name?: string; next_process_seq?: string | number; /** 외주/선택가능 연속 공정 개수 */ outsourceable_count?: number; } export interface OutsourceableListResponse { rows: OutsourceableWorkOrderRow[]; total: number; page: number; size: number; } export interface ReleaseRequestPayload { material_ids: string[]; warehouse_code?: string; memo?: string; } export interface ReleaseRequestResult { outbound_ids: string[]; material_ids: string[]; outbound_count: number; material_count: number; } interface ApiEnvelope { success: boolean; data?: T; message?: string; } // ───────────────────────────────────────────────────────────────────────────── // API 호출 // ───────────────────────────────────────────────────────────────────────────── /** 외주발주 목록 조회 */ export async function listOutsourcePurchaseOrders(params: ListParams = {}): Promise { const res = await apiClient.get>("/outsource-purchase", { params }); return res.data?.data || { rows: [], total: 0, page: 1, size: 50 }; } /** 외주발주 상세 조회 (마스터+공정+자재) */ export async function getOutsourcePurchaseOrder(id: string): Promise { const res = await apiClient.get>(`/outsource-purchase/${id}`); return res.data?.data || null; } /** 외주발주 등록 */ export async function createOutsourcePurchaseOrder(payload: OPOInputPayload): Promise { const res = await apiClient.post>("/outsource-purchase", payload); if (!res.data?.success) throw new Error(res.data?.message || "외주발주 등록 실패"); return res.data.data!; } /** 외주발주 수정 */ export async function updateOutsourcePurchaseOrder(id: string, payload: OPOInputPayload): Promise { const res = await apiClient.put>(`/outsource-purchase/${id}`, payload); if (!res.data?.success) throw new Error(res.data?.message || "외주발주 수정 실패"); return res.data.data!; } /** 외주발주 삭제 */ export async function deleteOutsourcePurchaseOrder(id: string): Promise<{ id: string; order_no: string }> { const res = await apiClient.delete>( `/outsource-purchase/${id}`, ); if (!res.data?.success) throw new Error(res.data?.message || "외주발주 삭제 실패"); return res.data.data!; } /** * 공정 자동표기 (TASK:ERP-019 재구현) * * 우선순위: * 1) work_order_id 가 있으면 → work_order_process 기준 (작업지시별 라우팅 + 진행상황) * 2) work_order_id 가 없거나 작업지시 라우팅이 비어있으면 → item_code 기반 폴백 * * 둘 중 하나는 반드시 제공해야 한다. */ export async function getAutoProcesses( args: { workOrderId?: string; itemCode?: string }, ): Promise { const params: Record = {}; if (args.workOrderId) params.work_order_id = args.workOrderId; if (args.itemCode) params.item_code = args.itemCode; if (!params.work_order_id && !params.item_code) { return { source: "none", routing: [], candidates: [] }; } const res = await apiClient.get>( "/outsource-purchase/auto-processes", { params }, ); return res.data?.data || { source: "none", routing: [], candidates: [] }; } /** * 공정 자재투입(material_input) 자동 채움 — 사급자재 체크 시 호출 * 응답: 외주발주 자재 입력 형식 배열 [{item_code, item_name, qty, unit}] */ export async function getProcessMaterials( args: { workOrderId?: string; routingDetailId: string }, ): Promise> { if (!args.routingDetailId) return []; const params: Record = { routing_detail_id: args.routingDetailId }; if (args.workOrderId) params.work_order_id = args.workOrderId; const res = await apiClient.get>>( "/outsource-purchase/process-materials", { params }, ); return res.data?.data || []; } /** * 외주발주 가능 작업지시 목록 (TASK:ERP-019 재구현) * "이전 공정 완료 + 다음 공정 외주/선택가능" 작업지시만 반환 */ export async function listOutsourceableWorkOrders( params: { keyword?: string; page?: number; size?: number } = {}, ): Promise { const res = await apiClient.get>( "/outsource-purchase/outsourceable-work-orders", { params }, ); return res.data?.data || { rows: [], total: 0, page: 1, size: 50 }; } /** 사급자재 출고요청 (선택된 자재 ID 배열) */ export async function requestMaterialRelease( payload: ReleaseRequestPayload, ): Promise { const res = await apiClient.post>( "/outsource-purchase/release-request", payload, ); if (!res.data?.success) throw new Error(res.data?.message || "사급자재 출고요청 실패"); return res.data.data!; } // ───────────────────────────────────────────────────────────────────────────── // 보조 마스터 조회 (공통 table-management API 활용) // ───────────────────────────────────────────────────────────────────────────── /** 외주업체 마스터 조회 (subcontractor_mng) — 기존 외주사 활용 */ export async function listSubcontractors(): Promise { const res = await apiClient.post(`/table-management/tables/subcontractor_mng/data`, { page: 1, size: 0, autoFilter: true, }); return res.data?.data?.data || res.data?.data?.rows || []; } /** 수주 목록 조회 (sales_order_mng) — 등록 모달 좌측 소스 */ export async function listSalesOrdersForOPO(keyword?: string): Promise { const filters: any[] = []; if (keyword) { filters.push({ columnName: "order_no", operator: "contains", value: keyword }); } const res = await apiClient.post(`/table-management/tables/sales_order_mng/data`, { page: 1, size: 0, dataFilter: filters.length ? { enabled: true, filters } : undefined, autoFilter: true, }); return res.data?.data?.data || res.data?.data?.rows || []; } /** 작업지시 목록 조회 (work_instruction) — 등록 모달 좌측 소스 */ export async function listWorkInstructionsForOPO(keyword?: string): Promise { const filters: any[] = []; if (keyword) { filters.push({ columnName: "work_instruction_no", operator: "contains", value: keyword }); } const res = await apiClient.post(`/table-management/tables/work_instruction/data`, { page: 1, size: 0, dataFilter: filters.length ? { enabled: true, filters } : undefined, autoFilter: true, }); return res.data?.data?.data || res.data?.data?.rows || []; } /** 품목 마스터 조회 (item_info) — 등록 모달 좌측 소스 */ export async function listItemsForOPO(keyword?: string): Promise { const filters: any[] = []; if (keyword) { filters.push({ columnName: "item_code", operator: "contains", value: keyword }); } const res = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: 0, dataFilter: filters.length ? { enabled: true, filters } : undefined, autoFilter: true, }); return res.data?.data?.data || res.data?.data?.rows || []; }