diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..69258c8 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,9 @@ dist-ssr *.njsproj *.sln *.sw? + +.env +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..c82c9b1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + @@ -9,5 +9,6 @@
+ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e68fa1a..0e03322 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,8 @@ import ChattingListPage from './pages/ChattingListPage'; import SignupPage from './pages/SignupPage'; import SigninPage from './pages/SigninPage'; import ChattingRoomPage from './pages/ChattingRoomPage'; +import TestPage from './test/TestPage'; +import Signupnaver from './pages/Signupnaver'; import VideoCallPage from './pages/VideoCallPage'; import { Global } from '@emotion/react'; import { GlobalStyle } from './styles/GlobalStyle'; @@ -22,6 +24,8 @@ const router = createBrowserRouter([ { path: 'signup', element: }, { path: 'chattinglist', element: }, { path: 'chatroom/:chatroom_id', element: }, + { path: 'test', element: }, + { path: 'signupnaver', element: }, { path: 'videocall', element: }, ], }, diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx new file mode 100644 index 0000000..65df1ec --- /dev/null +++ b/frontend/src/components/Button.tsx @@ -0,0 +1,7 @@ +export const Button = ({onClick,label,...rest})=>{ + return( + + ) +} \ No newline at end of file diff --git a/frontend/src/components/InputForm.tsx b/frontend/src/components/InputForm.tsx new file mode 100644 index 0000000..1101d1d --- /dev/null +++ b/frontend/src/components/InputForm.tsx @@ -0,0 +1,15 @@ +export const InputForm = ({ type, name, value, onChange, placeholder, title, ...rest }) => { + return ( + <> + {title} + + + ); +}; diff --git a/frontend/src/components/auth/NaverLoginButton.tsx b/frontend/src/components/auth/NaverLoginButton.tsx new file mode 100644 index 0000000..bec1db9 --- /dev/null +++ b/frontend/src/components/auth/NaverLoginButton.tsx @@ -0,0 +1,97 @@ +import styled from '@emotion/styled'; +import { useEffect, useRef } from 'react' + +const CLIENT_ID = import.meta.env.VITE_APP_CLIENT_ID; + +export const NaverLoginButton = () => { + const naverRef = useRef() + const { naver } = window + const REDIRECT_URI = "http://localhost:5173/signupnaver"; + + const initializeNaverLogin = () => { + const naverLogin = new naver.LoginWithNaverId({ + clientId: CLIENT_ID, + callbackUrl: REDIRECT_URI, + isPopup: false, + loginButton: { color: 'green', type: 3, height: 58 }, + callbackHandle: true, + }) + naverLogin.init() + + naverLogin.getLoginStatus(async function (status) { + if (status) { + const userid = naverLogin.user.getEmail() + const username = naverLogin.user.getName() + console.log(userid,username) + } + }) + } + + useEffect(() => { + initializeNaverLogin() + }, []) + + const handleNaverLogin = () => { + naverRef.current.children[0].click() + } + + + return ( + <> + + +
+
+
+ + + + + +
+ 네이버로 간편 가입 +
+
+
+ + ); +} + +const NaverIdLogin = styled.div` + display: none`; + +const NaverButtonStyles = styled.div` + .NaverLoginButton-wrapper { + display: flex; + } + + .Logo{ + display: flex; + } + + .btn-naver { + width: 300px; + height: 46px; + background: #03C75A; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + transition: background-color 0.3s; + } + + .text { + font-family: 'Noto Sans', sans-serif; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 24px; + text-align: center; + color: #ffffff; + } + + .btn-naver:hover { + background-color: #029445; + } +`; \ No newline at end of file diff --git a/frontend/src/hooks/useAuthQuery.ts b/frontend/src/hooks/useAuthQuery.ts new file mode 100644 index 0000000..e4b2372 --- /dev/null +++ b/frontend/src/hooks/useAuthQuery.ts @@ -0,0 +1,163 @@ +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 => { + 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({ + queryKey: ['checkEmailDuplicateQuery', userInfo.email], + queryFn: () => AuthApis.checkEmailDuplicate(userInfo), + }); + return { data, isLoading, error }; + }, + + checkUsernameDuplicate: async (userInfo:UserInfo): Promise => { + 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({ + queryKey: ['checkUsernameDuplicateQuery', userInfo.username], + queryFn: () => AuthApis.checkUsernameDuplicate(userInfo), + }); + return { data, isLoading, error }; + }, + + signup: async (userInfo:UserInfo, passwordConfirm: string) => { + try { + const response = await AuthApis.instance.post('/signup', { + username: userInfo.username, + email: userInfo.email, + password: userInfo.password, + passwordConfirm: passwordConfirm, + }); + const data = response.data; + + return data; + } catch (error) { + console.error("API 통신 에러:", error); + } + }, + + signupQuery: (userInfo: UserInfo,passwordConfirm:string) => { + const { data, isLoading, error } = useQuery({ + 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) { + console.error("API 통신 에러:", error); + } + }, + + //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; + }, + +}; + + diff --git a/frontend/src/pages/SigninPage.tsx b/frontend/src/pages/SigninPage.tsx index e1031c3..1177594 100644 --- a/frontend/src/pages/SigninPage.tsx +++ b/frontend/src/pages/SigninPage.tsx @@ -1,7 +1,93 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { InputForm } from "../components/InputForm"; +import { Button } from "../components/Button"; +import { NaverLoginButton } from "../components/auth/NaverLoginButton"; +import { useStore } from "zustand"; +import { UserInfoStore } from "../stores/UserInfoStore"; + +import { AuthApis } from "../hooks/useAuthQuery"; +import axios from "axios"; + + +const fetchDataExample = async () => { + instance: axios.create({ + baseURL: "http://localhost:8001/user-service/", + withCredentials: true, + }) + + try { + AuthApis.setupInterceptors(); + const response = await AuthApis.instance.get('/hello', { + headers: { Accesstoken: `Bearer ${localStorage.getItem('Accesstoken')}` }, + }); + const data = response.data; + console.log("로그인 성공") + return data; + } catch (error) { + } + }; + const SigninPage = () => { + const navigate = useNavigate(); + const userInfo = useStore(UserInfoStore); + + const onChange = (e: React.ChangeEvent) => { + const { value, name } = e.target; + + if (name === 'email') { + userInfo.updateEmail(value); + } else if (name === 'password') { + userInfo.updatePassword(value); + } + }; + + const onButtonClick = (action:string) => { + switch(action){ + case "onSignin": + AuthApis.signin(userInfo); + navigate("/"); + break; + case "onSignup": + navigate("/signup") + break; + case "onSignupKakao": + fetchDataExample(); + // navigate("/signupnaver") + break; + default: + console.error("error") + } + } + return( - <>It's SigninPage! - ) +
+
+

TadakTadak Logo

+ <>It's SigninPage! +
+ + {/* email input */} + + +
+ + {/* pw input */} + + +
+ +
+ +
+ +
+ +
+
+ ) + }; -export default SigninPage; +export default SigninPage; \ No newline at end of file diff --git a/frontend/src/pages/Signupnaver.tsx b/frontend/src/pages/Signupnaver.tsx new file mode 100644 index 0000000..94c6806 --- /dev/null +++ b/frontend/src/pages/Signupnaver.tsx @@ -0,0 +1,42 @@ +import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { UserInfoStore } from '../stores/UserInfoStore'; +import { useStore } from 'zustand'; + +const Signupnaver = () => { + const navigate = useNavigate(); + const userInfo = useStore(UserInfoStore); + + const userAccessToken = () => { + window.location.href.includes('access_token') && getToken() + navigate('/') + } + + const getToken = async () => { + const token = window.location.href.split('=')[1].split('&')[0] + localStorage.setItem('Accesstoken', token) + + // 네이버 API를 호출하여 사용자 정보 가져오기 proxy 적용중 수정필요 + try { + const response = await axios.get('/api/v1/nid/me', { + headers: { + 'Authorization': `Bearer ${token}` + }} + ) + console.log(response.data.response.email); + userInfo.updateEmail(response.data.response.email) + userInfo.updateUsername(response.data.response.nickname) + } catch (error) { + console.error('Error fetching user info', error); + } + } + + useEffect(() => { + userAccessToken() + }, []) + + return
; +}; + +export default Signupnaver; diff --git a/frontend/src/stores/UserInfoStore.ts b/frontend/src/stores/UserInfoStore.ts new file mode 100644 index 0000000..b4dfcc5 --- /dev/null +++ b/frontend/src/stores/UserInfoStore.ts @@ -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()(devtools(createUserInfoStore, { name: 'userInfo' })); +} else { + userInfoStoreTemp = create()(createUserInfoStore); +} + +export const UserInfoStore = userInfoStoreTemp; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a7fc6fb..66eda50 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -5,6 +5,7 @@ "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, + "types": ["vite/client"], /* Bundler mode */ "moduleResolution": "bundler",