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() {
} />
} />
} />
+ } />
} />
} />