feat(domain): enhance domain info lookup & handler refactoring
- 도메인 조회(info): 내 도메인 아니면 자동으로 WHOIS 조회 (naver.com 등 지원) - SMS 파싱: 정규식 실패 시 AI 폴백 로직 추가 - 리팩토링: UserService, ConversationService 분리 - 문서: README.md 및 CODE_REVIEW.md 업데이트
This commit is contained in:
@@ -1,45 +1,62 @@
|
||||
import { BankNotification } from '../types';
|
||||
import { Env, BankNotification } from '../types';
|
||||
import { parseQuotedPrintable } from '../utils/email-decoder';
|
||||
|
||||
/**
|
||||
* 은행 SMS 파싱 함수
|
||||
*
|
||||
* 지원 은행:
|
||||
* - 하나은행 (Web발신 + 기존 패턴)
|
||||
* - KB국민은행
|
||||
* - 신한은행
|
||||
* - 일반 패턴 (은행 불명)
|
||||
* 1단계: 정규표현식 기반 파싱 (빠름)
|
||||
* 2단계: 실패 시 AI 모델 기반 파싱 (느리지만 유연함)
|
||||
*
|
||||
* @param content - 이메일 원본 내용 (MIME 포함 가능)
|
||||
* @returns 파싱된 은행 알림 또는 null (파싱 실패)
|
||||
* @param content - 이메일 원본 내용
|
||||
* @param env - 환경 변수 (AI 사용 위함)
|
||||
* @returns 파싱된 은행 알림 또는 null
|
||||
*/
|
||||
export function parseBankSMS(content: string): BankNotification | null {
|
||||
// MIME 이메일 전처리
|
||||
export async function parseBankSMS(content: string, env?: Env): Promise<BankNotification | null> {
|
||||
// 1. 전처리 (MIME 디코딩 등)
|
||||
let text = preprocessText(content);
|
||||
|
||||
// 2. 정규표현식 파싱 시도
|
||||
const regexResult = parseWithRegex(text);
|
||||
if (regexResult) {
|
||||
return regexResult;
|
||||
}
|
||||
|
||||
// 3. AI 파싱 시도 (Fallback)
|
||||
if (env) {
|
||||
console.log('[BankSMS] 정규식 파싱 실패, AI 파싱 시도...');
|
||||
try {
|
||||
// AI 파싱은 비용/시간이 들므로 SMS 패턴이 의심될 때만 시도
|
||||
// "입금", "원" 같은 키워드가 있는지 확인
|
||||
if (text.includes('입금') && (text.includes('원') || text.match(/\d/))) {
|
||||
return await parseWithAI(text, env);
|
||||
} else {
|
||||
console.log('[BankSMS] 입금 키워드 없음, AI 파싱 건너뜀');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BankSMS] AI 파싱 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function preprocessText(content: string): string {
|
||||
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\]|\[신한\]|\[우리\]|\[농협\]/);
|
||||
const smsStartMatch = text.match(/ \[Web발신\]| \[하나은행\]| \[KB\]| \[신한\]| \[우리\]| \[농협\]/);
|
||||
if (smsStartMatch && smsStartMatch.index !== undefined) {
|
||||
// SMS 시작점부터 500자 추출
|
||||
text = text.slice(smsStartMatch.index, smsStartMatch.index + 500);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// 하나은행 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+)/;
|
||||
function parseWithRegex(text: string): BankNotification | null {
|
||||
// 하나은행 Web발신
|
||||
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;
|
||||
@@ -52,8 +69,8 @@ export function parseBankSMS(content: string): BankNotification | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 하나은행 기존 패턴: [하나은행] 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 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;
|
||||
@@ -67,8 +84,8 @@ export function parseBankSMS(content: string): BankNotification | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 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+)/;
|
||||
// KB국민은행
|
||||
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;
|
||||
@@ -81,8 +98,8 @@ export function parseBankSMS(content: string): BankNotification | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 신한은행 패턴: [신한] 01/16 입금 50,000원 홍길동
|
||||
const shinhanPattern = /\[신한\]\s*(\d{1,2}\/\d{1,2})?\s*입금\s*([\d,]+)원\s*(\S+)/;
|
||||
// 신한은행
|
||||
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;
|
||||
@@ -95,8 +112,8 @@ export function parseBankSMS(content: string): BankNotification | null {
|
||||
};
|
||||
}
|
||||
|
||||
// 일반 입금 패턴: 입금 50,000원 홍길동 또는 홍길동 50,000원 입금
|
||||
const genericPattern1 = /입금\s*([\d,]+)원?\s*(\S{2,10})/;
|
||||
// 일반 패턴
|
||||
const genericPattern1 = /입금\s*([\d,]+)원?\s*(\S{2,10})/;
|
||||
const genericPattern2 = /(\S{2,10})\s*([\d,]+)원?\s*입금/;
|
||||
|
||||
const genericMatch1 = text.match(genericPattern1);
|
||||
@@ -122,13 +139,89 @@ export function parseBankSMS(content: string): BankNotification | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜/시간 파싱 헬퍼 함수
|
||||
*
|
||||
* @param dateStr - "MM/DD" 형식
|
||||
* @param timeStr - "HH:MM" 형식 (선택)
|
||||
* @returns Date 객체
|
||||
*/
|
||||
async function parseWithAI(text: string, env: Env): Promise<BankNotification | null> {
|
||||
const prompt = `
|
||||
아래 텍스트는 은행 입금 알림 문자 메시지입니다.
|
||||
이 텍스트에서 [은행명, 입금자명, 입금액, 거래시간] 정보를 추출하여 JSON 형식으로 반환하세요.
|
||||
|
||||
텍스트:
|
||||
"""
|
||||
${text.slice(0, 500)}
|
||||
"""
|
||||
|
||||
요구사항:
|
||||
1. JSON 형식만 반환하세요. (마크다운 코드블록 없이)
|
||||
2. 필드명: bankName, depositorName, amount (숫자), transactionTime (YYYY-MM-DDTHH:mm:ss 형식, 연도는 ${new Date().getFullYear()}년 기준)
|
||||
3. 추출할 수 없는 필드는 null로 설정하세요.
|
||||
4. "입금"이 아닌 "출금"이거나 광고/스팸 메시지라면 null을 반환하세요.
|
||||
`;
|
||||
|
||||
let jsonStr = '';
|
||||
|
||||
// 1. OpenAI 시도
|
||||
if (env.OPENAI_API_KEY) {
|
||||
try {
|
||||
const response = await fetch('https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${env.OPENAI_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 200,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json() as any;
|
||||
jsonStr = data.choices[0].message.content;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[BankSMS] OpenAI 파싱 실패:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Workers AI 시도 (OpenAI 실패 또는 미설정 시)
|
||||
if (!jsonStr && env.AI) {
|
||||
try {
|
||||
const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct' as any, {
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 200,
|
||||
}) as any;
|
||||
jsonStr = response.response;
|
||||
} catch (e) {
|
||||
console.error('[BankSMS] Workers AI 파싱 실패:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!jsonStr) return null;
|
||||
|
||||
try {
|
||||
// JSON 파싱 (마크다운 제거 등 정제)
|
||||
const cleanJson = jsonStr.replace(/```json|```/g, '').trim();
|
||||
if (cleanJson === 'null') return null;
|
||||
|
||||
const result = JSON.parse(cleanJson);
|
||||
|
||||
if (result.amount && result.depositorName) {
|
||||
return {
|
||||
bankName: result.bankName || '알수없음',
|
||||
depositorName: result.depositorName,
|
||||
amount: typeof result.amount === 'string' ? parseInt(result.amount.replace(/,/g, '')) : result.amount,
|
||||
transactionTime: result.transactionTime ? new Date(result.transactionTime) : undefined,
|
||||
rawMessage: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[BankSMS] AI 응답 JSON 파싱 실패:', e, jsonStr);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseDateTime(dateStr: string, timeStr?: string): Date {
|
||||
const now = new Date();
|
||||
const [month, day] = dateStr.split('/').map(Number);
|
||||
@@ -140,4 +233,4 @@ function parseDateTime(dateStr: string, timeStr?: string): Date {
|
||||
}
|
||||
|
||||
return new Date(year, month - 1, day, hours, minutes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user