- Fix department, receiving, shippingOrder, shippingPlan controllers - Update admin pages (company management, disk usage) - Improve sales/logistics pages (order, shipping, outbound, receiving) - Enhance V2 components (file-upload, split-panel-layout, table-list) - Add SmartSelect common component - Update DataGrid, FullscreenDialog common components - Add gitignore rules for personal pipeline tools Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
650 lines
23 KiB
TypeScript
650 lines
23 KiB
TypeScript
/**
|
|
* 입고관리 컨트롤러
|
|
*
|
|
* 입고유형별 소스 테이블:
|
|
* - 구매입고 → purchase_order_mng (발주 헤더) + purchase_detail (발주 디테일)
|
|
* - 반품입고 → shipment_instruction + shipment_instruction_detail (출하)
|
|
* - 기타입고 → item_info (품목)
|
|
*/
|
|
|
|
import { Response } from "express";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
|
|
// 입고 목록 조회
|
|
export async function getList(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const {
|
|
inbound_type,
|
|
inbound_status,
|
|
search_keyword,
|
|
date_from,
|
|
date_to,
|
|
} = req.query;
|
|
|
|
const conditions: string[] = [];
|
|
const params: any[] = [];
|
|
let paramIdx = 1;
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 전체 조회
|
|
} else {
|
|
conditions.push(`im.company_code = $${paramIdx}`);
|
|
params.push(companyCode);
|
|
paramIdx++;
|
|
}
|
|
|
|
if (inbound_type && inbound_type !== "all") {
|
|
conditions.push(`im.inbound_type = $${paramIdx}`);
|
|
params.push(inbound_type);
|
|
paramIdx++;
|
|
}
|
|
|
|
if (inbound_status && inbound_status !== "all") {
|
|
conditions.push(`im.inbound_status = $${paramIdx}`);
|
|
params.push(inbound_status);
|
|
paramIdx++;
|
|
}
|
|
|
|
if (search_keyword) {
|
|
conditions.push(
|
|
`(im.inbound_number ILIKE $${paramIdx} OR im.item_name ILIKE $${paramIdx} OR im.item_number ILIKE $${paramIdx} OR im.supplier_name ILIKE $${paramIdx} OR im.reference_number ILIKE $${paramIdx})`
|
|
);
|
|
params.push(`%${search_keyword}%`);
|
|
paramIdx++;
|
|
}
|
|
|
|
if (date_from) {
|
|
conditions.push(`im.inbound_date >= $${paramIdx}::date`);
|
|
params.push(date_from);
|
|
paramIdx++;
|
|
}
|
|
if (date_to) {
|
|
conditions.push(`im.inbound_date <= $${paramIdx}::date`);
|
|
params.push(date_to);
|
|
paramIdx++;
|
|
}
|
|
|
|
const whereClause =
|
|
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
|
|
const query = `
|
|
SELECT
|
|
im.*,
|
|
wh.warehouse_name
|
|
FROM inbound_mng im
|
|
LEFT JOIN warehouse_info wh
|
|
ON im.warehouse_code = wh.warehouse_code
|
|
AND im.company_code = wh.company_code
|
|
${whereClause}
|
|
ORDER BY im.created_date DESC
|
|
`;
|
|
|
|
const pool = getPool();
|
|
const result = await pool.query(query, params);
|
|
|
|
logger.info("입고 목록 조회", {
|
|
companyCode,
|
|
rowCount: 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 create(req: AuthenticatedRequest, res: Response) {
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const { items, inbound_number, inbound_date, warehouse_code, location_code, inspector, manager, memo } = req.body;
|
|
|
|
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
return res.status(400).json({ success: false, message: "입고 품목이 없습니다." });
|
|
}
|
|
|
|
await client.query("BEGIN");
|
|
|
|
const insertedRows: any[] = [];
|
|
|
|
for (const item of items) {
|
|
const result = await client.query(
|
|
`INSERT INTO inbound_mng (
|
|
company_code, inbound_number, inbound_type, inbound_date,
|
|
reference_number, supplier_code, supplier_name,
|
|
item_number, item_name, spec, material, unit,
|
|
inbound_qty, unit_price, total_amount,
|
|
lot_number, warehouse_code, location_code,
|
|
inbound_status, inspection_status,
|
|
inspector, manager, memo,
|
|
source_table, source_id,
|
|
created_date, created_by, writer, status
|
|
) VALUES (
|
|
$1, $2, $3, $4::date,
|
|
$5, $6, $7,
|
|
$8, $9, $10, $11, $12,
|
|
$13, $14, $15,
|
|
$16, $17, $18,
|
|
$19, $20,
|
|
$21, $22, $23,
|
|
$24, $25,
|
|
NOW(), $26, $26, '입고'
|
|
) RETURNING *`,
|
|
[
|
|
companyCode,
|
|
inbound_number || item.inbound_number,
|
|
item.inbound_type,
|
|
inbound_date || item.inbound_date,
|
|
item.reference_number || null,
|
|
item.supplier_code || null,
|
|
item.supplier_name || null,
|
|
item.item_number || null,
|
|
item.item_name || null,
|
|
item.spec || null,
|
|
item.material || null,
|
|
item.unit || "EA",
|
|
item.inbound_qty || 0,
|
|
item.unit_price || 0,
|
|
item.total_amount || 0,
|
|
item.lot_number || null,
|
|
warehouse_code || item.warehouse_code || null,
|
|
location_code || item.location_code || null,
|
|
item.inbound_status || "대기",
|
|
item.inspection_status || "대기",
|
|
inspector || item.inspector || null,
|
|
manager || item.manager || null,
|
|
memo || item.memo || null,
|
|
item.source_table || null,
|
|
item.source_id || null,
|
|
userId,
|
|
]
|
|
);
|
|
|
|
insertedRows.push(result.rows[0]);
|
|
|
|
// 재고 업데이트 (inventory_stock): 입고 수량 증가
|
|
const itemCode = item.item_number || null;
|
|
const whCode = warehouse_code || item.warehouse_code || null;
|
|
const locCode = location_code || item.location_code || null;
|
|
const inQty = Number(item.inbound_qty) || 0;
|
|
if (itemCode && inQty > 0) {
|
|
const existingStock = await client.query(
|
|
`SELECT id FROM inventory_stock
|
|
WHERE company_code = $1 AND item_code = $2
|
|
AND COALESCE(warehouse_code, '') = COALESCE($3, '')
|
|
AND COALESCE(location_code, '') = COALESCE($4, '')
|
|
LIMIT 1`,
|
|
[companyCode, itemCode, whCode || '', locCode || '']
|
|
);
|
|
|
|
if (existingStock.rows.length > 0) {
|
|
await client.query(
|
|
`UPDATE inventory_stock
|
|
SET current_qty = CAST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1 AS text),
|
|
last_in_date = NOW(),
|
|
updated_date = NOW()
|
|
WHERE id = $2`,
|
|
[inQty, existingStock.rows[0].id]
|
|
);
|
|
} else {
|
|
await client.query(
|
|
`INSERT INTO inventory_stock (
|
|
company_code, item_code, warehouse_code, location_code,
|
|
current_qty, safety_qty, last_in_date,
|
|
created_date, updated_date, writer
|
|
) VALUES ($1, $2, $3, $4, $5, '0', NOW(), NOW(), NOW(), $6)`,
|
|
[companyCode, itemCode, whCode, locCode, String(inQty), userId]
|
|
);
|
|
}
|
|
}
|
|
|
|
// 구매입고인 경우 발주의 received_qty 업데이트
|
|
if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_order_mng") {
|
|
await client.query(
|
|
`UPDATE purchase_order_mng
|
|
SET received_qty = CAST(
|
|
COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1 AS text
|
|
),
|
|
remain_qty = CAST(
|
|
GREATEST(COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
|
|
- COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) - $1, 0) AS text
|
|
),
|
|
status = CASE
|
|
WHEN COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1
|
|
>= COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
|
|
THEN '입고완료'
|
|
ELSE '부분입고'
|
|
END,
|
|
updated_date = NOW()
|
|
WHERE id = $2 AND company_code = $3`,
|
|
[item.inbound_qty || 0, item.source_id, companyCode]
|
|
);
|
|
}
|
|
|
|
// 구매입고인 경우 purchase_detail 기반 발주의 헤더 상태 업데이트
|
|
if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_detail") {
|
|
// 해당 디테일의 발주번호 조회
|
|
const detailInfo = await client.query(
|
|
`SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`,
|
|
[item.source_id, companyCode]
|
|
);
|
|
if (detailInfo.rows.length > 0) {
|
|
const purchaseNo = detailInfo.rows[0].purchase_no;
|
|
// 해당 발주의 모든 디테일 잔량 확인
|
|
const unreceived = await client.query(
|
|
`SELECT pd.id
|
|
FROM purchase_detail pd
|
|
LEFT JOIN (
|
|
SELECT source_id, SUM(COALESCE(inbound_qty, 0)) AS total_received
|
|
FROM inbound_mng
|
|
WHERE source_table = 'purchase_detail' AND company_code = $1
|
|
GROUP BY source_id
|
|
) r ON r.source_id = pd.id
|
|
WHERE pd.purchase_no = $2 AND pd.company_code = $1
|
|
AND COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(r.total_received, 0) > 0
|
|
LIMIT 1`,
|
|
[companyCode, purchaseNo]
|
|
);
|
|
const newStatus = unreceived.rows.length === 0 ? '입고완료' : '부분입고';
|
|
await client.query(
|
|
`UPDATE purchase_order_mng SET status = $1, updated_date = NOW()
|
|
WHERE purchase_no = $2 AND company_code = $3`,
|
|
[newStatus, purchaseNo, companyCode]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
|
|
logger.info("입고 등록 완료", {
|
|
companyCode,
|
|
userId,
|
|
count: insertedRows.length,
|
|
inbound_number,
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: insertedRows,
|
|
message: `${insertedRows.length}건 입고 등록 완료`,
|
|
});
|
|
} catch (error: any) {
|
|
await client.query("ROLLBACK");
|
|
logger.error("입고 등록 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// 입고 수정
|
|
export async function update(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const { id } = req.params;
|
|
const {
|
|
inbound_date, inbound_qty, unit_price, total_amount,
|
|
lot_number, warehouse_code, location_code,
|
|
inbound_status, inspection_status,
|
|
inspector, manager: mgr, memo,
|
|
} = req.body;
|
|
|
|
const pool = getPool();
|
|
const result = await pool.query(
|
|
`UPDATE inbound_mng SET
|
|
inbound_date = COALESCE($1::date, inbound_date),
|
|
inbound_qty = COALESCE($2, inbound_qty),
|
|
unit_price = COALESCE($3, unit_price),
|
|
total_amount = COALESCE($4, total_amount),
|
|
lot_number = COALESCE($5, lot_number),
|
|
warehouse_code = COALESCE($6, warehouse_code),
|
|
location_code = COALESCE($7, location_code),
|
|
inbound_status = COALESCE($8, inbound_status),
|
|
inspection_status = COALESCE($9, inspection_status),
|
|
inspector = COALESCE($10, inspector),
|
|
manager = COALESCE($11, manager),
|
|
memo = COALESCE($12, memo),
|
|
updated_date = NOW(),
|
|
updated_by = $13
|
|
WHERE id = $14 AND company_code = $15
|
|
RETURNING *`,
|
|
[
|
|
inbound_date, inbound_qty, unit_price, total_amount,
|
|
lot_number, warehouse_code, location_code,
|
|
inbound_status, inspection_status,
|
|
inspector, mgr, memo,
|
|
userId, id, companyCode,
|
|
]
|
|
);
|
|
|
|
if (result.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: "입고 데이터를 찾을 수 없습니다." });
|
|
}
|
|
|
|
logger.info("입고 수정", { companyCode, userId, id });
|
|
|
|
return res.json({ success: true, data: result.rows[0] });
|
|
} catch (error: any) {
|
|
logger.error("입고 수정 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// 입고 삭제
|
|
export async function deleteReceiving(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { id } = req.params;
|
|
const pool = getPool();
|
|
|
|
const result = await pool.query(
|
|
`DELETE FROM inbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
|
|
[id, companyCode]
|
|
);
|
|
|
|
if (result.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
|
}
|
|
|
|
logger.info("입고 삭제", { companyCode, id });
|
|
|
|
return res.json({ success: true, message: "삭제 완료" });
|
|
} catch (error: any) {
|
|
logger.error("입고 삭제 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// 구매입고용: 발주 데이터 조회 (미입고분) - 신규 헤더-디테일 구조 + 레거시 단일 테이블 UNION ALL
|
|
export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { keyword, page, pageSize } = req.query;
|
|
const currentPage = Math.max(1, Number(page) || 1);
|
|
const limit = Math.min(500, Math.max(1, Number(pageSize) || 20));
|
|
const offset = (currentPage - 1) * limit;
|
|
|
|
const params: any[] = [companyCode];
|
|
let paramIdx = 2;
|
|
|
|
let keywordConditionDetail = "";
|
|
let keywordConditionLegacy = "";
|
|
if (keyword) {
|
|
keywordConditionDetail = `AND (pd.purchase_no ILIKE $${paramIdx} OR COALESCE(NULLIF(pd.item_name, ''), ii.item_name) ILIKE $${paramIdx} OR COALESCE(NULLIF(pd.item_code, ''), ii.item_number) ILIKE $${paramIdx} OR COALESCE(pd.supplier_name, po.supplier_name) ILIKE $${paramIdx})`;
|
|
keywordConditionLegacy = `AND (po.purchase_no ILIKE $${paramIdx} OR po.item_name ILIKE $${paramIdx} OR po.item_code ILIKE $${paramIdx} OR po.supplier_name ILIKE $${paramIdx})`;
|
|
params.push(`%${keyword}%`);
|
|
paramIdx++;
|
|
}
|
|
|
|
const baseQuery = `
|
|
WITH detail_received AS (
|
|
SELECT source_id, SUM(COALESCE(inbound_qty, 0)) AS total_received
|
|
FROM inbound_mng
|
|
WHERE source_table = 'purchase_detail' AND company_code = $1
|
|
GROUP BY source_id
|
|
),
|
|
combined AS (
|
|
-- 디테일 기반 발주 데이터 (신규 헤더-디테일 구조, 헤더 없는 디테일도 포함)
|
|
SELECT
|
|
pd.id,
|
|
COALESCE(po.purchase_no, pd.purchase_no) AS purchase_no,
|
|
po.order_date,
|
|
COALESCE(pd.supplier_code, po.supplier_code) AS supplier_code,
|
|
COALESCE(pd.supplier_name, po.supplier_name) AS supplier_name,
|
|
COALESCE(NULLIF(pd.item_code, ''), ii.item_number) AS item_code,
|
|
COALESCE(NULLIF(pd.item_name, ''), ii.item_name) AS item_name,
|
|
COALESCE(NULLIF(pd.spec, ''), ii.size) AS spec,
|
|
COALESCE(NULLIF(pd.material, ''), ii.material) AS material,
|
|
COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) AS order_qty,
|
|
COALESCE(dr.total_received, 0) AS received_qty,
|
|
COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(dr.total_received, 0) AS remain_qty,
|
|
COALESCE(CAST(NULLIF(pd.unit_price, '') AS numeric), 0) AS unit_price,
|
|
COALESCE(po.status, '') AS status,
|
|
COALESCE(pd.due_date, po.due_date) AS due_date,
|
|
'purchase_detail' AS source_table
|
|
FROM purchase_detail pd
|
|
LEFT JOIN purchase_order_mng po
|
|
ON pd.purchase_no = po.purchase_no AND pd.company_code = po.company_code
|
|
LEFT JOIN item_info ii ON pd.item_id = ii.id
|
|
LEFT JOIN detail_received dr ON dr.source_id = pd.id
|
|
WHERE pd.company_code = $1
|
|
AND COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(dr.total_received, 0) > 0
|
|
AND COALESCE(pd.approval_status, '') NOT IN ('반려')
|
|
AND COALESCE(po.status, '') NOT IN ('입고완료', '취소')
|
|
${keywordConditionDetail}
|
|
|
|
UNION ALL
|
|
|
|
-- 레거시 단일 테이블 데이터 (purchase_detail에 없는 발주)
|
|
SELECT
|
|
po.id,
|
|
po.purchase_no,
|
|
po.order_date,
|
|
po.supplier_code,
|
|
po.supplier_name,
|
|
po.item_code,
|
|
po.item_name,
|
|
po.spec,
|
|
po.material,
|
|
COALESCE(CAST(NULLIF(po.order_qty, '') AS numeric), 0) AS order_qty,
|
|
COALESCE(CAST(NULLIF(po.received_qty, '') AS numeric), 0) AS received_qty,
|
|
COALESCE(CAST(NULLIF(po.remain_qty, '') AS numeric),
|
|
COALESCE(CAST(NULLIF(po.order_qty, '') AS numeric), 0)
|
|
- COALESCE(CAST(NULLIF(po.received_qty, '') AS numeric), 0)
|
|
) AS remain_qty,
|
|
COALESCE(CAST(NULLIF(po.unit_price, '') AS numeric), 0) AS unit_price,
|
|
po.status,
|
|
po.due_date,
|
|
'purchase_order_mng' AS source_table
|
|
FROM purchase_order_mng po
|
|
WHERE po.company_code = $1
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM purchase_detail pd
|
|
WHERE pd.purchase_no = po.purchase_no AND pd.company_code = po.company_code
|
|
)
|
|
AND COALESCE(CAST(NULLIF(po.remain_qty, '') AS numeric),
|
|
COALESCE(CAST(NULLIF(po.order_qty, '') AS numeric), 0)
|
|
- COALESCE(CAST(NULLIF(po.received_qty, '') AS numeric), 0)
|
|
) > 0
|
|
AND po.status NOT IN ('입고완료', '취소')
|
|
${keywordConditionLegacy}
|
|
)`;
|
|
|
|
const pool = getPool();
|
|
|
|
const countResult = await pool.query(
|
|
`${baseQuery} SELECT COUNT(*) AS total FROM combined`,
|
|
params
|
|
);
|
|
const totalCount = parseInt(countResult.rows[0].total, 10);
|
|
|
|
const dataResult = await pool.query(
|
|
`${baseQuery} SELECT * FROM combined ORDER BY order_date DESC, purchase_no LIMIT ${limit} OFFSET ${offset}`,
|
|
params
|
|
);
|
|
|
|
return res.json({ success: true, data: dataResult.rows, totalCount });
|
|
} catch (error: any) {
|
|
logger.error("발주 데이터 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// 반품입고용: 출하 데이터 조회
|
|
export async function getShipments(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { keyword, page, pageSize } = req.query;
|
|
const currentPage = Math.max(1, Number(page) || 1);
|
|
const limit = Math.min(500, Math.max(1, Number(pageSize) || 20));
|
|
const offset = (currentPage - 1) * limit;
|
|
|
|
const conditions: string[] = ["si.company_code = $1"];
|
|
const params: any[] = [companyCode];
|
|
let paramIdx = 2;
|
|
|
|
if (keyword) {
|
|
conditions.push(
|
|
`(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`
|
|
);
|
|
params.push(`%${keyword}%`);
|
|
paramIdx++;
|
|
}
|
|
|
|
const whereClause = conditions.join(" AND ");
|
|
const pool = getPool();
|
|
|
|
const countResult = await pool.query(
|
|
`SELECT COUNT(*) AS total
|
|
FROM shipment_instruction si
|
|
JOIN shipment_instruction_detail sid
|
|
ON si.id = sid.instruction_id AND si.company_code = sid.company_code
|
|
WHERE ${whereClause}`,
|
|
params
|
|
);
|
|
const totalCount = parseInt(countResult.rows[0].total, 10);
|
|
|
|
const dataResult = await pool.query(
|
|
`SELECT
|
|
sid.id AS detail_id,
|
|
si.id AS instruction_id,
|
|
si.instruction_no,
|
|
si.instruction_date,
|
|
si.partner_id,
|
|
si.status AS instruction_status,
|
|
sid.item_code,
|
|
sid.item_name,
|
|
sid.spec,
|
|
sid.material,
|
|
COALESCE(sid.ship_qty, 0) AS ship_qty,
|
|
COALESCE(sid.order_qty, 0) AS order_qty,
|
|
sid.source_type
|
|
FROM shipment_instruction si
|
|
JOIN shipment_instruction_detail sid
|
|
ON si.id = sid.instruction_id
|
|
AND si.company_code = sid.company_code
|
|
WHERE ${whereClause}
|
|
ORDER BY si.instruction_date DESC, si.instruction_no
|
|
LIMIT ${limit} OFFSET ${offset}`,
|
|
params
|
|
);
|
|
|
|
return res.json({ success: true, data: dataResult.rows, totalCount });
|
|
} catch (error: any) {
|
|
logger.error("출하 데이터 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// 기타입고용: 품목 데이터 조회
|
|
export async function getItems(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { keyword, page, pageSize } = req.query;
|
|
const currentPage = Math.max(1, Number(page) || 1);
|
|
const limit = Math.min(500, Math.max(1, Number(pageSize) || 20));
|
|
const offset = (currentPage - 1) * limit;
|
|
|
|
const conditions: string[] = ["company_code = $1"];
|
|
const params: any[] = [companyCode];
|
|
let paramIdx = 2;
|
|
|
|
if (keyword) {
|
|
conditions.push(
|
|
`(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`
|
|
);
|
|
params.push(`%${keyword}%`);
|
|
paramIdx++;
|
|
}
|
|
|
|
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, 10);
|
|
|
|
const dataResult = await pool.query(
|
|
`SELECT
|
|
id, item_number, item_name, size AS spec, material, unit,
|
|
COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price
|
|
FROM item_info
|
|
WHERE ${whereClause}
|
|
ORDER BY item_name
|
|
LIMIT ${limit} OFFSET ${offset}`,
|
|
params
|
|
);
|
|
|
|
return res.json({ success: true, data: dataResult.rows, totalCount });
|
|
} catch (error: any) {
|
|
logger.error("품목 데이터 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// 입고번호 자동생성
|
|
export async function generateNumber(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const pool = getPool();
|
|
|
|
const today = new Date();
|
|
const yyyy = today.getFullYear();
|
|
const prefix = `RCV-${yyyy}-`;
|
|
|
|
const result = await pool.query(
|
|
`SELECT inbound_number FROM inbound_mng
|
|
WHERE company_code = $1 AND inbound_number LIKE $2
|
|
ORDER BY inbound_number DESC LIMIT 1`,
|
|
[companyCode, `${prefix}%`]
|
|
);
|
|
|
|
let seq = 1;
|
|
if (result.rows.length > 0) {
|
|
const lastNo = result.rows[0].inbound_number;
|
|
const lastSeq = parseInt(lastNo.replace(prefix, ""), 10);
|
|
if (!isNaN(lastSeq)) seq = lastSeq + 1;
|
|
}
|
|
|
|
const newNumber = `${prefix}${String(seq).padStart(4, "0")}`;
|
|
|
|
return res.json({ success: true, data: newNumber });
|
|
} catch (error: any) {
|
|
logger.error("입고번호 생성 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// 창고 목록 조회
|
|
export async function getWarehouses(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const pool = getPool();
|
|
|
|
const result = await pool.query(
|
|
`SELECT warehouse_code, warehouse_name, warehouse_type
|
|
FROM warehouse_info
|
|
WHERE company_code = $1 AND status != '삭제'
|
|
ORDER BY warehouse_name`,
|
|
[companyCode]
|
|
);
|
|
|
|
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 });
|
|
}
|
|
}
|