import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; /** * 수주 번호 생성 함수 * 형식: ORD + YYMMDD + 4자리 시퀀스 * 예: ORD250114001 */ async function generateOrderNumber(companyCode: string): Promise { const pool = getPool(); const today = new Date(); const year = today.getFullYear().toString().slice(2); // 25 const month = String(today.getMonth() + 1).padStart(2, "0"); // 01 const day = String(today.getDate()).padStart(2, "0"); // 14 const dateStr = `${year}${month}${day}`; // 250114 // 당일 수주 카운트 조회 const countQuery = ` SELECT COUNT(*) as count FROM order_mng_master WHERE objid LIKE $1 AND writer LIKE $2 `; const pattern = `ORD${dateStr}%`; const result = await pool.query(countQuery, [pattern, `%${companyCode}%`]); const count = parseInt(result.rows[0]?.count || "0"); const seq = count + 1; return `ORD${dateStr}${String(seq).padStart(4, "0")}`; // ORD250114001 } /** * 수주 등록 API * POST /api/orders */ export async function createOrder(req: AuthenticatedRequest, res: Response) { const pool = getPool(); try { const { inputMode, // 입력 방식 customerCode, // 거래처 코드 deliveryDate, // 납품일 items, // 품목 목록 memo, // 메모 } = req.body; // 멀티테넌시 const companyCode = req.user!.companyCode; const userId = req.user!.userId; // 유효성 검사 if (!customerCode) { return res.status(400).json({ success: false, message: "거래처 코드는 필수입니다", }); } if (!items || items.length === 0) { return res.status(400).json({ success: false, message: "품목은 최소 1개 이상 필요합니다", }); } // 수주 번호 생성 const orderNo = await generateOrderNumber(companyCode); // 전체 금액 계산 const totalAmount = items.reduce( (sum: number, item: any) => sum + (item.amount || 0), 0 ); // 수주 마스터 생성 const masterQuery = ` INSERT INTO order_mng_master ( objid, partner_objid, final_delivery_date, reason, status, reg_date, writer ) VALUES ($1, $2, $3, $4, $5, NOW(), $6) RETURNING * `; const masterResult = await pool.query(masterQuery, [ orderNo, customerCode, deliveryDate || null, memo || null, "진행중", `${userId}|${companyCode}`, ]); const masterObjid = masterResult.rows[0].objid; // 수주 상세 (품목) 생성 for (let i = 0; i < items.length; i++) { const item = items[i]; const subObjid = `${orderNo}_${i + 1}`; const subQuery = ` INSERT INTO order_mng_sub ( objid, order_mng_master_objid, part_objid, partner_objid, partner_price, partner_qty, delivery_date, status, regdate, writer ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), $9) `; await pool.query(subQuery, [ subObjid, masterObjid, item.item_code || item.id, // 품목 코드 customerCode, item.unit_price || 0, item.quantity || 0, item.delivery_date || deliveryDate || null, "진행중", `${userId}|${companyCode}`, ]); } logger.info("수주 등록 성공", { companyCode, orderNo, masterObjid, itemCount: items.length, totalAmount, }); res.json({ success: true, data: { orderNo, masterObjid, itemCount: items.length, totalAmount, }, message: "수주가 등록되었습니다", }); } catch (error: any) { logger.error("수주 등록 오류", { error: error.message, stack: error.stack, }); res.status(500).json({ success: false, message: error.message || "수주 등록 중 오류가 발생했습니다", }); } } /** * 수주 목록 조회 API (마스터 + 품목 JOIN) * GET /api/orders */ export async function getOrders(req: AuthenticatedRequest, res: Response) { const pool = getPool(); try { const { page = "1", limit = "20", searchText = "" } = req.query; const companyCode = req.user!.companyCode; const offset = (parseInt(page as string) - 1) * parseInt(limit as string); // WHERE 조건 const whereConditions: string[] = []; const params: any[] = []; let paramIndex = 1; // 멀티테넌시 (writer 필드에 company_code 포함) if (companyCode !== "*") { whereConditions.push(`m.writer LIKE $${paramIndex}`); params.push(`%${companyCode}%`); paramIndex++; } // 검색 if (searchText) { whereConditions.push(`m.objid LIKE $${paramIndex}`); params.push(`%${searchText}%`); paramIndex++; } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; // 카운트 쿼리 (고유한 수주 개수) const countQuery = ` SELECT COUNT(DISTINCT m.objid) as count FROM order_mng_master m ${whereClause} `; const countResult = await pool.query(countQuery, params); const total = parseInt(countResult.rows[0]?.count || "0"); // 데이터 쿼리 (마스터 + 품목 JOIN) const dataQuery = ` SELECT m.objid as order_no, m.partner_objid, m.final_delivery_date, m.reason, m.status, m.reg_date, m.writer, COALESCE( json_agg( CASE WHEN s.objid IS NOT NULL THEN json_build_object( 'sub_objid', s.objid, 'part_objid', s.part_objid, 'partner_price', s.partner_price, 'partner_qty', s.partner_qty, 'delivery_date', s.delivery_date, 'status', s.status, 'regdate', s.regdate ) END ORDER BY s.regdate ) FILTER (WHERE s.objid IS NOT NULL), '[]'::json ) as items FROM order_mng_master m LEFT JOIN order_mng_sub s ON m.objid = s.order_mng_master_objid ${whereClause} GROUP BY m.objid, m.partner_objid, m.final_delivery_date, m.reason, m.status, m.reg_date, m.writer ORDER BY m.reg_date DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; params.push(parseInt(limit as string)); params.push(offset); const dataResult = await pool.query(dataQuery, params); logger.info("수주 목록 조회 성공", { companyCode, total, page: parseInt(page as string), itemCount: dataResult.rows.length, }); res.json({ success: true, data: dataResult.rows, pagination: { total, page: parseInt(page as string), limit: parseInt(limit as string), }, }); } catch (error: any) { logger.error("수주 목록 조회 오류", { error: error.message }); res.status(500).json({ success: false, message: error.message, }); } }