# v10: 에러 핸들링, 최적화 및 프로젝트 마무리 ## 프로젝트 회고 v1부터 v9까지 우리는: 1. ✅ Money Forward OAuth 2.0 인증 구현 2. ✅ Money Forward API 거래 데이터 조회 3. ✅ Google Cloud Vertex AI 설정 및 연동 4. ✅ Gemini를 활용한 재무 분석 프롬프트 작성 5. ✅ 데이터 전처리 및 최적화 6. ✅ CLI 통합 및 자동화 이제 프로덕션 레벨로 끌어올리기 위한 마지막 단계를 진행합니다. ## 에러 핸들링 ### src/utils/error-handler.ts ```typescript export class AppError extends Error { constructor( message: string, public code: string, public statusCode: number = 500 ) { super(message); this.name = 'AppError'; } } export class AuthenticationError extends AppError { constructor(message: string) { super(message, 'AUTH_ERROR', 401); this.name = 'AuthenticationError'; } } export class APIError extends AppError { constructor(message: string, public service: string) { super(message, 'API_ERROR', 502); this.name = 'APIError'; } } export class ValidationError extends AppError { constructor(message: string) { super(message, 'VALIDATION_ERROR', 400); this.name = 'ValidationError'; } } export class ErrorHandler { static handle(error: unknown): void { if (error instanceof AuthenticationError) { console.error('❌ 인증 에러:', error.message); console.log('💡 `npm run auth` 명령어로 재인증하세요'); } else if (error instanceof APIError) { console.error(`❌ API 에러 (${error.service}):`, error.message); console.log('💡 서비스 상태를 확인하고 잠시 후 다시 시도하세요'); } else if (error instanceof ValidationError) { console.error('❌ 입력 에러:', error.message); } else if (error instanceof Error) { console.error('❌ 예상치 못한 에러:', error.message); console.error(error.stack); } else { console.error('❌ 알 수 없는 에러:', error); } process.exit(1); } } ``` ### Retry 로직 구현 #### src/utils/retry.ts ```typescript export async function retryWithBackoff( fn: () => Promise, maxRetries: number = 3, initialDelay: number = 1000 ): Promise { let lastError: Error; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; if (attempt < maxRetries - 1) { const delay = initialDelay * Math.pow(2, attempt); console.log(`⚠️ 시도 ${attempt + 1}/${maxRetries} 실패. ${delay}ms 후 재시도...`); await sleep(delay); } } } throw lastError!; } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } ``` ### MoneyForwardClient에 적용 ```typescript import { retryWithBackoff } from '../utils/retry'; import { APIError } from '../utils/error-handler'; export class MoneyForwardClient { async getTransactions(params?: any): Promise { try { return await retryWithBackoff(async () => { const response = await this.client.get('/transactions', { params }); return response.data.transactions; }); } catch (error) { throw new APIError('Money Forward 거래 내역 조회 실패', 'MoneyForward'); } } } ``` ## 로깅 시스템 ### src/utils/logger.ts ```typescript import chalk from 'chalk'; import fs from 'fs/promises'; import path from 'path'; export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, } export class Logger { private level: LogLevel; private logFile?: string; constructor(level: LogLevel = LogLevel.INFO, logFile?: string) { this.level = level; this.logFile = logFile; } async debug(message: string, ...args: any[]): Promise { if (this.level <= LogLevel.DEBUG) { console.log(chalk.gray(`[DEBUG] ${message}`), ...args); await this.writeToFile('DEBUG', message, args); } } async info(message: string, ...args: any[]): Promise { if (this.level <= LogLevel.INFO) { console.log(chalk.blue(`[INFO] ${message}`), ...args); await this.writeToFile('INFO', message, args); } } async warn(message: string, ...args: any[]): Promise { if (this.level <= LogLevel.WARN) { console.log(chalk.yellow(`[WARN] ${message}`), ...args); await this.writeToFile('WARN', message, args); } } async error(message: string, ...args: any[]): Promise { if (this.level <= LogLevel.ERROR) { console.error(chalk.red(`[ERROR] ${message}`), ...args); await this.writeToFile('ERROR', message, args); } } private async writeToFile( level: string, message: string, args: any[] ): Promise { if (!this.logFile) return; const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] [${level}] ${message} ${args.length > 0 ? JSON.stringify(args) : ''}\n`; const logDir = path.dirname(this.logFile); await fs.mkdir(logDir, { recursive: true }); await fs.appendFile(this.logFile, logMessage); } } // 싱글톤 인스턴스 export const logger = new Logger( process.env.LOG_LEVEL === 'debug' ? LogLevel.DEBUG : LogLevel.INFO, process.env.LOG_FILE || './logs/app.log' ); ``` ## 설정 관리 ### src/config/index.ts ```typescript import 'dotenv/config'; export interface AppConfig { moneyForward: { clientId: string; clientSecret: string; redirectUri: string; }; vertexAI: { projectId: string; location: string; model: string; }; app: { env: 'development' | 'production'; logLevel: string; cacheEnabled: boolean; }; } function getConfig(): AppConfig { const required = [ 'MONEYFORWARD_CLIENT_ID', 'MONEYFORWARD_CLIENT_SECRET', 'GOOGLE_CLOUD_PROJECT', ]; const missing = required.filter((key) => !process.env[key]); if (missing.length > 0) { throw new Error(`환경 변수 누락: ${missing.join(', ')}`); } return { moneyForward: { clientId: process.env.MONEYFORWARD_CLIENT_ID!, clientSecret: process.env.MONEYFORWARD_CLIENT_SECRET!, redirectUri: process.env.MONEYFORWARD_REDIRECT_URI || 'http://localhost:3000/callback', }, vertexAI: { projectId: process.env.GOOGLE_CLOUD_PROJECT!, location: process.env.VERTEX_AI_LOCATION || 'us-central1', model: process.env.VERTEX_AI_MODEL || 'gemini-1.5-flash', }, app: { env: (process.env.NODE_ENV as any) || 'development', logLevel: process.env.LOG_LEVEL || 'info', cacheEnabled: process.env.CACHE_ENABLED === 'true', }, }; } export const config = getConfig(); ``` ## 테스트 ### tests/unit/transaction-analyzer.test.ts ```typescript import { TransactionAnalyzer } from '../../src/utils/transaction-analyzer'; import { MFTransaction } from '../../src/types'; describe('TransactionAnalyzer', () => { const mockTransactions: MFTransaction[] = [ { id: '1', date: '2025-11-01', content: 'テスト1', amount: 1000, is_income: false, category: { id: 'c1', name: '食費' }, account: { id: 'a1', name: '銀行', type: 'bank', balance: 10000 }, }, { id: '2', date: '2025-11-02', content: 'テスト2', amount: 2000, is_income: true, category: { id: 'c2', name: '給与' }, account: { id: 'a1', name: '銀行', type: 'bank', balance: 12000 }, }, ]; test('基本統計を正しく計算', () => { const stats = TransactionAnalyzer.getBasicStats(mockTransactions); expect(stats.totalIncome).toBe(2000); expect(stats.totalExpense).toBe(1000); expect(stats.netChange).toBe(1000); expect(stats.transactionCount).toBe(2); }); test('カテゴリ別にグループ化', () => { const grouped = TransactionAnalyzer.groupByCategory(mockTransactions); expect(grouped.length).toBeGreaterThan(0); expect(grouped[0]).toHaveProperty('name'); expect(grouped[0]).toHaveProperty('amount'); }); }); ``` ### package.json에 테스트 추가 ```json { "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, "devDependencies": { "@types/jest": "^29.5.0", "jest": "^29.5.0", "ts-jest": "^29.1.0" } } ``` ### jest.config.js ```javascript module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['/tests'], testMatch: ['**/*.test.ts'], collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], }; ``` ## README.md 최종 작성 ```markdown # Money Forward + Vertex AI 재무 분석 도구 TypeScript로 작성된 자동 재무 분석 도구입니다. Money Forward API로 거래 데이터를 수집하고, Google Cloud Vertex AI (Gemini)로 AI 기반 인사이트를 제공합니다. ## 🌟 주요 기능 - ✅ Money Forward OAuth 2.0 인증 - ✅ 거래 내역 자동 수집 - ✅ Vertex AI (Gemini)를 활용한 AI 분석 - ✅ 카테고리별 지출 분석 - ✅ 전월 대비 분석 - ✅ 대화형 질문 응답 - ✅ 리포트 자동 생성 (Markdown, JSON, CSV) - ✅ CLI 및 자동화 지원 ## 📋 요구사항 - Node.js v18 이상 - Money Forward 계정 및 API 애플리케이션 - Google Cloud 계정 및 Vertex AI 접근 권한 ## 🚀 시작하기 ### 1. 설치 \`\`\`bash git clone cd moneyforward-vertexai-analyzer npm install \`\`\` ### 2. 환경 설정 \`\`\`bash cp .env.example .env # .env 파일에 실제 API 키 입력 \`\`\` ### 3. OAuth 인증 \`\`\`bash npm run auth \`\`\` 브라우저에서 Money Forward 인증을 완료합니다. ### 4. 분석 실행 \`\`\`bash # 기본 분석 (최근 1개월) npm run analyze # 특정 월 분석 npm run analyze -- -m 2025-11 # 상세 분석 + 파일 저장 npm run analyze -- -d -s # 대화형 모드 npm run chat \`\`\` ## 📚 CLI 옵션 \`\`\` -m, --month 분석할 월 지정 -d, --detailed 상세 분석 모드 -s, --save 분석 결과를 파일로 저장 -o, --output <경로> 저장 경로 지정 -h, --help 도움말 표시 \`\`\` ## 🏗️ 프로젝트 구조 \`\`\` src/ ├── moneyforward/ # Money Forward API 클라이언트 ├── vertexai/ # Vertex AI 클라이언트 ├── utils/ # 유틸리티 함수 ├── types/ # TypeScript 타입 정의 ├── config/ # 설정 관리 └── index.ts # 메인 엔트리 포인트 \`\`\` ## 🔒 보안 - API 키와 Secret는 `.env` 파일에 저장 - `.env`는 절대 Git에 커밋하지 않음 - 서비스 계정 키는 안전하게 보관 - OAuth 토큰은 `.tokens/` 디렉토리에 저장 ## 🧪 테스트 \`\`\`bash npm test npm run test:coverage \`\`\` ## 📝 라이선스 MIT ## 👤 작성자 Your Name ## 🙏 감사의 말 - [Money Forward](https://moneyforward.com) - [Google Cloud Vertex AI](https://cloud.google.com/vertex-ai) \`\`\` ## 배포 전 체크리스트 - [ ] 모든 환경 변수 설정 확인 - [ ] 에러 핸들링 구현 완료 - [ ] 로깅 시스템 구현 - [ ] 테스트 작성 및 통과 - [ ] README.md 작성 - [ ] `.gitignore` 확인 - [ ] 보안 취약점 점검 - [ ] 의존성 업데이트 - [ ] 라이선스 파일 추가 - [ ] CHANGELOG.md 작성 ## 개선 아이디어 ### 단기 1. **알림 기능**: 예산 초과 시 이메일/Slack 알림 2. **예산 관리**: 카테고리별 예산 설정 및 추적 3. **비교 분석**: 월별, 연도별 비교 차트 4. **대시보드**: 웹 UI 추가 (React/Next.js) ### 장기 1. **예측 모델**: 미래 지출 예측 2. **자동 카테고리 분류**: AI 기반 자동 분류 3. **다중 계정 지원**: 여러 Money Forward 계정 관리 4. **투자 분석**: 자산 포트폴리오 분석 5. **가족 공유**: 가계부 공유 기능 ## 프로젝트 완료! 축하합니다! Money Forward + Vertex AI 재무 분석 시스템을 성공적으로 구축했습니다. ### 배운 점 1. **OAuth 2.0**: 실제 서비스의 인증 플로우 구현 2. **API 통합**: 외부 API 연동 및 에러 처리 3. **AI 프롬프트 엔지니어링**: 효과적인 프롬프트 작성 4. **TypeScript**: 타입 안정성과 코드 품질 5. **클라우드 서비스**: GCP와 Vertex AI 활용 ### 다음 단계 1. 실제 데이터로 꾸준히 사용하며 개선 2. 커뮤니티와 공유 (GitHub, 블로그) 3. 오픈소스 기여 4. 상용 서비스로 확장 (선택) ## 참고 자료 - [Money Forward API 문서](https://developers.moneyforward.com) - [Vertex AI 문서](https://cloud.google.com/vertex-ai/docs) - [OAuth 2.0 RFC](https://datatracker.ietf.org/doc/html/rfc6749) - [TypeScript 공식 문서](https://www.typescriptlang.org/) - [Node.js 모범 사례](https://github.com/goldbergyoni/nodebestpractices) --- **작성일**: 2025-11-30 **상태**: ✅ 완료 **시리즈**: v1~v10 모두 완료 🎉 이 시리즈를 마치며, 여러분의 재무 관리에 AI가 든든한 조력자가 되길 바랍니다!