feat: POP 시연 준비 — 5개 화면 + 버그 수정 + 자동 창고 매칭
Some checks failed
Build and Push Images / build-and-push (push) Failing after 49s

- 구매입고: 검사기준 API 수정, 검사결과 DB 저장, 검사 미완료 확정 차단
- 판매출고: 재고 부족 사전 검증, 수주상세 ship_qty 반영, 에러 메시지 개선
- 공정실행: seq_no 비순차 대응(3곳), 자재투입 자동 창고 매칭 재고차감, 불필요 버튼 제거
- 검사관리+입출고관리: 신규 화면 (quality, inventory)
- 공통: ConfirmModal 커스텀 모달 (native confirm 대체)
This commit is contained in:
SeongHyun Kim
2026-04-09 14:38:28 +09:00
parent 1b62dae277
commit 327b4d01c2
25 changed files with 15182 additions and 12185 deletions

View File

@@ -7,70 +7,70 @@
* - 기타출고 → item_info (품목)
*/
import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import type { Response } from "express";
import { getPool } from "../database/db";
import type { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
// 출고 목록 조회
export async function getList(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const {
outbound_type,
outbound_status,
search_keyword,
date_from,
date_to,
} = req.query;
try {
const companyCode = req.user!.companyCode;
const {
outbound_type,
outbound_status,
search_keyword,
date_from,
date_to,
} = req.query;
const conditions: string[] = [];
const params: any[] = [];
let paramIdx = 1;
const conditions: string[] = [];
const params: any[] = [];
let paramIdx = 1;
if (companyCode === "*") {
// 최고 관리자: 전체 조회
} else {
conditions.push(`om.company_code = $${paramIdx}`);
params.push(companyCode);
paramIdx++;
}
if (companyCode === "*") {
// 최고 관리자: 전체 조회
} else {
conditions.push(`om.company_code = $${paramIdx}`);
params.push(companyCode);
paramIdx++;
}
if (outbound_type && outbound_type !== "all") {
conditions.push(`om.outbound_type = $${paramIdx}`);
params.push(outbound_type);
paramIdx++;
}
if (outbound_type && outbound_type !== "all") {
conditions.push(`om.outbound_type = $${paramIdx}`);
params.push(outbound_type);
paramIdx++;
}
if (outbound_status && outbound_status !== "all") {
conditions.push(`om.outbound_status = $${paramIdx}`);
params.push(outbound_status);
paramIdx++;
}
if (outbound_status && outbound_status !== "all") {
conditions.push(`om.outbound_status = $${paramIdx}`);
params.push(outbound_status);
paramIdx++;
}
if (search_keyword) {
conditions.push(
`(om.outbound_number ILIKE $${paramIdx} OR om.item_name ILIKE $${paramIdx} OR om.item_code ILIKE $${paramIdx} OR om.customer_name ILIKE $${paramIdx} OR om.reference_number ILIKE $${paramIdx})`
);
params.push(`%${search_keyword}%`);
paramIdx++;
}
if (search_keyword) {
conditions.push(
`(om.outbound_number ILIKE $${paramIdx} OR om.item_name ILIKE $${paramIdx} OR om.item_code ILIKE $${paramIdx} OR om.customer_name ILIKE $${paramIdx} OR om.reference_number ILIKE $${paramIdx})`,
);
params.push(`%${search_keyword}%`);
paramIdx++;
}
if (date_from) {
conditions.push(`om.outbound_date >= $${paramIdx}`);
params.push(date_from);
paramIdx++;
}
if (date_to) {
conditions.push(`om.outbound_date <= $${paramIdx}`);
params.push(date_to);
paramIdx++;
}
if (date_from) {
conditions.push(`om.outbound_date >= $${paramIdx}`);
params.push(date_from);
paramIdx++;
}
if (date_to) {
conditions.push(`om.outbound_date <= $${paramIdx}`);
params.push(date_to);
paramIdx++;
}
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const query = `
const query = `
SELECT
om.*,
wh.warehouse_name
@@ -82,42 +82,52 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
ORDER BY om.created_date DESC
`;
const pool = getPool();
const result = await pool.query(query, params);
const pool = getPool();
const result = await pool.query(query, params);
logger.info("출고 목록 조회", {
companyCode,
rowCount: result.rowCount,
});
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 });
}
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();
const pool = getPool();
const client = await pool.connect();
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const { items, outbound_number, outbound_date, warehouse_code, location_code, manager_id, memo } = req.body;
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const {
items,
outbound_number,
outbound_date,
warehouse_code,
location_code,
manager_id,
memo,
} = req.body;
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, message: "출고 품목이 없습니다." });
}
if (!items || !Array.isArray(items) || items.length === 0) {
return res
.status(400)
.json({ success: false, message: "출고 품목이 없습니다." });
}
await client.query("BEGIN");
await client.query("BEGIN");
const insertedRows: any[] = [];
const insertedRows: any[] = [];
for (const item of items) {
const result = await client.query(
`INSERT INTO outbound_mng (
for (const item of items) {
const result = await client.query(
`INSERT INTO outbound_mng (
id, company_code, outbound_number, outbound_type, outbound_date,
reference_number, customer_code, customer_name,
item_code, item_name, specification, material, unit,
@@ -138,182 +148,202 @@ export async function create(req: AuthenticatedRequest, res: Response) {
$26, $27, $28,
NOW(), $29, $29, '출고'
) RETURNING *`,
[
companyCode,
outbound_number || item.outbound_number,
item.outbound_type,
outbound_date || item.outbound_date,
item.reference_number || null,
item.customer_code || null,
item.customer_name || null,
item.item_code || item.item_number || null,
item.item_name || null,
item.spec || item.specification || null,
item.material || null,
item.unit || "EA",
item.outbound_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.outbound_status || "대기",
manager_id || item.manager_id || null,
memo || item.memo || null,
item.source_type || null,
item.sales_order_id || null,
item.shipment_plan_id || null,
item.item_info_id || null,
item.destination_code || null,
item.delivery_destination || null,
item.delivery_address || null,
userId,
]
);
[
companyCode,
outbound_number || item.outbound_number,
item.outbound_type,
outbound_date || item.outbound_date,
item.reference_number || null,
item.customer_code || null,
item.customer_name || null,
item.item_code || item.item_number || null,
item.item_name || null,
item.spec || item.specification || null,
item.material || null,
item.unit || "EA",
item.outbound_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.outbound_status || "대기",
manager_id || item.manager_id || null,
memo || item.memo || null,
item.source_type || null,
item.sales_order_id || null,
item.shipment_plan_id || null,
item.item_info_id || null,
item.destination_code || null,
item.delivery_destination || null,
item.delivery_address || null,
userId,
],
);
insertedRows.push(result.rows[0]);
insertedRows.push(result.rows[0]);
// 재고 업데이트 (inventory_stock): 출고 수량 차감
const itemCode = item.item_code || item.item_number || null;
const whCode = warehouse_code || item.warehouse_code || null;
const locCode = location_code || item.location_code || null;
const outQty = Number(item.outbound_qty) || 0;
if (itemCode && outQty > 0) {
// 재고 사전 검증: 부족 시 즉시 에러 (트랜잭션 ROLLBACK)
const stockCheck = await client.query(
`SELECT COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) as cur
// 재고 업데이트 (inventory_stock): 출고 수량 차감
const itemCode = item.item_code || item.item_number || null;
const whCode = warehouse_code || item.warehouse_code || null;
const locCode = location_code || item.location_code || null;
const outQty = Number(item.outbound_qty) || 0;
if (itemCode && outQty > 0) {
// 재고 사전 검증: 부족 시 즉시 에러 (트랜잭션 ROLLBACK)
const stockCheck = await client.query(
`SELECT COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) as cur
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 || '']
);
const currentStock = parseFloat(stockCheck.rows[0]?.cur || '0');
if (currentStock < outQty) {
throw new Error(
`재고 부족: ${item.item_name || itemCode} (창고 ${whCode || '미지정'}) — 현재 재고 ${currentStock}, 요청 출고 ${outQty}`
);
}
[companyCode, itemCode, whCode || "", locCode || ""],
);
const currentStock = parseFloat(stockCheck.rows[0]?.cur || "0");
if (currentStock < outQty) {
throw new Error(
`재고 부족: ${item.item_name || itemCode} (창고 ${whCode || "미지정"}) — 현재 재고 ${currentStock}, 요청 출고 ${outQty}`,
);
}
const existingStock = await client.query(
`SELECT id FROM inventory_stock
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 || '']
);
[companyCode, itemCode, whCode || "", locCode || ""],
);
if (existingStock.rows.length > 0) {
await client.query(
`UPDATE inventory_stock
if (existingStock.rows.length > 0) {
await client.query(
`UPDATE inventory_stock
SET current_qty = CAST(GREATEST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) - $1, 0) AS text),
last_out_date = NOW(),
updated_date = NOW()
WHERE id = $2`,
[outQty, existingStock.rows[0].id]
);
} else {
// 재고 레코드가 없으면 0으로 생성 (마이너스 방지)
await client.query(
`INSERT INTO inventory_stock (
[outQty, existingStock.rows[0].id],
);
} else {
// 재고 레코드가 없으면 0으로 생성 (마이너스 방지)
await client.query(
`INSERT INTO inventory_stock (
id, company_code, item_code, warehouse_code, location_code,
current_qty, safety_qty, last_out_date,
created_date, updated_date, writer
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '0', '0', NOW(), NOW(), NOW(), $5)`,
[companyCode, itemCode, whCode, locCode, userId]
);
}
[companyCode, itemCode, whCode, locCode, userId],
);
}
// 재고 이력 기록 (inventory_history)
const afterStockRes = await client.query(
`SELECT current_qty FROM inventory_stock
// 재고 이력 기록 (inventory_history)
const afterStockRes = await client.query(
`SELECT current_qty 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 || '']
);
const afterQty = afterStockRes.rows[0]?.current_qty || '0';
await client.query(
`INSERT INTO inventory_history (
[companyCode, itemCode, whCode || "", locCode || ""],
);
const afterQty = afterStockRes.rows[0]?.current_qty || "0";
await client.query(
`INSERT INTO inventory_history (
id, company_code, item_code, warehouse_code, location_code,
transaction_type, transaction_date, quantity, balance_qty, remark,
writer, created_date
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '출고', NOW(), $5, $6, $7, $8, NOW())`,
[companyCode, itemCode, whCode, locCode, String(-outQty), afterQty, item.outbound_type || '출고', userId]
);
}
[
companyCode,
itemCode,
whCode,
locCode,
String(-outQty),
afterQty,
item.outbound_type || "출고",
userId,
],
);
}
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
if (item.outbound_type === "판매출고" && item.source_id && item.source_type === "shipment_instruction_detail") {
const outQtyNum = Number(item.outbound_qty) || 0;
await client.query(
`UPDATE shipment_instruction_detail
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
if (
item.outbound_type === "판매출고" &&
item.source_id &&
item.source_type === "shipment_instruction_detail"
) {
const outQtyNum = Number(item.outbound_qty) || 0;
await client.query(
`UPDATE shipment_instruction_detail
SET ship_qty = COALESCE(ship_qty, 0) + $1,
updated_date = NOW()
WHERE id = $2 AND company_code = $3`,
[outQtyNum, item.source_id, companyCode]
);
[outQtyNum, item.source_id, companyCode],
);
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
const sidRes = await client.query(
`SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`,
[item.source_id, companyCode]
);
const detailId = sidRes.rows[0]?.detail_id;
if (detailId) {
await client.query(
`UPDATE sales_order_detail
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
const sidRes = await client.query(
`SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`,
[item.source_id, companyCode],
);
const detailId = sidRes.rows[0]?.detail_id;
if (detailId) {
await client.query(
`UPDATE sales_order_detail
SET ship_qty = (COALESCE(NULLIF(ship_qty,'')::numeric, 0) + $1)::text,
balance_qty = (COALESCE(NULLIF(qty,'')::numeric, 0) - COALESCE(NULLIF(ship_qty,'')::numeric, 0) - $1)::text,
updated_date = NOW()
WHERE id = $2 AND company_code = $3`,
[outQtyNum, detailId, companyCode]
);
}
}
}
[outQtyNum, detailId, companyCode],
);
}
}
}
await client.query("COMMIT");
await client.query("COMMIT");
logger.info("출고 등록 완료", {
companyCode,
userId,
count: insertedRows.length,
outbound_number,
});
logger.info("출고 등록 완료", {
companyCode,
userId,
count: insertedRows.length,
outbound_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();
}
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 {
outbound_date, outbound_qty, unit_price, total_amount,
lot_number, warehouse_code, location_code,
outbound_status, manager_id: mgr, memo,
} = req.body;
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const { id } = req.params;
const {
outbound_date,
outbound_qty,
unit_price,
total_amount,
lot_number,
warehouse_code,
location_code,
outbound_status,
manager_id: mgr,
memo,
} = req.body;
const pool = getPool();
const result = await pool.query(
`UPDATE outbound_mng SET
const pool = getPool();
const result = await pool.query(
`UPDATE outbound_mng SET
outbound_date = COALESCE($1, outbound_date),
outbound_qty = COALESCE($2, outbound_qty),
unit_price = COALESCE($3, unit_price),
@@ -328,73 +358,89 @@ export async function update(req: AuthenticatedRequest, res: Response) {
updated_by = $11
WHERE id = $12 AND company_code = $13
RETURNING *`,
[
outbound_date, outbound_qty, unit_price, total_amount,
lot_number, warehouse_code, location_code,
outbound_status, mgr, memo,
userId, id, companyCode,
]
);
[
outbound_date,
outbound_qty,
unit_price,
total_amount,
lot_number,
warehouse_code,
location_code,
outbound_status,
mgr,
memo,
userId,
id,
companyCode,
],
);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: "출고 데이터를 찾을 수 없습니다." });
}
if (result.rowCount === 0) {
return res
.status(404)
.json({ success: false, message: "출고 데이터를 찾을 수 없습니다." });
}
logger.info("출고 수정", { companyCode, userId, id });
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 });
}
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 deleteOutbound(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
const pool = getPool();
try {
const companyCode = req.user!.companyCode;
const { id } = req.params;
const pool = getPool();
const result = await pool.query(
`DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
[id, companyCode]
);
const result = await pool.query(
`DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
[id, companyCode],
);
if (result.rowCount === 0) {
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
}
if (result.rowCount === 0) {
return res
.status(404)
.json({ success: false, message: "데이터를 찾을 수 없습니다." });
}
logger.info("출고 삭제", { companyCode, id });
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 });
}
return res.json({ success: true, message: "삭제 완료" });
} catch (error: any) {
logger.error("출고 삭제 실패", { error: error.message });
return res.status(500).json({ success: false, message: error.message });
}
}
// 판매출고용: 출하지시 데이터 조회
export async function getShipmentInstructions(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
export async function getShipmentInstructions(
req: AuthenticatedRequest,
res: Response,
) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
const conditions: string[] = ["si.company_code = $1"];
const params: any[] = [companyCode];
let paramIdx = 2;
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++;
}
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 pool = getPool();
const result = await pool.query(
`SELECT
const pool = getPool();
const result = await pool.query(
`SELECT
sid.id AS detail_id,
si.id AS instruction_id,
si.instruction_no,
@@ -417,42 +463,45 @@ export async function getShipmentInstructions(req: AuthenticatedRequest, res: Re
WHERE ${conditions.join(" AND ")}
AND COALESCE(sid.plan_qty, 0) > COALESCE(sid.ship_qty, 0)
ORDER BY si.instruction_date DESC, si.instruction_no`,
params
);
params,
);
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 });
}
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 getPurchaseOrders(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
export async function getPurchaseOrders(
req: AuthenticatedRequest,
res: Response,
) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
const conditions: string[] = ["company_code = $1"];
const params: any[] = [companyCode];
let paramIdx = 2;
const conditions: string[] = ["company_code = $1"];
const params: any[] = [companyCode];
let paramIdx = 2;
// 입고된 것만 (반품 대상)
conditions.push(
`COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0`
);
// 입고된 것만 (반품 대상)
conditions.push(
`COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0`,
);
if (keyword) {
conditions.push(
`(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`
);
params.push(`%${keyword}%`);
paramIdx++;
}
if (keyword) {
conditions.push(
`(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`,
);
params.push(`%${keyword}%`);
paramIdx++;
}
const pool = getPool();
const result = await pool.query(
`SELECT
const pool = getPool();
const result = await pool.query(
`SELECT
id, purchase_no, order_date, supplier_code, supplier_name,
item_code, item_name, spec, material,
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) AS order_qty,
@@ -462,137 +511,146 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
FROM purchase_order_mng
WHERE ${conditions.join(" AND ")}
ORDER BY order_date DESC, purchase_no`,
params
);
params,
);
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 });
}
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 getItems(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
const conditions: string[] = ["company_code = $1"];
const params: any[] = [companyCode];
let paramIdx = 2;
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++;
}
if (keyword) {
conditions.push(
`(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`,
);
params.push(`%${keyword}%`);
paramIdx++;
}
const pool = getPool();
const result = await pool.query(
`SELECT
const pool = getPool();
const result = 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 ${conditions.join(" AND ")}
ORDER BY item_name`,
params
);
params,
);
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 });
}
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 generateNumber(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const ruleId = (req.query.ruleId as string) || (req.query.rule_id as string);
try {
const companyCode = req.user!.companyCode;
const ruleId =
(req.query.ruleId as string) || (req.query.rule_id as string);
// 1순위: POP 화면설정에서 선택한 채번규칙 사용
if (ruleId && ruleId !== "__none__") {
try {
const { numberingRuleService } = await import("../services/numberingRuleService");
const newNumber = await numberingRuleService.allocateCode(ruleId, companyCode);
return res.json({ success: true, data: newNumber });
} catch (e: any) {
logger.warn("선택한 채번규칙 사용 실패, 기본 채번으로 폴백", { ruleId, error: e.message });
}
}
// 1순위: POP 화면설정에서 선택한 채번규칙 사용
if (ruleId && ruleId !== "__none__") {
try {
const { numberingRuleService } = await import(
"../services/numberingRuleService"
);
const newNumber = await numberingRuleService.allocateCode(
ruleId,
companyCode,
);
return res.json({ success: true, data: newNumber });
} catch (e: any) {
logger.warn("선택한 채번규칙 사용 실패, 기본 채번으로 폴백", {
ruleId,
error: e.message,
});
}
}
// 2순위: 기본 하드코딩 채번 (OUT-YYYY-XXXX)
const pool = getPool();
const today = new Date();
const yyyy = today.getFullYear();
const prefix = `OUT-${yyyy}-`;
// 2순위: 기본 하드코딩 채번 (OUT-YYYY-XXXX)
const pool = getPool();
const today = new Date();
const yyyy = today.getFullYear();
const prefix = `OUT-${yyyy}-`;
const result = await pool.query(
`SELECT outbound_number FROM outbound_mng
const result = await pool.query(
`SELECT outbound_number FROM outbound_mng
WHERE company_code = $1 AND outbound_number LIKE $2
ORDER BY outbound_number DESC LIMIT 1`,
[companyCode, `${prefix}%`]
);
[companyCode, `${prefix}%`],
);
let seq = 1;
if (result.rows.length > 0) {
const lastNo = result.rows[0].outbound_number;
const lastSeq = parseInt(lastNo.replace(prefix, ""), 10);
if (!isNaN(lastSeq)) seq = lastSeq + 1;
}
let seq = 1;
if (result.rows.length > 0) {
const lastNo = result.rows[0].outbound_number;
const lastSeq = parseInt(lastNo.replace(prefix, ""), 10);
if (!isNaN(lastSeq)) seq = lastSeq + 1;
}
const newNumber = `${prefix}${String(seq).padStart(4, "0")}`;
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 });
}
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();
try {
const companyCode = req.user!.companyCode;
const pool = getPool();
const result = await pool.query(
`SELECT warehouse_code, warehouse_name, warehouse_type
const result = await pool.query(
`SELECT warehouse_code, warehouse_name, warehouse_type
FROM warehouse_info
WHERE company_code = $1 AND COALESCE(status, '') != '삭제'
ORDER BY warehouse_name`,
[companyCode]
);
[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 });
}
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 getLocations(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const warehouseCode = req.query.warehouse_code as string;
const pool = getPool();
try {
const companyCode = req.user!.companyCode;
const warehouseCode = req.query.warehouse_code as string;
const pool = getPool();
const result = await pool.query(
`SELECT location_code, location_name, warehouse_code
const result = await pool.query(
`SELECT location_code, location_name, warehouse_code
FROM warehouse_location
WHERE company_code = $1 ${warehouseCode ? "AND warehouse_code = $2" : ""}
ORDER BY location_code`,
warehouseCode ? [companyCode, warehouseCode] : [companyCode]
);
warehouseCode ? [companyCode, warehouseCode] : [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 });
}
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 });
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff