CASTAD-v0.1/server/services/schedulerService.js

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
};