diff --git a/.gitignore b/.gitignore index 1574ac1..31e385a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,10 @@ dist-ssr *.sln *.sw? +# Local region data converter +convert-region-data.js + + # Environment Variables .env .env.local diff --git a/index.html b/index.html index e31b9b1..3abdc4f 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,24 @@ - - + + + + + @@ -22,28 +33,38 @@ - + - + - + - + diff --git a/package-lock.json b/package-lock.json index d0966e4..d95ab23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,8 @@ "react-router-dom": "^7.5.0", "react-use": "^17.6.0", "swiper": "^11.2.8", - "tailwind-scrollbar-hide": "^2.0.0" + "tailwind-scrollbar-hide": "^2.0.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -57,6 +58,7 @@ "globals": "^15.15.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "json5": "^2.2.3", "kakao.maps.d.ts": "^0.1.40", "postcss": "^8.5.3", "prettier": "^3.5.3", @@ -4247,6 +4249,15 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4600,13 +4611,13 @@ } }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -5019,6 +5030,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -5204,6 +5228,15 @@ "node": ">= 0.12.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -5369,6 +5402,18 @@ } } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -6933,6 +6978,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -11864,6 +11918,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stack-generator": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", @@ -13363,9 +13429,9 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { @@ -13633,6 +13699,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -13787,6 +13871,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index 6dcc586..1a7ba57 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "react-router-dom": "^7.5.0", "react-use": "^17.6.0", "swiper": "^11.2.8", - "tailwind-scrollbar-hide": "^2.0.0" + "tailwind-scrollbar-hide": "^2.0.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -64,6 +65,7 @@ "globals": "^15.15.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "json5": "^2.2.3", "kakao.maps.d.ts": "^0.1.40", "postcss": "^8.5.3", "prettier": "^3.5.3", diff --git a/scripts/convert-seoul-data.js b/scripts/convert-seoul-data.js new file mode 100644 index 0000000..ac85734 --- /dev/null +++ b/scripts/convert-seoul-data.js @@ -0,0 +1,55 @@ +import fs from "fs"; +import path from "path"; +import xlsx from "xlsx"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const excelPath = path.join(__dirname, "../행정동좌표.xlsx"); // 업로드된 파일명 그대로 +const regionJsonPath = path.join(__dirname, "../src/data/regionData.json"); // 기존 JSON + +try { + console.log("📦 서울시 엑셀 → JSON 변환 중..."); + + // 1️⃣ 엑셀 파일 로드 + const workbook = xlsx.readFile(excelPath); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const rows = xlsx.utils.sheet_to_json(sheet); + + // 2️⃣ 기존 JSON 불러오기 + const regionData = JSON.parse(fs.readFileSync(regionJsonPath, "utf-8")); + + // 3️⃣ 서울특별시 데이터 생성 + const seoulData = {}; + + rows.forEach((row) => { + const city = row["sd_nm"]; + const district = row["sgg_nm"]; + const dong = row["emd_nm"]; + const lat = parseFloat(row["center_lati"]); + const lng = parseFloat(row["center_long"]); + + // ✅ 1. '서울특별시'만 처리 + if (city !== "서울특별시") return; + + // ✅ 2. 값 유효성 검사 + if (!district || !dong || isNaN(lat) || isNaN(lng)) return; + + // ✅ 3. seoulData에 누적 + if (!seoulData[district]) seoulData[district] = []; + seoulData[district].push({ name: dong, lat, lng }); +}); + + + // 4️⃣ 기존 regionData.json에 병합 + regionData["서울특별시"] = seoulData; + + // 5️⃣ 다시 저장 + fs.writeFileSync(regionJsonPath, JSON.stringify(regionData, null, 2), "utf-8"); + + console.log("✅ 완료! regionData.json에 서울특별시 데이터가 추가되었습니다."); +} catch (err) { + console.error("❌ 변환 오류:", err.message); +} diff --git a/scripts/convertRegion.js b/scripts/convertRegion.js new file mode 100644 index 0000000..6fa8478 --- /dev/null +++ b/scripts/convertRegion.js @@ -0,0 +1,56 @@ +import fs from "fs"; +import path from "path"; +import xlsx from "xlsx"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const excelPath = path.join(__dirname, "../행정동좌표.xlsx"); +const regionJsonPath = path.join(__dirname, "../src/data/regionData.json"); + +try { + + //엑셀 파일 로드 + const workbook = xlsx.readFile(excelPath); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const rows = xlsx.utils.sheet_to_json(sheet); + + // 기존 JSON 불러오기 + const regionData = JSON.parse(fs.readFileSync(regionJsonPath, "utf-8")); + + // 새로운 지역 데이터 누적용 객체 + const newRegionData = {}; + + rows.forEach((row) => { + const city = row["sd_nm"]; + const district = row["sgg_nm"]; + const dong = row["emd_nm"]; + const lat = parseFloat(row["center_lati"]); + const lng = parseFloat(row["center_long"]); + + // 서울, 부산만 처리 + if (!["서울특별시", "부산광역시"].includes(city)) return; + + if (!district || !dong || isNaN(lat) || isNaN(lng)) return; + + // 구조 생성 + if (!newRegionData[city]) newRegionData[city] = {}; + if (!newRegionData[city][district]) newRegionData[city][district] = []; + + newRegionData[city][district].push({ name: dong, lat, lng }); + }); + + // 기존 JSON에 병합 + for (const city of Object.keys(newRegionData)) { + regionData[city] = newRegionData[city]; + } + + // 저장 + fs.writeFileSync(regionJsonPath, JSON.stringify(regionData, null, 2), "utf-8"); + + console.log("완료"); +} catch (err) { + console.error(" 변환 오류:", err.message); +} diff --git a/src/App.tsx b/src/App.tsx index 7769114..6ef0a7a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux'; import { setLoggedIn } from '@/store/userSlice'; import { useSSE } from '@/hooks/useSSE'; import { initViewportHeight } from '@/utils/viewport'; -import { initGA, trackPageView } from '@/analytics/ga'; +import { initGA, trackPageView, trackReturningUser } from '@/analytics/ga'; import { initMixpanel } from '@/analytics/mixpanel'; export default function App() { @@ -30,6 +30,27 @@ export default function App() { initGA(); trackPageView(window.location.pathname + window.location.search); initMixpanel(); + + // 리텐션 측정: 첫 방문 후 7일 이내 재방문 체크 + const firstVisitKey = 'kkinicong_first_visit'; + const now = Date.now(); + const firstVisit = localStorage.getItem(firstVisitKey); + + if (!firstVisit) { + // 첫 방문 + localStorage.setItem(firstVisitKey, String(now)); + } else { + // 재방문 + const firstVisitTime = parseInt(firstVisit, 10); + const daysBetween = Math.floor( + (now - firstVisitTime) / (1000 * 60 * 60 * 24), + ); + + if (daysBetween > 0 && daysBetween <= 7) { + // 7일 이내 재방문 + trackReturningUser(daysBetween); + } + } }, []); return ( diff --git a/src/analytics/ga.ts b/src/analytics/ga.ts index a041dbb..c4d5326 100644 --- a/src/analytics/ga.ts +++ b/src/analytics/ga.ts @@ -1,9 +1,132 @@ import ReactGA from 'react-ga4'; +let isInitialized = false; + export const initGA = () => { + // 중복 초기화 방지 + if (isInitialized) { + return; + } + ReactGA.initialize('G-MFJ749RKHH'); // 측정 ID + isInitialized = true; }; export const trackPageView = (path: string) => { + if (!isInitialized) { + console.warn('GA4가 초기화되지 않았습니다. initGA()를 먼저 호출하세요.'); + return; + } ReactGA.send({ hitType: 'pageview', page: path }); }; + +// GA4 이벤트 추적 함수 +export const trackEvent = ( + eventName: string, + parameters?: Record, +) => { + if (!isInitialized) { + console.warn('GA4가 초기화되지 않았습니다. initGA()를 먼저 호출하세요.'); + return; + } + ReactGA.event(eventName, parameters); +}; + +// 검색 이벤트 +export const trackSearchStore = (keyword: string, resultCount: number) => { + trackEvent('search_store', { + keyword, + result_count: resultCount, + }); +}; + +// 가맹점 상세 페이지 조회 +export const trackViewStoreDetail = ( + storeId: string | number, + category: string, + region?: string, +) => { + trackEvent('view_store_detail', { + store_id: String(storeId), + category, + ...(region && { region }), + }); +}; + +// 전화/지도 버튼 클릭 +export const trackClickPhoneOrMap = ( + storeId: string | number, + clickType: 'phone' | 'map', +) => { + trackEvent('click_phone_or_map', { + store_id: String(storeId), + click_type: clickType, + }); +}; + +// 즐겨찾기/저장 +export const trackSaveStore = (storeId: string | number) => { + trackEvent('save_store', { + store_id: String(storeId), + }); +}; + +// 공유 기능 +export const trackShareStore = ( + storeId: string | number, + shareChannel?: string, +) => { + trackEvent('share_store', { + store_id: String(storeId), + ...(shareChannel && { share_channel: shareChannel }), + }); +}; + +// 카테고리 선택 +export const trackOpenCategory = (categoryName: string) => { + trackEvent('open_category', { + category_name: categoryName, + }); +}; + +// 필터 적용 +export const trackFilterSearch = (filterType: string, filterValue: string) => { + trackEvent('filter_search', { + filter_type: filterType, + filter_value: filterValue, + }); +}; + +// 후기 섹션 열람 +export const trackViewReview = (storeId: string | number) => { + trackEvent('view_review', { + store_id: String(storeId), + }); +}; + +// 리뷰 등록 +export const trackSubmitReview = ( + storeId: string | number, + rating: number, + textLength: number, +) => { + trackEvent('submit_review', { + store_id: String(storeId), + rating, + text_length: textLength, + }); +}; + +// 스크롤 깊이 +export const trackScrollDepth = (depthPercent: number) => { + trackEvent('scroll_depth', { + depth_percent: depthPercent, + }); +}; + +// 리텐션 측정 +export const trackReturningUser = (daysBetweenVisits: number) => { + trackEvent('returning_user', { + days_between_visits: daysBetweenVisits, + }); +}; diff --git a/src/assets/svgs/logo/card-congG.svg b/src/assets/svgs/logo/card-congG.svg index 4e6cadf..dfb0d25 100644 --- a/src/assets/svgs/logo/card-congG.svg +++ b/src/assets/svgs/logo/card-congG.svg @@ -1,9 +1,9 @@ - + - - + + - + - \ No newline at end of file + diff --git a/src/components/StoreDetail/StoreDetailInfo.tsx b/src/components/StoreDetail/StoreDetailInfo.tsx index 958d053..e2ee790 100644 --- a/src/components/StoreDetail/StoreDetailInfo.tsx +++ b/src/components/StoreDetail/StoreDetailInfo.tsx @@ -9,6 +9,7 @@ import dayjs from 'dayjs'; import LoginRequiredBottomSheet from '../common/LoginRequiredBottomSheet'; import axios from '@/api/axiosInstance'; import type { StoreDetail } from '@/types/store'; +import { trackSaveStore } from '@/analytics/ga'; interface StoreDetailInfoProps { store: StoreDetail; @@ -66,8 +67,13 @@ const StoreDetailInfo: React.FC = ({ }); if (response.data.isSuccess) { - setIsLiked(response.data.results.isScrapped); + const newIsLiked = response.data.results.isScrapped; + setIsLiked(newIsLiked); setLikeCount(response.data.results.scrapCount); + // 즐겨찾기 이벤트 태깅 (저장할 때만) + if (newIsLiked) { + trackSaveStore(storeId); + } } else { console.error('서버 응답 실패:', response.data.message); } @@ -81,7 +87,9 @@ const StoreDetailInfo: React.FC = ({
{/* 카테고리, 이름, 태그 */}
-

{category}

+

+ {category} +

{name} @@ -91,7 +99,9 @@ const StoreDetailInfo: React.FC = ({

{/* 주소 */} -

{address}

+

+ {address} +

{/* 영업시간 + 찜 아이콘 */}
@@ -136,7 +146,7 @@ const StoreDetailInfo: React.FC = ({ setIsBottomSheetOpen(false)} - pendingPath = {pendingPath} + pendingPath={pendingPath} />
); diff --git a/src/components/StoreDetail/StoreDetailMap.tsx b/src/components/StoreDetail/StoreDetailMap.tsx index 977503f..e94e910 100644 --- a/src/components/StoreDetail/StoreDetailMap.tsx +++ b/src/components/StoreDetail/StoreDetailMap.tsx @@ -8,6 +8,7 @@ import { useMemo, useState, useEffect } from 'react'; import MenuBtn from '@/assets/svgs/detail/menu-btn.svg?react'; import NavigationBtn from '@/assets/svgs/detail/navigation-btn.svg?react'; import axiosInstance from '@/api/axiosInstance'; +import { trackClickPhoneOrMap } from '@/analytics/ga'; import { MapMarker } from 'react-kakao-maps-sdk'; interface StoreDetailMapProps { @@ -167,7 +168,10 @@ const StoreDetailMap: React.FC = ({ const desktop = externalLinks?.directionUrlDesktop; if (!desktop) return; const webUrl = normalizeWebUrl(desktop); - if (webUrl) window.open(webUrl, '_blank', 'noopener'); + if (webUrl) { + trackClickPhoneOrMap(store.storeId, 'map'); + window.open(webUrl, '_blank', 'noopener'); + } }; /** Android: 앱 우선(앱→인텐트→웹) */ @@ -191,10 +195,14 @@ const StoreDetailMap: React.FC = ({ !isSafeScheme(appUrl) ) { // 좌표/URL 이상하면 그냥 웹 - if (webUrl) window.open(webUrl, '_blank', 'noopener'); + if (webUrl) { + trackClickPhoneOrMap(store.storeId, 'map'); + window.open(webUrl, '_blank', 'noopener'); + } return; } + trackClickPhoneOrMap(store.storeId, 'map'); openAppAndroid(appUrl, intentUrl, webUrl); }; @@ -225,6 +233,7 @@ const StoreDetailMap: React.FC = ({ const onClickIOSAnchor = (e: React.MouseEvent) => { if (!iosDeepLinks) return; const { webUrl } = iosDeepLinks; + trackClickPhoneOrMap(store.storeId, 'map'); // 전환 감지되면 타이머 취소 → 돌아와도 웹 안 뜸 let finished = false; diff --git a/src/components/StoreDetail/StoreDetailReview.tsx b/src/components/StoreDetail/StoreDetailReview.tsx index 920bb49..09a7eb5 100644 --- a/src/components/StoreDetail/StoreDetailReview.tsx +++ b/src/components/StoreDetail/StoreDetailReview.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'; import axios from '@/api/axiosInstance'; import LoginModal from '@/components/common/LoginRequiredBottomSheet'; import OptimizedImage from '../common/OptimizedImage'; +import { trackViewReview } from '@/analytics/ga'; interface StoreDetailReviewProps { store: StoreDetail; @@ -26,7 +27,6 @@ const StoreDetailReview: React.FC = ({ const [reviewCount, setReviewCount] = useState(0); const [showLoginModal, setShowLoginModal] = useState(false); - // 리뷰PR 커밋용 주석 열기 const handleReviewClick = () => { const isLoggedIn = !!localStorage.getItem('accessToken'); @@ -44,7 +44,6 @@ const StoreDetailReview: React.FC = ({ }, }); } else { - setShowLoginModal(true); } }; @@ -70,6 +69,8 @@ const StoreDetailReview: React.FC = ({ useEffect(() => { fetchReviews(0); + // 후기 섹션 열람 이벤트 태깅 + trackViewReview(store.storeId); }, [store.storeId]); return ( @@ -82,7 +83,7 @@ const StoreDetailReview: React.FC = ({

다녀오셨나요?

리뷰를 통해 경험을 공유해주세요!

- +