Merge branch 'main' of https://g.wace.me/jskim/vexplor_dev
Some checks failed
Build and Push Images / build-and-push (push) Failing after 56s

This commit is contained in:
SeongHyun Kim
2026-04-08 10:19:55 +09:00
54 changed files with 34351 additions and 9099 deletions

View File

@@ -7,9 +7,8 @@ import { query, queryOne } from "../database/db";
import { logger } from "../utils/logger";
import { encryptionService } from "../services/encryptionService";
import {
runScheduleNow,
sendSmartFactoryLog,
getTodayPlanStatus,
planDailySends,
} from "../utils/smartFactoryLog";
/**
@@ -254,7 +253,7 @@ export const upsertSchedule = async (
res: Response
): Promise<void> => {
try {
const { companyCode, isActive, timeStart, timeEnd, excludeWeekend, excludeHolidays } = req.body;
const { companyCode, isActive, timeStart, timeEnd, excludeWeekend, excludeHolidays, dailyCount } = req.body;
if (!companyCode) {
res.status(400).json({ success: false, message: "회사코드는 필수입니다." });
@@ -262,11 +261,11 @@ export const upsertSchedule = async (
}
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())
`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, updated_at = NOW()`,
exclude_weekend = $5, exclude_holidays = $6, daily_count = $7, updated_at = NOW()`,
[
companyCode,
isActive ?? false,
@@ -274,13 +273,12 @@ export const upsertSchedule = async (
timeEnd || "17:30",
excludeWeekend ?? true,
excludeHolidays ?? true,
Math.max(1, Math.min(3, dailyCount || 1)),
]
);
// 스케줄 변경 시 오늘 계획 재생성
await planDailySends();
res.json({ success: true, message: "스케줄이 저장되었습니다." });
// 계획은 매일 00:05에만 생성 (즉시 재생성하면 지난 시각 소급 전송 위험)
res.json({ success: true, message: "스케줄이 저장되었습니다. 내일 00:05부터 적용됩니다." });
} catch (error) {
logger.error("스케줄 저장 실패:", error);
res.status(500).json({ success: false, message: "스케줄 저장 실패" });
@@ -307,23 +305,6 @@ export const deleteSchedule = async (
/**
* 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
*/
@@ -502,3 +483,92 @@ export const deleteApiKey = async (
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: "즉시 전송 실패" });
}
};