feat: Implement smart factory log functionality

- Added a new controller for managing smart factory logs, including retrieval and statistics endpoints.
- Integrated smart factory log migration to set up the necessary database structure.
- Enhanced the authentication controller to include user name in log submissions.
- Developed a frontend page for displaying and filtering smart factory logs, accessible only to super admins.
- Implemented API calls for fetching logs and statistics, improving data visibility and management.

These changes aim to provide comprehensive logging capabilities for smart factory activities, enhancing monitoring and analysis for administrators.
This commit is contained in:
kjs
2026-04-07 10:35:16 +09:00
parent c48dd95045
commit 822f9ac35a
9 changed files with 895 additions and 7 deletions

View File

@@ -0,0 +1,218 @@
// 스마트공장 활용 로그 조회 컨트롤러
// 최고관리자(*) 전용 — 회사별 필터링 가능
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 : "알 수 없는 오류",
},
});
}
};