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:
kappa
2026-01-19 15:36:17 +09:00
parent 3bf42947a7
commit ab6c9a2efa
18 changed files with 2578 additions and 1958 deletions

156
src/tools/search-tool.ts Normal file
View File

@@ -0,0 +1,156 @@
import type { Env } from '../types';
// Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회)
const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions';
export const searchWebTool = {
type: 'function',
function: {
name: 'search_web',
description: '웹에서 최신 정보를 검색합니다. 실시간 가격, 뉴스, 현재 날짜 이후 정보, 특정 사실 확인이 필요할 때 반드시 사용하세요. "비트코인 가격", "오늘 뉴스", "~란", "~뭐야" 등의 질문에 사용합니다.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: '검색 쿼리',
},
},
required: ['query'],
},
},
};
export const lookupDocsTool = {
type: 'function',
function: {
name: 'lookup_docs',
description: '프로그래밍 라이브러리의 공식 문서를 조회합니다. React, OpenAI, Cloudflare Workers 등의 최신 문서와 코드 예제를 검색할 수 있습니다.',
parameters: {
type: 'object',
properties: {
library: {
type: 'string',
description: '라이브러리 이름 (예: react, openai, cloudflare-workers, next.js)',
},
query: {
type: 'string',
description: '찾고 싶은 내용 (예: hooks 사용법, API 호출 방법)',
},
},
required: ['library', 'query'],
},
},
};
export async function executeSearchWeb(args: { query: string }, env?: Env): Promise<string> {
let query = args.query;
try {
if (!env?.BRAVE_API_KEY) {
return `🔍 검색 기능이 설정되지 않았습니다.`;
}
// 한글이 포함된 경우 영문으로 번역 (기술 용어, 제품명 등)
const hasKorean = /[가-힣]/.test(query);
let translatedQuery = query;
if (hasKorean && env?.OPENAI_API_KEY) {
try {
const translateRes = await fetch(OPENAI_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: `사용자의 검색어를 영문으로 번역하세요.
- 외래어/기술용어는 원래 영문 표기로 변환 (예: 판골린→Pangolin, 도커→Docker)
- 일반 한국어는 영문으로 번역
- 검색에 최적화된 키워드로 변환
- 번역된 검색어만 출력, 설명 없이`
},
{ role: 'user', content: query }
],
max_tokens: 100,
temperature: 0.3,
}),
});
if (translateRes.ok) {
const translateData = await translateRes.json() as any;
translatedQuery = translateData.choices?.[0]?.message?.content?.trim() || query;
console.log(`[search_web] 번역: "${query}" → "${translatedQuery}"`);
}
} catch {
// 번역 실패 시 원본 사용
}
}
const response = await fetch(
`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(translatedQuery)}&count=5`,
{
headers: {
'Accept': 'application/json',
'X-Subscription-Token': env.BRAVE_API_KEY,
},
}
);
if (!response.ok) {
return `🔍 검색 오류: ${response.status}`;
}
const data = await response.json() as any;
// Web 검색 결과 파싱
const webResults = data.web?.results || [];
if (webResults.length === 0) {
return `🔍 "${query}"에 대한 검색 결과가 없습니다.`;
}
const results = webResults.slice(0, 3).map((r: any, i: number) =>
`${i + 1}. <b>${r.title}</b>\n ${r.description}\n ${r.url}`
).join('\n\n');
// 번역된 경우 원본 쿼리도 표시
const queryDisplay = (hasKorean && translatedQuery !== query)
? `${query} (→ ${translatedQuery})`
: query;
return `🔍 검색 결과: ${queryDisplay}\n\n${results}`;
} catch (error) {
return `검색 중 오류가 발생했습니다: ${String(error)}`;
}
}
export async function executeLookupDocs(args: { library: string; query: string }): Promise<string> {
const { library, query } = args;
try {
// Context7 REST API 직접 호출
// 1. 라이브러리 검색
const searchUrl = `https://context7.com/api/v2/libs/search?libraryName=${encodeURIComponent(library)}&query=${encodeURIComponent(query)}`;
const searchResponse = await fetch(searchUrl);
const searchData = await searchResponse.json() as any;
if (!searchData.libraries?.length) {
return `📚 "${library}" 라이브러리를 찾을 수 없습니다.`;
}
const libraryId = searchData.libraries[0].id;
// 2. 문서 조회
const docsUrl = `https://context7.com/api/v2/context?libraryId=${encodeURIComponent(libraryId)}&query=${encodeURIComponent(query)}`;
const docsResponse = await fetch(docsUrl);
const docsData = await docsResponse.json() as any;
if (docsData.error) {
return `📚 문서 조회 실패: ${docsData.message || docsData.error}`;
}
const content = docsData.context || docsData.content || JSON.stringify(docsData, null, 2);
return `📚 ${library} 문서 (${query}):\n\n${content.slice(0, 1500)}`;
} catch (error) {
return `📚 문서 조회 중 오류: ${String(error)}`;
}
}