Implement outsource purchase management functionality

- Added routes for outsource purchase management, including CRUD operations and additional features such as auto-processes and release requests.
- Created the `outsourcePurchaseController` to handle business logic for managing outsource purchase orders.
- Introduced the `outsourcePurchaseService` for service layer operations related to outsource purchases.
- Updated `app.ts` to include the new routes for outsource purchase management.

(TASK:ERP-019)
This commit is contained in:
kjs
2026-05-06 16:16:13 +09:00
parent 92f73af633
commit 4ad669361d
137 changed files with 89908 additions and 64 deletions

View File

@@ -0,0 +1,205 @@
/**
* 외주발주관리 컨트롤러 (TASK:ERP-019)
*
* 라우트:
* GET /outsource-purchase 목록
* GET /outsource-purchase/auto-processes 공정 자동표기 헬퍼
* GET /outsource-purchase/:id 상세
* POST /outsource-purchase 등록
* PUT /outsource-purchase/:id 수정
* DELETE /outsource-purchase/:id 삭제
* POST /outsource-purchase/release-request 사급자재 출고요청
*/
import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import * as svc from "../services/outsourcePurchaseService";
import { logger } from "../utils/logger";
function ok(res: Response, data: any, message = "성공") {
return res.json({ success: true, data, message });
}
function fail(res: Response, status: number, message: string, error?: any) {
if (error) logger.error(message, { error: error?.message });
return res.status(status).json({ success: false, message });
}
// ─────────────────────────────────────────────────────────────────────────────
// 목록
// ─────────────────────────────────────────────────────────────────────────────
export async function listOutsourceOrders(
req: AuthenticatedRequest,
res: Response
) {
try {
const companyCode = req.user!.companyCode;
const opts: svc.ListFilter = {
keyword: req.query.keyword as string | undefined,
status: req.query.status as string | undefined,
source_type: req.query.source_type as string | undefined,
date_from: req.query.date_from as string | undefined,
date_to: req.query.date_to as string | undefined,
page: req.query.page ? parseInt(req.query.page as string, 10) : 1,
size:
req.query.size !== undefined
? parseInt(req.query.size as string, 10)
: 50,
sort: req.query.sort as string | undefined,
};
const data = await svc.listOrders(companyCode, opts);
return ok(res, data);
} catch (e: any) {
return fail(res, 500, e?.message || "외주발주 목록 조회 실패", e);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 상세
// ─────────────────────────────────────────────────────────────────────────────
export async function getOutsourceOrderDetail(
req: AuthenticatedRequest,
res: Response
) {
try {
const companyCode = req.user!.companyCode;
const id = req.params.id;
if (!id) return fail(res, 400, "id 파라미터가 필요합니다");
const data = await svc.getOrderDetail(companyCode, id);
if (!data) return fail(res, 404, "외주발주를 찾을 수 없습니다");
return ok(res, data);
} catch (e: any) {
return fail(res, 500, e?.message || "외주발주 상세 조회 실패", e);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 등록
// ─────────────────────────────────────────────────────────────────────────────
export async function createOutsourceOrder(
req: AuthenticatedRequest,
res: Response
) {
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId || "system";
const payload = req.body as svc.OPOInput;
if (!payload || !payload.source_type) {
return fail(res, 400, "source_type은 필수입니다");
}
const data = await svc.createOrder(companyCode, userId, payload);
return ok(res, data, "외주발주 등록 완료");
} catch (e: any) {
return fail(res, 500, e?.message || "외주발주 등록 실패", e);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 수정
// ─────────────────────────────────────────────────────────────────────────────
export async function updateOutsourceOrder(
req: AuthenticatedRequest,
res: Response
) {
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId || "system";
const id = req.params.id;
if (!id) return fail(res, 400, "id 파라미터가 필요합니다");
const data = await svc.updateOrder(
companyCode,
userId,
id,
req.body as svc.OPOInput
);
return ok(res, data, "외주발주 수정 완료");
} catch (e: any) {
return fail(res, 500, e?.message || "외주발주 수정 실패", e);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 삭제
// ─────────────────────────────────────────────────────────────────────────────
export async function deleteOutsourceOrder(
req: AuthenticatedRequest,
res: Response
) {
try {
const companyCode = req.user!.companyCode;
const id = req.params.id;
if (!id) return fail(res, 400, "id 파라미터가 필요합니다");
const data = await svc.deleteOrder(companyCode, id);
return ok(res, data, "외주발주 삭제 완료");
} catch (e: any) {
return fail(res, 400, e?.message || "외주발주 삭제 실패", e);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 사급자재 출고요청
// ─────────────────────────────────────────────────────────────────────────────
export async function requestRelease(
req: AuthenticatedRequest,
res: Response
) {
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId || "system";
const payload = req.body as svc.ReleaseRequestPayload;
if (
!payload ||
!Array.isArray(payload.material_ids) ||
payload.material_ids.length === 0
) {
return fail(res, 400, "material_ids 배열이 필요합니다");
}
const data = await svc.requestRelease(companyCode, userId, payload);
return ok(res, data, "사급자재 출고요청 완료");
} catch (e: any) {
return fail(res, 400, e?.message || "사급자재 출고요청 실패", e);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 공정 자동표기 (헬퍼)
// (TASK:ERP-019 재구현 — work_order_id 우선, item_code는 폴백)
// ─────────────────────────────────────────────────────────────────────────────
export async function autoProcesses(
req: AuthenticatedRequest,
res: Response
) {
try {
const companyCode = req.user!.companyCode;
const itemCode = req.query.item_code as string | undefined;
const workOrderId = req.query.work_order_id as string | undefined;
if (!workOrderId && !itemCode) {
return fail(res, 400, "work_order_id 또는 item_code 중 하나는 필수입니다");
}
const data = await svc.autoSelectProcesses(companyCode, workOrderId, itemCode);
return ok(res, data);
} catch (e: any) {
return fail(res, 500, e?.message || "공정 자동표기 실패", e);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 외주발주 가능 작업지시 목록
// (TASK:ERP-019 재구현 — 좌측 리스트 필터)
// ─────────────────────────────────────────────────────────────────────────────
export async function listOutsourceableWorkOrders(
req: AuthenticatedRequest,
res: Response,
) {
try {
const companyCode = req.user!.companyCode;
const opts: svc.OutsourceableListFilter = {
keyword: req.query.keyword as string | undefined,
page: req.query.page ? parseInt(req.query.page as string, 10) : 1,
size: req.query.size !== undefined ? parseInt(req.query.size as string, 10) : 50,
};
const data = await svc.listOutsourceableWorkOrders(companyCode, opts);
return ok(res, data);
} catch (e: any) {
return fail(res, 500, e?.message || "외주발주 가능 작업지시 조회 실패", e);
}
}