import { createHash } from 'node:crypto'; import { db } from '../db/client.js'; import { listBoardPosts, createBoardPost } from './board-service.js'; import { listErrorLogs } from './error-log-service.js'; import { ensurePlanTable, PLAN_TABLE } from './plan-service.js'; const DEFAULT_ERROR_LOG_PLAN_GROUP_LIMIT = 6; const ERROR_LOG_BOARD_POST_MARKER_PREFIX = '`; } function buildErrorLogBoardPostTitle(candidate: ErrorLogCandidate) { const scope = candidate.requestPathGroup && candidate.requestPathGroup !== 'unknown' ? ` ${candidate.requestPathGroup}` : ''; const title = `에러로그 조치 계획: ${candidate.errorType}${scope}`.replace(/\s+/g, ' ').trim(); return title.length > 200 ? `${title.slice(0, 197).trimEnd()}...` : title; } function buildErrorLogBoardPostContent(candidate: ErrorLogCandidate, rangeStart: Date, rangeEnd: Date) { const detailNote = formatErrorLogPlanNote(candidate, rangeStart, rangeEnd); return [ buildBoardPostMarker(candidate.workId), `# ${buildErrorLogBoardPostTitle(candidate)}`, '', detailNote, ].join('\n'); } export async function registerErrorLogBoardPosts(args?: { rangeStart?: Date; rangeEnd?: Date; maxGroups?: number; }) { await ensurePlanTable(); const rangeEnd = args?.rangeEnd ?? new Date(); const rangeStart = args?.rangeStart ?? new Date(rangeEnd.getTime() - 24 * 60 * 60 * 1000); const maxGroups = args?.maxGroups ?? DEFAULT_ERROR_LOG_PLAN_GROUP_LIMIT; const [errorLogs, existingBoardPosts] = await Promise.all([ listErrorLogs(200), listBoardPosts(), ]); const recentLogs = filterLogsWithinRange(errorLogs as Array>, rangeStart, rangeEnd); const rawCandidates = buildErrorLogPlanCandidates(recentLogs as Array>); const candidates = coalesceErrorLogPlanCandidates(rawCandidates, maxGroups); const createdPosts: ErrorLogBoardPostRegistration[] = []; const skippedPosts: ErrorLogRegistrationSkip[] = []; for (const candidate of candidates) { const marker = buildBoardPostMarker(candidate.workId); const existingBoardPost = existingBoardPosts.find((post) => String(post.content ?? '').includes(marker)); if (existingBoardPost) { skippedPosts.push({ workId: candidate.workId, boardPostId: existingBoardPost.id, reason: `기존 게시글 #${existingBoardPost.id}가 있습니다.`, }); continue; } const latestOpenPlan = await db(PLAN_TABLE) .select(['id', 'status']) .where({ work_id: candidate.workId }) .whereNot({ status: '완료' as never }) .orderBy('id', 'desc') .first(); if (latestOpenPlan) { skippedPosts.push({ workId: candidate.workId, planId: Number(latestOpenPlan.id), reason: `기존 미완료 Plan #${latestOpenPlan.id}가 있습니다.`, }); continue; } const createdPost = await createBoardPost({ title: buildErrorLogBoardPostTitle(candidate), content: buildErrorLogBoardPostContent(candidate, rangeStart, rangeEnd), attachments: [], automationType: 'none', automationContextIds: [], requestExecutionMode: 'all_at_once', requestItems: [], }); createdPosts.push({ postId: createdPost.id, title: createdPost.title, workId: candidate.workId, count: candidate.count, }); } return { rangeStart, rangeEnd, recentLogs, rawCandidates, candidates, createdPosts, skippedPosts, }; }