import { Env, BankNotification } from '../types'; import { parseQuotedPrintable } from '../utils/email-decoder'; /** * 은행 SMS 파싱 함수 * * 1단계: 정규표현식 기반 파싱 (빠름) * 2단계: 실패 시 AI 모델 기반 파싱 (느리지만 유연함) * * @param content - 이메일 원본 내용 * @param env - 환경 변수 (AI 사용 위함) * @returns 파싱된 은행 알림 또는 null */ export async function parseBankSMS(content: string, env?: Env): Promise { // 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; text = parseQuotedPrintable(text); text = text.replace(//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) { text = text.slice(smsStartMatch.index, smsStartMatch.index + 500); } return text; } 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; return { bankName: '하나은행', depositorName: depositor.trim(), amount: parseInt(amountStr.replace(/,/g, '')), transactionTime: parseDateTime(date, time), rawMessage: text.slice(0, 500), }; } // 하나은행 레거시 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국민은행 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), }; } // 신한은행 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), }; } // 일반 패턴 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; } async function parseWithAI(text: string, env: Env): Promise { 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); 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); }