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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 }); } }