# 프롬프트 및 상호작용 기록 (Prompts) 이 문서는 프로젝트 진행 중 사용된 주요 프롬프트와 대화 내용을 기록합니다. --- ## 2025-11-27: 프로젝트 재개 및 완성 ### 세션 시작 **사용자**: ``` 오늘의 작업디렉토리는 20251126-googlesheet-vertexai-freee-script 로 어제의 작업을 이어서 한다. ``` **컨텍스트**: 전날(2025-11-26)에 v1~v5까지 진행했으나 Freee API 인증 문제(`invalid_grant`)로 중단된 상태. --- ### Phase 1: Freee API 인증 문제 해결 #### 문제 진단 **사용자**: (Authorization code 제공) ``` LWheey2lsXRuiVwvKxCg0goTLKRIWVHRPh4KS_pknzs ``` **발견된 이슈**: 1. 잘못된 BUSINESS_ID: `12261708` → 정확한 ID는 `12261605` 2. 만료된 리프레시 토큰 #### 해결 과정 **프롬프트**: `.env` 파일 수정 ```bash FREEE_BUSINESS_ID=12261605 FREEE_REFRESH_TOKEN=<새로발급받은토큰> ``` **테스트 실행**: ```bash npx ts-node src/index.ts ``` **성공**: 11개의 거래 내역 조회 성공 --- ### Phase 2: 토큰 영구 저장 시스템 구현 #### 사용자 질문 **사용자**: ``` freee의 authorization code 취득도 자동으로 하게 할 수 있을까? ``` **답변 요약**: OAuth2 보안 규정상 authorization code 획득은 자동화 불가능. 그러나 한 번의 인증 후 리프레시 토큰을 영구 저장하면 이후 완전 자동화 가능. #### 구현 방향 **핵심 아이디어**: `tokenManager.ts`를 생성하여 Rotating Refresh Token을 `tokens.json`에 자동 저장. **구현 코드 개요**: ```typescript // src/tokenManager.ts export const saveRefreshToken = (refreshToken: string): void => { const tokenData = { refresh_token: refreshToken, updated_at: new Date().toISOString() }; fs.writeFileSync(TOKEN_FILE_PATH, JSON.stringify(tokenData, null, 2)); }; export const loadRefreshToken = (): string | null => { if (!fs.existsSync(TOKEN_FILE_PATH)) return null; const data = fs.readFileSync(TOKEN_FILE_PATH, 'utf-8'); return JSON.parse(data).refresh_token; }; ``` **freeeClient.ts 수정**: ```typescript let currentRefreshToken = loadRefreshToken() || process.env.FREEE_REFRESH_TOKEN; // 토큰 갱신 시 자동 저장 if (newRefreshToken && newRefreshToken !== currentRefreshToken) { saveRefreshToken(newRefreshToken); currentRefreshToken = newRefreshToken; } ``` --- ### Phase 3: Google Sheets 자동 동기화 #### 사용자 요청 **사용자**: ``` 이 시트에 Freee에서 가져온 거래 내역을 자동으로 추가하는 기능을 구현해줘 ``` **Google Sheet URL**: https://docs.google.com/spreadsheets/d/12TdK5xep5OSyf8PEXnaVkG0qATIu7sHcLAGFPHFD3rw/edit #### 구현 내용 **핵심 기능**: 1. 시트 자동 생성 (`ensureSheetExists`) 2. 헤더 자동 추가 (`checkHeaderExists`) 3. 중복 체크 (`getExistingDealIds`) 4. 새 거래만 추가 (`appendFreeeDeals`) **주요 코드**: ```typescript export const appendFreeeDeals = async ( deals: any[], categories?: Map, sheetName: string = 'Freee거래내역' ) => { // 중복 체크 const existingIds = await getExistingDealIds(sheetName); const newDeals = deals.filter(deal => !existingIds.has(deal.id.toString())); // 데이터 행 생성 const rows = newDeals.map(deal => [ deal.id.toString(), deal.issue_date || '', deal.type === 'income' ? '수입' : '지출', deal.amount || 0, deal.status === 'settled' ? '완료' : deal.status, deal.partner_id || '', deal.ref_number || '', categories ? (categories.get(deal.id) || '기타') : '미분류', new Date().toISOString(), ]); // Sheets에 추가 await sheets.spreadsheets.values.append({...}); }; ``` --- ### Phase 4: Vertex AI 통합 #### 사용자 지시 **사용자**: ``` 네 실행해주세요 ``` **컨텍스트**: Vertex AI로 거래 내역을 자동 분류하는 기능 구현 요청. #### 설치 및 설정 **패키지 설치**: ```bash npm install @google-cloud/vertexai ``` **초기 설정 코드**: ```typescript import { VertexAI } from '@google-cloud/vertexai'; const PROJECT_ID = 'spatial-cargo-456805-u2'; const LOCATION = 'asia-northeast1'; const vertexAI = new VertexAI({ project: PROJECT_ID, location: LOCATION, googleAuthOptions: { keyFilename: path.resolve(GOOGLE_SERVICE_ACCOUNT_KEY_PATH), }, }); const model = vertexAI.getGenerativeModel({ model: 'gemini-1.5-flash', }); ``` #### 발생한 오류들 **오류 1: BILLING_DISABLED** ``` This API method requires billing to be enabled. ``` **사용자 대응**: ``` Billing을 활성화 하였습니다. ``` **오류 2: Model 404 Not Found** ``` Publisher Model `gemini-1.5-flash` was not found ``` **해결**: 프로젝트 ID 확인 및 모델 변경 **사용자 지시**: ``` 모델은 gemini-2.5-pro 를 사용해주세요 ``` **최종 해결**: ```typescript const model = vertexAI.getGenerativeModel({ model: 'gemini-2.5-pro', }); ``` #### 카테고리 분류 구현 **핵심 프롬프트**: ```typescript const prompt = ` 다음 거래 내역을 분석하여 가장 적절한 카테고리 하나만 선택해주세요. 거래 정보: - 날짜: ${deal.issue_date} - 타입: ${deal.type === 'income' ? '수입' : '지출'} - 금액: ${deal.amount}원 - 파트너 ID: ${deal.partner_id || '없음'} - 설명: ${deal.ref_number || '없음'} 가능한 카테고리: ${CATEGORIES.join(', ')} 다음 형식으로만 답변해주세요: 카테고리: [선택한 카테고리] `.trim(); ``` **배치 처리 로직**: ```typescript export const classifyTransactionsBatch = async (deals: any[]): Promise> => { const categories = new Map(); for (let i = 0; i < deals.length; i++) { const deal = deals[i]; const category = await classifyTransaction(deal); categories.set(deal.id, category); // API 레이트 제한 방지 if (i < deals.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } } return categories; }; ``` --- ### Phase 5: 최종 통합 및 테스트 #### 실행 프롬프트 **사용자**: ``` 네 실행해주세요 ``` **실행 명령**: ```bash npx ts-node src/index.ts ``` **실행 결과**: ``` 🚀 Freee → Vertex AI → Google Sheets 자동 동기화 시작... 📅 조회 기간: 2025-10-31 ~ 2025-11-27 1️⃣ Freee API에서 거래 내역 조회 중... ✅ 11개의 거래 내역을 가져왔습니다. 2️⃣ Vertex AI로 카테고리 자동 분류 중... Vertex AI로 11개의 거래 분류 중... 진행: 5/11 진행: 10/11 진행: 11/11 ✅ 11개의 거래 카테고리 분류 완료 3️⃣ Google Sheets에 거래 내역 추가 중... 추가할 새로운 거래가 없습니다. ✅ 동기화 완료! - 추가된 거래: 0개 - 중복 스킵: 11개 - AI 분류된 카테고리: 11개 ``` **분류 결과 예시**: - 거래 5 (891원): [수수료] - 거래 8 (3,503원): [수수료] - 거래 10 (16,680원): [식비] - 나머지: [기타] --- ### Phase 6: 블로그 문서화 #### 블로그 시리즈 작성 요청 **사용자**: ``` 좋습니다. 여기까지 해서 하나의 블로그가 완성될 것 같습니다. /var/www/tkim.planitai.co.jp/blog/20251126-googlesheet-vertexai-freee-script/ 안의 *v1.md ~ *.v10.md 를 작성할 수 있을까요? ``` **작성 내용**: - **v6.md**: 토큰 영구 저장 및 자동 갱신 완성 - **v7.md**: Vertex AI (Gemini) 설정 및 연동 - **v8.md**: Gemini를 사용한 거래 내역 자동 카테고리 분류 - **v9.md**: Google Sheets에 카테고리가 포함된 거래 내역 자동 저장 - **v10.md**: 자동화 완성 및 운영 가이드 **각 블로그 구성**: 1. 문제 정의 2. 단계별 구현 방법 3. 코드 예시 4. 테스트 및 확인 5. 다음 단계 안내 #### 일본어 기술 블로그 작성 요청 **사용자**: ``` v1.md ~ v10.md 의 내용을 절반정도로 축약해서 일본어로 바꾸어 googlesheet-vertexai-freee-script-ja.md 로 저장해주세요. 이 내용은 기술블로그로 투고할 내용입니다. ``` **제공된 피드백 (이전 블로그 기준)**: 1. ✅ 타이틀 형식: "실천 가이드|해결한 개발 수법" 2. ✅ 이니셜 섹션: CTO로서 회사의 엔지니어링 문화 소개 3. ✅ 제거할 섹션: はじめに, 動機と背景, 疑問と好奇心 4. ✅ 마지막 문단: "실제로 손을 움직이는 것으로 진정한 이해를 얻습니다" 5. ✅ 목차 추가 **최종 결과**: - 파일명: `googlesheet-vertexai-freee-script-ja.md` - 행수: 528줄 - 내용: v1~v10을 4개 Phase로 재구성, 코드 샘플 포함 **주요 섹션**: 1. イントロダクション (회사 CTO 인사) 2. プロジェクト概要 3. 技術スタック 4. 実装フェーズ (Phase 1-4) 5. 重要な技術的課題と解決策 6. システムアーキテクチャ 7. 運用と自動化 8. まとめ --- ## 주요 기술 의사결정 ### 1. OAuth2 토큰 관리 전략 **질문**: Authorization code 취득을 자동화할 수 있는가? **답변**: - OAuth2 보안 규정상 불가능 - 대신 Rotating Refresh Token을 영구 저장하는 방식으로 해결 - 한 번의 수동 인증 후 완전 자동화 달성 ### 2. Vertex AI 모델 선택 **초기**: `gemini-1.5-flash` (비용 효율적) **문제**: 404 Not Found 오류 **최종**: `gemini-2.5-pro` (사용자 지정) - 더 강력한 모델 - 분류 정확도 향상 - 비용은 높지만 안정적 ### 3. API 레이트 제한 대응 **전략**: 배치 처리 시 500ms 딜레이 추가 **코드**: ```typescript if (i < deals.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } ``` **이유**: - Vertex AI API 레이트 제한 방지 - 안정적인 연속 처리 보장 ### 4. 중복 데이터 처리 **전략**: 거래 ID 기반 중복 체크 **구현**: ```typescript const existingIds = await getExistingDealIds(sheetName); const newDeals = deals.filter(deal => !existingIds.has(deal.id.toString())); ``` **장점**: - 스크립트를 여러 번 실행해도 안전 - 데이터 무결성 보장 --- ## 트러블슈팅 로그 ### 1. invalid_grant 오류 **증상**: ``` invalid_grant: The provided authorization grant is invalid ``` **원인**: - 잘못된 BUSINESS_ID (12261708) - 만료된 리프레시 토큰 **해결**: 1. 올바른 BUSINESS_ID 확인 (12261605) 2. 새로운 authorization code로 토큰 재발급 3. `.env` 파일 업데이트 ### 2. Vertex AI BILLING_DISABLED **증상**: ``` This API method requires billing to be enabled. ``` **해결**: Google Cloud Console에서 Billing 활성화 ### 3. Model 404 Not Found **증상**: ``` Publisher Model `gemini-1.5-flash` was not found ``` **원인**: - 잘못된 프로젝트 ID - 모델 버전 문제 **해결**: 1. 서비스 계정 JSON에서 정확한 프로젝트 ID 확인 2. `gemini-2.5-pro`로 모델 변경 ### 4. TypeScript 타입 오류 **증상**: ``` Parameter 'deal' implicitly has 'any' type ``` **해결**: ```typescript deals.forEach((deal: any, index: number) => ...) ``` --- ## 성과 및 학습 ### 달성한 목표 - ✅ Freee API 완전 자동화 - ✅ AI 기반 자동 분류 - ✅ Google Sheets 자동 동기화 - ✅ 완전 무인 운용 가능 ### 기술적 학습 1. **OAuth2 Rotating Refresh Token**: 영구 저장의 중요성 2. **Vertex AI 설정**: 프로젝트 ID, 리전, 모델 선택의 중요성 3. **API 레이트 제한**: 적절한 딜레이로 안정성 확보 4. **TypeScript 비동기 처리**: Promise, async/await 패턴 활용 ### 문서화 - 10부작 블로그 시리즈 (한국어) - 통합 기술 블로그 (일본어, 528줄) - progress.md, prompts.md 작성 --- **최종 업데이트**: 2025-11-27