219 lines
6.3 KiB
TypeScript
219 lines
6.3 KiB
TypeScript
|
|
// 스마트공장 활용 로그 조회 컨트롤러
|
||
|
|
// 최고관리자(*) 전용 — 회사별 필터링 가능
|
||
|
|
|
||
|
|
import { Response } from "express";
|
||
|
|
import { AuthenticatedRequest } from "../middleware/permissionMiddleware";
|
||
|
|
import { query, queryOne } from "../database/db";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* GET /api/admin/smart-factory-log
|
||
|
|
* 스마트공장 로그 목록 조회
|
||
|
|
*/
|
||
|
|
export const getSmartFactoryLogs = async (
|
||
|
|
req: AuthenticatedRequest,
|
||
|
|
res: Response
|
||
|
|
): Promise<void> => {
|
||
|
|
try {
|
||
|
|
const {
|
||
|
|
companyCode,
|
||
|
|
userId,
|
||
|
|
sendStatus,
|
||
|
|
dateFrom,
|
||
|
|
dateTo,
|
||
|
|
search,
|
||
|
|
page = "1",
|
||
|
|
limit = "50",
|
||
|
|
} = req.query;
|
||
|
|
|
||
|
|
const whereConditions: string[] = [];
|
||
|
|
const queryParams: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
// 회사 필터
|
||
|
|
if (companyCode && companyCode !== "all") {
|
||
|
|
whereConditions.push(`sfl.company_code = $${paramIndex}`);
|
||
|
|
queryParams.push(companyCode);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 사용자 필터
|
||
|
|
if (userId && (userId as string).trim()) {
|
||
|
|
whereConditions.push(`sfl.user_id ILIKE $${paramIndex}`);
|
||
|
|
queryParams.push(`%${(userId as string).trim()}%`);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 전송 상태 필터
|
||
|
|
if (sendStatus && sendStatus !== "all") {
|
||
|
|
whereConditions.push(`sfl.send_status = $${paramIndex}`);
|
||
|
|
queryParams.push(sendStatus);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 날짜 범위 필터
|
||
|
|
if (dateFrom) {
|
||
|
|
whereConditions.push(`sfl.created_at >= $${paramIndex}`);
|
||
|
|
queryParams.push(dateFrom);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
if (dateTo) {
|
||
|
|
whereConditions.push(`sfl.created_at < ($${paramIndex}::date + 1)`);
|
||
|
|
queryParams.push(dateTo);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 통합 검색
|
||
|
|
if (search && (search as string).trim()) {
|
||
|
|
whereConditions.push(
|
||
|
|
`(sfl.user_id ILIKE $${paramIndex} OR sfl.user_name ILIKE $${paramIndex} OR sfl.connect_ip ILIKE $${paramIndex} OR sfl.error_message ILIKE $${paramIndex})`
|
||
|
|
);
|
||
|
|
queryParams.push(`%${(search as string).trim()}%`);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause =
|
||
|
|
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
|
||
|
|
|
||
|
|
// 총 개수
|
||
|
|
const countResult = await queryOne<{ total: string }>(
|
||
|
|
`SELECT COUNT(*) as total FROM smart_factory_log sfl ${whereClause}`,
|
||
|
|
queryParams
|
||
|
|
);
|
||
|
|
const total = parseInt(countResult?.total || "0", 10);
|
||
|
|
|
||
|
|
// 페이지네이션
|
||
|
|
const pageNum = Math.max(1, parseInt(page as string, 10));
|
||
|
|
const limitNum = Math.min(100, Math.max(1, parseInt(limit as string, 10)));
|
||
|
|
const offset = (pageNum - 1) * limitNum;
|
||
|
|
|
||
|
|
// 데이터 조회 (회사명 JOIN)
|
||
|
|
const logs = await query<any>(
|
||
|
|
`SELECT sfl.*, cm.company_name
|
||
|
|
FROM smart_factory_log sfl
|
||
|
|
LEFT JOIN company_mng cm ON cm.company_code = sfl.company_code
|
||
|
|
${whereClause}
|
||
|
|
ORDER BY sfl.created_at DESC
|
||
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
||
|
|
[...queryParams, limitNum, offset]
|
||
|
|
);
|
||
|
|
|
||
|
|
res.status(200).json({
|
||
|
|
success: true,
|
||
|
|
data: logs,
|
||
|
|
total,
|
||
|
|
page: pageNum,
|
||
|
|
limit: limitNum,
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
logger.error("스마트공장 로그 조회 실패:", error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: "스마트공장 로그 조회 중 오류가 발생했습니다.",
|
||
|
|
error: {
|
||
|
|
code: "SERVER_ERROR",
|
||
|
|
details: error instanceof Error ? error.message : "알 수 없는 오류",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* GET /api/admin/smart-factory-log/stats
|
||
|
|
* 스마트공장 로그 통계 (회사별 요약)
|
||
|
|
*/
|
||
|
|
export const getSmartFactoryLogStats = async (
|
||
|
|
req: AuthenticatedRequest,
|
||
|
|
res: Response
|
||
|
|
): Promise<void> => {
|
||
|
|
try {
|
||
|
|
const { companyCode, days = "30" } = req.query;
|
||
|
|
const daysNum = parseInt(days as string, 10) || 30;
|
||
|
|
|
||
|
|
const whereConditions: string[] = [
|
||
|
|
`sfl.created_at >= NOW() - INTERVAL '${daysNum} days'`,
|
||
|
|
];
|
||
|
|
const queryParams: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
if (companyCode && companyCode !== "all") {
|
||
|
|
whereConditions.push(`sfl.company_code = $${paramIndex}`);
|
||
|
|
queryParams.push(companyCode);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
||
|
|
|
||
|
|
// 상태별 건수
|
||
|
|
const statusCounts = await query<{ send_status: string; count: string }>(
|
||
|
|
`SELECT send_status, COUNT(*) as count
|
||
|
|
FROM smart_factory_log sfl
|
||
|
|
${whereClause}
|
||
|
|
GROUP BY send_status`,
|
||
|
|
queryParams
|
||
|
|
);
|
||
|
|
|
||
|
|
// 회사별 건수
|
||
|
|
const companyCounts = await query<{
|
||
|
|
company_code: string;
|
||
|
|
company_name: string;
|
||
|
|
count: string;
|
||
|
|
}>(
|
||
|
|
`SELECT sfl.company_code, COALESCE(cm.company_name, sfl.company_code) as company_name, COUNT(*) as count
|
||
|
|
FROM smart_factory_log sfl
|
||
|
|
LEFT JOIN company_mng cm ON cm.company_code = sfl.company_code
|
||
|
|
${whereClause}
|
||
|
|
GROUP BY sfl.company_code, cm.company_name
|
||
|
|
ORDER BY count DESC`,
|
||
|
|
queryParams
|
||
|
|
);
|
||
|
|
|
||
|
|
// 일별 추이
|
||
|
|
const dailyCounts = await query<{ date: string; count: string }>(
|
||
|
|
`SELECT DATE(sfl.created_at) as date, COUNT(*) as count
|
||
|
|
FROM smart_factory_log sfl
|
||
|
|
${whereClause}
|
||
|
|
GROUP BY DATE(sfl.created_at)
|
||
|
|
ORDER BY date DESC
|
||
|
|
LIMIT ${daysNum}`,
|
||
|
|
queryParams
|
||
|
|
);
|
||
|
|
|
||
|
|
// 전체 건수
|
||
|
|
const totalResult = await queryOne<{ total: string }>(
|
||
|
|
`SELECT COUNT(*) as total FROM smart_factory_log sfl ${whereClause}`,
|
||
|
|
queryParams
|
||
|
|
);
|
||
|
|
|
||
|
|
res.status(200).json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
total: parseInt(totalResult?.total || "0", 10),
|
||
|
|
statusCounts: statusCounts.map((r) => ({
|
||
|
|
status: r.send_status,
|
||
|
|
count: parseInt(r.count, 10),
|
||
|
|
})),
|
||
|
|
companyCounts: companyCounts.map((r) => ({
|
||
|
|
companyCode: r.company_code,
|
||
|
|
companyName: r.company_name,
|
||
|
|
count: parseInt(r.count, 10),
|
||
|
|
})),
|
||
|
|
dailyCounts: dailyCounts.map((r) => ({
|
||
|
|
date: r.date,
|
||
|
|
count: parseInt(r.count, 10),
|
||
|
|
})),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
logger.error("스마트공장 로그 통계 조회 실패:", error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: "통계 조회 중 오류가 발생했습니다.",
|
||
|
|
error: {
|
||
|
|
code: "SERVER_ERROR",
|
||
|
|
details: error instanceof Error ? error.message : "알 수 없는 오류",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|