From ebf83ed7ab8f6e15a5b8406d83bc5e9936317562 Mon Sep 17 00:00:00 2001 From: eeeyooon Date: Fri, 7 Mar 2025 14:47:59 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[YS-384]=20feat:=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=A1=9C=EB=94=A9=20=EC=8A=A4=ED=94=BC=EB=84=88=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 --- .../ExperimentPostCardListContainer.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/app/home/components/ExperimentPostContainer/ExperimentPostCardListContainer/ExperimentPostCardListContainer.tsx b/src/app/home/components/ExperimentPostContainer/ExperimentPostCardListContainer/ExperimentPostCardListContainer.tsx index 5491c197..753b5a19 100644 --- a/src/app/home/components/ExperimentPostContainer/ExperimentPostCardListContainer/ExperimentPostCardListContainer.tsx +++ b/src/app/home/components/ExperimentPostContainer/ExperimentPostCardListContainer/ExperimentPostCardListContainer.tsx @@ -9,6 +9,9 @@ import { import { ExperimentPostListFilters } from '@/apis/post'; import useExperimentPostListQuery from '@/app/home/hooks/useExperimentPostListQuery'; +import { emptySubTitle } from '@/app/my-posts/components/MyPostsTable/MyPostsTable.css'; +import { emptyViewLayout } from '@/app/post/[post_id]/components/ExperimentPostContainer/ExperimentPostContainer.css'; +import Spinner from '@/components/Spinner/Spinner'; interface PostCardListContainerProps { filters: ExperimentPostListFilters; @@ -22,8 +25,18 @@ const ExperimentPostCardListContainer = ({ filters, isLoading }: PostCardListCon fetchNextPage, isFetchingNextPage, isFetching, + isLoading: isListLoading, } = useExperimentPostListQuery(filters, isLoading); + if (isListLoading) { + return ( +
+ +

로딩중..

+
+ ); + } + return (
From 2d8ef205dad0e8386ec878885fe2d1cd4449cd08 Mon Sep 17 00:00:00 2001 From: eeeyooon Date: Fri, 7 Mar 2025 15:57:02 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[YS-384]=20chore:=20mixpanel=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 ++ pnpm-lock.yaml | 93 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 8c033d19..dbc016ce 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@tanstack/react-query": "^5.62.11", "@tanstack/react-query-devtools": "^5.62.11", "@tanstack/react-table": "^8.20.6", + "@types/mixpanel-browser": "^2.51.0", "@vanilla-extract/css": "^1.17.0", "@vanilla-extract/dynamic": "^2.1.2", "@vanilla-extract/next-plugin": "^2.4.8", @@ -29,6 +30,7 @@ "axios": "^1.7.9", "date-fns": "^4.1.0", "jira-prepare-commit-msg": "^1.7.2", + "mixpanel-browser": "^2.61.0", "next": "14.2.22", "react": "^18", "react-day-picker": "^9.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1032a2c4..63bd2b9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@tanstack/react-table': specifier: ^8.20.6 version: 8.20.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/mixpanel-browser': + specifier: ^2.51.0 + version: 2.51.0 '@vanilla-extract/css': specifier: ^1.17.0 version: 1.17.0(babel-plugin-macros@3.1.0) @@ -59,6 +62,9 @@ importers: jira-prepare-commit-msg: specifier: ^1.7.2 version: 1.7.2(typescript@5.7.2) + mixpanel-browser: + specifier: ^2.61.0 + version: 2.61.0 next: specifier: 14.2.22 version: 14.2.22(@babel/core@7.26.7)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1293,6 +1299,9 @@ packages: cpu: [x64] os: [win32] + '@rrweb/types@2.0.0-alpha.18': + resolution: {integrity: sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1342,6 +1351,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/css-font-loading-module@0.0.7': + resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -1369,6 +1381,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/mixpanel-browser@2.51.0': + resolution: {integrity: sha512-or4HZRDmqMJ1OP+O6h4hB+tVDKJffpVi2bFxi/Gem5TGnE94I7OtWKXtrwdDfSGhfOLL+WQIOUOxUZHAzHNi8A==} + '@types/node@20.17.10': resolution: {integrity: sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==} @@ -1529,6 +1544,9 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@xstate/fsm@1.6.5': + resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -1667,6 +1685,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -2171,6 +2193,9 @@ packages: fastq@1.18.0: resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2660,6 +2685,12 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mixpanel-browser@2.61.0: + resolution: {integrity: sha512-gAd8dKhJ/C5V9B07HpWQiTQMPHFn8+gAOX+/DDTBMbPhYYxDMheb5L68PSIqk2XUNZdOFIB5AIJMUKXAIp250Q==} + mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -2995,6 +3026,15 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrdom@2.0.0-alpha.18: + resolution: {integrity: sha512-fSFzFFxbqAViITyYVA4Z0o5G6p1nEqEr/N8vdgSKie9Rn0FJxDSNJgjV0yiCIzcDs0QR+hpvgFhpbdZ6JIr5Nw==} + + rrweb-snapshot@2.0.0-alpha.18: + resolution: {integrity: sha512-hBHZL/NfgQX6wO1D9mpwqFu1NJPpim+moIcKhFEjVTZVRUfCln+LOugRc4teVTCISYHN8Cw5e2iNTWCSm+SkoA==} + + rrweb@2.0.0-alpha.13: + resolution: {integrity: sha512-a8GXOCnzWHNaVZPa7hsrLZtNZ3CGjiL+YrkpLo0TfmxGLhjNZbWY2r7pE06p+FcjFNlgUVTmFrSJbK3kO7yxvw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4480,6 +4520,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.32.0': optional: true + '@rrweb/types@2.0.0-alpha.18': {} + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.10.4': {} @@ -4525,6 +4567,8 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/css-font-loading-module@0.0.7': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -4559,6 +4603,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/mixpanel-browser@2.51.0': {} + '@types/node@20.17.10': dependencies: undici-types: 6.19.8 @@ -4846,6 +4892,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@xstate/fsm@1.6.5': {} + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -5005,6 +5053,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + big.js@5.2.2: {} body-parser@1.20.3: @@ -5441,8 +5491,8 @@ snapshots: '@typescript-eslint/parser': 8.18.2(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.3(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -5461,7 +5511,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -5473,22 +5523,22 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.18.2(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5499,7 +5549,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5704,6 +5754,8 @@ snapshots: dependencies: reusify: 1.0.4 + fflate@0.4.8: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -6173,6 +6225,12 @@ snapshots: minipass@7.1.2: {} + mitt@3.0.1: {} + + mixpanel-browser@2.61.0: + dependencies: + rrweb: 2.0.0-alpha.13 + mlly@1.7.4: dependencies: acorn: 8.14.0 @@ -6543,6 +6601,25 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.32.0 fsevents: 2.3.3 + rrdom@2.0.0-alpha.18: + dependencies: + rrweb-snapshot: 2.0.0-alpha.18 + + rrweb-snapshot@2.0.0-alpha.18: + dependencies: + postcss: 8.5.1 + + rrweb@2.0.0-alpha.13: + dependencies: + '@rrweb/types': 2.0.0-alpha.18 + '@types/css-font-loading-module': 0.0.7 + '@xstate/fsm': 1.6.5 + base64-arraybuffer: 1.0.2 + fflate: 0.4.8 + mitt: 3.0.1 + rrdom: 2.0.0-alpha.18 + rrweb-snapshot: 2.0.0-alpha.18 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 From 0f112cf628c117fe1568089e11d44aeef9e50632 Mon Sep 17 00:00:00 2001 From: eeeyooon Date: Fri, 7 Mar 2025 15:57:21 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[YS-384]=20feat:=20=EB=AF=B9=EC=8A=A4=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/hooks/useGoogleLoginMutation.ts | 6 ++ src/app/login/hooks/useNaverLoginMutation.ts | 5 ++ src/app/providers.tsx | 11 ++++ src/lib/mixpanelClient.ts | 60 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 src/lib/mixpanelClient.ts diff --git a/src/app/login/hooks/useGoogleLoginMutation.ts b/src/app/login/hooks/useGoogleLoginMutation.ts index aab38522..94c961b7 100644 --- a/src/app/login/hooks/useGoogleLoginMutation.ts +++ b/src/app/login/hooks/useGoogleLoginMutation.ts @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation'; import { API } from '@/apis/config'; import { googleLogin } from '@/apis/login'; +import { identifyUser, setUserProperties } from '@/lib/mixpanelClient'; const useGoogleLoginMutation = () => { const router = useRouter(); @@ -14,11 +15,16 @@ const useGoogleLoginMutation = () => { API.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; sessionStorage.setItem('refreshToken', refreshToken); sessionStorage.setItem('role', memberInfo.role); + + identifyUser(memberInfo.oauthEmail); + setUserProperties({ email: memberInfo.oauthEmail, role: memberInfo.role }); + router.push('/'); return; } sessionStorage.setItem('email', memberInfo.oauthEmail); + router.push('/join'); }, onError: () => { diff --git a/src/app/login/hooks/useNaverLoginMutation.ts b/src/app/login/hooks/useNaverLoginMutation.ts index 8998321e..c99598e6 100644 --- a/src/app/login/hooks/useNaverLoginMutation.ts +++ b/src/app/login/hooks/useNaverLoginMutation.ts @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation'; import { API } from '@/apis/config'; import { naverLogin, NaverLoginParams } from '@/apis/login'; +import { identifyUser, setUserProperties } from '@/lib/mixpanelClient'; const useNaverLoginMutation = () => { const router = useRouter(); @@ -14,6 +15,10 @@ const useNaverLoginMutation = () => { API.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; sessionStorage.setItem('refreshToken', refreshToken); sessionStorage.setItem('role', memberInfo.role); + + identifyUser(memberInfo.oauthEmail); + setUserProperties({ email: memberInfo.oauthEmail, role: memberInfo.role }); + router.push('/'); return; } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 5af2ad72..e2836ffd 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -2,7 +2,10 @@ import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { usePathname } from 'next/navigation'; +import { useEffect } from 'react'; +import { trackEvent } from '@/lib/mixpanelClient'; import MSWProvider from '@/mocks/MSWProvider'; function makeQueryClient() { @@ -23,6 +26,14 @@ function getQueryClient() { export default function Providers({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient(); + const pathname = usePathname(); + + useEffect(() => { + if (pathname) { + trackEvent('Page Viewed', { page: pathname }); + } + }, [pathname]); + return ( diff --git a/src/lib/mixpanelClient.ts b/src/lib/mixpanelClient.ts new file mode 100644 index 00000000..4d74d4e1 --- /dev/null +++ b/src/lib/mixpanelClient.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ +import mixpanel from 'mixpanel-browser'; + +const MIXPANEL_TOKEN = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN; +let isMixpanelInitialized = false; + +export const initMixpanel = () => { + if (!MIXPANEL_TOKEN) { + console.warn('Mixpanel token is missing! Check your .env file.'); + return; + } + + if (!isMixpanelInitialized) { + mixpanel.init(MIXPANEL_TOKEN, { + debug: process.env.NODE_ENV === 'development', + track_pageview: false, + persistence: 'localStorage', + }); + isMixpanelInitialized = true; + console.log('✅ Mixpanel initialized'); + } +}; + +/** + * Mixpanel 이벤트 로깅 함수 + * @param event 이벤트 이름 + * @param properties 이벤트 속성 (선택) + */ + +//todo 수정 예정 +export const trackEvent = (event: string, properties?: Record) => { + if (!MIXPANEL_TOKEN) { + console.warn('Mixpanel Token is missing.'); + return; + } + + if (!isMixpanelInitialized) { + console.warn('Mixpanel is not initialized. Initializing now...'); + initMixpanel(); + } + mixpanel.track(event, properties); +}; +/** + * 사용자 ID 설정 (로그인 시 호출) + * @param userId 사용자 ID + */ +export const identifyUser = (userId: string) => { + if (!MIXPANEL_TOKEN) return; + mixpanel.identify(userId); +}; + +/** + * 사용자 속성 설정 (유저 프로필 업데이트) + * @param properties 사용자 속성 데이터 + */ +export const setUserProperties = (properties: Record) => { + if (!MIXPANEL_TOKEN) return; + mixpanel.people.set(properties); +}; From 09adb09e5e42024f56062bb6377321fb63bc78a8 Mon Sep 17 00:00:00 2001 From: eeeyooon Date: Sun, 9 Mar 2025 15:57:31 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[YS-384]=20feat:=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=B3=84=20Metadata=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/edit/[post_id]/layout.tsx | 7 +++++++ src/app/join/layout.tsx | 6 ++++++ src/app/login/layout.tsx | 7 +++++++ src/app/my-posts/layout.tsx | 6 ++++++ src/app/post/[post_id]/layout.tsx | 7 +++++++ src/app/upload/layout.tsx | 7 +++++++ src/app/user/profile/layout.tsx | 7 +++++++ 7 files changed, 47 insertions(+) diff --git a/src/app/edit/[post_id]/layout.tsx b/src/app/edit/[post_id]/layout.tsx index da55ae5e..cf3992fb 100644 --- a/src/app/edit/[post_id]/layout.tsx +++ b/src/app/edit/[post_id]/layout.tsx @@ -1,5 +1,12 @@ +import { Metadata } from 'next'; + import DefaultLayout from '@/components/layout/DefaultLayout/DefaultLayout'; +export const metadata: Metadata = { + title: '그라밋 | 공고 수정', + description: '그라밋 | 공고 수정', +}; + function EditLayout({ children }: { children: React.ReactNode }) { return {children}; } diff --git a/src/app/join/layout.tsx b/src/app/join/layout.tsx index 260b7b92..0d9fbc2d 100644 --- a/src/app/join/layout.tsx +++ b/src/app/join/layout.tsx @@ -1,7 +1,13 @@ +import { Metadata } from 'next'; import { Suspense } from 'react'; import { joinPageLayout } from './JoinPage.css'; +export const metadata: Metadata = { + title: '그라밋 | 회원가입', + description: '그라밋 | 회원가입', +}; + function JoinLayout({ children }: { children: React.ReactNode }) { return (
diff --git a/src/app/login/layout.tsx b/src/app/login/layout.tsx index 3ff28e5c..6cf19aad 100644 --- a/src/app/login/layout.tsx +++ b/src/app/login/layout.tsx @@ -1,5 +1,12 @@ +import { Metadata } from 'next'; + import { loginLayout } from './LoginPage.css'; +export const metadata: Metadata = { + title: '그라밋 | 로그인', + description: '그라밋 | 로그인', +}; + function LoginLayout({ children }: { children: React.ReactNode }) { return
{children}
; } diff --git a/src/app/my-posts/layout.tsx b/src/app/my-posts/layout.tsx index 976b3964..b4764f99 100644 --- a/src/app/my-posts/layout.tsx +++ b/src/app/my-posts/layout.tsx @@ -1,9 +1,15 @@ +import { Metadata } from 'next'; import { PropsWithChildren } from 'react'; import { myPostsLayout, myPostsLayoutContainer } from './MyPostsPage.css'; import Header from '@/components/Header/Header'; +export const metadata: Metadata = { + title: '그라밋 | 작성 글 목록', + description: '그라밋 | 작성 글 목록', +}; + function MyPostsLayout({ children }: PropsWithChildren) { return (
diff --git a/src/app/post/[post_id]/layout.tsx b/src/app/post/[post_id]/layout.tsx index 0953086e..21cd9dee 100644 --- a/src/app/post/[post_id]/layout.tsx +++ b/src/app/post/[post_id]/layout.tsx @@ -1,5 +1,12 @@ +import { Metadata } from 'next'; + import DefaultLayout from '@/components/layout/DefaultLayout/DefaultLayout'; +export const metadata: Metadata = { + title: '그라밋 | 공고 조회', + description: '그라밋 | 실험 공고 조회', +}; + function PostLayout({ children }: { children: React.ReactNode }) { return {children}; } diff --git a/src/app/upload/layout.tsx b/src/app/upload/layout.tsx index 8308a739..23178150 100644 --- a/src/app/upload/layout.tsx +++ b/src/app/upload/layout.tsx @@ -1,5 +1,12 @@ +import { Metadata } from 'next'; + import DefaultLayout from '@/components/layout/DefaultLayout/DefaultLayout'; +export const metadata: Metadata = { + title: '그라밋 | 실험 공고 등록', + description: '그라밋 | 실험 공고 등록', +}; + function UploadLayout({ children }: { children: React.ReactNode }) { return {children}; } diff --git a/src/app/user/profile/layout.tsx b/src/app/user/profile/layout.tsx index 80b16581..c2167669 100644 --- a/src/app/user/profile/layout.tsx +++ b/src/app/user/profile/layout.tsx @@ -1,7 +1,14 @@ +import { Metadata } from 'next'; + import { profilePageLayout } from './ProfilePage.css'; import UserLayout from '@/components/layout/UserLayout/UserLayout'; +export const metadata: Metadata = { + title: '그라밋 | 회원 정보', + description: '그라밋 | 회원 정보', +}; + function ProfileLayout({ children }: { children: React.ReactNode }) { return ( From b0c441a89559c6945e2c0fc44e84ebc37e0985ca Mon Sep 17 00:00:00 2001 From: eeeyooon Date: Sun, 9 Mar 2025 17:55:08 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[YS-384]=20assets:=20clip-path=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Icon/icons/AlertOutlined.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Icon/icons/AlertOutlined.tsx b/src/components/Icon/icons/AlertOutlined.tsx index 8b5d10f0..27a4304e 100644 --- a/src/components/Icon/icons/AlertOutlined.tsx +++ b/src/components/Icon/icons/AlertOutlined.tsx @@ -12,7 +12,7 @@ function AlertOutlined(props: CustomSVGProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - + Date: Sun, 9 Mar 2025 17:55:32 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[YS-384]=20feat:=20trackEvent=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=8B=9C=20mixpanel=20reset=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExperimentPostOutline.tsx | 11 ++++++++++- .../ParticipationGuideModal.tsx | 11 +++++++++++ src/app/providers.tsx | 15 +++++++-------- src/components/Header/Menu.tsx | 2 ++ src/lib/mixpanelClient.ts | 17 +++++++++++++++-- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/app/post/[post_id]/components/ExperimentPostOutline/ExperimentPostOutline.tsx b/src/app/post/[post_id]/components/ExperimentPostOutline/ExperimentPostOutline.tsx index cf9a87d1..170b55c4 100644 --- a/src/app/post/[post_id]/components/ExperimentPostOutline/ExperimentPostOutline.tsx +++ b/src/app/post/[post_id]/components/ExperimentPostOutline/ExperimentPostOutline.tsx @@ -25,6 +25,7 @@ import { UseQueryExperimentDetailsAPIResponse } from '../../hooks/useExperimentD import ParticipationGuideModal from '../ParticipationGuideModal/ParticipationGuideModal'; import { GenderType } from '@/app/upload/components/ApplyMethodSection/ApplyMethodSection'; +import { trackEvent } from '@/lib/mixpanelClient'; interface ExperimentPostOutlineProps { postDetailData: UseQueryExperimentDetailsAPIResponse; @@ -124,7 +125,15 @@ const ExperimentPostOutline = ({ postDetailData, applyMethodData }: ExperimentPo
{recruitStatus ? ( - ) : ( diff --git a/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx b/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx index 07f331f7..76e7dc2e 100644 --- a/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx +++ b/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx @@ -19,6 +19,7 @@ import { CommonModalProps } from '../../ExperimentPostPage.types'; import { UseApplyMethodQueryResponse } from '../../hooks/useApplyMethodQuery'; import Icon from '@/components/Icon'; +import { trackEvent } from '@/lib/mixpanelClient'; import { colors } from '@/styles/colors'; interface ParticipationGuideModalProps extends CommonModalProps { @@ -39,6 +40,10 @@ const ParticipationGuideModal = ({ navigator.clipboard.writeText(text).then(() => { setIsCopyToastOpen(true); }); + + trackEvent('ApplyMethod Interaction', { + action: 'Link Copied', + }); }; if (!applyMethodData) return null; @@ -88,6 +93,12 @@ const ParticipationGuideModal = ({ target="_blank" rel="noopener noreferrer" style={{ color: colors.primaryMint, textDecoration: 'underline' }} + onClick={() => { + trackEvent('ApplyMethod Interaction', { + action: 'Link Clicked', + link_url: applyMethodData.formUrl, + }); + }} > {applyMethodData.formUrl} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index e2836ffd..e57a71c5 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -2,10 +2,9 @@ import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { usePathname } from 'next/navigation'; import { useEffect } from 'react'; -import { trackEvent } from '@/lib/mixpanelClient'; +import { setUserProperties, trackEvent } from '@/lib/mixpanelClient'; import MSWProvider from '@/mocks/MSWProvider'; function makeQueryClient() { @@ -25,14 +24,14 @@ function getQueryClient() { export default function Providers({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient(); - - const pathname = usePathname(); - useEffect(() => { - if (pathname) { - trackEvent('Page Viewed', { page: pathname }); + if (typeof window !== 'undefined') { + const pageTitle = document.title; + + trackEvent('Page Viewed', { page: pageTitle }); + setUserProperties({ last_visited_page: pageTitle }); } - }, [pathname]); + }, []); // return ( diff --git a/src/components/Header/Menu.tsx b/src/components/Header/Menu.tsx index a57677e3..d50aa370 100644 --- a/src/components/Header/Menu.tsx +++ b/src/components/Header/Menu.tsx @@ -7,6 +7,7 @@ import { contentContainer, selectItem, triggerWrapper } from './Menu.css'; import { ParticipantResponse, ResearcherResponse } from '@/apis/login'; import Icon from '@/components/Icon'; import useSessionStorage from '@/hooks/useSessionStorage'; +import { logoutUser } from '@/lib/mixpanelClient'; import { isResearcherInfo } from '@/utils/typeGuard'; interface MenuProps { @@ -19,6 +20,7 @@ const Menu = ({ userInfo }: MenuProps) => { const logout = () => { clear(); + logoutUser(); window.location.href = '/'; }; diff --git a/src/lib/mixpanelClient.ts b/src/lib/mixpanelClient.ts index 4d74d4e1..a027e478 100644 --- a/src/lib/mixpanelClient.ts +++ b/src/lib/mixpanelClient.ts @@ -18,7 +18,7 @@ export const initMixpanel = () => { persistence: 'localStorage', }); isMixpanelInitialized = true; - console.log('✅ Mixpanel initialized'); + // console.log('Mixpanel initialized'); } }; @@ -36,7 +36,7 @@ export const trackEvent = (event: string, properties?: Record) => { } if (!isMixpanelInitialized) { - console.warn('Mixpanel is not initialized. Initializing now...'); + // console.warn('Mixpanel is not initialized. Initializing now...'); initMixpanel(); } mixpanel.track(event, properties); @@ -48,6 +48,9 @@ export const trackEvent = (event: string, properties?: Record) => { export const identifyUser = (userId: string) => { if (!MIXPANEL_TOKEN) return; mixpanel.identify(userId); + mixpanel.people.set_once({ + signup_date: new Date().toISOString(), // 최초 가입 시점 기록 + }); }; /** @@ -58,3 +61,13 @@ export const setUserProperties = (properties: Record) => { if (!MIXPANEL_TOKEN) return; mixpanel.people.set(properties); }; + +/** + * 사용자 로그아웃 (로그아웃 시 호출) + */ +export const logoutUser = () => { + if (!MIXPANEL_TOKEN) return; + + mixpanel.reset(); + // console.log('Mixpanel user data reset'); +}; From b93aad9f0be9813d5744c797c6c63cf50c384911 Mon Sep 17 00:00:00 2001 From: eeeyooon Date: Sun, 9 Mar 2025 18:01:24 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[YS-384]=20refactor:=20properties=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParticipationGuideModal/ParticipationGuideModal.tsx | 2 +- src/lib/mixpanelClient.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx b/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx index 76e7dc2e..78729914 100644 --- a/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx +++ b/src/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.tsx @@ -96,7 +96,7 @@ const ParticipationGuideModal = ({ onClick={() => { trackEvent('ApplyMethod Interaction', { action: 'Link Clicked', - link_url: applyMethodData.formUrl, + link_url: applyMethodData.formUrl ?? '', }); }} > diff --git a/src/lib/mixpanelClient.ts b/src/lib/mixpanelClient.ts index a027e478..9f02cae0 100644 --- a/src/lib/mixpanelClient.ts +++ b/src/lib/mixpanelClient.ts @@ -28,8 +28,7 @@ export const initMixpanel = () => { * @param properties 이벤트 속성 (선택) */ -//todo 수정 예정 -export const trackEvent = (event: string, properties?: Record) => { +export const trackEvent = (event: string, properties?: Record) => { if (!MIXPANEL_TOKEN) { console.warn('Mixpanel Token is missing.'); return;