gtag 추가

This commit is contained in:
2025-07-03 08:36:26 +09:00
parent 01aa5dfe66
commit 73b6439702
23 changed files with 414 additions and 17 deletions
+26
View File
@@ -33,6 +33,8 @@
"vee-validate": "^4.12.5", "vee-validate": "^4.12.5",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-chartjs": "^5.3.2", "vue-chartjs": "^5.3.2",
"vue-gtag": "^3.5.1",
"vue-gtag-next": "^1.14.0",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
@@ -3748,6 +3750,30 @@
} }
} }
}, },
"node_modules/vue-gtag": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/vue-gtag/-/vue-gtag-3.5.1.tgz",
"integrity": "sha512-fyXQeIIc2cMibgggCbIZKfP0NvVbMupi4yl7CIwnD2feL9MGVVjzN/3nb5mX9AmZhIrkYri9dmBX6jY0lRCKDg==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"peerDependenciesMeta": {
"vue-router": {
"optional": true
}
}
},
"node_modules/vue-gtag-next": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/vue-gtag-next/-/vue-gtag-next-1.14.0.tgz",
"integrity": "sha512-iJl+cOG2GU5NuxqzSSIpt03WVOvZqyKB9TOy7d55KiuvRklcnb2nlqxW5B/a3/sbIt7fla+XEkRyMCcoz0zAHw==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.11"
}
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
+2
View File
@@ -35,6 +35,8 @@
"vee-validate": "^4.12.5", "vee-validate": "^4.12.5",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-chartjs": "^5.3.2", "vue-chartjs": "^5.3.2",
"vue-gtag": "^3.5.1",
"vue-gtag-next": "^1.14.0",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
+31 -4
View File
@@ -191,8 +191,29 @@
} }
</style> </style>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-PPDRKT7Q');</script>
<!-- End Google Tag Manager -->
<script>
// 페이지 진입 시 GA 페이지뷰 이벤트 전송
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'page_view',
page_title: document.title,
page_path: window.location.pathname,
page_location: window.location.href
});
</script>
</head> </head>
<body> <body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PPDRKT7Q"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<div class="container"> <div class="container">
<h1>볼링공 속도 계산기</h1> <h1>볼링공 속도 계산기</h1>
@@ -206,8 +227,14 @@
<p>볼링공이 파울라인을 지날 때 '시작'을 누르고, 헤드핀에 도달할 때 '정지'를 누르세요.</p> <p>볼링공이 파울라인을 지날 때 '시작'을 누르고, 헤드핀에 도달할 때 '정지'를 누르세요.</p>
<div class="stopwatch-display" id="stopwatch-display">00:00.000</div> <div class="stopwatch-display" id="stopwatch-display">00:00.000</div>
<div class="stopwatch-container"> <div class="stopwatch-container">
<button class="toggle-btn" id="toggle-btn">시작</button> <button class="toggle-btn" id="toggle-btn"
<button class="reset-btn" id="reset-btn">초기화</button> onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'스톱워치'});">
시작
</button> <!-- [GA] 시작 버튼 클릭 추적 -->
<button class="reset-btn" id="reset-btn"
onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'초기화'});">
초기화
</button> <!-- [GA] 초기화 버튼 클릭 추적 -->
</div> </div>
</div> </div>
@@ -229,8 +256,8 @@
</div> </div>
<div class="buttons"> <div class="buttons">
<button onclick="calculateSpeed()">계산하기</button> <button onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'계산하기'}); calculateSpeed()">계산하기</button> <!-- [GA] 계산하기 버튼 클릭 추적 -->
<button onclick="resetValues()">초기화</button> <button onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'계산초기화'}); resetValues()">초기화</button> <!-- [GA] 계산초기화 버튼 클릭 추적 -->
</div> </div>
<div class="results" id="results"> <div class="results" id="results">
+21 -1
View File
@@ -7,7 +7,27 @@
</template> </template>
<script setup> <script setup>
// 별도 로직 없음 import { useGtag } from 'vue-gtag-next'
// Google Analytics(GA) 추적용 gtag 인스턴스 사용
const gtag = useGtag()
// 컴포넌트 마운트 시 페이지뷰 이벤트 발생
// (퍼블릭 레이아웃 진입 시 추적)
gtag.pageview({
page_title: '퍼블릭 레이아웃',
page_path: window.location.pathname,
page_location: window.location.href
})
// 주요 버튼 클릭 등 액션 추적용 함수 예시
const trackPublicAction = (actionName) => {
gtag.event('button_click', {
event_category: '퍼블릭',
event_label: actionName,
value: 1
})
}
</script> </script>
<style scoped> <style scoped>
+26 -1
View File
@@ -5,8 +5,33 @@
</template> </template>
<script> <script>
import { useGtag } from 'vue-gtag-next'
export default { export default {
name: 'ClubLayout' name: 'ClubLayout',
setup() {
const gtag = useGtag()
// 컴포넌트 마운트 시 페이지뷰 이벤트 발생
gtag.pageview({
page_title: '클럽 레이아웃',
page_path: window.location.pathname,
page_location: window.location.href
})
// 이벤트 추적 메서드 예시
const trackButtonClick = (buttonName) => {
gtag.event('button_click', {
'event_category': '클럽',
'event_label': buttonName,
'value': 1
})
}
return {
trackButtonClick
}
}
} }
</script> </script>
+13
View File
@@ -4,6 +4,9 @@ import App from './App.vue'
import router from './router' import router from './router'
import './assets/base.css' import './assets/base.css'
// Google Tag Manager 설정
import VueGtag from 'vue-gtag-next'
// dayjs configuration // dayjs configuration
import dayjs from 'dayjs' import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone'
@@ -79,6 +82,16 @@ app.use(ToastService)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
// Google Analytics(GA) vue-gtag-next 플러그인 등록
app.use(VueGtag, {
property: {
id: 'GTM-PPDRKT7Q'
},
appName: '볼링매니저',
pageTrackerScreenviewEnabled: true
}, router)
// GTM-PPDRKT7Q: 구글 태그매니저(GA4) ID
// Register custom components // Register custom components
app.component('AppHeader', AppHeader); app.component('AppHeader', AppHeader);
@@ -45,6 +45,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import adminService from '@/services/adminService'; import adminService from '@/services/adminService';
@@ -58,7 +59,14 @@ const currentPage = ref(1);
const rowsPerPage = ref(20); const rowsPerPage = ref(20);
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '활동로그관리',
page_path: window.location.pathname,
page_location: window.location.href
});
await fetchLogs(); await fetchLogs();
}); });
@@ -248,8 +248,10 @@ import { ref, reactive, onMounted } from 'vue';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import adminService from '@/services/adminService'; import adminService from '@/services/adminService';
import { useGtag } from 'vue-gtag-next';
const toast = useToast(); const toast = useToast();
const gtag = useGtag();
// //
const loading = ref(false); const loading = ref(false);
@@ -319,7 +321,14 @@ const member = reactive({
const canChangeOwner = ref(true); const canChangeOwner = ref(true);
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '클럽관리',
page_path: window.location.pathname,
page_location: window.location.href
});
await fetchClubs(); await fetchClubs();
}); });
+8
View File
@@ -129,6 +129,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -206,7 +207,14 @@ const chartOptions = {
}; };
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '관리자대시보드',
page_path: window.location.pathname,
page_location: window.location.href
});
try { try {
loading.value = true; loading.value = true;
await loadDashboardData(); await loadDashboardData();
@@ -202,12 +202,14 @@
<script setup> <script setup>
import { getStatusLabel, getStatusSeverity, getSubscriptionStatusLabel, getSubscriptionStatusSeverity, SUBSCRIPTION_STATUS_OPTIONS, SUBSCRIPTION_STATUS_LABELS } from '@/utils/enumMappings'; import { getStatusLabel, getStatusSeverity, getSubscriptionStatusLabel, getSubscriptionStatusSeverity, SUBSCRIPTION_STATUS_OPTIONS, SUBSCRIPTION_STATUS_LABELS } from '@/utils/enumMappings';
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed, watch } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import axios from 'axios'; import axios from 'axios';
import adminService from '@/services/adminService'; import adminService from '@/services/adminService';
const toast = useToast(); const toast = useToast();
const gtag = useGtag();
const API_URL = 'http://localhost:3030/api'; const API_URL = 'http://localhost:3030/api';
// //
@@ -272,7 +274,14 @@ const fetchPlanSubscriptions = async (planId) => {
}; };
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '구독플랜관리',
page_path: window.location.pathname,
page_location: window.location.href
});
await fetchPlans(); await fetchPlans();
await fetchFeatures(); await fetchFeatures();
await fetchClubs(); await fetchClubs();
@@ -125,6 +125,7 @@
<script setup> <script setup>
import { getStatusLabel, getRoleName, getRoleSeverity, getStatusSeverity, STATUS_OPTIONS, ROLE_OPTIONS } from '@/utils/enumMappings'; import { getStatusLabel, getRoleName, getRoleSeverity, getStatusSeverity, STATUS_OPTIONS, ROLE_OPTIONS } from '@/utils/enumMappings';
import { ref, onMounted, reactive, computed } from 'vue'; import { ref, onMounted, reactive, computed } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import adminService from '@/services/adminService'; import adminService from '@/services/adminService';
@@ -153,7 +154,14 @@ const dialogTitle = computed(() => {
}); });
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '사용자관리',
page_path: window.location.pathname,
page_location: window.location.href
});
await fetchUsers(); await fetchUsers();
}); });
+44 -3
View File
@@ -22,7 +22,7 @@
</div> </div>
<Button type="submit" label="로그인" class="w-full" :loading="loading" /> <Button type="submit" label="로그인" class="w-full" :loading="loading" />
<div class="register-link"> <div class="register-link">
<p>계정이 없으신가요? <a href="#" @click.prevent="showRegisterForm = true">회원가입</a></p> <p>계정이 없으신가요? <a href="#" @click.prevent="showRegisterForm = true; gtag.event('show_register_form', { 'event_category': '인증', 'event_label': '회원가입 폼 표시' });">회원가입</a></p>
</div> </div>
</form> </form>
@@ -65,7 +65,7 @@
<Button type="submit" label="회원가입" class="w-full" :loading="loading" /> <Button type="submit" label="회원가입" class="w-full" :loading="loading" />
<div class="login-link"> <div class="login-link">
<p>이미 계정이 있으신가요? <a href="#" @click.prevent="showRegisterForm = false">로그인</a></p> <p>이미 계정이 있으신가요? <a href="#" @click.prevent="showRegisterForm = false; gtag.event('show_login_form', { 'event_category': '인증', 'event_label': '로그인 폼 표시' });">로그인</a></p>
</div> </div>
</form> </form>
</div> </div>
@@ -74,14 +74,16 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, inject } from 'vue'; import { ref, computed, inject, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { useGtag } from 'vue-gtag-next';
import authService from '@/services/authService'; import authService from '@/services/authService';
const user = inject('user'); const user = inject('user');
const router = useRouter(); const router = useRouter();
const toast = useToast(); const toast = useToast();
const gtag = useGtag();
const username = ref(''); const username = ref('');
const password = ref(''); const password = ref('');
@@ -177,6 +179,13 @@ const validateRegisterForm = () => {
const handleLogin = async () => { const handleLogin = async () => {
if (!validateForm()) return; if (!validateForm()) return;
// Google Analytics -
gtag.event('login_attempt', {
'event_category': '인증',
'event_label': '로그인 시도',
'value': 1
});
loading.value = true; loading.value = true;
try { try {
@@ -198,6 +207,12 @@ const handleLogin = async () => {
// //
toast.add({ severity: 'success', summary: '성공', detail: '로그인에 성공했습니다.', life: 3000 }); toast.add({ severity: 'success', summary: '성공', detail: '로그인에 성공했습니다.', life: 3000 });
// Google Analytics -
gtag.event('login', {
'method': 'id_password',
'user_role': userData.role
});
// //
window.dispatchEvent(new CustomEvent('user-logged-in')); window.dispatchEvent(new CustomEvent('user-logged-in'));
@@ -210,6 +225,13 @@ const handleLogin = async () => {
} catch (error) { } catch (error) {
console.error('로그인 오류:', error); console.error('로그인 오류:', error);
// Google Analytics -
gtag.event('login_failed', {
'event_category': '인증',
'event_label': error.response?.data?.message || '인증 실패',
'value': 1
});
// //
const errorMessage = error.response?.data?.message || '로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.'; const errorMessage = error.response?.data?.message || '로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.';
toast.add({ severity: 'error', summary: '오류', detail: errorMessage, life: 3000 }); toast.add({ severity: 'error', summary: '오류', detail: errorMessage, life: 3000 });
@@ -222,6 +244,13 @@ const handleLogin = async () => {
const handleRegister = async () => { const handleRegister = async () => {
if (!validateRegisterForm()) return; if (!validateRegisterForm()) return;
// Google Analytics -
gtag.event('sign_up_attempt', {
'event_category': '인증',
'event_label': '회원가입 시도',
'value': 1
});
loading.value = true; loading.value = true;
try { try {
@@ -235,6 +264,11 @@ const handleRegister = async () => {
// //
toast.add({ severity: 'success', summary: '성공', detail: '회원가입에 성공했습니다. 로그인해주세요.', life: 3000 }); toast.add({ severity: 'success', summary: '성공', detail: '회원가입에 성공했습니다. 로그인해주세요.', life: 3000 });
// Google Analytics -
gtag.event('sign_up', {
'method': 'form'
});
// //
showRegisterForm.value = false; showRegisterForm.value = false;
@@ -251,6 +285,13 @@ const handleRegister = async () => {
} catch (error) { } catch (error) {
console.error('회원가입 오류:', error); console.error('회원가입 오류:', error);
// Google Analytics -
gtag.event('sign_up_failed', {
'event_category': '인증',
'event_label': error.response?.data?.message || '회원가입 실패',
'value': 1
});
// //
const errorMessage = error.response?.data?.message || '회원가입에 실패했습니다. 다시 시도해주세요.'; const errorMessage = error.response?.data?.message || '회원가입에 실패했습니다. 다시 시도해주세요.';
toast.add({ severity: 'error', summary: '오류', detail: errorMessage, life: 3000 }); toast.add({ severity: 'error', summary: '오류', detail: errorMessage, life: 3000 });
+8
View File
@@ -83,6 +83,7 @@
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { required, email, minLength, sameAs, helpers } from '@vuelidate/validators'; import { required, email, minLength, sameAs, helpers } from '@vuelidate/validators';
@@ -134,7 +135,14 @@ const rules = computed(() => ({
const v$ = useVuelidate(rules, form); const v$ = useVuelidate(rules, form);
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '프로필',
page_path: window.location.pathname,
page_location: window.location.href
});
await fetchUserProfile(); await fetchUserProfile();
}); });
+14
View File
@@ -35,6 +35,7 @@
<script setup> <script setup>
import { AVERAGE_CALCULATION_PERIOD_OPTIONS } from '@/utils/enumMappings'; import { AVERAGE_CALCULATION_PERIOD_OPTIONS } from '@/utils/enumMappings';
import { ref, computed, onMounted, inject } from 'vue'; import { ref, computed, onMounted, inject } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import clubService from '@/services/clubService'; import clubService from '@/services/clubService';
@@ -55,7 +56,14 @@ const loading = ref(false);
const clubNameError = ref(''); const clubNameError = ref('');
const clubDescriptionError = ref(''); const clubDescriptionError = ref('');
// Google Analytics
onMounted(() => { onMounted(() => {
const gtag = useGtag();
gtag.pageview({
page_title: '클럽생성',
page_path: window.location.pathname,
page_location: window.location.href
});
authStore.loadUserInfo(); authStore.loadUserInfo();
}); });
@@ -81,7 +89,13 @@ const validateForm = () => {
return isValid; return isValid;
}; };
// // GA
const handleCreateClub = async () => { const handleCreateClub = async () => {
const gtag = useGtag();
gtag.event('create_club_attempt', {
event_category: '클럽생성',
event_label: '클럽 생성 시도'
});
if (!validateForm()) return; if (!validateForm()) return;
if (!authStore.user) { if (!authStore.user) {
+7
View File
@@ -130,6 +130,7 @@
import { AVERAGE_CALCULATION_PERIOD_OPTIONS, getMemberTypeLabel } from '@/utils/enumMappings'; import { AVERAGE_CALCULATION_PERIOD_OPTIONS, getMemberTypeLabel } from '@/utils/enumMappings';
import { ref, onMounted, computed, inject } from 'vue'; import { ref, onMounted, computed, inject } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { useClubStore } from '@/stores/clubStore'; import { useClubStore } from '@/stores/clubStore';
@@ -309,7 +310,13 @@ const getFeatureIcon = (featureType) => {
}; };
// //
// GA
const saveClubInfo = async () => { const saveClubInfo = async () => {
const gtag = useGtag();
gtag.event('save_club_info', {
event_category: '클럽설정',
event_label: '클럽 정보 저장'
});
try { try {
if (!clubId) { if (!clubId) {
router.push('/club'); router.push('/club');
+15
View File
@@ -146,9 +146,11 @@ import { useRouter } from 'vue-router';
import apiClient from '@/services/api'; import apiClient from '@/services/api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { useGtag } from 'vue-gtag-next';
const router = useRouter(); const router = useRouter();
const toast = useToast(); const toast = useToast();
const gtag = useGtag();
// //
const loading = ref(false); const loading = ref(false);
@@ -260,6 +262,12 @@ const loadDashboardData = async () => {
// //
const navigateTo = (path) => { const navigateTo = (path) => {
// Google Analytics
gtag.event('navigation', {
'event_category': '대시보드',
'event_label': path,
'value': 1
});
router.push(path); router.push(path);
}; };
@@ -267,6 +275,13 @@ const navigateTo = (path) => {
onMounted(async () => { onMounted(async () => {
clubId.value = localStorage.getItem('clubId') || null; clubId.value = localStorage.getItem('clubId') || null;
await loadDashboardData(); await loadDashboardData();
// Google Analytics
gtag.pageview({
page_title: '클럽 대시보드',
page_path: window.location.pathname,
page_location: window.location.href
});
}); });
</script> </script>
+28
View File
@@ -29,6 +29,7 @@
<script setup> <script setup>
// getEventTypeLabel import . eventUtils.js getEventTypeLabel . // getEventTypeLabel import . eventUtils.js getEventTypeLabel .
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import FullCalendar from '@fullcalendar/vue3'; import FullCalendar from '@fullcalendar/vue3';
@@ -56,7 +57,14 @@ const loading = ref(false);
const clubId = ref(localStorage.getItem('clubId') || null); const clubId = ref(localStorage.getItem('clubId') || null);
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '이벤트캘린더',
page_path: window.location.pathname,
page_location: window.location.href
});
await fetchEvents(); await fetchEvents();
}); });
@@ -107,13 +115,27 @@ const getEventColor = (eventType) => {
}; };
// //
// GA
const handleEventClick = (info) => { const handleEventClick = (info) => {
const gtag = useGtag();
gtag.event('calendar_event_click', {
event_category: '이벤트캘린더',
event_label: '캘린더 이벤트 클릭',
value: info.event.id
});
const eventId = info.event.id; const eventId = info.event.id;
viewEventDetails(eventId); viewEventDetails(eventId);
}; };
// //
// GA
const viewEventDetails = async (eventId) => { const viewEventDetails = async (eventId) => {
const gtag = useGtag();
gtag.event('view_event_details', {
event_category: '이벤트캘린더',
event_label: '이벤트 상세보기',
value: eventId
});
try { try {
selectedEvent.value = await eventService.getEventById(eventId); selectedEvent.value = await eventService.getEventById(eventId);
eventDetailsDialog.value = true; eventDetailsDialog.value = true;
@@ -140,7 +162,13 @@ const removeParticipant = async (participant) => {
}; };
// //
// GA
const navigateToEventManagement = () => { const navigateToEventManagement = () => {
const gtag = useGtag();
gtag.event('go_to_event_management', {
event_category: '이벤트캘린더',
event_label: '이벤트관리 이동'
});
router.push('/club/events'); router.push('/club/events');
}; };
+48 -2
View File
@@ -122,6 +122,7 @@
<script setup> <script setup>
import { getEventTypeLabel, getEventStatusLabel, getStatusSeverity } from '@/utils/enumMappings'; import { getEventTypeLabel, getEventStatusLabel, getStatusSeverity } from '@/utils/enumMappings';
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useClubStore } from '@/stores/clubStore'; import { useClubStore } from '@/stores/clubStore';
@@ -203,7 +204,14 @@ const fetchClubMembers = async () => {
}; };
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '이벤트관리',
page_path: window.location.pathname,
page_location: window.location.href
});
if (!clubId.value) { if (!clubId.value) {
toast.add({ toast.add({
severity: 'error', severity: 'error',
@@ -265,7 +273,14 @@ const editEvent = (eventData) => {
}; };
// //
// GA
const viewEventDetails = async (eventData) => { const viewEventDetails = async (eventData) => {
const gtag = useGtag();
gtag.event('view_event_details', {
event_category: '이벤트관리',
event_label: '이벤트 상세보기',
value: eventData.id
});
try { try {
const response = await eventService.getEventById(eventData.id); const response = await eventService.getEventById(eventData.id);
event.value = response; event.value = response;
@@ -302,9 +317,16 @@ const confirmDeleteEvent = (eventData) => {
}; };
// //
// (/) GA
const saveEvent = async (eventData) => { const saveEvent = async (eventData) => {
const gtag = useGtag();
try { try {
if (eventData.id) { if (eventData.id) {
gtag.event('edit_event', {
event_category: '이벤트관리',
event_label: '이벤트 수정',
value: eventData.id
});
// //
const response = await eventService.updateEvent({ const response = await eventService.updateEvent({
...eventData, ...eventData,
@@ -312,6 +334,10 @@ const saveEvent = async (eventData) => {
}); });
await fetchEvents(); // await fetchEvents(); //
} else { } else {
gtag.event('create_event', {
event_category: '이벤트관리',
event_label: '이벤트 생성'
});
// //
const response = await eventService.createEvent({ const response = await eventService.createEvent({
...eventData, ...eventData,
@@ -327,6 +353,10 @@ const saveEvent = async (eventData) => {
}); });
hideDialog(); hideDialog();
} catch (error) { } catch (error) {
gtag.event('event_save_fail', {
event_category: '이벤트관리',
event_label: eventData.id ? '이벤트 수정 실패' : '이벤트 생성 실패'
});
toast.add({ toast.add({
severity: 'error', severity: 'error',
summary: '오류', summary: '오류',
@@ -337,17 +367,22 @@ const saveEvent = async (eventData) => {
}; };
// //
// GA
const deleteEvent = async () => { const deleteEvent = async () => {
const gtag = useGtag();
try { try {
await eventService.deleteEvent(clubId.value, event.value.id); await eventService.deleteEvent(clubId.value, event.value.id);
gtag.event('delete_event', {
event_category: '이벤트관리',
event_label: '이벤트 삭제',
value: event.value.id
});
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: '성공', summary: '성공',
detail: '이벤트가 삭제되었습니다.', detail: '이벤트가 삭제되었습니다.',
life: 3000 life: 3000
}); });
deleteEventDialog.value = false; deleteEventDialog.value = false;
const index = events.value.findIndex(e => e.id === event.value.id); const index = events.value.findIndex(e => e.id === event.value.id);
if (index > -1) { if (index > -1) {
@@ -355,6 +390,11 @@ const deleteEvent = async () => {
} }
event.value = {}; event.value = {};
} catch (error) { } catch (error) {
gtag.event('delete_event_fail', {
event_category: '이벤트관리',
event_label: '이벤트 삭제 실패',
value: event.value.id
});
toast.add({ toast.add({
severity: 'error', severity: 'error',
summary: '오류', summary: '오류',
@@ -398,7 +438,13 @@ const goToScoreEntry = () => {
}; };
// //
// GA
const navigateToCalendar = () => { const navigateToCalendar = () => {
const gtag = useGtag();
gtag.event('go_to_calendar', {
event_category: '이벤트관리',
event_label: '캘린더 이동'
});
const url = `/club/calendar?clubId=${clubId.value}`; const url = `/club/calendar?clubId=${clubId.value}`;
window.location.href = url; window.location.href = url;
}; };
+25 -2
View File
@@ -190,6 +190,7 @@
</template> </template>
<script setup> <script setup>
import { useGtag } from 'vue-gtag-next';
import { ref, reactive, computed, onMounted, watch } from 'vue'; import { ref, reactive, computed, onMounted, watch } from 'vue';
import clubService from '@/services/clubService'; import clubService from '@/services/clubService';
import { useClubStore } from '@/stores/clubStore'; import { useClubStore } from '@/stores/clubStore';
@@ -368,6 +369,7 @@ const loadMembers = async () => {
// //
const openNewMemberDialog = () => { const openNewMemberDialog = () => {
dialogMode.value = 'new';
member.value = { member.value = {
name: '', name: '',
gender: null, gender: null,
@@ -376,7 +378,6 @@ const openNewMemberDialog = () => {
email: '', email: '',
handicap: 0 handicap: 0
}; };
dialogMode.value = 'new';
memberDialog.value = true; memberDialog.value = true;
}; };
@@ -392,6 +393,8 @@ watch(() => member.value.gender, (newGender) => {
}); });
// //
const gtag = useGtag();
const editMember = (data) => { const editMember = (data) => {
member.value = { member.value = {
...data ...data
@@ -410,12 +413,18 @@ const hideDialog = () => {
const saveMember = async () => { const saveMember = async () => {
try { try {
const memberData = { ...member.value }; const memberData = { ...member.value };
const isNew = dialogMode.value === 'new'; const isNew = dialogMode.value === 'new';
const response = isNew const response = isNew
? await clubService.addClubMember(clubId.value, memberData) ? await clubService.addClubMember(clubId.value, memberData)
: await clubService.updateClubMember(clubId.value, memberData.id, memberData); : await clubService.updateClubMember(clubId.value, memberData.id, memberData);
// Google Analytics -
gtag.event(isNew ? 'add_member' : 'edit_member', {
'event_category': '회원관리',
'event_label': isNew ? '새 회원 추가' : '회원 정보 수정',
'value': 1
});
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: '성공', summary: '성공',
@@ -444,6 +453,12 @@ const deleteMember = async () => {
try { try {
await clubService.deleteClubMember(clubId.value, selectedMember.value.id); await clubService.deleteClubMember(clubId.value, selectedMember.value.id);
deleteDialog.value = false; deleteDialog.value = false;
// Google Analytics -
gtag.event('delete_member', {
'event_category': '회원관리',
'event_label': '회원 삭제',
'value': 1
});
toast.add({ severity: 'success', summary: '성공', detail: '회원이 삭제되었습니다.', life: 3000 }); toast.add({ severity: 'success', summary: '성공', detail: '회원이 삭제되었습니다.', life: 3000 });
await loadMembers(); await loadMembers();
} catch (error) { } catch (error) {
@@ -453,6 +468,14 @@ const deleteMember = async () => {
}; };
onMounted(() => {
// Google Analytics
gtag.pageview({
page_title: '회원 관리',
page_path: window.location.pathname,
page_location: window.location.href
});
});
</script> </script>
<style scoped> <style scoped>
@@ -199,6 +199,7 @@ import { ref, reactive, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import scoreService from '@/services/scoreService'; import scoreService from '@/services/scoreService';
import { useGtag } from 'vue-gtag-next';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@@ -411,7 +412,13 @@ const calculateTotal = (participant) => {
}; };
// //
// GA
const saveAllScores = async () => { const saveAllScores = async () => {
const gtag = useGtag();
gtag.event('save_scores', {
event_category: '점수관리',
event_label: '점수 저장'
});
try { try {
loading.value = true; loading.value = true;
await scoreService.saveScores(meetingId.value, participants.value); await scoreService.saveScores(meetingId.value, participants.value);
@@ -425,7 +432,13 @@ const saveAllScores = async () => {
}; };
// //
// GA
const exportScores = () => { const exportScores = () => {
const gtag = useGtag();
gtag.event('export_scores', {
event_category: '점수관리',
event_label: '점수 내보내기'
});
exportContent.value = [exportContentOptions.value[0]]; exportContent.value = [exportContentOptions.value[0]];
exportDialog.value = true; exportDialog.value = true;
}; };
@@ -446,12 +459,25 @@ const downloadExport = async () => {
}; };
// //
// GA
const goBack = () => { const goBack = () => {
const gtag = useGtag();
gtag.event('go_back', {
event_category: '점수관리',
event_label: '뒤로가기'
});
router.push('/clubs/meetings'); router.push('/clubs/meetings');
}; };
// //
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '점수관리',
page_path: window.location.pathname,
page_location: window.location.href
});
if (!clubId.value) { if (!clubId.value) {
toast.add({ severity: 'error', summary: '오류', detail: '클럽 ID가 설정되지 않았습니다.', life: 3000 }); toast.add({ severity: 'error', summary: '오류', detail: '클럽 ID가 설정되지 않았습니다.', life: 3000 });
return; return;
+21 -2
View File
@@ -6,7 +6,7 @@
</div> </div>
<div v-else> <div v-else>
<div class="period-selector"> <div class="period-selector">
<select v-model="selectedPeriod" @change="loadStatistics"> <select v-model="selectedPeriod" @change="onPeriodChange">
<option value="30">최근 30</option> <option value="30">최근 30</option>
<option value="90">최근 90</option> <option value="90">최근 90</option>
<option value="180">최근 180</option> <option value="180">최근 180</option>
@@ -184,6 +184,7 @@
<script> <script>
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { Bar as BarChart, Line as LineChart } from 'vue-chartjs'; import { Bar as BarChart, Line as LineChart } from 'vue-chartjs';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend } from 'chart.js'; import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend } from 'chart.js';
import apiClient from '@/services/api'; import apiClient from '@/services/api';
@@ -194,6 +195,7 @@ export default {
name: 'Statistics', name: 'Statistics',
components: { BarChart, LineChart }, components: { BarChart, LineChart },
setup() { setup() {
const gtag = useGtag();
const stats = ref({ const stats = ref({
summary: {}, summary: {},
scoreDistribution: [], scoreDistribution: [],
@@ -300,9 +302,25 @@ export default {
} }
onMounted(() => { onMounted(() => {
// Google Analytics
gtag.pageview({
page_title: '통계',
page_path: window.location.pathname,
page_location: window.location.href
});
loadStatistics(); loadStatistics();
}); });
// GA
const onPeriodChange = () => {
gtag.event('change_period', {
'event_category': '통계',
'event_label': selectedPeriod.value + '일',
'value': Number(selectedPeriod.value)
});
loadStatistics();
};
return { return {
stats, stats,
selectedPeriod, selectedPeriod,
@@ -314,7 +332,8 @@ export default {
attendanceTrendChartData, attendanceTrendChartData,
attendanceTrendChartOptions, attendanceTrendChartOptions,
participantTrendChartData, participantTrendChartData,
getHandicapTrendChartData getHandicapTrendChartData,
onPeriodChange
}; };
} }
}; };
@@ -765,7 +765,14 @@ function handleGuestAdded(guest) {
reload(); reload();
} }
// Google Analytics
onMounted(() => { onMounted(() => {
const gtag = useGtag();
gtag.pageview({
page_title: '이벤트공개페이지',
page_path: window.location.pathname,
page_location: window.location.href
});
const token = sessionStorage.getItem('event_token_' + publicHash); const token = sessionStorage.getItem('event_token_' + publicHash);
if (token) { if (token) {
fetchEventData(); fetchEventData();
@@ -16,13 +16,21 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const message = ref(''); const message = ref('');
// Google Analytics
onMounted(async () => { onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '결제실패',
page_path: window.location.pathname,
page_location: window.location.href
});
// URL // URL
message.value = route.query.message || '결제가 취소되었습니다.'; message.value = route.query.message || '결제가 취소되었습니다.';