diff --git a/public/assets/character/basic.png b/public/assets/character/basic.png new file mode 100644 index 00000000..6078f620 Binary files /dev/null and b/public/assets/character/basic.png differ diff --git a/public/assets/character/flag.png b/public/assets/character/flag.png new file mode 100644 index 00000000..598d59d8 Binary files /dev/null and b/public/assets/character/flag.png differ diff --git a/public/assets/character/sad.png b/public/assets/character/sad.png new file mode 100644 index 00000000..53a257b3 Binary files /dev/null and b/public/assets/character/sad.png differ diff --git a/src/apis/auth.ts b/src/apis/auth.ts index 88e7eb96..e2337387 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -18,6 +18,7 @@ interface LoginResponse { accessToken: string; refreshToken: string; memberId?: number; + landingStatus?: 'TO_MAIN' | 'TO_ONBOARDING'; } interface RegisterRequest { diff --git a/src/app/auth/kakaoCallback/page.tsx b/src/app/auth/kakaoCallback/page.tsx index 4df463c5..6eecea38 100644 --- a/src/app/auth/kakaoCallback/page.tsx +++ b/src/app/auth/kakaoCallback/page.tsx @@ -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'; @@ -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); }, }, diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 9932bb6e..9e8990c8 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -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'; @@ -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()) { @@ -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); }, }, @@ -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: () => { diff --git a/src/app/onboarding/RecommendFollowItem.tsx b/src/app/onboarding/RecommendFollowItem.tsx new file mode 100644 index 00000000..d740f08d --- /dev/null +++ b/src/app/onboarding/RecommendFollowItem.tsx @@ -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 ( +
+
+ +
+

{nickname}

+
    + {tags.map((tag, index) => ( +
  • + {tag} +
  • + ))} +
+
+
+ +
+ ); +} + +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', +}); diff --git a/src/app/onboarding/onboarding.constants.ts b/src/app/onboarding/onboarding.constants.ts new file mode 100644 index 00000000..c5ffefed --- /dev/null +++ b/src/app/onboarding/onboarding.constants.ts @@ -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 = [ + { + 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: ['#디자인', '#회의'], + }, +]; diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx new file mode 100644 index 00000000..de4a387f --- /dev/null +++ b/src/app/onboarding/page.tsx @@ -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([]); + 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( + 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 ( +
+ + 건너뛰기 + + ) : ( + + ) + } + /> +
+ {'10mm +

친구를 팔로우 해보세요!

+

팔로우를 통해 미션과 인증을 공유할 수 있어요.

+
+
    + {follows.map((props) => ( + + ))} +
+
+ ); +} + +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', +}); diff --git a/src/components/Header/CenterTextHeader.tsx b/src/components/Header/CenterTextHeader.tsx new file mode 100644 index 00000000..e07c52fa --- /dev/null +++ b/src/components/Header/CenterTextHeader.tsx @@ -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) { + return ( +
+
+
+

{title}

+
+
{rightComponent}
+
+ ); +} + +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', +}); diff --git a/src/constants/eventLog.ts b/src/constants/eventLog.ts index c596afde..843881d4 100644 --- a/src/constants/eventLog.ts +++ b/src/constants/eventLog.ts @@ -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', diff --git a/src/constants/router.ts b/src/constants/router.ts index 15dd2430..cebbe152 100644 --- a/src/constants/router.ts +++ b/src/constants/router.ts @@ -12,7 +12,9 @@ export const ROUTER = { FEED: { HOME: '/feed', }, - + ONBOARDING: { + HOME: '/onboarding', + }, RECORD: { CREATE: (id: string) => `/record/${id}`, SUCCESS: `/record/success`, // TODO: 여기있는것이 맞ㄴ느가? diff --git a/src/utils/appEnv.ts b/src/utils/appEnv.ts index 85cb1c0c..49c1cd00 100644 --- a/src/utils/appEnv.ts +++ b/src/utils/appEnv.ts @@ -11,3 +11,17 @@ const getUserAgent = () => { export const isWebView = () => RegExp(APP_USER_AGENT).test(getUserAgent()); export const isAndroid = () => RegExp(ANDROID).test(getUserAgent()); export const isIOS = () => RegExp(IOS).test(getUserAgent()); + +type EnvType = 'local' | 'dev' | 'real'; + +export const getEnv = (): EnvType => { + if (process.env.node_env === 'development') { + return 'local'; + } + + if (process.env.NEXT_PUBLIC_ENV === 'real') { + return 'real'; + } + + return 'dev'; +};