250 lines
8.0 KiB
JavaScript
250 lines
8.0 KiB
JavaScript
/**
|
|
* Scheduler Service
|
|
*
|
|
* 주간 자동 영상 생성을 위한 cron job 스케줄러
|
|
* - 매 시간마다 예약된 작업 확인
|
|
* - generation_queue에 작업 추가
|
|
* - 워커가 순차적으로 처리
|
|
*/
|
|
|
|
const cron = require('node-cron');
|
|
const db = require('../db');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* 예약된 자동 생성 작업 확인
|
|
*/
|
|
const getPendingGenerationJobs = () => {
|
|
return new Promise((resolve, reject) => {
|
|
const now = new Date().toISOString();
|
|
db.all(`
|
|
SELECT ags.*, u.username, u.email, pp.brand_name, pp.description, pp.address, pp.pension_types
|
|
FROM auto_generation_settings ags
|
|
JOIN users u ON ags.user_id = u.id
|
|
LEFT JOIN pension_profiles pp ON ags.pension_id = pp.id
|
|
WHERE ags.enabled = 1
|
|
AND ags.next_scheduled_at <= ?
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM generation_queue gq
|
|
WHERE gq.user_id = ags.user_id
|
|
AND gq.status IN ('pending', 'processing')
|
|
)
|
|
`, [now], (err, rows) => {
|
|
if (err) {
|
|
console.error('예약 작업 조회 실패:', err);
|
|
return resolve([]);
|
|
}
|
|
resolve(rows || []);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 작업 큐에 추가
|
|
*/
|
|
const addToQueue = (userId, pensionId) => {
|
|
return new Promise((resolve, reject) => {
|
|
db.run(`
|
|
INSERT INTO generation_queue (user_id, pension_id, status, scheduled_at)
|
|
VALUES (?, ?, 'pending', datetime('now'))
|
|
`, [userId, pensionId], function(err) {
|
|
if (err) {
|
|
console.error('큐 추가 실패:', err);
|
|
return resolve(null);
|
|
}
|
|
resolve(this.lastID);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 다음 예약 시간 업데이트
|
|
*/
|
|
const updateNextScheduledTime = (userId, dayOfWeek, timeOfDay) => {
|
|
return new Promise((resolve, reject) => {
|
|
const now = new Date();
|
|
const [hours, minutes] = (timeOfDay || '09:00').split(':').map(Number);
|
|
|
|
let next = new Date(now);
|
|
next.setHours(hours, minutes, 0, 0);
|
|
|
|
// 다음 주 같은 요일로 설정
|
|
const daysUntilNext = (dayOfWeek - now.getDay() + 7) % 7 || 7;
|
|
next.setDate(next.getDate() + daysUntilNext);
|
|
|
|
const nextIso = next.toISOString();
|
|
|
|
db.run(`
|
|
UPDATE auto_generation_settings
|
|
SET next_scheduled_at = ?, last_generated_at = datetime('now')
|
|
WHERE user_id = ?
|
|
`, [nextIso, userId], (err) => {
|
|
if (err) console.error('다음 예약 시간 업데이트 실패:', err);
|
|
resolve(nextIso);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 큐에서 대기 중인 작업 처리
|
|
*/
|
|
const processPendingQueue = async () => {
|
|
return new Promise((resolve, reject) => {
|
|
db.get(`
|
|
SELECT gq.*, pp.brand_name, pp.description, pp.address, pp.pension_types, pp.user_id
|
|
FROM generation_queue gq
|
|
LEFT JOIN pension_profiles pp ON gq.pension_id = pp.id
|
|
WHERE gq.status = 'pending'
|
|
ORDER BY gq.scheduled_at ASC
|
|
LIMIT 1
|
|
`, async (err, job) => {
|
|
if (err || !job) {
|
|
return resolve(null);
|
|
}
|
|
|
|
console.log(`[Scheduler] 작업 처리 시작: 사용자 ${job.user_id}, 펜션 ${job.pension_id}`);
|
|
|
|
// 상태를 processing으로 변경
|
|
db.run(`
|
|
UPDATE generation_queue
|
|
SET status = 'processing', started_at = datetime('now')
|
|
WHERE id = ?
|
|
`, [job.id]);
|
|
|
|
try {
|
|
// 영상 생성 로직 실행
|
|
const result = await generateVideoForJob(job);
|
|
|
|
// 성공 시 상태 업데이트
|
|
db.run(`
|
|
UPDATE generation_queue
|
|
SET status = 'completed', completed_at = datetime('now'),
|
|
result_video_path = ?, result_history_id = ?
|
|
WHERE id = ?
|
|
`, [result.videoPath, result.historyId, job.id]);
|
|
|
|
console.log(`[Scheduler] 작업 완료: ${job.id}`);
|
|
resolve(result);
|
|
|
|
} catch (error) {
|
|
console.error(`[Scheduler] 작업 실패: ${job.id}`, error);
|
|
|
|
// 실패 시 상태 업데이트
|
|
db.run(`
|
|
UPDATE generation_queue
|
|
SET status = 'failed', completed_at = datetime('now'),
|
|
error_message = ?
|
|
WHERE id = ?
|
|
`, [error.message, job.id]);
|
|
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 실제 영상 생성 실행
|
|
* TODO: 실제 영상 생성 로직과 연동 필요
|
|
*/
|
|
const generateVideoForJob = async (job) => {
|
|
// 펜션 정보 기반으로 영상 생성
|
|
const businessInfo = {
|
|
name: job.brand_name || '펜션',
|
|
description: job.description || '',
|
|
address: job.address || '',
|
|
pensionCategories: job.pension_types ? JSON.parse(job.pension_types) : []
|
|
};
|
|
|
|
// 실제 영상 생성 API 호출 (내부 API 사용)
|
|
// 현재는 placeholder - 실제 구현 시 render API 호출 필요
|
|
console.log('[Scheduler] 영상 생성 요청:', businessInfo.name);
|
|
|
|
// TODO: 실제 영상 생성 로직 구현
|
|
// const response = await fetch('http://localhost:3001/render', { ... });
|
|
|
|
return {
|
|
videoPath: null, // 실제 생성된 비디오 경로
|
|
historyId: null // 히스토리 ID
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 스케줄러 시작
|
|
*/
|
|
const startScheduler = () => {
|
|
console.log('[Scheduler] 자동 생성 스케줄러 시작');
|
|
|
|
// 매 시간 정각에 실행 (0분)
|
|
cron.schedule('0 * * * *', async () => {
|
|
console.log(`[Scheduler] 예약 작업 확인 중... ${new Date().toISOString()}`);
|
|
|
|
try {
|
|
// 예약된 작업 확인
|
|
const pendingJobs = await getPendingGenerationJobs();
|
|
console.log(`[Scheduler] ${pendingJobs.length}개의 예약 작업 발견`);
|
|
|
|
for (const job of pendingJobs) {
|
|
// 큐에 작업 추가
|
|
await addToQueue(job.user_id, job.pension_id);
|
|
|
|
// 다음 예약 시간 업데이트
|
|
await updateNextScheduledTime(job.user_id, job.day_of_week, job.time_of_day);
|
|
|
|
console.log(`[Scheduler] 작업 예약됨: 사용자 ${job.user_id}`);
|
|
}
|
|
|
|
// 대기 중인 작업 처리
|
|
await processPendingQueue();
|
|
|
|
} catch (error) {
|
|
console.error('[Scheduler] 스케줄러 오류:', error);
|
|
}
|
|
});
|
|
|
|
// 10분마다 큐 처리 (백업)
|
|
cron.schedule('*/10 * * * *', async () => {
|
|
try {
|
|
await processPendingQueue();
|
|
} catch (error) {
|
|
console.error('[Scheduler] 큐 처리 오류:', error);
|
|
}
|
|
});
|
|
|
|
console.log('[Scheduler] 스케줄러 등록 완료 - 매 시간 정각에 실행');
|
|
};
|
|
|
|
/**
|
|
* 수동으로 특정 사용자의 자동 생성 실행
|
|
*/
|
|
const triggerManualGeneration = async (userId) => {
|
|
return new Promise((resolve, reject) => {
|
|
db.get(`
|
|
SELECT ags.*, pp.id as pension_id
|
|
FROM auto_generation_settings ags
|
|
LEFT JOIN pension_profiles pp ON ags.pension_id = pp.id OR (pp.user_id = ags.user_id AND pp.is_default = 1)
|
|
WHERE ags.user_id = ?
|
|
`, [userId], async (err, settings) => {
|
|
if (err || !settings) {
|
|
return reject(new Error('자동 생성 설정을 찾을 수 없습니다.'));
|
|
}
|
|
|
|
const queueId = await addToQueue(userId, settings.pension_id);
|
|
if (!queueId) {
|
|
return reject(new Error('큐 추가 실패'));
|
|
}
|
|
|
|
// 즉시 처리
|
|
const result = await processPendingQueue();
|
|
resolve(result);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.exports = {
|
|
startScheduler,
|
|
getPendingGenerationJobs,
|
|
processPendingQueue,
|
|
triggerManualGeneration
|
|
};
|