Praxis der Chatbot-Formulardaten-Normalisierung und Serverless-DB-Integration
TuBrief 편집팀
2026년 8월 21일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Formulareingaben, die über interne Messenger-Bots eingehen, in eine Datenbank zu schreiben, ist überraschend umständlich. Der Grund dafür ist, dass die Payload-Strukturen von Slack und Discord völlig unterschiedlich sind. Wenn man jedes Mal Zeit mit der API-Integration verbringt und dabei die SDK-Dokumentation wälzt, überkommt einen als Backend-Entwickler schnell die Sinnkrise.
Slack sendet Daten bei der Einreichung von Modals als dreifach verschachteltes Objekt. Discord übermittelt Werte in Form eines Komponenten-Arrays. Hinzu kommt der Druck, innerhalb von 3 Sekunden nach dem Empfang eines Webhooks antworten zu müssen.
Durch die Kombination des Adapter-Musters und von Zod erstellen wir eine Domänennormalisierungsschicht.
SlackPayloadAdapter und DiscordPayloadAdapter.`typescript
import { z } from 'zod';
export const CommonFormSchema = z.object({
platform: z.enum(['SLACK', 'DISCORD']),
userId: z.string().min(1),
formId: z.string().min(1),
submittedAt: z.date(),
fields: z.object({
applicantName: z.string().min(2),
contactEmail: z.string().email(),
category: z.enum(['BUG', 'FEATURE', 'INQUIRY']),
description: z.string().max(2000),
}),
});
export type NormalizedFormData = z.infer;
export class SlackPayloadAdapter {
static adapt(rawPayload: any): NormalizedFormData {
const values = rawPayload.view?.state?.values || {};
return CommonFormSchema.parse({
platform: 'SLACK',
userId: rawPayload.user?.id,
formId: rawPayload.view?.callback_id,
submittedAt: new Date(),
fields: {
applicantName: values['name_block']?.['name_action']?.value,
contactEmail: values['email_block']?.['email_action']?.value,
category: values['category_block']?.['category_action']?.selected_option?.value,
description: values['desc_block']?.['desc_action']?.value,
},
});
}
}
export class DiscordPayloadAdapter {
static adapt(rawPayload: any): NormalizedFormData {
const components = rawPayload.data?.components || [];
const fieldMap: Record<string, string> = {};
for (const row of components) {
for (const comp of row.components || []) {
if (comp.custom_id) {
fieldMap[comp.custom_id] = comp.value || comp.values?.[0];
}
}
}
return CommonFormSchema.parse({
platform: 'DISCORD',
userId: rawPayload.member?.user?.id || rawPayload.user?.id,
formId: rawPayload.data?.custom_id,
submittedAt: new Date(),
fields: {
applicantName: fieldMap['applicant_name'],
contactEmail: fieldMap['contact_email'],
category: fieldMap['category'],
description: fieldMap['description'],
},
});
}
}
`
Wenn man die Adapter trennt, muss die Business-Logik selbst dann nicht geändert werden, wenn ein neuer Messenger hinzukommt. Das reduziert den Wartungsaufwand.
Vercel Serverless Functions starten für jeden Request eine eigene Instanz. Wenn man sich auf herkömmliche Weise direkt mit Postgres verbindet, sprengt das das Limit für max connections und es kommt zu Fehlern.
Wenn Sie den Serverless Pooler von Neon verwenden, können Sie die Latenz bei Verbindungen drastisch senken und gleichzeitige Anfragen verarbeiten. Duplikate müssen mithilfe eines Idempotenz-Schlüssels verhindert werden.
`typescript
import { Pool } from '@neondatabase/serverless';
const pool = new Pool({ connectionString: process.env.POSTGRES_URL });
export async function insertNormalizedFormsBulk(forms: NormalizedFormData[]) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const insertQuery = `
INSERT INTO form_responses (
idempotency_key,
platform,
user_id,
form_id,
payload,
created_at
)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (idempotency_key)
DO UPDATE SET
payload = EXCLUDED.payload,
created_at = EXCLUDED.created_at
RETURNING id;
`;
for (const form of forms) {
const idempotencyKey = `${form.platform}:${form.userId}:${form.formId}:${form.submittedAt.getTime()}`;
await client.query(insertQuery, [
idempotencyKey,
form.platform,
form.userId,
form.formId,
JSON.stringify(form.fields),
form.submittedAt,
]);
}
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
} }
`
Es ist sicherer, Webhooks schnell mit einer 200er-Antwort zu beantworten und das eigentliche Speichern asynchron abzuwickeln. Das ist der einzige Weg, um das 3-Sekunden-Timeout zu umgehen.
Wenn man ein Modal schließt und einen Fehler ausgibt, nur weil eine Facheingabe fehlerhaft war, frustriert das die Benutzer. Man sollte direkt innerhalb des geöffneten Modals Feedback geben.
`typescript
export function formatSlackValidationErrorResponse(zodError: z.ZodError) {
const errorMap: Record<string, string> = {};
for (const issue of zodError.issues) {
const fieldName = issue.path[issue.path.length - 1];
if (fieldName === 'contactEmail') {
errorMap['email_block'] = issue.message;
} else if (fieldName === 'applicantName') {
errorMap['name_block'] = issue.message;
} else if (fieldName === 'description') {
errorMap['desc_block'] = issue.message;
}
}
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
response_action: 'errors',
errors: errorMap,
}),
};
}
`
Wir verwalten Sitzungen über Redis und setzen ein TTL von 15 Minuten. Wenn man Fehlerprotokolle auswertet, stellt man fest, dass die meisten Abbrüche an E-Mail-Formaten oder Zeichenlimits liegen. Wenn man Modal-Hinweise anpasst und Echtzeit-Feedback einbaut, sinkt die Rate der Eingabefehler erheblich.