diff --git a/package-lock.json b/package-lock.json index ac994863..e68f74f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,9 @@ "react-scripts": "5.0.1", "react-toastify": "^9.1.3", "redux": "^4.2.1", + "shortid": "^2.2.16", "sweetalert2": "^11.7.28", + "tailwind-scrollbar-hide": "^1.1.7", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -15839,6 +15841,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "nanoid": "^2.1.0" + } + }, + "node_modules/shortid/node_modules/nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -16534,6 +16550,11 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tailwind-scrollbar-hide": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz", + "integrity": "sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA==" + }, "node_modules/tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", diff --git a/package.json b/package.json index 3d6ac776..4f26bd25 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "react-scripts": "5.0.1", "react-toastify": "^9.1.3", "redux": "^4.2.1", + "shortid": "^2.2.16", "sweetalert2": "^11.7.28", + "tailwind-scrollbar-hide": "^1.1.7", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/src/App.js b/src/App.js index 61b611c6..642fdcca 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,13 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; import HomePage from './pages/HomePage'; import LoginPage from './pages/LoginPage'; import KakaoOuathPage from './pages/KakaoOuathPage'; -import StartPostPage from './pages/StartPostPage'; +import MyPage from './pages/MyPage'; +import PostListPage from './pages/PostListPage'; +import PostDetailPage from './pages/PostDetailPage'; +import PostWriteIntroPage from './pages/PostWriteIntroPage'; +import PostWritePage from './pages/PostWritePage'; +import ErrorPage from './pages/ErrorPage'; + import './global.css'; function App() { @@ -12,7 +18,12 @@ function App() { } /> } /> } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> ); diff --git a/src/apis/.keep b/src/apis/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/apis/index.js b/src/apis/index.js new file mode 100644 index 00000000..efbb2f24 --- /dev/null +++ b/src/apis/index.js @@ -0,0 +1,80 @@ +import axios from 'axios'; +import Swal from 'sweetalert2'; +import { useNavigate } from 'react-router-dom'; + +const navigate = useNavigate(); + +// 프론트에서 API를 활용하기 위한 기본 axios 인스턴스 +const instance = axios.create({ + baseURL: process.env.REACT_APP_API_URL, // production level 에서는 env에서 baseURL을 넣어주어야함(보안 관련) + timeout: 1000, // 타임아웃이 없으면 무한정 wait가 걸려버릴 수 있음 + headers: { + 'Content-Type': 'application/json', // 서버단에서 JSON 형태를 많이써서, 프론트단에서 쏴줄 때 이러한 형태로 많이 쓴다(헤더 기본 설정) + }, +}); + +// request - 요청 +// 아래와 같이 설정해주면 axios 요청시 자동으로 header에 토큰을 넣어서 보내준다. +// 백엔드에서 받아온 JWT Access Token을 request header에 담아서 보내주는 코드 +instance.interceptors.request.use((config) => { + const token = localStorage.getItem('accessToken'); + if (token) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `${token}`; + } + return config; +}); + +// response - 응답 +// 백엔드로부터 오는 response를 중간에 처리해주는 미들웨어 역할 +// 400번대 에러들에 대한 에러 핸들링 +// 현재 Swal을 통해 보여주는 알림창의 내용 중 text에 해당하는 내용은 백엔드쪽에서 보내주는 내용에 따라 변경예정 +instance.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + // 401 error : 인증되지 않음 - 로그인 화면으로 이동 + // token은 백엔드에서 유효하지 않다면 401(Unauthorized) Http code를 보내주기에, 로그인하도록 처리 + if (error.response.status === 401) { + Swal.fire({ + icon: 'error', + title: '로그인을 진행해주세요!', + text: error.response.data.error.message, + confirmButtonText: '확인', + }).then(() => { + navigate('/login'); + // window.location.href = '/login'; + }); + return Promise.reject(error); + } + + // 404 error : 지정한 리소스를 찾을 수 없음 + // 에러 메시지를 띄워주고 & 잘못된 경로로 이동 시 ErrorPage로 이동 + if (error.response.status === 404) { + Swal.fire({ + icon: 'error', + title: '아이쿠! 에러가 발생했네요😅', + text: error.response.data.error.message, + confirmButtonText: '확인', + }).then(() => { + navigate('/errorPage'); + // window.location.href = '/errorPage'; + }); + return Promise.reject(error); + } + + // 401, 404 외의 다른 error에 대한 처리 및 에러 메시지 확인 가능 + Swal.fire({ + icon: 'error', + title: '내용을 다시 확인해 주세요!', + text: error.response.data.error.message, + confirmButtonText: '확인', + }); + // 성공인지 실패인지 여부에 따라 resolve, reject 처리 + // response를 제대로 받아도 만약 특정 데이터가 없을때 에러로 처리하고 싶다면 reject 처리 + return Promise.reject(error); + }, +); + +export default instance; diff --git a/src/assets/images/postWrite.png b/src/assets/images/postWrite.png new file mode 100644 index 00000000..09b6a388 Binary files /dev/null and b/src/assets/images/postWrite.png differ diff --git a/src/components/atoms/Button.jsx b/src/components/atoms/Button.jsx index 1037b924..3d839815 100644 --- a/src/components/atoms/Button.jsx +++ b/src/components/atoms/Button.jsx @@ -5,9 +5,10 @@ const Button = ({ onClick, disabled, children, + margin, width = 'w-64', height = 'h-8', - bgColor = 'bg-blue-600', + bgColor = 'bg-blue', textColor = 'text-white', bdRadius = 'rounded-lg', }) => { @@ -16,7 +17,7 @@ const Button = ({ type={type || 'button'} onClick={onClick} disabled={disabled} - className={`${width} ${height} ${textColor} ${bgColor} ${bdRadius}`} + className={`${width} ${height} ${textColor} ${bgColor} ${bdRadius} ${margin}`} > {children} diff --git a/src/components/atoms/Card.jsx b/src/components/atoms/Card.jsx index 15a1443b..13d0686e 100644 --- a/src/components/atoms/Card.jsx +++ b/src/components/atoms/Card.jsx @@ -10,17 +10,25 @@ const Card = ({ pickupLocation = '픽업 위치', pickupTip = 3000, deadline = 1696992289, + match = false, to = './', }) => { + const arrowColor = { + color: match ? '#555555' : '#8B8B8B', + }; + return ( - -
+
+
{orderLocation}
- +
{pickupLocation} @@ -28,10 +36,16 @@ const Card = ({
{`${comma(pickupTip)}원`}
-
{date(deadline)}
+
+ {date(deadline)} +
-
- + + {match ?
매칭완료
: ''} +
); }; diff --git a/src/components/atoms/Input.jsx b/src/components/atoms/Input.jsx index 571ce0cd..55b0982c 100644 --- a/src/components/atoms/Input.jsx +++ b/src/components/atoms/Input.jsx @@ -1,4 +1,4 @@ -const Input = ({ type, value, name, placeholder, ...inputProps }) => { +const Input = ({ type, value, name, placeholder, width = 'w-[18rem]', ...inputProps }) => { return ( { value={value} placeholder={placeholder} {...inputProps} - className="w-[20rem] rounded-xl border-gray-300 border-2 px-5 py-2 text-sm block my-4" + className={`${width} rounded-lg border-gray-300 border-2 px-5 py-2 text-sm block my-2`} /> ); }; diff --git a/src/components/atoms/Line.jsx b/src/components/atoms/Line.jsx new file mode 100644 index 00000000..2c6f875f --- /dev/null +++ b/src/components/atoms/Line.jsx @@ -0,0 +1,5 @@ +const Line = () => { + return
; +}; + +export default Line; diff --git a/src/components/atoms/LoginNav.jsx b/src/components/atoms/LoginNav.jsx new file mode 100644 index 00000000..99776151 --- /dev/null +++ b/src/components/atoms/LoginNav.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { BsArrowLeft } from 'react-icons/bs'; +import { MdHome } from 'react-icons/md'; +import { Link, useNavigate } from 'react-router-dom'; + +const LoginNav = () => { + const navigate = useNavigate(); + const goPreviousPage = () => { + navigate(-1); // 바로 이전 페이지로 이동 + }; + + return ( +
+
+ + + + +
+
+ ); +}; + +export default LoginNav; diff --git a/src/components/atoms/MinusBtn.jsx b/src/components/atoms/MinusBtn.jsx new file mode 100644 index 00000000..3039450e --- /dev/null +++ b/src/components/atoms/MinusBtn.jsx @@ -0,0 +1,7 @@ +import { PiMinus } from 'react-icons/pi'; + +const MinusBtn = ({ className, onClick }) => { + return ; +}; + +export default MinusBtn; diff --git a/src/components/atoms/Nav.jsx b/src/components/atoms/Nav.jsx index 239f766d..c13c9962 100644 --- a/src/components/atoms/Nav.jsx +++ b/src/components/atoms/Nav.jsx @@ -7,7 +7,7 @@ import logo from '../../assets/images/logo.png'; const Nav = () => { return (
-