Merge branch 'gbpark-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
commit
c81594b137
|
|
@ -145,6 +145,8 @@ import auditLogRoutes from "./routes/auditLogRoutes"; // 통합 변경 이력
|
||||||
import moldRoutes from "./routes/moldRoutes"; // 금형 관리
|
import moldRoutes from "./routes/moldRoutes"; // 금형 관리
|
||||||
import shippingPlanRoutes from "./routes/shippingPlanRoutes"; // 출하계획 관리
|
import shippingPlanRoutes from "./routes/shippingPlanRoutes"; // 출하계획 관리
|
||||||
import shippingOrderRoutes from "./routes/shippingOrderRoutes"; // 출하지시 관리
|
import shippingOrderRoutes from "./routes/shippingOrderRoutes"; // 출하지시 관리
|
||||||
|
import salesReportRoutes from "./routes/salesReportRoutes"; // 영업 리포트
|
||||||
|
import analyticsReportRoutes from "./routes/analyticsReportRoutes"; // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
||||||
import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프로젝트/ECN)
|
import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프로젝트/ECN)
|
||||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||||
|
|
@ -340,6 +342,8 @@ app.use("/api/audit-log", auditLogRoutes); // 통합 변경 이력
|
||||||
app.use("/api/mold", moldRoutes); // 금형 관리
|
app.use("/api/mold", moldRoutes); // 금형 관리
|
||||||
app.use("/api/shipping-plan", shippingPlanRoutes); // 출하계획 관리
|
app.use("/api/shipping-plan", shippingPlanRoutes); // 출하계획 관리
|
||||||
app.use("/api/shipping-order", shippingOrderRoutes); // 출하지시 관리
|
app.use("/api/shipping-order", shippingOrderRoutes); // 출하지시 관리
|
||||||
|
app.use("/api/sales-report", salesReportRoutes); // 영업 리포트
|
||||||
|
app.use("/api/report", analyticsReportRoutes); // 분석 리포트 (생산/재고/구매/품질/설비/금형)
|
||||||
app.use("/api/design", designRoutes); // 설계 모듈
|
app.use("/api/design", designRoutes); // 설계 모듈
|
||||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,488 @@
|
||||||
|
import { Response } from "express";
|
||||||
|
import { query } from "../database/db";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
function buildCompanyFilter(companyCode: string, alias: string, paramIdx: number) {
|
||||||
|
if (companyCode === "*") return { condition: "", params: [] as any[], nextIdx: paramIdx };
|
||||||
|
return {
|
||||||
|
condition: `${alias}.company_code = $${paramIdx}`,
|
||||||
|
params: [companyCode],
|
||||||
|
nextIdx: paramIdx + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDateFilter(startDate: string | undefined, endDate: string | undefined, dateExpr: string, paramIdx: number) {
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let idx = paramIdx;
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
conditions.push(`${dateExpr} >= $${idx}`);
|
||||||
|
params.push(startDate);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
conditions.push(`${dateExpr} <= $${idx}`);
|
||||||
|
params.push(endDate);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { conditions, params, nextIdx: idx };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWhereClause(conditions: string[]): string {
|
||||||
|
return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFilterSet(rows: any[], field: string, labelField?: string): { value: string; label: string }[] {
|
||||||
|
const set = new Map<string, string>();
|
||||||
|
rows.forEach((r: any) => {
|
||||||
|
const val = r[field];
|
||||||
|
if (val && val !== "미지정") set.set(val, r[labelField || field] || val);
|
||||||
|
});
|
||||||
|
return [...set.entries()].map(([value, label]) => ({ value, label }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 생산 리포트
|
||||||
|
// ============================================
|
||||||
|
export async function getProductionReportData(req: any, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
|
||||||
|
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
const cf = buildCompanyFilter(companyCode, "wi", idx);
|
||||||
|
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
|
||||||
|
|
||||||
|
const df = buildDateFilter(startDate, endDate, "COALESCE(wi.start_date, wi.created_date::date::text)", idx);
|
||||||
|
conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx;
|
||||||
|
|
||||||
|
const whereClause = buildWhereClause(conditions);
|
||||||
|
|
||||||
|
const dataQuery = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(wi.start_date, wi.created_date::date::text) as date,
|
||||||
|
COALESCE(wi.routing, '미지정') as process,
|
||||||
|
COALESCE(ei.equipment_name, wi.equipment_id, '미지정') as equipment,
|
||||||
|
COALESCE(ii.item_name, wi.item_id, '미지정') as item,
|
||||||
|
COALESCE(wi.worker, '미지정') as worker,
|
||||||
|
CAST(COALESCE(NULLIF(wi.qty, ''), '0') AS numeric) as "planQty",
|
||||||
|
COALESCE(pr.production_qty, 0) as "prodQty",
|
||||||
|
COALESCE(pr.defect_qty, 0) as "defectQty",
|
||||||
|
0 as "runTime",
|
||||||
|
0 as "downTime",
|
||||||
|
wi.status,
|
||||||
|
wi.company_code
|
||||||
|
FROM work_instruction wi
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT wo_id, company_code,
|
||||||
|
SUM(CAST(COALESCE(NULLIF(production_qty, ''), '0') AS numeric)) as production_qty,
|
||||||
|
SUM(CAST(COALESCE(NULLIF(defect_qty, ''), '0') AS numeric)) as defect_qty
|
||||||
|
FROM production_record GROUP BY wo_id, company_code
|
||||||
|
) pr ON wi.id = pr.wo_id AND wi.company_code = pr.company_code
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DISTINCT ON (equipment_code, company_code)
|
||||||
|
equipment_code, equipment_name, equipment_type, company_code
|
||||||
|
FROM equipment_info ORDER BY equipment_code, company_code, created_date DESC
|
||||||
|
) ei ON wi.equipment_id = ei.equipment_code AND wi.company_code = ei.company_code
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DISTINCT ON (item_number, company_code)
|
||||||
|
item_number, item_name, company_code
|
||||||
|
FROM item_info ORDER BY item_number, company_code, created_date DESC
|
||||||
|
) ii ON wi.item_id = ii.item_number AND wi.company_code = ii.company_code
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY date DESC NULLS LAST
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dataRows = await query(dataQuery, params);
|
||||||
|
|
||||||
|
logger.info("생산 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: dataRows,
|
||||||
|
filterOptions: {
|
||||||
|
processes: extractFilterSet(dataRows, "process"),
|
||||||
|
equipment: extractFilterSet(dataRows, "equipment"),
|
||||||
|
items: extractFilterSet(dataRows, "item"),
|
||||||
|
workers: extractFilterSet(dataRows, "worker"),
|
||||||
|
},
|
||||||
|
totalCount: dataRows.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("생산 리포트 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: "생산 리포트 데이터 조회에 실패했습니다", error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 재고 리포트
|
||||||
|
// ============================================
|
||||||
|
export async function getInventoryReportData(req: any, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
|
||||||
|
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
const cf = buildCompanyFilter(companyCode, "ist", idx);
|
||||||
|
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
|
||||||
|
|
||||||
|
const whereClause = buildWhereClause(conditions);
|
||||||
|
|
||||||
|
const dataQuery = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(ist.updated_date, ist.created_date)::date::text as date,
|
||||||
|
ist.item_code,
|
||||||
|
COALESCE(ii.item_name, ist.item_code, '미지정') as item,
|
||||||
|
COALESCE(wi.warehouse_name, ist.warehouse_code, '미지정') as warehouse,
|
||||||
|
'일반' as category,
|
||||||
|
CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '0') AS numeric) as "currentQty",
|
||||||
|
CAST(COALESCE(NULLIF(ist.safety_qty::text, ''), '0') AS numeric) as "safetyQty",
|
||||||
|
COALESCE(ih_in.in_qty, 0) as "inQty",
|
||||||
|
COALESCE(ih_out.out_qty, 0) as "outQty",
|
||||||
|
0 as "stockValue",
|
||||||
|
GREATEST(CAST(COALESCE(NULLIF(ist.safety_qty::text, ''), '0') AS numeric)
|
||||||
|
- CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '0') AS numeric), 0) as "shortageQty",
|
||||||
|
CASE WHEN CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '0') AS numeric) > 0
|
||||||
|
AND COALESCE(ih_out.out_qty, 0) > 0
|
||||||
|
THEN ROUND(COALESCE(ih_out.out_qty, 0)::numeric
|
||||||
|
/ CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '1') AS numeric), 2)
|
||||||
|
ELSE 0 END as "turnover",
|
||||||
|
ist.company_code
|
||||||
|
FROM inventory_stock ist
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DISTINCT ON (item_number, company_code)
|
||||||
|
item_number, item_name, company_code
|
||||||
|
FROM item_info ORDER BY item_number, company_code, created_date DESC
|
||||||
|
) ii ON ist.item_code = ii.item_number AND ist.company_code = ii.company_code
|
||||||
|
LEFT JOIN warehouse_info wi ON ist.warehouse_code = wi.warehouse_code
|
||||||
|
AND ist.company_code = wi.company_code
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT item_code, company_code,
|
||||||
|
SUM(CAST(COALESCE(NULLIF(quantity::text, ''), '0') AS numeric)) as in_qty
|
||||||
|
FROM inventory_history WHERE transaction_type = 'IN'
|
||||||
|
GROUP BY item_code, company_code
|
||||||
|
) ih_in ON ist.item_code = ih_in.item_code AND ist.company_code = ih_in.company_code
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT item_code, company_code,
|
||||||
|
SUM(CAST(COALESCE(NULLIF(quantity::text, ''), '0') AS numeric)) as out_qty
|
||||||
|
FROM inventory_history WHERE transaction_type = 'OUT'
|
||||||
|
GROUP BY item_code, company_code
|
||||||
|
) ih_out ON ist.item_code = ih_out.item_code AND ist.company_code = ih_out.company_code
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY date DESC NULLS LAST
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dataRows = await query(dataQuery, params);
|
||||||
|
|
||||||
|
logger.info("재고 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: dataRows,
|
||||||
|
filterOptions: {
|
||||||
|
items: extractFilterSet(dataRows, "item"),
|
||||||
|
warehouses: extractFilterSet(dataRows, "warehouse"),
|
||||||
|
categories: [
|
||||||
|
{ value: "원자재", label: "원자재" }, { value: "부자재", label: "부자재" },
|
||||||
|
{ value: "반제품", label: "반제품" }, { value: "완제품", label: "완제품" },
|
||||||
|
{ value: "일반", label: "일반" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
totalCount: dataRows.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("재고 리포트 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: "재고 리포트 데이터 조회에 실패했습니다", error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 구매 리포트
|
||||||
|
// ============================================
|
||||||
|
export async function getPurchaseReportData(req: any, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
|
||||||
|
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
const cf = buildCompanyFilter(companyCode, "po", idx);
|
||||||
|
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
|
||||||
|
|
||||||
|
const df = buildDateFilter(startDate, endDate, "COALESCE(po.order_date, po.created_date::date::text)", idx);
|
||||||
|
conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx;
|
||||||
|
|
||||||
|
const whereClause = buildWhereClause(conditions);
|
||||||
|
|
||||||
|
const dataQuery = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(po.order_date, po.created_date::date::text) as date,
|
||||||
|
po.purchase_no,
|
||||||
|
COALESCE(po.supplier_name, po.supplier_code, '미지정') as supplier,
|
||||||
|
COALESCE(po.item_name, po.item_code, '미지정') as item,
|
||||||
|
po.item_code,
|
||||||
|
COALESCE(po.manager, '미지정') as manager,
|
||||||
|
po.status,
|
||||||
|
CAST(COALESCE(NULLIF(po.order_qty, ''), '0') AS numeric) as "orderQty",
|
||||||
|
CAST(COALESCE(NULLIF(po.received_qty, ''), '0') AS numeric) as "receiveQty",
|
||||||
|
CAST(COALESCE(NULLIF(po.unit_price, ''), '0') AS numeric) as "unitPrice",
|
||||||
|
CAST(COALESCE(NULLIF(po.amount, ''), '0') AS numeric) as "orderAmt",
|
||||||
|
CAST(COALESCE(NULLIF(po.received_qty, ''), '0') AS numeric)
|
||||||
|
* CAST(COALESCE(NULLIF(po.unit_price, ''), '0') AS numeric) as "receiveAmt",
|
||||||
|
1 as "orderCnt",
|
||||||
|
po.company_code
|
||||||
|
FROM purchase_order_mng po
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY date DESC NULLS LAST
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dataRows = await query(dataQuery, params);
|
||||||
|
|
||||||
|
logger.info("구매 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: dataRows,
|
||||||
|
filterOptions: {
|
||||||
|
suppliers: extractFilterSet(dataRows, "supplier"),
|
||||||
|
items: extractFilterSet(dataRows, "item"),
|
||||||
|
managers: extractFilterSet(dataRows, "manager"),
|
||||||
|
statuses: extractFilterSet(dataRows, "status"),
|
||||||
|
},
|
||||||
|
totalCount: dataRows.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("구매 리포트 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: "구매 리포트 데이터 조회에 실패했습니다", error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 품질 리포트
|
||||||
|
// ============================================
|
||||||
|
export async function getQualityReportData(req: any, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
|
||||||
|
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
const cf = buildCompanyFilter(companyCode, "pr", idx);
|
||||||
|
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
|
||||||
|
|
||||||
|
const df = buildDateFilter(startDate, endDate, "COALESCE(pr.production_date, pr.created_date::date::text)", idx);
|
||||||
|
conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx;
|
||||||
|
|
||||||
|
const whereClause = buildWhereClause(conditions);
|
||||||
|
|
||||||
|
const dataQuery = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(pr.production_date, pr.created_date::date::text) as date,
|
||||||
|
COALESCE(ii.item_name, wi.item_id, '미지정') as item,
|
||||||
|
'일반검사' as "defectType",
|
||||||
|
COALESCE(wi.routing, '미지정') as process,
|
||||||
|
COALESCE(pr.worker_name, '미지정') as inspector,
|
||||||
|
CAST(COALESCE(NULLIF(pr.production_qty, ''), '0') AS numeric) as "inspQty",
|
||||||
|
CAST(COALESCE(NULLIF(pr.production_qty, ''), '0') AS numeric)
|
||||||
|
- CAST(COALESCE(NULLIF(pr.defect_qty, ''), '0') AS numeric) as "passQty",
|
||||||
|
CAST(COALESCE(NULLIF(pr.defect_qty, ''), '0') AS numeric) as "defectQty",
|
||||||
|
0 as "reworkQty",
|
||||||
|
0 as "scrapQty",
|
||||||
|
0 as "claimCnt",
|
||||||
|
pr.company_code
|
||||||
|
FROM production_record pr
|
||||||
|
LEFT JOIN work_instruction wi ON pr.wo_id = wi.id AND pr.company_code = wi.company_code
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DISTINCT ON (item_number, company_code)
|
||||||
|
item_number, item_name, company_code
|
||||||
|
FROM item_info ORDER BY item_number, company_code, created_date DESC
|
||||||
|
) ii ON wi.item_id = ii.item_number AND wi.company_code = ii.company_code
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY date DESC NULLS LAST
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dataRows = await query(dataQuery, params);
|
||||||
|
|
||||||
|
logger.info("품질 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: dataRows,
|
||||||
|
filterOptions: {
|
||||||
|
items: extractFilterSet(dataRows, "item"),
|
||||||
|
defectTypes: [
|
||||||
|
{ value: "외관불량", label: "외관불량" }, { value: "치수불량", label: "치수불량" },
|
||||||
|
{ value: "기능불량", label: "기능불량" }, { value: "재질불량", label: "재질불량" },
|
||||||
|
{ value: "일반검사", label: "일반검사" },
|
||||||
|
],
|
||||||
|
processes: extractFilterSet(dataRows, "process"),
|
||||||
|
inspectors: extractFilterSet(dataRows, "inspector"),
|
||||||
|
},
|
||||||
|
totalCount: dataRows.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("품질 리포트 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: "품질 리포트 데이터 조회에 실패했습니다", error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 설비 리포트
|
||||||
|
// ============================================
|
||||||
|
export async function getEquipmentReportData(req: any, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
|
||||||
|
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
const cf = buildCompanyFilter(companyCode, "ei", idx);
|
||||||
|
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
|
||||||
|
|
||||||
|
const whereClause = buildWhereClause(conditions);
|
||||||
|
|
||||||
|
const dataQuery = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(ei.updated_date, ei.created_date)::date::text as date,
|
||||||
|
ei.equipment_code,
|
||||||
|
COALESCE(ei.equipment_name, ei.equipment_code) as equipment,
|
||||||
|
COALESCE(ei.equipment_type, '미지정') as "equipType",
|
||||||
|
COALESCE(ei.location, '미지정') as line,
|
||||||
|
COALESCE(ui.user_name, ei.manager_id, '미지정') as manager,
|
||||||
|
ei.status,
|
||||||
|
CAST(COALESCE(NULLIF(ei.capacity_per_day::text, ''), '0') AS numeric) as "runTime",
|
||||||
|
0 as "downTime",
|
||||||
|
100 as "opRate",
|
||||||
|
0 as "faultCnt",
|
||||||
|
0 as "mtbf",
|
||||||
|
0 as "mttr",
|
||||||
|
0 as "maintCost",
|
||||||
|
CAST(COALESCE(NULLIF(ei.capacity_per_day::text, ''), '0') AS numeric) as "prodQty",
|
||||||
|
ei.company_code
|
||||||
|
FROM equipment_info ei
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DISTINCT ON (user_id) user_id, user_name FROM user_info
|
||||||
|
) ui ON ei.manager_id = ui.user_id
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY equipment ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dataRows = await query(dataQuery, params);
|
||||||
|
|
||||||
|
logger.info("설비 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: dataRows,
|
||||||
|
filterOptions: {
|
||||||
|
equipment: extractFilterSet(dataRows, "equipment"),
|
||||||
|
equipTypes: extractFilterSet(dataRows, "equipType"),
|
||||||
|
lines: extractFilterSet(dataRows, "line"),
|
||||||
|
managers: extractFilterSet(dataRows, "manager"),
|
||||||
|
},
|
||||||
|
totalCount: dataRows.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("설비 리포트 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: "설비 리포트 데이터 조회에 실패했습니다", error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 금형 리포트
|
||||||
|
// ============================================
|
||||||
|
export async function getMoldReportData(req: any, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
|
||||||
|
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
const cf = buildCompanyFilter(companyCode, "mm", idx);
|
||||||
|
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
|
||||||
|
|
||||||
|
const whereClause = buildWhereClause(conditions);
|
||||||
|
|
||||||
|
const dataQuery = `
|
||||||
|
SELECT
|
||||||
|
COALESCE(mm.updated_date, mm.created_date)::date::text as date,
|
||||||
|
mm.mold_code,
|
||||||
|
COALESCE(mm.mold_name, mm.mold_code) as mold,
|
||||||
|
COALESCE(mm.mold_type, mm.category, '미지정') as "moldType",
|
||||||
|
COALESCE(ii.item_name, '미지정') as item,
|
||||||
|
COALESCE(mm.manufacturer, '미지정') as maker,
|
||||||
|
mm.operation_status as status,
|
||||||
|
CAST(COALESCE(NULLIF(mm.shot_count::text, ''), '0') AS numeric) as "shotCnt",
|
||||||
|
CAST(COALESCE(NULLIF(mm.warranty_shot_count::text, ''), '0') AS numeric) as "guaranteeShot",
|
||||||
|
CASE WHEN CAST(COALESCE(NULLIF(mm.warranty_shot_count::text, ''), '0') AS numeric) > 0
|
||||||
|
THEN ROUND(
|
||||||
|
CAST(COALESCE(NULLIF(mm.shot_count::text, ''), '0') AS numeric) * 100.0
|
||||||
|
/ CAST(COALESCE(NULLIF(mm.warranty_shot_count::text, ''), '1') AS numeric), 1)
|
||||||
|
ELSE 0 END as "lifeRate",
|
||||||
|
0 as "repairCnt",
|
||||||
|
0 as "repairCost",
|
||||||
|
0 as "prodQty",
|
||||||
|
0 as "defectRate",
|
||||||
|
CAST(COALESCE(NULLIF(mm.cavity_count::text, ''), '0') AS numeric) as "cavityUse",
|
||||||
|
mm.company_code
|
||||||
|
FROM mold_mng mm
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DISTINCT ON (item_number, company_code)
|
||||||
|
item_number, item_name, company_code
|
||||||
|
FROM item_info ORDER BY item_number, company_code, created_date DESC
|
||||||
|
) ii ON mm.mold_code = ii.item_number AND mm.company_code = ii.company_code
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY mold ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dataRows = await query(dataQuery, params);
|
||||||
|
|
||||||
|
logger.info("금형 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: dataRows,
|
||||||
|
filterOptions: {
|
||||||
|
molds: extractFilterSet(dataRows, "mold"),
|
||||||
|
moldTypes: extractFilterSet(dataRows, "moldType"),
|
||||||
|
items: extractFilterSet(dataRows, "item"),
|
||||||
|
makers: extractFilterSet(dataRows, "maker"),
|
||||||
|
},
|
||||||
|
totalCount: dataRows.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("금형 리포트 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: "금형 리포트 데이터 조회에 실패했습니다", error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { Response } from "express";
|
||||||
|
import { query } from "../database/db";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 영업 리포트 컨트롤러
|
||||||
|
* - 수주 데이터를 기반으로 집계/분석용 원본 데이터를 반환
|
||||||
|
* - 프론트엔드에서 그룹핑/집계/필터링 처리
|
||||||
|
*/
|
||||||
|
export async function getSalesReportData(
|
||||||
|
req: any,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
if (!companyCode) {
|
||||||
|
res.status(401).json({ success: false, message: "인증 정보가 없습니다" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
let paramIdx = 1;
|
||||||
|
|
||||||
|
// 멀티테넌시: 최고관리자는 전체, 일반 회사는 자기 데이터만
|
||||||
|
if (companyCode !== "*") {
|
||||||
|
conditions.push(`som.company_code = $${paramIdx}`);
|
||||||
|
params.push(companyCode);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜 필터 (due_date 또는 order_date 기준)
|
||||||
|
if (startDate) {
|
||||||
|
conditions.push(
|
||||||
|
`COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) >= $${paramIdx}`
|
||||||
|
);
|
||||||
|
params.push(startDate);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
conditions.push(
|
||||||
|
`COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) <= $${paramIdx}`
|
||||||
|
);
|
||||||
|
params.push(endDate);
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause =
|
||||||
|
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||||
|
|
||||||
|
const dataQuery = `
|
||||||
|
SELECT
|
||||||
|
som.order_no,
|
||||||
|
COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) as date,
|
||||||
|
som.order_date,
|
||||||
|
som.partner_id,
|
||||||
|
COALESCE(cm.customer_name, som.partner_id, '미지정') as customer,
|
||||||
|
sod.part_code,
|
||||||
|
COALESCE(ii.item_name, sod.part_name, sod.part_code, '미지정') as item,
|
||||||
|
CAST(COALESCE(NULLIF(sod.qty, ''), '0') AS numeric) as "orderQty",
|
||||||
|
CAST(COALESCE(NULLIF(sod.ship_qty, ''), '0') AS numeric) as "shipQty",
|
||||||
|
CAST(COALESCE(NULLIF(sod.unit_price, ''), '0') AS numeric) as "unitPrice",
|
||||||
|
CAST(COALESCE(NULLIF(sod.amount, ''), '0') AS numeric) as "orderAmt",
|
||||||
|
1 as "orderCount",
|
||||||
|
som.status,
|
||||||
|
som.company_code
|
||||||
|
FROM sales_order_mng som
|
||||||
|
JOIN sales_order_detail sod
|
||||||
|
ON som.order_no = sod.order_no
|
||||||
|
AND som.company_code = sod.company_code
|
||||||
|
LEFT JOIN customer_mng cm
|
||||||
|
ON som.partner_id = cm.customer_code
|
||||||
|
AND som.company_code = cm.company_code
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT DISTINCT ON (item_number, company_code)
|
||||||
|
item_number, item_name, company_code
|
||||||
|
FROM item_info
|
||||||
|
ORDER BY item_number, company_code, created_date DESC
|
||||||
|
) ii
|
||||||
|
ON sod.part_code = ii.item_number
|
||||||
|
AND sod.company_code = ii.company_code
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY date DESC NULLS LAST
|
||||||
|
`;
|
||||||
|
|
||||||
|
// query()는 rows 배열을 직접 반환
|
||||||
|
const dataRows = await query(dataQuery, params);
|
||||||
|
|
||||||
|
// 필터 옵션 조회 (거래처, 품목, 상태)
|
||||||
|
const filterParams: any[] = [];
|
||||||
|
let filterWhere = "";
|
||||||
|
|
||||||
|
if (companyCode !== "*") {
|
||||||
|
filterWhere = `WHERE company_code = $1`;
|
||||||
|
filterParams.push(companyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusWhere = filterWhere
|
||||||
|
? `${filterWhere} AND status IS NOT NULL`
|
||||||
|
: `WHERE status IS NOT NULL`;
|
||||||
|
|
||||||
|
const [customersRows, statusRows] = await Promise.all([
|
||||||
|
query(
|
||||||
|
`SELECT DISTINCT customer_code as value, customer_name as label
|
||||||
|
FROM customer_mng ${filterWhere}
|
||||||
|
ORDER BY customer_name`,
|
||||||
|
filterParams
|
||||||
|
),
|
||||||
|
query(
|
||||||
|
`SELECT DISTINCT status as value, status as label
|
||||||
|
FROM sales_order_mng ${statusWhere}
|
||||||
|
ORDER BY status`,
|
||||||
|
filterParams
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 품목은 데이터에서 추출 (실제 수주에 사용된 품목만)
|
||||||
|
const itemSet = new Map<string, string>();
|
||||||
|
dataRows.forEach((row: any) => {
|
||||||
|
if (row.part_code && !itemSet.has(row.part_code)) {
|
||||||
|
itemSet.set(row.part_code, row.item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const items = Array.from(itemSet.entries()).map(([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.info("영업 리포트 데이터 조회", {
|
||||||
|
companyCode,
|
||||||
|
rowCount: dataRows.length,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: dataRows,
|
||||||
|
filterOptions: {
|
||||||
|
customers: customersRows,
|
||||||
|
items,
|
||||||
|
statuses: statusRows,
|
||||||
|
},
|
||||||
|
totalCount: dataRows.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("영업 리포트 데이터 조회 실패", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "영업 리포트 데이터 조회에 실패했습니다",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
import {
|
||||||
|
getProductionReportData,
|
||||||
|
getInventoryReportData,
|
||||||
|
getPurchaseReportData,
|
||||||
|
getQualityReportData,
|
||||||
|
getEquipmentReportData,
|
||||||
|
getMoldReportData,
|
||||||
|
} from "../controllers/analyticsReportController";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(authenticateToken);
|
||||||
|
|
||||||
|
router.get("/production/data", getProductionReportData);
|
||||||
|
router.get("/inventory/data", getInventoryReportData);
|
||||||
|
router.get("/purchase/data", getPurchaseReportData);
|
||||||
|
router.get("/quality/data", getQualityReportData);
|
||||||
|
router.get("/equipment/data", getEquipmentReportData);
|
||||||
|
router.get("/mold/data", getMoldReportData);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
import { getSalesReportData } from "../controllers/salesReportController";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(authenticateToken);
|
||||||
|
|
||||||
|
// 영업 리포트 원본 데이터 조회
|
||||||
|
router.get("/data", getSalesReportData);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReportEngine, { ReportConfig } from "@/components/admin/report/ReportEngine";
|
||||||
|
|
||||||
|
const config: ReportConfig = {
|
||||||
|
key: "equipment_report_v2",
|
||||||
|
title: "설비 리포트",
|
||||||
|
description: "설비 가동/보전 다중 조건 비교 분석",
|
||||||
|
apiEndpoint: "/report/equipment/data",
|
||||||
|
metrics: [
|
||||||
|
{ id: "runTime", name: "가동시간", unit: "H", color: "#3b82f6" },
|
||||||
|
{ id: "downTime", name: "비가동시간", unit: "H", color: "#ef4444" },
|
||||||
|
{ id: "opRate", name: "가동률", unit: "%", color: "#10b981", isRate: true },
|
||||||
|
{ id: "faultCnt", name: "고장횟수", unit: "회", color: "#f59e0b" },
|
||||||
|
{ id: "mtbf", name: "MTBF", unit: "H", color: "#8b5cf6" },
|
||||||
|
{ id: "mttr", name: "MTTR", unit: "H", color: "#ec4899" },
|
||||||
|
{ id: "maintCost", name: "보전비용", unit: "만원", color: "#06b6d4" },
|
||||||
|
{ id: "prodQty", name: "생산수량", unit: "EA", color: "#84cc16" },
|
||||||
|
],
|
||||||
|
groupByOptions: [
|
||||||
|
{ id: "equipment", name: "설비별" },
|
||||||
|
{ id: "equipType", name: "설비유형별" },
|
||||||
|
{ id: "line", name: "라인별" },
|
||||||
|
{ id: "manager", name: "담당자별" },
|
||||||
|
{ id: "monthly", name: "월별" },
|
||||||
|
{ id: "quarterly", name: "분기별" },
|
||||||
|
{ id: "weekly", name: "주별" },
|
||||||
|
{ id: "daily", name: "일별" },
|
||||||
|
],
|
||||||
|
defaultGroupBy: "equipment",
|
||||||
|
defaultMetrics: ["runTime"],
|
||||||
|
thresholds: [
|
||||||
|
{ id: "fault", label: "고장횟수 ≥", defaultValue: 3, unit: "회" },
|
||||||
|
{ id: "opRate", label: "가동률 ≤", defaultValue: 80, unit: "%" },
|
||||||
|
],
|
||||||
|
filterFieldDefs: [
|
||||||
|
{ id: "equipment", name: "설비", type: "select", optionKey: "equipment" },
|
||||||
|
{ id: "equipType", name: "설비유형", type: "select", optionKey: "equipTypes" },
|
||||||
|
{ id: "line", name: "라인", type: "select", optionKey: "lines" },
|
||||||
|
{ id: "manager", name: "담당자", type: "select", optionKey: "managers" },
|
||||||
|
{ id: "opRate", name: "가동률", type: "number" },
|
||||||
|
{ id: "faultCnt", name: "고장횟수", type: "number" },
|
||||||
|
],
|
||||||
|
drilldownColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "equipment", name: "설비" },
|
||||||
|
{ id: "equipType", name: "설비유형" },
|
||||||
|
{ id: "line", name: "라인" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "runTime", name: "가동시간", align: "right", format: "number" },
|
||||||
|
{ id: "downTime", name: "비가동시간", align: "right", format: "number" },
|
||||||
|
{ id: "opRate", name: "가동률(%)", align: "right", format: "number" },
|
||||||
|
{ id: "faultCnt", name: "고장횟수", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
rawDataColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "equipment_code", name: "설비코드" },
|
||||||
|
{ id: "equipment", name: "설비명" },
|
||||||
|
{ id: "equipType", name: "설비유형" },
|
||||||
|
{ id: "line", name: "라인" },
|
||||||
|
{ id: "manager", name: "담당자" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "runTime", name: "가동시간", align: "right", format: "number" },
|
||||||
|
{ id: "downTime", name: "비가동시간", align: "right", format: "number" },
|
||||||
|
{ id: "faultCnt", name: "고장횟수", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
emptyMessage: "설비 데이터가 없습니다",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EquipmentReportPage() {
|
||||||
|
return <ReportEngine config={config} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReportEngine, { ReportConfig } from "@/components/admin/report/ReportEngine";
|
||||||
|
|
||||||
|
const config: ReportConfig = {
|
||||||
|
key: "inventory_report_v2",
|
||||||
|
title: "재고 리포트",
|
||||||
|
description: "재고 현황 다중 조건 비교 분석",
|
||||||
|
apiEndpoint: "/report/inventory/data",
|
||||||
|
metrics: [
|
||||||
|
{ id: "currentQty", name: "현재고", unit: "EA", color: "#3b82f6" },
|
||||||
|
{ id: "safetyQty", name: "안전재고", unit: "EA", color: "#10b981" },
|
||||||
|
{ id: "inQty", name: "입고수량", unit: "EA", color: "#f59e0b" },
|
||||||
|
{ id: "outQty", name: "출고수량", unit: "EA", color: "#ef4444" },
|
||||||
|
{ id: "turnover", name: "회전율", unit: "회", color: "#8b5cf6", isRate: true },
|
||||||
|
{ id: "stockValue", name: "재고금액", unit: "만원", color: "#ec4899" },
|
||||||
|
{ id: "shortageQty", name: "부족수량", unit: "EA", color: "#06b6d4" },
|
||||||
|
],
|
||||||
|
groupByOptions: [
|
||||||
|
{ id: "item", name: "품목별" },
|
||||||
|
{ id: "warehouse", name: "창고별" },
|
||||||
|
{ id: "category", name: "카테고리별" },
|
||||||
|
{ id: "monthly", name: "월별" },
|
||||||
|
{ id: "quarterly", name: "분기별" },
|
||||||
|
{ id: "weekly", name: "주별" },
|
||||||
|
{ id: "daily", name: "일별" },
|
||||||
|
],
|
||||||
|
defaultGroupBy: "item",
|
||||||
|
defaultMetrics: ["currentQty"],
|
||||||
|
thresholds: [
|
||||||
|
{ id: "safety", label: "안전재고 이하 경고", defaultValue: 0, unit: "EA" },
|
||||||
|
{ id: "turnover", label: "회전율 ≤", defaultValue: 2, unit: "회" },
|
||||||
|
],
|
||||||
|
filterFieldDefs: [
|
||||||
|
{ id: "item", name: "품목", type: "select", optionKey: "items" },
|
||||||
|
{ id: "warehouse", name: "창고", type: "select", optionKey: "warehouses" },
|
||||||
|
{ id: "category", name: "카테고리", type: "select", optionKey: "categories" },
|
||||||
|
{ id: "currentQty", name: "현재고", type: "number" },
|
||||||
|
{ id: "turnover", name: "회전율", type: "number" },
|
||||||
|
],
|
||||||
|
drilldownColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "item", name: "품목" },
|
||||||
|
{ id: "warehouse", name: "창고" },
|
||||||
|
{ id: "currentQty", name: "현재고", align: "right", format: "number" },
|
||||||
|
{ id: "safetyQty", name: "안전재고", align: "right", format: "number" },
|
||||||
|
{ id: "inQty", name: "입고", align: "right", format: "number" },
|
||||||
|
{ id: "outQty", name: "출고", align: "right", format: "number" },
|
||||||
|
{ id: "shortageQty", name: "부족", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
rawDataColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "item_code", name: "품목코드" },
|
||||||
|
{ id: "item", name: "품목명" },
|
||||||
|
{ id: "warehouse", name: "창고" },
|
||||||
|
{ id: "category", name: "카테고리" },
|
||||||
|
{ id: "currentQty", name: "현재고", align: "right", format: "number" },
|
||||||
|
{ id: "safetyQty", name: "안전재고", align: "right", format: "number" },
|
||||||
|
{ id: "inQty", name: "입고", align: "right", format: "number" },
|
||||||
|
{ id: "outQty", name: "출고", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
emptyMessage: "재고 데이터가 없습니다",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function InventoryReportPage() {
|
||||||
|
return <ReportEngine config={config} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReportEngine, { ReportConfig } from "@/components/admin/report/ReportEngine";
|
||||||
|
|
||||||
|
const config: ReportConfig = {
|
||||||
|
key: "mold_report_v2",
|
||||||
|
title: "금형 리포트",
|
||||||
|
description: "금형 수명/관리 다중 조건 비교 분석",
|
||||||
|
apiEndpoint: "/report/mold/data",
|
||||||
|
metrics: [
|
||||||
|
{ id: "shotCnt", name: "타수", unit: "회", color: "#3b82f6" },
|
||||||
|
{ id: "guaranteeShot", name: "보증타수", unit: "회", color: "#10b981" },
|
||||||
|
{ id: "lifeRate", name: "수명률", unit: "%", color: "#f59e0b", isRate: true },
|
||||||
|
{ id: "repairCnt", name: "수리횟수", unit: "회", color: "#ef4444" },
|
||||||
|
{ id: "repairCost", name: "수리비용", unit: "만원", color: "#8b5cf6" },
|
||||||
|
{ id: "prodQty", name: "생산수량", unit: "EA", color: "#ec4899" },
|
||||||
|
{ id: "defectRate", name: "불량률", unit: "%", color: "#06b6d4", isRate: true },
|
||||||
|
{ id: "cavityUse", name: "캐비티사용률", unit: "%", color: "#84cc16", isRate: true },
|
||||||
|
],
|
||||||
|
groupByOptions: [
|
||||||
|
{ id: "mold", name: "금형별" },
|
||||||
|
{ id: "moldType", name: "금형유형별" },
|
||||||
|
{ id: "item", name: "적용품목별" },
|
||||||
|
{ id: "maker", name: "제조사별" },
|
||||||
|
{ id: "monthly", name: "월별" },
|
||||||
|
{ id: "quarterly", name: "분기별" },
|
||||||
|
{ id: "weekly", name: "주별" },
|
||||||
|
{ id: "daily", name: "일별" },
|
||||||
|
],
|
||||||
|
defaultGroupBy: "mold",
|
||||||
|
defaultMetrics: ["shotCnt"],
|
||||||
|
thresholds: [
|
||||||
|
{ id: "life", label: "보증타수 도달률 ≥", defaultValue: 90, unit: "%" },
|
||||||
|
{ id: "cost", label: "수리비용 ≥", defaultValue: 100, unit: "만원" },
|
||||||
|
],
|
||||||
|
filterFieldDefs: [
|
||||||
|
{ id: "mold", name: "금형", type: "select", optionKey: "molds" },
|
||||||
|
{ id: "moldType", name: "금형유형", type: "select", optionKey: "moldTypes" },
|
||||||
|
{ id: "item", name: "적용품목", type: "select", optionKey: "items" },
|
||||||
|
{ id: "maker", name: "제조사", type: "select", optionKey: "makers" },
|
||||||
|
{ id: "shotCnt", name: "타수", type: "number" },
|
||||||
|
{ id: "lifeRate", name: "수명률", type: "number" },
|
||||||
|
],
|
||||||
|
drilldownColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "mold", name: "금형" },
|
||||||
|
{ id: "moldType", name: "금형유형" },
|
||||||
|
{ id: "item", name: "적용품목" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "shotCnt", name: "타수", align: "right", format: "number" },
|
||||||
|
{ id: "guaranteeShot", name: "보증타수", align: "right", format: "number" },
|
||||||
|
{ id: "lifeRate", name: "수명률(%)", align: "right", format: "number" },
|
||||||
|
{ id: "repairCnt", name: "수리횟수", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
rawDataColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "mold_code", name: "금형코드" },
|
||||||
|
{ id: "mold", name: "금형명" },
|
||||||
|
{ id: "moldType", name: "금형유형" },
|
||||||
|
{ id: "maker", name: "제조사" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "shotCnt", name: "타수", align: "right", format: "number" },
|
||||||
|
{ id: "guaranteeShot", name: "보증타수", align: "right", format: "number" },
|
||||||
|
{ id: "lifeRate", name: "수명률(%)", align: "right", format: "number" },
|
||||||
|
{ id: "repairCnt", name: "수리횟수", align: "right", format: "number" },
|
||||||
|
{ id: "repairCost", name: "수리비용", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
emptyMessage: "금형 데이터가 없습니다",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MoldReportPage() {
|
||||||
|
return <ReportEngine config={config} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReportEngine, { ReportConfig } from "@/components/admin/report/ReportEngine";
|
||||||
|
|
||||||
|
const config: ReportConfig = {
|
||||||
|
key: "production_report_v2",
|
||||||
|
title: "생산 리포트",
|
||||||
|
description: "생산 실적 다중 조건 비교 분석",
|
||||||
|
apiEndpoint: "/report/production/data",
|
||||||
|
metrics: [
|
||||||
|
{ id: "prodQty", name: "생산량", unit: "EA", color: "#3b82f6" },
|
||||||
|
{ id: "planQty", name: "계획수량", unit: "EA", color: "#10b981" },
|
||||||
|
{ id: "defectQty", name: "불량수량", unit: "EA", color: "#ef4444" },
|
||||||
|
{ id: "defectRate", name: "불량률", unit: "%", color: "#f59e0b", isRate: true },
|
||||||
|
{ id: "opRate", name: "가동률", unit: "%", color: "#8b5cf6", isRate: true },
|
||||||
|
{ id: "achRate", name: "달성률", unit: "%", color: "#ec4899", isRate: true },
|
||||||
|
{ id: "runTime", name: "가동시간", unit: "H", color: "#06b6d4" },
|
||||||
|
{ id: "downTime", name: "비가동시간", unit: "H", color: "#84cc16" },
|
||||||
|
],
|
||||||
|
groupByOptions: [
|
||||||
|
{ id: "process", name: "공정별" },
|
||||||
|
{ id: "equipment", name: "설비별" },
|
||||||
|
{ id: "item", name: "품목별" },
|
||||||
|
{ id: "worker", name: "작업자별" },
|
||||||
|
{ id: "monthly", name: "월별" },
|
||||||
|
{ id: "quarterly", name: "분기별" },
|
||||||
|
{ id: "weekly", name: "주별" },
|
||||||
|
{ id: "daily", name: "일별" },
|
||||||
|
],
|
||||||
|
defaultGroupBy: "process",
|
||||||
|
defaultMetrics: ["prodQty"],
|
||||||
|
thresholds: [
|
||||||
|
{ id: "defect", label: "불량률 ≥", defaultValue: 3, unit: "%" },
|
||||||
|
{ id: "opRate", label: "가동률 ≤", defaultValue: 85, unit: "%" },
|
||||||
|
],
|
||||||
|
filterFieldDefs: [
|
||||||
|
{ id: "process", name: "공정", type: "select", optionKey: "processes" },
|
||||||
|
{ id: "equipment", name: "설비", type: "select", optionKey: "equipment" },
|
||||||
|
{ id: "item", name: "품목", type: "select", optionKey: "items" },
|
||||||
|
{ id: "worker", name: "작업자", type: "select", optionKey: "workers" },
|
||||||
|
{ id: "prodQty", name: "생산량", type: "number" },
|
||||||
|
{ id: "defectRate", name: "불량률", type: "number" },
|
||||||
|
],
|
||||||
|
drilldownColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "process", name: "공정" },
|
||||||
|
{ id: "equipment", name: "설비" },
|
||||||
|
{ id: "item", name: "품목" },
|
||||||
|
{ id: "worker", name: "작업자" },
|
||||||
|
{ id: "prodQty", name: "생산량", align: "right", format: "number" },
|
||||||
|
{ id: "planQty", name: "계획수량", align: "right", format: "number" },
|
||||||
|
{ id: "defectQty", name: "불량수량", align: "right", format: "number" },
|
||||||
|
{ id: "defectRate", name: "불량률(%)", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
rawDataColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "process", name: "공정" },
|
||||||
|
{ id: "equipment", name: "설비" },
|
||||||
|
{ id: "item", name: "품목" },
|
||||||
|
{ id: "worker", name: "작업자" },
|
||||||
|
{ id: "prodQty", name: "생산량", align: "right", format: "number" },
|
||||||
|
{ id: "planQty", name: "계획수량", align: "right", format: "number" },
|
||||||
|
{ id: "defectQty", name: "불량수량", align: "right", format: "number" },
|
||||||
|
{ id: "runTime", name: "가동시간", align: "right", format: "number" },
|
||||||
|
{ id: "downTime", name: "비가동시간", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
enrichRow: (d) => ({
|
||||||
|
...d,
|
||||||
|
defectRate: d.prodQty > 0 ? parseFloat((d.defectQty / d.prodQty * 100).toFixed(1)) : 0,
|
||||||
|
opRate: (d.runTime + d.downTime) > 0
|
||||||
|
? parseFloat(((d.runTime / (d.runTime + d.downTime)) * 100).toFixed(1))
|
||||||
|
: 0,
|
||||||
|
achRate: d.planQty > 0 ? parseFloat((d.prodQty / d.planQty * 100).toFixed(1)) : 0,
|
||||||
|
}),
|
||||||
|
emptyMessage: "생산 데이터가 없습니다",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProductionReportPage() {
|
||||||
|
return <ReportEngine config={config} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReportEngine, { ReportConfig } from "@/components/admin/report/ReportEngine";
|
||||||
|
|
||||||
|
const config: ReportConfig = {
|
||||||
|
key: "purchase_report_v2",
|
||||||
|
title: "구매 리포트",
|
||||||
|
description: "구매/발주 다중 조건 비교 분석",
|
||||||
|
apiEndpoint: "/report/purchase/data",
|
||||||
|
metrics: [
|
||||||
|
{ id: "orderAmt", name: "발주금액", unit: "원", color: "#3b82f6" },
|
||||||
|
{ id: "receiveAmt", name: "입고금액", unit: "원", color: "#10b981" },
|
||||||
|
{ id: "orderQty", name: "발주수량", unit: "EA", color: "#f59e0b" },
|
||||||
|
{ id: "receiveQty", name: "입고수량", unit: "EA", color: "#8b5cf6" },
|
||||||
|
{ id: "receiveRate", name: "입고율", unit: "%", color: "#ef4444", isRate: true },
|
||||||
|
{ id: "unitPrice", name: "단가", unit: "원", color: "#ec4899" },
|
||||||
|
{ id: "orderCnt", name: "발주건수", unit: "건", color: "#f97316" },
|
||||||
|
],
|
||||||
|
groupByOptions: [
|
||||||
|
{ id: "supplier", name: "공급업체별" },
|
||||||
|
{ id: "item", name: "품목별" },
|
||||||
|
{ id: "manager", name: "구매담당별" },
|
||||||
|
{ id: "status", name: "상태별" },
|
||||||
|
{ id: "monthly", name: "월별" },
|
||||||
|
{ id: "quarterly", name: "분기별" },
|
||||||
|
{ id: "weekly", name: "주별" },
|
||||||
|
{ id: "daily", name: "일별" },
|
||||||
|
],
|
||||||
|
defaultGroupBy: "supplier",
|
||||||
|
defaultMetrics: ["orderAmt"],
|
||||||
|
thresholds: [
|
||||||
|
{ id: "delay", label: "납기지연 ≥", defaultValue: 3, unit: "일" },
|
||||||
|
{ id: "price", label: "단가변동률 ≥", defaultValue: 10, unit: "%" },
|
||||||
|
],
|
||||||
|
filterFieldDefs: [
|
||||||
|
{ id: "supplier", name: "공급업체", type: "select", optionKey: "suppliers" },
|
||||||
|
{ id: "item", name: "품목", type: "select", optionKey: "items" },
|
||||||
|
{ id: "manager", name: "구매담당", type: "select", optionKey: "managers" },
|
||||||
|
{ id: "status", name: "상태", type: "select", optionKey: "statuses" },
|
||||||
|
{ id: "orderAmt", name: "발주금액", type: "number" },
|
||||||
|
{ id: "orderQty", name: "발주수량", type: "number" },
|
||||||
|
],
|
||||||
|
drilldownColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "purchase_no", name: "발주번호" },
|
||||||
|
{ id: "supplier", name: "공급업체" },
|
||||||
|
{ id: "item", name: "품목" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "orderQty", name: "발주수량", align: "right", format: "number" },
|
||||||
|
{ id: "receiveQty", name: "입고수량", align: "right", format: "number" },
|
||||||
|
{ id: "unitPrice", name: "단가", align: "right", format: "number" },
|
||||||
|
{ id: "orderAmt", name: "발주금액", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
rawDataColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "purchase_no", name: "발주번호" },
|
||||||
|
{ id: "supplier", name: "공급업체" },
|
||||||
|
{ id: "item_code", name: "품목코드" },
|
||||||
|
{ id: "item", name: "품목명" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "orderQty", name: "발주수량", align: "right", format: "number" },
|
||||||
|
{ id: "receiveQty", name: "입고수량", align: "right", format: "number" },
|
||||||
|
{ id: "unitPrice", name: "단가", align: "right", format: "number" },
|
||||||
|
{ id: "orderAmt", name: "발주금액", align: "right", format: "number" },
|
||||||
|
{ id: "manager", name: "담당자" },
|
||||||
|
],
|
||||||
|
enrichRow: (d) => ({
|
||||||
|
...d,
|
||||||
|
receiveRate: d.orderQty > 0 ? parseFloat((d.receiveQty / d.orderQty * 100).toFixed(1)) : 0,
|
||||||
|
}),
|
||||||
|
emptyMessage: "구매 데이터가 없습니다",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PurchaseReportPage() {
|
||||||
|
return <ReportEngine config={config} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReportEngine, { ReportConfig } from "@/components/admin/report/ReportEngine";
|
||||||
|
|
||||||
|
const config: ReportConfig = {
|
||||||
|
key: "quality_report_v2",
|
||||||
|
title: "품질 리포트",
|
||||||
|
description: "품질/검사 다중 조건 비교 분석",
|
||||||
|
apiEndpoint: "/report/quality/data",
|
||||||
|
metrics: [
|
||||||
|
{ id: "defectQty", name: "불량수량", unit: "EA", color: "#ef4444" },
|
||||||
|
{ id: "defectRate", name: "불량률", unit: "%", color: "#f59e0b", isRate: true },
|
||||||
|
{ id: "inspQty", name: "검사수량", unit: "EA", color: "#3b82f6" },
|
||||||
|
{ id: "passQty", name: "합격수량", unit: "EA", color: "#10b981" },
|
||||||
|
{ id: "passRate", name: "합격률", unit: "%", color: "#8b5cf6", isRate: true },
|
||||||
|
{ id: "reworkQty", name: "재작업수량", unit: "EA", color: "#ec4899" },
|
||||||
|
{ id: "scrapQty", name: "폐기수량", unit: "EA", color: "#06b6d4" },
|
||||||
|
{ id: "claimCnt", name: "클레임건수", unit: "건", color: "#84cc16" },
|
||||||
|
],
|
||||||
|
groupByOptions: [
|
||||||
|
{ id: "item", name: "품목별" },
|
||||||
|
{ id: "defectType", name: "불량유형별" },
|
||||||
|
{ id: "process", name: "공정별" },
|
||||||
|
{ id: "inspector", name: "검사자별" },
|
||||||
|
{ id: "monthly", name: "월별" },
|
||||||
|
{ id: "quarterly", name: "분기별" },
|
||||||
|
{ id: "weekly", name: "주별" },
|
||||||
|
{ id: "daily", name: "일별" },
|
||||||
|
],
|
||||||
|
defaultGroupBy: "item",
|
||||||
|
defaultMetrics: ["defectQty"],
|
||||||
|
thresholds: [
|
||||||
|
{ id: "defectRate", label: "불량률 ≥", defaultValue: 5, unit: "%" },
|
||||||
|
{ id: "defectQty", label: "불량수량 ≥", defaultValue: 20, unit: "EA" },
|
||||||
|
],
|
||||||
|
filterFieldDefs: [
|
||||||
|
{ id: "item", name: "품목", type: "select", optionKey: "items" },
|
||||||
|
{ id: "defectType", name: "불량유형", type: "select", optionKey: "defectTypes" },
|
||||||
|
{ id: "process", name: "공정", type: "select", optionKey: "processes" },
|
||||||
|
{ id: "inspector", name: "검사자", type: "select", optionKey: "inspectors" },
|
||||||
|
{ id: "defectQty", name: "불량수량", type: "number" },
|
||||||
|
{ id: "defectRate", name: "불량률", type: "number" },
|
||||||
|
],
|
||||||
|
drilldownColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "item", name: "품목" },
|
||||||
|
{ id: "defectType", name: "불량유형" },
|
||||||
|
{ id: "process", name: "공정" },
|
||||||
|
{ id: "inspector", name: "검사자" },
|
||||||
|
{ id: "inspQty", name: "검사수량", align: "right", format: "number" },
|
||||||
|
{ id: "defectQty", name: "불량수량", align: "right", format: "number" },
|
||||||
|
{ id: "defectRate", name: "불량률(%)", align: "right", format: "number" },
|
||||||
|
{ id: "passRate", name: "합격률(%)", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
rawDataColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "item", name: "품목" },
|
||||||
|
{ id: "defectType", name: "불량유형" },
|
||||||
|
{ id: "process", name: "공정" },
|
||||||
|
{ id: "inspector", name: "검사자" },
|
||||||
|
{ id: "inspQty", name: "검사수량", align: "right", format: "number" },
|
||||||
|
{ id: "passQty", name: "합격수량", align: "right", format: "number" },
|
||||||
|
{ id: "defectQty", name: "불량수량", align: "right", format: "number" },
|
||||||
|
{ id: "reworkQty", name: "재작업", align: "right", format: "number" },
|
||||||
|
{ id: "scrapQty", name: "폐기", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
enrichRow: (d) => ({
|
||||||
|
...d,
|
||||||
|
defectRate: d.inspQty > 0 ? parseFloat((d.defectQty / d.inspQty * 100).toFixed(1)) : 0,
|
||||||
|
passRate: d.inspQty > 0 ? parseFloat((d.passQty / d.inspQty * 100).toFixed(1)) : 0,
|
||||||
|
}),
|
||||||
|
emptyMessage: "품질 데이터가 없습니다",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function QualityReportPage() {
|
||||||
|
return <ReportEngine config={config} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReportEngine, { ReportConfig } from "@/components/admin/report/ReportEngine";
|
||||||
|
|
||||||
|
const config: ReportConfig = {
|
||||||
|
key: "sales_report_v2",
|
||||||
|
title: "영업 리포트",
|
||||||
|
description: "다중 조건 비교 분석",
|
||||||
|
apiEndpoint: "/sales-report/data",
|
||||||
|
metrics: [
|
||||||
|
{ id: "orderAmt", name: "수주금액", unit: "원", color: "#3b82f6" },
|
||||||
|
{ id: "orderQty", name: "수주수량", unit: "EA", color: "#10b981" },
|
||||||
|
{ id: "shipQty", name: "출하수량", unit: "EA", color: "#ef4444" },
|
||||||
|
{ id: "unitPrice", name: "단가", unit: "원", color: "#8b5cf6" },
|
||||||
|
{ id: "orderCount", name: "수주건수", unit: "건", color: "#f59e0b" },
|
||||||
|
],
|
||||||
|
groupByOptions: [
|
||||||
|
{ id: "customer", name: "거래처별" },
|
||||||
|
{ id: "item", name: "품목별" },
|
||||||
|
{ id: "status", name: "상태별" },
|
||||||
|
{ id: "monthly", name: "월별" },
|
||||||
|
{ id: "quarterly", name: "분기별" },
|
||||||
|
{ id: "weekly", name: "주별" },
|
||||||
|
{ id: "daily", name: "일별" },
|
||||||
|
],
|
||||||
|
defaultGroupBy: "customer",
|
||||||
|
defaultMetrics: ["orderAmt"],
|
||||||
|
thresholds: [
|
||||||
|
{ id: "low", label: "목표 미달 ≤", defaultValue: 80, unit: "%" },
|
||||||
|
{ id: "high", label: "목표 초과 ≥", defaultValue: 120, unit: "%" },
|
||||||
|
],
|
||||||
|
filterFieldDefs: [
|
||||||
|
{ id: "customer", name: "거래처", type: "select", optionKey: "customers" },
|
||||||
|
{ id: "item", name: "품목", type: "select", optionKey: "items" },
|
||||||
|
{ id: "status", name: "상태", type: "select", optionKey: "statuses" },
|
||||||
|
{ id: "orderAmt", name: "수주금액", type: "number" },
|
||||||
|
{ id: "orderQty", name: "수주수량", type: "number" },
|
||||||
|
],
|
||||||
|
drilldownColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "order_no", name: "수주번호" },
|
||||||
|
{ id: "customer", name: "거래처" },
|
||||||
|
{ id: "item", name: "품목" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "orderQty", name: "수주수량", align: "right", format: "number" },
|
||||||
|
{ id: "unitPrice", name: "단가", align: "right", format: "number" },
|
||||||
|
{ id: "orderAmt", name: "수주금액", align: "right", format: "number" },
|
||||||
|
{ id: "shipQty", name: "출하수량", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
rawDataColumns: [
|
||||||
|
{ id: "date", name: "날짜", format: "date" },
|
||||||
|
{ id: "order_no", name: "수주번호" },
|
||||||
|
{ id: "customer", name: "거래처" },
|
||||||
|
{ id: "part_code", name: "품목코드" },
|
||||||
|
{ id: "item", name: "품목명" },
|
||||||
|
{ id: "status", name: "상태", format: "badge" },
|
||||||
|
{ id: "orderQty", name: "수주수량", align: "right", format: "number" },
|
||||||
|
{ id: "unitPrice", name: "단가", align: "right", format: "number" },
|
||||||
|
{ id: "orderAmt", name: "수주금액", align: "right", format: "number" },
|
||||||
|
{ id: "shipQty", name: "출하수량", align: "right", format: "number" },
|
||||||
|
],
|
||||||
|
emptyMessage: "수주 데이터가 없습니다",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SalesReportPage() {
|
||||||
|
return <ReportEngine config={config} />;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue