[agent-pipeline] pipe-20260311130636-hzyn round-2
This commit is contained in:
parent
4f603bd41e
commit
1b2d42ffc5
|
|
@ -0,0 +1,995 @@
|
|||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { query, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// ============================================================
|
||||
// 포장단위(pkg_unit) CRUD
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 포장단위 목록 조회
|
||||
* GET /api/packaging/pkg-units
|
||||
*/
|
||||
export const getPkgUnits = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { search, pkg_type, status } = req.query;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 멀티테넌시 필터
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 전체 조회
|
||||
} else {
|
||||
conditions.push(`pu.company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (search && typeof search === "string" && search.trim()) {
|
||||
conditions.push(
|
||||
`(pu.pkg_code ILIKE $${paramIndex} OR pu.pkg_name ILIKE $${paramIndex})`
|
||||
);
|
||||
params.push(`%${search.trim()}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (pkg_type && typeof pkg_type === "string" && pkg_type.trim()) {
|
||||
conditions.push(`pu.pkg_type = $${paramIndex}`);
|
||||
params.push(pkg_type.trim());
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (status && typeof status === "string" && status.trim()) {
|
||||
conditions.push(`pu.status = $${paramIndex}`);
|
||||
params.push(status.trim());
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
pu.*,
|
||||
(SELECT COUNT(*) FROM pkg_unit_item pui
|
||||
WHERE pui.pkg_code = pu.pkg_code AND pui.company_code = pu.company_code
|
||||
) AS item_count
|
||||
FROM pkg_unit pu
|
||||
${whereClause}
|
||||
ORDER BY pu.created_date DESC
|
||||
`;
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
logger.info("포장단위 목록 조회 성공", {
|
||||
companyCode,
|
||||
count: rows.length,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (error: any) {
|
||||
logger.error("포장단위 목록 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 포장단위 상세 조회
|
||||
* GET /api/packaging/pkg-units/:id
|
||||
*/
|
||||
export const getPkgUnitById = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
let sql: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
sql = `SELECT * FROM pkg_unit WHERE id = $1`;
|
||||
params = [id];
|
||||
} else {
|
||||
sql = `SELECT * FROM pkg_unit WHERE id = $1 AND company_code = $2`;
|
||||
params = [id, companyCode];
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "포장단위를 찾을 수 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("포장단위 상세 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 포장단위 등록
|
||||
* POST /api/packaging/pkg-units
|
||||
*/
|
||||
export const createPkgUnit = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
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: "포장코드(pkg_code)와 포장명(pkg_name)은 필수입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
const dupCheck = await query(
|
||||
`SELECT id FROM pkg_unit WHERE pkg_code = $1 AND company_code = $2`,
|
||||
[pkg_code, companyCode]
|
||||
);
|
||||
if (dupCheck.length > 0) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
message: `포장코드 '${pkg_code}'가 이미 존재합니다.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
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 *
|
||||
`;
|
||||
|
||||
const rows = await query(sql, [
|
||||
companyCode,
|
||||
pkg_code,
|
||||
pkg_name,
|
||||
pkg_type || null,
|
||||
status || "ACTIVE",
|
||||
width_mm || null,
|
||||
length_mm || null,
|
||||
height_mm || null,
|
||||
self_weight_kg || null,
|
||||
max_load_kg || null,
|
||||
volume_l || null,
|
||||
remarks || null,
|
||||
userId,
|
||||
]);
|
||||
|
||||
logger.info("포장단위 등록 성공", { companyCode, pkg_code });
|
||||
res.status(201).json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("포장단위 등록 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 포장단위 수정
|
||||
* PUT /api/packaging/pkg-units/:id
|
||||
*/
|
||||
export const updatePkgUnit = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const {
|
||||
pkg_name,
|
||||
pkg_type,
|
||||
status,
|
||||
width_mm,
|
||||
length_mm,
|
||||
height_mm,
|
||||
self_weight_kg,
|
||||
max_load_kg,
|
||||
volume_l,
|
||||
remarks,
|
||||
} = req.body;
|
||||
|
||||
const setClauses: string[] = ["updated_date = NOW()", "writer = $1"];
|
||||
const params: any[] = [userId];
|
||||
let paramIndex = 2;
|
||||
|
||||
const fieldMap: Record<string, any> = {
|
||||
pkg_name,
|
||||
pkg_type,
|
||||
status,
|
||||
width_mm,
|
||||
length_mm,
|
||||
height_mm,
|
||||
self_weight_kg,
|
||||
max_load_kg,
|
||||
volume_l,
|
||||
remarks,
|
||||
};
|
||||
|
||||
for (const [col, val] of Object.entries(fieldMap)) {
|
||||
if (val !== undefined) {
|
||||
setClauses.push(`${col} = $${paramIndex}`);
|
||||
params.push(val);
|
||||
paramIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// WHERE: id + company_code
|
||||
params.push(id);
|
||||
const idIdx = paramIndex;
|
||||
paramIndex++;
|
||||
|
||||
let whereClause: string;
|
||||
if (companyCode === "*") {
|
||||
whereClause = `WHERE id = $${idIdx}`;
|
||||
} else {
|
||||
params.push(companyCode);
|
||||
whereClause = `WHERE id = $${idIdx} AND company_code = $${paramIndex}`;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
UPDATE pkg_unit
|
||||
SET ${setClauses.join(", ")}
|
||||
${whereClause}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "포장단위를 찾을 수 없거나 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("포장단위 수정 성공", { companyCode, id });
|
||||
res.json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("포장단위 수정 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 포장단위 삭제
|
||||
* DELETE /api/packaging/pkg-units/:id
|
||||
*/
|
||||
export const deletePkgUnit = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
// 트랜잭션으로 관련 데이터 함께 삭제
|
||||
const result = await transaction(async (client) => {
|
||||
// 삭제 대상의 pkg_code 조회 (관계 데이터 삭제용)
|
||||
let findSql: string;
|
||||
let findParams: any[];
|
||||
if (companyCode === "*") {
|
||||
findSql = `SELECT pkg_code, company_code FROM pkg_unit WHERE id = $1`;
|
||||
findParams = [id];
|
||||
} else {
|
||||
findSql = `SELECT pkg_code, company_code FROM pkg_unit WHERE id = $1 AND company_code = $2`;
|
||||
findParams = [id, companyCode];
|
||||
}
|
||||
|
||||
const found = await client.query(findSql, findParams);
|
||||
if (found.rowCount === 0) return null;
|
||||
|
||||
const { pkg_code, company_code: targetCompany } = found.rows[0];
|
||||
|
||||
// 매칭품목 먼저 삭제
|
||||
await client.query(
|
||||
`DELETE FROM pkg_unit_item WHERE pkg_code = $1 AND company_code = $2`,
|
||||
[pkg_code, targetCompany]
|
||||
);
|
||||
|
||||
// 적재함 포장구성에서 참조 삭제
|
||||
await client.query(
|
||||
`DELETE FROM loading_unit_pkg WHERE pkg_code = $1 AND company_code = $2`,
|
||||
[pkg_code, targetCompany]
|
||||
);
|
||||
|
||||
// 포장단위 삭제
|
||||
const del = await client.query(
|
||||
`DELETE FROM pkg_unit WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, targetCompany]
|
||||
);
|
||||
|
||||
return del.rows[0];
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "포장단위를 찾을 수 없거나 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("포장단위 삭제 성공", { companyCode, id });
|
||||
res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
logger.error("포장단위 삭제 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 포장단위 매칭품목(pkg_unit_item) N:M
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 매칭품목 목록 조회 (포장단위 기준)
|
||||
* GET /api/packaging/pkg-unit-items?pkg_code=XXX
|
||||
*/
|
||||
export const getPkgUnitItems = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { pkg_code } = req.query;
|
||||
|
||||
if (!pkg_code) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "pkg_code 파라미터가 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const conditions: string[] = [`pui.pkg_code = $1`];
|
||||
const params: any[] = [pkg_code];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (companyCode !== "*") {
|
||||
conditions.push(`pui.company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT pui.*
|
||||
FROM pkg_unit_item pui
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY pui.created_date DESC
|
||||
`;
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
logger.info("매칭품목 조회 성공", { companyCode, pkg_code, count: rows.length });
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (error: any) {
|
||||
logger.error("매칭품목 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 매칭품목 추가
|
||||
* POST /api/packaging/pkg-unit-items
|
||||
*/
|
||||
export const createPkgUnitItem = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { pkg_code, item_number, pkg_qty } = req.body;
|
||||
|
||||
if (!pkg_code || !item_number) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "pkg_code와 item_number는 필수입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
const dup = await query(
|
||||
`SELECT id FROM pkg_unit_item WHERE pkg_code = $1 AND item_number = $2 AND company_code = $3`,
|
||||
[pkg_code, item_number, companyCode]
|
||||
);
|
||||
if (dup.length > 0) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
message: `이미 매칭된 품목입니다: ${item_number}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
INSERT INTO pkg_unit_item (company_code, pkg_code, item_number, pkg_qty, writer)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const rows = await query(sql, [
|
||||
companyCode,
|
||||
pkg_code,
|
||||
item_number,
|
||||
pkg_qty || null,
|
||||
userId,
|
||||
]);
|
||||
|
||||
logger.info("매칭품목 추가 성공", { companyCode, pkg_code, item_number });
|
||||
res.status(201).json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("매칭품목 추가 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 매칭품목 삭제
|
||||
* DELETE /api/packaging/pkg-unit-items/:id
|
||||
*/
|
||||
export const deletePkgUnitItem = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
let sql: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
sql = `DELETE FROM pkg_unit_item WHERE id = $1 RETURNING id`;
|
||||
params = [id];
|
||||
} else {
|
||||
sql = `DELETE FROM pkg_unit_item WHERE id = $1 AND company_code = $2 RETURNING id`;
|
||||
params = [id, companyCode];
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "매칭품목을 찾을 수 없거나 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("매칭품목 삭제 성공", { companyCode, id });
|
||||
res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
logger.error("매칭품목 삭제 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 적재함(loading_unit) CRUD
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 적재함 목록 조회
|
||||
* GET /api/packaging/loading-units
|
||||
*/
|
||||
export const getLoadingUnits = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { search, loading_type, status } = req.query;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 전체 조회
|
||||
} else {
|
||||
conditions.push(`lu.company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (search && typeof search === "string" && search.trim()) {
|
||||
conditions.push(
|
||||
`(lu.loading_code ILIKE $${paramIndex} OR lu.loading_name ILIKE $${paramIndex})`
|
||||
);
|
||||
params.push(`%${search.trim()}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (loading_type && typeof loading_type === "string" && loading_type.trim()) {
|
||||
conditions.push(`lu.loading_type = $${paramIndex}`);
|
||||
params.push(loading_type.trim());
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (status && typeof status === "string" && status.trim()) {
|
||||
conditions.push(`lu.status = $${paramIndex}`);
|
||||
params.push(status.trim());
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
lu.*,
|
||||
(SELECT COUNT(*) FROM loading_unit_pkg lup
|
||||
WHERE lup.loading_code = lu.loading_code AND lup.company_code = lu.company_code
|
||||
) AS pkg_count
|
||||
FROM loading_unit lu
|
||||
${whereClause}
|
||||
ORDER BY lu.created_date DESC
|
||||
`;
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
logger.info("적재함 목록 조회 성공", { companyCode, count: rows.length });
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (error: any) {
|
||||
logger.error("적재함 목록 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 적재함 상세 조회
|
||||
* GET /api/packaging/loading-units/:id
|
||||
*/
|
||||
export const getLoadingUnitById = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
let sql: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
sql = `SELECT * FROM loading_unit WHERE id = $1`;
|
||||
params = [id];
|
||||
} else {
|
||||
sql = `SELECT * FROM loading_unit WHERE id = $1 AND company_code = $2`;
|
||||
params = [id, companyCode];
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "적재함을 찾을 수 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("적재함 상세 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 적재함 등록
|
||||
* POST /api/packaging/loading-units
|
||||
*/
|
||||
export const createLoadingUnit = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
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: "적재코드(loading_code)와 적재명(loading_name)은 필수입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
const dupCheck = await query(
|
||||
`SELECT id FROM loading_unit WHERE loading_code = $1 AND company_code = $2`,
|
||||
[loading_code, companyCode]
|
||||
);
|
||||
if (dupCheck.length > 0) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
message: `적재코드 '${loading_code}'가 이미 존재합니다.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
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 *
|
||||
`;
|
||||
|
||||
const rows = await query(sql, [
|
||||
companyCode,
|
||||
loading_code,
|
||||
loading_name,
|
||||
loading_type || null,
|
||||
status || "ACTIVE",
|
||||
width_mm || null,
|
||||
length_mm || null,
|
||||
height_mm || null,
|
||||
self_weight_kg || null,
|
||||
max_load_kg || null,
|
||||
max_stack || null,
|
||||
remarks || null,
|
||||
userId,
|
||||
]);
|
||||
|
||||
logger.info("적재함 등록 성공", { companyCode, loading_code });
|
||||
res.status(201).json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("적재함 등록 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 적재함 수정
|
||||
* PUT /api/packaging/loading-units/:id
|
||||
*/
|
||||
export const updateLoadingUnit = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const {
|
||||
loading_name,
|
||||
loading_type,
|
||||
status,
|
||||
width_mm,
|
||||
length_mm,
|
||||
height_mm,
|
||||
self_weight_kg,
|
||||
max_load_kg,
|
||||
max_stack,
|
||||
remarks,
|
||||
} = req.body;
|
||||
|
||||
const setClauses: string[] = ["updated_date = NOW()", "writer = $1"];
|
||||
const params: any[] = [userId];
|
||||
let paramIndex = 2;
|
||||
|
||||
const fieldMap: Record<string, any> = {
|
||||
loading_name,
|
||||
loading_type,
|
||||
status,
|
||||
width_mm,
|
||||
length_mm,
|
||||
height_mm,
|
||||
self_weight_kg,
|
||||
max_load_kg,
|
||||
max_stack,
|
||||
remarks,
|
||||
};
|
||||
|
||||
for (const [col, val] of Object.entries(fieldMap)) {
|
||||
if (val !== undefined) {
|
||||
setClauses.push(`${col} = $${paramIndex}`);
|
||||
params.push(val);
|
||||
paramIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
params.push(id);
|
||||
const idIdx = paramIndex;
|
||||
paramIndex++;
|
||||
|
||||
let whereClause: string;
|
||||
if (companyCode === "*") {
|
||||
whereClause = `WHERE id = $${idIdx}`;
|
||||
} else {
|
||||
params.push(companyCode);
|
||||
whereClause = `WHERE id = $${idIdx} AND company_code = $${paramIndex}`;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
UPDATE loading_unit
|
||||
SET ${setClauses.join(", ")}
|
||||
${whereClause}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "적재함을 찾을 수 없거나 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("적재함 수정 성공", { companyCode, id });
|
||||
res.json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("적재함 수정 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 적재함 삭제
|
||||
* DELETE /api/packaging/loading-units/:id
|
||||
*/
|
||||
export const deleteLoadingUnit = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await transaction(async (client) => {
|
||||
let findSql: string;
|
||||
let findParams: any[];
|
||||
if (companyCode === "*") {
|
||||
findSql = `SELECT loading_code, company_code FROM loading_unit WHERE id = $1`;
|
||||
findParams = [id];
|
||||
} else {
|
||||
findSql = `SELECT loading_code, company_code FROM loading_unit WHERE id = $1 AND company_code = $2`;
|
||||
findParams = [id, companyCode];
|
||||
}
|
||||
|
||||
const found = await client.query(findSql, findParams);
|
||||
if (found.rowCount === 0) return null;
|
||||
|
||||
const { loading_code, company_code: targetCompany } = found.rows[0];
|
||||
|
||||
// 포장구성 먼저 삭제
|
||||
await client.query(
|
||||
`DELETE FROM loading_unit_pkg WHERE loading_code = $1 AND company_code = $2`,
|
||||
[loading_code, targetCompany]
|
||||
);
|
||||
|
||||
// 적재함 삭제
|
||||
const del = await client.query(
|
||||
`DELETE FROM loading_unit WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, targetCompany]
|
||||
);
|
||||
|
||||
return del.rows[0];
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "적재함을 찾을 수 없거나 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("적재함 삭제 성공", { companyCode, id });
|
||||
res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
logger.error("적재함 삭제 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 적재함 포장구성(loading_unit_pkg) N:M
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 포장구성 목록 조회 (적재함 기준)
|
||||
* GET /api/packaging/loading-unit-pkgs?loading_code=XXX
|
||||
*/
|
||||
export const getLoadingUnitPkgs = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { loading_code } = req.query;
|
||||
|
||||
if (!loading_code) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "loading_code 파라미터가 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const conditions: string[] = [`lup.loading_code = $1`];
|
||||
const params: any[] = [loading_code];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (companyCode !== "*") {
|
||||
conditions.push(`lup.company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
lup.*,
|
||||
pu.pkg_name
|
||||
FROM loading_unit_pkg lup
|
||||
LEFT JOIN pkg_unit pu
|
||||
ON lup.pkg_code = pu.pkg_code AND lup.company_code = pu.company_code
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY lup.created_date DESC
|
||||
`;
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
logger.info("포장구성 조회 성공", {
|
||||
companyCode,
|
||||
loading_code,
|
||||
count: rows.length,
|
||||
});
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (error: any) {
|
||||
logger.error("포장구성 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 포장구성 추가
|
||||
* POST /api/packaging/loading-unit-pkgs
|
||||
*/
|
||||
export const createLoadingUnitPkg = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { loading_code, pkg_code, max_load_qty, load_method } = req.body;
|
||||
|
||||
if (!loading_code || !pkg_code) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "loading_code와 pkg_code는 필수입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
const dup = await query(
|
||||
`SELECT id FROM loading_unit_pkg WHERE loading_code = $1 AND pkg_code = $2 AND company_code = $3`,
|
||||
[loading_code, pkg_code, companyCode]
|
||||
);
|
||||
if (dup.length > 0) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
message: `이미 등록된 포장구성입니다: ${pkg_code}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = `
|
||||
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 *
|
||||
`;
|
||||
|
||||
const rows = await query(sql, [
|
||||
companyCode,
|
||||
loading_code,
|
||||
pkg_code,
|
||||
max_load_qty || null,
|
||||
load_method || null,
|
||||
userId,
|
||||
]);
|
||||
|
||||
logger.info("포장구성 추가 성공", { companyCode, loading_code, pkg_code });
|
||||
res.status(201).json({ success: true, data: rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("포장구성 추가 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 포장구성 삭제
|
||||
* DELETE /api/packaging/loading-unit-pkgs/:id
|
||||
*/
|
||||
export const deleteLoadingUnitPkg = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
let sql: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
sql = `DELETE FROM loading_unit_pkg WHERE id = $1 RETURNING id`;
|
||||
params = [id];
|
||||
} else {
|
||||
sql = `DELETE FROM loading_unit_pkg WHERE id = $1 AND company_code = $2 RETURNING id`;
|
||||
params = [id, companyCode];
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "포장구성을 찾을 수 없거나 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("포장구성 삭제 성공", { companyCode, id });
|
||||
res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
logger.error("포장구성 삭제 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,10 +1,50 @@
|
|||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import {
|
||||
getPkgUnits,
|
||||
getPkgUnitById,
|
||||
createPkgUnit,
|
||||
updatePkgUnit,
|
||||
deletePkgUnit,
|
||||
getPkgUnitItems,
|
||||
createPkgUnitItem,
|
||||
deletePkgUnitItem,
|
||||
getLoadingUnits,
|
||||
getLoadingUnitById,
|
||||
createLoadingUnit,
|
||||
updateLoadingUnit,
|
||||
deleteLoadingUnit,
|
||||
getLoadingUnitPkgs,
|
||||
createLoadingUnitPkg,
|
||||
deleteLoadingUnitPkg,
|
||||
} from "../controllers/packagingController";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
|
||||
// TODO: 포장/적재정보 관리 API 구현 예정
|
||||
// 포장단위 CRUD
|
||||
router.get("/pkg-units", getPkgUnits);
|
||||
router.get("/pkg-units/:id", getPkgUnitById);
|
||||
router.post("/pkg-units", createPkgUnit);
|
||||
router.put("/pkg-units/:id", updatePkgUnit);
|
||||
router.delete("/pkg-units/:id", deletePkgUnit);
|
||||
|
||||
// 포장단위 매칭품목 (N:M)
|
||||
router.get("/pkg-unit-items", getPkgUnitItems);
|
||||
router.post("/pkg-unit-items", createPkgUnitItem);
|
||||
router.delete("/pkg-unit-items/:id", deletePkgUnitItem);
|
||||
|
||||
// 적재함 CRUD
|
||||
router.get("/loading-units", getLoadingUnits);
|
||||
router.get("/loading-units/:id", getLoadingUnitById);
|
||||
router.post("/loading-units", createLoadingUnit);
|
||||
router.put("/loading-units/:id", updateLoadingUnit);
|
||||
router.delete("/loading-units/:id", deleteLoadingUnit);
|
||||
|
||||
// 적재함 포장구성 (N:M)
|
||||
router.get("/loading-unit-pkgs", getLoadingUnitPkgs);
|
||||
router.post("/loading-unit-pkgs", createLoadingUnitPkg);
|
||||
router.delete("/loading-unit-pkgs/:id", deleteLoadingUnitPkg);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
Loading…
Reference in New Issue