186 lines
5.5 KiB
JavaScript
186 lines
5.5 KiB
JavaScript
/**
|
|
* PPT 썸네일 작업 이벤트 로그 — PostgreSQL 우선, PG 없을 때만 data/thumbnail-events.json 폴백
|
|
*/
|
|
const fs = require("fs/promises");
|
|
const { v4: uuidv4 } = require("uuid");
|
|
|
|
/**
|
|
* @param {import("pg").QueryResultRow} row
|
|
*/
|
|
function mapRowToEvent(row) {
|
|
if (!row) return null;
|
|
return {
|
|
id: row.id,
|
|
at: row.occurred_at ? new Date(row.occurred_at).toISOString() : null,
|
|
type: row.event_type,
|
|
lectureId: row.lecture_id || undefined,
|
|
lectureTitle: row.lecture_title || undefined,
|
|
reason: row.reason || undefined,
|
|
force: row.force_flag === true,
|
|
queueSizeAfter: row.queue_size_after != null ? Number(row.queue_size_after) : undefined,
|
|
retryCount: row.retry_count != null ? Number(row.retry_count) : undefined,
|
|
durationMs: row.duration_ms != null ? Number(row.duration_ms) : undefined,
|
|
error: row.error_text || undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {import("pg").Pool | null} pgPool
|
|
* @param {string} jsonPath
|
|
*/
|
|
async function readThumbnailEvents(pgPool, jsonPath) {
|
|
if (pgPool) {
|
|
const r = await pgPool.query(
|
|
`SELECT id, occurred_at, event_type, lecture_id, lecture_title, reason, force_flag,
|
|
queue_size_after, retry_count, duration_ms, error_text
|
|
FROM lecture_thumbnail_events
|
|
ORDER BY occurred_at ASC`
|
|
);
|
|
return (r.rows || []).map(mapRowToEvent);
|
|
}
|
|
try {
|
|
const raw = await fs.readFile(jsonPath, "utf-8");
|
|
const parsed = JSON.parse(raw);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import("pg").Pool} pgPool
|
|
* @param {number} keep
|
|
*/
|
|
async function pruneThumbnailEvents(pgPool, keep) {
|
|
const k = Math.max(Number(keep) || 200, 20);
|
|
await pgPool.query(
|
|
`DELETE FROM lecture_thumbnail_events a
|
|
USING (
|
|
SELECT id FROM (
|
|
SELECT id, ROW_NUMBER() OVER (ORDER BY occurred_at DESC) AS rn
|
|
FROM lecture_thumbnail_events
|
|
) t WHERE t.rn > $1
|
|
) d
|
|
WHERE a.id = d.id`,
|
|
[k]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {import("pg").Pool | null} pgPool
|
|
* @param {string} jsonPath
|
|
* @param {number} keep
|
|
* @param {object} payload
|
|
*/
|
|
async function appendThumbnailEvent(pgPool, jsonPath, keep, payload) {
|
|
const id = uuidv4();
|
|
const at = new Date().toISOString();
|
|
if (pgPool) {
|
|
await pgPool.query(
|
|
`INSERT INTO lecture_thumbnail_events (
|
|
id, occurred_at, event_type, lecture_id, lecture_title, reason, force_flag,
|
|
queue_size_after, retry_count, duration_ms, error_text
|
|
) VALUES ($1, $2::timestamptz, $3, $4::uuid, $5, $6, $7, $8, $9, $10, $11)`,
|
|
[
|
|
id,
|
|
at,
|
|
payload.type || "unknown",
|
|
payload.lectureId || null,
|
|
payload.lectureTitle || null,
|
|
payload.reason || null,
|
|
payload.force === true,
|
|
payload.queueSizeAfter != null ? payload.queueSizeAfter : null,
|
|
payload.retryCount != null ? payload.retryCount : null,
|
|
payload.durationMs != null ? payload.durationMs : null,
|
|
payload.error || null,
|
|
]
|
|
);
|
|
await pruneThumbnailEvents(pgPool, keep);
|
|
return;
|
|
}
|
|
const events = await readThumbnailEvents(null, jsonPath);
|
|
events.push({ id, at, ...payload });
|
|
const sliced = events.slice(-Math.max(keep, 20));
|
|
await fs.writeFile(jsonPath, JSON.stringify(sliced, null, 2), "utf-8");
|
|
}
|
|
|
|
/**
|
|
* @param {import("pg").Pool | null} pgPool
|
|
* @param {string} jsonPath
|
|
*/
|
|
async function clearThumbnailEvents(pgPool, jsonPath) {
|
|
if (pgPool) {
|
|
await pgPool.query(`DELETE FROM lecture_thumbnail_events`);
|
|
return;
|
|
}
|
|
await fs.writeFile(jsonPath, "[]", "utf-8");
|
|
}
|
|
|
|
/**
|
|
* 기존 JSON 파일을 한 번 DB로 옮긴 뒤 파일을 백업 이름으로 변경 (PG 사용 시)
|
|
* @param {import("pg").Pool | null} pgPool
|
|
* @param {string} jsonPath
|
|
*/
|
|
/**
|
|
* @param {import("pg").Pool | null} pgPool
|
|
* @param {string} jsonPath
|
|
* @param {number} [keep]
|
|
*/
|
|
async function migrateThumbnailEventsFromJson(pgPool, jsonPath, keep) {
|
|
if (!pgPool) return;
|
|
const keepN = Math.max(Number(keep) || 200, 20);
|
|
let rows;
|
|
try {
|
|
const raw = await fs.readFile(jsonPath, "utf-8");
|
|
const parsed = JSON.parse(raw);
|
|
rows = Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return;
|
|
}
|
|
if (rows.length === 0) return;
|
|
|
|
for (const evt of rows) {
|
|
if (!evt || !evt.id || !evt.type) continue;
|
|
try {
|
|
await pgPool.query(
|
|
`INSERT INTO lecture_thumbnail_events (
|
|
id, occurred_at, event_type, lecture_id, lecture_title, reason, force_flag,
|
|
queue_size_after, retry_count, duration_ms, error_text
|
|
) VALUES ($1::uuid, $2::timestamptz, $3, $4::uuid, $5, $6, $7, $8, $9, $10, $11)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
[
|
|
evt.id,
|
|
evt.at || new Date().toISOString(),
|
|
evt.type,
|
|
evt.lectureId || null,
|
|
evt.lectureTitle || null,
|
|
evt.reason || null,
|
|
evt.force === true,
|
|
evt.queueSizeAfter != null ? evt.queueSizeAfter : null,
|
|
evt.retryCount != null ? evt.retryCount : null,
|
|
evt.durationMs != null ? evt.durationMs : null,
|
|
evt.error || null,
|
|
]
|
|
);
|
|
} catch {
|
|
/* ignore row */
|
|
}
|
|
}
|
|
await pruneThumbnailEvents(pgPool, keepN);
|
|
try {
|
|
const fsSync = require("fs");
|
|
if (fsSync.existsSync(jsonPath)) {
|
|
fsSync.renameSync(jsonPath, `${jsonPath}.migrated.bak`);
|
|
}
|
|
} catch (e) {
|
|
console.warn("[thumbnail-events] JSON migration rename failed:", e?.message || e);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
readThumbnailEvents,
|
|
appendThumbnailEvent,
|
|
clearThumbnailEvents,
|
|
migrateThumbnailEventsFromJson,
|
|
};
|