diff --git a/backend-node/src/controllers/packagingController.ts b/backend-node/src/controllers/packagingController.ts new file mode 100644 index 00000000..681d9ebc --- /dev/null +++ b/backend-node/src/controllers/packagingController.ts @@ -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 => { + 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 => { + 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 => { + 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 => { + 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 = { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 = { + 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 => { + 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 => { + 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 => { + 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 => { + 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 }); + } +}; diff --git a/backend-node/src/routes/packagingRoutes.ts b/backend-node/src/routes/packagingRoutes.ts index f501269e..ffbf5d14 100644 --- a/backend-node/src/routes/packagingRoutes.ts +++ b/backend-node/src/routes/packagingRoutes.ts @@ -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;