jskim-node #423
|
|
@ -150,6 +150,7 @@ import salesReportRoutes from "./routes/salesReportRoutes"; // 영업 리포트
|
||||||
import analyticsReportRoutes from "./routes/analyticsReportRoutes"; // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
import analyticsReportRoutes from "./routes/analyticsReportRoutes"; // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
||||||
import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프로젝트/ECN)
|
import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프로젝트/ECN)
|
||||||
import materialStatusRoutes from "./routes/materialStatusRoutes"; // 자재현황
|
import materialStatusRoutes from "./routes/materialStatusRoutes"; // 자재현황
|
||||||
|
import receivingRoutes from "./routes/receivingRoutes"; // 입고관리
|
||||||
import processInfoRoutes from "./routes/processInfoRoutes"; // 공정정보관리
|
import processInfoRoutes from "./routes/processInfoRoutes"; // 공정정보관리
|
||||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||||
|
|
@ -351,6 +352,7 @@ app.use("/api/work-instruction", workInstructionRoutes); // 작업지시 관리
|
||||||
app.use("/api/sales-report", salesReportRoutes); // 영업 리포트
|
app.use("/api/sales-report", salesReportRoutes); // 영업 리포트
|
||||||
app.use("/api/report", analyticsReportRoutes); // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
app.use("/api/report", analyticsReportRoutes); // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
||||||
app.use("/api/design", designRoutes); // 설계 모듈
|
app.use("/api/design", designRoutes); // 설계 모듈
|
||||||
|
app.use("/api/receiving", receivingRoutes); // 입고관리
|
||||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,487 @@
|
||||||
|
/**
|
||||||
|
* 입고관리 컨트롤러
|
||||||
|
*
|
||||||
|
* 입고유형별 소스 테이블:
|
||||||
|
* - 구매입고 → purchase_order_mng (발주)
|
||||||
|
* - 반품입고 → 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]);
|
||||||
|
|
||||||
|
// 구매입고인 경우 발주의 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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 구매입고용: 발주 데이터 조회 (미입고분)
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 잔량이 있는 것만 조회
|
||||||
|
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 ('입고완료', '취소')`);
|
||||||
|
|
||||||
|
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
|
||||||
|
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`,
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 반품입고용: 출하 데이터 조회
|
||||||
|
export async function getShipments(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;
|
||||||
|
|
||||||
|
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
|
||||||
|
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 ${conditions.join(" AND ")}
|
||||||
|
ORDER BY si.instruction_date DESC, si.instruction_no`,
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기타입고용: 품목 데이터 조회
|
||||||
|
export async function getItems(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;
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* 입고관리 라우트
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router } from "express";
|
||||||
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
import * as receivingController from "../controllers/receivingController";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(authenticateToken);
|
||||||
|
|
||||||
|
// 입고 목록 조회
|
||||||
|
router.get("/list", receivingController.getList);
|
||||||
|
|
||||||
|
// 입고번호 자동생성
|
||||||
|
router.get("/generate-number", receivingController.generateNumber);
|
||||||
|
|
||||||
|
// 창고 목록 조회
|
||||||
|
router.get("/warehouses", receivingController.getWarehouses);
|
||||||
|
|
||||||
|
// 소스 데이터: 발주 (구매입고)
|
||||||
|
router.get("/source/purchase-orders", receivingController.getPurchaseOrders);
|
||||||
|
|
||||||
|
// 소스 데이터: 출하 (반품입고)
|
||||||
|
router.get("/source/shipments", receivingController.getShipments);
|
||||||
|
|
||||||
|
// 소스 데이터: 품목 (기타입고)
|
||||||
|
router.get("/source/items", receivingController.getItems);
|
||||||
|
|
||||||
|
// 입고 등록
|
||||||
|
router.post("/", receivingController.create);
|
||||||
|
|
||||||
|
// 입고 수정
|
||||||
|
router.put("/:id", receivingController.update);
|
||||||
|
|
||||||
|
// 입고 삭제
|
||||||
|
router.delete("/:id", receivingController.deleteReceiving);
|
||||||
|
|
||||||
|
export default router;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -212,21 +212,24 @@ export default function ClaimManagementPage() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 거래처 목록 조회
|
// 거래처 목록 조회
|
||||||
const fetchCustomers = useCallback(async () => {
|
const fetchCustomers = useCallback(async (force = false) => {
|
||||||
if (customers.length > 0) return;
|
if (!force && customers.length > 0) return;
|
||||||
setCustomersLoading(true);
|
setCustomersLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.post("/table-management/tables/customer_mng/data", {
|
const res = await apiClient.post("/table-management/tables/customer_mng/data", {
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 9999,
|
size: 9999,
|
||||||
autoFilter: true,
|
autoFilter: { enabled: true },
|
||||||
});
|
});
|
||||||
if (res.data?.success && res.data?.data?.rows) {
|
const rows = res.data?.data?.data || res.data?.data?.rows;
|
||||||
const list: CustomerOption[] = res.data.data.rows.map((row: any) => ({
|
if (res.data?.success && Array.isArray(rows)) {
|
||||||
customerCode: row.customer_code || "",
|
const list: CustomerOption[] = rows.map((row: any) => ({
|
||||||
|
customerCode: row.customer_code || row.id || "",
|
||||||
customerName: row.customer_name || "",
|
customerName: row.customer_name || "",
|
||||||
}));
|
}));
|
||||||
setCustomers(list);
|
setCustomers(list);
|
||||||
|
} else {
|
||||||
|
console.warn("거래처 응답 구조 확인:", JSON.stringify(res.data, null, 2));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("거래처 목록 조회 실패:", e);
|
console.error("거래처 목록 조회 실패:", e);
|
||||||
|
|
@ -236,19 +239,20 @@ export default function ClaimManagementPage() {
|
||||||
}, [customers.length]);
|
}, [customers.length]);
|
||||||
|
|
||||||
// 수주 목록 조회
|
// 수주 목록 조회
|
||||||
const fetchSalesOrders = useCallback(async () => {
|
const fetchSalesOrders = useCallback(async (force = false) => {
|
||||||
if (salesOrders.length > 0) return;
|
if (!force && salesOrders.length > 0) return;
|
||||||
setOrdersLoading(true);
|
setOrdersLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.post("/table-management/tables/sales_order_mng/data", {
|
const res = await apiClient.post("/table-management/tables/sales_order_mng/data", {
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 9999,
|
size: 9999,
|
||||||
autoFilter: true,
|
autoFilter: { enabled: true },
|
||||||
});
|
});
|
||||||
if (res.data?.success && res.data?.data?.rows) {
|
const rows = res.data?.data?.data || res.data?.data?.rows;
|
||||||
|
if (res.data?.success && Array.isArray(rows)) {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const list: SalesOrderOption[] = [];
|
const list: SalesOrderOption[] = [];
|
||||||
for (const row of res.data.data.rows) {
|
for (const row of rows) {
|
||||||
const orderNo = row.order_no || "";
|
const orderNo = row.order_no || "";
|
||||||
if (!orderNo || seen.has(orderNo)) continue;
|
if (!orderNo || seen.has(orderNo)) continue;
|
||||||
seen.add(orderNo);
|
seen.add(orderNo);
|
||||||
|
|
@ -259,6 +263,8 @@ export default function ClaimManagementPage() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setSalesOrders(list);
|
setSalesOrders(list);
|
||||||
|
} else {
|
||||||
|
console.warn("수주 응답 구조 확인:", JSON.stringify(res.data, null, 2));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("수주 목록 조회 실패:", e);
|
console.error("수주 목록 조회 실패:", e);
|
||||||
|
|
@ -343,6 +349,8 @@ export default function ClaimManagementPage() {
|
||||||
processContent: "",
|
processContent: "",
|
||||||
});
|
});
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
|
fetchCustomers(true);
|
||||||
|
fetchSalesOrders(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditModal = (claimNo: string) => {
|
const openEditModal = (claimNo: string) => {
|
||||||
|
|
@ -351,6 +359,8 @@ export default function ClaimManagementPage() {
|
||||||
setIsEditMode(true);
|
setIsEditMode(true);
|
||||||
setFormData({ ...claim });
|
setFormData({ ...claim });
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
|
fetchCustomers(true);
|
||||||
|
fetchSalesOrders(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormChange = (field: keyof Claim, value: string) => {
|
const handleFormChange = (field: keyof Claim, value: string) => {
|
||||||
|
|
@ -893,7 +903,7 @@ export default function ClaimManagementPage() {
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={customerOpen}
|
aria-expanded={customerOpen}
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal"
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal"
|
||||||
onClick={() => fetchCustomers()}
|
onClick={() => fetchCustomers(false)}
|
||||||
>
|
>
|
||||||
{formData.customerName || "거래처 선택"}
|
{formData.customerName || "거래처 선택"}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
|
@ -985,7 +995,7 @@ export default function ClaimManagementPage() {
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={orderOpen}
|
aria-expanded={orderOpen}
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal"
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal"
|
||||||
onClick={() => fetchSalesOrders()}
|
onClick={() => fetchSalesOrders(false)}
|
||||||
>
|
>
|
||||||
{formData.orderNo || "수주번호 선택"}
|
{formData.orderNo || "수주번호 선택"}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,23 @@ export default function ShippingPlanPage() {
|
||||||
|
|
||||||
const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]);
|
const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]);
|
||||||
|
|
||||||
|
const groupedData = useMemo(() => {
|
||||||
|
const orderMap = new Map<string, ShipmentPlanListItem[]>();
|
||||||
|
const orderKeys: string[] = [];
|
||||||
|
data.forEach(plan => {
|
||||||
|
const key = plan.order_no || `_no_order_${plan.id}`;
|
||||||
|
if (!orderMap.has(key)) {
|
||||||
|
orderMap.set(key, []);
|
||||||
|
orderKeys.push(key);
|
||||||
|
}
|
||||||
|
orderMap.get(key)!.push(plan);
|
||||||
|
});
|
||||||
|
return orderKeys.map(key => ({
|
||||||
|
orderNo: key,
|
||||||
|
plans: orderMap.get(key)!,
|
||||||
|
}));
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const handleRowClick = (plan: ShipmentPlanListItem) => {
|
const handleRowClick = (plan: ShipmentPlanListItem) => {
|
||||||
if (isDetailChanged && selectedId !== plan.id) {
|
if (isDetailChanged && selectedId !== plan.id) {
|
||||||
if (!confirm("변경사항이 있습니다. 저장하지 않고 이동하시겠습니까?")) return;
|
if (!confirm("변경사항이 있습니다. 저장하지 않고 이동하시겠습니까?")) return;
|
||||||
|
|
@ -298,59 +315,73 @@ export default function ShippingPlanPage() {
|
||||||
onCheckedChange={handleCheckAll}
|
onCheckedChange={handleCheckAll}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[50px] text-center">No</TableHead>
|
<TableHead className="w-[160px]">수주번호</TableHead>
|
||||||
<TableHead className="w-[80px]">상태</TableHead>
|
<TableHead className="w-[100px] text-center">납기일</TableHead>
|
||||||
<TableHead className="w-[140px]">수주번호</TableHead>
|
|
||||||
<TableHead className="w-[120px]">거래처</TableHead>
|
<TableHead className="w-[120px]">거래처</TableHead>
|
||||||
<TableHead className="w-[100px]">품목코드</TableHead>
|
<TableHead className="w-[100px]">품목코드</TableHead>
|
||||||
<TableHead>품목명</TableHead>
|
<TableHead>품목명</TableHead>
|
||||||
<TableHead className="w-[80px] text-right">수주수량</TableHead>
|
<TableHead className="w-[80px] text-right">수주수량</TableHead>
|
||||||
<TableHead className="w-[80px] text-right">계획수량</TableHead>
|
<TableHead className="w-[80px] text-right">계획수량</TableHead>
|
||||||
<TableHead className="w-[100px] text-center">출하계획일</TableHead>
|
<TableHead className="w-[100px] text-center">출하계획일</TableHead>
|
||||||
<TableHead className="w-[100px] text-center">납기일</TableHead>
|
<TableHead className="w-[80px] text-center">상태</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.length === 0 ? (
|
{groupedData.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={11} className="h-32 text-center text-muted-foreground">
|
<TableCell colSpan={10} className="h-32 text-center text-muted-foreground">
|
||||||
출하계획이 없습니다.
|
출하계획이 없습니다.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
data.map((plan, idx) => (
|
groupedData.map((group) =>
|
||||||
|
group.plans.map((plan, planIdx) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={plan.id}
|
key={plan.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer hover:bg-muted/50 transition-colors",
|
"cursor-pointer hover:bg-muted/50 transition-colors",
|
||||||
selectedId === plan.id && "bg-primary/5",
|
selectedId === plan.id && "bg-primary/5",
|
||||||
plan.status === "CANCELLED" && "opacity-60 bg-slate-50"
|
plan.status === "CANCELLED" && "opacity-60 bg-slate-50",
|
||||||
|
planIdx === 0 && "border-t-2 border-t-border"
|
||||||
)}
|
)}
|
||||||
onClick={() => handleRowClick(plan)}
|
onClick={() => handleRowClick(plan)}
|
||||||
>
|
>
|
||||||
<TableCell className="text-center" onClick={e => e.stopPropagation()}>
|
<TableCell className="text-center" onClick={e => e.stopPropagation()}>
|
||||||
|
{planIdx === 0 && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={checkedIds.includes(plan.id)}
|
checked={group.plans.every(p => checkedIds.includes(p.id))}
|
||||||
onCheckedChange={(c) => handleCheck(plan.id, c as boolean)}
|
onCheckedChange={(c) => {
|
||||||
disabled={plan.status === "CANCELLED"}
|
if (c) {
|
||||||
|
setCheckedIds(prev => [...new Set([...prev, ...group.plans.filter(p => p.status !== "CANCELLED").map(p => p.id)])]);
|
||||||
|
} else {
|
||||||
|
setCheckedIds(prev => prev.filter(id => !group.plans.some(p => p.id === id)));
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{planIdx === 0 ? (plan.order_no || "-") : ""}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{planIdx === 0 ? formatDate(plan.due_date) : ""}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={cn("px-2 py-1 rounded-full text-[11px] font-medium border", getStatusColor(plan.status))}>
|
{planIdx === 0 ? (plan.customer_name || "-") : ""}
|
||||||
{getStatusLabel(plan.status)}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-medium">{plan.order_no || "-"}</TableCell>
|
|
||||||
<TableCell>{plan.customer_name || "-"}</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground text-xs">{plan.part_code || "-"}</TableCell>
|
<TableCell className="text-muted-foreground text-xs">{plan.part_code || "-"}</TableCell>
|
||||||
<TableCell className="font-medium">{plan.part_name || "-"}</TableCell>
|
<TableCell className="font-medium">{plan.part_name || "-"}</TableCell>
|
||||||
<TableCell className="text-right">{formatNumber(plan.order_qty)}</TableCell>
|
<TableCell className="text-right">{formatNumber(plan.order_qty)}</TableCell>
|
||||||
<TableCell className="text-right font-semibold text-primary">{formatNumber(plan.plan_qty)}</TableCell>
|
<TableCell className="text-right font-semibold text-primary">{formatNumber(plan.plan_qty)}</TableCell>
|
||||||
<TableCell className="text-center">{formatDate(plan.plan_date)}</TableCell>
|
<TableCell className="text-center">{formatDate(plan.plan_date)}</TableCell>
|
||||||
<TableCell className="text-center">{formatDate(plan.due_date)}</TableCell>
|
<TableCell className="text-center">
|
||||||
|
<span className={cn("px-2 py-1 rounded-full text-[11px] font-medium border", getStatusColor(plan.status))}>
|
||||||
|
{getStatusLabel(plan.status)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
@ -390,10 +421,6 @@ export default function ShippingPlanPage() {
|
||||||
<section>
|
<section>
|
||||||
<h3 className="text-sm font-semibold mb-3">기본 정보</h3>
|
<h3 className="text-sm font-semibold mb-3">기본 정보</h3>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground text-xs block mb-1">출하계획번호</span>
|
|
||||||
<span className="font-medium">{selectedPlan.shipment_plan_no || "-"}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground text-xs block mb-1">상태</span>
|
<span className="text-muted-foreground text-xs block mb-1">상태</span>
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border inline-block", getStatusColor(selectedPlan.status))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border inline-block", getStatusColor(selectedPlan.status))}>
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,16 @@ select {
|
||||||
font-family: "Gaegu", cursive;
|
font-family: "Gaegu", cursive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 전체 폰트 사이즈 16px 통일 (버튼 제외) ===== */
|
||||||
|
body *:not(button, [role="button"]) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body button *,
|
||||||
|
body [role="button"] * {
|
||||||
|
font-size: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Component-Specific Overrides ===== */
|
/* ===== Component-Specific Overrides ===== */
|
||||||
/* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */
|
/* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */
|
||||||
/* 예: Calendar, Table 등의 미세 조정 */
|
/* 예: Calendar, Table 등의 미세 조정 */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { apiClient } from "./client";
|
||||||
|
|
||||||
|
// --- 타입 정의 ---
|
||||||
|
|
||||||
|
export interface InboundItem {
|
||||||
|
id: string;
|
||||||
|
company_code: string;
|
||||||
|
inbound_number: string;
|
||||||
|
inbound_type: string;
|
||||||
|
inbound_date: string;
|
||||||
|
reference_number: string | null;
|
||||||
|
supplier_code: string | null;
|
||||||
|
supplier_name: string | null;
|
||||||
|
item_number: string | null;
|
||||||
|
item_name: string | null;
|
||||||
|
spec: string | null;
|
||||||
|
material: string | null;
|
||||||
|
unit: string | null;
|
||||||
|
inbound_qty: number;
|
||||||
|
unit_price: number;
|
||||||
|
total_amount: number;
|
||||||
|
lot_number: string | null;
|
||||||
|
warehouse_code: string | null;
|
||||||
|
warehouse_name?: string | null;
|
||||||
|
location_code: string | null;
|
||||||
|
inbound_status: string;
|
||||||
|
inspection_status: string | null;
|
||||||
|
inspector: string | null;
|
||||||
|
manager: string | null;
|
||||||
|
memo: string | null;
|
||||||
|
source_table: string | null;
|
||||||
|
source_id: string | null;
|
||||||
|
created_date: string;
|
||||||
|
created_by: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderSource {
|
||||||
|
id: string;
|
||||||
|
purchase_no: string;
|
||||||
|
order_date: string;
|
||||||
|
supplier_code: string;
|
||||||
|
supplier_name: string;
|
||||||
|
item_code: string;
|
||||||
|
item_name: string;
|
||||||
|
spec: string | null;
|
||||||
|
material: string | null;
|
||||||
|
order_qty: number;
|
||||||
|
received_qty: number;
|
||||||
|
remain_qty: number;
|
||||||
|
unit_price: number;
|
||||||
|
status: string;
|
||||||
|
due_date: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipmentSource {
|
||||||
|
detail_id: number;
|
||||||
|
instruction_id: number;
|
||||||
|
instruction_no: string;
|
||||||
|
instruction_date: string;
|
||||||
|
partner_id: string;
|
||||||
|
instruction_status: string;
|
||||||
|
item_code: string;
|
||||||
|
item_name: string;
|
||||||
|
spec: string | null;
|
||||||
|
material: string | null;
|
||||||
|
ship_qty: number;
|
||||||
|
order_qty: number;
|
||||||
|
source_type: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemSource {
|
||||||
|
id: string;
|
||||||
|
item_number: string;
|
||||||
|
item_name: string;
|
||||||
|
spec: string | null;
|
||||||
|
material: string | null;
|
||||||
|
unit: string | null;
|
||||||
|
standard_price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarehouseOption {
|
||||||
|
warehouse_code: string;
|
||||||
|
warehouse_name: string;
|
||||||
|
warehouse_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateReceivingPayload {
|
||||||
|
inbound_number: string;
|
||||||
|
inbound_date: string;
|
||||||
|
warehouse_code?: string;
|
||||||
|
location_code?: string;
|
||||||
|
inspector?: string;
|
||||||
|
manager?: string;
|
||||||
|
memo?: string;
|
||||||
|
items: Array<{
|
||||||
|
inbound_type: string;
|
||||||
|
reference_number?: string;
|
||||||
|
supplier_code?: string;
|
||||||
|
supplier_name?: string;
|
||||||
|
item_number?: string;
|
||||||
|
item_name?: string;
|
||||||
|
spec?: string;
|
||||||
|
material?: string;
|
||||||
|
unit?: string;
|
||||||
|
inbound_qty: number;
|
||||||
|
unit_price?: number;
|
||||||
|
total_amount?: number;
|
||||||
|
lot_number?: string;
|
||||||
|
warehouse_code?: string;
|
||||||
|
location_code?: string;
|
||||||
|
inbound_status?: string;
|
||||||
|
inspection_status?: string;
|
||||||
|
inspector?: string;
|
||||||
|
manager?: string;
|
||||||
|
memo?: string;
|
||||||
|
source_table: string;
|
||||||
|
source_id: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- API 호출 ---
|
||||||
|
|
||||||
|
export async function getReceivingList(params?: {
|
||||||
|
inbound_type?: string;
|
||||||
|
inbound_status?: string;
|
||||||
|
search_keyword?: string;
|
||||||
|
date_from?: string;
|
||||||
|
date_to?: string;
|
||||||
|
}) {
|
||||||
|
const res = await apiClient.get("/receiving/list", { params });
|
||||||
|
return res.data as { success: boolean; data: InboundItem[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createReceiving(payload: CreateReceivingPayload) {
|
||||||
|
const res = await apiClient.post("/receiving", payload);
|
||||||
|
return res.data as { success: boolean; data: InboundItem[]; message?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateReceiving(id: string, payload: Partial<InboundItem>) {
|
||||||
|
const res = await apiClient.put(`/receiving/${id}`, payload);
|
||||||
|
return res.data as { success: boolean; data: InboundItem };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteReceiving(id: string) {
|
||||||
|
const res = await apiClient.delete(`/receiving/${id}`);
|
||||||
|
return res.data as { success: boolean; message?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateReceivingNumber() {
|
||||||
|
const res = await apiClient.get("/receiving/generate-number");
|
||||||
|
return res.data as { success: boolean; data: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReceivingWarehouses() {
|
||||||
|
const res = await apiClient.get("/receiving/warehouses");
|
||||||
|
return res.data as { success: boolean; data: WarehouseOption[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 소스 데이터 조회
|
||||||
|
export async function getPurchaseOrderSources(keyword?: string) {
|
||||||
|
const res = await apiClient.get("/receiving/source/purchase-orders", {
|
||||||
|
params: keyword ? { keyword } : {},
|
||||||
|
});
|
||||||
|
return res.data as { success: boolean; data: PurchaseOrderSource[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getShipmentSources(keyword?: string) {
|
||||||
|
const res = await apiClient.get("/receiving/source/shipments", {
|
||||||
|
params: keyword ? { keyword } : {},
|
||||||
|
});
|
||||||
|
return res.data as { success: boolean; data: ShipmentSource[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getItemSources(keyword?: string) {
|
||||||
|
const res = await apiClient.get("/receiving/source/items", {
|
||||||
|
params: keyword ? { keyword } : {},
|
||||||
|
});
|
||||||
|
return res.data as { success: boolean; data: ItemSource[] };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue