483 lines
18 KiB
TypeScript
483 lines
18 KiB
TypeScript
|
|
/**
|
||
|
|
* 출하지시 컨트롤러 (shipment_instruction + shipment_instruction_detail)
|
||
|
|
*/
|
||
|
|
import { Response } from "express";
|
||
|
|
import { AuthenticatedRequest } from "../types/auth";
|
||
|
|
import { getPool } from "../database/db";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
import { numberingRuleService } from "../services/numberingRuleService";
|
||
|
|
|
||
|
|
// ─── 출하지시 목록 조회 ───
|
||
|
|
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
const { dateFrom, dateTo, status, customer, keyword } = req.query;
|
||
|
|
|
||
|
|
const conditions: string[] = [];
|
||
|
|
const params: any[] = [];
|
||
|
|
let idx = 1;
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
conditions.push(`si.company_code = $${idx}`);
|
||
|
|
params.push(companyCode);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
if (dateFrom) {
|
||
|
|
conditions.push(`si.instruction_date >= $${idx}::date`);
|
||
|
|
params.push(dateFrom);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
if (dateTo) {
|
||
|
|
conditions.push(`si.instruction_date <= $${idx}::date`);
|
||
|
|
params.push(dateTo);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
if (status) {
|
||
|
|
conditions.push(`si.status = $${idx}`);
|
||
|
|
params.push(status);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
if (customer) {
|
||
|
|
conditions.push(`(c.customer_name ILIKE $${idx} OR si.partner_id ILIKE $${idx})`);
|
||
|
|
params.push(`%${customer}%`);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
if (keyword) {
|
||
|
|
conditions.push(`(si.instruction_no ILIKE $${idx} OR si.memo ILIKE $${idx})`);
|
||
|
|
params.push(`%${keyword}%`);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
|
||
|
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
SELECT
|
||
|
|
si.*,
|
||
|
|
COALESCE(c.customer_name, si.partner_id, '') AS customer_name,
|
||
|
|
COALESCE(
|
||
|
|
json_agg(
|
||
|
|
json_build_object(
|
||
|
|
'id', sid.id,
|
||
|
|
'item_code', sid.item_code,
|
||
|
|
'item_name', COALESCE(i.item_name, sid.item_name, sid.item_code),
|
||
|
|
'spec', sid.spec,
|
||
|
|
'material', sid.material,
|
||
|
|
'order_qty', sid.order_qty,
|
||
|
|
'plan_qty', sid.plan_qty,
|
||
|
|
'ship_qty', sid.ship_qty,
|
||
|
|
'source_type', sid.source_type,
|
||
|
|
'shipment_plan_id', sid.shipment_plan_id,
|
||
|
|
'sales_order_id', sid.sales_order_id,
|
||
|
|
'detail_id', sid.detail_id
|
||
|
|
)
|
||
|
|
) FILTER (WHERE sid.id IS NOT NULL),
|
||
|
|
'[]'
|
||
|
|
) AS items
|
||
|
|
FROM shipment_instruction si
|
||
|
|
LEFT JOIN customer_mng c
|
||
|
|
ON si.partner_id = c.customer_code AND si.company_code = c.company_code
|
||
|
|
LEFT JOIN shipment_instruction_detail sid
|
||
|
|
ON si.id = sid.instruction_id AND si.company_code = sid.company_code
|
||
|
|
LEFT JOIN LATERAL (
|
||
|
|
SELECT item_name FROM item_info
|
||
|
|
WHERE item_number = sid.item_code AND company_code = si.company_code
|
||
|
|
LIMIT 1
|
||
|
|
) i ON true
|
||
|
|
${where}
|
||
|
|
GROUP BY si.id, c.customer_name
|
||
|
|
ORDER BY si.created_date DESC
|
||
|
|
`;
|
||
|
|
|
||
|
|
const pool = getPool();
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
logger.info("출하지시 목록 조회", { companyCode, count: result.rowCount });
|
||
|
|
return res.json({ success: true, data: result.rows });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("출하지시 목록 조회 실패", { error: error.message });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 다음 출하지시번호 미리보기 ───
|
||
|
|
export async function previewNextNo(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
let instructionNo: string;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
||
|
|
companyCode, "shipment_instruction", "instruction_no"
|
||
|
|
);
|
||
|
|
if (rule) {
|
||
|
|
instructionNo = await numberingRuleService.previewCode(
|
||
|
|
rule.ruleId, companyCode, {}
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
throw new Error("채번 규칙 없음");
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
const pool = getPool();
|
||
|
|
const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
||
|
|
const seqRes = await pool.query(
|
||
|
|
`SELECT COUNT(*) + 1 AS seq FROM shipment_instruction WHERE company_code = $1 AND instruction_no LIKE $2`,
|
||
|
|
[companyCode, `SI-${today}-%`]
|
||
|
|
);
|
||
|
|
const seq = String(seqRes.rows[0].seq).padStart(3, "0");
|
||
|
|
instructionNo = `SI-${today}-${seq}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return res.json({ success: true, instructionNo });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("출하지시번호 미리보기 실패", { error: error.message });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 출하지시 저장 (신규/수정) ───
|
||
|
|
export async function save(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
const userId = req.user!.userId;
|
||
|
|
const {
|
||
|
|
id: editId,
|
||
|
|
instructionDate,
|
||
|
|
partnerId,
|
||
|
|
status: orderStatus,
|
||
|
|
memo,
|
||
|
|
carrierName,
|
||
|
|
vehicleNo,
|
||
|
|
driverName,
|
||
|
|
driverContact,
|
||
|
|
arrivalTime,
|
||
|
|
deliveryAddress,
|
||
|
|
items,
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
if (!instructionDate) {
|
||
|
|
return res.status(400).json({ success: false, message: "출하지시일은 필수입니다" });
|
||
|
|
}
|
||
|
|
if (!items || items.length === 0) {
|
||
|
|
return res.status(400).json({ success: false, message: "품목을 선택해주세요" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const pool = getPool();
|
||
|
|
const client = await pool.connect();
|
||
|
|
|
||
|
|
try {
|
||
|
|
await client.query("BEGIN");
|
||
|
|
|
||
|
|
let instructionId: number;
|
||
|
|
let instructionNo: string;
|
||
|
|
|
||
|
|
if (editId) {
|
||
|
|
// 수정
|
||
|
|
const check = await client.query(
|
||
|
|
`SELECT id, instruction_no FROM shipment_instruction WHERE id = $1 AND company_code = $2`,
|
||
|
|
[editId, companyCode]
|
||
|
|
);
|
||
|
|
if (check.rowCount === 0) {
|
||
|
|
throw new Error("출하지시를 찾을 수 없습니다");
|
||
|
|
}
|
||
|
|
instructionId = editId;
|
||
|
|
instructionNo = check.rows[0].instruction_no;
|
||
|
|
|
||
|
|
await client.query(
|
||
|
|
`UPDATE shipment_instruction SET
|
||
|
|
instruction_date = $1::date, partner_id = $2, status = $3, memo = $4,
|
||
|
|
carrier_name = $5, vehicle_no = $6, driver_name = $7, driver_contact = $8,
|
||
|
|
arrival_time = $9, delivery_address = $10,
|
||
|
|
updated_date = NOW(), updated_by = $11
|
||
|
|
WHERE id = $12 AND company_code = $13`,
|
||
|
|
[
|
||
|
|
instructionDate, partnerId, orderStatus || "READY", memo,
|
||
|
|
carrierName, vehicleNo, driverName, driverContact,
|
||
|
|
arrivalTime || null, deliveryAddress,
|
||
|
|
userId, editId, companyCode,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
// 기존 디테일 삭제 후 재삽입
|
||
|
|
await client.query(
|
||
|
|
`DELETE FROM shipment_instruction_detail WHERE instruction_id = $1 AND company_code = $2`,
|
||
|
|
[editId, companyCode]
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
// 신규 - 채번 규칙이 있으면 사용, 없으면 자체 생성
|
||
|
|
try {
|
||
|
|
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
||
|
|
companyCode, "shipment_instruction", "instruction_no"
|
||
|
|
);
|
||
|
|
if (rule) {
|
||
|
|
instructionNo = await numberingRuleService.allocateCode(
|
||
|
|
rule.ruleId, companyCode, { instruction_date: instructionDate }
|
||
|
|
);
|
||
|
|
logger.info("채번 규칙으로 출하지시번호 생성", { ruleId: rule.ruleId, instructionNo });
|
||
|
|
} else {
|
||
|
|
throw new Error("채번 규칙 없음 - 폴백");
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
||
|
|
const seqRes = await client.query(
|
||
|
|
`SELECT COUNT(*) + 1 AS seq FROM shipment_instruction WHERE company_code = $1 AND instruction_no LIKE $2`,
|
||
|
|
[companyCode, `SI-${today}-%`]
|
||
|
|
);
|
||
|
|
const seq = String(seqRes.rows[0].seq).padStart(3, "0");
|
||
|
|
instructionNo = `SI-${today}-${seq}`;
|
||
|
|
logger.info("폴백으로 출하지시번호 생성", { instructionNo });
|
||
|
|
}
|
||
|
|
|
||
|
|
const insertRes = await client.query(
|
||
|
|
`INSERT INTO shipment_instruction
|
||
|
|
(company_code, instruction_no, instruction_date, partner_id, status, memo,
|
||
|
|
carrier_name, vehicle_no, driver_name, driver_contact, arrival_time, delivery_address,
|
||
|
|
created_date, created_by)
|
||
|
|
VALUES ($1, $2, $3::date, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), $13)
|
||
|
|
RETURNING id`,
|
||
|
|
[
|
||
|
|
companyCode, instructionNo, instructionDate, partnerId,
|
||
|
|
orderStatus || "READY", memo,
|
||
|
|
carrierName, vehicleNo, driverName, driverContact,
|
||
|
|
arrivalTime || null, deliveryAddress, userId,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
instructionId = insertRes.rows[0].id;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 디테일 삽입
|
||
|
|
for (const item of items) {
|
||
|
|
await client.query(
|
||
|
|
`INSERT INTO shipment_instruction_detail
|
||
|
|
(company_code, instruction_id, shipment_plan_id, sales_order_id, detail_id,
|
||
|
|
item_code, item_name, spec, material, order_qty, plan_qty, ship_qty,
|
||
|
|
source_type, created_date, created_by)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), $14)`,
|
||
|
|
[
|
||
|
|
companyCode, instructionId,
|
||
|
|
item.shipmentPlanId || null, item.salesOrderId || null, item.detailId || null,
|
||
|
|
item.itemCode, item.itemName, item.spec, item.material,
|
||
|
|
item.orderQty || 0, item.planQty || 0, item.shipQty || 0,
|
||
|
|
item.sourceType || "shipmentPlan", userId,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
await client.query("COMMIT");
|
||
|
|
|
||
|
|
logger.info("출하지시 저장 완료", { companyCode, instructionId, instructionNo, itemCount: items.length });
|
||
|
|
return res.json({ success: true, data: { id: instructionId, instructionNo } });
|
||
|
|
} catch (txErr) {
|
||
|
|
await client.query("ROLLBACK");
|
||
|
|
throw txErr;
|
||
|
|
} finally {
|
||
|
|
client.release();
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("출하지시 저장 실패", { error: error.message, stack: error.stack });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 출하지시 삭제 ───
|
||
|
|
export async function remove(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
const { ids } = req.body;
|
||
|
|
|
||
|
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||
|
|
return res.status(400).json({ success: false, message: "삭제할 ID가 필요합니다" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const pool = getPool();
|
||
|
|
// CASCADE로 디테일도 자동 삭제
|
||
|
|
const result = await pool.query(
|
||
|
|
`DELETE FROM shipment_instruction WHERE id = ANY($1::int[]) AND company_code = $2 RETURNING id`,
|
||
|
|
[ids, companyCode]
|
||
|
|
);
|
||
|
|
|
||
|
|
logger.info("출하지시 삭제", { companyCode, deletedCount: result.rowCount });
|
||
|
|
return res.json({ success: true, deletedCount: result.rowCount });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("출하지시 삭제 실패", { error: error.message });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 출하계획 목록 (모달 왼쪽 패널용) ───
|
||
|
|
export async function getShipmentPlanSource(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
const { keyword, customer, page: pageStr, pageSize: pageSizeStr } = req.query;
|
||
|
|
const page = Math.max(1, parseInt(pageStr as string) || 1);
|
||
|
|
const pageSize = Math.min(100, Math.max(1, parseInt(pageSizeStr as string) || 20));
|
||
|
|
const offset = (page - 1) * pageSize;
|
||
|
|
|
||
|
|
const conditions = ["sp.company_code = $1", "sp.status = 'READY'"];
|
||
|
|
const params: any[] = [companyCode];
|
||
|
|
let idx = 2;
|
||
|
|
|
||
|
|
if (keyword) {
|
||
|
|
conditions.push(`(COALESCE(d.part_code, m.part_code, '') ILIKE $${idx} OR COALESCE(i.item_name, d.part_name, m.part_name, '') ILIKE $${idx})`);
|
||
|
|
params.push(`%${keyword}%`);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
if (customer) {
|
||
|
|
conditions.push(`(c.customer_name ILIKE $${idx} OR COALESCE(m.partner_id, d.delivery_partner_code, '') ILIKE $${idx})`);
|
||
|
|
params.push(`%${customer}%`);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause = conditions.join(" AND ");
|
||
|
|
const fromClause = `
|
||
|
|
FROM shipment_plan sp
|
||
|
|
LEFT JOIN sales_order_detail d ON sp.detail_id = d.id AND sp.company_code = d.company_code
|
||
|
|
LEFT JOIN sales_order_mng m ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||
|
|
LEFT JOIN LATERAL (
|
||
|
|
SELECT item_name FROM item_info
|
||
|
|
WHERE item_number = COALESCE(d.part_code, m.part_code) AND company_code = sp.company_code
|
||
|
|
LIMIT 1
|
||
|
|
) i ON true
|
||
|
|
LEFT JOIN customer_mng c
|
||
|
|
ON COALESCE(m.partner_id, d.delivery_partner_code) = c.customer_code AND sp.company_code = c.company_code
|
||
|
|
WHERE ${whereClause}
|
||
|
|
`;
|
||
|
|
|
||
|
|
const pool = getPool();
|
||
|
|
const countResult = await pool.query(`SELECT COUNT(*) AS total ${fromClause}`, params);
|
||
|
|
const totalCount = parseInt(countResult.rows[0].total);
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
SELECT
|
||
|
|
sp.id, sp.plan_qty, sp.plan_date, sp.status, sp.shipment_plan_no,
|
||
|
|
COALESCE(m.order_no, d.order_no, '') AS order_no,
|
||
|
|
COALESCE(d.part_code, m.part_code, '') AS item_code,
|
||
|
|
COALESCE(i.item_name, d.part_name, m.part_name, COALESCE(d.part_code, m.part_code, '')) AS item_name,
|
||
|
|
COALESCE(d.spec, m.spec, '') AS spec,
|
||
|
|
COALESCE(m.material, '') AS material,
|
||
|
|
COALESCE(c.customer_name, m.partner_id, d.delivery_partner_code, '') AS customer_name,
|
||
|
|
COALESCE(m.partner_id, d.delivery_partner_code, '') AS partner_code,
|
||
|
|
sp.detail_id, sp.sales_order_id
|
||
|
|
${fromClause}
|
||
|
|
ORDER BY sp.created_date DESC
|
||
|
|
LIMIT $${idx} OFFSET $${idx + 1}
|
||
|
|
`;
|
||
|
|
params.push(pageSize, offset);
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
return res.json({ success: true, data: result.rows, totalCount, page, pageSize });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("출하계획 소스 조회 실패", { error: error.message });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 수주 목록 (모달 왼쪽 패널용) ───
|
||
|
|
export async function getSalesOrderSource(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
const { keyword, customer, page: pageStr, pageSize: pageSizeStr } = req.query;
|
||
|
|
const page = Math.max(1, parseInt(pageStr as string) || 1);
|
||
|
|
const pageSize = Math.min(100, Math.max(1, parseInt(pageSizeStr as string) || 20));
|
||
|
|
const offset = (page - 1) * pageSize;
|
||
|
|
|
||
|
|
const conditions = ["d.company_code = $1"];
|
||
|
|
const params: any[] = [companyCode];
|
||
|
|
let idx = 2;
|
||
|
|
|
||
|
|
if (keyword) {
|
||
|
|
conditions.push(`(d.part_code ILIKE $${idx} OR COALESCE(i.item_name, d.part_name, d.part_code) ILIKE $${idx} OR d.order_no ILIKE $${idx})`);
|
||
|
|
params.push(`%${keyword}%`);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
if (customer) {
|
||
|
|
conditions.push(`(c.customer_name ILIKE $${idx} OR COALESCE(d.delivery_partner_code, m.partner_id, '') ILIKE $${idx})`);
|
||
|
|
params.push(`%${customer}%`);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause = conditions.join(" AND ");
|
||
|
|
const fromClause = `
|
||
|
|
FROM sales_order_detail d
|
||
|
|
LEFT JOIN sales_order_mng m ON d.order_no = m.order_no AND d.company_code = m.company_code
|
||
|
|
LEFT JOIN LATERAL (
|
||
|
|
SELECT item_name FROM item_info
|
||
|
|
WHERE item_number = d.part_code AND company_code = d.company_code
|
||
|
|
LIMIT 1
|
||
|
|
) i ON true
|
||
|
|
LEFT JOIN customer_mng c
|
||
|
|
ON COALESCE(d.delivery_partner_code, m.partner_id) = c.customer_code AND d.company_code = c.company_code
|
||
|
|
WHERE ${whereClause}
|
||
|
|
`;
|
||
|
|
|
||
|
|
const pool = getPool();
|
||
|
|
const countResult = await pool.query(`SELECT COUNT(*) AS total ${fromClause}`, params);
|
||
|
|
const totalCount = parseInt(countResult.rows[0].total);
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
SELECT
|
||
|
|
d.id, d.order_no, d.part_code AS item_code,
|
||
|
|
COALESCE(i.item_name, d.part_name, d.part_code) AS item_name,
|
||
|
|
COALESCE(d.spec, '') AS spec, COALESCE(m.material, '') AS material,
|
||
|
|
COALESCE(NULLIF(d.qty,'')::numeric, 0) AS qty,
|
||
|
|
COALESCE(NULLIF(d.balance_qty,'')::numeric, 0) AS balance_qty,
|
||
|
|
COALESCE(c.customer_name, COALESCE(d.delivery_partner_code, m.partner_id, '')) AS customer_name,
|
||
|
|
COALESCE(d.delivery_partner_code, m.partner_id, '') AS partner_code,
|
||
|
|
m.id AS master_id
|
||
|
|
${fromClause}
|
||
|
|
ORDER BY d.created_date DESC
|
||
|
|
LIMIT $${idx} OFFSET $${idx + 1}
|
||
|
|
`;
|
||
|
|
params.push(pageSize, offset);
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
return res.json({ success: true, data: result.rows, totalCount, page, pageSize });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("수주 소스 조회 실패", { error: error.message });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 품목 목록 (모달 왼쪽 패널용) ───
|
||
|
|
export async function getItemSource(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
const { keyword, page: pageStr, pageSize: pageSizeStr } = req.query;
|
||
|
|
const page = Math.max(1, parseInt(pageStr as string) || 1);
|
||
|
|
const pageSize = Math.min(100, Math.max(1, parseInt(pageSizeStr as string) || 20));
|
||
|
|
const offset = (page - 1) * pageSize;
|
||
|
|
|
||
|
|
const conditions = ["company_code = $1"];
|
||
|
|
const params: any[] = [companyCode];
|
||
|
|
let idx = 2;
|
||
|
|
|
||
|
|
if (keyword) {
|
||
|
|
conditions.push(`(item_number ILIKE $${idx} OR item_name ILIKE $${idx})`);
|
||
|
|
params.push(`%${keyword}%`);
|
||
|
|
idx++;
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause = conditions.join(" AND ");
|
||
|
|
|
||
|
|
const pool = getPool();
|
||
|
|
const countResult = await pool.query(`SELECT COUNT(*) AS total FROM item_info WHERE ${whereClause}`, params);
|
||
|
|
const totalCount = parseInt(countResult.rows[0].total);
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
SELECT
|
||
|
|
item_number AS item_code, item_name,
|
||
|
|
COALESCE(size, '') AS spec, COALESCE(material, '') AS material
|
||
|
|
FROM item_info
|
||
|
|
WHERE ${whereClause}
|
||
|
|
ORDER BY item_name
|
||
|
|
LIMIT $${idx} OFFSET $${idx + 1}
|
||
|
|
`;
|
||
|
|
params.push(pageSize, offset);
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
return res.json({ success: true, data: result.rows, totalCount, page, pageSize });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("품목 소스 조회 실패", { error: error.message });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|