# v6: Stripe 데이터 조회 및 집계 ## 개요 이번 단계에서는 Stripe에서 결제 데이터를 조회하고, ChatGPT에 전달할 수 있는 형태로 가공합니다. 오늘의 매출을 분석하기 위해 필요한 모든 정보를 수집하고 구조화합니다. ## 데이터 조회 모듈 ### src/stripe/fetch-data.ts ```typescript import Stripe from 'stripe'; import { stripe } from './client.js'; import { PaymentData, SalesStats } from '../types/index.js'; /** * 특정 날짜 범위의 Payment Intent 조회 */ export async function fetchPaymentIntents( startDate: Date, endDate?: Date ): Promise { const startTimestamp = Math.floor(startDate.getTime() / 1000); const endTimestamp = endDate ? Math.floor(endDate.getTime() / 1000) : Math.floor(Date.now() / 1000); const allPayments: Stripe.PaymentIntent[] = []; let hasMore = true; let startingAfter: string | undefined = undefined; // Pagination 처리 while (hasMore) { const params: Stripe.PaymentIntentListParams = { created: { gte: startTimestamp, lte: endTimestamp, }, limit: 100, // 한 번에 최대 100개 }; if (startingAfter) { params.starting_after = startingAfter; } const paymentIntents = await stripe.paymentIntents.list(params); allPayments.push(...paymentIntents.data); hasMore = paymentIntents.has_more; if (hasMore && paymentIntents.data.length > 0) { startingAfter = paymentIntents.data[paymentIntents.data.length - 1].id; } } return allPayments; } /** * 오늘의 Payment Intent 조회 */ export async function fetchTodayPayments(): Promise { const today = new Date(); today.setHours(0, 0, 0, 0); console.log(`📅 ${today.toLocaleDateString()} 결제 데이터 조회 중...`); const payments = await fetchPaymentIntents(today); console.log(`✅ ${payments.length}건의 결제를 찾았습니다.`); return payments; } /** * Payment Intent를 간소화된 PaymentData로 변환 */ export function transformPaymentIntent(pi: Stripe.PaymentIntent): PaymentData { return { id: pi.id, amount: pi.amount, currency: pi.currency, description: pi.description || 'No description', customerEmail: pi.receipt_email || undefined, status: pi.status, createdAt: new Date(pi.created * 1000), category: pi.metadata?.category, productName: pi.metadata?.product_name, }; } /** * 결제 데이터 집계 */ export async function aggregateSalesData(): Promise { const paymentIntents = await fetchTodayPayments(); // 성공한 결제만 필터링 const successfulPayments = paymentIntents.filter( (pi) => pi.status === 'succeeded' ); // PaymentData로 변환 const payments = successfulPayments.map(transformPaymentIntent); // 집계 계산 const totalAmount = payments.reduce((sum, p) => sum + p.amount, 0); const totalCount = payments.length; const averageAmount = totalCount > 0 ? totalAmount / totalCount : 0; // 카테고리별 집계 const byCategory: Record = {}; payments.forEach((payment) => { const category = payment.category || 'unknown'; if (!byCategory[category]) { byCategory[category] = { count: 0, amount: 0 }; } byCategory[category].count++; byCategory[category].amount += payment.amount; }); // 시간대별 집계 const byHour: Record = {}; payments.forEach((payment) => { const hour = payment.createdAt.getHours(); byHour[hour] = (byHour[hour] || 0) + 1; }); const today = new Date(); const currency = payments.length > 0 ? payments[0].currency : 'usd'; return { date: today.toISOString().split('T')[0], totalAmount, totalCount, averageAmount, currency, payments, byCategory, byHour, }; } ``` ## 타입 정의 확장 ### src/types/index.ts (업데이트) ```typescript import Stripe from 'stripe'; /** * 결제 정보 */ export interface PaymentData { id: string; amount: number; currency: string; description: string; customerEmail?: string; status: string; createdAt: Date; category?: string; productName?: string; } /** * 매출 통계 */ export interface SalesStats { date: string; totalAmount: number; totalCount: number; averageAmount: number; currency: string; payments: PaymentData[]; byCategory: Record; byHour: Record; } /** * ChatGPT 분석 결과 */ export interface AnalysisResult { summary: string; insights: string[]; recommendations: string[]; rawResponse: string; } ``` ## 데이터 포맷팅 유틸리티 ### src/utils/formatter.ts ```typescript import { SalesStats, PaymentData } from '../types/index.js'; /** * 금액을 달러 형식으로 포맷 */ export function formatCurrency(amount: number, currency: string = 'usd'): string { const dollars = amount / 100; if (currency === 'usd') { return `$${dollars.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, })}`; } else if (currency === 'krw') { return `₩${Math.round(dollars * 100).toLocaleString('ko-KR')}`; } else { return `${dollars.toFixed(2)} ${currency.toUpperCase()}`; } } /** * 날짜를 읽기 쉬운 형식으로 포맷 */ export function formatDate(date: Date): string { return date.toLocaleString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } /** * 매출 통계를 텍스트로 포맷 */ export function formatSalesStats(stats: SalesStats): string { const lines: string[] = []; lines.push(`=== 매출 분석 (${stats.date}) ===\n`); // 기본 통계 lines.push('📊 기본 통계'); lines.push(`총 매출: ${formatCurrency(stats.totalAmount, stats.currency)}`); lines.push(`결제 건수: ${stats.totalCount}건`); if (stats.totalCount > 0) { lines.push( `평균 결제 금액: ${formatCurrency(stats.averageAmount, stats.currency)}` ); } // 카테고리별 통계 if (Object.keys(stats.byCategory).length > 0) { lines.push('\n📦 카테고리별 매출'); const sortedCategories = Object.entries(stats.byCategory).sort( (a, b) => b[1].amount - a[1].amount ); sortedCategories.forEach(([category, data]) => { const percentage = ((data.amount / stats.totalAmount) * 100).toFixed(1); lines.push( ` ${category}: ${formatCurrency(data.amount, stats.currency)} (${ data.count }건, ${percentage}%)` ); }); } // 시간대별 통계 if (Object.keys(stats.byHour).length > 0) { lines.push('\n⏰ 시간대별 결제 건수'); const sortedHours = Object.entries(stats.byHour) .map(([hour, count]) => ({ hour: parseInt(hour), count })) .sort((a, b) => a.hour - b.hour); sortedHours.forEach(({ hour, count }) => { const bar = '█'.repeat(Math.ceil(count / 2)); lines.push(` ${hour.toString().padStart(2, '0')}:00 | ${bar} ${count}건`); }); } // Top 5 결제 if (stats.payments.length > 0) { lines.push('\n💰 최고 결제 Top 5'); const topPayments = [...stats.payments] .sort((a, b) => b.amount - a.amount) .slice(0, 5); topPayments.forEach((payment, index) => { lines.push( ` ${index + 1}. ${formatCurrency(payment.amount, payment.currency)} - ${ payment.productName || payment.description }` ); }); } return lines.join('\n'); } /** * 매출 통계를 ChatGPT에 전달할 형식으로 변환 */ export function formatForChatGPT(stats: SalesStats): string { const lines: string[] = []; lines.push(`날짜: ${stats.date}`); lines.push(`총 매출: ${formatCurrency(stats.totalAmount, stats.currency)}`); lines.push(`결제 건수: ${stats.totalCount}건`); lines.push( `평균 결제 금액: ${formatCurrency(stats.averageAmount, stats.currency)}` ); // 카테고리별 lines.push('\n카테고리별 매출:'); Object.entries(stats.byCategory) .sort((a, b) => b[1].amount - a[1].amount) .forEach(([category, data]) => { lines.push( `- ${category}: ${formatCurrency(data.amount, stats.currency)} (${ data.count }건)` ); }); // 시간대별 const peakHours = Object.entries(stats.byHour) .sort((a, b) => b[1] - a[1]) .slice(0, 3); lines.push('\n결제가 많은 시간대:'); peakHours.forEach(([hour, count]) => { lines.push(`- ${hour}시: ${count}건`); }); // 개별 결제 (요약) lines.push(`\n개별 결제 내역 (총 ${stats.payments.length}건):`); stats.payments.slice(0, 10).forEach((payment, index) => { lines.push( `${index + 1}. ${formatCurrency(payment.amount, payment.currency)} - ${ payment.productName || payment.description } (${formatDate(payment.createdAt)})` ); }); if (stats.payments.length > 10) { lines.push(`... 외 ${stats.payments.length - 10}건`); } return lines.join('\n'); } ``` ## 데이터 조회 테스트 스크립트 ### src/test-fetch.ts ```typescript import { aggregateSalesData } from './stripe/fetch-data.js'; import { formatSalesStats } from './utils/formatter.js'; async function main() { console.log('📊 Stripe 데이터 조회 및 집계 테스트\n'); try { const stats = await aggregateSalesData(); // 포맷팅된 통계 출력 console.log(formatSalesStats(stats)); // 원시 데이터도 확인 console.log('\n📋 원시 데이터:'); console.log(JSON.stringify(stats, null, 2)); console.log('\n✨ 테스트 완료!'); } catch (error) { console.error('❌ 에러 발생:', error); process.exit(1); } } main(); ``` ### package.json에 스크립트 추가 ```json { "scripts": { "fetch": "tsx src/test-fetch.ts", "stats": "tsx src/test-fetch.ts" } } ``` ## 실행 및 테스트 ### 1. 먼저 테스트 결제 생성 ```bash npm run generate 10 ``` ### 2. 데이터 조회 및 집계 ```bash npm run fetch ``` ### 예상 출력 ``` 📊 Stripe 데이터 조회 및 집계 테스트 📅 2025-11-28 결제 데이터 조회 중... ✅ 10건의 결제를 찾았습니다. === 매출 분석 (2025-11-28) === 📊 기본 통계 총 매출: $5,432.00 결제 건수: 10건 평균 결제 금액: $543.20 📦 카테고리별 매출 electronics: $2,450.00 (4건, 45.1%) books: $1,680.00 (3건, 30.9%) clothing: $902.00 (2건, 16.6%) home: $400.00 (1건, 7.4%) ⏰ 시간대별 결제 건수 09:00 | █ 1건 11:00 | ██ 2건 14:00 | ███ 3건 16:00 | ██ 2건 19:00 | █ 1건 21:00 | █ 1건 💰 최고 결제 Top 5 1. $850.00 - Smart Watch 2. $720.00 - Mechanical Keyboard 3. $560.00 - The Pragmatic Programmer 4. $480.00 - Wireless Headphones 5. $420.00 - Clean Code ✨ 테스트 완료! ``` ## 고급 집계 기능 ### 시간대별 트렌드 분석 ```typescript /** * 가장 활발한 시간대 찾기 */ export function findPeakHours(stats: SalesStats): number[] { const hourCounts = Object.entries(stats.byHour) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([hour]) => parseInt(hour)); return hourCounts; } /** * 가장 조용한 시간대 찾기 */ export function findQuietHours(stats: SalesStats): number[] { const allHours = Array.from({ length: 24 }, (_, i) => i); const activeHours = Object.keys(stats.byHour).map(Number); const quietHours = allHours.filter((h) => !activeHours.includes(h)); return quietHours; } ``` ### 카테고리 인사이트 ```typescript /** * 가장 인기 있는 카테고리 */ export function getTopCategory(stats: SalesStats): { category: string; amount: number; count: number; } { const entries = Object.entries(stats.byCategory); if (entries.length === 0) { return { category: 'none', amount: 0, count: 0 }; } const [category, data] = entries.sort((a, b) => b[1].amount - a[1].amount)[0]; return { category, ...data }; } /** * 평균 주문 금액이 가장 높은 카테고리 */ export function getHighestAOVCategory(stats: SalesStats): { category: string; aov: number; } { const entries = Object.entries(stats.byCategory); if (entries.length === 0) { return { category: 'none', aov: 0 }; } const [category, data] = entries .map(([cat, data]) => ({ category: cat, aov: data.amount / data.count, data, })) .sort((a, b) => b.aov - a.aov)[0]; return { category, aov: data.aov }; } ``` ### src/utils/insights.ts ```typescript import { SalesStats } from '../types/index.js'; import { formatCurrency } from './formatter.js'; /** * 자동 인사이트 생성 */ export function generateInsights(stats: SalesStats): string[] { const insights: string[] = []; // 1. 거래 규모 분석 if (stats.totalCount === 0) { insights.push('오늘은 아직 거래가 없습니다.'); return insights; } insights.push( `오늘 총 ${stats.totalCount}건의 거래가 발생하여 ${formatCurrency( stats.totalAmount, stats.currency )}의 매출을 기록했습니다.` ); // 2. 평균 주문 금액 const aov = stats.averageAmount; if (aov > 50000) { // $500 insights.push( `평균 주문 금액이 ${formatCurrency( aov, stats.currency )}로 높은 편입니다. 고가 상품이 잘 팔리고 있습니다.` ); } else if (aov < 10000) { // $100 insights.push( `평균 주문 금액이 ${formatCurrency( aov, stats.currency )}로 낮은 편입니다. 저가 상품 위주로 판매되고 있습니다.` ); } // 3. 카테고리 분석 const categories = Object.entries(stats.byCategory); if (categories.length > 0) { const [topCategory, topData] = categories.sort( (a, b) => b[1].amount - a[1].amount )[0]; const percentage = ((topData.amount / stats.totalAmount) * 100).toFixed(1); insights.push( `${topCategory} 카테고리가 전체 매출의 ${percentage}%를 차지하며 가장 높은 비중을 보입니다.` ); } // 4. 시간대 분석 const hours = Object.entries(stats.byHour); if (hours.length > 0) { const [peakHour] = hours.sort((a, b) => b[1] - a[1])[0]; insights.push( `${peakHour}시에 가장 많은 거래가 발생했습니다 (${stats.byHour[Number(peakHour)]}건).` ); } return insights; } ``` ## 데이터 캐싱 (선택사항) 대량의 데이터를 반복해서 조회하는 경우 캐싱을 고려할 수 있습니다: ```typescript // 간단한 메모리 캐시 const cache = new Map(); export async function getCachedSalesData( maxAge: number = 5 * 60 * 1000 // 5분 ): Promise { const today = new Date().toISOString().split('T')[0]; const cached = cache.get(today); if (cached && Date.now() - cached.timestamp < maxAge) { console.log('📦 캐시된 데이터 사용'); return cached.data; } console.log('🔄 새로운 데이터 조회'); const data = await aggregateSalesData(); cache.set(today, { data, timestamp: Date.now() }); return data; } ``` ## 체크리스트 v6를 완료하기 전에 다음을 확인하세요: - [ ] `src/stripe/fetch-data.ts` 작성 - [ ] `src/types/index.ts` 업데이트 - [ ] `src/utils/formatter.ts` 작성 - [ ] `src/utils/insights.ts` 작성 - [ ] `src/test-fetch.ts` 작성 - [ ] package.json 스크립트 추가 - [ ] 테스트 결제 생성 (`npm run generate`) - [ ] 데이터 조회 테스트 (`npm run fetch`) - [ ] 통계가 정확하게 집계되는지 확인 - [ ] 포맷팅이 제대로 작동하는지 확인 ## 트러블슈팅 ### 1. "No payments found" 출력 **원인**: 오늘 생성된 결제가 없음 **해결**: `npm run generate`로 테스트 결제 먼저 생성 ### 2. Pagination 문제 **원인**: 100건 이상의 결제가 있을 때 일부만 조회됨 **해결**: 코드에 이미 pagination 처리가 되어 있음. `hasMore` 확인 ### 3. 시간대 오류 **원인**: 타임존 차이 **해결**: UTC 기준으로 조회하거나, 로컬 시간대 설정 ## 다음 단계 v7에서는 OpenAI ChatGPT API를 연동합니다. 준비할 것: - OpenAI API 키 - 매출 데이터가 제대로 집계되는지 확인 --- **작성일**: 2025-11-28 **상태**: ✅ 완료 **다음**: v7 - ChatGPT API 연동