Files
vexplor_dev/backend-node/src/controllers/smartFactoryLogController.ts

575 lines
17 KiB
TypeScript
Raw Normal View History

// 스마트공장 활용 로그 조회 컨트롤러
// 최고관리자(*) 전용 — 회사별 필터링 가능
import { Response } from "express";
import { AuthenticatedRequest } from "../middleware/permissionMiddleware";
import { query, queryOne } from "../database/db";
import { logger } from "../utils/logger";
import { encryptionService } from "../services/encryptionService";
import {
sendSmartFactoryLog,
getTodayPlanStatus,
} from "../utils/smartFactoryLog";
/**
* 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 : "알 수 없는 오류",
},
});
}
};
// ─── 스케줄 관리 API ───
/**
* GET /api/admin/smart-factory-log/schedules
*/
export const getSchedules = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const schedules = await query<any>(
`SELECT s.*, cm.company_name
FROM smart_factory_schedule s
LEFT JOIN company_mng cm ON cm.company_code = s.company_code
ORDER BY s.company_code`
);
res.json({ success: true, data: schedules });
} catch (error) {
logger.error("스케줄 조회 실패:", error);
res.status(500).json({ success: false, message: "스케줄 조회 실패" });
}
};
/**
* POST /api/admin/smart-factory-log/schedules
*/
export const upsertSchedule = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode, isActive, timeStart, timeEnd, excludeWeekend, excludeHolidays, dailyCount } = req.body;
if (!companyCode) {
res.status(400).json({ success: false, message: "회사코드는 필수입니다." });
return;
}
await query(
`INSERT INTO smart_factory_schedule (company_code, is_active, time_start, time_end, exclude_weekend, exclude_holidays, daily_count, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (company_code) DO UPDATE SET
is_active = $2, time_start = $3, time_end = $4,
exclude_weekend = $5, exclude_holidays = $6, daily_count = $7, updated_at = NOW()`,
[
companyCode,
isActive ?? false,
timeStart || "08:30",
timeEnd || "17:30",
excludeWeekend ?? true,
excludeHolidays ?? true,
Math.max(1, Math.min(3, dailyCount || 1)),
]
);
// 계획은 매일 00:05에만 생성 (즉시 재생성하면 지난 시각 소급 전송 위험)
res.json({ success: true, message: "스케줄이 저장되었습니다. 내일 00:05부터 적용됩니다." });
} catch (error) {
logger.error("스케줄 저장 실패:", error);
res.status(500).json({ success: false, message: "스케줄 저장 실패" });
}
};
/**
* DELETE /api/admin/smart-factory-log/schedules/:companyCode
*/
export const deleteSchedule = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode } = req.params;
await query("DELETE FROM smart_factory_schedule WHERE company_code = $1", [companyCode]);
res.json({ success: true, message: "스케줄이 삭제되었습니다." });
} catch (error) {
logger.error("스케줄 삭제 실패:", error);
res.status(500).json({ success: false, message: "스케줄 삭제 실패" });
}
};
/**
* POST /api/admin/smart-factory-log/schedules/:companyCode/run-now
*/
/**
* GET /api/admin/smart-factory-log/schedules/today-plan
*/
export const getTodayPlanHandler = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const plan = getTodayPlanStatus();
res.json({ success: true, data: plan });
} catch (error) {
logger.error("오늘 계획 조회 실패:", error);
res.status(500).json({ success: false, message: "오늘 계획 조회 실패" });
}
};
// ─── 공휴일 관리 API ───
/**
* GET /api/admin/smart-factory-log/holidays
*/
export const getHolidays = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const holidays = await query<any>(
"SELECT id, holiday_date, holiday_name, created_at FROM smart_factory_holidays ORDER BY holiday_date"
);
res.json({ success: true, data: holidays });
} catch (error) {
logger.error("공휴일 조회 실패:", error);
res.status(500).json({ success: false, message: "공휴일 조회 실패" });
}
};
/**
* POST /api/admin/smart-factory-log/holidays
*/
export const addHoliday = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { holidayDate, holidayName } = req.body;
if (!holidayDate || !holidayName) {
res.status(400).json({ success: false, message: "날짜와 이름은 필수입니다." });
return;
}
await query(
"INSERT INTO smart_factory_holidays (holiday_date, holiday_name) VALUES ($1, $2) ON CONFLICT (holiday_date) DO UPDATE SET holiday_name = $2",
[holidayDate, holidayName]
);
res.json({ success: true, message: "공휴일이 추가되었습니다." });
} catch (error) {
logger.error("공휴일 추가 실패:", error);
res.status(500).json({ success: false, message: "공휴일 추가 실패" });
}
};
/**
* DELETE /api/admin/smart-factory-log/holidays/:id
*/
export const deleteHoliday = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { id } = req.params;
await query("DELETE FROM smart_factory_holidays WHERE id = $1", [id]);
res.json({ success: true, message: "공휴일이 삭제되었습니다." });
} catch (error) {
logger.error("공휴일 삭제 실패:", error);
res.status(500).json({ success: false, message: "공휴일 삭제 실패" });
}
};
// ─── API 키 관리 ───
/**
* GET /api/admin/smart-factory-log/api-keys
* + API (DB키 , )
*/
export const getApiKeys = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const companies = await query<any>(
`SELECT cm.company_code, cm.company_name, ak.api_key
FROM company_mng cm
LEFT JOIN smart_factory_api_keys ak ON ak.company_code = cm.company_code
WHERE cm.company_code != '*'
ORDER BY cm.company_code`
);
const result = companies.map((c: any) => {
let dbKeyDecrypted: string | null = null;
if (c.api_key) {
try {
dbKeyDecrypted = encryptionService.decrypt(c.api_key);
} catch {
dbKeyDecrypted = "(복호화 실패)";
}
}
return {
companyCode: c.company_code,
companyName: c.company_name,
hasDbKey: !!c.api_key,
dbKey: dbKeyDecrypted,
hasEnvKey: !!process.env[`SMART_FACTORY_API_KEY_${c.company_code}`],
};
});
res.json({ success: true, data: result });
} catch (error) {
logger.error("API 키 목록 조회 실패:", error);
res.status(500).json({ success: false, message: "API 키 목록 조회 실패" });
}
};
/**
* POST /api/admin/smart-factory-log/api-keys
* API ()
*/
export const saveApiKey = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode, apiKey } = req.body;
if (!companyCode || !apiKey) {
res.status(400).json({ success: false, message: "회사코드와 API 키는 필수입니다." });
return;
}
const encrypted = encryptionService.encrypt(apiKey);
await query(
`INSERT INTO smart_factory_api_keys (company_code, api_key, updated_at)
VALUES ($1, $2, NOW())
ON CONFLICT (company_code) DO UPDATE SET api_key = $2, updated_at = NOW()`,
[companyCode, encrypted]
);
res.json({ success: true, message: "API 키가 저장되었습니다." });
} catch (error) {
logger.error("API 키 저장 실패:", error);
res.status(500).json({ success: false, message: "API 키 저장 실패" });
}
};
/**
* DELETE /api/admin/smart-factory-log/api-keys/:companyCode
* API ( )
*/
export const deleteApiKey = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode } = req.params;
await query(
"DELETE FROM smart_factory_api_keys WHERE company_code = $1",
[companyCode]
);
res.json({ success: true, message: "API 키가 삭제되었습니다." });
} catch (error) {
logger.error("API 키 삭제 실패:", error);
res.status(500).json({ success: false, message: "API 키 삭제 실패" });
}
};
// ─── 즉시 전송 ───
/**
* GET /api/admin/smart-factory-log/users/:companyCode
* ( )
*/
export const getCompanyUsers = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode } = req.params;
const users = await query<any>(
`SELECT user_id, user_name, dept_name
FROM user_info
WHERE company_code = $1 AND (status = 'active' OR status IS NULL)
ORDER BY user_name`,
[companyCode]
);
res.json({ success: true, data: users });
} catch (error) {
logger.error("사용자 목록 조회 실패:", error);
res.status(500).json({ success: false, message: "사용자 목록 조회 실패" });
}
};
/**
* POST /api/admin/smart-factory-log/send-now
*
* body: { companyCode, userIds: string[], timeStart?, timeEnd? }
*/
export const sendNow = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode, userIds } = req.body;
logger.info(`=== 즉시 전송 API 호출 === companyCode=${companyCode}, userIds=${JSON.stringify(userIds)}`);
if (!companyCode || !userIds || userIds.length === 0) {
res.status(400).json({ success: false, message: "회사코드와 사용자를 선택해주세요." });
return;
}
// 사용자 정보 조회
const users = await query<{ user_id: string; user_name: string }>(
`SELECT user_id, user_name FROM user_info WHERE company_code = $1 AND user_id = ANY($2)`,
[companyCode, userIds]
);
logger.info(`즉시 전송 대상: ${users.length}명 (조회된 사용자: ${users.map(u => u.user_id).join(", ")})`);
// 현재 시간으로 즉시 전송
let success = 0;
let fail = 0;
const remoteAddr = req.ip || "127.0.0.1";
for (const user of users) {
try {
logger.info(`즉시 전송 시작: ${user.user_id}`);
await sendSmartFactoryLog({
userId: user.user_id,
userName: user.user_name,
remoteAddr,
useType: "접속",
companyCode,
});
success++;
logger.info(`즉시 전송 성공: ${user.user_id}`);
} catch (e) {
fail++;
logger.error(`즉시 전송 실패: ${user.user_id}`, e);
}
}
res.json({
success: true,
data: { total: users.length, success, fail },
message: `${success}명 전송 완료${fail > 0 ? `, ${fail}명 실패` : ""}`,
});
} catch (error) {
logger.error("즉시 전송 실패:", error);
res.status(500).json({ success: false, message: "즉시 전송 실패" });
}
};