refactor: 파일 분리 리팩토링 (routes, services, tools, utils)
아키텍처 개선: - index.ts: 921줄 → 205줄 (77% 감소) - openai-service.ts: 1,356줄 → 148줄 (89% 감소) 새로운 디렉토리 구조: - src/routes/ - Webhook, API, Health check 핸들러 - webhook.ts (287줄) - api.ts (318줄) - health.ts (14줄) - src/services/ - 비즈니스 로직 - bank-sms-parser.ts (143줄) - deposit-matcher.ts (88줄) - src/tools/ - Function Calling 도구 모듈화 - weather-tool.ts (37줄) - search-tool.ts (156줄) - domain-tool.ts (725줄) - deposit-tool.ts (183줄) - utility-tools.ts (60줄) - index.ts (104줄) - 도구 레지스트리 - src/utils/ - 유틸리티 함수 - email-decoder.ts - Quoted-Printable 디코더 타입 에러 수정: - routes/webhook.ts: text undefined 체크 - summary-service.ts: D1 타입 캐스팅 - summary-service.ts: Workers AI 타입 처리 - n8n-service.ts: Workers AI 타입 + 미사용 변수 제거 빌드 검증: - TypeScript 타입 체크 통과 - Wrangler dev 로컬 빌드 성공 문서: - REFACTORING_SUMMARY.md 추가 - ROUTE_ARCHITECTURE.md 추가 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
143
src/services/bank-sms-parser.ts
Normal file
143
src/services/bank-sms-parser.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { BankNotification } from '../types';
|
||||
import { parseQuotedPrintable } from '../utils/email-decoder';
|
||||
|
||||
/**
|
||||
* 은행 SMS 파싱 함수
|
||||
*
|
||||
* 지원 은행:
|
||||
* - 하나은행 (Web발신 + 기존 패턴)
|
||||
* - KB국민은행
|
||||
* - 신한은행
|
||||
* - 일반 패턴 (은행 불명)
|
||||
*
|
||||
* @param content - 이메일 원본 내용 (MIME 포함 가능)
|
||||
* @returns 파싱된 은행 알림 또는 null (파싱 실패)
|
||||
*/
|
||||
export function parseBankSMS(content: string): BankNotification | null {
|
||||
// MIME 이메일 전처리
|
||||
let text = content;
|
||||
|
||||
// Quoted-Printable UTF-8 디코딩
|
||||
text = parseQuotedPrintable(text);
|
||||
|
||||
// HTML <br/> 태그를 줄바꿈으로 변환
|
||||
text = text.replace(/<br\s*\/?>/gi, '\n');
|
||||
|
||||
// 줄바꿈 정규화
|
||||
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
// [Web발신] 또는 은행 키워드가 있는 부분만 추출
|
||||
const smsStartMatch = text.match(/\[Web발신\]|\[하나은행\]|\[KB\]|\[신한\]|\[우리\]|\[농협\]/);
|
||||
if (smsStartMatch && smsStartMatch.index !== undefined) {
|
||||
// SMS 시작점부터 500자 추출
|
||||
text = text.slice(smsStartMatch.index, smsStartMatch.index + 500);
|
||||
}
|
||||
|
||||
// 하나은행 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) {
|
||||
const [, date, time, amountStr, depositor, balanceStr] = hanaMatch;
|
||||
return {
|
||||
bankName: '하나은행',
|
||||
depositorName: depositor,
|
||||
amount: parseInt(amountStr.replace(/,/g, '')),
|
||||
balanceAfter: balanceStr ? parseInt(balanceStr.replace(/,/g, '')) : undefined,
|
||||
transactionTime: parseDateTime(date, time),
|
||||
rawMessage: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
// KB국민은행 패턴: [KB] 입금 50,000원 01/16 14:30 홍길동
|
||||
const kbPattern = /\[KB\]\s*입금\s*([\d,]+)원\s*(\d{1,2}\/\d{1,2})?\s*(\d{1,2}:\d{2})?\s*(\S+)/;
|
||||
const kbMatch = text.match(kbPattern);
|
||||
if (kbMatch) {
|
||||
const [, amountStr, date, time, depositor] = kbMatch;
|
||||
return {
|
||||
bankName: 'KB국민은행',
|
||||
depositorName: depositor,
|
||||
amount: parseInt(amountStr.replace(/,/g, '')),
|
||||
transactionTime: date ? parseDateTime(date, time) : undefined,
|
||||
rawMessage: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
// 신한은행 패턴: [신한] 01/16 입금 50,000원 홍길동
|
||||
const shinhanPattern = /\[신한\]\s*(\d{1,2}\/\d{1,2})?\s*입금\s*([\d,]+)원\s*(\S+)/;
|
||||
const shinhanMatch = text.match(shinhanPattern);
|
||||
if (shinhanMatch) {
|
||||
const [, date, amountStr, depositor] = shinhanMatch;
|
||||
return {
|
||||
bankName: '신한은행',
|
||||
depositorName: depositor,
|
||||
amount: parseInt(amountStr.replace(/,/g, '')),
|
||||
transactionTime: date ? parseDateTime(date) : undefined,
|
||||
rawMessage: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
// 일반 입금 패턴: 입금 50,000원 홍길동 또는 홍길동 50,000원 입금
|
||||
const genericPattern1 = /입금\s*([\d,]+)원?\s*(\S{2,10})/;
|
||||
const genericPattern2 = /(\S{2,10})\s*([\d,]+)원?\s*입금/;
|
||||
|
||||
const genericMatch1 = text.match(genericPattern1);
|
||||
if (genericMatch1) {
|
||||
return {
|
||||
bankName: '알수없음',
|
||||
depositorName: genericMatch1[2],
|
||||
amount: parseInt(genericMatch1[1].replace(/,/g, '')),
|
||||
rawMessage: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
const genericMatch2 = text.match(genericPattern2);
|
||||
if (genericMatch2) {
|
||||
return {
|
||||
bankName: '알수없음',
|
||||
depositorName: genericMatch2[1],
|
||||
amount: parseInt(genericMatch2[2].replace(/,/g, '')),
|
||||
rawMessage: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜/시간 파싱 헬퍼 함수
|
||||
*
|
||||
* @param dateStr - "MM/DD" 형식
|
||||
* @param timeStr - "HH:MM" 형식 (선택)
|
||||
* @returns Date 객체
|
||||
*/
|
||||
function parseDateTime(dateStr: string, timeStr?: string): Date {
|
||||
const now = new Date();
|
||||
const [month, day] = dateStr.split('/').map(Number);
|
||||
const year = now.getFullYear();
|
||||
|
||||
let hours = 0, minutes = 0;
|
||||
if (timeStr) {
|
||||
[hours, minutes] = timeStr.split(':').map(Number);
|
||||
}
|
||||
|
||||
return new Date(year, month - 1, day, hours, minutes);
|
||||
}
|
||||
Reference in New Issue
Block a user