diff --git a/src/components/ApplyBtn/index.tsx b/src/components/ApplyBtn/index.tsx new file mode 100644 index 0000000..aade53e --- /dev/null +++ b/src/components/ApplyBtn/index.tsx @@ -0,0 +1,27 @@ +import { Button } from '@mui/material'; + +interface ApplyBtnProp { + status: string; +} +export default function ApplyBtn({ status }: ApplyBtnProp) { + if (status === '대여가능') { + return ( + + ); + } + if (status === '대여불가능') { + return ( + + ); + } + if (status === '대여중') { + return ( + + ); + } + return null; +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 31ad53e..60601cb 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,19 +1,17 @@ -import { useRecoilValue } from 'recoil'; import { Link, useNavigate } from 'react-router-dom'; import { useState } from 'react'; import { Typography, Box, Button } from '@mui/material'; import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import ConfirmDialog from '../ConfirmDialog'; -import { userAtom } from '../../recoil/atom'; export default function Header() { const navigate = useNavigate(); const isAuthenticated = sessionStorage.getItem('accessToken'); + const userNickname = sessionStorage.getItem('userNickname'); const [isOpen, setIsOpen] = useState(false); - const userState = useRecoilValue(userAtom); const handleLogout = (e: React.MouseEvent) => { e.preventDefault(); sessionStorage.removeItem('accessToken'); @@ -56,7 +54,7 @@ export default function Header() { {isAuthenticated ? ( - {userState.nickname} 님 + {userNickname} 님 diff --git a/src/components/StatusSign/index.tsx b/src/components/StatusSign/index.tsx new file mode 100644 index 0000000..5f40158 --- /dev/null +++ b/src/components/StatusSign/index.tsx @@ -0,0 +1,73 @@ +import { Box, Typography } from '@mui/material'; + +interface StatusSignProp { + status: string; +} +export default function StatusSign({ status }: StatusSignProp) { + if (status === '대여가능') { + return ( + + + {status} + + ); + } + if (status === '대여불가능') { + return ( + + + {status} + + ); + } + if (status === '대여중') { + return ( + + + {status} + + ); + } + if (status === '공개중') { + return ( + + + {status} + + ); + } + + return null; +} diff --git a/src/components/WishBtn/index.tsx b/src/components/WishBtn/index.tsx new file mode 100644 index 0000000..454a830 --- /dev/null +++ b/src/components/WishBtn/index.tsx @@ -0,0 +1,18 @@ +import { IconButton } from '@mui/material'; +import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; +import FavoriteIcon from '@mui/icons-material/Favorite'; + +interface WishBtnProps { + handleWish: React.MouseEventHandler; + isWished: boolean; +} + +const WishBtn: React.FC = ({ handleWish, isWished = false }: WishBtnProps) => { + return ( + + {isWished ? : } + + ); +}; + +export default WishBtn; diff --git a/src/data/messages.ts b/src/data/messages.ts index b600bc9..0fa35f2 100644 --- a/src/data/messages.ts +++ b/src/data/messages.ts @@ -5,3 +5,13 @@ export const LOGIN_MESSAGE = { export const SEARCH_MESSAGE = { SEARCH_FAIL: '검색에 실패하였습니다 :(', }; +export const CLOTHES_MESSAGE = { + CLOTHES_NOT_FOUND: '옷을 찾을 수 없어요 :(', + CLOTHES_DELETED: '옷이 삭제되었어요', + CLOTHES_EDITED: '옷 정보가 수정되었어요', +}; +export const WISH_MESSAGE = { + WISH_CREATED: '찜한 옷에 추가되었어요', + WISH_DELETED: '찜한 옷에서 삭제되었어요', + WISH_FAIL: '알 수 없는 오류가 발생했습니다', +}; diff --git a/src/hooks/api/auth/login.ts b/src/hooks/api/auth/login.ts index 967d26c..e221b54 100644 --- a/src/hooks/api/auth/login.ts +++ b/src/hooks/api/auth/login.ts @@ -14,6 +14,8 @@ export async function postLoginAPICall(values: Login) { if (response.status === 200) { const { id, nickname, isBannded, accessToken } = response.data; sessionStorage.setItem('accessToken', accessToken); + sessionStorage.setItem('userId', id); + sessionStorage.setItem('userNickname', nickname); enqueueSnackbar(`${nickname}님 안녕하세요 :)`, { variant: 'success' }); if (isBannded === true) { enqueueSnackbar(`현재 ${nickname}님의 계정은 사용 정지되었습니다.`, { variant: 'warning' }); diff --git a/src/hooks/api/clothes/clothes.ts b/src/hooks/api/clothes/clothes.ts new file mode 100644 index 0000000..2a48a9f --- /dev/null +++ b/src/hooks/api/clothes/clothes.ts @@ -0,0 +1,101 @@ +import { enqueueSnackbar } from 'notistack'; +import axios, { AxiosError } from 'axios'; + +import { CLOTHES_MESSAGE } from '../../../data/messages'; + +interface GetClothesParams { + clothesId: number; + token?: string; +} +interface Reviewer { + id?: number; + username: string; + nickname?: string; +} +interface Review { + review: string; + reviewer: Reviewer; +} +interface Owner { + id?: number; + nickname?: string; + location?: string; +} +export interface GetClothesResponse { + id: number; + description: string; + category: string; + season: string; + status: string; + isOpen: boolean; + name: string; + tag: string; + image: string; + owner: Owner; + review: Review[]; + isWished: boolean; +} + +export async function getClothesAPICall({ clothesId, token }: GetClothesParams) { + try { + const response = await axios.get(`${process.env.REACT_APP_API_URL}/clothes/${clothesId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.status === 200) { + return response.data; + } + } catch (err) { + if (err instanceof AxiosError) { + enqueueSnackbar(err.response?.data?.message ?? CLOTHES_MESSAGE.CLOTHES_NOT_FOUND, { variant: 'error' }); + } else { + enqueueSnackbar(CLOTHES_MESSAGE.CLOTHES_NOT_FOUND, { variant: 'error' }); + } + } + return null; +} + +export async function deleteClothesAPICall({ clothesId, token }: GetClothesParams) { + try { + const response = await axios.delete(`${process.env.REACT_APP_API_URL}/clothes/${clothesId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.status === 200) { + enqueueSnackbar(CLOTHES_MESSAGE.CLOTHES_DELETED, { variant: 'success' }); + } + } catch (err) { + if (err instanceof AxiosError) { + enqueueSnackbar(err.response?.data?.message ?? CLOTHES_MESSAGE.CLOTHES_NOT_FOUND, { variant: 'error' }); + } else { + enqueueSnackbar(CLOTHES_MESSAGE.CLOTHES_NOT_FOUND, { variant: 'error' }); + } + } +} +interface Clothes { + category: string; + season: string; + status: string; + isOpen: boolean; + name: string; + tag: string; +} +interface EditClothesParams { + clothesId: number; + token?: string; + clothes: Clothes; +} +export async function editClothesAPICall({ clothesId, token, clothes }: EditClothesParams) { + try { + const response = await axios.put(`${process.env.REACT_APP_API_URL}/clothes/${clothesId}`, clothes, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.status === 200) { + enqueueSnackbar(CLOTHES_MESSAGE.CLOTHES_EDITED, { variant: 'success' }); + } + } catch (err) { + if (err instanceof AxiosError) { + enqueueSnackbar(err.response?.data?.message ?? CLOTHES_MESSAGE.CLOTHES_NOT_FOUND, { variant: 'error' }); + } else { + enqueueSnackbar(CLOTHES_MESSAGE.CLOTHES_NOT_FOUND, { variant: 'error' }); + } + } +} diff --git a/src/hooks/api/wish/wish.ts b/src/hooks/api/wish/wish.ts new file mode 100644 index 0000000..3a05463 --- /dev/null +++ b/src/hooks/api/wish/wish.ts @@ -0,0 +1,55 @@ +import { enqueueSnackbar } from 'notistack'; +import axios, { AxiosError } from 'axios'; + +import { WISH_MESSAGE } from '../../../data/messages'; + +interface WishAPIParams { + clothesId: number; + token: string; +} + +export async function createWishAPICall({ clothesId, token }: WishAPIParams) { + try { + const response = await axios.post( + `${process.env.REACT_APP_API_URL}/wish/${clothesId}`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + if (response.status === 200) { + enqueueSnackbar(WISH_MESSAGE.WISH_CREATED, { variant: 'success' }); + } + } catch (err) { + if (err instanceof AxiosError) { + enqueueSnackbar(err.response?.data?.message ?? WISH_MESSAGE.WISH_FAIL, { variant: 'error' }); + } else { + enqueueSnackbar(WISH_MESSAGE.WISH_FAIL, { variant: 'error' }); + } + } +} + +export async function deleteWishAPICall({ clothesId, token }: WishAPIParams) { + try { + const response = await axios.put( + `${process.env.REACT_APP_API_URL}/wish/${clothesId}`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + if (response.status === 200) { + enqueueSnackbar(WISH_MESSAGE.WISH_DELETED, { variant: 'success' }); + } + } catch (err) { + if (err instanceof AxiosError) { + enqueueSnackbar(err.response?.data?.message ?? WISH_MESSAGE.WISH_FAIL, { variant: 'error' }); + } else { + enqueueSnackbar(WISH_MESSAGE.WISH_FAIL, { variant: 'error' }); + } + } +} diff --git a/src/models/enum.ts b/src/models/enum.ts index e7efd04..fb23029 100644 --- a/src/models/enum.ts +++ b/src/models/enum.ts @@ -27,8 +27,4 @@ export enum Season { WINTER = '겨울', } -export enum Status { - AVAILABLE = '대여가능', - UNAVAILABLE = '대여불가능', - RENTED = '대여중', -} +export const StatusEnums = ['대여가능', '대여중', '대여불가능']; diff --git a/src/pages/Clothes/index.tsx b/src/pages/Clothes/index.tsx new file mode 100644 index 0000000..1e3c7cd --- /dev/null +++ b/src/pages/Clothes/index.tsx @@ -0,0 +1,223 @@ +import { useParams } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { Box, Card, Chip, Divider, MenuItem, Select, Typography } from '@mui/material'; +import LocationOnIcon from '@mui/icons-material/LocationOn'; + +import { StatusEnums } from '../../models/enum'; +import { createWishAPICall, deleteWishAPICall } from '../../hooks/api/wish/wish'; +import { + GetClothesResponse, + deleteClothesAPICall, + editClothesAPICall, + getClothesAPICall, +} from '../../hooks/api/clothes/clothes'; +import WishBtn from '../../components/WishBtn'; +import StatusSign from '../../components/StatusSign'; +import ConfirmDialog from '../../components/ConfirmDialog'; +import CancelSubmitBtns from '../../components/CancelSubmitBtn'; +import ApplyBtn from '../../components/ApplyBtn'; + +// TODO: 사용자 페이지 링크 추가 +export function ClothesPage() { + const { id } = useParams(); + const clothesId = Number(id); + const userId = Number(sessionStorage.getItem('userId')); + const token = sessionStorage.getItem('accessToken') ?? ''; + const [isWish, setIsWish] = useState(false); + + const [confirmDialogIsOpen, setConfirmDialogIsOpen] = useState(false); + + const [clothes, setClothes] = useState(null); + const getClothes = async () => { + try { + const result = await getClothesAPICall({ clothesId, token }); + setClothes(result); + } catch (error) { + console.error(error); + } + }; + useEffect(() => { + getClothes(); + }, [clothesId]); + useEffect(() => { + if (clothes) { + setIsWish(clothes?.isWished ?? false); + } + }, [clothes]); + const isMyClothes = userId === clothes?.owner.id; + + const handleEdit = () => {}; + const handleDeleteBtnClick = () => { + setConfirmDialogIsOpen(true); + }; + const handleDelete = () => { + deleteClothesAPICall({ clothesId, token }); + setConfirmDialogIsOpen(false); + }; + const handleCancel = () => { + setConfirmDialogIsOpen(false); + }; + const handleWish = async () => { + try { + if (isWish) { + await deleteWishAPICall({ clothesId, token }); + } else { + await createWishAPICall({ clothesId, token }); + } + setIsWish(!isWish); + } catch (error) { + console.error(error); + } + }; + const handleStatusChange = async (value: string) => { + try { + await editClothesAPICall({ + clothesId, + token, + clothes: { + category: clothes?.category || '', + season: clothes?.season || '', + status: value, + isOpen: clothes?.isOpen || false, + name: clothes?.name || '', + tag: clothes?.tag || '', + }, + }); + getClothes(); + } catch (error) { + console.error(error); + } + }; + const handleIsOpenChange = async (value: string) => { + try { + await editClothesAPICall({ + clothesId, + token, + clothes: { + category: clothes?.category || '', + season: clothes?.season || '', + status: clothes?.status || '', + isOpen: value === 'true', + name: clothes?.name || '', + tag: clothes?.tag || '', + }, + }); + getClothes(); + } catch (error) { + console.error(error); + } + }; + + return ( + <> + + + + {clothes?.image ? ( + + ) : ( + + )} + {isMyClothes ? ( + + ) : ( + + )} + + + {isMyClothes ? ( + + + + + ) : ( + + )} + + + {clothes?.name} + + {clothes?.category} + {!isMyClothes && } + + {!isMyClothes && ( + + + {clothes?.owner.nickname}님 + + + {clothes?.owner.location} + + )} + + + + 상품정보 + + + {clothes?.description} + + + + 리뷰 + + + {clothes?.review.map((review) => ( + + {review.reviewer.nickname} | {review.review} + + ))} + + + + + + + ); +} diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 5a7466d..5a04727 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,14 +1,11 @@ -import { useSetRecoilState } from 'recoil'; import { Link, useNavigate } from 'react-router-dom'; import { useState } from 'react'; import { Box, Button, TextField, Typography } from '@mui/material'; -import { userAtom } from '../../recoil/atom'; import { postLoginAPICall } from '../../hooks/api/auth/login'; export function LoginPage() { const navigate = useNavigate(); - const setUserState = useSetRecoilState(userAtom); const [values, setValues] = useState({ username: '', password: '', @@ -24,11 +21,9 @@ export function LoginPage() { const handleSubmit: React.FormEventHandler = async (e) => { e.preventDefault(); try { - const response = await postLoginAPICall(values); - if (response) { - setUserState({ id: response.id, nickname: response.nickname }); - navigate('/'); - } + await postLoginAPICall(values); + navigate('/'); + console.log(sessionStorage.getItem('accessToken')); } catch (error) { // console.error(error); } diff --git a/src/recoil/atom.ts b/src/recoil/atom.ts deleted file mode 100644 index fa2c22c..0000000 --- a/src/recoil/atom.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from 'recoil'; - -import { UserStandard } from '../models/user'; - -export const userAtom = atom({ - key: 'user', - default: { id: 0, nickname: '' }, -}); diff --git a/src/route/index.tsx b/src/route/index.tsx index d14415a..a4124e3 100644 --- a/src/route/index.tsx +++ b/src/route/index.tsx @@ -5,6 +5,7 @@ import { ProfilePage } from '../pages/Profile'; import MyPage from '../pages/Mypage'; import { MainPage } from '../pages/Main'; import { LoginPage } from '../pages/Login'; +import { ClothesPage } from '../pages/Clothes'; /** * 어느 url에 어떤 페이지를 보여줄지 정해주는 컴포넌트입니다. @@ -16,6 +17,7 @@ export function RouteComponent() { } /> } /> } /> + } /> } /> } />