Initial import
This commit is contained in:
540
etc/servers/work-server/src/services/plan-schedule-service.ts
Executable file
540
etc/servers/work-server/src/services/plan-schedule-service.ts
Executable file
@@ -0,0 +1,540 @@
|
||||
import { z } from 'zod';
|
||||
import { db } from '../db/client.js';
|
||||
import {
|
||||
createCompletedPlanExecutionLogItem,
|
||||
createPlanActionHistory,
|
||||
createPlanItem,
|
||||
ensurePlanTable,
|
||||
normalizePlanAutomationType,
|
||||
planAutomationTypeSchema,
|
||||
} from './plan-service.js';
|
||||
import { getKstNowParts } from './worklog-automation-utils.js';
|
||||
import { registerErrorLogBoardPosts } from './error-log-plan-registration-service.js';
|
||||
|
||||
export const PLAN_SCHEDULED_TASK_TABLE = 'plan_scheduled_tasks';
|
||||
const scheduleModes = ['interval', 'daily'] as const;
|
||||
const repeatIntervalUnits = ['minute', 'hour', 'day', 'week', 'month'] as const;
|
||||
const DEFAULT_DAILY_RUN_TIME = '09:00';
|
||||
|
||||
export const createPlanScheduledTaskSchema = z.object({
|
||||
workId: z.string().trim().optional().default('반복작업'),
|
||||
note: z.string().default(''),
|
||||
automationType: z.preprocess((value) => value, planAutomationTypeSchema.default('none')),
|
||||
releaseTarget: z.string().trim().min(1).default('release'),
|
||||
jangsingProcessingRequired: z.boolean().default(true),
|
||||
autoDeployToMain: z.boolean().default(true),
|
||||
enabled: z.boolean().default(true),
|
||||
immediateRunEnabled: z.boolean().default(true),
|
||||
scheduleMode: z.enum(scheduleModes).default('interval'),
|
||||
repeatIntervalValue: z.coerce.number().int().min(1).max(525600).default(60),
|
||||
repeatIntervalUnit: z.enum(repeatIntervalUnits).default('minute'),
|
||||
repeatIntervalMinutes: z.coerce.number().int().min(1).max(525600).optional(),
|
||||
dailyRunTime: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).default(DEFAULT_DAILY_RUN_TIME),
|
||||
});
|
||||
|
||||
export const updatePlanScheduledTaskSchema = createPlanScheduledTaskSchema.partial();
|
||||
|
||||
function normalizeScheduledWorkId(value?: string | null) {
|
||||
const workId = String(value ?? '').trim();
|
||||
const normalized = workId.replace(/^\[|\]$/g, '').replace(/\s+/g, '').toLowerCase();
|
||||
|
||||
if (!workId || normalized === '작업id' || normalized === 'workid' || normalized === 'undefined' || normalized === 'null') {
|
||||
return '반복작업';
|
||||
}
|
||||
|
||||
return workId;
|
||||
}
|
||||
|
||||
function normalizeRepeatIntervalMinutes(value: unknown) {
|
||||
const numericValue = Number(value);
|
||||
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return 60;
|
||||
}
|
||||
|
||||
return Math.min(525600, Math.max(1, Math.round(numericValue)));
|
||||
}
|
||||
|
||||
function normalizeRepeatIntervalValue(value: unknown) {
|
||||
const numericValue = Number(value);
|
||||
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return 60;
|
||||
}
|
||||
|
||||
return Math.min(525600, Math.max(1, Math.round(numericValue)));
|
||||
}
|
||||
|
||||
function normalizeRepeatIntervalUnit(value: unknown): (typeof repeatIntervalUnits)[number] {
|
||||
return repeatIntervalUnits.includes(value as (typeof repeatIntervalUnits)[number])
|
||||
? (value as (typeof repeatIntervalUnits)[number])
|
||||
: 'minute';
|
||||
}
|
||||
|
||||
function normalizeScheduleMode(value: unknown): (typeof scheduleModes)[number] {
|
||||
return scheduleModes.includes(value as (typeof scheduleModes)[number])
|
||||
? (value as (typeof scheduleModes)[number])
|
||||
: 'interval';
|
||||
}
|
||||
|
||||
function normalizeDailyRunTime(value: unknown) {
|
||||
return typeof value === 'string' && /^([01]\d|2[0-3]):[0-5]\d$/.test(value)
|
||||
? value
|
||||
: DEFAULT_DAILY_RUN_TIME;
|
||||
}
|
||||
|
||||
function toRepeatIntervalMinutes(value: unknown, unit: unknown) {
|
||||
const repeatIntervalValue = normalizeRepeatIntervalValue(value);
|
||||
const repeatIntervalUnit = normalizeRepeatIntervalUnit(unit);
|
||||
|
||||
if (repeatIntervalUnit === 'day') {
|
||||
return repeatIntervalValue * 24 * 60;
|
||||
}
|
||||
|
||||
if (repeatIntervalUnit === 'week') {
|
||||
return repeatIntervalValue * 7 * 24 * 60;
|
||||
}
|
||||
|
||||
if (repeatIntervalUnit === 'month') {
|
||||
return repeatIntervalValue * 30 * 24 * 60;
|
||||
}
|
||||
|
||||
if (repeatIntervalUnit === 'hour') {
|
||||
return repeatIntervalValue * 60;
|
||||
}
|
||||
|
||||
return repeatIntervalValue;
|
||||
}
|
||||
|
||||
function normalizeBoolean(value: unknown, fallback: boolean) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value === 0 || value === '0' || value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value === 1 || value === '1' || value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function formatScheduleWorkId(workId: string, date: Date) {
|
||||
const timestamp = [
|
||||
date.getFullYear(),
|
||||
String(date.getMonth() + 1).padStart(2, '0'),
|
||||
String(date.getDate()).padStart(2, '0'),
|
||||
String(date.getHours()).padStart(2, '0'),
|
||||
String(date.getMinutes()).padStart(2, '0'),
|
||||
].join('');
|
||||
|
||||
return `${normalizeScheduledWorkId(workId)}-${timestamp}`;
|
||||
}
|
||||
|
||||
function getKstDateKey(value: unknown) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = value instanceof Date ? value : new Date(String(value));
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getKstNowParts(date).dateKey;
|
||||
}
|
||||
|
||||
function isDailyScheduleDue(row: Record<string, unknown>, now: Date) {
|
||||
const nowParts = getKstNowParts(now);
|
||||
const [hours, minutes] = normalizeDailyRunTime(row.daily_run_time).split(':').map((value) => Number(value));
|
||||
const scheduledMinutesOfDay = hours * 60 + minutes;
|
||||
return nowParts.minutesOfDay >= scheduledMinutesOfDay && getKstDateKey(row.last_registered_at) !== nowParts.dateKey;
|
||||
}
|
||||
|
||||
function isIntervalScheduleDue(row: Record<string, unknown>, now: Date) {
|
||||
const lastRegisteredAt = row.last_registered_at ? new Date(String(row.last_registered_at)) : null;
|
||||
const intervalBaseAt = lastRegisteredAt && !Number.isNaN(lastRegisteredAt.getTime())
|
||||
? lastRegisteredAt
|
||||
: new Date(String(row.created_at ?? now.toISOString()));
|
||||
|
||||
if (Number.isNaN(intervalBaseAt.getTime())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const repeatIntervalMinutes = normalizeRepeatIntervalMinutes(
|
||||
row.repeat_interval_minutes ?? toRepeatIntervalMinutes(row.repeat_interval_value, row.repeat_interval_unit),
|
||||
);
|
||||
return intervalBaseAt.getTime() <= now.getTime() - repeatIntervalMinutes * 60 * 1000;
|
||||
}
|
||||
|
||||
function isScheduleDue(row: Record<string, unknown>, now: Date) {
|
||||
if (!row.last_registered_at && normalizeBoolean(row.immediate_run_enabled, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return normalizeScheduleMode(row.schedule_mode) === 'daily'
|
||||
? isDailyScheduleDue(row, now)
|
||||
: isIntervalScheduleDue(row, now);
|
||||
}
|
||||
|
||||
export function mapPlanScheduledTaskRow(row: Record<string, unknown>) {
|
||||
return {
|
||||
id: row.id,
|
||||
workId: row.work_id,
|
||||
note: row.note,
|
||||
automationType: normalizePlanAutomationType(row.automation_type),
|
||||
releaseTarget: row.release_target,
|
||||
jangsingProcessingRequired: Boolean(row.jangsing_processing_required ?? true),
|
||||
autoDeployToMain: Boolean(row.auto_deploy_to_main ?? true),
|
||||
enabled: Boolean(row.enabled ?? true),
|
||||
immediateRunEnabled: normalizeBoolean(row.immediate_run_enabled, true),
|
||||
scheduleMode: normalizeScheduleMode(row.schedule_mode),
|
||||
repeatIntervalValue: Number(row.repeat_interval_value ?? row.repeat_interval_minutes ?? 60),
|
||||
repeatIntervalUnit: normalizeRepeatIntervalUnit(row.repeat_interval_unit),
|
||||
repeatIntervalMinutes: normalizeRepeatIntervalMinutes(
|
||||
row.repeat_interval_minutes ?? toRepeatIntervalMinutes(row.repeat_interval_value ?? 60, row.repeat_interval_unit),
|
||||
),
|
||||
dailyRunTime: normalizeDailyRunTime(row.daily_run_time),
|
||||
lastRegisteredAt: row.last_registered_at,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensurePlanScheduledTaskColumn(
|
||||
columnName: string,
|
||||
addColumn: (table: any) => void,
|
||||
) {
|
||||
const hasColumn = await db.schema.hasColumn(PLAN_SCHEDULED_TASK_TABLE, columnName);
|
||||
|
||||
if (hasColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.schema.alterTable(PLAN_SCHEDULED_TASK_TABLE, (table) => {
|
||||
addColumn(table);
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensurePlanScheduledTaskTable() {
|
||||
const exists = await db.schema.hasTable(PLAN_SCHEDULED_TASK_TABLE);
|
||||
|
||||
if (!exists) {
|
||||
await db.schema.createTable(PLAN_SCHEDULED_TASK_TABLE, (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('work_id', 120).notNullable().defaultTo('반복작업');
|
||||
table.text('note').notNullable().defaultTo('');
|
||||
table.string('automation_type', 40).notNullable().defaultTo('none');
|
||||
table.string('release_target', 120).notNullable().defaultTo('release');
|
||||
table.boolean('jangsing_processing_required').notNullable().defaultTo(true);
|
||||
table.boolean('auto_deploy_to_main').notNullable().defaultTo(true);
|
||||
table.boolean('enabled').notNullable().defaultTo(true);
|
||||
table.boolean('immediate_run_enabled').notNullable().defaultTo(true);
|
||||
table.string('schedule_mode', 20).notNullable().defaultTo('interval');
|
||||
table.integer('repeat_interval_value').notNullable().defaultTo(60);
|
||||
table.string('repeat_interval_unit', 20).notNullable().defaultTo('minute');
|
||||
table.integer('repeat_interval_minutes').notNullable().defaultTo(60);
|
||||
table.string('daily_run_time', 5).notNullable().defaultTo(DEFAULT_DAILY_RUN_TIME);
|
||||
table.timestamp('last_registered_at', { useTz: true }).nullable();
|
||||
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
|
||||
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await ensurePlanScheduledTaskColumn('release_target', (table) => {
|
||||
table.string('release_target', 120).notNullable().defaultTo('release');
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('automation_type', (table) => {
|
||||
table.string('automation_type', 40).notNullable().defaultTo('none');
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('jangsing_processing_required', (table) => {
|
||||
table.boolean('jangsing_processing_required').notNullable().defaultTo(true);
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('auto_deploy_to_main', (table) => {
|
||||
table.boolean('auto_deploy_to_main').notNullable().defaultTo(true);
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('enabled', (table) => {
|
||||
table.boolean('enabled').notNullable().defaultTo(true);
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('immediate_run_enabled', (table) => {
|
||||
table.boolean('immediate_run_enabled').notNullable().defaultTo(true);
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('schedule_mode', (table) => {
|
||||
table.string('schedule_mode', 20).notNullable().defaultTo('interval');
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('repeat_interval_value', (table) => {
|
||||
table.integer('repeat_interval_value').notNullable().defaultTo(60);
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('repeat_interval_unit', (table) => {
|
||||
table.string('repeat_interval_unit', 20).notNullable().defaultTo('minute');
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('repeat_interval_minutes', (table) => {
|
||||
table.integer('repeat_interval_minutes').notNullable().defaultTo(60);
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('daily_run_time', (table) => {
|
||||
table.string('daily_run_time', 5).notNullable().defaultTo(DEFAULT_DAILY_RUN_TIME);
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('last_registered_at', (table) => {
|
||||
table.timestamp('last_registered_at', { useTz: true }).nullable();
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('created_at', (table) => {
|
||||
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
|
||||
});
|
||||
await ensurePlanScheduledTaskColumn('updated_at', (table) => {
|
||||
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
|
||||
});
|
||||
|
||||
await db(PLAN_SCHEDULED_TASK_TABLE)
|
||||
.where({ automation_type: 'plan_registration' })
|
||||
.update({ automation_type: 'plan' });
|
||||
await db(PLAN_SCHEDULED_TASK_TABLE)
|
||||
.where({ automation_type: 'general_development' })
|
||||
.update({ automation_type: 'auto_worker' });
|
||||
|
||||
await db(PLAN_SCHEDULED_TASK_TABLE)
|
||||
.where({ repeat_interval_unit: 'minute' })
|
||||
.update({
|
||||
repeat_interval_value: db.raw('repeat_interval_minutes'),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listPlanScheduledTasks() {
|
||||
await ensurePlanScheduledTaskTable();
|
||||
|
||||
return db(PLAN_SCHEDULED_TASK_TABLE).select('*').orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
export async function getPlanScheduledTaskById(id: number) {
|
||||
await ensurePlanScheduledTaskTable();
|
||||
|
||||
return db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).first();
|
||||
}
|
||||
|
||||
export async function createPlanScheduledTask(payload: z.infer<typeof createPlanScheduledTaskSchema>) {
|
||||
await ensurePlanScheduledTaskTable();
|
||||
const scheduleMode = normalizeScheduleMode(payload.scheduleMode);
|
||||
const repeatIntervalValue = normalizeRepeatIntervalValue(payload.repeatIntervalValue);
|
||||
const repeatIntervalUnit = normalizeRepeatIntervalUnit(payload.repeatIntervalUnit);
|
||||
|
||||
const rows = await db(PLAN_SCHEDULED_TASK_TABLE)
|
||||
.insert({
|
||||
work_id: normalizeScheduledWorkId(payload.workId),
|
||||
note: payload.note,
|
||||
automation_type: normalizePlanAutomationType(payload.automationType),
|
||||
release_target: payload.releaseTarget,
|
||||
jangsing_processing_required: payload.jangsingProcessingRequired,
|
||||
auto_deploy_to_main: payload.autoDeployToMain,
|
||||
enabled: payload.enabled,
|
||||
immediate_run_enabled: payload.immediateRunEnabled,
|
||||
schedule_mode: scheduleMode,
|
||||
repeat_interval_value: repeatIntervalValue,
|
||||
repeat_interval_unit: repeatIntervalUnit,
|
||||
repeat_interval_minutes: normalizeRepeatIntervalMinutes(
|
||||
payload.repeatIntervalMinutes ?? toRepeatIntervalMinutes(repeatIntervalValue, repeatIntervalUnit),
|
||||
),
|
||||
daily_run_time: normalizeDailyRunTime(payload.dailyRunTime),
|
||||
updated_at: db.fn.now(),
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
export async function updatePlanScheduledTask(id: number, payload: z.infer<typeof updatePlanScheduledTaskSchema>) {
|
||||
await ensurePlanScheduledTaskTable();
|
||||
|
||||
const currentRow = await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).first();
|
||||
|
||||
if (!currentRow) {
|
||||
return null;
|
||||
}
|
||||
const scheduleMode = normalizeScheduleMode(payload.scheduleMode ?? currentRow.schedule_mode);
|
||||
const repeatIntervalValue = normalizeRepeatIntervalValue(
|
||||
payload.repeatIntervalValue ?? currentRow.repeat_interval_value ?? currentRow.repeat_interval_minutes ?? 60,
|
||||
);
|
||||
const repeatIntervalUnit = normalizeRepeatIntervalUnit(payload.repeatIntervalUnit ?? currentRow.repeat_interval_unit);
|
||||
|
||||
const rows = await db(PLAN_SCHEDULED_TASK_TABLE)
|
||||
.where({ id })
|
||||
.update({
|
||||
work_id: payload.workId === undefined ? currentRow.work_id : normalizeScheduledWorkId(payload.workId),
|
||||
note: payload.note ?? currentRow.note,
|
||||
automation_type: normalizePlanAutomationType(payload.automationType ?? currentRow.automation_type),
|
||||
release_target: payload.releaseTarget ?? currentRow.release_target ?? 'release',
|
||||
jangsing_processing_required:
|
||||
payload.jangsingProcessingRequired ?? currentRow.jangsing_processing_required ?? true,
|
||||
auto_deploy_to_main: payload.autoDeployToMain ?? currentRow.auto_deploy_to_main ?? true,
|
||||
enabled: payload.enabled ?? currentRow.enabled ?? true,
|
||||
immediate_run_enabled: payload.immediateRunEnabled ?? currentRow.immediate_run_enabled ?? true,
|
||||
schedule_mode: scheduleMode,
|
||||
repeat_interval_value: repeatIntervalValue,
|
||||
repeat_interval_unit: repeatIntervalUnit,
|
||||
repeat_interval_minutes: normalizeRepeatIntervalMinutes(
|
||||
payload.repeatIntervalMinutes
|
||||
?? toRepeatIntervalMinutes(repeatIntervalValue, repeatIntervalUnit)
|
||||
?? currentRow.repeat_interval_minutes
|
||||
?? 60,
|
||||
),
|
||||
daily_run_time: normalizeDailyRunTime(payload.dailyRunTime ?? currentRow.daily_run_time),
|
||||
updated_at: db.fn.now(),
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function deletePlanScheduledTask(id: number) {
|
||||
await ensurePlanScheduledTaskTable();
|
||||
|
||||
const currentRow = await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).first();
|
||||
|
||||
if (!currentRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).delete();
|
||||
|
||||
return currentRow;
|
||||
}
|
||||
|
||||
export async function registerDuePlanScheduledTasks(now = new Date()) {
|
||||
await ensurePlanTable();
|
||||
await ensurePlanScheduledTaskTable();
|
||||
|
||||
const rows = await db(PLAN_SCHEDULED_TASK_TABLE).where({ enabled: true }).orderBy('id', 'asc');
|
||||
const registered = [];
|
||||
|
||||
for (const row of rows.filter((item) => isScheduleDue(item, now))) {
|
||||
const registration = await registerPlanScheduledTaskRow(row, now);
|
||||
|
||||
if (registration.createdPlan || registration.createdBoardPosts.length > 0) {
|
||||
registered.push(registration);
|
||||
}
|
||||
}
|
||||
|
||||
return registered;
|
||||
}
|
||||
|
||||
async function registerPlanScheduledTaskRow(row: Record<string, unknown>, now: Date) {
|
||||
if (normalizePlanAutomationType(row.automation_type) === 'plan') {
|
||||
const rangeEnd = now;
|
||||
const lastRegisteredAt = row.last_registered_at ? new Date(String(row.last_registered_at)) : null;
|
||||
const rangeStart =
|
||||
lastRegisteredAt && !Number.isNaN(lastRegisteredAt.getTime())
|
||||
? lastRegisteredAt
|
||||
: new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const registration = await registerErrorLogBoardPosts({
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
});
|
||||
const executionLogNoteLines = [
|
||||
`Plan 스케줄 #${row.id} 실행 이력입니다.`,
|
||||
`조회 구간: ${rangeStart.toISOString()} ~ ${rangeEnd.toISOString()}`,
|
||||
`신규 게시글 등록: ${registration.createdPosts.length}건`,
|
||||
`중복 제외: ${registration.skippedPosts.length}건`,
|
||||
];
|
||||
|
||||
if (registration.createdPosts.length > 0) {
|
||||
executionLogNoteLines.push('');
|
||||
executionLogNoteLines.push('등록된 게시글:');
|
||||
executionLogNoteLines.push(
|
||||
...registration.createdPosts.map((post) => `- 게시글 #${post.postId} ${post.workId} (${post.count}건)`),
|
||||
);
|
||||
}
|
||||
|
||||
if (registration.skippedPosts.length > 0) {
|
||||
executionLogNoteLines.push('');
|
||||
executionLogNoteLines.push('제외된 항목:');
|
||||
executionLogNoteLines.push(
|
||||
...registration.skippedPosts.map((post) => `- ${post.workId}: ${post.reason}`),
|
||||
);
|
||||
}
|
||||
|
||||
const executionLog = await createCompletedPlanExecutionLogItem({
|
||||
workId: formatScheduleWorkId(String(row.work_id ?? '반복작업'), now),
|
||||
note: executionLogNoteLines.join('\n'),
|
||||
automationType: 'plan',
|
||||
releaseTarget: String(row.release_target ?? 'release'),
|
||||
jangsingProcessingRequired: Boolean(row.jangsing_processing_required ?? true),
|
||||
autoDeployToMain: Boolean(row.auto_deploy_to_main ?? true),
|
||||
repeatRequestEnabled: false,
|
||||
repeatIntervalMinutes: normalizeRepeatIntervalMinutes(
|
||||
row.repeat_interval_minutes ?? toRepeatIntervalMinutes(row.repeat_interval_value, row.repeat_interval_unit),
|
||||
),
|
||||
});
|
||||
|
||||
await createPlanActionHistory(
|
||||
Number(executionLog.id),
|
||||
'스케줄등록',
|
||||
`Plan 스케줄 #${row.id} 실행 이력을 저장했습니다.`,
|
||||
);
|
||||
await createPlanActionHistory(
|
||||
Number(executionLog.id),
|
||||
'완료처리',
|
||||
registration.createdPosts.length > 0
|
||||
? `Plan 게시판 글 ${registration.createdPosts.length}건을 등록했습니다.`
|
||||
: '등록할 신규 Plan 게시판 글이 없었습니다.',
|
||||
);
|
||||
|
||||
await db(PLAN_SCHEDULED_TASK_TABLE)
|
||||
.where({ id: row.id })
|
||||
.update({
|
||||
last_registered_at: now,
|
||||
updated_at: db.fn.now(),
|
||||
});
|
||||
|
||||
return {
|
||||
createdPlan: executionLog,
|
||||
createdBoardPosts: registration.createdPosts,
|
||||
};
|
||||
}
|
||||
|
||||
const repeatIntervalMinutes = normalizeRepeatIntervalMinutes(
|
||||
row.repeat_interval_minutes ?? toRepeatIntervalMinutes(row.repeat_interval_value, row.repeat_interval_unit),
|
||||
);
|
||||
const createdPlan = await createPlanItem({
|
||||
workId: formatScheduleWorkId(String(row.work_id ?? '반복작업'), now),
|
||||
note: String(row.note ?? ''),
|
||||
automationType: normalizePlanAutomationType(row.automation_type),
|
||||
releaseTarget: String(row.release_target ?? 'release'),
|
||||
jangsingProcessingRequired: Boolean(row.jangsing_processing_required ?? true),
|
||||
autoDeployToMain: Boolean(row.auto_deploy_to_main ?? true),
|
||||
repeatRequestEnabled: false,
|
||||
repeatIntervalMinutes,
|
||||
});
|
||||
|
||||
await db(PLAN_SCHEDULED_TASK_TABLE)
|
||||
.where({ id: row.id })
|
||||
.update({
|
||||
last_registered_at: now,
|
||||
updated_at: db.fn.now(),
|
||||
});
|
||||
await createPlanActionHistory(
|
||||
Number(createdPlan.id),
|
||||
'스케줄등록',
|
||||
`Plan 스케줄 #${row.id} 반복 작업에서 등록했습니다.`,
|
||||
);
|
||||
|
||||
return {
|
||||
createdPlan,
|
||||
createdBoardPosts: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function registerPlanScheduledTaskNow(id: number, now = new Date()) {
|
||||
await ensurePlanTable();
|
||||
await ensurePlanScheduledTaskTable();
|
||||
|
||||
const row = await db(PLAN_SCHEDULED_TASK_TABLE).where({ id, enabled: true }).first();
|
||||
|
||||
if (!row || !isScheduleDue(row, now)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return registerPlanScheduledTaskRow(row, now);
|
||||
}
|
||||
Reference in New Issue
Block a user