# v12: CLI 도구 완성 및 프로젝트 마무리 ## 이번 단계에서 할 일 1. CLI 진입점 구현 (index.ts) 2. 파라미터 처리 및 검증 3. 에러 핸들링 4. 전체 실행 플로우 통합 5. 프로젝트 최종 정리 ## 1. CLI 구조 ### 실행 방식 ```bash # 기본 실행 npx ts-node src/index.ts --spreadsheet-id= # 옵션 포함 npx ts-node src/index.ts \ --spreadsheet-id=1abc...xyz \ --csv-path=./data/transactions.csv \ --skip-ai \ --verbose ``` ### 옵션 목록 | 옵션 | 단축 | 필수 | 기본값 | 설명 | |------|------|------|--------|------| | `--spreadsheet-id` | `-s` | ✅ | - | 대상 Google Sheet ID | | `--csv-path` | `-c` | - | stdin | CSV 파일 경로 | | `--skip-ai` | - | - | false | AI 분석 스킵 | | `--verbose` | `-v` | - | false | 상세 로그 출력 | | `--help` | `-h` | - | - | 도움말 표시 | ## 2. 진입점 구현 ### index.ts ```typescript #!/usr/bin/env node /** * 재무 대시보드 생성 CLI * * Usage: * npx ts-node src/index.ts --spreadsheet-id= [options] * * Options: * -s, --spreadsheet-id Google Sheet ID (필수) * -c, --csv-path CSV 파일 경로 * --skip-ai AI 분석 스킵 * -v, --verbose 상세 로그 * -h, --help 도움말 */ import { Command } from 'commander'; import { DashboardOrchestrator } from './orchestrator'; import * as dotenv from 'dotenv'; dotenv.config(); const program = new Command(); program .name('financial-dashboard') .description('Google Sheet 재무 대시보드 자동 생성 도구') .version('1.0.0') .requiredOption('-s, --spreadsheet-id ', 'Google Sheet ID') .option('-c, --csv-path ', 'CSV 파일 경로') .option('--skip-ai', 'AI 분석 스킵', false) .option('-v, --verbose', '상세 로그 출력', false) .parse(process.argv); const options = program.opts(); async function main(): Promise { console.log('\n╔═══════════════════════════════════════════════════════╗'); console.log('║ 💰 재무 대시보드 자동 생성 도구 v1.0.0 ║'); console.log('╚═══════════════════════════════════════════════════════╝\n'); const orchestrator = new DashboardOrchestrator({ spreadsheetId: options.spreadsheetId, csvPath: options.csvPath, skipAI: options.skipAi, verbose: options.verbose, }); await orchestrator.run(); } main().catch((error) => { console.error('\n❌ 오류 발생:', error.message); process.exit(1); }); ``` ## 3. 오케스트레이터 구현 ### orchestrator.ts ```typescript /** * 대시보드 생성 오케스트레이터 * 전체 플로우를 관리하는 메인 클래스 */ import { SheetsClient } from './sheets/client'; import { CSVReader } from './sheets/reader'; import { FinancialCalculator } from './analysis/calculator'; import { DataAggregator } from './analysis/aggregator'; import { DashboardCreator } from './dashboard/creator'; import { TrendChartGenerator } from './charts/trend-charts'; import { RatioChartGenerator } from './charts/ratio-charts'; import { KPIWidgetGenerator } from './charts/kpi-widgets'; import { InsightWriter } from './insights/writer'; import { Transaction } from './types'; interface OrchestratorOptions { spreadsheetId: string; csvPath?: string; skipAI?: boolean; verbose?: boolean; } export class DashboardOrchestrator { private options: OrchestratorOptions; private sheetsClient: SheetsClient; constructor(options: OrchestratorOptions) { this.options = options; this.sheetsClient = new SheetsClient(); } /** * 전체 대시보드 생성 실행 */ async run(): Promise { const startTime = Date.now(); try { // 1. 데이터 로드 console.log('📂 데이터 로드 중...'); const transactions = await this.loadData(); console.log(` ✅ ${transactions.length}건의 거래 데이터 로드 완료\n`); // 2. 데이터 분석 console.log('📊 데이터 분석 중...'); const calculator = new FinancialCalculator(); const aggregator = new DataAggregator(); const monthlySummaries = calculator.calculateMonthlySummaries(transactions); const categoryBreakdown = calculator.calculateCategoryBreakdown( transactions.filter((t) => t.type === '지출') ); const incomeBreakdown = aggregator.getIncomeBreakdown( transactions.filter((t) => t.type === '수입') ); const annualStats = aggregator.getAnnualStats(transactions); console.log(` ✅ 월별 요약: ${monthlySummaries.length}개월`); console.log(` ✅ 지출 카테고리: ${categoryBreakdown.length}개`); console.log(` ✅ 수입원: ${incomeBreakdown.length}개\n`); // 3. Dashboard 시트 생성 console.log('📋 Dashboard 시트 생성 중...'); const dashboardCreator = new DashboardCreator(this.sheetsClient); const sheetId = await dashboardCreator.createDashboardSheet( this.options.spreadsheetId, monthlySummaries, categoryBreakdown, annualStats ); console.log(` ✅ Dashboard 시트 생성 완료 (ID: ${sheetId})\n`); // 4. 트렌드 차트 생성 console.log('📈 트렌드 차트 생성 중...'); const trendCharts = new TrendChartGenerator(this.sheetsClient); await trendCharts.createAllTrendCharts( this.options.spreadsheetId, sheetId, monthlySummaries, categoryBreakdown ); // 5. 비율/비교 차트 생성 console.log('🥧 비율/비교 차트 생성 중...'); const ratioCharts = new RatioChartGenerator(this.sheetsClient); await ratioCharts.createAllRatioCharts( this.options.spreadsheetId, sheetId, monthlySummaries, categoryBreakdown, incomeBreakdown ); // 6. KPI 위젯 생성 console.log('📊 KPI 위젯 생성 중...'); const kpiWidgets = new KPIWidgetGenerator(this.sheetsClient); await kpiWidgets.createAllKPIWidgets( this.options.spreadsheetId, sheetId, monthlySummaries, categoryBreakdown, annualStats ); // 7. AI 인사이트 (옵션) if (!this.options.skipAI) { console.log('🤖 AI 인사이트 생성 중...'); const insightWriter = new InsightWriter(this.sheetsClient); await insightWriter.generateAndWriteInsights( this.options.spreadsheetId, sheetId, monthlySummaries, categoryBreakdown, annualStats ); } else { console.log('⏭️ AI 인사이트 스킵됨\n'); } // 8. 완료 const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); this.printSummary(annualStats, elapsed); } catch (error) { throw new Error(`대시보드 생성 실패: ${(error as Error).message}`); } } /** * 데이터 로드 */ private async loadData(): Promise { if (this.options.csvPath) { // CSV 파일에서 로드 const reader = new CSVReader(); return reader.readFromFile(this.options.csvPath); } else { // Google Sheet에서 로드 const data = await this.sheetsClient.readSheet( this.options.spreadsheetId, 'Data!A:G' ); return this.parseSheetData(data); } } /** * Sheet 데이터 파싱 */ private parseSheetData(data: string[][]): Transaction[] { return data.slice(1).map((row) => ({ date: row[0], type: row[1] as '수입' | '지출', category: row[2], subcategory: row[3], amount: parseFloat(row[4]), description: row[5], payment_method: row[6], })); } /** * 결과 요약 출력 */ private printSummary( stats: { totalIncome: number; totalExpense: number; netSavings: number; savingsRate: number; }, elapsed: string ): void { console.log('\n╔═══════════════════════════════════════════════════════╗'); console.log('║ ✅ 대시보드 생성 완료! ║'); console.log('╠═══════════════════════════════════════════════════════╣'); console.log(`║ 📊 총 수입: ¥${stats.totalIncome.toLocaleString().padStart(15)} ║`); console.log(`║ 💸 총 지출: ¥${stats.totalExpense.toLocaleString().padStart(15)} ║`); console.log(`║ 💰 순 저축: ¥${stats.netSavings.toLocaleString().padStart(15)} ║`); console.log(`║ 📈 저축률: ${stats.savingsRate.toFixed(1).padStart(16)}% ║`); console.log('╠═══════════════════════════════════════════════════════╣'); console.log(`║ ⏱️ 소요 시간: ${elapsed.padStart(17)}초 ║`); console.log('╚═══════════════════════════════════════════════════════╝'); console.log(`\n🔗 스프레드시트 열기:`); console.log(` https://docs.google.com/spreadsheets/d/${this.options.spreadsheetId}\n`); } } ``` ## 4. CSV 리더 구현 ### sheets/reader.ts 확장 ```typescript /** * CSV 파일 리더 */ import * as fs from 'fs'; import * as path from 'path'; import { Transaction } from '../types'; export class CSVReader { /** * CSV 파일 읽기 */ readFromFile(filePath: string): Transaction[] { const absolutePath = path.resolve(filePath); if (!fs.existsSync(absolutePath)) { throw new Error(`파일을 찾을 수 없습니다: ${absolutePath}`); } const content = fs.readFileSync(absolutePath, 'utf-8'); return this.parseCSV(content); } /** * CSV 문자열 파싱 */ parseCSV(content: string): Transaction[] { const lines = content.trim().split('\n'); const header = lines[0].split(','); // 헤더 검증 const requiredHeaders = ['date', 'type', 'category', 'subcategory', 'amount']; const missingHeaders = requiredHeaders.filter( (h) => !header.map((x) => x.toLowerCase().trim()).includes(h) ); if (missingHeaders.length > 0) { throw new Error(`필수 헤더 누락: ${missingHeaders.join(', ')}`); } // 데이터 파싱 return lines.slice(1).map((line, index) => { const values = this.parseCSVLine(line); if (values.length < 5) { throw new Error(`라인 ${index + 2}: 필드 수 부족`); } const amount = parseFloat(values[4]); if (isNaN(amount)) { throw new Error(`라인 ${index + 2}: 잘못된 금액 형식`); } return { date: values[0].trim(), type: values[1].trim() as '수입' | '지출', category: values[2].trim(), subcategory: values[3].trim(), amount, description: values[5]?.trim() || '', payment_method: values[6]?.trim() || '', }; }); } /** * CSV 라인 파싱 (따옴표 처리) */ private parseCSVLine(line: string): string[] { const result: string[] = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { result.push(current); current = ''; } else { current += char; } } result.push(current); return result; } } ``` ## 5. 에러 핸들링 ### 커스텀 에러 클래스 ```typescript /** * 커스텀 에러 클래스들 */ export class DashboardError extends Error { constructor(message: string, public code: string) { super(message); this.name = 'DashboardError'; } } export class AuthenticationError extends DashboardError { constructor(message: string) { super(message, 'AUTH_ERROR'); this.name = 'AuthenticationError'; } } export class ValidationError extends DashboardError { constructor(message: string) { super(message, 'VALIDATION_ERROR'); this.name = 'ValidationError'; } } export class APIError extends DashboardError { constructor(message: string, public statusCode?: number) { super(message, 'API_ERROR'); this.name = 'APIError'; } } ``` ### 에러 처리 미들웨어 ```typescript /** * 에러 처리 유틸리티 */ export function handleError(error: unknown): never { if (error instanceof DashboardError) { console.error(`\n❌ [${error.code}] ${error.message}`); if (error instanceof AuthenticationError) { console.error('\n💡 해결 방법:'); console.error(' 1. .env 파일에 GOOGLE_SERVICE_ACCOUNT_KEY 확인'); console.error(' 2. 서비스 계정에 스프레드시트 접근 권한 부여'); } else if (error instanceof ValidationError) { console.error('\n💡 해결 방법:'); console.error(' 1. CSV 파일 형식 확인'); console.error(' 2. 필수 필드 존재 확인 (date, type, category, subcategory, amount)'); } } else if (error instanceof Error) { console.error(`\n❌ 예상치 못한 오류: ${error.message}`); console.error('\n스택 트레이스:'); console.error(error.stack); } else { console.error('\n❌ 알 수 없는 오류 발생'); } process.exit(1); } ``` ## 6. 전체 프로젝트 구조 ``` project/ ├── package.json ├── tsconfig.json ├── .env # 환경 변수 ├── .env.example ├── .gitignore └── src/ ├── index.ts # CLI 진입점 ├── orchestrator.ts # 메인 오케스트레이터 ├── types/ │ └── index.ts # 타입 정의 ├── sheets/ │ ├── client.ts # Sheets API 클라이언트 │ ├── reader.ts # CSV/Sheet 리더 │ └── writer.ts # 데이터 작성 ├── analysis/ │ ├── categories.ts # 카테고리 매핑 │ ├── calculator.ts # 재무 계산 │ └── aggregator.ts # 데이터 집계 ├── dashboard/ │ ├── creator.ts # 대시보드 생성 │ └── formatter.ts # 셀 서식 ├── charts/ │ ├── trend-charts.ts # 트렌드 차트 │ ├── ratio-charts.ts # 비율 차트 │ └── kpi-widgets.ts # KPI 위젯 ├── insights/ │ └── writer.ts # AI 인사이트 ├── vertexai/ │ ├── client.ts # Vertex AI 클라이언트 │ └── analyzer.ts # AI 분석기 └── errors/ └── index.ts # 커스텀 에러 ``` ## 7. 실행 예시 ### 기본 실행 ```bash # 환경 변수 설정 export GOOGLE_SERVICE_ACCOUNT_KEY='{"type":"service_account",...}' export GOOGLE_PROJECT_ID='my-project-id' # 실행 npx ts-node src/index.ts -s 1abc123xyz ``` ### 실행 결과 ``` ╔═══════════════════════════════════════════════════════╗ ║ 💰 재무 대시보드 자동 생성 도구 v1.0.0 ║ ╚═══════════════════════════════════════════════════════╝ 📂 데이터 로드 중... ✅ 544건의 거래 데이터 로드 완료 📊 데이터 분석 중... ✅ 월별 요약: 12개월 ✅ 지출 카테고리: 10개 ✅ 수입원: 3개 📋 Dashboard 시트 생성 중... ✅ KPI 섹션 작성 ✅ 월별 추이 테이블 ✅ 카테고리별 집계 ✅ Dashboard 시트 생성 완료 (ID: 123456789) 📈 트렌드 차트 생성 중... ✅ 라인 차트 (월별 수입/지출 추이) ✅ 영역 차트 (누적 저축 추이) ✅ 바 차트 (카테고리별 비교) ✅ 스택 바 차트 (월별 구성 비율) 🥧 비율/비교 차트 생성 중... ✅ 파이 차트 (지출 카테고리 비율) ✅ 도넛 차트 (수입원 비율) ✅ 콤보 차트 (수입/지출 + 저축률) ✅ 워터폴 차트 (월간 순자산 변동) 📊 KPI 위젯 생성 중... ✅ 스파크라인 (미니 추이 그래프) ✅ 스코어카드 (KPI 요약) ✅ 진행바 (예산 소진율) ✅ 히트맵 (월별 지출 강도) ✅ 게이지 (목표 달성률) 🤖 AI 인사이트 생성 중... ✅ AI 분석 성공 ✅ 섹션 헤더 ✅ 종합 분석 요약 ✅ 핵심 발견사항 ✅ 개선 제안 ✅ 월간 목표 트래커 ✅ 서식 적용 ✅ 자연어 요약 생성 ╔═══════════════════════════════════════════════════════╗ ║ ✅ 대시보드 생성 완료! ║ ╠═══════════════════════════════════════════════════════╣ ║ 📊 총 수입: ¥ 5,343,495 ║ ║ 💸 총 지출: ¥ 4,224,666 ║ ║ 💰 순 저축: ¥ 1,118,829 ║ ║ 📈 저축률: 20.9% ║ ╠═══════════════════════════════════════════════════════╣ ║ ⏱️ 소요 시간: 12.34초 ║ ╚═══════════════════════════════════════════════════════╝ 🔗 스프레드시트 열기: https://docs.google.com/spreadsheets/d/1abc123xyz ``` ## 8. 블로그 시리즈 요약 | 버전 | 주제 | 핵심 내용 | |------|------|----------| | v1 | 프로젝트 소개 | 아키텍처, 기술 스택, 목표 | | v2 | 환경 설정 | Node.js, TypeScript, 더미 데이터 | | v3 | Sheets API 기초 | 인증, CRUD, 서식 | | v4 | 데이터 구조 | 타입 정의, 인터페이스 | | v5 | 재무 분석 | 계산기, 집계, MoM 분석 | | v6 | Vertex AI | Gemini API, 프롬프트 엔지니어링 | | v7 | 대시보드 생성 | KPI 섹션, 테이블, 서식 | | v8 | 트렌드 차트 | LINE, AREA, COLUMN, STACKED BAR | | v9 | 비율 차트 | PIE, DONUT, COMBO, WATERFALL | | v10 | KPI 위젯 | SPARKLINE, 게이지, 히트맵, 진행바 | | v11 | AI 인사이트 | 분석 결과 시각화, 목표 트래커 | | v12 | CLI 완성 | 파라미터 처리, 에러 핸들링, 통합 | ## 9. 향후 개선 방향 ### 추가 기능 아이디어 1. **웹 UI**: React/Next.js 기반 웹 인터페이스 2. **정기 실행**: GitHub Actions로 월간 자동 업데이트 3. **알림 연동**: Slack/Discord 알림 4. **다국어 지원**: 영어/일본어 대시보드 5. **예산 관리**: 카테고리별 예산 설정 및 알림 ### 성능 최적화 1. **배치 처리**: API 호출 최소화 2. **캐싱**: 분석 결과 캐싱 3. **병렬 처리**: 차트 생성 병렬화 ## 10. 마무리 이 프로젝트를 통해 구현한 것들: 1. **Google Sheets API** 활용 - 스프레드시트 자동화 2. **TypeScript** - 타입 안전한 코드 3. **Vertex AI (Gemini)** - AI 기반 재무 분석 4. **차트 시각화** - 12종 이상의 차트 5. **CLI 도구** - 사용하기 쉬운 인터페이스 --- **작성일**: 2025-12-01 **상태**: ✅ 완료 **프로젝트**: Google Sheet 재무 대시보드 자동 생성 도구