Skip to content

Commit

Permalink
[FEAT] 공유 페이지에서 회원가입, 홈페이지 이동 버튼 추가 및 이벤트 로그 추적 (#880)
Browse files Browse the repository at this point in the history
* chore: mixpanel 의존성 설치

* feat: 서버에서 로그인 여부 판단하는 함수 구현

* feat: mixpannel 초기화 세팅

* feat:  사용자 action에 따라 동작하는 useEventLogger 추가

* feat: 페이지에 접속할 때 마다 동작하는 ScreenLogger 구현

* feat: 공유 페이지에서 회원가입 버튼 이벤트 추가

* feat: 공유 페이지에서 회원가입, 로그인, 홈페이지 이동 버튼 추가

* feat: 유저 웹사이트 페이지 접속 이벤트 추가

* ci/cd: 개발 서버 배포시 믹스패널 환경변수 추가

* fix: 공유폴더 버튼 영역 올바른 헤더에 위치

* fix: 필요없는 if문 삭제

* feat: 로그인한 유저만 홈으로 가기 버튼 보이게 변경
  • Loading branch information
dmdgpdi authored Dec 27, 2024
1 parent a1cde58 commit 37beb84
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 50 deletions.
1 change: 1 addition & 0 deletions .github/workflows/baguni.test.client.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
echo "NEXT_PUBLIC_API=${{ secrets.FRONT_NEXT_PUBLIC_API }}" >> .env.production
echo "NEXT_PUBLIC_DOMAIN=${{ secrets.FRONT_NEXT_PUBLIC_DOMAIN }}" >> .env.production
echo "NEXT_PUBLIC_REDIRECT_URL=${{secrets.FRONT_NEXT_PUBLIC_REDIRECT_URL}}" >> .env.production
echo "NEXT_PUBLIC_MIXPANEL_TOKEN=${{secrets.FRONT_NEXT_PUBLIC_MIXPANEL_TOKEN}}" >> .env.production
- # .env.sentry-build-plugin 설정
name: Create .env.sentry-build-plugin
Expand Down
3 changes: 2 additions & 1 deletion frontend/techpick/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NEXT_PUBLIC_DOMAIN='localhost를 실행할 도메인'
NEXT_PUBLIC_API='API 주소 요청 주소'
NEXT_PUBLIC_REDIRECT_URL=로그인 이후, 리다이렉트할 URL
NEXT_PUBLIC_REDIRECT_URL=로그인 이후, 리다이렉트할 URL
NEXT_PUBLIC_MIXPANEL_TOKEN=믹스패널에 사용하는 토큰
3 changes: 3 additions & 0 deletions frontend/techpick/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"ky": "^1.7.2",
"lottie-web": "^5.12.2",
"lucide-react": "^0.447.0",
"mixpanel": "^0.18.0",
"mixpanel-browser": "^2.58.0",
"next": "14.2.9",
"re-resizable": "^6.10.0",
"react": "^18",
Expand Down Expand Up @@ -76,6 +78,7 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@types/dompurify": "^3",
"@types/mixpanel-browser": "^2",
"@types/node": "^22.5.4",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
21 changes: 12 additions & 9 deletions frontend/techpick/src/app/(signed)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import type { PropsWithChildren } from 'react';
import { FolderAndPickDndContextProvider, FolderTree } from '@/components';
import { FeedbackToolbar } from '@/components/FeedbackToolbar';
import { ScreenLogger } from '@/components/ScreenLogger';
import ShortcutKey from '@/components/ShortcutKey';
import { pageContainerLayout } from './layout.css';

export default function SignedLayout({ children }: PropsWithChildren) {
return (
<div className={pageContainerLayout}>
<FolderAndPickDndContextProvider>
<FolderTree />
{/** 선택한 폴더에 따른 컨텐츠가 나옵니다. */}
{children}
<FeedbackToolbar />
<ShortcutKey />
</FolderAndPickDndContextProvider>
</div>
<ScreenLogger eventName="page_view_signed_user">
<div className={pageContainerLayout}>
<FolderAndPickDndContextProvider>
<FolderTree />
{/** 선택한 폴더에 따른 컨텐츠가 나옵니다. */}
{children}
<FeedbackToolbar />
<ShortcutKey />
</FolderAndPickDndContextProvider>
</div>
</ScreenLogger>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import Link from 'next/link';
import { ROUTES } from '@/constants';
import { useEventLogger } from '@/hooks';
import { signUpButtonStyle } from './page.css';

export function SignUpLinkButton() {
const { trackEvent: trackSignUpButtonClick } = useEventLogger({
eventName: 'shared_page_sign_up_button_click',
});

return (
<Link
href={ROUTES.LOGIN}
onClick={trackSignUpButtonClick}
onKeyDown={(e) => {
if (e.key === 'Enter') {
trackSignUpButtonClick();
}
}}
>
<button className={signUpButtonStyle}>회원가입</button>
</Link>
);
}
32 changes: 32 additions & 0 deletions frontend/techpick/src/app/(unsigned)/share/[uuid]/page.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { style } from '@vanilla-extract/css';
import {
orangeGhostButtonStyle,
orangeOutlineButtonStyle,
orangeSolidButtonStyle,
} from '@/styles/orangeButtonStyle.css';

export const buttonSectionStyle = style({
display: 'flex',
gap: '12px',
padding: '16px',
});

export const sharedPageButtonStyle = style({
width: '100px',
height: '32px',
});

export const homeNavigateButtonStyle = style([
sharedPageButtonStyle,
orangeGhostButtonStyle,
]);

export const loginButtonStyle = style([
sharedPageButtonStyle,
orangeOutlineButtonStyle,
]);

export const signUpButtonStyle = style([
sharedPageButtonStyle,
orangeSolidButtonStyle,
]);
94 changes: 54 additions & 40 deletions frontend/techpick/src/app/(unsigned)/share/[uuid]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { FolderOpenIcon } from 'lucide-react';
import { getShareFolderById } from '@/apis/folder/getShareFolderById';
import { PickRecordHeader } from '@/components';
Expand All @@ -13,6 +14,15 @@ import { FolderContentLayout } from '@/components/FolderContentLayout';
import { Gap } from '@/components/Gap';
import { PickContentLayout } from '@/components/PickContentLayout';
import { SharePickRecord } from '@/components/PickRecord/SharePickRecord';
import { ScreenLogger } from '@/components/ScreenLogger';
import { ROUTES } from '@/constants';
import { isLoginUser } from '@/utils';
import {
buttonSectionStyle,
homeNavigateButtonStyle,
loginButtonStyle,
} from './page.css';
import { SignUpLinkButton } from './SignUpLinkButton';

const EmptyPickRecordImage = dynamic(
() =>
Expand All @@ -27,10 +37,18 @@ const EmptyPickRecordImage = dynamic(
export default async function Page({ params }: { params: { uuid: string } }) {
const { uuid } = params;
const sharedFolder = await getShareFolderById(uuid);
const isLoggedIn = await isLoginUser();
const pickList = sharedFolder.pickList;

if (pickList.length === 0) {
return (
return (
<ScreenLogger
eventName="shared_page_view"
logInfo={{
folderUUID: uuid,
folderName: sharedFolder.folderName,
pickCount: pickList.length,
}}
>
<FolderContentLayout>
<div className={folderContentHeaderStyle}>
<Gap verticalSize="gap32" horizontalSize="gap32">
Expand All @@ -39,47 +57,43 @@ export default async function Page({ params }: { params: { uuid: string } }) {
<h1 className={folderNameStyle}>{sharedFolder.folderName}</h1>
</div>
</Gap>

<div className={buttonSectionStyle}>
{isLoggedIn ? (
<Link href={ROUTES.HOME}>
<button className={homeNavigateButtonStyle}>홈으로 가기</button>
</Link>
) : (
<>
<Link href={ROUTES.LOGIN}>
<button className={loginButtonStyle}>로그인</button>
</Link>
<SignUpLinkButton />
</>
)}
</div>
</div>
<PickContentLayout>
<EmptyPickRecordImage
title="공유된 북마크가 없습니다."
description="폴더 내 공유된 북마크가 존재하지 않습니다."
/>
<PickRecordHeader />
{pickList.length === 0 ? (
<EmptyPickRecordImage
title="공유된 북마크가 없습니다."
description="폴더 내 공유된 북마크가 존재하지 않습니다."
/>
) : (
pickList.map((pick) => {
return (
<SharePickRecord
key={pick.title}
pickInfo={pick}
tagList={sharedFolder.tagList!}
folderAccessToken={uuid}
/>
);
})
)}
</PickContentLayout>
</FolderContentLayout>
);
}

return (
<FolderContentLayout>
<div className={folderContentHeaderStyle}>
<Gap verticalSize="gap32" horizontalSize="gap32">
<div className={currentFolderNameSectionStyle}>
<FolderOpenIcon size={28} className={folderOpenIconStyle} />
<h1 className={folderNameStyle}>{sharedFolder.folderName}</h1>
</div>
</Gap>
</div>
<PickContentLayout>
<PickRecordHeader />
{pickList.length === 0 ? (
<EmptyPickRecordImage
title="공유된 북마크가 없습니다."
description="폴더 내 공유된 북마크가 존재하지 않습니다."
/>
) : (
pickList.map((pick) => {
return (
<SharePickRecord
key={pick.title}
pickInfo={pick}
tagList={sharedFolder.tagList!}
folderAccessToken={uuid}
/>
);
})
)}
</PickContentLayout>
</FolderContentLayout>
</ScreenLogger>
);
}
30 changes: 30 additions & 0 deletions frontend/techpick/src/components/ScreenLogger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { PropsWithChildren } from 'react';
import { mixpanelServer } from '@/libs/mixpanel-server';

/**
* @description 특정 페이지에 방문했는지 확인하는 컴포넌트입니다.
* @param eventName 해당 이벤트의 이름입니다. snake case로 명세해주세요. ex) shared_page_view
* @param userId 인증된 사용자의 ID입니다. 없을 경우 익명 사용자로 처리됩니다.
* @param logInfo 이벤트의 추가적인 정보를 담고 싶을 때 사용해주세요.
*/
export function ScreenLogger({
eventName,
userId = 'anonymous',
logInfo = {},
children,
}: PropsWithChildren<ScreenLoggerProps>) {
const properties: Record<string, unknown> = {
...logInfo,
$user_id: userId,
};

mixpanelServer.track(eventName, properties);

return <>{children}</>;
}

interface ScreenLoggerProps {
eventName: string;
userId?: string;
logInfo?: object;
}
1 change: 1 addition & 0 deletions frontend/techpick/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export { useFetchPickRecordByFolderId } from './useFetchPickRecordByFolderId';
export { useDisclosure } from './useDisclosure';
export { useRecommendPickToFolderDndMonitor } from './useRecommendPickToFolderDndMonitor';
export { useLocalStorage } from './useLocalStorage';
export { useEventLogger } from './useEventLogger';
export { useImageLoader } from './useImageLoader';
32 changes: 32 additions & 0 deletions frontend/techpick/src/hooks/useEventLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import mixpanel from '@/libs/mixpanel-client';

/**
* @description 특정 액션에 로그를 추가하는 훅입니다.
* @param eventName 해당 이벤트의 이름입니다. snake case로 명세해주세요. ex) shared_page_sign_up_button_click
* @param userId 인증된 사용자의 ID입니다. 없을 경우 'anonymous'로 처리됩니다.
* @param logInfo 이벤트의 추가적인 정보를 담고 싶을 때 사용해주세요.
* @returns trackEvent 액션에 추가해주세요.
*/
export function useEventLogger({
eventName,
userId = 'anonymous',
logInfo = {},
}: UseEventLoggerParameter) {
const trackEvent = () => {
mixpanel.identify(userId);
mixpanel.track(eventName, {
userId,
...logInfo,
});
};

return { trackEvent };
}

interface UseEventLoggerParameter {
eventName: string;
userId?: string;
logInfo?: object;
}
13 changes: 13 additions & 0 deletions frontend/techpick/src/libs/mixpanel-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import mixpanel from 'mixpanel-browser';

const MIXPANEL_TOKEN = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;

if (typeof window !== 'undefined' && MIXPANEL_TOKEN) {
mixpanel.init(MIXPANEL_TOKEN, {
debug: process.env.NODE_ENV !== 'production',
track_pageview: true,
persistence: 'localStorage',
});
}

export default mixpanel;
14 changes: 14 additions & 0 deletions frontend/techpick/src/libs/mixpanel-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Mixpanel from 'mixpanel';

const mixpanelToken = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;

if (!mixpanelToken) {
throw new Error(
'NEXT_PUBLIC_MIXPANEL_TOKEN is not set in environment variables'
);
}

/**
* @description 서버 컴포넌트나 서버 환경에서 mixpanel을 활용할 때 사용합니다.
*/
export const mixpanelServer = Mixpanel.init(mixpanelToken);
1 change: 1 addition & 0 deletions frontend/techpick/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export { isRecommendPickDraggableObject } from './isRecommendPickDraggableObject
export { getItemFromLocalStorage } from './getItemFromLocalStorage';
export { setItemToLocalStorage } from './setItemToLocalStorage';
export { isMacOS } from './isMacOS';
export { isLoginUser } from './isLoginUser';
13 changes: 13 additions & 0 deletions frontend/techpick/src/utils/isLoginUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use server';

import { cookies } from 'next/headers';

/**
* @description cookie에 접근해서 로그인 유무를 판단하는 함수입니다.
*/
export const isLoginUser = async () => {
const cookieStore = cookies();
const accessToken = await cookieStore.get('access_token');

return accessToken !== undefined;
};
Loading

0 comments on commit 37beb84

Please sign in to comment.