# v3: Google Sheets API 연동 기초 ## 이번 단계에서 할 일 1. Google Sheets API 인증 구현 2. 기본 CRUD 작업 (읽기/쓰기/업데이트) 3. 더미 데이터를 Google Sheet에 import 4. TypeScript 타입 정의 ## 1. 타입 정의 (src/types/index.ts) 먼저 프로젝트 전체에서 사용할 타입을 정의합니다: ```typescript /** * 재무 대시보드 타입 정의 */ // 거래 타입 export type TransactionType = 'income' | 'expense'; // 결제 수단 export type PaymentMethod = '현금' | '신용카드' | '체크카드' | '계좌이체' | 'PayPay' | 'Suica'; // 수입 카테고리 export type IncomeCategory = '급여' | '보너스' | '부수입'; // 지출 카테고리 export type ExpenseCategory = | '식비' | '주거비' | '유틸리티' | '교통비' | '쇼핑' | '의료비' | '교육비' | '엔터테인먼트' | '보험료'; // 전체 카테고리 export type Category = IncomeCategory | ExpenseCategory; // 거래 데이터 export interface Transaction { date: string; // YYYY-MM-DD type: TransactionType; category: Category; subcategory: string; amount: number; description: string; payment_method: PaymentMethod; } // 월별 요약 export interface MonthlySummary { month: string; // YYYY-MM totalIncome: number; totalExpense: number; netSavings: number; transactionCount: number; categoryBreakdown: CategoryBreakdown[]; } // 카테고리별 분석 export interface CategoryBreakdown { category: Category; amount: number; percentage: number; transactionCount: number; } ``` ## 2. Sheets 클라이언트 구현 (src/sheets/client.ts) Google Sheets API와 통신하는 핵심 클라이언트: ```typescript /** * Google Sheets API 클라이언트 */ import { google, sheets_v4 } from 'googleapis'; import { JWT } from 'google-auth-library'; import * as fs from 'fs'; import * as path from 'path'; export class SheetsClient { private sheets: sheets_v4.Sheets; private auth: JWT; constructor() { this.auth = this.createAuth(); this.sheets = google.sheets({ version: 'v4', auth: this.auth }); } /** * 서비스 계정 인증 생성 */ private createAuth(): JWT { const keyPath = process.env.GOOGLE_APPLICATION_CREDENTIALS || './service-account-key.json'; const absolutePath = path.resolve(keyPath); if (!fs.existsSync(absolutePath)) { throw new Error(`서비스 계정 키 파일을 찾을 수 없습니다: ${absolutePath}`); } const credentials = JSON.parse(fs.readFileSync(absolutePath, 'utf-8')); return new JWT({ email: credentials.client_email, key: credentials.private_key, scopes: [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive.file', ], }); } /** * 시트 데이터 읽기 */ async readSheet( spreadsheetId: string, range: string ): Promise { const response = await this.sheets.spreadsheets.values.get({ spreadsheetId, range, }); return (response.data.values as string[][]) || []; } /** * 시트에 데이터 쓰기 */ async writeSheet( spreadsheetId: string, range: string, values: (string | number)[][] ): Promise { const response = await this.sheets.spreadsheets.values.update({ spreadsheetId, range, valueInputOption: 'USER_ENTERED', requestBody: { values }, }); return response.data; } /** * 새 시트 생성 */ async createSheet( spreadsheetId: string, sheetTitle: string ): Promise { const response = await this.sheets.spreadsheets.batchUpdate({ spreadsheetId, requestBody: { requests: [{ addSheet: { properties: { title: sheetTitle }, }, }], }, }); return response.data.replies?.[0]?.addSheet?.properties || {}; } /** * 시트 클리어 */ async clearSheet(spreadsheetId: string, range: string): Promise { await this.sheets.spreadsheets.values.clear({ spreadsheetId, range, }); } /** * 배치 업데이트 (차트, 서식 등) */ async batchUpdate( spreadsheetId: string, requests: sheets_v4.Schema$Request[] ): Promise { const response = await this.sheets.spreadsheets.batchUpdate({ spreadsheetId, requestBody: { requests }, }); return response.data; } } ``` ## 3. 데이터 읽기 모듈 (src/sheets/reader.ts) ```typescript /** * Google Sheets 데이터 읽기 모듈 */ import { SheetsClient } from './client'; import { Transaction, TransactionType, PaymentMethod, Category } from '../types'; export class SheetsReader { private client: SheetsClient; constructor(client: SheetsClient) { this.client = client; } /** * 거래 데이터 읽기 */ async readTransactions( spreadsheetId: string, sheetName: string = 'RawData' ): Promise { const range = `${sheetName}!A:G`; const rows = await this.client.readSheet(spreadsheetId, range); if (rows.length <= 1) { return []; // 헤더만 있거나 비어있음 } const [header, ...dataRows] = rows; return dataRows.map((row) => this.parseTransaction(row, header)); } /** * 행 데이터를 Transaction 객체로 변환 */ private parseTransaction(row: string[], header: string[]): Transaction { const getColumnIndex = (name: string): number => { return header.findIndex(h => h.toLowerCase() === name.toLowerCase()); }; const getValue = (columnName: string): string => { const index = getColumnIndex(columnName); return index >= 0 && row[index] ? row[index] : ''; }; return { date: getValue('date'), type: getValue('type') as TransactionType, category: getValue('category') as Category, subcategory: getValue('subcategory'), amount: parseFloat(getValue('amount')) || 0, description: getValue('description'), payment_method: getValue('payment_method') as PaymentMethod, }; } /** * 특정 기간의 거래 데이터 필터링 */ async readTransactionsByPeriod( spreadsheetId: string, sheetName: string, startDate: string, endDate: string ): Promise { const all = await this.readTransactions(spreadsheetId, sheetName); return all.filter(t => t.date >= startDate && t.date <= endDate); } } ``` ## 4. 데이터 쓰기 모듈 (src/sheets/writer.ts) ```typescript /** * Google Sheets 데이터 쓰기 모듈 */ import { SheetsClient } from './client'; import { Transaction } from '../types'; export class SheetsWriter { private client: SheetsClient; constructor(client: SheetsClient) { this.client = client; } /** * 거래 데이터 쓰기 */ async writeTransactions( spreadsheetId: string, sheetName: string, transactions: Transaction[] ): Promise { // 시트 생성 또는 클리어 await this.client.createOrClearSheet(spreadsheetId, sheetName); // 헤더 + 데이터 const header = ['date', 'type', 'category', 'subcategory', 'amount', 'description', 'payment_method']; const dataRows = transactions.map(t => [ t.date, t.type, t.category, t.subcategory, t.amount, t.description, t.payment_method, ]); await this.client.writeSheet( spreadsheetId, `${sheetName}!A1`, [header, ...dataRows] ); console.log(`✅ ${transactions.length}건 저장 완료`); } /** * CSV 파일에서 import */ async importFromCSV( spreadsheetId: string, sheetName: string, csvPath: string ): Promise { const fs = await import('fs'); const { parse } = await import('csv-parse/sync'); const csvContent = fs.readFileSync(csvPath, 'utf-8'); const records = parse(csvContent, { columns: true, skip_empty_lines: true, }); const transactions: Transaction[] = records.map((r: any) => ({ date: r.date, type: r.type, category: r.category, subcategory: r.subcategory, amount: parseFloat(r.amount), description: r.description, payment_method: r.payment_method, })); await this.writeTransactions(spreadsheetId, sheetName, transactions); return transactions.length; } } ``` ## 5. 테스트 스크립트 ### src/test-sheets.ts ```typescript /** * Google Sheets API 테스트 */ import * as dotenv from 'dotenv'; dotenv.config(); import { SheetsClient } from './sheets/client'; import { SheetsWriter } from './sheets/writer'; import { SheetsReader } from './sheets/reader'; async function main() { const spreadsheetId = process.env.TEST_SPREADSHEET_ID; if (!spreadsheetId) { console.error('❌ TEST_SPREADSHEET_ID가 설정되지 않았습니다.'); process.exit(1); } console.log('🚀 Google Sheets API 테스트 시작...\n'); try { // 1. 클라이언트 초기화 const client = new SheetsClient(); const writer = new SheetsWriter(client); const reader = new SheetsReader(client); console.log('✅ 클라이언트 초기화 성공\n'); // 2. CSV 데이터 import console.log('📥 CSV 데이터 import 중...'); const csvPath = '../data/dummy-financial-data.csv'; const count = await writer.importFromCSV(spreadsheetId, 'RawData', csvPath); console.log(`✅ ${count}건 import 완료\n`); // 3. 데이터 읽기 테스트 console.log('📖 데이터 읽기 테스트...'); const transactions = await reader.readTransactions(spreadsheetId, 'RawData'); console.log(`✅ ${transactions.length}건 읽기 완료`); // 4. 통계 출력 const income = transactions .filter(t => t.type === 'income') .reduce((sum, t) => sum + t.amount, 0); const expense = transactions .filter(t => t.type === 'expense') .reduce((sum, t) => sum + t.amount, 0); console.log('\n📊 요약 통계:'); console.log(` 총 수입: ¥${income.toLocaleString()}`); console.log(` 총 지출: ¥${expense.toLocaleString()}`); console.log(` 순저축: ¥${(income - expense).toLocaleString()}`); } catch (error) { console.error('❌ 오류 발생:', error); process.exit(1); } } main(); ``` ## 6. 실행 및 테스트 ### 패키지 설치 ```bash cd project npm install ``` ### 환경 변수 설정 ```bash cp .env.example .env # .env 파일을 편집하여 실제 값 입력 ``` ### 테스트 실행 ```bash npx ts-node src/test-sheets.ts ``` ### 예상 출력 ``` 🚀 Google Sheets API 테스트 시작... ✅ 클라이언트 초기화 성공 📥 CSV 데이터 import 중... ✅ 544건의 거래 데이터를 'RawData' 시트에 저장했습니다. ✅ 544건 import 완료 📖 데이터 읽기 테스트... ✅ 544건 읽기 완료 📊 요약 통계: 총 수입: ¥5,343,495 총 지출: ¥4,224,666 순저축: ¥1,118,829 ``` ## 7. API 주요 메서드 요약 | 메서드 | 용도 | 예시 | |--------|------|------| | `readSheet()` | 데이터 읽기 | `client.readSheet(id, 'Sheet1!A:G')` | | `writeSheet()` | 데이터 쓰기 | `client.writeSheet(id, 'Sheet1!A1', data)` | | `appendSheet()` | 데이터 추가 | `client.appendSheet(id, 'Sheet1', rows)` | | `createSheet()` | 시트 생성 | `client.createSheet(id, 'NewSheet')` | | `clearSheet()` | 시트 클리어 | `client.clearSheet(id, 'Sheet1')` | | `batchUpdate()` | 배치 작업 | 서식, 차트 등 | ## 현재 프로젝트 구조 ``` project/ ├── src/ │ ├── sheets/ │ │ ├── client.ts ✅ API 클라이언트 │ │ ├── reader.ts ✅ 데이터 읽기 │ │ └── writer.ts ✅ 데이터 쓰기 │ ├── types/ │ │ └── index.ts ✅ 타입 정의 │ └── test-sheets.ts ✅ 테스트 스크립트 ├── package.json ✅ ├── tsconfig.json ✅ ├── .env.example ✅ └── .gitignore ✅ ``` ## 다음 단계 v4에서는: - 재무 데이터 구조 설계 (시트별 역할 정의) - 분석용 시트 템플릿 생성 - 카테고리 매핑 테이블 구현 --- **작성일**: 2025-12-01 **상태**: ✅ 완료 **다음**: v4 - 재무 데이터 구조 설계