Merge branch 'main' of https://g.wace.me/jskim/vexplor_dev
This commit is contained in:
@@ -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: "즉시 전송 실패" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
getSchedules,
|
||||
upsertSchedule,
|
||||
deleteSchedule,
|
||||
runScheduleNowHandler,
|
||||
getTodayPlanHandler,
|
||||
getHolidays,
|
||||
addHoliday,
|
||||
@@ -46,6 +45,8 @@ import {
|
||||
getApiKeys,
|
||||
saveApiKey,
|
||||
deleteApiKey,
|
||||
getCompanyUsers,
|
||||
sendNow,
|
||||
} from "../controllers/smartFactoryLogController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { requireSuperAdmin } from "../middleware/permissionMiddleware";
|
||||
@@ -108,13 +109,17 @@ router.get("/smart-factory-log/schedules", requireSuperAdmin, getSchedules);
|
||||
router.get("/smart-factory-log/schedules/today-plan", requireSuperAdmin, getTodayPlanHandler);
|
||||
router.post("/smart-factory-log/schedules", requireSuperAdmin, upsertSchedule);
|
||||
router.delete("/smart-factory-log/schedules/:companyCode", requireSuperAdmin, deleteSchedule);
|
||||
router.post("/smart-factory-log/schedules/:companyCode/run-now", requireSuperAdmin, runScheduleNowHandler);
|
||||
|
||||
|
||||
// 스마트공장 공휴일 관리 (최고관리자 전용)
|
||||
router.get("/smart-factory-log/holidays", requireSuperAdmin, getHolidays);
|
||||
router.post("/smart-factory-log/holidays", requireSuperAdmin, addHoliday);
|
||||
router.delete("/smart-factory-log/holidays/:id", requireSuperAdmin, deleteHoliday);
|
||||
|
||||
// 스마트공장 즉시 전송 (최고관리자 전용)
|
||||
router.get("/smart-factory-log/users/:companyCode", requireSuperAdmin, getCompanyUsers);
|
||||
router.post("/smart-factory-log/send-now", requireSuperAdmin, sendNow);
|
||||
|
||||
// 스마트공장 API 키 관리 (최고관리자 전용)
|
||||
router.get("/smart-factory-log/api-keys", requireSuperAdmin, getApiKeys);
|
||||
router.post("/smart-factory-log/api-keys", requireSuperAdmin, saveApiKey);
|
||||
|
||||
@@ -16,7 +16,8 @@ interface ScheduledEntry {
|
||||
userId: string;
|
||||
userName: string;
|
||||
companyCode: string;
|
||||
scheduledTime: Date; // 초 단위까지 배정된 시각
|
||||
scheduledTime: Date;
|
||||
useType: "접속" | "종료";
|
||||
sent: boolean;
|
||||
}
|
||||
|
||||
@@ -74,17 +75,19 @@ export async function sendSmartFactoryLog(params: {
|
||||
dataUsgqty: "",
|
||||
};
|
||||
|
||||
const encodedLogData = encodeURIComponent(JSON.stringify(logData));
|
||||
const logDataJson = JSON.stringify(logData);
|
||||
|
||||
const response = await axios.get(SMART_FACTORY_LOG_URL, {
|
||||
params: { logData: encodedLogData },
|
||||
params: { logData: logDataJson },
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
logger.info("스마트공장 로그 전송 완료", {
|
||||
userId: params.userId,
|
||||
status: response.status,
|
||||
});
|
||||
const responseBody = typeof response.data === "string" ? response.data : JSON.stringify(response.data);
|
||||
|
||||
logger.info(`스마트공장 로그 전송 완료: userId=${params.userId}, status=${response.status}, body=${responseBody}`);
|
||||
|
||||
// 응답 body에 에러가 있을 수 있음 (HTTP 200이지만 실제 실패)
|
||||
const isRealSuccess = !responseBody.includes("FAIL") && !responseBody.includes("error") && !responseBody.includes("ERR");
|
||||
|
||||
await saveLog({
|
||||
companyCode: params.companyCode || "",
|
||||
@@ -92,9 +95,9 @@ export async function sendSmartFactoryLog(params: {
|
||||
userName: params.userName,
|
||||
useType,
|
||||
connectIp: params.remoteAddr,
|
||||
sendStatus: "SUCCESS",
|
||||
sendStatus: isRealSuccess ? "SUCCESS" : "FAIL",
|
||||
responseStatus: response.status,
|
||||
errorMessage: null,
|
||||
errorMessage: isRealSuccess ? null : responseBody,
|
||||
logDt: logTimeToUse,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -142,9 +145,8 @@ export async function initSmartFactoryScheduler(): Promise<void> {
|
||||
}
|
||||
}, { timezone: "Asia/Seoul" });
|
||||
|
||||
// 서버 시작 시 오늘 계획이 아직 없으면 바로 생성
|
||||
await planDailySends();
|
||||
|
||||
// 서버 시작 시에는 계획 생성하지 않음 (00:05 cron에서만 생성)
|
||||
// 서버 재시작 시 이미 지난 시각의 로그가 한꺼번에 전송되는 것 방지
|
||||
logger.info("스마트공장 로그 스케줄러 초기화 완료 (매일 00:05 계획 생성, 매분 전송 실행)");
|
||||
}
|
||||
|
||||
@@ -163,8 +165,9 @@ export async function planDailySends(): Promise<void> {
|
||||
time_end: string;
|
||||
exclude_weekend: boolean;
|
||||
exclude_holidays: boolean;
|
||||
daily_count: number;
|
||||
}>(
|
||||
"SELECT company_code, time_start, time_end, exclude_weekend, exclude_holidays FROM smart_factory_schedule WHERE is_active = true"
|
||||
"SELECT company_code, time_start, time_end, exclude_weekend, exclude_holidays, daily_count FROM smart_factory_schedule WHERE is_active = true"
|
||||
);
|
||||
|
||||
if (schedules.length === 0) return;
|
||||
@@ -173,7 +176,7 @@ export async function planDailySends(): Promise<void> {
|
||||
await refreshHolidayCache();
|
||||
|
||||
for (const schedule of schedules) {
|
||||
const { company_code, time_start, time_end, exclude_weekend, exclude_holidays } = schedule;
|
||||
const { company_code, time_start, time_end, exclude_weekend, exclude_holidays, daily_count } = schedule;
|
||||
|
||||
// 주말 체크
|
||||
if (exclude_weekend && (dayOfWeek === 0 || dayOfWeek === 6)) {
|
||||
@@ -225,11 +228,12 @@ export async function planDailySends(): Promise<void> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 랜덤 시각 배정 (초 단위)
|
||||
const entries = assignRandomTimes(attendees, today, time_start, time_end, company_code);
|
||||
// 접속/종료 쌍 + 다회 시각 배정
|
||||
const entries = assignSessionPairs(attendees, today, time_start, time_end, company_code, daily_count);
|
||||
dailyPlan.set(company_code, entries);
|
||||
|
||||
logger.info(`스마트공장 스케줄 ${company_code}: ${entries.length}/${pendingUsers.length}명 계획 생성 (${time_start}~${time_end})`);
|
||||
const sessionCount = entries.filter((e) => e.useType === "접속").length;
|
||||
logger.info(`스마트공장 스케줄 ${company_code}: ${attendees.length}명 × 최대${daily_count}회 = ${sessionCount}세션 계획 (${time_start}~${time_end})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,10 +249,7 @@ async function executeScheduledSends(): Promise<void> {
|
||||
if (entry.sent) continue;
|
||||
|
||||
const entryMinute = entry.scheduledTime.getHours() * 60 + entry.scheduledTime.getMinutes();
|
||||
if (entryMinute > currentMinute) continue; // 아직 안 됨
|
||||
if (entryMinute < currentMinute) {
|
||||
// 이미 지난 분인데 못 보낸 것 — 보냄
|
||||
}
|
||||
if (entryMinute !== currentMinute) continue; // 정확히 해당 분에만 전송
|
||||
|
||||
// 전송
|
||||
entry.sent = true;
|
||||
@@ -261,7 +262,7 @@ async function executeScheduledSends(): Promise<void> {
|
||||
userId: entry.userId,
|
||||
userName: entry.userName,
|
||||
remoteAddr: randomIp,
|
||||
useType: "접속",
|
||||
useType: entry.useType,
|
||||
companyCode: entry.companyCode,
|
||||
logTime: entry.scheduledTime,
|
||||
});
|
||||
@@ -275,71 +276,6 @@ async function executeScheduledSends(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 즉시 실행 (관리자 테스트용)
|
||||
*/
|
||||
export async function runScheduleNow(companyCode: string): Promise<{ total: number; sent: number; skipped: number }> {
|
||||
const schedule = await query<{
|
||||
time_start: string;
|
||||
time_end: string;
|
||||
}>(
|
||||
"SELECT time_start, time_end FROM smart_factory_schedule WHERE company_code = $1 AND is_active = true",
|
||||
[companyCode]
|
||||
);
|
||||
|
||||
if (schedule.length === 0) {
|
||||
throw new Error("활성 스케줄이 없습니다.");
|
||||
}
|
||||
|
||||
// API 키 확인
|
||||
const apiKey = await getApiKey(companyCode);
|
||||
if (!apiKey) {
|
||||
throw new Error("API 키가 설정되지 않았습니다. API 키 관리에서 먼저 등록해주세요.");
|
||||
}
|
||||
|
||||
const { time_start, time_end } = schedule[0];
|
||||
const today = new Date();
|
||||
|
||||
// 사용자 조회
|
||||
const users = await query<{ user_id: string; user_name: string }>(
|
||||
"SELECT user_id, user_name FROM user_info WHERE company_code = $1 AND (status = 'active' OR status IS NULL)",
|
||||
[companyCode]
|
||||
);
|
||||
|
||||
// 오늘 이미 전송된 사용자 제외
|
||||
const todayStr = formatDate(today);
|
||||
const alreadySent = await query<{ user_id: string }>(
|
||||
"SELECT DISTINCT user_id FROM smart_factory_log WHERE company_code = $1 AND send_status = 'SUCCESS' AND created_at >= $2::date AND created_at < ($2::date + 1)",
|
||||
[companyCode, todayStr]
|
||||
);
|
||||
const alreadySentSet = new Set(alreadySent.map((r) => r.user_id));
|
||||
const pendingUsers = users.filter((u) => !alreadySentSet.has(u.user_id));
|
||||
|
||||
let sent = 0;
|
||||
for (const user of pendingUsers) {
|
||||
// 시간 범위 내 랜덤 시각 생성
|
||||
const randomTime = generateRandomTime(today, time_start, time_end);
|
||||
const randomIp = `192.168.0.${Math.floor(Math.random() * 254) + 1}`;
|
||||
|
||||
try {
|
||||
await sendSmartFactoryLog({
|
||||
userId: user.user_id,
|
||||
userName: user.user_name,
|
||||
remoteAddr: randomIp,
|
||||
useType: "접속",
|
||||
companyCode,
|
||||
logTime: randomTime,
|
||||
});
|
||||
sent++;
|
||||
} catch (e) {
|
||||
logger.error(`스마트공장 즉시 전송 실패: ${user.user_id}`, e);
|
||||
}
|
||||
|
||||
await sleep(300);
|
||||
}
|
||||
|
||||
return { total: users.length, sent, skipped: alreadySentSet.size };
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘 실행 계획 현황 반환
|
||||
@@ -365,13 +301,17 @@ export function getTodayPlanStatus(): Array<{
|
||||
|
||||
// ─── 내부 함수 ───
|
||||
|
||||
/** 시간 범위 내에서 사용자들에게 랜덤 시각(초 단위) 배정 */
|
||||
function assignRandomTimes(
|
||||
/**
|
||||
* 사용자별 접속/종료 쌍을 생성
|
||||
* dailyCount: 최대 접속 횟수 (사용자별 1~dailyCount 랜덤)
|
||||
*/
|
||||
function assignSessionPairs(
|
||||
users: Array<{ user_id: string; user_name: string }>,
|
||||
today: Date,
|
||||
timeStart: string,
|
||||
timeEnd: string,
|
||||
companyCode: string
|
||||
companyCode: string,
|
||||
dailyCount: number
|
||||
): ScheduledEntry[] {
|
||||
const [startH, startM] = timeStart.split(":").map(Number);
|
||||
const [endH, endM] = timeEnd.split(":").map(Number);
|
||||
@@ -381,49 +321,68 @@ function assignRandomTimes(
|
||||
|
||||
if (totalSec <= 0) return [];
|
||||
|
||||
const slotSize = totalSec / users.length;
|
||||
const allEntries: ScheduledEntry[] = [];
|
||||
const maxCount = Math.max(1, Math.min(3, dailyCount));
|
||||
|
||||
const entries: ScheduledEntry[] = users.map((user, idx) => {
|
||||
// 각 슬롯 내에서 랜덤 오프셋 (초 단위)
|
||||
const slotStart = startSec + Math.floor(slotSize * idx);
|
||||
const randomOffset = Math.floor(Math.random() * slotSize);
|
||||
const assignedSec = Math.min(slotStart + randomOffset, endSec - 1);
|
||||
for (const user of users) {
|
||||
// 사용자별 1 ~ maxCount 사이 랜덤 횟수
|
||||
const count = Math.floor(Math.random() * maxCount) + 1;
|
||||
// 시간대를 횟수로 균등 분할
|
||||
const slotSec = Math.floor(totalSec / count);
|
||||
|
||||
const h = Math.floor(assignedSec / 3600);
|
||||
const m = Math.floor((assignedSec % 3600) / 60);
|
||||
const s = assignedSec % 60;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const slotStart = startSec + slotSec * i;
|
||||
const slotEnd = i < count - 1 ? slotStart + slotSec : endSec;
|
||||
|
||||
const scheduledTime = new Date(today);
|
||||
scheduledTime.setHours(h, m, s, Math.floor(Math.random() * 1000));
|
||||
// 접속 시각: 슬롯 전반부에서 랜덤
|
||||
const loginWindow = Math.floor((slotEnd - slotStart) * 0.4); // 슬롯의 앞 40%
|
||||
const loginSec = slotStart + Math.floor(Math.random() * Math.max(loginWindow, 60));
|
||||
const clampedLoginSec = Math.min(loginSec, endSec - 120); // 최소 2분 여유
|
||||
|
||||
return {
|
||||
userId: user.user_id,
|
||||
userName: user.user_name,
|
||||
companyCode,
|
||||
scheduledTime,
|
||||
sent: false,
|
||||
};
|
||||
});
|
||||
// 종료 시각: 접속 후 30분~2시간 사이 랜덤
|
||||
const minSession = 30 * 60; // 30분
|
||||
const maxSession = 120 * 60; // 2시간
|
||||
const sessionLen = minSession + Math.floor(Math.random() * (maxSession - minSession));
|
||||
const logoutSec = Math.min(clampedLoginSec + sessionLen, endSec - 1);
|
||||
|
||||
// 접속과 종료 시각이 너무 가까우면(2분 미만) 스킵
|
||||
if (logoutSec - clampedLoginSec < 120) continue;
|
||||
|
||||
const loginTime = secToDate(today, clampedLoginSec);
|
||||
const logoutTime = secToDate(today, logoutSec);
|
||||
|
||||
allEntries.push({
|
||||
userId: user.user_id,
|
||||
userName: user.user_name,
|
||||
companyCode,
|
||||
scheduledTime: loginTime,
|
||||
useType: "접속",
|
||||
sent: false,
|
||||
});
|
||||
|
||||
allEntries.push({
|
||||
userId: user.user_id,
|
||||
userName: user.user_name,
|
||||
companyCode,
|
||||
scheduledTime: logoutTime,
|
||||
useType: "종료",
|
||||
sent: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 시각순 정렬
|
||||
return entries.sort((a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime());
|
||||
return allEntries.sort((a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime());
|
||||
}
|
||||
|
||||
/** 단일 랜덤 시각 생성 (즉시 실행용) */
|
||||
function generateRandomTime(today: Date, timeStart: string, timeEnd: string): Date {
|
||||
const [startH, startM] = timeStart.split(":").map(Number);
|
||||
const [endH, endM] = timeEnd.split(":").map(Number);
|
||||
const startSec = startH * 3600 + startM * 60;
|
||||
const endSec = endH * 3600 + endM * 60;
|
||||
const randomSec = startSec + Math.floor(Math.random() * (endSec - startSec));
|
||||
|
||||
const h = Math.floor(randomSec / 3600);
|
||||
const m = Math.floor((randomSec % 3600) / 60);
|
||||
const s = randomSec % 60;
|
||||
|
||||
const time = new Date(today);
|
||||
time.setHours(h, m, s, Math.floor(Math.random() * 1000));
|
||||
return time;
|
||||
/** 초(하루 내)를 Date로 변환 */
|
||||
function secToDate(today: Date, sec: number): Date {
|
||||
const h = Math.floor(sec / 3600);
|
||||
const m = Math.floor((sec % 3600) / 60);
|
||||
const s = sec % 60;
|
||||
const d = new Date(today);
|
||||
d.setHours(h, m, s, Math.floor(Math.random() * 1000));
|
||||
return d;
|
||||
}
|
||||
|
||||
/** 공휴일 캐시 갱신 */
|
||||
|
||||
Reference in New Issue
Block a user