WIP: POP + packaging 작업 중
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -227,10 +227,24 @@ export class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
// 새로운 JWT 토큰 발급 (company_code만 변경)
|
||||
// 전환 대상 회사명 조회
|
||||
let targetCompanyName: string | undefined;
|
||||
if (companyCode === "*") {
|
||||
targetCompanyName = "공통";
|
||||
} else {
|
||||
const { query: dbQuery } = await import("../database/db");
|
||||
const companyRows = await dbQuery<{ company_name: string }>(
|
||||
"SELECT company_name FROM company_mng WHERE company_code = $1",
|
||||
[companyCode.trim()]
|
||||
);
|
||||
targetCompanyName = companyRows[0]?.company_name || companyCode.trim();
|
||||
}
|
||||
|
||||
// 새로운 JWT 토큰 발급 (company_code + company_name 변경)
|
||||
const newPersonBean: PersonBean = {
|
||||
...currentUser,
|
||||
companyCode: companyCode.trim(), // 전환할 회사 코드로 변경
|
||||
companyCode: companyCode.trim(),
|
||||
companyName: targetCompanyName,
|
||||
};
|
||||
|
||||
const newToken = JwtUtils.generateToken(newPersonBean);
|
||||
@@ -355,6 +369,7 @@ export class AuthController {
|
||||
deptName: dbUserInfo.deptName || "",
|
||||
companyCode: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
|
||||
company_code: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
|
||||
companyName: userInfo.companyName || dbUserInfo.companyName || "", // JWT 토큰 우선 (회사 전환 시 갱신됨)
|
||||
userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선
|
||||
userTypeName: dbUserInfo.userTypeName || "일반사용자",
|
||||
email: dbUserInfo.email || "",
|
||||
|
||||
@@ -161,6 +161,38 @@ export async function deletePkgUnit(
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 품목별 포장단위 조회 (item_number → pkg_unit 목록)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
export async function getPkgUnitsByItem(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { itemNumber } = req.params;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT pu.id, pu.pkg_code, pu.pkg_name, pu.pkg_type, pu.status,
|
||||
pu.width_mm, pu.length_mm, pu.height_mm,
|
||||
pu.self_weight_kg, pu.max_load_kg, pu.volume_l,
|
||||
pui.pkg_qty
|
||||
FROM pkg_unit_item pui
|
||||
JOIN pkg_unit pu ON pui.pkg_code = pu.pkg_code AND pui.company_code = pu.company_code
|
||||
WHERE pui.item_number = $1 AND pui.company_code = $2 AND pu.status = 'ACTIVE'
|
||||
ORDER BY pu.pkg_name`,
|
||||
[itemNumber, companyCode]
|
||||
);
|
||||
|
||||
res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("품목별 포장단위 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 포장단위 매칭품목 (pkg_unit_item) CRUD
|
||||
// ──────────────────────────────────────────────
|
||||
@@ -405,6 +437,38 @@ export async function deleteLoadingUnit(
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 포장코드별 적재함 조회 (pkg_code → loading_unit 목록)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
export async function getLoadingUnitsByPkg(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { pkgCode } = req.params;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT lu.id, lu.loading_code, lu.loading_name, lu.loading_type, lu.status,
|
||||
lu.width_mm, lu.length_mm, lu.height_mm,
|
||||
lu.self_weight_kg, lu.max_load_kg, lu.max_stack,
|
||||
lup.max_load_qty, lup.load_method
|
||||
FROM loading_unit_pkg lup
|
||||
JOIN loading_unit lu ON lup.loading_code = lu.loading_code AND lup.company_code = lu.company_code
|
||||
WHERE lup.pkg_code = $1 AND lup.company_code = $2 AND lu.status = 'ACTIVE'
|
||||
ORDER BY lu.loading_name`,
|
||||
[pkgCode, companyCode]
|
||||
);
|
||||
|
||||
res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("포장별 적재함 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 적재함 포장구성 (loading_unit_pkg) CRUD
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import {
|
||||
getPkgUnits, createPkgUnit, updatePkgUnit, deletePkgUnit,
|
||||
getPkgUnitsByItem,
|
||||
getPkgUnitItems, createPkgUnitItem, deletePkgUnitItem,
|
||||
getLoadingUnits, createLoadingUnit, updateLoadingUnit, deleteLoadingUnit,
|
||||
getLoadingUnitsByPkg,
|
||||
getLoadingUnitPkgs, createLoadingUnitPkg, deleteLoadingUnitPkg,
|
||||
getItemsByDivision, getGeneralItems,
|
||||
} from "../controllers/packagingController";
|
||||
@@ -18,6 +20,9 @@ router.post("/pkg-units", createPkgUnit);
|
||||
router.put("/pkg-units/:id", updatePkgUnit);
|
||||
router.delete("/pkg-units/:id", deletePkgUnit);
|
||||
|
||||
// 품목별 포장단위 조회
|
||||
router.get("/pkg-units-by-item/:itemNumber", getPkgUnitsByItem);
|
||||
|
||||
// 포장단위 매칭품목
|
||||
router.get("/pkg-unit-items/:pkgCode", getPkgUnitItems);
|
||||
router.post("/pkg-unit-items", createPkgUnitItem);
|
||||
@@ -29,6 +34,9 @@ router.post("/loading-units", createLoadingUnit);
|
||||
router.put("/loading-units/:id", updateLoadingUnit);
|
||||
router.delete("/loading-units/:id", deleteLoadingUnit);
|
||||
|
||||
// 포장코드별 적재함 조회
|
||||
router.get("/loading-units-by-pkg/:pkgCode", getLoadingUnitsByPkg);
|
||||
|
||||
// 적재함 포장구성
|
||||
router.get("/loading-unit-pkgs/:loadingCode", getLoadingUnitPkgs);
|
||||
router.post("/loading-unit-pkgs", createLoadingUnitPkg);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
saveMaterialInput,
|
||||
getMaterialInputs,
|
||||
getChecklistItems,
|
||||
getProcessList,
|
||||
} from "../controllers/popProductionController";
|
||||
|
||||
const router = Router();
|
||||
@@ -51,5 +52,6 @@ router.get("/bom-materials/:processId", getBomMaterials);
|
||||
router.post("/material-input", saveMaterialInput);
|
||||
router.get("/material-inputs/:processId", getMaterialInputs);
|
||||
router.get("/checklist-items/:processId", getChecklistItems);
|
||||
router.get("/processes", getProcessList);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -28,6 +28,9 @@ router.get("/source/shipments", receivingController.getShipments);
|
||||
// 소스 데이터: 품목 (기타입고)
|
||||
router.get("/source/items", receivingController.getItems);
|
||||
|
||||
// 소스 데이터: 생산실적 (생산입고)
|
||||
router.get("/source/production-results", receivingController.getProductionResults);
|
||||
|
||||
// 입고 등록
|
||||
router.post("/", receivingController.create);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user