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/issue-4 #11

Merged
merged 7 commits into from
Feb 14, 2024
Merged
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
Feat: 회원가입 페이지 완성 및 리다이렉트페이지 구현
DingX2 committed Feb 14, 2024
commit 36259191f6b7477ea20dfd64823685d43a249908
3 changes: 3 additions & 0 deletions BlueMosaic/src/App.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import { Trash } from './pages/Trash'

import { ThemeProvider } from '@emotion/react';
import GlobalStyle from './styles/GlobalStyle';
import RedirectPage from './pages/RedirectPage'

function App() {

@@ -29,6 +30,8 @@ function App() {
<Route path='/rank' element={<Ranking/>}/>
<Route path='/signin' element={<Signin/>}/>
<Route path='/signup' element={<Signup/>}/>
<Route path='/signupRedirect' element={<RedirectPage/>}/>

</Routes>
</BrowserRouter>
</ThemeProvider>
11 changes: 11 additions & 0 deletions BlueMosaic/src/assets/GoogleBtn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions BlueMosaic/src/components/common/InputForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import styled from "@emotion/styled"

export const InputForm = ({ type, name, value, onChange, placeholder, title, ...rest }) => {
return (
<InputFormWrapper>
<span>{title}</span>
<input
type={type}
name={name}
value={value}
placeholder={placeholder}
onChange={onChange}
{...rest}
/>
</InputFormWrapper>
);
};

const InputFormWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;

input{
display: flex;
padding: 0.8125rem 14.3125rem 0.8125rem 1rem;
align-items: center;
border-radius: 0.5rem;
border: 1px solid var(--Stroke, #DADADA);
}

span {
color: var(--1st-text, #4A4543);
font-family: Roboto;
font-style: normal;
font-weight: 500;
line-height: normal;
}`
161 changes: 161 additions & 0 deletions BlueMosaic/src/hooks/useAuthQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import axios,{ AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { UserInfo } from '../stores/UserInfoStore';
import { useQuery } from '@tanstack/react-query';

export const AuthApis = {
instance: axios.create({
baseURL: "http://localhost:8001/user-service/",
withCredentials: true,
}),

checkEmailDuplicate: async (userInfo: UserInfo): Promise<boolean> => {
const response = await AuthApis.instance.get(`/signup/exists-email/${userInfo.email}`, {
email: userInfo.email,
});
return response.data.valid === true;
},

useCheckEmailDuplicateQuery: (userInfo: UserInfo) => {
const { data, isLoading, error } = useQuery<Boolean, Error>({
queryKey: ['checkEmailDuplicateQuery', userInfo.email],
queryFn: () => AuthApis.checkEmailDuplicate(userInfo),
});
return { data, isLoading, error };
},

checkUsernameDuplicate: async (userInfo:UserInfo): Promise<Boolean> => {
const response = await AuthApis.instance.get(`/signup/exists-username/${userInfo.username}`, {
username: userInfo.username,
});
return response.data.valid === true;
},

checkUsernameDuplicateQuery: (userInfo: UserInfo) => {
const { data, isLoading, error } = useQuery<Boolean, Error>({
queryKey: ['checkUsernameDuplicateQuery', userInfo.username],
queryFn: () => AuthApis.checkUsernameDuplicate(userInfo),
});
return { data, isLoading, error };
},

signup: async (userInfo:UserInfo, passwordConfirm: string, authCode: string) => {
try {
const response = await AuthApis.instance.post('/signup', {
username: userInfo.username,
email: userInfo.email,
password: userInfo.password,
passwordConfirm: passwordConfirm,
authCode: authCode
});
const data = response.data;

return data;
} catch (error) {
console.error("API 통신 에러:", error);
}
},

signupQuery: (userInfo: UserInfo,passwordConfirm:string) => {
const { data, isLoading, error } = useQuery<Boolean, Error>({
queryKey: ['signupQuery', userInfo,passwordConfirm],
queryFn: () => AuthApis.signup(userInfo, passwordConfirm),
});
return { data, isLoading, error };
},


signin: async (userInfo:UserInfo) => {
try {
const response = await AuthApis.instance.post('/login', {
email: userInfo.email,
password: userInfo.password,
});
const data = response.data;

const rawAccessToken = response.headers.get('Accesstoken');
const rawRefreshToken = response.headers.get('RefreshToken');

const Accesstoken = rawAccessToken ? rawAccessToken.replace(/^Bearer\s+/i, '') : null;
const Refreshtoken = rawRefreshToken ? rawRefreshToken.replace(/^Bearer\s+/i, '') : null;

// console.log(`토큰 발급\n Accesstoken:${Accesstoken}\n Refreshtoken:${Refreshtoken}`)

localStorage.setItem('Accesstoken', Accesstoken);
localStorage.setItem('Refreshtoken', Refreshtoken);

return data;
} catch (error) {
return null;
}
},

//refreshToken
setupInterceptors: () => {
AuthApis.instance.interceptors.response.use(
(response: AxiosResponse) => response,
async (error) => {
const originalRequest = error.config;

if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
const refreshToken = localStorage.getItem('refreshToken');
// 재발급받는 경로
const response = await AuthApis.instance.get('/hello', {
headers: {Refreshtoken : `Bearer ${localStorage.getItem('Refreshtoken')}`}
});

const rawAccessToken = response.headers.get('Accesstoken');
const rawRefreshToken = response.headers.get('Refreshtoken');

const newAccesstoken = rawAccessToken ? rawAccessToken.replace(/^Bearer\s+/i, '') : null;
const newRefreshtoken = rawRefreshToken ? rawRefreshToken.replace(/^Bearer\s+/i, '') : null;

// console.log(`토큰 갱신\n newAccesstoken:${newAccesstoken}\n newRefreshtoken:${newRefreshtoken}`)

localStorage.setItem('Accesstoken',newAccesstoken);
localStorage.setItem('Refreshtoken',newRefreshtoken);

originalRequest.headers['Authorization'] = `Bearer ${newAccesstoken}`;

return AuthApis.instance(originalRequest);
} catch (refreshError) {
console.error("토큰 갱신 실패", refreshError);
localStorage.removeItem('Accesstoken');
localStorage.removeItem('Refreshtoken');
window.location.href = '/signin';
}
}

return Promise.reject(error);
}
);
},


// 리팩토링용 코드
fetchData: async (endpoint: string, type: string, data?: any) => {
let response;

switch (type) {
case 'get':
response = await AuthApis.instance.get(`http://localhost:8001/user-service/${endpoint}`, {
headers: { Authorization: `Bearer ${localStorage.getItem('jwtToken')}` },
});
break;

case 'post':
response = await AuthApis.instance.post(`http://localhost:8001/user-service/${endpoint}`, data, {
headers: { Authorization: `Bearer ${localStorage.getItem('jwtToken')}` },
});
break;

default:
console.error("Invalid type:", type);
}

return response;
},

};
37 changes: 37 additions & 0 deletions BlueMosaic/src/pages/RedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useEffect } from 'react';
import { AuthApis } from "../hooks/useAuthQuery";
import { useNavigate } from "react-router-dom";

const RedirectPage = () => {
const navigate = useNavigate();

useEffect(() => {
const fetchData = async () => {
try {
const response = await AuthApis.signin();

const rawAccessToken = response.headers.get('Accesstoken');
const rawRefreshToken = response.headers.get('RefreshToken');

const accessToken = rawAccessToken ? rawAccessToken.replace(/^Bearer\s+/i, '') : null;
const refreshToken = rawRefreshToken ? rawRefreshToken.replace(/^Bearer\s+/i, '') : null;

localStorage.setItem('AccessToken', accessToken);
localStorage.setItem('RefreshToken', refreshToken);

if (accessToken) {
navigate("/");
}
} catch (error) {
// 오류 처리 로직을 추가하세요.
console.error('로그인 요청 중 오류 발생:', error);
}
};

fetchData();
}, [navigate]);

return <></>;
}

export default RedirectPage;
22 changes: 18 additions & 4 deletions BlueMosaic/src/pages/Signin.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import styled from "@emotion/styled/"
import { Wrapper, Container, DivContainter } from "../styles/Layout"
import { Wrapper, Container, DivCenterContainter } from "../styles/Layout"
import LoginBackround from "../assets/LoginBackgrorund.jpg"
import WaterWave from 'react-water-wave';
import GoogleSVG from "../assets/Google.svg"
import GoogleBtn from "../assets/GoogleBtn.svg"

export const Signin = () => {

const handleGoogleLogin = (event) => {
event.preventDefault();
window.location.href = '/oauth2/authorization/google';
};

return(
<WaterWave imageUrl={LoginBackround}>
{({ pause, play }) => (
<Wrapper>
<Container>
<DivContainter>

</DivContainter>
<DivCenterContainter>
<img src={GoogleSVG} alt="google"/>
<button onClick={handleGoogleLogin}>
<img src={GoogleBtn} alt="googleBtn"/>
</button>
<section>
<p>Don't have an account?</p>
<span>Sign up</span>
</section>
</DivCenterContainter>
</Container>
</Wrapper>
)}
49 changes: 44 additions & 5 deletions BlueMosaic/src/pages/Signup.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
import styled from "@emotion/styled/"
import { Wrapper, Container } from "../styles/Layout"
import { Wrapper, Container, DivContainer } from "../styles/Layout"
import Signupbackground from "../assets/SignupBackground.jpg"
import WaterWave from 'react-water-wave';
import GoogleSVG from "../assets/Google.svg"
import GoogleBtn from "../assets/GoogleBtn.svg"
import { InputForm } from "../components/common/InputForm";
import { useStore } from "zustand";
import { UserInfoStore } from "../stores/UserInfoStore"
import { useState } from "react";

export const Signup = () => {
const userInfo = useStore(UserInfoStore);
const [passwordConfirm, setPasswordConfirm] = useState<string>("");

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value, name } = e.target;
switch(name){
case "email": userInfo.updateEmail(value); break;
case "password": userInfo.updatePassword(value); break;
case "passwordConfirm": setPasswordConfirm(value); break;
case "username": userInfo.updateUsername(value);
}

}

const handleGoogleLogin = (event) => {
event.preventDefault();
window.location.href = '/home';
};

return(
<WaterWave imageUrl={Signupbackground}>
{({ pause, play }) => (
<Wrapper>
<Container>
<h1>이너리ㅓㅣㅇ나ㅣㄹ나</h1>
</Container>
</Wrapper>
<Container>
<DivContainer>
<img src={GoogleSVG} alt="google"/>

{/* email */}
<InputForm title="Email" type="text" placeholder="Enter your Email" name="email" value={userInfo?.email} onChange={onChange} />

<InputForm title="Password" type="password" placeholder="Enter your Password" name="password" value={userInfo?.password} onChange={onChange} />

<InputForm title="Password Confirm" type="password" placeholder="Enter your Password Again" name="passwordConfirm" value={passwordConfirm} onChange={onChange} />

<InputForm title="Username" type="text" placeholder="Enter your Email" name="username" value={userInfo?.username} onChange={onChange} />

<button onClick={handleGoogleLogin}>
<img src={GoogleBtn} alt="googleBtn"/>
</button>
</DivContainer>
</Container>
</Wrapper>
)}
</WaterWave>
)
31 changes: 31 additions & 0 deletions BlueMosaic/src/stores/UserInfoStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

export interface UserInfo {
email: string;
password: string;
username: string;
updateEmail: (email: UserInfo['email']) => void;
updatePassword: (password: UserInfo['password']) => void;
updateUsername: (username: UserInfo['username']) => void;
}

const createUserInfoStore = (set) => ({
email: '',
password: '',
username: '',
updateEmail: (email: string) => set({ email }),
updatePassword: (password: string) => set({ password }),
updateUsername: (username: string) => set({ username }),
});

let userInfoStoreTemp;

//devtools
if (import.meta.env.DEV) {
userInfoStoreTemp = create<UserInfo>()(devtools(createUserInfoStore, { name: 'userInfo' }));
} else {
userInfoStoreTemp = create<UserInfo>()(createUserInfoStore);
}

export const UserInfoStore = userInfoStoreTemp;
71 changes: 68 additions & 3 deletions BlueMosaic/src/styles/Layout.ts
Original file line number Diff line number Diff line change
@@ -15,14 +15,14 @@ export const WrapperWave = styled(WaterWave)`
export const Container = styled.section`
width: 100%;
height: 100%;
max-width: 1280px;
margin: 0 auto;
display: flex;
flex-direction: coulmn;
flex-direction: column;
justify-content: center;
align-items: center;
`;

export const DivContainter = styled.div`
export const DivCenterContainter = styled.div`
display: flex;
width: 37.5rem;
height: 27.875rem;
@@ -34,4 +34,69 @@ export const DivContainter = styled.div`
flex-shrink: 0;
border-radius: 3.125rem;
background: var(----white-color, #FFF);
section{
display: flex;
align-items: center;
gap: 0.5rem;
}
p {
color: var(--2nd-text, #808080);
font-family: Roboto;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: normal;
}
span {
color: var(----googleRed-color, #EA4335);
font-family: Roboto;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: normal;
}
button {
border: none;
background: none;
padding: 0;
cursor: pointer;
transition: filter 0.3s ease;
}
button:hover {
filter: brightness(70%);
}
`


export const DivContainer = styled.section`
display: flex;
width: 37.5rem;
height: 64rem;
margin-left: auto;
padding-top: 4.1875rem;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2.5rem;
flex-shrink: 0;
border-radius: 3.125rem 0rem 0rem 3.125rem;
background: var(----white-color, #FFF);
button {
border: none;
background: none;
padding: 0;
cursor: pointer;
transition: filter 0.3s ease;
}
button:hover {
filter: brightness(70%);
}
`