Skip to content
This repository was archived by the owner on Nov 8, 2023. It is now read-only.

[feat] OAuth SignIn 구현 #42

Merged
merged 19 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
20618fa
#4 feat: Sign up data에 passwordConfirm field 추가
Kakamotobi Oct 19, 2023
39aee66
#4 feat: Sign up, 닉네임/이메일 중복검사 api 및 mock 추가
Kakamotobi Oct 19, 2023
eecf64c
#4 fix: useText 초기값 validate 적용
Kakamotobi Oct 19, 2023
d255467
#4 feat: NicknameSubPage "다음" 버튼 disabled 조건 적용
Kakamotobi Oct 19, 2023
bfbe37f
#4 feat: Email verification code api 및 mock 추가
Kakamotobi Oct 19, 2023
15228c8
#4 feat: Sign up subpage "다음" 버튼 disabled 조건 추가
Kakamotobi Oct 19, 2023
9ff9762
#4 feat: Signup nickname 중복 체크 기능 추가
Kakamotobi Oct 19, 2023
6e8cef7
#4 feat: Signup email 중복 체크 기능 추가
Kakamotobi Oct 19, 2023
2d85e91
#4 feat: Signup password confirm mismatch 에러 메시지 추가
Kakamotobi Oct 19, 2023
d009390
#4 feat: Signup email verification code 요청 추가
Kakamotobi Oct 19, 2023
0ec3377
#4 feat: Google SignIn 추가
Kakamotobi Oct 19, 2023
77bdfb7
#4 feat: Popup window 구현
Kakamotobi Oct 22, 2023
2472c39
#4 feat: Kakao 로그인 버튼 구현
Kakamotobi Oct 22, 2023
d08a930
#4 feat: Naver 로그인 버튼 구현
Kakamotobi Oct 23, 2023
09f2f46
#4 style: console.log 제거
Kakamotobi Oct 23, 2023
47ee0f3
#4 fix: Window.naver doesn't exist type error
Kakamotobi Oct 23, 2023
dc4f126
#4 refactor: KakaoSignInButton oAuthPopUpWindow type guard 적용
Kakamotobi Oct 23, 2023
a654e24
#4 refactor: Env variables 상수화
Kakamotobi Oct 23, 2023
07a3f10
Merge branch 'dev-fe' into fe/feat/#4-signup-page
Kakamotobi Oct 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions fe/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FineAnts</title>
<!-- Naver Login SDK -->
<script
src="https://static.nid.naver.com/js/naveridlogin_js_sdk_2.0.2.js"
defer></script>
</head>
<body>
<div id="root"></div>
Expand Down
10 changes: 10 additions & 0 deletions fe/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions fe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@lukemorales/query-key-factory": "^1.3.2",
"@mui/material": "^5.14.13",
"@mui/styled-engine-sc": "^6.0.0-alpha.1",
"@react-oauth/google": "^0.11.1",
"@tanstack/react-query": "^4.36.1",
"axios": "^1.5.1",
"lightweight-charts": "^4.1.0",
Expand Down
31 changes: 27 additions & 4 deletions fe/src/api/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type SignUpData = {
nickname: string;
email: string;
password: string;
passwordConfirm: string;
verificationCode: string;
};

Expand All @@ -31,8 +32,7 @@ type AccessTokenData = {
};

export const postSignUp = async (body: SignUpData) => {
console.log("body:", body);
const res = await fetcher.post<Response<null>>("/users", body);
const res = await fetcher.post<Response<null>>("/auth/signup", body);
return res.data;
};

Expand All @@ -41,12 +41,12 @@ export const postSignIn = async (body: SignInCredentials) => {
return res.data;
};

export const getOAuthSignIn = async (
export const postOAuthSignIn = async (
provider: OAuthProvider,
authCode: string
) => {
const res = await fetcher.get<Response<SignInData>>(
`/auth/oauth/${provider}?code=${authCode}`
`/auth/${provider}/login?code=${authCode}`
);
return res.data;
};
Expand Down Expand Up @@ -78,3 +78,26 @@ export const patchUserInfo = async (body: FormData) => {
});
return res.data;
};

export const postNicknameDuplicateCheck = async (nickname: string) => {
const res = await fetcher.post<Response<null>>(
"/auth/signup/duplicationcheck/nickname",
{ nickname }
);
return res.data;
};

export const postEmailDuplicateCheck = async (email: string) => {
const res = await fetcher.post<Response<null>>(
"/auth/signup/duplicationcheck/email",
{ email }
);
return res.data;
};

export const postEmailVerification = async (email: string) => {
const res = await fetcher.post<Response<null>>("/auth/signup/verifyEmail", {
email,
});
return res.data;
};
4 changes: 2 additions & 2 deletions fe/src/api/auth/queries/useOAuthSignInMutation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OAuthProvider, getOAuthSignIn } from "@api/auth";
import { OAuthProvider, postOAuthSignIn } from "@api/auth";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import Routes from "router/Routes";
Expand All @@ -15,7 +15,7 @@ export default function useOAuthSignInMutation() {
}: {
provider: OAuthProvider;
authCode: string;
}) => getOAuthSignIn(provider, authCode),
}) => postOAuthSignIn(provider, authCode),
onSuccess: ({ data }) => {
const { accessToken, refreshToken } = data;

Expand Down
6 changes: 1 addition & 5 deletions fe/src/api/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { BASE_API_URL } from "@constants/config";
import { refreshAccessToken } from "api/auth";
import { HTTPSTATUS } from "api/types";
import axios from "axios";
import Routes from "router/Routes";

const BASE_API_URL =
process.env.NODE_ENV === "production"
? import.meta.env.VITE_API_URL_PROD
: import.meta.env.VITE_API_URL_DEV;

export const fetcher = axios.create({
baseURL: `${BASE_API_URL}/api`,
headers: { "Content-Type": "application/json" },
Expand Down
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 fe/src/assets/images/kakao_login_medium_wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions fe/src/components/auth/GoogleSignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import useOAuthSignInMutation from "@api/auth/queries/useOAuthSignInMutation";
import { GoogleLogin } from "@react-oauth/google";

export default function GoogleSignInButton() {
const { mutate: oAuthSignInMutate } = useOAuthSignInMutation();

return (
// TODO: custom login button (`useGoogleLogin` "auth-code" flow)
<GoogleLogin
onSuccess={(credentialResponse) => {
const authCode = credentialResponse.credential;

if (authCode) {
oAuthSignInMutate({
provider: "google",
authCode,
});
}
}}
onError={() => {
// TODO: Handle error from Google
console.log("Login Failed");
}}
/>
);
}
39 changes: 39 additions & 0 deletions fe/src/components/auth/KakaoSignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import kakaoLoginButtonImage from "@assets/images/kakao_login_medium_wide.png";
import { CLIENT_URL, KAKAO_CLIENT_ID } from "@constants/config";
import { WindowContext } from "@context/WindowContext";
import openPopUpWindow from "@utils/openPopUpWindow";
import { useContext } from "react";
import { styled } from "styled-components";

export default function KakaoSignInButton() {
const { onOpenPopUpWindow } = useContext(WindowContext);

const onKakaoSignIn = () => {
const oAuthPopUpWindow = openPopUpWindow(
`https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${KAKAO_CLIENT_ID}&redirect_uri=${CLIENT_URL}/signin?provider=kakao`,
"kakaoOAuth",
500,
600
); // TODO: handle case where popup doesn't show (Ex: user blocked popups)

if (oAuthPopUpWindow) {
onOpenPopUpWindow(oAuthPopUpWindow);
}
};

return (
<StyledKakaoSignInButton type="button" onClick={onKakaoSignIn}>
<img src={kakaoLoginButtonImage} alt="카카오 로그인" />
</StyledKakaoSignInButton>
);
}

const StyledKakaoSignInButton = styled.button`
width: 228px;
height: 44px;

img {
width: 100%;
object-fit: fill;
}
`;
24 changes: 24 additions & 0 deletions fe/src/components/auth/NaverSignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CLIENT_URL, NAVER_CLIENT_ID } from "@constants/config";
import { useEffect } from "react";
import styled from "styled-components";

export default function NaverSignInButton() {
const naverLogin = new (window as any).naver.LoginWithNaverId({
clientId: NAVER_CLIENT_ID,
callbackUrl: `${CLIENT_URL}/signin?provider=naver`,
isPopup: true,
loginButton: {
color: "green",
type: 3,
height: 40,
},
});

useEffect(() => {
naverLogin.init();
}, []);

return <StyledNaverSignInButton id="naverIdLogin" />;
}

const StyledNaverSignInButton = styled.div``;
15 changes: 15 additions & 0 deletions fe/src/constants/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const CLIENT_URL =
process.env.NODE_ENV === "production"
? import.meta.env.VITE_CLIENT_URL_PROD
: import.meta.env.VITE_CLIENT_URL_DEV;

export const BASE_API_URL =
process.env.NODE_ENV === "production"
? import.meta.env.VITE_API_URL_PROD
: import.meta.env.VITE_API_URL_DEV;

export const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;

export const KAKAO_CLIENT_ID = import.meta.env.VITE_KAKAO_CLIENT_ID;

export const NAVER_CLIENT_ID = import.meta.env.VITE_NAVER_CLIENT_ID;
33 changes: 33 additions & 0 deletions fe/src/context/WindowContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ReactNode, createContext, useState } from "react";

export const WindowContext = createContext<{
popUpWindow: Window | null;
onOpenPopUpWindow: (targetWindow: Window) => void;
closePopUpWindow: () => void;
}>({
popUpWindow: null,
onOpenPopUpWindow: () => {},
closePopUpWindow: () => {},
});

export function WindowProvider({ children }: { children: ReactNode }) {
const [popUpWindow, setPopUpWindow] = useState<Window | null>(null);
Copy link
Member

Choose a reason for hiding this comment

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

const [popUpWindow, setPopUpWindow] = useState<Window>();

이렇게 초기값을 안주고 undefined로 사용할 수도 있는데 null을 사용하신 이유가 undefined와 null의 영어적 의미나 뜻 때문인가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

undefined의 정의("변수가 선언은 되었지만 아직 값을 할당받지 않았다")를 보았을 때는 undefined가 적절한 것 같지만, popup window를 닫을 때 setPopUpWindow(undefined)는 어색한 것 같아요. 저는 개인적으로 undefined는 처음에 변수를 선언할 때는 빈 값으로서 괜찮지만 후에 값이 생겼다가 없애는 경우에는 null("값이 없다")이 더 적절한 것 같아서 null을 사용했습니다.

https://stackoverflow.com/a/57249968/15330887

Copy link
Member

Choose a reason for hiding this comment

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

저도 setPopUpWindow(undefined) 처럼 undefined로 set하는다는게 조금 어색하게 느껴져서 물어본 질문 이였습니다👍


const onOpenPopUpWindow = (targetWindow: Window) => {
setPopUpWindow(targetWindow);
};

const closePopUpWindow = () => {
if (popUpWindow) {
popUpWindow.close();
setPopUpWindow(null);
}
};

return (
<WindowContext.Provider
value={{ popUpWindow, onOpenPopUpWindow, closePopUpWindow }}>
{children}
</WindowContext.Provider>
);
}
6 changes: 5 additions & 1 deletion fe/src/hooks/useText.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";

type Props = {
initialValue?: string;
Expand Down Expand Up @@ -31,6 +31,10 @@ export default function useText(options?: Props) {
setValue(newVal);
};

useEffect(() => {
onChange(initialValue);
}, []);

return {
value,
error,
Expand Down
2 changes: 1 addition & 1 deletion fe/src/mocks/browserServiceWorker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { setupWorker } from "msw";
import handlers from "./handlers/index";
import handlers from "./handlers";

export default setupWorker(...handlers);
62 changes: 62 additions & 0 deletions fe/src/mocks/data/authData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { HTTPSTATUS } from "@api/types";

export const successfulSignUpData = {
code: HTTPSTATUS.created,
status: "Created",
message: "회원가입이 완료되었습니다",
data: {
member: {
nickname: "Kakamotobi",
email: "[email protected]",
},
},
};

export const unsuccessfulSignUpData = {
code: HTTPSTATUS.badRequest,
status: "Bad Request",
message: "회원가입이 실패했습니다",
data: null,
};

export const successfulNicknameDuplicationCheckData = {
code: HTTPSTATUS.success,
status: "Success",
message: "닉네임이 사용 가능합니다",
data: null,
};

export const unsuccessfulNicknameDuplicationCheckData = {
code: HTTPSTATUS.badRequest,
status: "Bad Request",
message: "닉네임이 중복되었습니다",
data: null,
};

export const successfulEmailDuplicationCheckData = {
code: HTTPSTATUS.success,
status: "Success",
message: "이메일이 사용 가능합니다",
data: null,
};

export const unsuccessfulEmailDuplicationCheckData = {
code: HTTPSTATUS.badRequest,
status: "Bad Request",
message: "이메일이 중복되었습니다",
data: null,
};

export const successfulEmailVerificationData = {
code: HTTPSTATUS.success,
status: "Success",
message: "이메일로 검증코드를 전송하였습니다",
data: null,
};

export const unsuccessfulEmailVerificationData = {
code: HTTPSTATUS.badRequest,
status: "Bad Request",
message: "이메일 검증코드 전송을 실패했습니다",
data: null,
};
Loading