/** * Google Cloud Billing Service * BigQuery를 통해 실제 결제 데이터를 조회합니다. */ const { BigQuery } = require('@google-cloud/bigquery'); const path = require('path'); // 환경 변수 const KEY_PATH = process.env.GOOGLE_BILLING_KEY_PATH?.replace('./server/', path.join(__dirname, '/').replace('/server/', '/')); const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT_ID || 'grand-solstice-477822-s9'; const DATASET_ID = process.env.GOOGLE_BILLING_DATASET_ID || 'billing_export'; const BILLING_TABLE_PREFIX = 'gcp_billing_export'; let bigqueryClient = null; /** * BigQuery 클라이언트 초기화 */ const getBigQueryClient = () => { if (!bigqueryClient) { const keyPath = KEY_PATH || path.join(__dirname, 'google-billing-key.json'); bigqueryClient = new BigQuery({ keyFilename: keyPath, projectId: PROJECT_ID }); } return bigqueryClient; }; /** * 결제 테이블 이름 찾기 */ const findBillingTable = async () => { try { const bigquery = getBigQueryClient(); const dataset = bigquery.dataset(DATASET_ID); const [tables] = await dataset.getTables(); const billingTable = tables.find(t => t.id.includes(BILLING_TABLE_PREFIX) || t.id.includes('cloud_billing') ); return billingTable ? billingTable.id : null; } catch (error) { console.error('[BillingService] 테이블 조회 오류:', error.message); return null; } }; /** * 서비스별 비용 요약 (최근 N일) */ const getCostByService = async (days = 30) => { try { const tableName = await findBillingTable(); if (!tableName) { return { error: '결제 데이터 테이블이 아직 생성되지 않았습니다. 24-48시간 후 다시 시도하세요.' }; } const bigquery = getBigQueryClient(); const query = ` SELECT service.description as service_name, SUM(cost) as total_cost, SUM(CASE WHEN cost > 0 THEN cost ELSE 0 END) as actual_cost, currency, COUNT(*) as usage_count FROM \`${PROJECT_ID}.${DATASET_ID}.${tableName}\` WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL ${days} DAY) GROUP BY service.description, currency HAVING total_cost != 0 ORDER BY total_cost DESC `; const [rows] = await bigquery.query(query); return { period: `${days}일`, services: rows.map(row => ({ serviceName: row.service_name, totalCost: parseFloat(row.total_cost?.toFixed(6) || 0), actualCost: parseFloat(row.actual_cost?.toFixed(6) || 0), currency: row.currency, usageCount: row.usage_count })), totalCost: rows.reduce((sum, r) => sum + (r.actual_cost || 0), 0) }; } catch (error) { console.error('[BillingService] 서비스별 비용 조회 오류:', error.message); return { error: error.message }; } }; /** * 일별 비용 추이 (최근 N일) */ const getDailyCostTrend = async (days = 30) => { try { const tableName = await findBillingTable(); if (!tableName) { return { error: '결제 데이터 테이블이 아직 생성되지 않았습니다.' }; } const bigquery = getBigQueryClient(); const query = ` SELECT DATE(usage_start_time) as date, SUM(cost) as total_cost, currency FROM \`${PROJECT_ID}.${DATASET_ID}.${tableName}\` WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL ${days} DAY) GROUP BY DATE(usage_start_time), currency ORDER BY date DESC `; const [rows] = await bigquery.query(query); return { period: `${days}일`, daily: rows.map(row => ({ date: row.date?.value || row.date, cost: parseFloat(row.total_cost?.toFixed(6) || 0), currency: row.currency })) }; } catch (error) { console.error('[BillingService] 일별 비용 조회 오류:', error.message); return { error: error.message }; } }; /** * SKU별 상세 비용 (Gemini API 등) */ const getCostBySKU = async (days = 30, serviceFilter = null) => { try { const tableName = await findBillingTable(); if (!tableName) { return { error: '결제 데이터 테이블이 아직 생성되지 않았습니다.' }; } const bigquery = getBigQueryClient(); let whereClause = `DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL ${days} DAY)`; if (serviceFilter) { whereClause += ` AND LOWER(service.description) LIKE '%${serviceFilter.toLowerCase()}%'`; } const query = ` SELECT service.description as service_name, sku.description as sku_name, SUM(cost) as total_cost, SUM(usage.amount) as total_usage, usage.unit as usage_unit, currency FROM \`${PROJECT_ID}.${DATASET_ID}.${tableName}\` WHERE ${whereClause} GROUP BY service.description, sku.description, usage.unit, currency HAVING total_cost > 0 ORDER BY total_cost DESC LIMIT 50 `; const [rows] = await bigquery.query(query); return { period: `${days}일`, filter: serviceFilter, skus: rows.map(row => ({ serviceName: row.service_name, skuName: row.sku_name, totalCost: parseFloat(row.total_cost?.toFixed(6) || 0), totalUsage: row.total_usage, usageUnit: row.usage_unit, currency: row.currency })) }; } catch (error) { console.error('[BillingService] SKU별 비용 조회 오류:', error.message); return { error: error.message }; } }; /** * 월별 비용 요약 */ const getMonthlyCost = async (months = 6) => { try { const tableName = await findBillingTable(); if (!tableName) { return { error: '결제 데이터 테이블이 아직 생성되지 않았습니다.' }; } const bigquery = getBigQueryClient(); const query = ` SELECT FORMAT_DATE('%Y-%m', DATE(usage_start_time)) as month, service.description as service_name, SUM(cost) as total_cost, currency FROM \`${PROJECT_ID}.${DATASET_ID}.${tableName}\` WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL ${months} MONTH) GROUP BY month, service.description, currency HAVING total_cost > 0 ORDER BY month DESC, total_cost DESC `; const [rows] = await bigquery.query(query); // 월별로 그룹화 const byMonth = {}; rows.forEach(row => { if (!byMonth[row.month]) { byMonth[row.month] = { month: row.month, services: [], total: 0 }; } byMonth[row.month].services.push({ serviceName: row.service_name, cost: parseFloat(row.total_cost?.toFixed(6) || 0), currency: row.currency }); byMonth[row.month].total += row.total_cost || 0; }); return { period: `${months}개월`, monthly: Object.values(byMonth).map(m => ({ ...m, total: parseFloat(m.total.toFixed(6)) })) }; } catch (error) { console.error('[BillingService] 월별 비용 조회 오류:', error.message); return { error: error.message }; } }; /** * Gemini/Vertex AI 비용만 조회 */ const getGeminiCost = async (days = 30) => { return getCostBySKU(days, 'vertex ai'); }; /** * 전체 비용 대시보드 데이터 */ const getBillingDashboard = async (days = 30) => { try { const [byService, dailyTrend, geminiCost] = await Promise.all([ getCostByService(days), getDailyCostTrend(days), getGeminiCost(days) ]); // 총 비용 계산 const totalCost = byService.services?.reduce((sum, s) => sum + s.actualCost, 0) || 0; // KRW 환산 const usdToKrw = 1350; return { period: `${days}일`, summary: { totalCostUSD: totalCost.toFixed(4), totalCostKRW: Math.round(totalCost * usdToKrw), serviceCount: byService.services?.length || 0 }, byService: byService.services || [], dailyTrend: dailyTrend.daily || [], geminiUsage: geminiCost.skus || [], exchangeRate: { USD_KRW: usdToKrw }, dataSource: 'Google Cloud BigQuery (실제 결제 데이터)', error: byService.error || dailyTrend.error || null }; } catch (error) { console.error('[BillingService] 대시보드 데이터 조회 오류:', error.message); return { error: error.message }; } }; /** * 결제 데이터 사용 가능 여부 확인 */ const checkBillingDataAvailable = async () => { try { const tableName = await findBillingTable(); return { available: !!tableName, tableName: tableName, message: tableName ? '결제 데이터를 사용할 수 있습니다.' : '결제 데이터 테이블이 아직 생성되지 않았습니다. BigQuery Export 설정 후 24-48시간이 필요합니다.' }; } catch (error) { return { available: false, error: error.message }; } }; module.exports = { getCostByService, getDailyCostTrend, getCostBySKU, getMonthlyCost, getGeminiCost, getBillingDashboard, checkBillingDataAvailable };