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 (
+
+
+ 건너뛰기
+
+ ) : (
+
+ )
+ }
+ />
+
+

+
친구를 팔로우 해보세요!
+
팔로우를 통해 미션과 인증을 공유할 수 있어요.
+
+
+ {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';
+};