chore: test deploy snapshot
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { Knex } from 'knex';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../db/client.js';
|
||||
import { sendNotifications } from './notification-service.js';
|
||||
@@ -8,6 +9,7 @@ const MAX_CATEGORY_COUNT = 16;
|
||||
const MAX_RESULT_COUNT = 12;
|
||||
const TICKET_BAY_FETCH_TIMEOUT_MS = 12_000;
|
||||
const TICKET_BAY_PRODUCT_PAGE_SIZE = 100;
|
||||
const BASEBALL_TICKET_BAY_BATCH_ADVISORY_LOCK_KEY = 741_205_261;
|
||||
|
||||
const teamKeywordMap: Record<string, string> = {
|
||||
LG: 'LG',
|
||||
@@ -690,6 +692,9 @@ export type BaseballTicketBayTimeWindow = {
|
||||
|
||||
export type BaseballTicketBayAlertItem = {
|
||||
id: string;
|
||||
clientId: string;
|
||||
ownerType: BaseballTicketBayOwnerType;
|
||||
ownerId: string;
|
||||
title: string;
|
||||
eventDate: string;
|
||||
team: string;
|
||||
@@ -714,6 +719,9 @@ export type BaseballTicketBayAlertLogStatus = 'info' | 'success' | 'warning' | '
|
||||
|
||||
export type BaseballTicketBayAlertLogItem = {
|
||||
id: string;
|
||||
clientId: string;
|
||||
ownerType: BaseballTicketBayOwnerType;
|
||||
ownerId: string;
|
||||
alertId: string | null;
|
||||
alertTitle: string;
|
||||
action: BaseballTicketBayAlertLogAction;
|
||||
@@ -759,9 +767,21 @@ export type BaseballTicketBayAlertMutation = {
|
||||
timeWindows: BaseballTicketBayTimeWindow[];
|
||||
};
|
||||
|
||||
type BaseballTicketBayOwnerType = 'client' | 'shared-token';
|
||||
|
||||
type BaseballTicketBayOwnerScope =
|
||||
| { kind: 'all' }
|
||||
| {
|
||||
kind: 'owner';
|
||||
ownerType: BaseballTicketBayOwnerType;
|
||||
ownerId: string;
|
||||
};
|
||||
|
||||
type BaseballTicketBayAlertRow = {
|
||||
id: string;
|
||||
client_id: string;
|
||||
owner_type: BaseballTicketBayOwnerType;
|
||||
owner_id: string;
|
||||
app_origin: string | null;
|
||||
app_domain: string | null;
|
||||
title: string;
|
||||
@@ -786,6 +806,8 @@ type BaseballTicketBayAlertRow = {
|
||||
type BaseballTicketBayLogRow = {
|
||||
id: string;
|
||||
client_id: string;
|
||||
owner_type: BaseballTicketBayOwnerType;
|
||||
owner_id: string;
|
||||
alert_id: string | null;
|
||||
alert_title: string;
|
||||
action: BaseballTicketBayAlertLogAction;
|
||||
@@ -810,6 +832,25 @@ function createId(prefix: string) {
|
||||
return `${prefix}-${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
function normalizeOwnerType(value: unknown): BaseballTicketBayOwnerType {
|
||||
return normalizeText(value) === 'shared-token' ? 'shared-token' : 'client';
|
||||
}
|
||||
|
||||
function normalizeOwnerId(row: { owner_id?: unknown; client_id?: unknown }) {
|
||||
return normalizeText(row.owner_id) || normalizeText(row.client_id);
|
||||
}
|
||||
|
||||
function applyOwnerScope(query: Knex.QueryBuilder, scope: BaseballTicketBayOwnerScope) {
|
||||
if (scope.kind === 'all') {
|
||||
return query;
|
||||
}
|
||||
|
||||
return query.where({
|
||||
owner_type: scope.ownerType,
|
||||
owner_id: scope.ownerId,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeNumericValue(value: unknown) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
@@ -858,6 +899,9 @@ function parseTimeWindows(value: string) {
|
||||
function mapAlertRow(row: BaseballTicketBayAlertRow): BaseballTicketBayAlertItem {
|
||||
return {
|
||||
id: normalizeText(row.id),
|
||||
clientId: normalizeText(row.client_id),
|
||||
ownerType: normalizeOwnerType(row.owner_type),
|
||||
ownerId: normalizeOwnerId(row),
|
||||
title: normalizeText(row.title),
|
||||
eventDate: normalizeText(row.event_date),
|
||||
team: normalizeText(row.team) || '전체',
|
||||
@@ -891,6 +935,9 @@ function mapLogRow(row: BaseballTicketBayLogRow): BaseballTicketBayAlertLogItem
|
||||
|
||||
return {
|
||||
id: normalizeText(row.id),
|
||||
clientId: normalizeText(row.client_id),
|
||||
ownerType: normalizeOwnerType(row.owner_type),
|
||||
ownerId: normalizeOwnerId(row),
|
||||
alertId: row.alert_id ? normalizeText(row.alert_id) : null,
|
||||
alertTitle: normalizeText(row.alert_title),
|
||||
action: row.action,
|
||||
@@ -978,6 +1025,8 @@ export async function ensureBaseballTicketBayTables() {
|
||||
await db.schema.createTable(BASEBALL_TICKET_BAY_ALERT_TABLE, (table) => {
|
||||
table.string('id', 120).primary();
|
||||
table.string('client_id', 200).notNullable().index();
|
||||
table.string('owner_type', 40).notNullable().defaultTo('client').index();
|
||||
table.string('owner_id', 200).notNullable().defaultTo('').index();
|
||||
table.text('app_origin').nullable();
|
||||
table.string('app_domain', 255).nullable();
|
||||
table.string('title', 255).notNullable();
|
||||
@@ -1006,6 +1055,8 @@ export async function ensureBaseballTicketBayTables() {
|
||||
await db.schema.createTable(BASEBALL_TICKET_BAY_LOG_TABLE, (table) => {
|
||||
table.string('id', 120).primary();
|
||||
table.string('client_id', 200).notNullable().index();
|
||||
table.string('owner_type', 40).notNullable().defaultTo('client').index();
|
||||
table.string('owner_id', 200).notNullable().defaultTo('').index();
|
||||
table.string('alert_id', 120).nullable().index();
|
||||
table.string('alert_title', 255).notNullable();
|
||||
table.string('action', 40).notNullable();
|
||||
@@ -1025,6 +1076,60 @@ export async function ensureBaseballTicketBayTables() {
|
||||
});
|
||||
}
|
||||
|
||||
const hasAlertOwnerTypeColumn = await db.schema.hasColumn(BASEBALL_TICKET_BAY_ALERT_TABLE, 'owner_type');
|
||||
|
||||
if (!hasAlertOwnerTypeColumn) {
|
||||
await db.schema.alterTable(BASEBALL_TICKET_BAY_ALERT_TABLE, (table) => {
|
||||
table.string('owner_type', 40).notNullable().defaultTo('client').index();
|
||||
});
|
||||
}
|
||||
|
||||
const hasAlertOwnerIdColumn = await db.schema.hasColumn(BASEBALL_TICKET_BAY_ALERT_TABLE, 'owner_id');
|
||||
|
||||
if (!hasAlertOwnerIdColumn) {
|
||||
await db.schema.alterTable(BASEBALL_TICKET_BAY_ALERT_TABLE, (table) => {
|
||||
table.string('owner_id', 200).notNullable().defaultTo('').index();
|
||||
});
|
||||
}
|
||||
|
||||
await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.where((builder) => {
|
||||
builder.whereNull('owner_type').orWhere('owner_type', '');
|
||||
})
|
||||
.update({ owner_type: 'client' });
|
||||
await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.where((builder) => {
|
||||
builder.whereNull('owner_id').orWhere('owner_id', '');
|
||||
})
|
||||
.update({ owner_id: db.ref('client_id') });
|
||||
|
||||
const hasLogOwnerTypeColumn = await db.schema.hasColumn(BASEBALL_TICKET_BAY_LOG_TABLE, 'owner_type');
|
||||
|
||||
if (!hasLogOwnerTypeColumn) {
|
||||
await db.schema.alterTable(BASEBALL_TICKET_BAY_LOG_TABLE, (table) => {
|
||||
table.string('owner_type', 40).notNullable().defaultTo('client').index();
|
||||
});
|
||||
}
|
||||
|
||||
const hasLogOwnerIdColumn = await db.schema.hasColumn(BASEBALL_TICKET_BAY_LOG_TABLE, 'owner_id');
|
||||
|
||||
if (!hasLogOwnerIdColumn) {
|
||||
await db.schema.alterTable(BASEBALL_TICKET_BAY_LOG_TABLE, (table) => {
|
||||
table.string('owner_id', 200).notNullable().defaultTo('').index();
|
||||
});
|
||||
}
|
||||
|
||||
await db(BASEBALL_TICKET_BAY_LOG_TABLE)
|
||||
.where((builder) => {
|
||||
builder.whereNull('owner_type').orWhere('owner_type', '');
|
||||
})
|
||||
.update({ owner_type: 'client' });
|
||||
await db(BASEBALL_TICKET_BAY_LOG_TABLE)
|
||||
.where((builder) => {
|
||||
builder.whereNull('owner_id').orWhere('owner_id', '');
|
||||
})
|
||||
.update({ owner_id: db.ref('client_id') });
|
||||
|
||||
const hasSeenTable = await db.schema.hasTable(BASEBALL_TICKET_BAY_SEEN_PRODUCT_TABLE);
|
||||
|
||||
if (!hasSeenTable) {
|
||||
@@ -1046,33 +1151,39 @@ export async function ensureBaseballTicketBayTables() {
|
||||
return baseballTicketBayTableSetupPromise;
|
||||
}
|
||||
|
||||
export async function listBaseballTicketBayAlerts(clientId: string) {
|
||||
export async function listBaseballTicketBayAlerts(scope: BaseballTicketBayOwnerScope) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
const rows = await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ client_id: clientId })
|
||||
.orderBy([{ column: 'event_date', order: 'asc' }, { column: 'created_at', order: 'desc' }]);
|
||||
return rows.map((row) => mapAlertRow(row as BaseballTicketBayAlertRow));
|
||||
const query = applyOwnerScope(db(BASEBALL_TICKET_BAY_ALERT_TABLE).select('*'), scope);
|
||||
|
||||
const rows = (await query.orderBy([
|
||||
{ column: 'event_date', order: 'asc' },
|
||||
{ column: 'owner_type', order: 'asc' },
|
||||
{ column: 'owner_id', order: 'asc' },
|
||||
{ column: 'client_id', order: 'asc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])) as BaseballTicketBayAlertRow[];
|
||||
return rows.map((row: BaseballTicketBayAlertRow) => mapAlertRow(row));
|
||||
}
|
||||
|
||||
export async function listBaseballTicketBayLogs(clientId: string, alertId?: string) {
|
||||
export async function listBaseballTicketBayLogs(
|
||||
scope: BaseballTicketBayOwnerScope,
|
||||
alertId?: string,
|
||||
) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
let query = db(BASEBALL_TICKET_BAY_LOG_TABLE)
|
||||
.select('*')
|
||||
.where({ client_id: clientId })
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(200);
|
||||
let query = applyOwnerScope(db(BASEBALL_TICKET_BAY_LOG_TABLE).select('*'), scope).orderBy('created_at', 'desc').limit(200);
|
||||
|
||||
if (alertId) {
|
||||
query = query.andWhere({ alert_id: alertId });
|
||||
}
|
||||
|
||||
const rows = await query;
|
||||
return rows.map((row) => mapLogRow(row as BaseballTicketBayLogRow));
|
||||
const rows = (await query) as BaseballTicketBayLogRow[];
|
||||
return rows.map((row: BaseballTicketBayLogRow) => mapLogRow(row));
|
||||
}
|
||||
|
||||
export async function createBaseballTicketBayLog(args: {
|
||||
clientId: string;
|
||||
ownerType: BaseballTicketBayOwnerType;
|
||||
ownerId: string;
|
||||
alertId?: string | null;
|
||||
alertTitle: string;
|
||||
action: BaseballTicketBayAlertLogAction;
|
||||
@@ -1085,6 +1196,8 @@ export async function createBaseballTicketBayLog(args: {
|
||||
const row: BaseballTicketBayLogRow = {
|
||||
id: createId('log'),
|
||||
client_id: args.clientId,
|
||||
owner_type: args.ownerType,
|
||||
owner_id: args.ownerId,
|
||||
alert_id: args.alertId ?? null,
|
||||
alert_title: args.alertTitle,
|
||||
action: args.action,
|
||||
@@ -1098,40 +1211,43 @@ export async function createBaseballTicketBayLog(args: {
|
||||
return mapLogRow(row);
|
||||
}
|
||||
|
||||
export async function deleteBaseballTicketBayLog(logId: string, clientId: string) {
|
||||
export async function deleteBaseballTicketBayLog(logId: string, scope: BaseballTicketBayOwnerScope) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
|
||||
const existing = await db(BASEBALL_TICKET_BAY_LOG_TABLE)
|
||||
.select('*')
|
||||
.where({
|
||||
id: logId,
|
||||
client_id: clientId,
|
||||
})
|
||||
.first();
|
||||
const existing = await applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_LOG_TABLE)
|
||||
.select('*')
|
||||
.where({
|
||||
id: logId,
|
||||
}),
|
||||
scope,
|
||||
).first();
|
||||
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await db(BASEBALL_TICKET_BAY_LOG_TABLE)
|
||||
.where({
|
||||
await applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_LOG_TABLE).where({
|
||||
id: logId,
|
||||
client_id: clientId,
|
||||
})
|
||||
.delete();
|
||||
}),
|
||||
scope,
|
||||
).delete();
|
||||
|
||||
return mapLogRow(existing as BaseballTicketBayLogRow);
|
||||
}
|
||||
|
||||
export async function createBaseballTicketBayAlert(
|
||||
payload: BaseballTicketBayAlertMutation,
|
||||
context: { clientId: string; appOrigin?: string; appDomain?: string },
|
||||
context: { clientId: string; ownerType: BaseballTicketBayOwnerType; ownerId: string; appOrigin?: string; appDomain?: string },
|
||||
) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
const now = new Date().toISOString();
|
||||
const row: BaseballTicketBayAlertRow = {
|
||||
id: createId('alert'),
|
||||
client_id: context.clientId,
|
||||
owner_type: context.ownerType,
|
||||
owner_id: context.ownerId,
|
||||
app_origin: normalizeText(context.appOrigin) || null,
|
||||
app_domain: normalizeText(context.appDomain) || null,
|
||||
title: payload.title.trim(),
|
||||
@@ -1159,13 +1275,15 @@ export async function createBaseballTicketBayAlert(
|
||||
export async function updateBaseballTicketBayAlert(
|
||||
alertId: string,
|
||||
payload: Partial<BaseballTicketBayAlertMutation>,
|
||||
context: { clientId: string; appOrigin?: string; appDomain?: string },
|
||||
context: { clientId: string; ownerType: BaseballTicketBayOwnerType; ownerId: string; appOrigin?: string; appDomain?: string },
|
||||
) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
const current = await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId, client_id: context.clientId })
|
||||
.first();
|
||||
const current = await applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId }),
|
||||
{ kind: 'owner', ownerType: context.ownerType, ownerId: context.ownerId },
|
||||
).first();
|
||||
|
||||
if (!current) {
|
||||
throw new Error('수정할 알림을 찾지 못했습니다.');
|
||||
@@ -1191,37 +1309,53 @@ export async function updateBaseballTicketBayAlert(
|
||||
if (context.appOrigin) patch.app_origin = normalizeText(context.appOrigin);
|
||||
if (context.appDomain) patch.app_domain = normalizeText(context.appDomain);
|
||||
|
||||
await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.where({ id: alertId, client_id: context.clientId })
|
||||
.update(patch);
|
||||
await applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_ALERT_TABLE).where({ id: alertId }),
|
||||
{ kind: 'owner', ownerType: context.ownerType, ownerId: context.ownerId },
|
||||
).update(patch);
|
||||
|
||||
const updated = await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId, client_id: context.clientId })
|
||||
.first();
|
||||
const updated = await applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId }),
|
||||
{ kind: 'owner', ownerType: context.ownerType, ownerId: context.ownerId },
|
||||
).first();
|
||||
return mapAlertRow(updated as BaseballTicketBayAlertRow);
|
||||
}
|
||||
|
||||
export async function deleteBaseballTicketBayAlert(alertId: string, clientId: string) {
|
||||
export async function deleteBaseballTicketBayAlert(alertId: string, scope: BaseballTicketBayOwnerScope) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
const row = await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId, client_id: clientId })
|
||||
.first();
|
||||
const row = await applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId }),
|
||||
scope,
|
||||
).first();
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.where({ id: alertId, client_id: clientId })
|
||||
.delete();
|
||||
await applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_ALERT_TABLE).where({ id: alertId }),
|
||||
scope,
|
||||
).delete();
|
||||
return mapAlertRow(row as BaseballTicketBayAlertRow);
|
||||
}
|
||||
|
||||
async function getAlertRow(alertId: string) {
|
||||
async function getAlertRow(alertId: string, scope?: BaseballTicketBayOwnerScope) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
const row = await db(BASEBALL_TICKET_BAY_ALERT_TABLE).select('*').where({ id: alertId }).first();
|
||||
const scopedQuery = scope
|
||||
? applyOwnerScope(
|
||||
db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId }),
|
||||
scope,
|
||||
)
|
||||
: db(BASEBALL_TICKET_BAY_ALERT_TABLE)
|
||||
.select('*')
|
||||
.where({ id: alertId });
|
||||
const row = await scopedQuery.first();
|
||||
return row ? (row as BaseballTicketBayAlertRow) : null;
|
||||
}
|
||||
|
||||
@@ -1268,8 +1402,11 @@ async function updateAlertRunTimestamp(alertId: string, patch: { lastRunAt: stri
|
||||
});
|
||||
}
|
||||
|
||||
export async function runBaseballTicketBayAlert(alertId: string, options?: { ignoreTimeWindow?: boolean }) {
|
||||
const row = await getAlertRow(alertId);
|
||||
export async function runBaseballTicketBayAlert(
|
||||
alertId: string,
|
||||
options?: { ignoreTimeWindow?: boolean; scope?: BaseballTicketBayOwnerScope },
|
||||
) {
|
||||
const row = await getAlertRow(alertId, options?.scope);
|
||||
|
||||
if (!row) {
|
||||
throw new Error('실행할 알림을 찾지 못했습니다.');
|
||||
@@ -1281,6 +1418,8 @@ export async function runBaseballTicketBayAlert(alertId: string, options?: { ign
|
||||
if (!options?.ignoreTimeWindow && isWithinBlockedTime(alert.timeWindows, now)) {
|
||||
const log = await createBaseballTicketBayLog({
|
||||
clientId: row.client_id,
|
||||
ownerType: normalizeOwnerType(row.owner_type),
|
||||
ownerId: normalizeOwnerId(row),
|
||||
alertId: alert.id,
|
||||
alertTitle: alert.title,
|
||||
action: 'run',
|
||||
@@ -1328,6 +1467,8 @@ export async function runBaseballTicketBayAlert(alertId: string, options?: { ign
|
||||
: [];
|
||||
const log = await createBaseballTicketBayLog({
|
||||
clientId: row.client_id,
|
||||
ownerType: normalizeOwnerType(row.owner_type),
|
||||
ownerId: normalizeOwnerId(row),
|
||||
alertId: alert.id,
|
||||
alertTitle: alert.title,
|
||||
action: 'run',
|
||||
@@ -1372,6 +1513,8 @@ export async function runBaseballTicketBayAlert(alertId: string, options?: { ign
|
||||
].join('\n');
|
||||
const log = await createBaseballTicketBayLog({
|
||||
clientId: row.client_id,
|
||||
ownerType: normalizeOwnerType(row.owner_type),
|
||||
ownerId: normalizeOwnerId(row),
|
||||
alertId: alert.id,
|
||||
alertTitle: alert.title,
|
||||
action: 'run',
|
||||
@@ -1414,36 +1557,61 @@ function isAlertDue(alert: BaseballTicketBayAlertItem, now: Date) {
|
||||
return now.getTime() - lastRunAt >= alert.batchIntervalMinutes * 60 * 1000;
|
||||
}
|
||||
|
||||
function readBooleanLikeValue(value: unknown) {
|
||||
return value === true || value === 't' || value === 'true' || value === 1 || value === '1';
|
||||
}
|
||||
|
||||
async function tryAcquireBaseballTicketBayBatchLock() {
|
||||
const result = (await db.raw('select pg_try_advisory_lock(?) as locked', [
|
||||
BASEBALL_TICKET_BAY_BATCH_ADVISORY_LOCK_KEY,
|
||||
])) as { rows?: Array<{ locked?: unknown }> };
|
||||
return readBooleanLikeValue(result.rows?.[0]?.locked);
|
||||
}
|
||||
|
||||
async function releaseBaseballTicketBayBatchLock() {
|
||||
await db.raw('select pg_advisory_unlock(?)', [BASEBALL_TICKET_BAY_BATCH_ADVISORY_LOCK_KEY]);
|
||||
}
|
||||
|
||||
export async function processDueBaseballTicketBayAlerts(now = new Date()) {
|
||||
await ensureBaseballTicketBayTables();
|
||||
const rows = await db(BASEBALL_TICKET_BAY_ALERT_TABLE).select('*').where({ active: true });
|
||||
const results: Array<{ alertId: string; ok: boolean; message?: string }> = [];
|
||||
|
||||
for (const row of rows as BaseballTicketBayAlertRow[]) {
|
||||
const alert = mapAlertRow(row);
|
||||
|
||||
if (!isAlertDue(alert, now)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await runBaseballTicketBayAlert(alert.id);
|
||||
results.push({ alertId: alert.id, ok: true });
|
||||
} catch (error) {
|
||||
const handledError = error instanceof Error ? error : new Error(String(error));
|
||||
await createBaseballTicketBayLog({
|
||||
clientId: row.client_id,
|
||||
alertId: alert.id,
|
||||
alertTitle: alert.title,
|
||||
action: 'run',
|
||||
status: 'error',
|
||||
message: handledError.message || '배치 실행에 실패했습니다.',
|
||||
detail: '',
|
||||
});
|
||||
await updateAlertRunTimestamp(alert.id, { lastRunAt: now.toISOString(), lastMatchAt: alert.lastMatchAt });
|
||||
results.push({ alertId: alert.id, ok: false, message: handledError.message });
|
||||
}
|
||||
if (!(await tryAcquireBaseballTicketBayBatchLock())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results;
|
||||
try {
|
||||
await ensureBaseballTicketBayTables();
|
||||
const rows = await db(BASEBALL_TICKET_BAY_ALERT_TABLE).select('*').where({ active: true });
|
||||
const results: Array<{ alertId: string; ok: boolean; message?: string }> = [];
|
||||
|
||||
for (const row of rows as BaseballTicketBayAlertRow[]) {
|
||||
const alert = mapAlertRow(row);
|
||||
|
||||
if (!isAlertDue(alert, now)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await runBaseballTicketBayAlert(alert.id);
|
||||
results.push({ alertId: alert.id, ok: true });
|
||||
} catch (error) {
|
||||
const handledError = error instanceof Error ? error : new Error(String(error));
|
||||
await createBaseballTicketBayLog({
|
||||
clientId: row.client_id,
|
||||
ownerType: normalizeOwnerType(row.owner_type),
|
||||
ownerId: normalizeOwnerId(row),
|
||||
alertId: alert.id,
|
||||
alertTitle: alert.title,
|
||||
action: 'run',
|
||||
status: 'error',
|
||||
message: handledError.message || '배치 실행에 실패했습니다.',
|
||||
detail: '',
|
||||
});
|
||||
await updateAlertRunTimestamp(alert.id, { lastRunAt: now.toISOString(), lastMatchAt: alert.lastMatchAt });
|
||||
results.push({ alertId: alert.id, ok: false, message: handledError.message });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
await releaseBaseballTicketBayBatchLock();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user