feat: Add outsourcing outbound functionality
- Introduced a new controller for managing outsourcing outbound processes, including automatic candidate retrieval and outbound list management. - Implemented API routes for fetching candidates, listing outsourcing outbounds, and creating new outbound records. - Enhanced the SQL queries to ensure proper filtering by company code and to utilize existing outbound management tables effectively. - Added new routes for handling outsourcing outbound operations in the Express application, improving the overall functionality of the logistics module.
This commit is contained in:
@@ -164,6 +164,7 @@ import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프
|
||||
import materialStatusRoutes from "./routes/materialStatusRoutes"; // 자재현황
|
||||
import receivingRoutes from "./routes/receivingRoutes"; // 입고관리
|
||||
import outboundRoutes from "./routes/outboundRoutes"; // 출고관리
|
||||
import outsourcingOutboundRoutes from "./routes/outsourcingOutboundRoutes"; // 외주출고
|
||||
import processInfoRoutes from "./routes/processInfoRoutes"; // 공정정보관리
|
||||
import quoteRoutes from "./routes/quoteRoutes"; // 견적관리
|
||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||
@@ -388,6 +389,7 @@ app.use("/api/report", analyticsReportRoutes); // 분석 리포트 (생산/재
|
||||
app.use("/api/design", designRoutes); // 설계 모듈
|
||||
app.use("/api/receiving", receivingRoutes); // 입고관리
|
||||
app.use("/api/outbound", outboundRoutes); // 출고관리
|
||||
app.use("/api/outsourcing-outbound", outsourcingOutboundRoutes); // 외주출고
|
||||
app.use("/api/quotes", quoteRoutes); // 견적관리
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* 외주출고 컨트롤러
|
||||
*
|
||||
* 이전 공정이 완료되고 다음 공정이 외주 공정이면
|
||||
* 자동으로 외주출고 대상 목록에 표시 → 출고 처리
|
||||
*
|
||||
* 출고 데이터는 기존 outbound_mng 테이블 재사용
|
||||
* (outbound_type='외주출고', source_type='work_order_process')
|
||||
*/
|
||||
|
||||
import type { Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import type { AuthenticatedRequest } from "../types/auth";
|
||||
import { adjustInventory } from "../utils/inventoryUtils";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* 외주출고 대상 자동 조회
|
||||
* GET /api/outsourcing-outbound/candidates
|
||||
*
|
||||
* 이전 공정 완료 + 다음 공정이 외주 공정인 건 자동 표시
|
||||
*/
|
||||
export async function getCandidates(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
let keywordCondition = "";
|
||||
const params: any[] = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
if (companyCode !== "*") {
|
||||
params.push(companyCode);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
keywordCondition = `AND (
|
||||
wi.instruction_no ILIKE $${paramIdx}
|
||||
OR wi.item_name ILIKE $${paramIdx}
|
||||
OR wi.item_code ILIKE $${paramIdx}
|
||||
OR sm.subcontractor_name ILIKE $${paramIdx}
|
||||
)`;
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const companyFilter = companyCode !== "*"
|
||||
? `wop_done.company_code = $1`
|
||||
: `1=1`;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
wop_done.id AS completed_process_id,
|
||||
wop_done.wo_id,
|
||||
wop_done.seq_no AS completed_seq_no,
|
||||
wop_done.process_code AS completed_process_code,
|
||||
COALESCE(pm_done.process_name, wop_done.process_name, wop_done.process_code) AS completed_process_name,
|
||||
COALESCE(CAST(NULLIF(wop_done.good_qty, '') AS numeric), 0) AS good_qty,
|
||||
wop_next.id AS next_process_id,
|
||||
wop_next.seq_no AS next_seq_no,
|
||||
wop_next.process_code AS next_process_code,
|
||||
COALESCE(pm_next.process_name, wop_next.process_name, wop_next.process_code) AS next_process_name,
|
||||
wop_next.status AS next_status,
|
||||
wi.instruction_no,
|
||||
wi.item_code,
|
||||
wi.item_name,
|
||||
ii.size AS spec,
|
||||
ii.material,
|
||||
ii.inventory_unit AS unit,
|
||||
sm.id AS subcontractor_id,
|
||||
sm.subcontractor_code,
|
||||
sm.subcontractor_name
|
||||
FROM work_order_process wop_done
|
||||
INNER JOIN work_instruction wi
|
||||
ON wop_done.wo_id = wi.id
|
||||
AND wop_done.company_code = wi.company_code
|
||||
-- 다음 공정 (바로 다음 seq_no)
|
||||
INNER JOIN LATERAL (
|
||||
SELECT wop2.*
|
||||
FROM work_order_process wop2
|
||||
WHERE wop2.wo_id = wop_done.wo_id
|
||||
AND wop2.company_code = wop_done.company_code
|
||||
AND wop2.parent_process_id IS NULL
|
||||
AND CAST(wop2.seq_no AS int) > CAST(wop_done.seq_no AS int)
|
||||
ORDER BY CAST(wop2.seq_no AS int)
|
||||
LIMIT 1
|
||||
) wop_next ON TRUE
|
||||
-- 다음 공정이 외주인지 확인
|
||||
INNER JOIN item_routing_subcontractor irs
|
||||
ON irs.routing_detail_id = wop_next.routing_detail_id
|
||||
INNER JOIN subcontractor_mng sm
|
||||
ON irs.subcontractor_id = sm.id
|
||||
LEFT JOIN item_info ii
|
||||
ON wi.item_code = ii.item_number AND wi.company_code = ii.company_code
|
||||
LEFT JOIN process_mng pm_done
|
||||
ON wop_done.process_code = pm_done.process_code AND wop_done.company_code = pm_done.company_code
|
||||
LEFT JOIN process_mng pm_next
|
||||
ON wop_next.process_code = pm_next.process_code AND wop_next.company_code = pm_next.company_code
|
||||
WHERE ${companyFilter}
|
||||
AND wop_done.parent_process_id IS NULL
|
||||
AND wop_done.status IN ('completed', 'acceptable')
|
||||
AND COALESCE(CAST(NULLIF(wop_done.good_qty, '') AS numeric), 0) > 0
|
||||
-- 아직 외주출고 등록 안 된 건만
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM outbound_mng om
|
||||
WHERE om.outbound_type = '외주출고'
|
||||
AND om.source_type = 'work_order_process'
|
||||
AND om.source_id = wop_done.id
|
||||
${companyCode !== "*" ? "AND om.company_code = $1" : ""}
|
||||
)
|
||||
${keywordCondition}
|
||||
ORDER BY wi.instruction_no, CAST(wop_done.seq_no AS int)
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("외주출고 대상 조회", { companyCode, count: result.rowCount });
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("외주출고 대상 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외주출고 목록 조회
|
||||
* GET /api/outsourcing-outbound/list
|
||||
*/
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { outbound_status, search_keyword, date_from, date_to } = req.query;
|
||||
|
||||
const conditions: string[] = ["om.outbound_type = '외주출고'"];
|
||||
const params: any[] = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
if (companyCode !== "*") {
|
||||
conditions.push(`om.company_code = $${paramIdx}`);
|
||||
params.push(companyCode);
|
||||
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 (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 = `WHERE ${conditions.join(" AND ")}`;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT om.*, wh.warehouse_name
|
||||
FROM outbound_mng om
|
||||
LEFT JOIN warehouse_info wh ON om.warehouse_code = wh.warehouse_code AND om.company_code = wh.company_code
|
||||
${whereClause}
|
||||
ORDER BY om.created_date DESC`,
|
||||
params,
|
||||
);
|
||||
|
||||
logger.info("외주출고 목록 조회", { companyCode, count: result.rowCount });
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("외주출고 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외주출고 등록
|
||||
* POST /api/outsourcing-outbound
|
||||
*/
|
||||
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,
|
||||
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: "출고 품목이 없습니다." });
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
const insertedRows: any[] = [];
|
||||
|
||||
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,
|
||||
outbound_qty, unit_price, total_amount,
|
||||
warehouse_code, location_code,
|
||||
outbound_status, manager_id, memo,
|
||||
source_type, source_id,
|
||||
created_date, created_by, writer, status
|
||||
) VALUES (
|
||||
gen_random_uuid()::text, $1, $2, '외주출고', $3,
|
||||
$4, $5, $6,
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, 0, 0,
|
||||
$13, $14,
|
||||
'출고완료', $15, $16,
|
||||
'work_order_process', $17,
|
||||
NOW(), $18, $18, '출고'
|
||||
) RETURNING *`,
|
||||
[
|
||||
companyCode,
|
||||
outbound_number || item.outbound_number,
|
||||
outbound_date || item.outbound_date,
|
||||
item.reference_number || null, // 작업지시번호
|
||||
item.subcontractor_code || null, // 외주사코드 → customer_code
|
||||
item.subcontractor_name || null, // 외주사명 → customer_name
|
||||
item.item_code || null,
|
||||
item.item_name || null,
|
||||
item.spec || null,
|
||||
item.material || null,
|
||||
item.unit || null,
|
||||
item.outbound_qty || 0,
|
||||
warehouse_code || item.warehouse_code || null,
|
||||
location_code || item.location_code || null,
|
||||
manager_id || item.manager_id || null,
|
||||
memo || item.memo || null,
|
||||
item.completed_process_id || null, // source_id = 완료된 공정 ID
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
insertedRows.push(result.rows[0]);
|
||||
|
||||
// 재고 차감
|
||||
const itemCode = item.item_code || 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 && whCode) {
|
||||
await adjustInventory(client, {
|
||||
companyCode,
|
||||
userId,
|
||||
itemCode,
|
||||
whCode,
|
||||
locCode,
|
||||
delta: -outQty,
|
||||
transactionType: "외주출고",
|
||||
remark: `외주출고 (${outbound_number || ""}) → ${item.subcontractor_name || ""}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외주출고 수정
|
||||
* PUT /api/outsourcing-outbound/:id
|
||||
*/
|
||||
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, warehouse_code, location_code, memo } = req.body;
|
||||
|
||||
const pool = getPool();
|
||||
const companyCondition = companyCode === "*" ? "" : `AND company_code = '${companyCode}'`;
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE outbound_mng SET
|
||||
outbound_date = COALESCE($1::date, outbound_date),
|
||||
outbound_qty = COALESCE($2::numeric, outbound_qty),
|
||||
warehouse_code = COALESCE($3, warehouse_code),
|
||||
location_code = COALESCE($4, location_code),
|
||||
memo = COALESCE($5, memo),
|
||||
updated_date = NOW(),
|
||||
updated_by = $6
|
||||
WHERE id = $7 ${companyCondition}
|
||||
RETURNING *`,
|
||||
[outbound_date, outbound_qty, warehouse_code, location_code, memo, userId, id],
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, 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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외주출고 삭제
|
||||
* DELETE /api/outsourcing-outbound/:id
|
||||
*/
|
||||
export async function deleteOutbound(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 삭제 전 데이터 조회 (재고 복구용)
|
||||
const companyCondition = companyCode === "*" ? "" : `AND company_code = $2`;
|
||||
const queryParams = companyCode === "*" ? [id] : [id, companyCode];
|
||||
|
||||
const oldRes = await client.query(
|
||||
`SELECT * FROM outbound_mng WHERE id = $1 ${companyCondition}`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
if (oldRes.rowCount === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
const old = oldRes.rows[0];
|
||||
const itemCode = old.item_code || null;
|
||||
const whCode = old.warehouse_code || null;
|
||||
const locCode = old.location_code || null;
|
||||
const oldQty = Number(old.outbound_qty) || 0;
|
||||
|
||||
// 재고 복구
|
||||
if (itemCode && oldQty > 0 && whCode) {
|
||||
await adjustInventory(client, {
|
||||
companyCode: old.company_code,
|
||||
userId,
|
||||
itemCode,
|
||||
whCode,
|
||||
locCode,
|
||||
delta: +oldQty,
|
||||
transactionType: "외주출고취소",
|
||||
remark: `외주출고 삭제 (${old.outbound_number || ""})`,
|
||||
});
|
||||
}
|
||||
|
||||
// 삭제
|
||||
await client.query(
|
||||
`DELETE FROM outbound_mng WHERE id = $1 ${companyCondition}`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("외주출고 삭제", { companyCode, id });
|
||||
return res.json({ success: true, message: "삭제되었습니다." });
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외주출고번호 자동생성
|
||||
* GET /api/outsourcing-outbound/generate-number
|
||||
*/
|
||||
export async function generateNumber(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const pool = getPool();
|
||||
const yyyy = new Date().getFullYear();
|
||||
const prefix = `OSOUT-${yyyy}-`;
|
||||
|
||||
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}%`],
|
||||
);
|
||||
|
||||
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")}`;
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 창고 목록 (outbound 컨트롤러와 공유)
|
||||
*/
|
||||
export async function getWarehouses(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const pool = getPool();
|
||||
|
||||
const condition = companyCode === "*" ? "" : `WHERE company_code = $1`;
|
||||
const params = companyCode === "*" ? [] : [companyCode];
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT warehouse_code, warehouse_name, warehouse_type FROM warehouse_info ${condition} ORDER BY warehouse_name`,
|
||||
params,
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
@@ -394,7 +394,30 @@ export async function getProductionPlanSource(req: AuthenticatedRequest, res: Re
|
||||
const pool = getPool();
|
||||
const cnt = await pool.query(`SELECT COUNT(*) AS total FROM production_plan_mng p WHERE ${w}`, params);
|
||||
params.push(pageSize, offset);
|
||||
const rows = await pool.query(`SELECT p.id, p.plan_no, p.item_code, COALESCE(p.item_name,'') AS item_name, COALESCE(p.plan_qty,0) AS plan_qty, p.start_date, p.end_date, p.status, COALESCE(p.equipment_name,'') AS equipment_name FROM production_plan_mng p WHERE ${w} ORDER BY p.created_date DESC LIMIT $${idx} OFFSET $${idx+1}`, params);
|
||||
// work_instruction_detail에서 해당 계획에 이미 내린 작업지시 수량 합계 → applied_qty, remain_qty
|
||||
const rows = await pool.query(
|
||||
`SELECT p.id, p.plan_no, p.item_code,
|
||||
COALESCE(p.item_name,'') AS item_name,
|
||||
COALESCE(p.plan_qty,0) AS plan_qty,
|
||||
p.start_date, p.end_date, p.status,
|
||||
COALESCE(p.equipment_name,'') AS equipment_name,
|
||||
COALESCE(wi.applied_qty, 0) AS applied_qty,
|
||||
(COALESCE(CAST(NULLIF(p.plan_qty::text, '') AS numeric), 0)
|
||||
- COALESCE(wi.applied_qty, 0)) AS remain_qty
|
||||
FROM production_plan_mng p
|
||||
LEFT JOIN (
|
||||
SELECT source_id,
|
||||
SUM(COALESCE(CAST(NULLIF(qty, '') AS numeric), 0)) AS applied_qty
|
||||
FROM work_instruction_detail
|
||||
WHERE source_table = 'production_plan_mng'
|
||||
AND company_code = $1
|
||||
GROUP BY source_id
|
||||
) wi ON wi.source_id = p.id::text
|
||||
WHERE ${w}
|
||||
ORDER BY p.created_date DESC
|
||||
LIMIT $${idx} OFFSET $${idx+1}`,
|
||||
params,
|
||||
);
|
||||
return res.json({ success: true, data: rows.rows, totalCount: parseInt(cnt.rows[0].total), page, pageSize });
|
||||
} catch (error: any) { return res.status(500).json({ success: false, message: error.message }); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import * as ctrl from "../controllers/outsourcingOutboundController";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
|
||||
// 외주출고 대상 자동 조회
|
||||
router.get("/candidates", ctrl.getCandidates);
|
||||
|
||||
// 외주출고 목록 조회
|
||||
router.get("/list", ctrl.getList);
|
||||
|
||||
// 외주출고번호 자동생성
|
||||
router.get("/generate-number", ctrl.generateNumber);
|
||||
|
||||
// 창고 목록
|
||||
router.get("/warehouses", ctrl.getWarehouses);
|
||||
|
||||
// 외주출고 등록
|
||||
router.post("/", ctrl.create);
|
||||
|
||||
// 외주출고 수정
|
||||
router.put("/:id", ctrl.update);
|
||||
|
||||
// 외주출고 삭제
|
||||
router.delete("/:id", ctrl.deleteOutbound);
|
||||
|
||||
export default router;
|
||||
@@ -312,6 +312,11 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -311,6 +311,11 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -185,11 +185,15 @@ export default function WorkInstructionPage() {
|
||||
case "order": r = await getWISalesOrderSource(params); break;
|
||||
case "item": r = await getWIItemSource(params); break;
|
||||
}
|
||||
if (r?.success) { setRegSourceData(r.data || []); setRegTotalCount(r.totalCount || 0); }
|
||||
if (r?.success) {
|
||||
// 생산계획 근거는 백엔드에서 applied_qty / remain_qty 포함해 내려옴
|
||||
setRegSourceData(r.data || []);
|
||||
setRegTotalCount(r.totalCount || 0);
|
||||
}
|
||||
} catch {} finally { setRegSourceLoading(false); }
|
||||
}, [regSourceType, regKeyword, regPage, regPageSize]);
|
||||
|
||||
useEffect(() => { if (isRegModalOpen && regSourceType) { setRegPage(1); setRegCheckedIds(new Set()); fetchRegSource(1); } }, [regSourceType]);
|
||||
useEffect(() => { if (isRegModalOpen && regSourceType) { setRegPage(1); setRegCheckedIds(new Set()); fetchRegSource(1); } }, [isRegModalOpen, regSourceType]);
|
||||
|
||||
const getRegId = (item: any) => regSourceType === "item" ? (item.item_code || item.id) : String(item.id);
|
||||
const toggleRegItem = (id: string) => { setRegCheckedIds(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; }); };
|
||||
@@ -202,7 +206,13 @@ export default function WorkInstructionPage() {
|
||||
if (!regCheckedIds.has(getRegId(item))) continue;
|
||||
if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code });
|
||||
else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id });
|
||||
else items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: Number(item.plan_qty || 1), remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id });
|
||||
else {
|
||||
// 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능)
|
||||
const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null
|
||||
? Number(item.remain_qty)
|
||||
: Number(item.plan_qty || 1);
|
||||
items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id });
|
||||
}
|
||||
}
|
||||
|
||||
// 동일품목 합산
|
||||
@@ -578,7 +588,7 @@ export default function WorkInstructionPage() {
|
||||
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"><Checkbox checked={regSourceData.length > 0 && regCheckedIds.size === regSourceData.length} onCheckedChange={toggleRegAll} /></TableHead>
|
||||
{regSourceType === "item" && <><TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead><TableHead>품목명</TableHead><TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead></>}
|
||||
{regSourceType === "order" && <><TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수주번호</TableHead><TableHead className="w-[100px]">품번</TableHead><TableHead>품목명</TableHead><TableHead className="w-[100px]">규격</TableHead><TableHead className="w-[80px] text-right">수량</TableHead><TableHead className="w-[100px]">납기일</TableHead></>}
|
||||
{regSourceType === "production" && <><TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획번호</TableHead><TableHead className="w-[100px]">품번</TableHead><TableHead>품목명</TableHead><TableHead className="w-[80px] text-right">계획수량</TableHead><TableHead className="w-[90px]">시작일</TableHead><TableHead className="w-[90px]">완료일</TableHead><TableHead className="w-[100px]">설비</TableHead></>}
|
||||
{regSourceType === "production" && <><TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획번호</TableHead><TableHead className="w-[100px]">품번</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-[90px]">시작일</TableHead><TableHead className="w-[90px]">완료일</TableHead><TableHead className="w-[100px]">설비</TableHead></>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -590,7 +600,7 @@ export default function WorkInstructionPage() {
|
||||
<TableCell className="text-center" onClick={e => e.stopPropagation()}><Checkbox checked={checked} onCheckedChange={() => toggleRegItem(id)} /></TableCell>
|
||||
{regSourceType === "item" && <><TableCell className="text-[13px] font-medium">{item.item_code}</TableCell><TableCell className="text-sm">{item.item_name}</TableCell><TableCell className="text-[13px]">{item.spec || "-"}</TableCell></>}
|
||||
{regSourceType === "order" && <><TableCell className="text-[13px]">{item.order_no}</TableCell><TableCell className="text-[13px]">{item.item_code}</TableCell><TableCell className="text-sm">{item.item_name}</TableCell><TableCell className="text-[13px]">{item.spec || "-"}</TableCell><TableCell className="text-right text-[13px]">{Number(item.qty || 0).toLocaleString()}</TableCell><TableCell className="text-[13px]">{item.due_date || "-"}</TableCell></>}
|
||||
{regSourceType === "production" && <><TableCell className="text-[13px]">{item.plan_no}</TableCell><TableCell className="text-[13px]">{item.item_code}</TableCell><TableCell className="text-sm">{item.item_name}</TableCell><TableCell className="text-right text-[13px]">{Number(item.plan_qty || 0).toLocaleString()}</TableCell><TableCell className="text-[13px]">{item.start_date ? String(item.start_date).split("T")[0] : "-"}</TableCell><TableCell className="text-[13px]">{item.end_date ? String(item.end_date).split("T")[0] : "-"}</TableCell><TableCell className="text-[13px]">{item.equipment_name || "-"}</TableCell></>}
|
||||
{regSourceType === "production" && <><TableCell className="text-[13px]">{item.plan_no}</TableCell><TableCell className="text-[13px]">{item.item_code}</TableCell><TableCell className="text-sm">{item.item_name}</TableCell><TableCell className="text-right text-[13px]">{Number(item.plan_qty || 0).toLocaleString()}</TableCell><TableCell className="text-right text-[13px] text-muted-foreground">{Number(item.applied_qty || 0).toLocaleString()}</TableCell><TableCell className={cn("text-right text-[13px] font-semibold", Number(item.remain_qty ?? item.plan_qty ?? 0) < 0 && "text-destructive")}>{Number(item.remain_qty ?? item.plan_qty ?? 0).toLocaleString()}</TableCell><TableCell className="text-[13px]">{item.start_date ? String(item.start_date).split("T")[0] : "-"}</TableCell><TableCell className="text-[13px]">{item.end_date ? String(item.end_date).split("T")[0] : "-"}</TableCell><TableCell className="text-[13px]">{item.equipment_name || "-"}</TableCell></>}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -312,6 +312,11 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -311,6 +311,11 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -312,6 +312,11 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -311,6 +311,11 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -318,6 +318,11 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -191,13 +191,13 @@ export default function CustomerManagementPage() {
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${CUSTOMER_TABLE}/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/${CUSTOMER_TABLE}/${col}/values?filterCompanyCode=COMPANY_30`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_30`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export default function CustomerManagementPage() {
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values?filterCompanyCode=COMPANY_30`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export default function CustomerManagementPage() {
|
||||
|
||||
// 세금유형 카테고리
|
||||
try {
|
||||
const taxRes = await apiClient.get(`/table-categories/customer_tax_type/tax_type_name/values`);
|
||||
const taxRes = await apiClient.get(`/table-categories/customer_tax_type/tax_type_name/values?filterCompanyCode=COMPANY_30`);
|
||||
if (taxRes.data?.success) setTaxTypeOptions(flatten(taxRes.data.data || []));
|
||||
} catch { /* skip */ }
|
||||
};
|
||||
@@ -593,9 +593,12 @@ export default function CustomerManagementPage() {
|
||||
} catch { /* skip */ }
|
||||
};
|
||||
|
||||
const openCustomerEdit = () => {
|
||||
if (!selectedCustomer) return;
|
||||
const rawData = rawCustomers.find((c) => c.id === selectedCustomerId);
|
||||
const openCustomerEdit = (rowArg?: any) => {
|
||||
const targetId = rowArg?.id ?? selectedCustomerId;
|
||||
const rawData =
|
||||
(rowArg && !("_resolved" in rowArg) ? rowArg : null) ||
|
||||
rawCustomers.find((c) => String(c.id) === String(targetId));
|
||||
if (!rawData && !selectedCustomer) return;
|
||||
setCustomerForm({ ...(rawData || selectedCustomer) });
|
||||
setFormErrors({});
|
||||
setCustomerEditMode(true);
|
||||
@@ -607,8 +610,10 @@ export default function CustomerManagementPage() {
|
||||
setModalContactEditId(null);
|
||||
setModalDeliveryEditId(null);
|
||||
// 수정 모드에서는 바로 조회
|
||||
const code = (rawData || selectedCustomer).customer_code;
|
||||
const id = (rawData || selectedCustomer).id;
|
||||
const targetCustomer = rawData || selectedCustomer;
|
||||
if (!targetCustomer) { setCustomerModalOpen(true); return; }
|
||||
const code = targetCustomer.customer_code;
|
||||
const id = targetCustomer.id;
|
||||
if (id) {
|
||||
fetchModalContacts(id);
|
||||
// 세금유형 로드
|
||||
@@ -1478,7 +1483,11 @@ export default function CustomerManagementPage() {
|
||||
emptyMessage="등록된 거래처가 없어요"
|
||||
selectedId={selectedCustomerId}
|
||||
onSelect={(id) => setSelectedCustomerId(id)}
|
||||
onRowDoubleClick={(row) => { setSelectedCustomerId(row.id); openCustomerEdit(); }}
|
||||
onRowDoubleClick={(row) => {
|
||||
setSelectedCustomerId(row.id);
|
||||
const rawRow = rawCustomers.find((c) => String(c.id) === String(row.id));
|
||||
openCustomerEdit(rawRow || row);
|
||||
}}
|
||||
showRowNumber
|
||||
showPagination
|
||||
defaultPageSize={20}
|
||||
|
||||
@@ -317,6 +317,11 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
Settings2,
|
||||
Save,
|
||||
Package,
|
||||
Pencil,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -1482,6 +1483,15 @@ export default function BomManagementPage() {
|
||||
<Plus className="w-3.5 h-3.5 mr-1" />
|
||||
등록
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={openEditModal}
|
||||
disabled={!selectedBomId}
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5 mr-1" />
|
||||
수정
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-border mx-0.5" />
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -329,6 +329,11 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -329,6 +329,11 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -312,6 +312,11 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -311,6 +311,11 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -318,6 +318,11 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -191,13 +191,13 @@ export default function CustomerManagementPage() {
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${CUSTOMER_TABLE}/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/${CUSTOMER_TABLE}/${col}/values?filterCompanyCode=COMPANY_9`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_9`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export default function CustomerManagementPage() {
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values?filterCompanyCode=COMPANY_9`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export default function CustomerManagementPage() {
|
||||
|
||||
// 세금유형 카테고리
|
||||
try {
|
||||
const taxRes = await apiClient.get(`/table-categories/customer_tax_type/tax_type_name/values`);
|
||||
const taxRes = await apiClient.get(`/table-categories/customer_tax_type/tax_type_name/values?filterCompanyCode=COMPANY_9`);
|
||||
if (taxRes.data?.success) setTaxTypeOptions(flatten(taxRes.data.data || []));
|
||||
} catch { /* skip */ }
|
||||
};
|
||||
|
||||
@@ -317,6 +317,11 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
// 카테고리 로드 완료 전엔 대기 — 먼저 나간 unfiltered 요청이 나중에 도착해
|
||||
// filtered 결과를 덮어쓰는 race condition 방지
|
||||
if (!categoryOptions["division"]?.length) {
|
||||
return;
|
||||
}
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
|
||||
@@ -172,6 +172,7 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/COMPANY_16/logistics/info": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/info/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/outsourcing/subcontractor": dynamic(() => import("@/app/(main)/COMPANY_16/outsourcing/subcontractor/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/outsourcing/subcontractor-item": dynamic(() => import("@/app/(main)/COMPANY_16/outsourcing/subcontractor-item/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/outsourcing/outbound": dynamic(() => import("@/app/(main)/COMPANY_16/outsourcing/outbound/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/purchase/order": dynamic(() => import("@/app/(main)/COMPANY_16/purchase/order/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_16/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_16/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }),
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { apiClient } from "./client";
|
||||
|
||||
// ===== 타입 =====
|
||||
|
||||
export interface OutsourcingCandidate {
|
||||
completed_process_id: string;
|
||||
wo_id: string;
|
||||
completed_seq_no: string;
|
||||
completed_process_code: string;
|
||||
completed_process_name: string;
|
||||
good_qty: number;
|
||||
next_process_id: string;
|
||||
next_seq_no: string;
|
||||
next_process_code: string;
|
||||
next_process_name: string;
|
||||
next_status: string;
|
||||
instruction_no: string;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
spec: string;
|
||||
material: string;
|
||||
unit: string;
|
||||
subcontractor_id: string;
|
||||
subcontractor_code: string;
|
||||
subcontractor_name: string;
|
||||
}
|
||||
|
||||
export interface OutsourcingOutboundItem {
|
||||
id: string;
|
||||
outbound_number: string;
|
||||
outbound_type: string;
|
||||
outbound_date: string;
|
||||
reference_number: string;
|
||||
customer_code: string;
|
||||
customer_name: string;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
specification: string;
|
||||
material: string;
|
||||
unit: string;
|
||||
outbound_qty: number;
|
||||
warehouse_code: string;
|
||||
warehouse_name?: string;
|
||||
location_code: string;
|
||||
outbound_status: string;
|
||||
source_type: string;
|
||||
source_id: string;
|
||||
manager_id: string;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
export interface WarehouseOption {
|
||||
warehouse_code: string;
|
||||
warehouse_name: string;
|
||||
warehouse_type?: string;
|
||||
}
|
||||
|
||||
// ===== API 함수 =====
|
||||
|
||||
export async function getCandidates(keyword?: string) {
|
||||
const params: Record<string, string> = {};
|
||||
if (keyword) params.keyword = keyword;
|
||||
const res = await apiClient.get("/outsourcing-outbound/candidates", { params });
|
||||
return res.data as { success: boolean; data: OutsourcingCandidate[] };
|
||||
}
|
||||
|
||||
export async function getOutsourcingOutboundList(params?: {
|
||||
outbound_status?: string;
|
||||
search_keyword?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}) {
|
||||
const res = await apiClient.get("/outsourcing-outbound/list", { params: params || {} });
|
||||
return res.data as { success: boolean; data: OutsourcingOutboundItem[] };
|
||||
}
|
||||
|
||||
export async function createOutsourcingOutbound(payload: {
|
||||
outbound_number: string;
|
||||
outbound_date: string;
|
||||
warehouse_code?: string;
|
||||
location_code?: string;
|
||||
manager_id?: string;
|
||||
memo?: string;
|
||||
items: Array<{
|
||||
reference_number?: string;
|
||||
subcontractor_code?: string;
|
||||
subcontractor_name?: string;
|
||||
item_code?: string;
|
||||
item_name?: string;
|
||||
spec?: string;
|
||||
material?: string;
|
||||
unit?: string;
|
||||
outbound_qty: number;
|
||||
completed_process_id?: string;
|
||||
warehouse_code?: string;
|
||||
location_code?: string;
|
||||
}>;
|
||||
}) {
|
||||
const res = await apiClient.post("/outsourcing-outbound", payload);
|
||||
return res.data as { success: boolean; data: OutsourcingOutboundItem[]; message?: string };
|
||||
}
|
||||
|
||||
export async function updateOutsourcingOutbound(id: string, payload: Partial<OutsourcingOutboundItem>) {
|
||||
const res = await apiClient.put(`/outsourcing-outbound/${id}`, payload);
|
||||
return res.data as { success: boolean; data: OutsourcingOutboundItem };
|
||||
}
|
||||
|
||||
export async function deleteOutsourcingOutbound(id: string) {
|
||||
const res = await apiClient.delete(`/outsourcing-outbound/${id}`);
|
||||
return res.data as { success: boolean; message?: string };
|
||||
}
|
||||
|
||||
export async function generateOutsourcingOutboundNumber() {
|
||||
const res = await apiClient.get("/outsourcing-outbound/generate-number");
|
||||
return res.data as { success: boolean; data: string };
|
||||
}
|
||||
|
||||
export async function getOutsourcingWarehouses() {
|
||||
const res = await apiClient.get("/outsourcing-outbound/warehouses");
|
||||
return res.data as { success: boolean; data: WarehouseOption[] };
|
||||
}
|
||||
Reference in New Issue
Block a user