Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TMN-58] 회원가입 후 온보딩 #642

Merged
merged 9 commits into from
Mar 28, 2024
Binary file added public/assets/character/basic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/character/flag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/character/sad.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/apis/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface LoginResponse {
accessToken: string;
refreshToken: string;
memberId?: number;
landingStatus?: 'TO_MAIN' | 'TO_ONBOARDING';
}

interface RegisterRequest {
Expand Down
7 changes: 6 additions & 1 deletion src/app/auth/kakaoCallback/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { useSocialLogin } from '@/apis/auth';
import Loading from '@/components/Loading';
import { AUTH_PROVIDER } from '@/constants/common';
import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog';
import { ROUTER } from '@/constants/router';
import { eventLogger } from '@/utils';

Expand Down Expand Up @@ -32,7 +33,11 @@ export default function KakaoCallbackPage() {
if (successData?.memberId) {
eventLogger.identify(successData.memberId.toString());
}

if (successData.landingStatus === 'TO_ONBOARDING') {
eventLogger.logEvent(EVENT_LOG_CATEGORY.ONBOARDING, EVENT_LOG_NAME.ONBOARDING.SUCCESS_SIGNUP);
router.push(ROUTER.ONBOARDING.HOME);
return;
}
router.push(params.get('state') ?? ROUTER.HOME);
},
},
Expand Down
14 changes: 11 additions & 3 deletions src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useSocialLogin, useUpdateMemberFcmToken } from '@/apis/auth';
// import Button from '@/components/Button/Button';
import ButtonSocialLogin from '@/components/ButtonSocialLogin/ButtonSocialLogin';
import { AUTH_PROVIDER, WINDOW_CUSTOM_EVENT } from '@/constants/common';
import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog';
import { NATIVE_CUSTOM_EVENTS } from '@/constants/nativeCustomEvent';
import { ROUTER } from '@/constants/router';
import { eventLogger } from '@/utils';
Expand Down Expand Up @@ -35,9 +36,6 @@ export default function LoginPage() {
const { mutate: updateMemberFcmTokenMutate } = useUpdateMemberFcmToken();
const search = useSearchParams();
const redirectUrl = search.get('redirect') ?? ROUTER.HOME;
// const onClickGuest = () => {
// router.push(ROUTER.GUEST.MISSION.NEW);
// };

const onClickAppleLogin = () => {
if (isWebView()) {
Expand Down Expand Up @@ -81,6 +79,11 @@ export default function LoginPage() {
if (data?.memberId) {
eventLogger.identify(data.memberId.toString());
}
if (data.landingStatus === 'TO_ONBOARDING') {
eventLogger.logEvent(EVENT_LOG_CATEGORY.ONBOARDING, EVENT_LOG_NAME.ONBOARDING.SUCCESS_SIGNUP);
router.push(ROUTER.ONBOARDING.HOME);
return;
}
router.push(redirectUrl);
},
},
Expand All @@ -103,6 +106,11 @@ export default function LoginPage() {
updateMemberFcmTokenMutate({ fcmToken: event.detail.data.deviceToken });
}
// 지금 당장은 필요없지만 나중을 위해 작동하도록 한다
if (data.landingStatus === 'TO_ONBOARDING') {
eventLogger.logEvent(EVENT_LOG_CATEGORY.ONBOARDING, EVENT_LOG_NAME.ONBOARDING.SUCCESS_SIGNUP);
router.push(ROUTER.ONBOARDING.HOME);
return;
}
router.push(redirectUrl);
},
onError: () => {
Expand Down
77 changes: 77 additions & 0 deletions src/app/onboarding/RecommendFollowItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import Button from '@/components/Button/Button';
import Thumbnail from '@/components/Thumbnail/Thumbnail';
import { css } from '@styled-system/css';

interface RecommendFollowItemProps {
id: number;
nickname: string;
profileImageUrl: string | null;
tags: string[];
onChangeFollow: (id: number) => void;
isFollowing: boolean;
}

function RecommendFollowItem({
profileImageUrl,
nickname,
tags,
onChangeFollow,
isFollowing,
id,
}: RecommendFollowItemProps) {
const handleFollow = () => {
onChangeFollow(id);
};

return (
<div className={followItemWrapperCss}>
<div className={leftWrapperCss}>
<Thumbnail size={'h36'} variant={'filled'} url={profileImageUrl} />
<div>
<p className={nicknameCss}>{nickname}</p>
<ul className={tagListCss}>
{tags.map((tag, index) => (
<li className={tagCss} key={index}>
{tag}
</li>
))}
</ul>
</div>
</div>
<Button onClick={handleFollow} size={'small'} variant={isFollowing ? 'secondary' : 'primary'}>
{isFollowing ? '팔로우' : '팔로잉'}
</Button>
</div>
);
}

export default React.memo(RecommendFollowItem);

const leftWrapperCss = css({
display: 'flex',
alignItems: 'center',
gap: '12px',
});

const nicknameCss = css({
textStyle: 'subtitle4',
color: 'text.secondary',
});
const tagListCss = css({
display: 'flex',
gap: '6px',
});

const tagCss = css({
textStyle: 'body6',
color: 'text.tertiary',
});

const followItemWrapperCss = css({
display: 'flex',
justifyContent: 'space-between',

width: '100%',
padding: '8px',
});
65 changes: 65 additions & 0 deletions src/app/onboarding/onboarding.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export const RECOMMENDATION = [
{
id: 41,
nickname: '수미미칩',
profileImageUrl: null,
tags: ['#기타연주', '#감사일기', '#개발자'],
},
{
id: 16,
nickname: '123',
profileImageUrl: 'https://kr.object.ncloudstorage.com/10mm-images/dev/member_profile/16/image.jpeg',
tags: ['#카페출근', '#출근독서', '#스타벅스'],
},
{
id: 15,
nickname: '1212333331212',
profileImageUrl: 'https://kr.object.ncloudstorage.com/10mm-images/dev/member_profile/15/image.jpeg',
tags: ['#카페출근', '#출근독서', '#스타벅스'],
},
];

export const RECOMMENDATION_REAL = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.env.node_env 애 떠러 REAL을 보여줄지 말지 컨트롤 하는건 어떨까요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node_env 인 dev 배포환경일때는 'production'으로 보이지 않나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEXT_PUBLIC_ENV 라는 환경변수를 따로 만들게요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cadc522

스크린샷 2024-03-28 오후 12 43 35

여기서 getEnv 함수 만들었고, 버셀에 환경변수 추가해주었습니다.

{
id: 4,
nickname: '도모',
profileImageUrl: 'https://image.10mm.today/prod/member_profile/4/38e6a5a9-a547-4051-95ea-cc112feabe21.jpeg',
tags: ['#기타연주', '#감사일기', '#개발자'],
},
{
id: 2,
nickname: 'ybchar',
profileImageUrl: 'https://image.10mm.today/prod/member_profile/2/2443459a-94b5-4048-98df-5fe356378a62.jpeg',
tags: ['#개발', '#산책하기'],
},
{
id: 73,
nickname: '안암위스키남',
profileImageUrl: 'https://image.10mm.today/prod/member_profile/73/a362d028-ad4c-49aa-9912-e32bc041793d.jpeg',
tags: ['#공부', '#헬스', '#디자인'],
},
{
id: 1,
nickname: '우보틀',
profileImageUrl: 'https://image.10mm.today/prod/member_profile/1/b83aeeb8-9acd-4915-aee4-ea87ec142b3b.jpeg',
tags: ['#카페출근', '#출근독서', '#스타벅스'],
},
{
id: 8,
nickname: '수미칩',
profileImageUrl: 'https://image.10mm.today/prod/member_profile/8/70d5cb9b-95ff-406f-93bd-9a68a2d8d5b9.png',
tags: ['#운동', '#뜨개질', '#폭주기관차'],
},
{
id: 7,
nickname: '유우비트',
profileImageUrl: 'https://image.10mm.today/prod/member_profile/7/9fbdf25a-101e-4fb5-9956-6030e7407382.jpeg',
tags: ['#운동', '#헬스', '#영화'],
},
{
id: 9,
nickname: '집가고시퍼',
profileImageUrl: 'https://image.10mm.today/prod/member_profile/9/722d875d-eff0-498d-9644-e1c8eb514414.jpeg',
tags: ['#디자인', '#회의'],
},
];
117 changes: 117 additions & 0 deletions src/app/onboarding/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { FOLLOW_API } from '@/apis/follow';
import { RECOMMENDATION, RECOMMENDATION_REAL } from '@/app/onboarding/onboarding.constants';
import RecommendFollowItem from '@/app/onboarding/RecommendFollowItem';
import Button from '@/components/Button/Button';
import CenterTextHeader from '@/components/Header/CenterTextHeader';
import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog';
import { ROUTER } from '@/constants/router';
import { eventLogger } from '@/utils';
import { getEnv } from '@/utils/appEnv';
import { css } from '@styled-system/css';

function OnboardingPage() {
const [followList, setFollowList] = useState<number[]>([]);
const router = useRouter();
const handleFollow = useCallback((id: number) => {
eventLogger.logEvent(EVENT_LOG_CATEGORY.ONBOARDING, EVENT_LOG_NAME.ONBOARDING.SELECT_FOLLOW);
setFollowList((prevState) => {
if (prevState.includes(id)) {
return prevState.filter((followId) => followId !== id);
}

return [...prevState, id];
});
}, []);

const isSkip = followList.length === 0;

const handleSkip = () => {
eventLogger.logEvent(EVENT_LOG_CATEGORY.ONBOARDING, EVENT_LOG_NAME.ONBOARDING.CLICK_SKIP);
router.replace(ROUTER.HOME);
};

const handleComplete = async () => {
await Promise.all(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

followList.map((id) => {
return FOLLOW_API.addFollow(id);
}),
);
eventLogger.logEvent(EVENT_LOG_CATEGORY.ONBOARDING, EVENT_LOG_NAME.ONBOARDING.CLICK_CONFIRM, {
followList: followList.join(','),
followCount: followList.length,
});
router.replace(ROUTER.HOME);
};

const follows = getEnv() === 'real' ? RECOMMENDATION_REAL : RECOMMENDATION;

return (
<div>
<CenterTextHeader
title={'추천 친구'}
rightComponent={
isSkip ? (
<Button variant={'ghost'} size={'medium'} onClick={handleSkip}>
건너뛰기
</Button>
) : (
<Button variant={'ghost'} size={'medium'} onClick={handleComplete}>
완료
</Button>
)
}
/>
<div className={textSectionCss}>
<img className={assetImagCss} src={'/assets/character/flag.png'} alt={'10mm character flag'} />
<p className={titleCss}>친구를 팔로우 해보세요!</p>
<p className={subTitleCss}>팔로우를 통해 미션과 인증을 공유할 수 있어요.</p>
</div>
<ul className={followListCss}>
{follows.map((props) => (
<RecommendFollowItem
key={props.id.toString()}
{...props}
isFollowing={followList.includes(props.id)}
onChangeFollow={handleFollow}
/>
))}
</ul>
</div>
);
}

export default OnboardingPage;

const followListCss = css({
padding: '0 16px',
});

const assetImagCss = css({
width: '112px',
height: '84px',
});

const titleCss = css({
textStyle: 'title3',
color: 'text.primary',
marginBottom: '4px',
});

const subTitleCss = css({
textStyle: 'body4',
color: 'gray.gray600',
});

const textSectionCss = css({
paddingTop: '28px',
paddingBottom: '56px',

display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
});
43 changes: 43 additions & 0 deletions src/components/Header/CenterTextHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { css, cx } from '@styled-system/css';
import { center } from '@styled-system/patterns';

interface CenterTextHeaderProps {
title: string;
rightComponent?: React.ReactNode;
}

function CenterTextHeader({ title, rightComponent }: CenterTextHeaderProps) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 임시로 사용하는 Header인거죠?
나중에도 사용하게 된다면 디자인 시스템에 추가하면 좋을 것 같네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 빠르게 작업하기 위해서 추가한 컴포넌트입니다

return (
<div className={headerWrapperCss}>
<div className={sectionWrapperCss} />
<div className={cx(sectionWrapperCss, centerCss)}>
<h2 className={titleCss}>{title}</h2>
</div>
<div className={cx(sectionWrapperCss, flexEndCss)}>{rightComponent}</div>
</div>
);
}

export default CenterTextHeader;

const headerWrapperCss = css({
height: '44px',
display: 'flex',
});

const titleCss = css({
color: 'text.primary',
textStyle: 'subtitle1',
});

const sectionWrapperCss = css({
width: '33%',
});

const centerCss = center();

const flexEndCss = css({
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
});
7 changes: 7 additions & 0 deletions src/constants/eventLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ export const EVENT_LOG_CATEGORY = {
FOLLOW_PROFILE: 'follow_profile',
FEED: 'feed',
REACTION: 'reaction',
ONBOARDING: 'onboarding',
};

type EventLogCategoryType = keyof typeof EVENT_LOG_CATEGORY;

export const EVENT_LOG_NAME = {
ONBOARDING: {
SUCCESS_SIGNUP: 'success/signUp',
SELECT_FOLLOW: 'select/follow',
CLICK_SKIP: 'click/skip',
CLICK_CONFIRM: 'click/confirm',
},
MISSION_DETAIL: {
CLICK_CALENDER_ARROW: 'click/calendarArrow',
CLICK_CALENDER: 'click/calendar',
Expand Down
Loading
Loading