From 9cf97235f2bd9352c39ab547c09a92b74afd7a89 Mon Sep 17 00:00:00 2001 From: seungwon-yu Date: Wed, 20 Aug 2025 13:08:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EA=B5=AC=EA=B8=80=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/analytics/google-analytics.js | 104 +++++++++++++++++++ src/analytics/index.js | 80 ++++++++++++++ src/components/report/TaxComparisonChart.vue | 5 + src/main.js | 4 + src/router/index.js | 6 ++ src/views/auth/SignupView.vue | 5 + src/views/goal/GoalEditView.vue | 4 + src/views/onBoarding/LoginPage.vue | 5 + src/views/report/ReportView.vue | 5 + 9 files changed, 218 insertions(+) create mode 100644 src/analytics/google-analytics.js create mode 100644 src/analytics/index.js diff --git a/src/analytics/google-analytics.js b/src/analytics/google-analytics.js new file mode 100644 index 0000000..69ae43d --- /dev/null +++ b/src/analytics/google-analytics.js @@ -0,0 +1,104 @@ +// Google Analytics 설정 및 추적 함수들 + +// Google Analytics 설정 ID +const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID || 'G-XXXXXXXXXX' + +// Google Analytics 초기화 +export const initGA = () => { + // 이미 로드되었는지 확인 + if (window.gtag) { + console.log('Google Analytics already initialized') + return + } + + // gtag 스크립트 로드 + const script = document.createElement('script') + script.async = true + script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}` + document.head.appendChild(script) + + // gtag 초기화 + window.dataLayer = window.dataLayer || [] + window.gtag = function() { + window.dataLayer.push(arguments) + } + + window.gtag('js', new Date()) + window.gtag('config', GA_MEASUREMENT_ID, { + // 개인정보 보호 설정 + anonymize_ip: true, + send_page_view: false, // 수동으로 페이지뷰 추적 + // 쿠키 만료 시간 (90일) + cookie_expires: 90 * 24 * 60 * 60 + }) + + console.log('Google Analytics initialized:', GA_MEASUREMENT_ID) +} + +// 페이지뷰 추적 +export const trackPageView = (page_title, page_location = window.location.href) => { + if (typeof window.gtag !== 'undefined') { + window.gtag('event', 'page_view', { + page_title, + page_location + }) + console.log('GA Page View:', page_title, page_location) + } +} + +// 커스텀 이벤트 추적 +export const trackEvent = (action, category = 'engagement', label = '', value = 0) => { + if (typeof window.gtag !== 'undefined') { + window.gtag('event', action, { + event_category: category, + event_label: label, + value: value + }) + console.log('GA Event:', action, category, label, value) + } +} + +// 로그인 이벤트 +export const trackLogin = (method = 'email') => { + trackEvent('login', 'auth', method) +} + +// 회원가입 이벤트 +export const trackSignup = (method = 'email') => { + trackEvent('sign_up', 'auth', method) +} + +// 목표 생성 이벤트 +export const trackGoalCreated = (goalType, amount = 0) => { + trackEvent('goal_created', 'goal', goalType, amount) +} + +// ISA 계좌 등록 이벤트 +export const trackIsaRegistration = () => { + trackEvent('isa_registration', 'account', 'isa_account') +} + +// 예적금 등록 이벤트 +export const trackDepositRegistration = () => { + trackEvent('deposit_registration', 'account', 'deposit_account') +} + +// 투자 추천 클릭 이벤트 +export const trackInvestmentRecommendation = (recommendationType) => { + trackEvent('investment_recommendation_click', 'recommendation', recommendationType) +} + +// 리포트 조회 이벤트 +export const trackReportView = (reportType) => { + trackEvent('report_view', 'report', reportType) +} + +// 에러 추적 +export const trackError = (errorMessage, errorLocation = '') => { + trackEvent('error', 'error', `${errorLocation}: ${errorMessage}`) +} + +// 사용자 행동 추적 +export const trackUserAction = (action, section, details = '') => { + trackEvent(action, 'user_interaction', `${section}_${details}`) +} \ No newline at end of file diff --git a/src/analytics/index.js b/src/analytics/index.js new file mode 100644 index 0000000..91fe01d --- /dev/null +++ b/src/analytics/index.js @@ -0,0 +1,80 @@ +// Google Analytics 4 간단 구현 + +const GA_ID = 'G-KP0NDR0HLP' + +// 초기화 +export const initGA = () => { + const script = document.createElement('script') + script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}` + script.async = true + document.head.appendChild(script) + + window.dataLayer = window.dataLayer || [] + window.gtag = function() { window.dataLayer.push(arguments) } + + gtag('js', new Date()) + gtag('config', GA_ID) +} + +// 페이지 조회 +export const trackPage = (page) => { + if (window.gtag) { + gtag('event', 'page_view', { page_title: page }) + } +} + +// 이벤트 추적 +export const trackEvent = (action, category = 'general') => { + if (window.gtag) { + gtag('event', action, { event_category: category }) + } +} + +// 금융 앱 특화 이벤트들 +export const trackGoalCreated = (goalType, amount) => { + if (window.gtag) { + gtag('event', 'goal_created', { + event_category: 'goal', + goal_type: goalType, + goal_amount: amount + }) + } +} + +export const trackAccountLinked = (accountType, bank) => { + if (window.gtag) { + gtag('event', 'account_linked', { + event_category: 'account', + account_type: accountType, + bank_name: bank + }) + } +} + +export const trackInvestmentRecommendation = (productType, amount) => { + if (window.gtag) { + gtag('event', 'investment_recommendation_view', { + event_category: 'investment', + product_type: productType, + recommended_amount: amount + }) + } +} + +export const trackReportView = (reportType) => { + if (window.gtag) { + gtag('event', 'report_viewed', { + event_category: 'report', + report_type: reportType + }) + } +} + +export const trackTaxSavingsView = (savingsAmount) => { + if (window.gtag) { + gtag('event', 'tax_savings_viewed', { + event_category: 'report', + savings_amount: savingsAmount + }) + } +} \ No newline at end of file diff --git a/src/components/report/TaxComparisonChart.vue b/src/components/report/TaxComparisonChart.vue index 24e06dd..0291ad4 100644 --- a/src/components/report/TaxComparisonChart.vue +++ b/src/components/report/TaxComparisonChart.vue @@ -13,6 +13,7 @@ import { CanvasRenderer } from "echarts/renderers"; import { BarChart, LineChart } from "echarts/charts"; import { GridComponent, TooltipComponent, LegendComponent } from "echarts/components"; import api from "@/api"; +import { trackTaxSavingsView } from "@/analytics"; use([CanvasRenderer, BarChart, LineChart, GridComponent, TooltipComponent, LegendComponent]); @@ -174,6 +175,10 @@ const fetchData = async () => { quarterlyData: [], summary: {}, }; + + // GA 세금 절약 데이터 조회 추적 + const totalSavings = taxData.value.quarterlyData?.reduce((sum, item) => sum + (item.isaTaxSaved || 0), 0) || 0; + trackTaxSavingsView(totalSavings); } catch (err) { console.error("세금 비교 데이터 가져오기 실패:", err); error.value = true; diff --git a/src/main.js b/src/main.js index 0e6c8d6..82870eb 100644 --- a/src/main.js +++ b/src/main.js @@ -4,10 +4,14 @@ import { createPinia } from "pinia"; import App from "./App.vue"; import router from "./router"; import "./index.css"; +import { initGA } from "./analytics"; const app = createApp(App); app.use(createPinia()); app.use(router); +// Google Analytics 초기화 +initGA(); + app.mount("#app"); diff --git a/src/router/index.js b/src/router/index.js index e04b6ee..e72dadc 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -12,6 +12,7 @@ import alarmRoutes from "./alarm"; import reportRoutes from "./report"; import { STORAGE_KEYS } from "@/utils/constants"; import { useAuthStore } from "@/stores/auth"; +import { trackPage } from "@/analytics"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -92,4 +93,9 @@ router.beforeEach((to, _from, next) => { next(); }); +// 페이지 뷰 추적 +router.afterEach((to) => { + trackPage(to.name || to.path); +}); + export default router; diff --git a/src/views/auth/SignupView.vue b/src/views/auth/SignupView.vue index de457bf..aac09b2 100644 --- a/src/views/auth/SignupView.vue +++ b/src/views/auth/SignupView.vue @@ -38,6 +38,7 @@ import Button from "@/components/base/Button.vue"; import goBackButton from "@/components/base/GoBackButton.vue"; import BaseTextInput from "@/components/base/BaseTextInput.vue"; import authApi from "@/api/authApi"; +import { trackEvent } from "@/analytics"; // import { useUserStore } from "@/stores/user"; @@ -73,6 +74,10 @@ const handleRegister = async () => { password: formData.password, nickname: formData.username, }); + + // GA 회원가입 이벤트 추적 + trackEvent('sign_up', 'auth'); + router.push("/agree-condition"); } catch (error) { const msg = error?.response?.data?.message || "회원 가입 실패"; diff --git a/src/views/goal/GoalEditView.vue b/src/views/goal/GoalEditView.vue index 900b732..41bae75 100644 --- a/src/views/goal/GoalEditView.vue +++ b/src/views/goal/GoalEditView.vue @@ -414,6 +414,7 @@ import DefaultLayout from "@/components/layouts/DefaultLayout.vue"; import AddRegisterModal from "./AddRegisterModal.vue"; import Api from "@/api/objectApi"; import isaApi from "@/api/isaApi"; +import { trackGoalCreated } from "@/analytics"; const route = useRoute(); const router = useRouter(); @@ -723,6 +724,9 @@ const handleCompleteGoal = async () => { const ok = res?.status === 200 || res?.status === 201 || res?.data?.status === "OK"; if (ok) { + // GA 목표 생성/수정 이벤트 추적 + trackGoalCreated(goalName.value, goalAmount.value); + localStorage.removeItem("currentGoalId"); router.push({ path: "/goal" }); } else { diff --git a/src/views/onBoarding/LoginPage.vue b/src/views/onBoarding/LoginPage.vue index c5d9186..4a19a17 100644 --- a/src/views/onBoarding/LoginPage.vue +++ b/src/views/onBoarding/LoginPage.vue @@ -50,6 +50,7 @@ import Button from "@/components/base/Button.vue"; import { useAuthStore } from "@/stores/auth"; import { ref, reactive } from "vue"; import { useRouter } from "vue-router"; +import { trackEvent } from "@/analytics"; const auth = useAuthStore(); const userData = reactive({ email: "", @@ -63,6 +64,10 @@ const login = async () => { try { isLoading.value = true; await auth.login(userData); + + // GA 로그인 이벤트 추적 + trackEvent('login', 'auth'); + //semi_user , user 따라 라우팅 const authData = JSON.parse(localStorage.getItem("auth")); const userRole = authData?.user?.role; diff --git a/src/views/report/ReportView.vue b/src/views/report/ReportView.vue index 9cdc02a..d296549 100644 --- a/src/views/report/ReportView.vue +++ b/src/views/report/ReportView.vue @@ -91,6 +91,7 @@ import IsaReport from "@/components/report/IsaReport.vue"; import { ref, onMounted } from "vue"; import api from "@/api"; import TaxSavingsSummary from "@/components/report/TaxSavingsSummary.vue"; +import { trackReportView } from "@/analytics"; const activeTab = ref("isa"); const goals = ref([]); @@ -113,9 +114,13 @@ const fetchGoals = async () => { const setActiveTab = (tab) => { activeTab.value = tab; + // GA 리포트 탭 변경 추적 + trackReportView(tab === 'isa' ? 'ISA 리포트' : '투자 리포트'); }; onMounted(() => { fetchGoals(); + // GA 리포트 페이지 접속 추적 + trackReportView('리포트 메인'); });