feat: Gmail → Apps Script → Worker 입금 알림 연동
- POST /api/bank-notification 엔드포인트 추가 - 하나은행 Web발신 SMS 패턴 파싱 지원 - Gmail message_id 기반 중복 방지 - BANK_API_SECRET 인증 추가 - deposit_transactions 자동 매칭 구현 - 문서 업데이트 (CLAUDE.md, README.md) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
86
src/index.ts
86
src/index.ts
@@ -169,6 +169,71 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// Bank Notification API (Gmail → Apps Script → Worker)
|
||||
if (url.pathname === '/api/bank-notification' && request.method === 'POST') {
|
||||
try {
|
||||
const body = await request.json() as { content: string; secret?: string; messageId?: string };
|
||||
|
||||
// 간단한 인증 (BANK_API_SECRET 또는 WEBHOOK_SECRET 사용)
|
||||
const apiSecret = (env as any).BANK_API_SECRET || env.WEBHOOK_SECRET;
|
||||
if (apiSecret && body.secret !== apiSecret) {
|
||||
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
console.log('[API] Bank notification received:', body.content?.slice(0, 100));
|
||||
|
||||
// 메일 ID로 중복 체크
|
||||
if (body.messageId) {
|
||||
const existing = await env.DB.prepare(
|
||||
'SELECT id FROM bank_notifications WHERE message_id = ?'
|
||||
).bind(body.messageId).first();
|
||||
|
||||
if (existing) {
|
||||
console.log('[API] 중복 메일 무시:', body.messageId);
|
||||
return Response.json({ success: true, duplicate: true, messageId: body.messageId });
|
||||
}
|
||||
}
|
||||
|
||||
// SMS 파싱
|
||||
const notification = parseBankSMS(body.content || '');
|
||||
if (!notification) {
|
||||
console.log('[API] 파싱 실패:', body.content);
|
||||
return Response.json({ error: 'Parse failed', content: body.content }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('[API] 파싱 결과:', notification);
|
||||
|
||||
// DB에 저장
|
||||
const insertResult = await env.DB.prepare(
|
||||
`INSERT INTO bank_notifications (bank_name, depositor_name, amount, balance_after, transaction_time, raw_message, message_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
).bind(
|
||||
notification.bankName,
|
||||
notification.depositorName,
|
||||
notification.amount,
|
||||
notification.balanceAfter || null,
|
||||
notification.transactionTime?.toISOString() || null,
|
||||
notification.rawMessage,
|
||||
body.messageId || null
|
||||
).run();
|
||||
|
||||
const notificationId = insertResult.meta.last_row_id;
|
||||
console.log('[API] 알림 저장 완료, ID:', notificationId);
|
||||
|
||||
// 자동 매칭 시도
|
||||
const matched = await tryAutoMatch(env.DB, notificationId as number, notification);
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
notification,
|
||||
matched: !!matched
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[API] Bank notification error:', error);
|
||||
return Response.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Telegram Webhook 처리
|
||||
if (url.pathname === '/webhook') {
|
||||
// 보안 검증
|
||||
@@ -271,7 +336,26 @@ function parseBankSMS(content: string): BankNotification | null {
|
||||
// 이메일에서 SMS 본문 추출 (여러 줄에 걸쳐 있을 수 있음)
|
||||
const text = content.replace(/\r\n/g, '\n').replace(/=\n/g, '');
|
||||
|
||||
// 하나은행 패턴: [하나은행] 01/16 14:30 입금 50,000원 홍길동 잔액 1,234,567원
|
||||
// 하나은행 Web발신 패턴 (여러 줄):
|
||||
// [Web발신]
|
||||
// 하나,01/16, 22:12
|
||||
// 427******27104
|
||||
// 입금1원
|
||||
// 황병하
|
||||
const hanaWebPattern = /\[Web발신\]\s*하나[,\s]*(\d{1,2}\/\d{1,2})[,\s]*(\d{1,2}:\d{2})\s*[\d*]+\s*입금([\d,]+)원\s*(\S+)/;
|
||||
const hanaWebMatch = text.match(hanaWebPattern);
|
||||
if (hanaWebMatch) {
|
||||
const [, date, time, amountStr, depositor] = hanaWebMatch;
|
||||
return {
|
||||
bankName: '하나은행',
|
||||
depositorName: depositor.trim(),
|
||||
amount: parseInt(amountStr.replace(/,/g, '')),
|
||||
transactionTime: parseDateTime(date, time),
|
||||
rawMessage: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
// 하나은행 기존 패턴: [하나은행] 01/16 14:30 입금 50,000원 홍길동 잔액 1,234,567원
|
||||
const hanaPattern = /\[하나은행\]\s*(\d{1,2}\/\d{1,2})\s*(\d{1,2}:\d{2})?\s*입금\s*([\d,]+)원\s*(\S+?)(?:\s+잔액\s*([\d,]+)원)?/;
|
||||
const hanaMatch = text.match(hanaPattern);
|
||||
if (hanaMatch) {
|
||||
|
||||
Reference in New Issue
Block a user