WIP: POP + packaging 작업 중

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kmh
2026-04-22 14:55:28 +09:00
parent f117534d6c
commit 4ed4d5f66e
30 changed files with 1705 additions and 907 deletions

View File

@@ -155,8 +155,13 @@ export async function create(req: AuthenticatedRequest, res: Response) {
.json({ success: false, message: "입고 품목이 없습니다." });
}
// 첫 번째 아이템에서 inbound_type 추출 (헤더용)
const inboundType = items[0].inbound_type || null;
// 헤더용 inbound_type: 단일이면 그 값, 혼합이면 "혼합입고"
const uniqueInboundTypes = [...new Set(items.map((i: any) => i.inbound_type).filter(Boolean))];
const inboundType = uniqueInboundTypes.length === 1
? uniqueInboundTypes[0]
: uniqueInboundTypes.length > 1
? "혼합입고"
: (items[0].inbound_type || null);
const inboundNumber = inbound_number || items[0].inbound_number;
await client.query("BEGIN");
@@ -331,12 +336,11 @@ export async function create(req: AuthenticatedRequest, res: Response) {
);
}
// 2c. 구매입고인 경우 발주의 received_qty 업데이트 — 기존 로직 유지
if (
item.inbound_type === "구매입고" &&
item.source_id &&
item.source_table === "purchase_order_mng"
) {
// 2c. source_table 기준 소스 데이터 업데이트 (이중 입고 방지)
const srcTable = item.source_table;
const srcId = item.source_id;
if (srcTable === "purchase_order_mng" && srcId) {
await client.query(
`UPDATE purchase_order_mng
SET received_qty = CAST(
@@ -354,17 +358,9 @@ export async function create(req: AuthenticatedRequest, res: Response) {
END,
updated_date = NOW()
WHERE id = $2 AND company_code = $3`,
[item.inbound_qty || 0, item.source_id, companyCode],
[item.inbound_qty || 0, srcId, companyCode],
);
}
// 구매입고인 경우 purchase_detail 품목별 입고수량 업데이트
if (
item.inbound_type === "구매입고" &&
item.source_id &&
item.source_table === "purchase_detail"
) {
// 1. 해당 purchase_detail의 received_qty 누적 업데이트
} else if (srcTable === "purchase_detail" && srcId) {
await client.query(
`UPDATE purchase_detail SET
received_qty = CAST(
@@ -377,17 +373,15 @@ export async function create(req: AuthenticatedRequest, res: Response) {
),
updated_date = NOW()
WHERE id = $2 AND company_code = $3`,
[item.inbound_qty || 0, item.source_id, companyCode],
[item.inbound_qty || 0, srcId, companyCode],
);
// 2. 발주 헤더 상태 업데이트
const detailInfo = await client.query(
`SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`,
[item.source_id, companyCode],
[srcId, companyCode],
);
if (detailInfo.rows.length > 0) {
const purchaseNo = detailInfo.rows[0].purchase_no;
// 잔량 있는 디테일이 있는지 확인
const unreceived = await client.query(
`SELECT id FROM purchase_detail
WHERE purchase_no = $1 AND company_code = $2
@@ -419,6 +413,28 @@ export async function create(req: AuthenticatedRequest, res: Response) {
[newStatus, purchaseNo, companyCode],
);
}
} else if (srcTable === "work_order_process" && srcId) {
// 생산입고: target_warehouse_id 세팅 (이중 입고 방지)
const whCode = warehouse_code || item.warehouse_code || null;
const locCode = location_code || item.location_code || null;
await client.query(
`UPDATE work_order_process
SET target_warehouse_id = $3,
target_location_code = $4,
writer = $5,
updated_date = NOW()
WHERE id = $1 AND company_code = $2
AND target_warehouse_id IS NULL`,
[srcId, companyCode, whCode, locCode || null, userId],
);
} else if (srcTable && srcId) {
// 미처리 소스 테이블 — 추후 업데이트 로직 추가 필요
logger.warn("입고 소스 업데이트 미처리", {
source_table: srcTable,
source_id: srcId,
inbound_type: item.inbound_type,
item_number: item.item_number,
});
}
}
@@ -1044,6 +1060,8 @@ export async function getShipments(req: AuthenticatedRequest, res: Response) {
si.instruction_no,
si.instruction_date,
si.partner_id,
si.partner_id AS partner_code,
COALESCE(cm.customer_name, si.partner_id) AS partner_name,
si.status AS instruction_status,
sid.item_code,
sid.item_name,
@@ -1056,6 +1074,9 @@ export async function getShipments(req: AuthenticatedRequest, res: Response) {
JOIN shipment_instruction_detail sid
ON si.id = sid.instruction_id
AND si.company_code = sid.company_code
LEFT JOIN customer_mng cm
ON cm.customer_code = si.partner_id
AND cm.company_code = si.company_code
WHERE ${whereClause}
ORDER BY si.instruction_date DESC, si.instruction_no
LIMIT ${limit} OFFSET ${offset}`,
@@ -1126,6 +1147,88 @@ export async function getItems(req: AuthenticatedRequest, res: Response) {
}
}
// 생산입고용: 실적이 등록된 작업지시 공정 데이터 조회 (미입고분)
export async function getProductionResults(
req: AuthenticatedRequest,
res: Response,
) {
try {
const companyCode = req.user!.companyCode;
const { processCode, keyword, pageSize } = req.query;
if (!processCode) {
return res
.status(400)
.json({ success: false, message: "processCode 필수" });
}
const limit = Math.min(500, Math.max(1, Number(pageSize) || 50));
const params: any[] = [companyCode, processCode];
let paramIdx = 3;
let keywordCondition = "";
if (keyword) {
keywordCondition = `AND (wi.work_instruction_no ILIKE $${paramIdx} OR COALESCE(ii.item_name, '') ILIKE $${paramIdx} OR COALESCE(ii.item_number, '') ILIKE $${paramIdx})`;
params.push(`%${keyword}%`);
paramIdx++;
}
const pool = getPool();
const dataResult = await pool.query(
`SELECT
wop.id,
wop.wo_id,
wi.work_instruction_no,
wi.start_date AS order_date,
wop.process_code,
wop.process_name,
wop.seq_no,
COALESCE(ii.item_number, wi.item_id) AS item_code,
COALESCE(ii.item_name, ii.item_number, wi.item_id) AS item_name,
COALESCE(ii.size, '') AS spec,
COALESCE(ii.material, '') AS material,
COALESCE(CAST(NULLIF(wop.good_qty, '') AS numeric), 0)
+ COALESCE(CAST(NULLIF(wop.concession_qty, '') AS numeric), 0) AS order_qty,
0 AS received_qty,
COALESCE(CAST(NULLIF(wop.good_qty, '') AS numeric), 0)
+ COALESCE(CAST(NULLIF(wop.concession_qty, '') AS numeric), 0) AS remain_qty,
'work_order_process' AS source_table,
wop.result_status,
COALESCE(ii.image, NULL) AS image,
CASE WHEN EXISTS (
SELECT 1 FROM item_inspection_info iii
WHERE iii.company_code = wop.company_code
AND COALESCE(iii.is_active, 'Y') = 'Y'
AND iii.item_code = COALESCE(ii.item_number, wi.item_id)
) THEN 'self' ELSE NULL END AS inspection_type
FROM work_order_process wop
JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
LEFT JOIN (
SELECT DISTINCT ON (id, company_code)
id, item_number, item_name, size, material, image, company_code
FROM item_info
ORDER BY id, company_code, created_date DESC
) ii ON wi.item_id = ii.id AND wi.company_code = ii.company_code
WHERE wop.company_code = $1
AND wop.process_code = $2
AND wop.parent_process_id IS NULL
AND (wop.is_rework IS NULL OR wop.is_rework != 'Y')
AND COALESCE(CAST(NULLIF(wop.good_qty, '') AS numeric), 0) > 0
AND wop.target_warehouse_id IS NULL
${keywordCondition}
ORDER BY wi.work_instruction_no, CAST(wop.seq_no AS int)
LIMIT ${limit}`,
params,
);
return res.json({ success: true, data: dataResult.rows });
} catch (error: any) {
logger.error("생산입고 소스 데이터 조회 실패", { error: error.message });
return res.status(500).json({ success: false, message: error.message });
}
}
// 입고번호 자동생성
export async function generateNumber(req: AuthenticatedRequest, res: Response) {
try {