// 스마트공장 활용 로그 조회 컨트롤러 // 최고관리자(*) 전용 — 회사별 필터링 가능 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 { runScheduleNow, getTodayPlanStatus, planDailySends, } from "../utils/smartFactoryLog"; /** * GET /api/admin/smart-factory-log * 스마트공장 로그 목록 조회 */ export const getSmartFactoryLogs = async ( req: AuthenticatedRequest, res: Response ): Promise => { 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( `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 => { 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 => { try { const schedules = await query( `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 => { try { const { companyCode, isActive, timeStart, timeEnd, excludeWeekend, excludeHolidays } = 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, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW()) ON CONFLICT (company_code) DO UPDATE SET is_active = $2, time_start = $3, time_end = $4, exclude_weekend = $5, exclude_holidays = $6, updated_at = NOW()`, [ companyCode, isActive ?? false, timeStart || "08:30", timeEnd || "17:30", excludeWeekend ?? true, excludeHolidays ?? true, ] ); // 스케줄 변경 시 오늘 계획 재생성 await planDailySends(); res.json({ success: true, message: "스케줄이 저장되었습니다." }); } 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 => { 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 */ export const runScheduleNowHandler = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { const { companyCode } = req.params; const result = await runScheduleNow(companyCode); res.json({ success: true, data: result }); } catch (error) { logger.error("즉시 실행 실패:", error); res.status(500).json({ success: false, message: error instanceof Error ? error.message : "즉시 실행 실패", }); } }; /** * GET /api/admin/smart-factory-log/schedules/today-plan */ export const getTodayPlanHandler = async ( req: AuthenticatedRequest, res: Response ): Promise => { 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 => { try { const holidays = await query( "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 => { 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 => { 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 => { try { const companies = await query( `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 => { 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 => { 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 키 삭제 실패" }); } };