Initial import

This commit is contained in:
how2ice
2026-04-21 03:33:23 +09:00
commit 9e4b70f1f1
495 changed files with 94680 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import {
listIosNotificationTokens,
getAutomationNotificationPreference,
getWebPushConfig,
registerIosNotificationToken,
registerAutomationNotificationPreferenceSchema,
registerIosTokenSchema,
registerWebPushSubscription,
registerWebPushSubscriptionSchema,
sendNotifications,
sendIosNotificationSchema,
setupNotificationTables,
upsertAutomationNotificationPreference,
unregisterIosNotificationToken,
unregisterIosTokenSchema,
unregisterWebPushSubscription,
unregisterWebPushSubscriptionSchema,
} from '../services/notification-service.js';
import {
createNotificationMessage,
deleteNotificationMessage,
getNotificationMessage,
listNotificationMessages,
notificationMessageListQuerySchema,
notificationMessagePayloadSchema,
notificationMessageReadPayloadSchema,
updateNotificationMessageReadState,
} from '../services/notification-message-service.js';
const automationNotificationPreferenceQuerySchema = z.object({
targetKind: z.enum(['client', 'ios-token', 'ios-token-client', 'web-endpoint']).optional(),
targetId: z.string().trim().min(1).max(1000).optional(),
});
type AutomationNotificationPreferenceTargetKind = NonNullable<
z.infer<typeof automationNotificationPreferenceQuerySchema>['targetKind']
>;
function getClientIdHeader(request: { headers: Record<string, string | string[] | undefined> }) {
const rawClientId = request.headers['x-client-id'];
const clientId = Array.isArray(rawClientId) ? rawClientId[0] : rawClientId;
return clientId?.trim() ?? '';
}
export async function registerNotificationRoutes(app: FastifyInstance) {
app.post('/api/notifications/setup', async () => setupNotificationTables());
app.get('/api/notifications/tokens', async () => ({
items: await listIosNotificationTokens(),
}));
app.get('/api/notifications/webpush/config', async () => getWebPushConfig());
app.get('/api/notifications/messages', async (request) => {
const query = notificationMessageListQuerySchema.parse(request.query ?? {});
return {
ok: true,
...(await listNotificationMessages(query)),
};
});
app.get('/api/notifications/messages/:id', async (request, reply) => {
const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
const item = await getNotificationMessage(id);
if (!item) {
return reply.code(404).send({
message: '알림 메시지를 찾을 수 없습니다.',
});
}
return {
ok: true,
item,
};
});
app.post('/api/notifications/messages', async (request) => {
const item = await createNotificationMessage(notificationMessagePayloadSchema.parse(request.body ?? {}));
return {
ok: true,
item,
};
});
app.patch('/api/notifications/messages/:id', async (request, reply) => {
const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
const item = await updateNotificationMessageReadState(id, notificationMessageReadPayloadSchema.parse(request.body ?? {}));
if (!item) {
return reply.code(404).send({
message: '상태를 변경할 알림 메시지를 찾을 수 없습니다.',
});
}
return {
ok: true,
item,
};
});
app.delete('/api/notifications/messages/:id', async (request, reply) => {
const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
const deleted = await deleteNotificationMessage(id);
if (!deleted) {
return reply.code(404).send({
message: '삭제할 알림 메시지를 찾을 수 없습니다.',
});
}
return {
ok: true,
deleted: true,
};
});
app.get('/api/notifications/preferences/automation', async (request) => {
const query = automationNotificationPreferenceQuerySchema.parse(request.query ?? {});
const targetId = query.targetId || getClientIdHeader(request);
const targetKind = query.targetId ? query.targetKind ?? 'client' : 'client';
return {
ok: true,
automation: await getAutomationNotificationPreferenceWithFallback(targetId, targetKind),
};
});
app.put('/api/notifications/preferences/automation', async (request, reply) => {
try {
const payload = registerAutomationNotificationPreferenceSchema.parse(request.body ?? {});
const targetId = payload.targetId || getClientIdHeader(request);
if (!targetId) {
throw new Error('알림 설정을 저장할 클라이언트 ID가 없습니다.');
}
return upsertAutomationNotificationPreference({
...payload,
targetId,
});
} catch (error) {
return reply.code(409).send({
message: error instanceof Error ? error.message : '알림 설정 저장에 실패했습니다.',
});
}
});
app.put('/api/notifications/tokens/ios', async (request) => {
const payload = registerIosTokenSchema.parse(request.body ?? {});
return registerIosNotificationToken(payload);
});
app.delete('/api/notifications/tokens/ios', async (request) => {
const payload = unregisterIosTokenSchema.parse(request.body ?? {});
return unregisterIosNotificationToken(payload.token);
});
app.put('/api/notifications/subscriptions/web', async (request) => {
const payload = registerWebPushSubscriptionSchema.parse(request.body ?? {});
return registerWebPushSubscription(payload);
});
app.delete('/api/notifications/subscriptions/web', async (request) => {
const payload = unregisterWebPushSubscriptionSchema.parse(request.body ?? {});
return unregisterWebPushSubscription(payload.endpoint);
});
app.post('/api/notifications/send', async (request) => {
const payload = sendIosNotificationSchema.parse(request.body ?? {});
return sendNotifications(payload);
});
app.post('/api/notifications/send-test', async (request) => {
const payload = sendIosNotificationSchema.parse(request.body ?? {});
return sendNotifications(payload);
});
}
async function getAutomationNotificationPreferenceWithFallback(
targetId: string,
targetKind: AutomationNotificationPreferenceTargetKind,
) {
const automation = await getAutomationNotificationPreference(targetId, targetKind);
if (automation || targetKind !== 'ios-token-client') {
return automation;
}
const [token, clientId] = targetId.split('::client::');
if (token?.trim()) {
const tokenAutomation = await getAutomationNotificationPreference(token.trim(), 'ios-token');
if (tokenAutomation) {
return tokenAutomation;
}
}
if (clientId?.trim()) {
return getAutomationNotificationPreference(clientId.trim(), 'client');
}
return null;
}