diff --git a/Week4/LoginAndSignup/index.html b/Week4/LoginAndSignup/index.html new file mode 100644 index 0000000..531f77d --- /dev/null +++ b/Week4/LoginAndSignup/index.html @@ -0,0 +1,13 @@ + + + + + + 로그인 & 회원가입 + + +
+
+ + + diff --git a/Week4/LoginAndSignup/src/App.css b/Week4/LoginAndSignup/src/App.css new file mode 100644 index 0000000..e69de29 diff --git a/Week4/LoginAndSignup/src/App.jsx b/Week4/LoginAndSignup/src/App.jsx new file mode 100644 index 0000000..566c731 --- /dev/null +++ b/Week4/LoginAndSignup/src/App.jsx @@ -0,0 +1,15 @@ +import Router from './components/Router'; +import './App.css' +import { GlobalStyle } from './style/GlobalStyle'; + +function App() { + + return ( + <> + + + + ) +} + +export default App diff --git a/Week4/LoginAndSignup/src/assets/profile/profile.png b/Week4/LoginAndSignup/src/assets/profile/profile.png new file mode 100644 index 0000000..5974b5c Binary files /dev/null and b/Week4/LoginAndSignup/src/assets/profile/profile.png differ diff --git a/Week4/LoginAndSignup/src/components/Router.jsx b/Week4/LoginAndSignup/src/components/Router.jsx new file mode 100644 index 0000000..46265a1 --- /dev/null +++ b/Week4/LoginAndSignup/src/components/Router.jsx @@ -0,0 +1,18 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import SignUp from '../pages/SignUp/SignUp'; +import Login from '../pages/Login/Login'; +import MyPage from '../pages/MyPage/MyPage'; + +const Router = () => { + return ( + + + } /> + } /> + } /> + + + ); +}; + +export default Router; \ No newline at end of file diff --git a/Week4/LoginAndSignup/src/components/Toast.jsx b/Week4/LoginAndSignup/src/components/Toast.jsx new file mode 100644 index 0000000..e541132 --- /dev/null +++ b/Week4/LoginAndSignup/src/components/Toast.jsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import styled from 'styled-components'; + +const Toast = ({ error, setError, errMessage }) => { + useEffect(() => { + setTimeout(() => { + setError(false); + }, 2000); + }, [error]); + + return createPortal( + + {errMessage} + , document.getElementById("toast") + ); +}; + +export default Toast; + +const ToastWrapper = styled.div` + background-color: var(--color-bg); + position: fixed; + bottom: 11rem; + right: 25rem; + padding: 0.6rem; + border-radius: 1rem; +`; + +const ToastMessage = styled.div` + color: black; +`; \ No newline at end of file diff --git a/Week4/LoginAndSignup/src/index.css b/Week4/LoginAndSignup/src/index.css new file mode 100644 index 0000000..e69de29 diff --git a/Week4/LoginAndSignup/src/main.jsx b/Week4/LoginAndSignup/src/main.jsx new file mode 100644 index 0000000..54b39dd --- /dev/null +++ b/Week4/LoginAndSignup/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/Week4/LoginAndSignup/src/pages/Login/Login.jsx b/Week4/LoginAndSignup/src/pages/Login/Login.jsx new file mode 100644 index 0000000..3787e65 --- /dev/null +++ b/Week4/LoginAndSignup/src/pages/Login/Login.jsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import * as S from './style'; +import axios from "axios"; +import Toast from '../../components/Toast'; +import { useNavigate } from 'react-router-dom'; + +const Login = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(false); + const [errMessage, setErrMessage] = useState(""); + const navigate = useNavigate(); + const saveUsername = (event) => { + setUsername(event.target.value); + }; + + const savePassword = (event) => { + setPassword(event.target.value); + }; + + const moveSignupPage = () => { + navigate(`/signup`); + }; + + const getData = async () => { + try { + const res = await axios.post(`${import.meta.env.VITE_BASE_URL}/api/v1/members/sign-in`, { + username: username, + password: password, + }) + console.log("✨성공🤩✨"); + console.log(`아이디 : ${res.data.username}`); + console.log(`비번 : ${res.data.password}`); + navigate(`/mypage/${res.data.id}`); + } catch (err) { + setError(true); + setErrMessage(err.response.data.message); + } + }; + + return ( + <> + + + Login + ID + + PASSWORD + + + + 로그인 + 회원가입 + + + {error ? + + : null + } + + ); +}; + +export default Login; \ No newline at end of file diff --git a/Week4/LoginAndSignup/src/pages/Login/style.js b/Week4/LoginAndSignup/src/pages/Login/style.js new file mode 100644 index 0000000..6b130dd --- /dev/null +++ b/Week4/LoginAndSignup/src/pages/Login/style.js @@ -0,0 +1,77 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + width: 23rem; + height: 25rem; + background-color: var(--color-bg); + border-radius: 1rem; + display: inline-block; + flex-direction: column; + box-shadow: 12px 15px 63px -1px rgba(0,0,0,0.81); + -webkit-box-shadow: 12px 15px 63px -1px rgba(0,0,0,0.81); + -moz-box-shadow: 12px 15px 63px -1px rgba(0,0,0,0.81); +`; + +export const PageTitle = styled.h3` + text-align: center; + font-size: 1.5rem; + font-weight: bold; + padding: 2rem; + margin-bottom: 1rem; + color: var(--color-accent); +`; + +export const InputContainer = styled.div` + line-height: 40px; +`; + +export const ButtonContainer = styled.div` + display: block; + margin-left: 4rem; + margin-top: 3rem; +`; + +export const Field = styled.div` + display: inline; + margin-right: 1.5rem; + margin-left: 2rem; + &.id-field { + margin-right: 6rem; + } + &.pwd-field { + margin-right: 3.5rem; + } +`; + +export const Button = styled.button` + display: block; + width: 70%; + margin: 1rem; + padding: 0.4rem; + border-radius: 0.5rem; + font-weight: bold; + border: solid; + background-color: var(--color-button-bg); + color: var(--color-accent); + &:hover { + background-color: var(--color-accent); + color: var(--color-button-bg); + font-weight: bold; + } +`; + +export const SignUpBtn = styled.button` + display: block; + width: 70%; + margin: 1rem; + padding: 0.4rem; + border-radius: 0.5rem; + font-weight: bold; + border: solid; + text-align: center; + &:hover { + background-color: #000000; + color: #FF1493; + font-weight: bold; + } +`; \ No newline at end of file diff --git a/Week4/LoginAndSignup/src/pages/MyPage/MyPage.jsx b/Week4/LoginAndSignup/src/pages/MyPage/MyPage.jsx new file mode 100644 index 0000000..fe2bd7f --- /dev/null +++ b/Week4/LoginAndSignup/src/pages/MyPage/MyPage.jsx @@ -0,0 +1,48 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import axios from "axios"; +import * as S from './style'; +import { useState } from 'react'; +import profileImg from "../../assets/profile/profile.png"; + +const MyPage = () => { + const { userId } = useParams(); + const navigate = useNavigate(); + const [username, setUsername] = useState(""); + const [nickname, setNickname] = useState(""); + + const moveLoginPage = () => { + navigate(`/login`); + }; + + const getLoginData = async () => { + try { + axios.get(`${import.meta.env.VITE_BASE_URL}/api/v1/members/${userId}`, { + userId: userId, + }).then((response) => { + setUsername(response.data.username); + setNickname(response.data.nickname); + console.log("✨🔥성공🔥✨"); + + }) + } catch (err) { + console.log(err); + } + }; + getLoginData(); + + return ( + + MY PAGE + + + ID : {username} + + + 닉네임 : {nickname} + + 로그아웃 + + ); +}; + +export default MyPage; \ No newline at end of file diff --git a/Week4/LoginAndSignup/src/pages/MyPage/style.js b/Week4/LoginAndSignup/src/pages/MyPage/style.js new file mode 100644 index 0000000..d231351 --- /dev/null +++ b/Week4/LoginAndSignup/src/pages/MyPage/style.js @@ -0,0 +1,50 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + width: 23rem; + height: 20rem; + line-height: 40px; + background-color: var(--color-bg); + border-radius: 1rem; + display: inline-block; + flex-direction: column; + -webkit-box-shadow: 0px 0px 17px 1px rgba(93,93,93,0.72); + box-shadow: 0px 0px 17px 1px rgba(93,93,93,0.72); +`; + +export const PageTitle = styled.h3` + text-align: center; + font-size: 1.5rem; + padding: 2rem; + font-weight: bold; + padding-bottom: 0; +`; + +export const TextArea = styled.div` + text-align: center; + background-color: var(--color-light-pink); + font-size: 1rem; + font-weight: bold; + padding: 0.2rem; +`; + +export const Profile = styled.img` + width: 4rem; + height: auto; + margin-left: 9.5rem; +`; + +export const Button = styled.button` + display: block; + width: 70%; + margin-left: 3.5rem; + margin-top: 1.5rem; + padding: 0.4rem; + border-radius: 0.5rem; + border: solid; + &:hover { + background-color: var(--color-button-bg); + color: var(--color-light-pink); + font-weight: bold; + } +`; diff --git a/Week4/LoginAndSignup/src/pages/Signup/Signup.jsx b/Week4/LoginAndSignup/src/pages/Signup/Signup.jsx new file mode 100644 index 0000000..74e0994 --- /dev/null +++ b/Week4/LoginAndSignup/src/pages/Signup/Signup.jsx @@ -0,0 +1,156 @@ +import { GlobalStyle } from '../../style/GlobalStyle' +import axios from "axios"; +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import * as S from './style'; + +const SignUp = () => { + const [username, setUsername] = useState(''); + const [nickname, setNickname] = useState(''); + const [password, setPassword] = useState(''); + const [isExist, setIsExist] = useState('none'); + const [isPasswordConfirm, setIsPasswordConfirm] = useState(false); + const [signupButton, setSignupButton] = useState(false); + const navigate = useNavigate(); + + const saveUsername = event => { + setUsername(event.target.value); + }; + + const savePassword = event => { + setPassword(event.target.value); + }; + + const saveNickName = event => { + setNickname(event.target.value); + }; + + const moveLoginPage = () => { + navigate(`/login`); + }; + + const postData = async () => { + try { + axios.post(`${import.meta.env.VITE_BASE_URL}/api/v1/members`, { + "username": username, + "nickname": nickname, + "password": password + }).then(() => { + console.log("성공🤩"); + console.log(`아이디 : ${username}`); + console.log(`비번 : ${password}`); + console.log(`닉네임 : ${nickname}`); + }) + } catch (err) { + console.log(err); + } + }; + + const duplicationCheck = () => { + let inputID = document.querySelector(".id-input").value; + + axios.get(`${import.meta.env.VITE_BASE_URL}/api/v1/members/check`, { + params: { + "username": `${inputID}`, + }, + }) + .then((response) => { + const isDuplicate = response.data.isExist; + console.log(isDuplicate); + if (isDuplicate) { + setIsExist('exist'); + console.log("중복되는 아이디 입니다."); + } else { + setUsername(inputID); + setIsExist('notExist'); + console.log("🔥사용 가능한 아이디입니다.🔥"); + } + }) + .catch(function (error) { + console.log(error); + }); + }; + + const onChangePasswordConfirm = useCallback( + (e) => { + const passwordConfirmCurrent = e.target.value + if (password === passwordConfirmCurrent) { + console.log('✅비밀번호 일치✅'); + setIsPasswordConfirm(true) + } else { + console.log('🚨비밀번호 불일치🚨'); + setIsPasswordConfirm(false) + } + }, + [password] + ) + + useEffect(() => { + username && isExist === 'notExist' && isPasswordConfirm && nickname ? ( + setSignupButton(true) + ) : ( + setSignupButton(false) + ) + }, [username, isExist, isPasswordConfirm, nickname]); + + return ( + <> + + + Sign Up + ID + { + saveUsername(e); + setIsExist('none'); + }} /> + { + saveUsername(event); + duplicationCheck(); + }} + >중복체크 + + 비밀번호 + + 비밀번호 확인 + + 닉네임 + + { + postData(); + moveLoginPage(); + }}> + 회원가입 + + + + ); +}; + +export default SignUp; + diff --git a/Week4/LoginAndSignup/src/pages/Signup/style.js b/Week4/LoginAndSignup/src/pages/Signup/style.js new file mode 100644 index 0000000..dd948c3 --- /dev/null +++ b/Week4/LoginAndSignup/src/pages/Signup/style.js @@ -0,0 +1,92 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + width: 23rem; + height: 25rem; + line-height: 40px; + background-color: var(--color-bg); + border-radius: 1rem; + display: inline-block; + flex-direction: column; + box-shadow: 12px 15px 63px -1px rgba(0,0,0,0.81); + -webkit-box-shadow: 12px 15px 63px -1px rgba(0,0,0,0.81); + -moz-box-shadow: 12px 15px 63px -1px rgba(0,0,0,0.81); +`; + +export const PageTitle = styled.h3` + text-align: center; + font-size: 1.5rem; + color: var(--color-accent); + padding: 2rem; + font-weight: bold; +`; + +export const Field = styled.div` + display: inline; + margin-right: 1.5rem; + margin-left: 2rem; + &.id-field { + margin-right: 6rem; + } + &.pwd-field { + margin-right: 3.5rem; + } + &.pwd-error { + margin-right: 1.5rem; + } + &.pwd-success > input { + margin-right: 1.5rem; + border-color: green; + } + &.nickname-field { + margin-right: 4.35rem; + } +`; + +export const Input = styled.input` + padding: 0.28rem; + border-radius: 0.5rem; + border: solid; +`; + +export const CheckButton = styled.button` + display: inline; + margin: 0.5rem; + border-radius: 0.7rem; + padding: 0.28rem; + font-weight: bold; + border: solid; + &.id-notExist { + background-color: green; + } + &.id-exist { + background-color: red; + } + &.none { + background-color: black; + color: var(--color-accent); + font-weight: normal; + } +` +export const SignUpBtn = styled.button` + display: block; + width: 60%; + margin: auto; + margin-top: 3rem; + padding: 0.4rem; + border-radius: 0.5rem; + font-weight: bold; + border: solid; + text-align: center; + background-color: var(--color-button-bg); + color: var(--color-accent); + &:hover { + background-color: var(--color-accent); + color: var(--color-button-bg); + font-weight: bold; + } + &:disabled { + opacity: 0.4; + pointer-events: none; // disabled 되었을 땐 hover효과 없음 + } +`; diff --git a/Week4/LoginAndSignup/src/style/GlobalStyle.jsx b/Week4/LoginAndSignup/src/style/GlobalStyle.jsx new file mode 100644 index 0000000..78a51e3 --- /dev/null +++ b/Week4/LoginAndSignup/src/style/GlobalStyle.jsx @@ -0,0 +1,44 @@ +import { createGlobalStyle } from "styled-components"; +import reset from "styled-reset"; + +export const GlobalStyle = createGlobalStyle` +${reset} + +:root { + --color-bg: #f5f5f5; + --color-button-bg: #000000; + --color-light-pink: #FFD2D7; + --color-accent: #FF1493; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: rgb(219, 221, 223); /* gradient CSS*/ +} + +button { + cursor: pointer; +} + +* { + box-sizing: border-box; +} + +input { + padding: 0.28rem; + border-radius: 0.5rem; + border: solid; +} + +`; + + diff --git a/Week4/LoginAndSignup/src/style/thinkHW/image.png b/Week4/LoginAndSignup/src/style/thinkHW/image.png new file mode 100644 index 0000000..59463c4 Binary files /dev/null and b/Week4/LoginAndSignup/src/style/thinkHW/image.png differ diff --git a/Week4/LoginAndSignup/src/style/thinkHW/week4.md b/Week4/LoginAndSignup/src/style/thinkHW/week4.md new file mode 100644 index 0000000..f8ae686 --- /dev/null +++ b/Week4/LoginAndSignup/src/style/thinkHW/week4.md @@ -0,0 +1,47 @@ +## 🖤 API 통신에 대하여 + +### ➰ 에러 / 로딩 처리를 하는 방법에는 어떤 것들이 있을까? +> 에러 처리 방법 +처리 방법을 따지기 전에 막는 방법 먼저 살펴보자. +##### 에러 전파 최대한 막기 +- try catch문과 throw 구문을 예로 들면, 이들은 에러가 발생한 곳에서 에러 처리에 대한 권한을 가장 먼저 갖고, 처리할 수 없는 경우 다른 곳으로 처리를 위임하게 된다. => `에러 전파` +- 에러가 전파되는 3가지의 레이어를 정의하자면 다음과 같을 것이다. + 1. 에러가 최초로 발생한 함수 (e.g. fetch) + 2. 1번 함수의 반환값을 표현하는 컴포넌트 + 3. 2번 컴포넌트를 감싸는 에러 바운더리 + +#### 🔍 React ErrorBoundary (에러 처리 방법) +- ErrorBoundary는 데이터를 가져올 때 에러가 발생하면 그 에러에 대한 핸들링 처리를 위임 받을 수 있는 컴포넌트이다. +- try-catch문처럼 동작한다. + +#### 🔍 React Suspense (로딩 처리 방법) +- Suspense는 데이터를 가져올 때 데이터의 준비가 끝나지 않았을 때에는 컴포넌트를 렌더링하지 않고 지정한 컴포넌트를 보여줄 수 있는 컴포넌트를 의미한다. +- children으로 들어간 컴포넌트가 비동기 처리할 때의 처리를 외부인 Suspense로 위임 받을 수 있다. + + +### ➰ 패칭 라이브러리란 무엇이고 어떤 것들이 있을까? + +![Alt text](image.png) + +- React와 함께 사용 가능한 데이터 패칭 라이브러리는 axios, swr, tanstack query(전 react-query), Redux Toolkit Query, Apollo Client가 있다. + + +### ➰ 패칭 라이브러리를 쓰는 이유는 무엇일까? +> Server State 분리의 필요성 + +*Server State: 서버로부터 받아오는 state(e.g. 비동기 로직을 통해 세팅하는 state) +- redux에 server state를 같이 관리하게 되면서 store가 점점 비대해지고, 관심사의 분리가 어렵게 되는 문제들이 발생할 수 있다. +- 또한 DB에 있는 자료들을 프론트에서 렌더링하기 위해 임시적으로 redux store에 자료들을 보관하는 용도인데, 시간이 지날수록 실제 DB의 자료와 redux store에 보관된 자료들의 일관성이 깨질 수 있다. +-> 패칭 라이브러리가 데이터의 일관성 유지를 대신 수행해준다. + +- 간편한 비동기 데이터 요청 +- 상태관리 및 업데이트 용이 +- 캐싱과 최적화 +- 로딩, 오류 처리 +- 서버사이드렌더링(SSR)과의 호환 + + +출처 +https://velog.io/@0715yk/FE-Without-Redux-MiddleWares +https://velog.io/@diso592/%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%A8%EC%B9%AD-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B0%B0%EA%B2%BD%EA%B3%BC-%EA%B0%9C%EB%85%90 + diff --git a/Week4/LoginAndSignup/thinkHW/image.png b/Week4/LoginAndSignup/thinkHW/image.png new file mode 100644 index 0000000..59463c4 Binary files /dev/null and b/Week4/LoginAndSignup/thinkHW/image.png differ diff --git a/Week4/LoginAndSignup/thinkHW/week4.md b/Week4/LoginAndSignup/thinkHW/week4.md new file mode 100644 index 0000000..f8ae686 --- /dev/null +++ b/Week4/LoginAndSignup/thinkHW/week4.md @@ -0,0 +1,47 @@ +## 🖤 API 통신에 대하여 + +### ➰ 에러 / 로딩 처리를 하는 방법에는 어떤 것들이 있을까? +> 에러 처리 방법 +처리 방법을 따지기 전에 막는 방법 먼저 살펴보자. +##### 에러 전파 최대한 막기 +- try catch문과 throw 구문을 예로 들면, 이들은 에러가 발생한 곳에서 에러 처리에 대한 권한을 가장 먼저 갖고, 처리할 수 없는 경우 다른 곳으로 처리를 위임하게 된다. => `에러 전파` +- 에러가 전파되는 3가지의 레이어를 정의하자면 다음과 같을 것이다. + 1. 에러가 최초로 발생한 함수 (e.g. fetch) + 2. 1번 함수의 반환값을 표현하는 컴포넌트 + 3. 2번 컴포넌트를 감싸는 에러 바운더리 + +#### 🔍 React ErrorBoundary (에러 처리 방법) +- ErrorBoundary는 데이터를 가져올 때 에러가 발생하면 그 에러에 대한 핸들링 처리를 위임 받을 수 있는 컴포넌트이다. +- try-catch문처럼 동작한다. + +#### 🔍 React Suspense (로딩 처리 방법) +- Suspense는 데이터를 가져올 때 데이터의 준비가 끝나지 않았을 때에는 컴포넌트를 렌더링하지 않고 지정한 컴포넌트를 보여줄 수 있는 컴포넌트를 의미한다. +- children으로 들어간 컴포넌트가 비동기 처리할 때의 처리를 외부인 Suspense로 위임 받을 수 있다. + + +### ➰ 패칭 라이브러리란 무엇이고 어떤 것들이 있을까? + +![Alt text](image.png) + +- React와 함께 사용 가능한 데이터 패칭 라이브러리는 axios, swr, tanstack query(전 react-query), Redux Toolkit Query, Apollo Client가 있다. + + +### ➰ 패칭 라이브러리를 쓰는 이유는 무엇일까? +> Server State 분리의 필요성 + +*Server State: 서버로부터 받아오는 state(e.g. 비동기 로직을 통해 세팅하는 state) +- redux에 server state를 같이 관리하게 되면서 store가 점점 비대해지고, 관심사의 분리가 어렵게 되는 문제들이 발생할 수 있다. +- 또한 DB에 있는 자료들을 프론트에서 렌더링하기 위해 임시적으로 redux store에 자료들을 보관하는 용도인데, 시간이 지날수록 실제 DB의 자료와 redux store에 보관된 자료들의 일관성이 깨질 수 있다. +-> 패칭 라이브러리가 데이터의 일관성 유지를 대신 수행해준다. + +- 간편한 비동기 데이터 요청 +- 상태관리 및 업데이트 용이 +- 캐싱과 최적화 +- 로딩, 오류 처리 +- 서버사이드렌더링(SSR)과의 호환 + + +출처 +https://velog.io/@0715yk/FE-Without-Redux-MiddleWares +https://velog.io/@diso592/%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%A8%EC%B9%AD-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B0%B0%EA%B2%BD%EA%B3%BC-%EA%B0%9C%EB%85%90 +