Initial commit: AI platform app (server, views, lib, data, deploy docs)
Made-with: Cursor
This commit is contained in:
185
lib/thumbnail-events-store.js
Normal file
185
lib/thumbnail-events-store.js
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
Reference in New Issue
Block a user