239 lines
5.9 KiB
TypeScript
239 lines
5.9 KiB
TypeScript
|
|
import { Request, Response } from "express";
|
||
|
|
import { getPool } from "../database/db";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 수주 번호 생성 함수
|
||
|
|
* 형식: ORD + YYMMDD + 4자리 시퀀스
|
||
|
|
* 예: ORD250114001
|
||
|
|
*/
|
||
|
|
async function generateOrderNumber(companyCode: string): Promise<string> {
|
||
|
|
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: Request, 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
|
||
|
|
* GET /api/orders
|
||
|
|
*/
|
||
|
|
export async function getOrders(req: Request, 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(`writer LIKE $${paramIndex}`);
|
||
|
|
params.push(`%${companyCode}%`);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 검색
|
||
|
|
if (searchText) {
|
||
|
|
whereConditions.push(`objid LIKE $${paramIndex}`);
|
||
|
|
params.push(`%${searchText}%`);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause =
|
||
|
|
whereConditions.length > 0
|
||
|
|
? `WHERE ${whereConditions.join(" AND ")}`
|
||
|
|
: "";
|
||
|
|
|
||
|
|
// 카운트 쿼리
|
||
|
|
const countQuery = `SELECT COUNT(*) as count FROM order_mng_master ${whereClause}`;
|
||
|
|
const countResult = await pool.query(countQuery, params);
|
||
|
|
const total = parseInt(countResult.rows[0]?.count || "0");
|
||
|
|
|
||
|
|
// 데이터 쿼리
|
||
|
|
const dataQuery = `
|
||
|
|
SELECT * FROM order_mng_master
|
||
|
|
${whereClause}
|
||
|
|
ORDER BY reg_date DESC
|
||
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||
|
|
`;
|
||
|
|
|
||
|
|
params.push(parseInt(limit as string));
|
||
|
|
params.push(offset);
|
||
|
|
|
||
|
|
const dataResult = await pool.query(dataQuery, params);
|
||
|
|
|
||
|
|
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,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|