Files
vexplor/backend-node/src/controllers/packagingController.ts
kjs e2f18b19bc Implement outbound management features with new routes and controller
- Added outbound management routes for listing, creating, updating, and deleting outbound records.
- Introduced a new outbound controller to handle business logic for outbound operations, including inventory updates and source data retrieval.
- Enhanced the application by integrating outbound management functionalities into the existing logistics module.
- Improved user experience with responsive design and real-time data handling for outbound operations.
2026-03-25 10:48:47 +09:00

588 lines
20 KiB
TypeScript

import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
import { getPool } from "../database/db";
// ──────────────────────────────────────────────
// 포장단위 (pkg_unit) CRUD
// ──────────────────────────────────────────────
export async function getPkgUnits(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const pool = getPool();
let sql: string;
let params: any[];
if (companyCode === "*") {
sql = `SELECT * FROM pkg_unit ORDER BY company_code, created_date DESC`;
params = [];
} else {
sql = `SELECT * FROM pkg_unit WHERE company_code = $1 ORDER BY created_date DESC`;
params = [companyCode];
}
const result = await pool.query(sql, params);
logger.info("포장단위 목록 조회", { companyCode, count: result.rowCount });
res.json({ success: true, data: result.rows });
} catch (error: any) {
logger.error("포장단위 목록 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function createPkgUnit(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const pool = getPool();
const {
pkg_code, pkg_name, pkg_type, status,
width_mm, length_mm, height_mm,
self_weight_kg, max_load_kg, volume_l, remarks,
} = req.body;
if (!pkg_code || !pkg_name) {
res.status(400).json({ success: false, message: "포장코드와 포장명은 필수입니다." });
return;
}
const dup = await pool.query(
`SELECT id FROM pkg_unit WHERE pkg_code = $1 AND company_code = $2`,
[pkg_code, companyCode]
);
if (dup.rowCount && dup.rowCount > 0) {
res.status(409).json({ success: false, message: "이미 존재하는 포장코드입니다." });
return;
}
const result = await pool.query(
`INSERT INTO pkg_unit
(company_code, pkg_code, pkg_name, pkg_type, status,
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, volume_l, remarks, writer)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
RETURNING *`,
[companyCode, pkg_code, pkg_name, pkg_type, status || "ACTIVE",
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, volume_l, remarks,
req.user!.userId]
);
logger.info("포장단위 등록", { companyCode, pkg_code });
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
logger.error("포장단위 등록 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function updatePkgUnit(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
const pool = getPool();
const {
pkg_name, pkg_type, status,
width_mm, length_mm, height_mm,
self_weight_kg, max_load_kg, volume_l, remarks,
} = req.body;
const result = await pool.query(
`UPDATE pkg_unit SET
pkg_name=$1, pkg_type=$2, status=$3,
width_mm=$4, length_mm=$5, height_mm=$6,
self_weight_kg=$7, max_load_kg=$8, volume_l=$9, remarks=$10,
updated_date=NOW(), writer=$11
WHERE id=$12 AND company_code=$13
RETURNING *`,
[pkg_name, pkg_type, status,
width_mm, length_mm, height_mm,
self_weight_kg, max_load_kg, volume_l, remarks,
req.user!.userId, id, companyCode]
);
if (result.rowCount === 0) {
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
return;
}
logger.info("포장단위 수정", { companyCode, id });
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
logger.error("포장단위 수정 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function deletePkgUnit(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
await client.query("BEGIN");
await client.query(
`DELETE FROM pkg_unit_item WHERE pkg_code = (SELECT pkg_code FROM pkg_unit WHERE id=$1 AND company_code=$2) AND company_code=$2`,
[id, companyCode]
);
const result = await client.query(
`DELETE FROM pkg_unit WHERE id=$1 AND company_code=$2 RETURNING id`,
[id, companyCode]
);
await client.query("COMMIT");
if (result.rowCount === 0) {
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
return;
}
logger.info("포장단위 삭제", { companyCode, id });
res.json({ success: true });
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("포장단위 삭제 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
} finally {
client.release();
}
}
// ──────────────────────────────────────────────
// 포장단위 매칭품목 (pkg_unit_item) CRUD
// ──────────────────────────────────────────────
export async function getPkgUnitItems(
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 * FROM pkg_unit_item WHERE pkg_code=$1 AND company_code=$2 ORDER BY created_date DESC`,
[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 });
}
}
export async function createPkgUnitItem(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const pool = getPool();
const { pkg_code, item_number, pkg_qty } = req.body;
if (!pkg_code || !item_number) {
res.status(400).json({ success: false, message: "포장코드와 품번은 필수입니다." });
return;
}
const result = await pool.query(
`INSERT INTO pkg_unit_item (company_code, pkg_code, item_number, pkg_qty, writer)
VALUES ($1,$2,$3,$4,$5)
RETURNING *`,
[companyCode, pkg_code, item_number, pkg_qty, req.user!.userId]
);
logger.info("매칭품목 추가", { companyCode, pkg_code, item_number });
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
logger.error("매칭품목 추가 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function deletePkgUnitItem(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
const pool = getPool();
const result = await pool.query(
`DELETE FROM pkg_unit_item WHERE id=$1 AND company_code=$2 RETURNING id`,
[id, companyCode]
);
if (result.rowCount === 0) {
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
return;
}
logger.info("매칭품목 삭제", { companyCode, id });
res.json({ success: true });
} catch (error: any) {
logger.error("매칭품목 삭제 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
// ──────────────────────────────────────────────
// 적재함 (loading_unit) CRUD
// ──────────────────────────────────────────────
export async function getLoadingUnits(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const pool = getPool();
let sql: string;
let params: any[];
if (companyCode === "*") {
sql = `SELECT * FROM loading_unit ORDER BY company_code, created_date DESC`;
params = [];
} else {
sql = `SELECT * FROM loading_unit WHERE company_code = $1 ORDER BY created_date DESC`;
params = [companyCode];
}
const result = await pool.query(sql, params);
logger.info("적재함 목록 조회", { companyCode, count: result.rowCount });
res.json({ success: true, data: result.rows });
} catch (error: any) {
logger.error("적재함 목록 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function createLoadingUnit(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const pool = getPool();
const {
loading_code, loading_name, loading_type, status,
width_mm, length_mm, height_mm,
self_weight_kg, max_load_kg, max_stack, remarks,
} = req.body;
if (!loading_code || !loading_name) {
res.status(400).json({ success: false, message: "적재함코드와 적재함명은 필수입니다." });
return;
}
const dup = await pool.query(
`SELECT id FROM loading_unit WHERE loading_code=$1 AND company_code=$2`,
[loading_code, companyCode]
);
if (dup.rowCount && dup.rowCount > 0) {
res.status(409).json({ success: false, message: "이미 존재하는 적재함코드입니다." });
return;
}
const result = await pool.query(
`INSERT INTO loading_unit
(company_code, loading_code, loading_name, loading_type, status,
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, max_stack, remarks, writer)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
RETURNING *`,
[companyCode, loading_code, loading_name, loading_type, status || "ACTIVE",
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, max_stack, remarks,
req.user!.userId]
);
logger.info("적재함 등록", { companyCode, loading_code });
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
logger.error("적재함 등록 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function updateLoadingUnit(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
const pool = getPool();
const {
loading_name, loading_type, status,
width_mm, length_mm, height_mm,
self_weight_kg, max_load_kg, max_stack, remarks,
} = req.body;
const result = await pool.query(
`UPDATE loading_unit SET
loading_name=$1, loading_type=$2, status=$3,
width_mm=$4, length_mm=$5, height_mm=$6,
self_weight_kg=$7, max_load_kg=$8, max_stack=$9, remarks=$10,
updated_date=NOW(), writer=$11
WHERE id=$12 AND company_code=$13
RETURNING *`,
[loading_name, loading_type, status,
width_mm, length_mm, height_mm,
self_weight_kg, max_load_kg, max_stack, remarks,
req.user!.userId, id, companyCode]
);
if (result.rowCount === 0) {
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
return;
}
logger.info("적재함 수정", { companyCode, id });
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
logger.error("적재함 수정 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function deleteLoadingUnit(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
await client.query("BEGIN");
await client.query(
`DELETE FROM loading_unit_pkg WHERE loading_code = (SELECT loading_code FROM loading_unit WHERE id=$1 AND company_code=$2) AND company_code=$2`,
[id, companyCode]
);
const result = await client.query(
`DELETE FROM loading_unit WHERE id=$1 AND company_code=$2 RETURNING id`,
[id, companyCode]
);
await client.query("COMMIT");
if (result.rowCount === 0) {
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
return;
}
logger.info("적재함 삭제", { companyCode, id });
res.json({ success: true });
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("적재함 삭제 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
} finally {
client.release();
}
}
// ──────────────────────────────────────────────
// 적재함 포장구성 (loading_unit_pkg) CRUD
// ──────────────────────────────────────────────
export async function getLoadingUnitPkgs(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const { loadingCode } = req.params;
const pool = getPool();
const result = await pool.query(
`SELECT * FROM loading_unit_pkg WHERE loading_code=$1 AND company_code=$2 ORDER BY created_date DESC`,
[loadingCode, 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 });
}
}
export async function createLoadingUnitPkg(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const pool = getPool();
const { loading_code, pkg_code, max_load_qty, load_method } = req.body;
if (!loading_code || !pkg_code) {
res.status(400).json({ success: false, message: "적재함코드와 포장코드는 필수입니다." });
return;
}
const result = await pool.query(
`INSERT INTO loading_unit_pkg (company_code, loading_code, pkg_code, max_load_qty, load_method, writer)
VALUES ($1,$2,$3,$4,$5,$6)
RETURNING *`,
[companyCode, loading_code, pkg_code, max_load_qty, load_method, req.user!.userId]
);
logger.info("적재구성 추가", { companyCode, loading_code, pkg_code });
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
logger.error("적재구성 추가 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function deleteLoadingUnitPkg(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
const pool = getPool();
const result = await pool.query(
`DELETE FROM loading_unit_pkg WHERE id=$1 AND company_code=$2 RETURNING id`,
[id, companyCode]
);
if (result.rowCount === 0) {
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
return;
}
logger.info("적재구성 삭제", { companyCode, id });
res.json({ success: true });
} catch (error: any) {
logger.error("적재구성 삭제 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
// ──────────────────────────────────────────────
// 품목정보 연동 (division별 item_info 조회)
// ──────────────────────────────────────────────
export async function getItemsByDivision(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const { divisionLabel } = req.params;
const { keyword } = req.query;
const pool = getPool();
// division 카테고리에서 해당 라벨의 코드 찾기
const catResult = await pool.query(
`SELECT value_code FROM category_values
WHERE table_name = 'item_info' AND column_name = 'division'
AND value_label = $1 AND company_code = $2
LIMIT 1`,
[divisionLabel, companyCode]
);
if (catResult.rows.length === 0) {
res.json({ success: true, data: [] });
return;
}
const divisionCode = catResult.rows[0].value_code;
const conditions: string[] = ["company_code = $1", `$2 = ANY(string_to_array(division, ','))`];
const params: any[] = [companyCode, divisionCode];
let paramIdx = 3;
if (keyword) {
conditions.push(`(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`);
params.push(`%${keyword}%`);
paramIdx++;
}
const result = await pool.query(
`SELECT id, item_number, item_name, size, material, unit, division
FROM item_info
WHERE ${conditions.join(" AND ")}
ORDER BY item_name`,
params
);
logger.info(`품목 조회 (division=${divisionLabel})`, { companyCode, count: result.rowCount });
res.json({ success: true, data: result.rows });
} catch (error: any) {
logger.error("품목 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
// 일반 품목 조회 (포장재/적재함 제외, 매칭용)
export async function getGeneralItems(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
const pool = getPool();
// 포장재/적재함 division 코드 조회
const catResult = await pool.query(
`SELECT value_code FROM category_values
WHERE table_name = 'item_info' AND column_name = 'division'
AND value_label IN ('포장재', '적재함') AND company_code = $1`,
[companyCode]
);
const excludeCodes = catResult.rows.map((r: any) => r.value_code);
const conditions: string[] = ["company_code = $1"];
const params: any[] = [companyCode];
let paramIdx = 2;
if (excludeCodes.length > 0) {
// 다중 값(콤마 구분) 지원: 포장재/적재함 코드가 포함된 품목 제외
const excludeConditions = excludeCodes.map((_: any, i: number) => `$${paramIdx + i} = ANY(string_to_array(division, ','))`);
conditions.push(`(division IS NULL OR division = '' OR NOT (${excludeConditions.join(" OR ")}))`);
params.push(...excludeCodes);
paramIdx += excludeCodes.length;
}
if (keyword) {
conditions.push(`(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`);
params.push(`%${keyword}%`);
paramIdx++;
}
const result = await pool.query(
`SELECT id, item_number, item_name, size AS spec, material, unit, division
FROM item_info
WHERE ${conditions.join(" AND ")}
ORDER BY item_name
LIMIT 200`,
params
);
res.json({ success: true, data: result.rows });
} catch (error: any) {
logger.error("일반 품목 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}