진행 중이던 POP 관련 변경사항을 한 번에 묶어 커밋.
- backend
- popProductionController: 생산공정 처리/접수 로직 대폭 갱신 (+663)
- receivingController, popInventoryRoutes, adminService 보강
- popInOutDetailController / popInOutHistoryController 신규
- frontend (POP)
- 생산 화면 (DefectTypeModal / ProcessWork / WorkOrderList / main page)
COMPANY_7/8/9/10/16/29/30 동기화
- 입출고 이력·디테일 화면 신규 (inventory/page, inventory/inout-manage,
InOutDetailModal) 7개사
- COMPANY_7 입고 화면 (InboundCartPage / ProductionInbound /
inbound/production/page) 보강
- COMPANY_7 재고조정 화면 (inventory/adjust) UI 골격 신규
- frontend lib
- popInOutDetail / popInOutHistory API 클라이언트 신규
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
4.7 KiB
TypeScript
160 lines
4.7 KiB
TypeScript
/**
|
|
* POP 입출고관리 디테일 단건 조회 컨트롤러
|
|
*
|
|
* inout-history 카드 클릭 시 모달용 단건 조회 API 3종 (read-only).
|
|
* 라우트는 popInventoryRoutes.ts 에서 source 별로 분리:
|
|
* GET /api/pop/inventory/inout-detail/inbound/:id (+ ?detail_id=...)
|
|
* GET /api/pop/inventory/inout-detail/outbound/:id
|
|
* GET /api/pop/inventory/inout-detail/history/:id
|
|
*/
|
|
|
|
import type { Response } from "express";
|
|
import { getPool } from "../database/db";
|
|
import type { AuthenticatedRequest } from "../types/auth";
|
|
import { logger } from "../utils/logger";
|
|
|
|
// ===== inbound: inbound_mng (헤더) + inbound_detail (1행, optional) =====
|
|
|
|
export async function getInboundDetail(
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { id } = req.params;
|
|
const { detail_id } = req.query as Record<string, string | undefined>;
|
|
|
|
const pool = getPool();
|
|
|
|
const headerSql = `
|
|
SELECT
|
|
im.*,
|
|
wh.warehouse_name,
|
|
u.user_name AS writer_name
|
|
FROM inbound_mng im
|
|
LEFT JOIN warehouse_info wh
|
|
ON wh.warehouse_code = im.warehouse_code
|
|
AND wh.company_code = im.company_code
|
|
LEFT JOIN user_info u
|
|
ON u.user_id = im.writer
|
|
AND u.company_code = im.company_code
|
|
WHERE im.id = $1
|
|
AND ($2::text = '*' OR im.company_code = $2)
|
|
`;
|
|
const headerResult = await pool.query(headerSql, [id, companyCode]);
|
|
if (headerResult.rowCount === 0) {
|
|
return res
|
|
.status(404)
|
|
.json({ success: false, message: "입고 데이터를 찾을 수 없습니다." });
|
|
}
|
|
const header = headerResult.rows[0];
|
|
|
|
let detail: Record<string, unknown> | null = null;
|
|
if (detail_id) {
|
|
const detailResult = await pool.query(
|
|
`SELECT * FROM inbound_detail
|
|
WHERE id = $1
|
|
AND ($2::text = '*' OR company_code = $2)`,
|
|
[detail_id, companyCode],
|
|
);
|
|
detail = detailResult.rows[0] ?? null;
|
|
}
|
|
|
|
return res.json({ success: true, data: { header, detail } });
|
|
} catch (error: unknown) {
|
|
const msg = error instanceof Error ? error.message : "조회 실패";
|
|
logger.error("입고 디테일 단건 조회 실패", { error: msg });
|
|
return res.status(500).json({ success: false, message: msg });
|
|
}
|
|
}
|
|
|
|
// ===== outbound: outbound_mng (단건) =====
|
|
|
|
export async function getOutboundDetail(
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { id } = req.params;
|
|
|
|
const pool = getPool();
|
|
|
|
const sql = `
|
|
SELECT
|
|
om.*,
|
|
wh.warehouse_name,
|
|
u.user_name AS writer_name
|
|
FROM outbound_mng om
|
|
LEFT JOIN warehouse_info wh
|
|
ON wh.warehouse_code = om.warehouse_code
|
|
AND wh.company_code = om.company_code
|
|
LEFT JOIN user_info u
|
|
ON u.user_id = om.writer
|
|
AND u.company_code = om.company_code
|
|
WHERE om.id = $1
|
|
AND ($2::text = '*' OR om.company_code = $2)
|
|
`;
|
|
const result = await pool.query(sql, [id, companyCode]);
|
|
if (result.rowCount === 0) {
|
|
return res
|
|
.status(404)
|
|
.json({ success: false, message: "출고 데이터를 찾을 수 없습니다." });
|
|
}
|
|
|
|
return res.json({ success: true, data: { row: result.rows[0] } });
|
|
} catch (error: unknown) {
|
|
const msg = error instanceof Error ? error.message : "조회 실패";
|
|
logger.error("출고 디테일 단건 조회 실패", { error: msg });
|
|
return res.status(500).json({ success: false, message: msg });
|
|
}
|
|
}
|
|
|
|
// ===== history: inventory_history (단건) + warehouse + item_info JOIN =====
|
|
|
|
export async function getHistoryDetail(
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { id } = req.params;
|
|
|
|
const pool = getPool();
|
|
|
|
const sql = `
|
|
SELECT
|
|
ih.*,
|
|
wh.warehouse_name,
|
|
ii.item_name AS joined_item_name,
|
|
ii.unit AS joined_unit
|
|
FROM inventory_history ih
|
|
LEFT JOIN warehouse_info wh
|
|
ON wh.warehouse_code = ih.warehouse_code
|
|
AND wh.company_code = ih.company_code
|
|
LEFT JOIN (
|
|
SELECT DISTINCT ON (item_number, company_code)
|
|
item_number, item_name, unit, company_code
|
|
FROM item_info
|
|
ORDER BY item_number, company_code, created_date DESC
|
|
) ii ON ih.item_code = ii.item_number
|
|
AND ih.company_code = ii.company_code
|
|
WHERE ih.id = $1
|
|
AND ($2::text = '*' OR ih.company_code = $2)
|
|
`;
|
|
const result = await pool.query(sql, [id, companyCode]);
|
|
if (result.rowCount === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "재고이력 데이터를 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
return res.json({ success: true, data: { row: result.rows[0] } });
|
|
} catch (error: unknown) {
|
|
const msg = error instanceof Error ? error.message : "조회 실패";
|
|
logger.error("재고이력 디테일 단건 조회 실패", { error: msg });
|
|
return res.status(500).json({ success: false, message: msg });
|
|
}
|
|
}
|