Enhance backend controllers, frontend pages, and V2 components

- 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>
This commit is contained in:
kmh
2026-03-30 11:52:03 +09:00
parent 348da95823
commit b97ca1a1c5
23 changed files with 1012 additions and 365 deletions

View File

@@ -2,7 +2,7 @@
* 입고관리 컨트롤러
*
* 입고유형별 소스 테이블:
* - 구매입고 → purchase_order_mng (발주)
* - 구매입고 → purchase_order_mng (발주 헤더) + purchase_detail (발주 디테일)
* - 반품입고 → shipment_instruction + shipment_instruction_detail (출하)
* - 기타입고 → item_info (품목)
*/
@@ -228,6 +228,39 @@ export async function create(req: AuthenticatedRequest, res: Response) {
[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");
@@ -332,50 +365,115 @@ export async function deleteReceiving(req: AuthenticatedRequest, res: Response)
}
}
// 구매입고용: 발주 데이터 조회 (미입고분)
// 구매입고용: 발주 데이터 조회 (미입고분) - 신규 헤더-디테일 구조 + 레거시 단일 테이블 UNION ALL
export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
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;
// 잔량이 있는 것만 조회
conditions.push(
`COALESCE(CAST(NULLIF(remain_qty, '') AS numeric), COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0)) > 0`
);
conditions.push(`status NOT IN ('입고완료', '취소')`);
let keywordConditionDetail = "";
let keywordConditionLegacy = "";
if (keyword) {
conditions.push(
`(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`
);
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 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,
COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) AS received_qty,
COALESCE(CAST(NULLIF(remain_qty, '') AS numeric),
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
- COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0)
) AS remain_qty,
COALESCE(CAST(NULLIF(unit_price, '') AS numeric), 0) AS unit_price,
status, due_date
FROM purchase_order_mng
WHERE ${conditions.join(" AND ")}
ORDER BY order_date DESC, purchase_no`,
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: result.rows });
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 });
@@ -386,7 +484,10 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
export async function getShipments(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
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];
@@ -400,8 +501,20 @@ export async function getShipments(req: AuthenticatedRequest, res: Response) {
paramIdx++;
}
const whereClause = conditions.join(" AND ");
const pool = getPool();
const result = await pool.query(
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,
@@ -420,12 +533,13 @@ export async function getShipments(req: AuthenticatedRequest, res: Response) {
JOIN shipment_instruction_detail sid
ON si.id = sid.instruction_id
AND si.company_code = sid.company_code
WHERE ${conditions.join(" AND ")}
ORDER BY si.instruction_date DESC, si.instruction_no`,
WHERE ${whereClause}
ORDER BY si.instruction_date DESC, si.instruction_no
LIMIT ${limit} OFFSET ${offset}`,
params
);
return res.json({ success: true, data: result.rows });
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 });
@@ -436,7 +550,10 @@ export async function getShipments(req: AuthenticatedRequest, res: Response) {
export async function getItems(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { keyword } = req.query;
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];
@@ -450,18 +567,27 @@ export async function getItems(req: AuthenticatedRequest, res: Response) {
paramIdx++;
}
const whereClause = conditions.join(" AND ");
const pool = getPool();
const result = await pool.query(
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 ${conditions.join(" AND ")}
ORDER BY item_name`,
WHERE ${whereClause}
ORDER BY item_name
LIMIT ${limit} OFFSET ${offset}`,
params
);
return res.json({ success: true, data: result.rows });
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 });