From 89afca8e04bc15f7c3c17120f1889e6d8a0682e0 Mon Sep 17 00:00:00 2001 From: Yun-Changseop Date: Tue, 20 Aug 2024 03:14:25 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Feature=20-=20SignUp,=20SignIn=20AP?= =?UTF-8?q?I=20Connection=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Sign In API Connection * feat: Sign Up API Connection --- package-lock.json | 16 +++++ package.json | 2 + src/api/axios.ts | 51 ++++++++++++++- .../auth/sign-in/sign-in-form/SignInForm.tsx | 26 ++++++-- .../auth/sign-up/sign-up-form/SignUpForm.tsx | 62 ++++++++++++++++--- src/router.tsx | 6 +- 6 files changed, 145 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index aea4538..c39a96f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^1.7.4", + "js-cookie": "^3.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.3.0", @@ -20,6 +21,7 @@ "styled-components": "^6.1.12" }, "devDependencies": { + "@types/js-cookie": "^3.0.6", "@types/node": "^22.4.1", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -961,6 +963,12 @@ "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", "dev": true }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2278,6 +2286,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 364f75f..b364e45 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.7.4", + "js-cookie": "^3.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.3.0", @@ -22,6 +23,7 @@ "styled-components": "^6.1.12" }, "devDependencies": { + "@types/js-cookie": "^3.0.6", "@types/node": "^22.4.1", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", diff --git a/src/api/axios.ts b/src/api/axios.ts index 5c35190..35c5eef 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -1,14 +1,63 @@ import axios from "axios"; +import Cookie from "js-cookie"; +import Cookies from "js-cookie"; const { VITE_APP_SERVER_PORT } = import.meta.env; +const accessToken = Cookies.get('access_token'); + + const instance = axios.create({ - baseURL: VITE_APP_SERVER_PORT + "/api/v1/", + baseURL: VITE_APP_SERVER_PORT, withCredentials: true, headers: { "Content-Type": "application/json", + "Authorization": `Bearer ${accessToken}`, }, }); +instance.interceptors.request.use((config) => { + const accessToken = Cookies.get('accessToken'); + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}`; + config.withCredentials = true; + } + + return config; + }, (error) => { + return Promise.reject(error); + } +); + +instance.interceptors.response.use( + response => response, + async (error) => { + if (error.response && error.response.status === 401) { + const originalRequest = error.config; + if (!originalRequest._retry) { + originalRequest._retry = true; + + try { + // 토큰 재발급 요청 + const response = await axios.post(`${import.meta.env.VITE_APP_SERVER_URL}/api/v1/auth/reissue/token`, {}, { + withCredentials: true, + }); + + if (response.status === 200) { + const accessToken = Cookies.get('accessToken'); + originalRequest.headers['Authorization'] = `Bearer ${accessToken}`; + return instance(originalRequest); + } + } catch (refreshError) { + return Promise.reject(refreshError); + } + } + } + + return Promise.reject(error); + } +); + export default instance; + diff --git a/src/components/auth/sign-in/sign-in-form/SignInForm.tsx b/src/components/auth/sign-in/sign-in-form/SignInForm.tsx index c74808c..73a0d6d 100644 --- a/src/components/auth/sign-in/sign-in-form/SignInForm.tsx +++ b/src/components/auth/sign-in/sign-in-form/SignInForm.tsx @@ -6,6 +6,7 @@ import RadiusButton from "@components/common/button/radius-button/RadiusButton.t import InputField from "@components/common/input-field/InputField.tsx"; import { CONSTANT } from "../../../../constants/Constant.ts"; import Label from "@components/common/input-field/label/Label.tsx"; +import instance from "../../../../api/axios.ts"; interface props { onClick: () => void; @@ -26,13 +27,28 @@ export default function SingInForm(props: props) { setPassword(e.target.value); }; - const handleLogin = () => { - console.log(email, password); - - // Todo: 로그인 로직 구현 + const handleLogin = async () => { if (isValid) { - navigate("/"); + const formData = new FormData(); + + formData.append("serial_id", email); + formData.append("password", password); + + try { + const response = await instance.post("/api/auth/login", formData, { + headers: { + "Content-Type": "multipart/form-data" + }, + }); + + if (response.status === 201) { + navigate("/home"); + } + } catch (error) { + console.error(error); + alert("로그인에 실패했습니다."); + } } }; diff --git a/src/components/auth/sign-up/sign-up-form/SignUpForm.tsx b/src/components/auth/sign-up/sign-up-form/SignUpForm.tsx index 63fb251..028eaa3 100644 --- a/src/components/auth/sign-up/sign-up-form/SignUpForm.tsx +++ b/src/components/auth/sign-up/sign-up-form/SignUpForm.tsx @@ -7,6 +7,7 @@ import Label from "@components/common/input-field/label/Label.tsx"; import InputField from "@components/common/input-field/InputField.tsx"; import RadiusButton from "@components/common/button/radius-button/RadiusButton.tsx"; import RectangleButton from "@components/common/button/rectangle-button/RectangleButton.tsx"; +import instance from "../../../../api/axios.ts"; export default function SignUpForm() { const navigate = useNavigate(); @@ -23,6 +24,7 @@ export default function SignUpForm() { const [isValidEmail, setIsValidEmail] = useState(false); const [isValidAuthCode, setIsValidAuthCode] = useState(false); const [isVerification, setIsVerification] = useState(false); + const [tempToken, setTempToken] = useState(""); const handleName = (e: React.ChangeEvent) => { setName(e.target.value); @@ -87,29 +89,71 @@ export default function SignUpForm() { } }, [name, password, passwordCheck]); - const handleAuthCodeButtonClick = () => { + const handleAuthCodeButtonClick = async () => { if (isValidEmail && !isIssued) { - alert("인증번호가 발송되었습니다."); - setIsIssued(true); + try { + const response = await instance.post("/api/auth/validations/email", { + email: email, + is_duplicate_check: false, + }) + + if (response.status === 200) { + alert("인증번호가 발송되었습니다."); + setIsIssued(true); + } + + const tryCount = response.data.data.tryCnt; + + if (tryCount > 5) { + alert("인증번호 발송 횟수를 초과했습니다."); + return; + } + } catch (error) { + alert("인증번호 전송에 실패했습니다"); + } } }; - const handleVerificationButtonClick = () => { + const handleVerificationButtonClick = async () => { if ( authCode !== "" && isIssued && !isVerification && CONSTANT.REGEX.AUTH_CODE.test(authCode) ) { - alert("인증되었습니다."); - setIsVerification(true); + try { + const response = await instance.post("/api/auth/validations/authentication-code", { + email: email, + authentication_code: authCode, + }); + + if (response.status === 201) { + setIsVerification(true); + setTempToken(response.data.data.temporary_token); + alert("인증되었습니다."); + } + } catch (error) { + alert("인증에 실패했습니다."); + } } }; - const handleSubmitButtonClick = () => { + const handleSubmitButtonClick = async () => { if (isValid) { - alert("회원가입이 완료되었습니다."); - navigate("/"); + try { + const response = await instance.post("/api/auth/sign-up", { + nickname: name, + password: password, + temporary_token: tempToken, + }) + + if (response.status === 201) { + alert("회원가입이 완료되었습니다."); + navigate("/home"); + } + } catch (error) { + alert("회원가입에 실패했습니다."); + } } }; diff --git a/src/router.tsx b/src/router.tsx index ee7fc59..dfa4f40 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -23,7 +23,7 @@ const Wrapper = styled.div` const Layout: React.FC = () => { const location = useLocation(); - const showNavbar = location.pathname !== "/entry"; + const showNavbar = location.pathname !== "/"; return ( @@ -38,9 +38,9 @@ export default function Router(): JSX.Element { }> - } /> + } /> + } /> } /> - } />