feat: Implement smart factory schedule management functionality

- Added new API endpoints for managing smart factory schedules, including retrieval, creation, updating, and deletion of schedules.
- Integrated schedule management into the smart factory log controller, enhancing the overall functionality.
- Implemented a scheduler initialization process to automate daily plan generation and scheduled sends.
- Developed a frontend page for monitoring equipment, production, and quality, with real-time data fetching and auto-refresh capabilities.

These changes aim to provide comprehensive scheduling capabilities for smart factory operations, improving efficiency and operational visibility for users.
This commit is contained in:
kjs
2026-04-07 14:16:26 +09:00
parent 9aa8ca136b
commit c3e973bb1a
13 changed files with 3222 additions and 389 deletions

View File

@@ -5,6 +5,12 @@ 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
@@ -216,3 +222,283 @@ export const getSmartFactoryLogStats = async (
});
}
};
// ─── 스케줄 관리 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 } = 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<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
*/
export const runScheduleNowHandler = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
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<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 키 삭제 실패" });
}
};