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

Feat/#5: 카카오 로그인 구현 #8

Merged
merged 9 commits into from
Dec 29, 2024
178 changes: 14 additions & 164 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,174 +1,24 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import toast from 'react-hot-toast';
import axiosInstance from '@/lib/axios';
import {
LoginWrapper,
ButtonSignupWrapper,
LogoContainer,
} from '@/app/(auth)/login/Login.styles';
import Logo from '@/styles/Icon/Logo.svg';
// import MainLayout from '@/styles/Icon/mainLayout.svg'; // 용량이 너무 크다. . .
import Image from 'next/image';
import Input from '@common/Input/Input';
import AutoLoginCheckbox from '@layout/Login/AutoLogin';
import Button from '@common/Button/Button';
import SignupTabs from '@layout/Login/SignUpTabs';
import useUserStore from '@/stores/useUserStore';
import ValidationMessage from '@/components/Common/ValidationMessage/ValidationMessage';
import { signIn, signOut, useSession } from 'next-auth/react';
import { LoginWrapper } from './Login.styles';

interface LoginError {
email: string;
password: string;
}

const Login = () => {
const router = useRouter();
const { setNickname, setEmail } = useUserStore();

const [isHiddenPassword, setIsHiddenPassword] = useState(true);
const [emailInput, setEmailInput] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<LoginError>({ email: '', password: '' });
const [isLoading, setIsLoading] = useState(false);
const [isFormValid, setIsFormValid] = useState(false);

const handlePasswordVisibility = () => {
setIsHiddenPassword((prev) => !prev);
};

const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};

const validateForm = (): boolean => {
let isValid = true;
const newErrors = { email: '', password: '' };

if (emailInput) {
if (!validateEmail(emailInput)) {
newErrors.email = '이메일 형식이 올바르지 않습니다 (예: [email protected])';
isValid = false;
}
}

if (!emailInput || !password) {
isValid = false;
}

setErrors(newErrors);
setIsFormValid(isValid);
return isValid;
};

const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();

if (!validateForm()) {
return;
}

setIsLoading(true);
try {
const response = await axiosInstance.post('/api/login', {
email: emailInput,
password,
});

const nickname = response.data;

setEmail(emailInput);
setNickname(nickname);

localStorage.setItem('userId', emailInput);
toast.success(`안녕하세요 ${nickname}님 환영합니다!`);
router.push('/home');
} catch (error) {
console.error('Login error:', error);
setErrors({
email: '',
password: '이메일 또는 비밀번호가 올바르지 않습니다.',
});
setIsFormValid(false);
} finally {
setIsLoading(false);
}
};

const handleEmailChange = (value: string) => {
setEmailInput(value);
if (errors.email) {
setErrors({ ...errors, email: '' });
}

if (value && !validateEmail(value)) {
setErrors((prev) => ({
...prev,
email: '이메일 형식이 올바르지 않습니다 (예: [email protected])',
}));
setIsFormValid(false);
} else {
setIsFormValid(!!value && !!password);
}
};

const handlePasswordChange = (value: string) => {
setPassword(value);
setErrors({ ...errors, password: '' });
setIsFormValid(!!value && !!emailInput && validateEmail(emailInput));
};
const LoginPage = () => {
const { data: session } = useSession();
console.log(session?.accessToken);
Comment on lines +6 to +8
Copy link
Member Author

Choose a reason for hiding this comment

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

useSession을 통해 accessToken에 접근 가능합니다.


return (
<LoginWrapper>
<LogoContainer>
<Image src={Logo} alt="logo" priority />
{/* <Image src={MainLayout} alt="mainLayout" /> */}
</LogoContainer>
<form onSubmit={handleLogin} autoComplete="off">
<Input
title="이메일"
placeholder="이메일을 입력해주세요"
onChange={handleEmailChange}
autoComplete="new-email"
validation={
errors.email ? (
<ValidationMessage message={errors.email} type="error" />
) : null
}
/>
<Input
title="비밀번호"
placeholder="비밀번호를 입력해주세요"
isButton={true}
isHiddenPassword={isHiddenPassword}
handleClick={handlePasswordVisibility}
onChange={handlePasswordChange}
type={isHiddenPassword ? 'password' : 'text'}
autoComplete="new-password"
validation={
errors.password ? (
<ValidationMessage message={errors.password} type="error" />
) : null
}
/>
<AutoLoginCheckbox />
<ButtonSignupWrapper>
<Button
buttonType={isFormValid ? 'purple' : 'gray'}
buttonSize="large"
buttonHeight="default"
styleType="coloredBackground"
label="로그인"
disabled={isLoading}
/>
<SignupTabs />
</ButtonSignupWrapper>
</form>
{session ? (
<>
<p>{session.user?.email}님 환영합니다</p>
<button onClick={() => signOut()}>logout</button>
</>
) : (
<button onClick={() => signIn('kakao')}>login</button>
)}
</LoginWrapper>
);
};

export default Login;
export default LoginPage;
36 changes: 36 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import NextAuth, { NextAuthOptions } from 'next-auth';
import KakaoProvider from 'next-auth/providers/kakao';

const authOptions: NextAuthOptions = {
providers: [
KakaoProvider({
clientId: process.env.KAKAO_CLIENT_ID as string,
clientSecret: process.env.KAKAO_CLIENT_SECRET as string,
}),
],
session: {
strategy: 'jwt', //JWT 기반 인증
maxAge: 24 * 60 * 60, // 24시간
},
secret: process.env.NEXTAUTH_SECRET, //JWT 암호화 키 설정
callbacks: {
async signIn({ user }) {
return !!user.email;
},
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
return session;
},
},
};
Comment on lines +4 to +32
Copy link
Member Author

@minjeongss minjeongss Dec 28, 2024

Choose a reason for hiding this comment

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

  • providers: 카카오만 등록되어 있고, 이곳에 애플을 등록하시면 됩니다.
  • session, secret: jwt관련 설정
  • callbacks
    • jwt: token 등록
    • session: session 등록
    • signIn: user의 email이 없다면 로그인에 실패하도록 설정했습니다. (저희는 email로 구분을 하니까요!)

auth에 대한 구조를 위와 같이 구성했습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

구조 확인했어요 ! 정리해주신덕에 조금은 수월하게 작업하지 않을까 싶네요 !
구분자 == 이메일 잊지않고 마저 로그인 구현하겠습니다아 🤍


const handler = NextAuth(authOptions);

export { authOptions, handler as GET, handler as POST };
49 changes: 26 additions & 23 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Container from '@/styles/Container';
import GlobalStyle from '@/styles/GlobalStyle';
import { Toaster } from 'react-hot-toast';
import colors from '@styles/color/palette';
import SessionWrapper from '@/components/Layout/Login/SessionWrapper';

export { metadata };
export { viewport };
Expand All @@ -15,29 +16,31 @@ export default function RootLayout({
children: ReactNode;
}>) {
return (
<html lang="ko">
<body suppressHydrationWarning>
<StyledComponentsRegistry>
<GlobalStyle />
<Container>{children}</Container>
<Toaster
position="bottom-center"
toastOptions={{
error: {
duration: 2000,
style: {
background: colors.etc.white,
color: colors.etc.black,
fontSize: '14px',
padding: '12px 20px',
borderRadius: '4px',
maxWidth: '280px',
<SessionWrapper>
<html lang="ko">
<body suppressHydrationWarning>
<StyledComponentsRegistry>
<GlobalStyle />
<Container>{children}</Container>
<Toaster
position="bottom-center"
toastOptions={{
error: {
duration: 2000,
style: {
background: colors.etc.white,
color: colors.etc.black,
fontSize: '14px',
padding: '12px 20px',
borderRadius: '4px',
maxWidth: '280px',
},
},
},
}}
/>
</StyledComponentsRegistry>
</body>
</html>
}}
/>
</StyledComponentsRegistry>
</body>
</html>
</SessionWrapper>
);
}
8 changes: 8 additions & 0 deletions components/Layout/Login/SessionWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';
import { SessionProvider } from 'next-auth/react';

const SessionWrapper = ({ children }: { children: React.ReactNode }) => {
return <SessionProvider>{children}</SessionProvider>;
};

export default SessionWrapper;
12 changes: 12 additions & 0 deletions lib/types/next-auth.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'next-auth';

declare module 'next-auth' {
interface Session {
accessToken?: string;
}

interface JWT {
accessToken?: string;
refreshToken?: string;
}
}
Loading