-
+
- {Array(4)
+ {Array(3)
.fill(0)
.map((_, i) => (
))}
-
- 로그인 하기
-
+
-
- 30초만에 회원가입 하기
+
+ 이메일로 로그인
+
+
+ {isRegistered && (
+
+ )}
+
+
);
diff --git a/src/app/(form)/join/page.tsx b/src/app/(form)/join/page.tsx
index 76d9bf3c..4bbaa3af 100644
--- a/src/app/(form)/join/page.tsx
+++ b/src/app/(form)/join/page.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { JoinForm } from '@/features/Join';
import { Metadata } from 'next';
+import GoogleJoinForm from '@/features/Join/components/GoogleJoinForm';
export const metadata: Metadata = {
title: '회원가입',
@@ -12,8 +13,10 @@ export const metadata: Metadata = {
},
};
-function JoinPage() {
- return
;
+function JoinPage({ searchParams }: { searchParams: { type: string } }) {
+ const { type } = searchParams;
+
+ return type === 'google' ?
:
;
}
export default JoinPage;
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index 96ebfcd9..2a8603ce 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -8,6 +8,7 @@ import { getButtonColor } from '@/utils/getButtonColor';
interface Props extends HTMLMotionProps<'button'> {
children: React.ReactNode;
theme?: 'normal' | 'error' | 'gray' | 'light' | string;
+ extraClass?: string;
}
/** 기본 버튼 컴포넌트
@@ -18,6 +19,7 @@ function Button({
type = 'button',
disabled = false,
theme = 'normal',
+ extraClass,
...props
}: Props) {
const buttonType = getButtonColor(theme);
@@ -26,7 +28,7 @@ function Button({
-
- value.length === 0 ||
- value === watch('password') ||
- '비밀번호가 일치하지 않아요.',
- },
- onChange: async (e) => {
- const { value } = e.target;
- if (value.length > 0) {
- await trigger('passwordCheck');
- }
},
})}
/>
diff --git a/src/components/OnBoarding/OnBoardingSlide.tsx b/src/components/OnBoarding/OnBoardingSlide.tsx
index 20b473f1..026fbce5 100644
--- a/src/components/OnBoarding/OnBoardingSlide.tsx
+++ b/src/components/OnBoarding/OnBoardingSlide.tsx
@@ -5,7 +5,7 @@ import Slider, { Settings } from 'react-slick';
import Slide1 from 'public/images/onBoarding/slide-1.svg';
import Slide2 from 'public/images/onBoarding/slide-2.svg';
import Slide3 from 'public/images/onBoarding/slide-3.svg';
-import Slide4 from 'public/images/onBoarding/slide-4.svg';
+import LightBg from 'public/images/flash/light-bg-onboarding.svg';
import BackButton from '../Button/BackButton';
interface Props {
@@ -23,73 +23,51 @@ function OnBoardingSlide({ setActiveSlide }: Props) {
slidesToShow: 1,
slidesToScroll: 1,
autoplay: false,
+ adaptiveHeight: true,
beforeChange: (_, next) => {
setActiveSlide(next + 1);
},
};
return (
-
+
router.back()}
extraClass='px-page'
/>
-
-
-
-
- {onBoarding[1].title()}
-
-
-
-
+
+
+
+ {onBoarding[1].title()}
+
+
+
-
-
-
-
- {onBoarding[2].title()}
-
-
-
-
+
+
-
-
-
- {onBoarding[3].title()}
-
-
-
-
+
+
+ {onBoarding[2].title()}
+
+
+
+
+
+
-
-
-
- {onBoarding[4].title()}
-
-
-
-
+
+
+ {onBoarding[3].title()}
+
+
+
+
+
+
diff --git a/src/components/Profile/GenderSelector.tsx b/src/components/Profile/GenderSelector.tsx
index 49e537ff..cd5bd242 100644
--- a/src/components/Profile/GenderSelector.tsx
+++ b/src/components/Profile/GenderSelector.tsx
@@ -27,7 +27,7 @@ function GenderSelector({ theme }: Props) {
diff --git a/src/components/Profile/ProfileForm.tsx b/src/components/Profile/ProfileForm.tsx
index d7499907..4d80e7bc 100644
--- a/src/components/Profile/ProfileForm.tsx
+++ b/src/components/Profile/ProfileForm.tsx
@@ -21,7 +21,6 @@ function ProfileForm({ type = 'join', theme = 'dark', username }: Props) {
return (
-
diff --git a/src/constants/jobs.ts b/src/constants/jobs.ts
index 68221335..2705b10b 100644
--- a/src/constants/jobs.ts
+++ b/src/constants/jobs.ts
@@ -1,4 +1,5 @@
export const JOBS = [
+ { id: 0, value: '선택' },
{ id: 1, value: '학생' },
{ id: 2, value: '기획/마케팅/경영' },
{ id: 3, value: '디자인/크리에이티브' },
diff --git a/src/data/ui/onBoarding.tsx b/src/data/ui/onBoarding.tsx
index 6f70b6b0..7704abd9 100644
--- a/src/data/ui/onBoarding.tsx
+++ b/src/data/ui/onBoarding.tsx
@@ -8,36 +8,33 @@ export const onBoarding: OnBoarding = {
1: {
title: () => (
<>
- 우물에 책을 추가해
+ 독서 기록으로
- 탈출하세요!
+ 개구리를
+
+ 우물에서 구출하세요
>
),
},
2: {
title: () => (
<>
- 다양한 캐릭터로
+ 독서 친구들과
+
+ 소통해
- 우물을 꾸며요!
+ 시야를 확장해요
>
),
},
3: {
title: () => (
<>
- 나의 독서 성향에
+ 기록해 얻은
- 맞는 책을 추천해줘요!
- >
- ),
- },
- 4: {
- title: () => (
- <>
- 내 독서기록을
+ 포인트로
- 공유하고 소통해요!
+ 다양한 개구리를 획득해요
>
),
},
diff --git a/src/features/Join/api/join.api.ts b/src/features/Join/api/join.api.ts
index 3e4345af..c80b17ff 100644
--- a/src/features/Join/api/join.api.ts
+++ b/src/features/Join/api/join.api.ts
@@ -4,6 +4,8 @@ import {
GetUsernameAvailabilityReq,
SignUp,
SignUpReq,
+ SignInGoogle,
+ SignInGoogleReq,
} from '@frolog/frolog-api';
export const signUp = async (formData: SignUpReq) => {
@@ -15,3 +17,8 @@ export const checkNickname = async (req: GetUsernameAvailabilityReq) => {
const data = await new GetUsernameAvailability(baseOptions).fetch(req);
return data.result;
};
+
+export const googleSignIn = async (req: SignInGoogleReq) => {
+ const data = await new SignInGoogle(baseOptions).fetch(req);
+ return data;
+};
diff --git a/src/features/Join/components/GoogleJoinForm.tsx b/src/features/Join/components/GoogleJoinForm.tsx
new file mode 100644
index 00000000..5dc0e7be
--- /dev/null
+++ b/src/features/Join/components/GoogleJoinForm.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { STORAGE_KEY } from '@/constants/storage';
+import { FormProvider, useForm } from 'react-hook-form';
+import LoadingOverlay from '@/components/Spinner/LoadingOverlay';
+import { JoinForm as JoinFormType } from '../types/form';
+import Step4 from './step4/Step4';
+import Step1 from './step1/Step1';
+import { defaultValue } from '../data/joinForm';
+import { useJoin } from '../hooks/useJoin';
+
+function GoogleJoinForm() {
+ const methods = useForm({
+ mode: 'onBlur',
+ defaultValues:
+ typeof window !== 'undefined' &&
+ localStorage.getItem(STORAGE_KEY.joinFormKey)
+ ? JSON.parse(localStorage.getItem(STORAGE_KEY.joinFormKey)!)
+ : defaultValue,
+ });
+ const { getValues, handleSubmit } = methods;
+ const { joinUser, step, isLoading } = useJoin(getValues);
+
+ return (
+
+
+ {isLoading && }
+
+ );
+}
+
+export default GoogleJoinForm;
diff --git a/src/features/Join/components/step2/Step2.tsx b/src/features/Join/components/step2/Step2.tsx
index 724acbf3..4fb80b58 100644
--- a/src/features/Join/components/step2/Step2.tsx
+++ b/src/features/Join/components/step2/Step2.tsx
@@ -23,12 +23,11 @@ function Step2() {
} = useFormContext();
const email = watch('email');
const password = watch('password');
- const passwordCheck = watch('passwordCheck');
useEffect(() => {
- const disabled = Boolean(!email || !password || !passwordCheck || !isValid);
+ const disabled = Boolean(!email || !password || !isValid);
setIsDisabled(disabled);
- }, [email, password, passwordCheck, isValid, errors]);
+ }, [email, password, isValid, errors]);
return (
<>
diff --git a/src/features/Join/components/step4/Step4.tsx b/src/features/Join/components/step4/Step4.tsx
index bd5aadbc..0e9fb30a 100644
--- a/src/features/Join/components/step4/Step4.tsx
+++ b/src/features/Join/components/step4/Step4.tsx
@@ -1,26 +1,31 @@
'use client';
import React from 'react';
-import { useFormContext } from 'react-hook-form';
import Button from '@/components/Button/Button';
import ProfileForm from '@/components/Profile/ProfileForm';
+import { useFormContext } from 'react-hook-form';
/** 회원가입 4단계: 정보 입력 폼 */
function Step4() {
- const {
- watch,
- formState: { errors },
- } = useFormContext();
+ const { watch } = useFormContext();
+ const job = watch('personal_infos.occupation.value');
+ const gender = watch('personal_infos.gender.value');
+ const birthDate = watch('personal_infos.birth_date.value');
+ const isDisabled = Boolean(!gender || job === '선택' || !birthDate);
return (
<>
-
+
+
+ 맞춤 정보를 바탕으로
+
+ 책과 친구를 추천할게요!
+
+
+
>
);
}
diff --git a/src/features/Join/data/joinForm.ts b/src/features/Join/data/joinForm.ts
index ddbb5c2f..e4778991 100644
--- a/src/features/Join/data/joinForm.ts
+++ b/src/features/Join/data/joinForm.ts
@@ -4,8 +4,6 @@ import { JoinForm } from '../types/form';
export const defaultValue: JoinForm = {
email: '',
password: '',
- passwordCheck: '',
- username: '',
consents: {
age: {
@@ -32,17 +30,16 @@ export const defaultValue: JoinForm = {
personal_infos: {
occupation: {
- value: '학생',
+ value: '선택',
visibility: true,
},
+ gender: {
+ visibility: false,
+ },
birth_date: {
value: getMinDate(),
visibility: true,
},
- gender: {
- value: '남성',
- visibility: true,
- },
},
};
diff --git a/src/features/Join/hooks/useGoogle.ts b/src/features/Join/hooks/useGoogle.ts
new file mode 100644
index 00000000..aead185c
--- /dev/null
+++ b/src/features/Join/hooks/useGoogle.ts
@@ -0,0 +1,72 @@
+import { useMutation } from '@tanstack/react-query';
+import { SignInGoogleReq, SignInGoogleRes } from '@frolog/frolog-api';
+import { useRouter } from 'next/navigation';
+import { PAGES } from '@/constants/page';
+import { STORAGE_KEY } from '@/constants/storage';
+import { defaultValue } from '@/features/Join/data/joinForm';
+import { useAuthActions } from '@/store/authStore';
+import { signIn } from 'next-auth/react';
+import { useState } from 'react';
+import { toast } from '@/modules/Toast';
+import { ERROR_ALERT } from '@/constants/message';
+import { googleSignIn } from '../api/join.api';
+
+export const useGoogle = () => {
+ const router = useRouter();
+ const { setEmailVerifiedToken } = useAuthActions();
+ const [isRegistered, setIsRegistered] = useState(false);
+
+ const { mutate: handleGoogleSignIn } = useMutation<
+ SignInGoogleRes,
+ Error,
+ SignInGoogleReq
+ >({
+ mutationFn: async (req: SignInGoogleReq) => {
+ const res = await googleSignIn(req);
+ return res;
+ },
+ onSuccess: async (res) => {
+ if (res.is_registered && res.login_type === 'local') {
+ setIsRegistered(true);
+ setTimeout(() => {
+ setIsRegistered(false);
+ }, 2000);
+ return;
+ }
+
+ if (!res.result && !res.is_registered) {
+ setEmailVerifiedToken(res.email_verified_token!);
+ localStorage.setItem(
+ STORAGE_KEY.joinFormKey,
+ JSON.stringify({ ...defaultValue, email: res.email })
+ );
+ router.push(`${PAGES.JOIN}?type=google`);
+ return;
+ }
+
+ if (res.result && res.is_registered) {
+ const result = await signIn('credentials', {
+ isGoogle: true,
+ id: res.id,
+ result: res.result,
+ redirect: false,
+ email: res.email,
+ password: '',
+ isRemember: true,
+ accessToken: res.access_token,
+ refreshToken: res.refresh_token,
+ });
+
+ if (result?.ok) {
+ router.push(PAGES.HOME);
+ router.refresh();
+ }
+ }
+ },
+ onError: () => {
+ toast.error(ERROR_ALERT);
+ },
+ });
+
+ return { handleGoogleSignIn, isRegistered };
+};
diff --git a/src/features/Join/hooks/useJoin.ts b/src/features/Join/hooks/useJoin.ts
index 58434811..0dd58129 100644
--- a/src/features/Join/hooks/useJoin.ts
+++ b/src/features/Join/hooks/useJoin.ts
@@ -26,12 +26,16 @@ export const useJoin = (getValues: () => JoinForm) => {
/** 로그인 처리 핸들러 */
const { mutate: handleLogin } = useMutation({
- mutationFn: async (username: string) => {
+ mutationFn: async (social_verified_token?: string) => {
const account = localStorage.getItem(STORAGE_KEY.tempAccountKey);
if (account) {
- const res = await userLogin(JSON.parse(account));
- router.replace(`${PAGES.JOIN_FINISH}?username=${username}`);
+ const res = await userLogin({
+ ...JSON.parse(account),
+ social_verified_token,
+ });
+
+ router.replace(PAGES.JOIN_FINISH);
return res;
} else {
throw new Error();
@@ -71,21 +75,24 @@ export const useJoin = (getValues: () => JoinForm) => {
mutationFn: async (formData: SignUpReq) => {
setIsLoading(true);
const res = await signUp(formData);
+
return res;
},
onError: () => {
toast.error(ERROR_ALERT);
setIsLoading(false);
},
- onSuccess: (_result, formData) => {
+ onSuccess: (result, formData) => {
resetToken();
localStorage.removeItem(STORAGE_KEY.joinFormKey);
localStorage.setItem(
STORAGE_KEY.tempAccountKey,
- JSON.stringify({ email: formData.email, password: formData.password })
+ JSON.stringify({
+ email: formData.email,
+ password: formData.password || undefined,
+ })
);
- // todo: 회원 가입후 로그인 처리
- handleLogin('dev test');
+ handleLogin(result.social_verified_token);
},
});
diff --git a/src/features/Join/types/form.d.ts b/src/features/Join/types/form.d.ts
index 3072b583..e2ce662b 100644
--- a/src/features/Join/types/form.d.ts
+++ b/src/features/Join/types/form.d.ts
@@ -21,9 +21,7 @@ export interface Info {
export interface JoinForm {
email: string;
- password: string;
- passwordCheck: string;
- username: string | null;
+ password?: string;
// 약관 동의 리스트(Array)
consents: {
diff --git a/src/features/Join/utils/transformJoinForm.ts b/src/features/Join/utils/transformJoinForm.ts
index c756ab0a..ea7b17ec 100644
--- a/src/features/Join/utils/transformJoinForm.ts
+++ b/src/features/Join/utils/transformJoinForm.ts
@@ -17,8 +17,7 @@ export const transformJoinForm = (
return {
email: joinFormData.email,
email_verified_token,
- password: joinFormData.password,
- username: joinFormData.username!,
+ password: joinFormData.password || undefined,
consents: transformedConsents,
personal_infos: transformeInfoToArray(joinFormData.personal_infos),
};
diff --git a/src/features/Login/hooks/useLogin.ts b/src/features/Login/hooks/useLogin.ts
index 57e2ee27..37ff9c39 100644
--- a/src/features/Login/hooks/useLogin.ts
+++ b/src/features/Login/hooks/useLogin.ts
@@ -26,6 +26,7 @@ export const useLogin = (type: 'login' | 'test') => {
email: data.email,
password: data.password,
isRemember: isSaved,
+ social_verified_token: data.social_verified_token,
});
if (result?.ok) {
diff --git a/src/features/Login/types/login.d.ts b/src/features/Login/types/login.d.ts
index 71125af3..499030b6 100644
--- a/src/features/Login/types/login.d.ts
+++ b/src/features/Login/types/login.d.ts
@@ -1,4 +1,5 @@
export interface LoginForm {
email: string;
- password: string;
+ password?: string;
+ social_verified_token?: string;
}
diff --git a/src/middleware.ts b/src/middleware.ts
index 96c24bf6..e3348d7e 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,4 +1,4 @@
-import { RefreshToken, RefreshTokenRes } from '@frolog/frolog-api';
+import { RefreshTokenRes } from '@frolog/frolog-api';
import { encode, getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
@@ -7,15 +7,23 @@ import { getExpFromToken } from './utils/auth/decodeToken';
const protectedRoutes: string[] = [
'/frolog-test',
'/profile',
- '/join/finish',
'/flash',
'/well',
'/comments',
'/new-memo',
'/new-review',
+ '/join/finish',
'/quit',
'/terms',
'/store',
+ '/feed',
+ '/search',
+ '/memo',
+ '/review',
+ '/explore',
+ '/book',
+ '/search-home',
+ '/mission',
]; // 로그인이 필요한 페이지 목록
const publicRoutes: string[] = [
'/onboarding',
diff --git a/src/styles/components.css b/src/styles/components.css
index 7daf34b2..12834b12 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -26,7 +26,7 @@
@apply h-[8px] w-[8px] rounded-[50%] bg-main;
}
.non-active-circle {
- @apply h-[8px] w-[8px] rounded-[50%] border-[1.2px] border-gray-600;
+ @apply h-[8px] w-[8px] rounded-[50%] border-[1.2px] border-gray-500;
}
.add-button-wrapper {
@apply w-full px-page pb-[24px];
diff --git a/src/utils/auth/nextAuth.ts b/src/utils/auth/nextAuth.ts
index 62b06192..eed0dc56 100644
--- a/src/utils/auth/nextAuth.ts
+++ b/src/utils/auth/nextAuth.ts
@@ -17,14 +17,31 @@ export const authOptions: NextAuthOptions = {
email: { label: 'Email' },
password: { label: 'Password' },
isRemember: { label: 'isRemember' },
+ isGoogle: { label: 'isGoogle' },
+ id: { label: 'id' },
+ result: { label: 'result' },
+ accessToken: { label: 'accessToken' },
+ refreshToken: { label: 'refreshToken' },
+ social_verified_token: { label: 'social_verified_token' },
},
async authorize(credentials) {
if (!credentials) return null;
- const data = await logIn.fetch({
- email: credentials.email,
- password: credentials.password,
- });
+ const data = credentials.isGoogle
+ ? {
+ id: credentials.id,
+ result: credentials.result,
+ access_token: credentials.accessToken,
+ refresh_token: credentials.refreshToken,
+ }
+ : await logIn.fetch({
+ email: credentials.email,
+ password: credentials.password,
+ social_verified_token:
+ credentials.social_verified_token === 'undefined'
+ ? undefined
+ : credentials.social_verified_token,
+ });
// 기본 우물 확인
let defaultWellId = null;