diff --git a/index.html b/index.html index c950b882..d296974a 100644 --- a/index.html +++ b/index.html @@ -1,53 +1,73 @@ + + + + + + + - - - - - - - + + + + - - - - + + + - - - + Lecue + - Lecue - + + + - - - + +
+
+
+ + - -
-
- - - - - - - - \ No newline at end of file + + + + diff --git a/src/EditNickname/api/patchNickname.ts b/src/EditNickname/api/patchNickname.ts new file mode 100644 index 00000000..08dd9ca3 --- /dev/null +++ b/src/EditNickname/api/patchNickname.ts @@ -0,0 +1,16 @@ +import { api } from '../../libs/api'; + +export const patchNickname = async (token: string, nickname: string) => { + const response = await api.patch( + '/api/nickname', + { nickname: nickname }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }, + ); + + return { code: response.data.code }; +}; diff --git a/src/EditNickname/components/EditButton/EditButton.style.ts b/src/EditNickname/components/EditButton/EditButton.style.ts new file mode 100644 index 00000000..1ded588b --- /dev/null +++ b/src/EditNickname/components/EditButton/EditButton.style.ts @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +export const ButtonWrapper = styled.section` + display: flex; + justify-content: center; + align-items: end; + + width: 100%; + height: calc(100% - 12.1rem); + margin-bottom: 2rem; +`; diff --git a/src/EditNickname/components/EditButton/index.tsx b/src/EditNickname/components/EditButton/index.tsx new file mode 100644 index 00000000..5e5a758b --- /dev/null +++ b/src/EditNickname/components/EditButton/index.tsx @@ -0,0 +1,43 @@ +import Button from '../../../components/common/Button'; +import usePatchNickname from '../../hooks/usePatchNickname'; +import { EditButtonProps } from '../../types/editNicknameTypes'; +import * as S from './EditButton.style'; + +function EditButton({ + isActive, + token, + nickname, + handleSetIsValid, + handleSetIsActive, +}: EditButtonProps) { + const patchMutation = usePatchNickname({ + handleSetIsValid, + handleSetIsActive, + token, + nickname, + }); + + const handelClickSubmitBtn = (token: string, nickname: string) => { + const patchNickname = nickname.trim(); + + patchMutation.mutate({ + nickname: patchNickname, + token: token, + }); + }; + + return ( + + + + ); +} + +export default EditButton; diff --git a/src/EditNickname/components/NicknameInput/NicknameInput.style.ts b/src/EditNickname/components/NicknameInput/NicknameInput.style.ts new file mode 100644 index 00000000..eb4d8fd8 --- /dev/null +++ b/src/EditNickname/components/NicknameInput/NicknameInput.style.ts @@ -0,0 +1,56 @@ +import styled from '@emotion/styled'; + +export const NicknameInputWrapper = styled.section` + display: flex; + align-items: center; + flex-direction: column; + + width: 100%; + margin-top: 1.2rem; +`; + +export const InputContainer = styled.div<{ + isEmpty: boolean; + isValid: string; +}>` + display: flex; + gap: 1.6rem; + justify-content: space-between; + align-items: center; + + width: 100%; + padding: 1.9rem 2rem; + + ${({ theme }) => theme.fonts.Body3_R_14}; + + border: 0.1rem solid + ${({ theme, isEmpty, isValid }) => + isValid === 'special' || isValid === 'duplicate' || isValid === 'space' + ? theme.colors.red + : isEmpty || isValid === 'enter' + ? theme.colors.LG + : theme.colors.BG}; + border-radius: 0.8rem; + background-color: ${({ theme }) => theme.colors.white}; +`; + +export const Input = styled.input<{ isValid: string }>` + width: 100%; + + color: ${({ theme, isValid }) => + isValid === 'enter' ? theme.colors.MG : theme.colors.BG}; + ${({ theme }) => theme.fonts.Body2_M_14}; +`; + +export const WordCount = styled.p` + color: ${({ theme }) => theme.colors.WG}; + ${({ theme }) => theme.fonts.E_Body2_R_14}; +`; + +export const WarnigMsg = styled.p` + width: 100%; + padding-top: 0.9rem; + + ${({ theme }) => theme.fonts.Caption1_R_12}; + color: ${({ theme }) => theme.colors.red}; +`; diff --git a/src/EditNickname/components/NicknameInput/index.tsx b/src/EditNickname/components/NicknameInput/index.tsx new file mode 100644 index 00000000..0a9123c1 --- /dev/null +++ b/src/EditNickname/components/NicknameInput/index.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react'; + +import { NicknameInputProps } from '../../types/editNicknameTypes'; +import handleChangeInput from '../../utils/handleCheckInput'; +import * as S from './NicknameInput.style'; + +function NicknameInput({ + nickname, + isValid, + handleSetNickname, + handleSetIsValid, +}: NicknameInputProps) { + const [wordCnt, setWordCnt] = useState(0); + const currentNickname: string = localStorage.getItem('nickname') || ''; + + const handleSetWordCnt = (wordCnt: number) => { + setWordCnt(wordCnt); + }; + + return ( + + + { + handleChangeInput({ + handleSetNickname, + handleSetWordCnt, + handleSetIsValid, + currentNickname, + e, + }); + }} + /> + ({wordCnt}/8) + + {isValid === 'special' ? ( + 특수문자/이모지는 사용 불가능해요 + ) : isValid === 'duplicate' ? ( + 이미 있는 닉네임이에요 + ) : ( + isValid === 'space' && ( + 마지막 공백 제외 2자 이상 입력해주세요 + ) + )} + + ); +} + +export default NicknameInput; diff --git a/src/EditNickname/constants/.gitkeep b/src/EditNickname/constants/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/EditNickname/hooks/usePatchNickname.ts b/src/EditNickname/hooks/usePatchNickname.ts new file mode 100644 index 00000000..f31a1331 --- /dev/null +++ b/src/EditNickname/hooks/usePatchNickname.ts @@ -0,0 +1,43 @@ +import { AxiosError } from 'axios'; +import { useMutation } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import { patchNickname } from '../api/patchNickname'; +import { + patchNicknameProps, + usePatchNicknameProps, +} from '../types/editNicknameTypes'; + +const usePatchNickname = (props: usePatchNicknameProps) => { + const { handleSetIsValid, handleSetIsActive, token, nickname } = props; + + const navigate = useNavigate(); + + const mutation = useMutation({ + mutationFn: async ({ token, nickname }: patchNicknameProps) => { + return await patchNickname(token, nickname); + }, + onError: (err: AxiosError) => { + const code = err.response?.status; + if (code === 409) { + // 닉네임 중복코드 : 409 + handleSetIsValid('duplicate'); + handleSetIsActive(false); + } else if (code === 400) { + handleSetIsValid('space'); + handleSetIsActive(false); + } else { + navigate('/error'); + } + }, + onSuccess: () => { + window.localStorage.setItem('token', token); + window.localStorage.setItem('nickname', nickname); + navigate('/'); + }, + }); + + return mutation; +}; + +export default usePatchNickname; diff --git a/src/EditNickname/page/EditNickname.style.ts b/src/EditNickname/page/EditNickname.style.ts new file mode 100644 index 00000000..bbb446a2 --- /dev/null +++ b/src/EditNickname/page/EditNickname.style.ts @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +export const EditNicknameBodyWrapper = styled.div` + display: flex; + align-items: center; + flex-direction: column; + + width: 100%; + height: calc(100dvh - 5.4rem); + padding: 0 1.6rem; + margin-top: 5.4rem; +`; + +export const NicknameInputSection = styled.section` + width: 100%; + margin-top: 3.4rem; +`; + +export const NicknameInputSectionTitle = styled.h2` + color: ${({ theme }) => theme.colors.BG}; + + ${({ theme }) => theme.fonts.Head2_SB_18} +`; diff --git a/src/EditNickname/page/index.tsx b/src/EditNickname/page/index.tsx new file mode 100644 index 00000000..86d813a4 --- /dev/null +++ b/src/EditNickname/page/index.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +import Header from '../../components/common/Header'; +import EditButton from '../components/EditButton'; +import NicknameInput from '../components/NicknameInput'; +import { isValidState } from '../types/editNicknameTypes'; +import * as S from './EditNickname.style'; + +function EditNickname() { + const [isActive, setIsActive] = useState(false); + const [nickname, setNickname] = useState( + localStorage.getItem('nickname') || '', + ); + const [isValid, setIsValid] = useState('enter'); + + const { state } = useLocation(); + + const handleSetNickname = (nickname: string) => { + setNickname(nickname); + }; + + const handleSetIsValid = (isValid: isValidState) => { + setIsValid(isValid); + }; + + const handleSetIsActive = (isActive: boolean) => { + setIsActive(isActive); + }; + + useEffect(() => { + isValid === 'valid' ? handleSetIsActive(true) : handleSetIsActive(false); + }, [isValid]); + + return ( + +
+ + + 나의 닉네임 + + + + + + ); +} + +export default EditNickname; diff --git a/src/EditNickname/types/editNicknameTypes.ts b/src/EditNickname/types/editNicknameTypes.ts new file mode 100644 index 00000000..d54509cd --- /dev/null +++ b/src/EditNickname/types/editNicknameTypes.ts @@ -0,0 +1,41 @@ +export type isValidState = + | 'valid' + | 'special' + | 'duplicate' + | 'space' + | 'enter'; + +export interface NicknameInputProps { + nickname: string; + isValid: string; + handleSetNickname: (nickname: string) => void; + handleSetIsActive: (isActive: boolean) => void; + handleSetIsValid: (isValid: isValidState) => void; +} + +export interface EditButtonProps { + token: string; + nickname: string; + isActive: boolean; + isValid: string; + handleSetIsValid: (isValid: isValidState) => void; + handleSetIsActive: (isActive: boolean) => void; +} + +export interface CheckNicknameProps { + handleSetNickname: (nickname: string) => void; + handleSetWordCnt: (wordCnt: number) => void; + handleSetIsValid: (isValid: isValidState) => void; + currentNickname: string; + e: React.ChangeEvent; +} + +export interface patchNicknameProps { + token: string; + nickname: string; +} + +export interface usePatchNicknameProps extends patchNicknameProps { + handleSetIsActive: (isActive: boolean) => void; + handleSetIsValid: (isValid: isValidState) => void; +} diff --git a/src/EditNickname/utils/checkInputRange.ts b/src/EditNickname/utils/checkInputRange.ts new file mode 100644 index 00000000..ae995c51 --- /dev/null +++ b/src/EditNickname/utils/checkInputRange.ts @@ -0,0 +1,7 @@ +/** 영어, 숫자, 문자, 공백인지 체크하는 정규식 함수 */ +const checkInputRange = (str: string) => { + const regExp = /[ㄱ-ㅎㅏ-ㅣ가-힣0-9a-zA-Z\s]/g; + return regExp.test(str) || str.length === 0; +}; + +export default checkInputRange; diff --git a/src/EditNickname/utils/handleCheckInput.ts b/src/EditNickname/utils/handleCheckInput.ts new file mode 100644 index 00000000..289307a1 --- /dev/null +++ b/src/EditNickname/utils/handleCheckInput.ts @@ -0,0 +1,30 @@ +import { CheckNicknameProps } from '../types/editNicknameTypes'; +import checkInputRange from './checkInputRange'; + +/** 8자 이하 & 한글, 영어, 숫자만 입력 가능하도록 & 첫번째 글자는 공백 불가능 체크 함수*/ +const handleChangeInput = (props: CheckNicknameProps) => { + const { handleSetNickname, handleSetWordCnt, handleSetIsValid, e } = props; + + const input = e.target.value; + + if (e.target.value.length <= 8 && checkInputRange(input[input.length - 1])) { + if (e.target.value === ' ') { + handleSetNickname(''); + handleSetWordCnt(0); + } else if (e.target.value.trim().length < 2) { + handleSetNickname(e.target.value); + handleSetWordCnt(e.target.value.length); + handleSetIsValid('space'); + } else { + handleSetNickname(e.target.value); + handleSetIsValid('valid'); + handleSetWordCnt(e.target.value.length); + } + } else { + e.target.value.length > 8 + ? handleSetIsValid('valid') + : handleSetIsValid('special'); + } +}; + +export default handleChangeInput; diff --git a/src/Enter/api/.gitkeep b/src/Enter/api/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Enter/components/.gitkeep b/src/Enter/components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Enter/constants/.gitkeep b/src/Enter/constants/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Enter/hooks/.gitkeep b/src/Enter/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Enter/page/Enter.style.ts b/src/Enter/page/Enter.style.ts new file mode 100644 index 00000000..366653b8 --- /dev/null +++ b/src/Enter/page/Enter.style.ts @@ -0,0 +1,65 @@ +import styled from '@emotion/styled'; + +export const MypageBodyWrapper = styled.div` + width: 100%; + margin-top: 5.4rem; +`; + +export const NicknameWrapper = styled.div<{ variant?: string }>` + display: flex; + justify-content: space-between; + align-items: center; + + height: 8.9rem; + padding: 3.2rem 1.6rem 2.9rem; + + border-bottom: 0.6rem solid ${({ theme }) => theme.colors.LG_2}; + cursor: ${({ variant }) => variant === 'login' && 'pointer'}; +`; +export const NicknameText = styled.span` + color: ${({ theme }) => theme.colors.BG}; + ${({ theme }) => theme.fonts.Head1_B_20}; +`; +export const MenuWrapper = styled.div` + display: flex; + flex-direction: column; + + padding: 2rem 1.6rem 1rem; +`; + +export const Line = styled.div` + width: 100%; + height: 0.1rem; + + background-color: ${({ theme }) => theme.colors.LG}; +`; +export const Tab = styled.li` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 3.2rem; + margin: 0.525rem 0; + cursor: pointer; +`; +export const SubTitle = styled.span` + padding-bottom: 1em; + + color: ${({ theme }) => theme.colors.WG}; + ${({ theme }) => theme.fonts.Caption2_SB_12}; +`; +export const Link = styled.a` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 3.2rem; + margin: 0.525rem 0; + cursor: pointer; +`; +export const Text = styled.span` + ${({ theme }) => theme.fonts.Title2_M_16}; + color: ${({ theme }) => theme.colors.BG}; +`; diff --git a/src/Enter/page/index.tsx b/src/Enter/page/index.tsx new file mode 100644 index 00000000..ba7b1d3a --- /dev/null +++ b/src/Enter/page/index.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { IcMypageArrowRight } from '../../assets'; +import Header from '../../components/common/Header'; +import useGetMyNickName from '../../libs/hooks/useGetMyNickname'; +import * as S from './Enter.style'; + +function Enter() { + const [nickname, setNickname] = useState(''); + const navigate = useNavigate(); + const { state } = useLocation(); + if (state) { + const { myNickName } = useGetMyNickName(); + if (nickname === '' || nickname !== myNickName) setNickname(myNickName); + } + + const handleClickNickname = () => { + navigate('edit-nickname', { state: state }); + }; + const handleClickHistory = () => { + navigate('select-history'); + }; + const handleClickLogin = () => { + navigate('/login'); + }; + + const handleClickLogout = () => { + const isLogout = confirm('로그아웃하시겠습니까?'); + + if (isLogout) { + window.localStorage.clear(); + navigate('/', { state: { step: 1 } }); + } + }; + + return ( + +
+ {state ? ( + + + {nickname}님, 안녕하세요 + + + 프로필 + + 닉네임 수정 + + + + 내 기록보기 + + + + + + 서비스 이용 방침 + + 팀 소개 + + + + 공지사항 + + + + 문의하기 + + + + + + 기타 + + 로그아웃 + + + + + ) : ( + + + 로그인하세요 + + + + 서비스 이용 방침 + + 팀 소개 + + + + 공지사항 + + + + 문의하기 + + + + + )} + + ); +} + +export default Enter; diff --git a/src/Mypage/api/deleteMyBook.ts b/src/History/api/deleteMyBook.ts similarity index 100% rename from src/Mypage/api/deleteMyBook.ts rename to src/History/api/deleteMyBook.ts diff --git a/src/Mypage/api/getMyBookList.ts b/src/History/api/getMyBookList.ts similarity index 89% rename from src/Mypage/api/getMyBookList.ts rename to src/History/api/getMyBookList.ts index 09a063d3..7b111402 100644 --- a/src/Mypage/api/getMyBookList.ts +++ b/src/History/api/getMyBookList.ts @@ -8,5 +8,6 @@ export async function getMyBookList() { Authorization: `Bearer ${token}`, }, }); - return data.data.data.bookList; + + return data.data.data; } diff --git a/src/History/api/getMyFavorite.ts b/src/History/api/getMyFavorite.ts new file mode 100644 index 00000000..8c6d3a84 --- /dev/null +++ b/src/History/api/getMyFavorite.ts @@ -0,0 +1,13 @@ +import { api } from '../../libs/api'; + +export async function getMyFavorite() { + const token = localStorage.getItem('token'); + const data = await api.get(`/api/mypage/favorite`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + return data.data.data; +} diff --git a/src/Mypage/api/getMyNoteList.ts b/src/History/api/getMyNoteList.ts similarity index 89% rename from src/Mypage/api/getMyNoteList.ts rename to src/History/api/getMyNoteList.ts index 188b66a8..9f25ccc2 100644 --- a/src/Mypage/api/getMyNoteList.ts +++ b/src/History/api/getMyNoteList.ts @@ -8,5 +8,5 @@ export async function getMyNoteList() { Authorization: `Bearer ${token}`, }, }); - return data.data.data.noteList; + return data.data.data; } diff --git a/src/History/components/HistoryEmptyView/HistoryEmptyView.style.ts b/src/History/components/HistoryEmptyView/HistoryEmptyView.style.ts new file mode 100644 index 00000000..e6ec0edc --- /dev/null +++ b/src/History/components/HistoryEmptyView/HistoryEmptyView.style.ts @@ -0,0 +1,26 @@ +import styled from '@emotion/styled'; + +export const HistoryEmptyViewWrapper = styled.section` + display: flex; + justify-content: center; + + width: 100%; + height: calc(100dvh - 13.62rem); +`; + +export const HistoryEmptyViewNotice = styled.div` + display: flex; + align-items: center; + flex-direction: column; + + margin-top: 12.3rem; +`; + +export const HistoryEmptyViewText = styled.p` + margin-top: 3rem; + + color: ${({ theme }) => theme.colors.WG}; + + text-align: center; + ${({ theme }) => theme.fonts.Body2_M_14}; +`; diff --git a/src/History/components/HistoryEmptyView/index.tsx b/src/History/components/HistoryEmptyView/index.tsx new file mode 100644 index 00000000..84e4f0aa --- /dev/null +++ b/src/History/components/HistoryEmptyView/index.tsx @@ -0,0 +1,23 @@ +import { ImgMyPageNotexist } from '../../../assets'; +import { HistoryEmptyViewProps } from '../../types/historyType'; +import * as S from './HistoryEmptyView.style'; + +function HistoryEmptyView({ + topLineText, + bottomLineText, +}: HistoryEmptyViewProps) { + return ( + + + + + {topLineText} +
+ {bottomLineText} +
+
+
+ ); +} + +export default HistoryEmptyView; diff --git a/src/History/components/MyFavoriteBookList/MyFavoriteBookList.style.ts b/src/History/components/MyFavoriteBookList/MyFavoriteBookList.style.ts new file mode 100644 index 00000000..620cab98 --- /dev/null +++ b/src/History/components/MyFavoriteBookList/MyFavoriteBookList.style.ts @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; + +export const MyFavoriteBookListWrapper = styled.section` + display: grid; + gap: 2rem; + grid-template-columns: repeat(3, 1fr); + justify-items: center; + + padding-bottom: 2rem; +`; diff --git a/src/History/components/MyFavoriteBookList/index.tsx b/src/History/components/MyFavoriteBookList/index.tsx new file mode 100644 index 00000000..c5b2975c --- /dev/null +++ b/src/History/components/MyFavoriteBookList/index.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import LecueBook from '../../../components/common/LecueBook'; +import useGetMyFavorite from '../../hooks/useGetMyFavorite'; +import { FavoriteBookProps } from '../../types/historyType'; +import HistoryEmptyView from '../HistoryEmptyView'; +import * as S from './MyFavoriteBookList.style'; + +function MyFavoriteBookList() { + const { myFavoriteList } = useGetMyFavorite(); + + return ( + + {myFavoriteList && myFavoriteList.length !== 0 ? ( + + {myFavoriteList.map((book: FavoriteBookProps) => { + return ( + + ); + })} + + ) : ( + + )} + + ); +} + +export default MyFavoriteBookList; diff --git a/src/Mypage/components/LecueBook/LecueBook.style.ts b/src/History/components/MyLecueBook/MyLecueBook.style.ts similarity index 75% rename from src/Mypage/components/LecueBook/LecueBook.style.ts rename to src/History/components/MyLecueBook/MyLecueBook.style.ts index be02ab3a..49c6eab0 100644 --- a/src/Mypage/components/LecueBook/LecueBook.style.ts +++ b/src/History/components/MyLecueBook/MyLecueBook.style.ts @@ -8,15 +8,15 @@ export const Wrapper = styled.li` width: 100%; height: 11.4rem; - padding: 1.2rem 1.1rem 0.9rem 1.9rem; + padding: 1.3rem 1.8rem 1.1rem 2.5rem; border-radius: 0.4rem; - background-color: ${({ theme }) => theme.colors.background}; + background-color: ${({ theme }) => theme.colors.white}; `; export const BookWrapper = styled.div` display: flex; - justify-content: space-between; + justify-content: center; flex-direction: column; width: 100%; @@ -27,20 +27,27 @@ export const BookWrapper = styled.div` export const Header = styled.div` display: flex; - justify-content: space-between; + gap: 0.5rem; align-items: center; width: 100%; + margin-bottom: 1.2rem; `; export const Name = styled.p` ${({ theme }) => theme.fonts.Head2_SB_18}; + color: ${({ theme }) => theme.colors.BG}; +`; + +export const Favorite = styled.button` + width: 2.2rem; + height: 2.2rem; `; export const TrashBtn = styled.button` position: absolute; - top: 1rem; - right: 1rem; + top: 1.2rem; + right: 1.8rem; width: 3.2rem; height: 3.2rem; @@ -48,17 +55,20 @@ export const TrashBtn = styled.button` export const Title = styled.p` ${({ theme }) => theme.fonts.Title1_SB_16}; + color: ${({ theme }) => theme.colors.BG}; `; export const Footer = styled.div` display: flex; justify-content: space-between; - align-items: baseline; + align-items: flex-end; width: 100%; `; export const Date = styled.p` + padding-bottom: 0.4rem; + ${({ theme }) => theme.fonts.E_Caption_R_12}; color: ${({ theme }) => theme.colors.DG50}; `; diff --git a/src/Mypage/components/LecueBook/index.tsx b/src/History/components/MyLecueBook/index.tsx similarity index 52% rename from src/Mypage/components/LecueBook/index.tsx rename to src/History/components/MyLecueBook/index.tsx index 915592be..2b4a64a2 100644 --- a/src/Mypage/components/LecueBook/index.tsx +++ b/src/History/components/MyLecueBook/index.tsx @@ -1,21 +1,33 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { IcWaste } from '../../../assets'; +import { IcStar, IcStarDefault, IcWaste } from '../../../assets'; import CommonModal from '../../../components/common/Modal/CommonModal'; +import useDeleteFavorite from '../../../libs/hooks/useDeleteFavorite'; +import usePostFavorite from '../../../libs/hooks/usePostFavorite'; import useDeleteMyBook from '../../hooks/useDeleteMyBook'; -import { LecueBookProps } from '../../types/myPageType'; -import * as S from './LecueBook.style'; - -function LecueBook(props: LecueBookProps) { - const { bookId, favoriteName, title, bookDate, noteNum, bookUuid } = props; +import { LecueBookProps } from '../../types/historyType'; +import * as S from './MyLecueBook.style'; +function MyLecueBook(props: LecueBookProps) { + const { + bookId, + favoriteName, + title, + bookDate, + noteNum, + bookUuid, + isFavorite, + } = props; const [noteCount, setNoteCount] = useState(''); const [modalOn, setModalOn] = useState(false); + const [favorite, setFavorite] = useState(isFavorite); const navigate = useNavigate(); const deleteMutation = useDeleteMyBook(); + const FavoritePostMutation = usePostFavorite(); + const FavoriteDeleteMutation = useDeleteFavorite(); const convertNoteCount = (noteNum: number) => { setNoteCount(noteNum.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')); @@ -32,13 +44,27 @@ function LecueBook(props: LecueBookProps) { event.stopPropagation(); }; + const handleClickFavoriteBtn = ( + event: React.MouseEvent, + bookId: number, + ) => { + event.stopPropagation(); + if (favorite) { + FavoriteDeleteMutation.mutate(bookId); + setFavorite(false); + } else { + FavoritePostMutation.mutate(bookId); + setFavorite(true); + } + }; + const handleFn = () => { deleteMutation.mutate(bookId); }; useEffect(() => { convertNoteCount(noteNum); - }); + }, [favorite]); return ( @@ -49,13 +75,24 @@ function LecueBook(props: LecueBookProps) { > {favoriteName} + { + handleClickFavoriteBtn(event, bookId); + }} + > + {favorite ? : } + {title} {bookDate} {noteCount}개 - handleClickTrashBtn(event)}> + handleClickTrashBtn(event)} + > @@ -71,4 +108,4 @@ function LecueBook(props: LecueBookProps) { ); } -export default LecueBook; +export default MyLecueBook; diff --git a/src/History/components/MyLecueBookList/MyLecueBookList.style.ts b/src/History/components/MyLecueBookList/MyLecueBookList.style.ts new file mode 100644 index 00000000..6a2cf763 --- /dev/null +++ b/src/History/components/MyLecueBookList/MyLecueBookList.style.ts @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; + +export const Wrapper = styled.article` + display: flex; + gap: 1.2rem; + flex-direction: column; + + width: 100%; + padding-bottom: 2rem; +`; diff --git a/src/History/components/MyLecueBookList/index.tsx b/src/History/components/MyLecueBookList/index.tsx new file mode 100644 index 00000000..fff2906a --- /dev/null +++ b/src/History/components/MyLecueBookList/index.tsx @@ -0,0 +1,37 @@ +import useGetMyBookList from '../../hooks/useGetMyBookList'; +import { LecueBookProps } from '../../types/historyType'; +import HistoryEmptyView from '../HistoryEmptyView'; +import MyLecueBook from '../MyLecueBook'; +import * as S from './MyLecueBookList.style'; + +function MyLecueBookList() { + const { myBookList } = useGetMyBookList(); + + return ( + + {myBookList && myBookList.length !== 0 ? ( + myBookList.map((book: LecueBookProps) => { + return ( + + ); + }) + ) : ( + + )} + + ); +} + +export default MyLecueBookList; diff --git a/src/History/components/MyLetter/MyLetter.style.ts b/src/History/components/MyLetter/MyLetter.style.ts new file mode 100644 index 00000000..0a6c7d99 --- /dev/null +++ b/src/History/components/MyLetter/MyLetter.style.ts @@ -0,0 +1,56 @@ +import styled from '@emotion/styled'; + +export const MyLetterWrapper = styled.article` + width: 100%; + height: 16.3rem; + padding: 1.3rem 1.2rem; + + border-radius: 0.4rem; + background-color: ${({ theme }) => theme.colors.sub_purple}; +`; + +export const MyLetterFavorite = styled.h1` + line-height: 2.1rem; + + ${({ theme }) => theme.fonts.Title1_SB_16}; +`; + +export const MyLetterTitle = styled.h2` + overflow: hidden; + + margin-top: 0.4rem; + + line-height: 2.1rem; + + white-space: nowrap; + text-overflow: ellipsis; + + ${({ theme }) => theme.fonts.Body4_SB_14}; +`; + +export const MyLetterContent = styled.p` + display: -webkit-box; + word-wrap: break-word; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + + overflow: hidden; + text-overflow: ellipsis; + + width: 100%; + height: 6rem; + margin-top: 0.7rem; + + ${({ theme }) => theme.fonts.Body3_R_14}; +`; + +export const MyLetterDate = styled.p` + width: 100%; + margin-top: 1.5rem; + + color: ${({ theme }) => theme.colors.DG}; + + text-align: right; + + ${({ theme }) => theme.fonts.E_Caption_R_12}; +`; diff --git a/src/History/components/MyLetter/index.tsx b/src/History/components/MyLetter/index.tsx new file mode 100644 index 00000000..4d1a71c5 --- /dev/null +++ b/src/History/components/MyLetter/index.tsx @@ -0,0 +1,29 @@ +import { useNavigate } from 'react-router-dom'; + +import { MyLetterProps } from '../../types/historyType'; +import * as S from './MyLetter.style'; + +function MyLetter({ + bookUuid, + favoriteName, + title, + content, + noteDate, +}: MyLetterProps) { + const navigate = useNavigate(); + + const handleClickMyLetter = () => { + navigate(`/lecue-book/${bookUuid}`); + }; + + return ( + + {favoriteName} + {title} + {content} + {noteDate} + + ); +} + +export default MyLetter; diff --git a/src/History/components/MyLetterList/MyLetterList.style.ts b/src/History/components/MyLetterList/MyLetterList.style.ts new file mode 100644 index 00000000..ff5a8c26 --- /dev/null +++ b/src/History/components/MyLetterList/MyLetterList.style.ts @@ -0,0 +1,17 @@ +import styled from '@emotion/styled'; + +export const MyLetterListWrapper = styled.div` + display: flex; + flex-direction: column; + + width: 100%; +`; + +export const GridViewWrapper = styled.div` + display: grid; + gap: 0.6rem 0.7rem; + grid-template-columns: repeat(2, 1fr); + + width: 100%; + padding-bottom: 2rem; +`; diff --git a/src/History/components/MyLetterList/index.tsx b/src/History/components/MyLetterList/index.tsx new file mode 100644 index 00000000..df5832dd --- /dev/null +++ b/src/History/components/MyLetterList/index.tsx @@ -0,0 +1,32 @@ +import useGetNoteList from '../../hooks/useGetMyNoteList'; +import { MyLetterProps } from '../../types/historyType'; +import HistoryEmptyView from '../HistoryEmptyView'; +import MyLetter from '../MyLetter'; +import * as S from './MyLetterList.style'; + +function MyLetterList() { + const { myNoteList } = useGetNoteList(); + + return ( + + {myNoteList && myNoteList.length !== 0 ? ( + + {myNoteList.map((letter: MyLetterProps) => { + return ; + })} + + ) : ( + + )} + + + ); +} + +export default MyLetterList; diff --git a/src/History/components/SelectModal/SelectModal.style.ts b/src/History/components/SelectModal/SelectModal.style.ts new file mode 100644 index 00000000..971d2057 --- /dev/null +++ b/src/History/components/SelectModal/SelectModal.style.ts @@ -0,0 +1,85 @@ +import { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; + +const slideUp = keyframes` + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +`; + +const slideDown = keyframes` + from { + transform: translateY(0); + } + to { + transform: translateY(100%); + } +`; + +export const SelectModalWrapper = styled.div` + display: flex; + justify-content: center; + align-items: flex-end; + position: fixed; + top: 0; + z-index: 9; + + width: 100%; + height: 100dvh; + + background: ${({ theme }) => theme.colors.Modal}; +`; + +export const SelectModalContainer = styled.div<{ + modalOn: boolean; + animationDirection: string; +}>` + position: relative; + z-index: 10; + + width: 100%; + height: 18rem; + + border-radius: 1rem 1rem 0 0; + background: ${({ theme }) => theme.colors.white}; + + animation: ${({ animationDirection }) => + animationDirection === 'slideUp' ? slideUp : slideDown} + 0.1s linear forwards; +`; + +export const OptionList = styled.li` + width: 100%; + + & > :not(:last-child) { + border-bottom: 0.1rem solid ${({ theme }) => theme.colors.background}; + } +`; + +export const OptionListItem = styled.button` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + padding: 1.2rem 1.6rem; +`; + +export const OptionListItemText = styled.p` + padding-top: 0.3rem; + + color: ${({ theme }) => theme.colors.BG}; + ${({ theme }) => theme.fonts.Title2_M_16}; +`; + +export const ClosebarContainer = styled.div` + display: flex; + justify-content: center; + + width: 100%; + height: 3rem; + padding-top: 1.2rem; +`; diff --git a/src/History/components/SelectModal/index.tsx b/src/History/components/SelectModal/index.tsx new file mode 100644 index 00000000..bb5d3c56 --- /dev/null +++ b/src/History/components/SelectModal/index.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; + +import { IcMypageArrowRight, IcMypageTouchbar } from '../../../assets'; +import { optionList } from '../../constants/optionList'; +import SelectModalPortal from '../SelectModalPortal'; +import * as S from './SelectModal.style'; + +interface SelectModalProps { + modalOn: boolean; + closeModal: () => void; + selectOption: (option: number) => void; + selectedModalOptionList: Array; +} + +function SelectModal({ + modalOn, + closeModal, + selectOption, + selectedModalOptionList, +}: SelectModalProps) { + const [animationDirection, setAnimationDirection] = useState('slideUp'); + + const handleCloseModal = () => { + setAnimationDirection('slideDown'); + setTimeout(() => { + closeModal(); + setAnimationDirection('slideUp'); + }, 200); + }; + + return ( + + handleCloseModal()}> + e.stopPropagation()} + animationDirection={animationDirection} + modalOn={modalOn} + > + handleCloseModal()}> + + + + {selectedModalOptionList.map((item) => ( + { + selectOption(item); + handleCloseModal(); + }} + > + {optionList[item]} + + + ))} + + + + + ); +} + +export default SelectModal; diff --git a/src/History/components/SelectModalPortal/index.tsx b/src/History/components/SelectModalPortal/index.tsx new file mode 100644 index 00000000..35ab0fe1 --- /dev/null +++ b/src/History/components/SelectModalPortal/index.tsx @@ -0,0 +1,12 @@ +import ReactDOM from 'react-dom'; + +interface SelectModalPortalProps { + children: React.ReactNode; +} + +function SelectModalPortal({ children }: SelectModalPortalProps) { + const el: HTMLElement | null = document.getElementById('historyselect-modal'); + return ReactDOM.createPortal(children, el as Element | DocumentFragment); +} + +export default SelectModalPortal; diff --git a/src/History/constants/optionList.ts b/src/History/constants/optionList.ts new file mode 100644 index 00000000..ae15fd04 --- /dev/null +++ b/src/History/constants/optionList.ts @@ -0,0 +1,5 @@ +export const optionList: Record = { + 1: '즐겨찾기한 레큐북', + 2: '내가 만든 레큐북', + 3: '내가 남긴 레터', +}; diff --git a/src/Mypage/hooks/useDeleteMyBook.ts b/src/History/hooks/useDeleteMyBook.ts similarity index 100% rename from src/Mypage/hooks/useDeleteMyBook.ts rename to src/History/hooks/useDeleteMyBook.ts diff --git a/src/Mypage/hooks/useGetMyBookList.ts b/src/History/hooks/useGetMyBookList.ts similarity index 100% rename from src/Mypage/hooks/useGetMyBookList.ts rename to src/History/hooks/useGetMyBookList.ts diff --git a/src/History/hooks/useGetMyFavorite.ts b/src/History/hooks/useGetMyFavorite.ts new file mode 100644 index 00000000..23956ee3 --- /dev/null +++ b/src/History/hooks/useGetMyFavorite.ts @@ -0,0 +1,20 @@ +import { useQuery } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import { getMyFavorite } from '../api/getMyFavorite'; + +export default function useGetMyFavorite() { + const navigate = useNavigate(); + const { data: myFavoriteList, isLoading } = useQuery( + ['get-mypage-favorite'], + () => getMyFavorite(), + { + onError: () => { + navigate('/error'); + }, + refetchOnWindowFocus: false, + }, + ); + + return { myFavoriteList, isLoading }; +} diff --git a/src/Mypage/hooks/useGetMyNoteList.ts b/src/History/hooks/useGetMyNoteList.ts similarity index 100% rename from src/Mypage/hooks/useGetMyNoteList.ts rename to src/History/hooks/useGetMyNoteList.ts diff --git a/src/History/page/History.style.ts b/src/History/page/History.style.ts new file mode 100644 index 00000000..877ee91f --- /dev/null +++ b/src/History/page/History.style.ts @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; + +export const HistoryPageBodyWrapper = styled.div` + width: 100%; + padding: 0 1.6rem; + margin-top: 5.4rem; +`; +export const HistorySelectButton = styled.button` + display: flex; + gap: 0.5rem; + align-items: center; + + margin: 3.4rem 0 2rem; +`; +export const CurrentHistoryOption = styled.h2` + padding-top: 0.3rem; + + color: ${({ theme }) => theme.colors.BG}; + ${({ theme }) => theme.fonts.Head2_SB_18}; +`; diff --git a/src/History/page/index.tsx b/src/History/page/index.tsx new file mode 100644 index 00000000..adcd9aa9 --- /dev/null +++ b/src/History/page/index.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { IcArrowDownBlack } from '../../assets'; +import Header from '../../components/common/Header'; +import MyFavoriteBookList from '../components/MyFavoriteBookList'; +import MyLecueBookList from '../components/MyLecueBookList'; +import MyLetterList from '../components/MyLetterList'; +import SelectModal from '../components/SelectModal'; +import { optionList } from '../constants/optionList'; +import * as S from './History.style'; + +function History() { + const location = useLocation(); + + const [modalOn, setModalOn] = useState(false); + const [selectedOption, setSelectedOption] = useState(location.state); + const handleClickHistorySelectButton = () => { + setModalOn(true); + }; + + return ( + + {modalOn && ( + setModalOn(false)} + selectOption={(option: number) => setSelectedOption(option)} + selectedModalOptionList={[1, 2, 3].filter( + (num) => num !== selectedOption, + )} + /> + )} +
+ + + + {optionList[selectedOption]} + + + + { + { + 1: , + 2: , + 3: , + }[selectedOption] + } + + + ); +} + +export default History; diff --git a/src/History/types/historyType.ts b/src/History/types/historyType.ts new file mode 100644 index 00000000..ab67188d --- /dev/null +++ b/src/History/types/historyType.ts @@ -0,0 +1,38 @@ +export interface LecueBookType { + bookUuid: string; + bookId: number; + favoriteName: string; + title: string; + bookDate: string; + noteNum: number; + isFavorite: boolean; +} + +export interface LecueBookProps extends LecueBookType { + key: number; +} + +export interface FavoriteBookType { + bookId: number; + bookUuid: string; + favoriteImage: string; + favoriteName: string; +} + +export interface FavoriteBookProps extends FavoriteBookType { + key: number; +} + +export interface MyLetterProps { + bookUuid: string; + noteId: number; + favoriteName: string; + title: string; + content: string; + noteDate: string; +} + +export interface HistoryEmptyViewProps { + topLineText: string; + bottomLineText: string; +} diff --git a/src/HistoryEnter/api/.gitkeep b/src/HistoryEnter/api/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/HistoryEnter/components/.gitkeep b/src/HistoryEnter/components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/HistoryEnter/constants/.gitkeep b/src/HistoryEnter/constants/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/HistoryEnter/hooks/.gitkeep b/src/HistoryEnter/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/HistoryEnter/page/HistoryEnter.style.ts b/src/HistoryEnter/page/HistoryEnter.style.ts new file mode 100644 index 00000000..148b8196 --- /dev/null +++ b/src/HistoryEnter/page/HistoryEnter.style.ts @@ -0,0 +1,30 @@ +import styled from '@emotion/styled'; + +export const HistoryEnterPageBodyWrapper = styled.section` + display: flex; + gap: 1.2rem; + flex-direction: column; + + width: 100%; + padding: 2.9rem 1.6rem; + margin-top: 5.4rem; +`; + +export const Tab = styled.li<{ variant: string }>` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 13rem; + padding: 0 ${({ variant }) => (variant === 'book' ? 2.9 : 1.9)}rem 0 3rem; + + border-radius: 0.2rem; + background-color: ${({ theme }) => theme.colors.white}; + cursor: pointer; +`; + +export const Text = styled.h1` + color: ${({ theme }) => theme.colors.BG}; + ${({ theme }) => theme.fonts.Title1_SB_16}; +`; diff --git a/src/HistoryEnter/page/index.tsx b/src/HistoryEnter/page/index.tsx new file mode 100644 index 00000000..51f2f5d3 --- /dev/null +++ b/src/HistoryEnter/page/index.tsx @@ -0,0 +1,60 @@ +import React, { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { + ImgMypageFavoriteLecueBook, + ImgMypageLetter, + ImgMypageMakeLecueBook, +} from '../../assets'; +import Header from '../../components/common/Header'; +import * as S from './HistoryEnter.style'; + +function HistoryEnter() { + const navigate = useNavigate(); + + const HistoryEnterList = useMemo( + () => [ + { + title: '즐겨찾기한 레큐북', + variant: 'book', + image: , + handleClickTab: () => navigate('/mypage/history', { state: 1 }), + }, + { + title: '내가 만든 레큐북', + variant: 'book', + image: , + handleClickTab: () => navigate('/mypage/history', { state: 2 }), + }, + { + title: '내가 남긴 레터', + variant: 'letter', + image: , + handleClickTab: () => navigate('/mypage/history', { state: 3 }), + }, + ], + [], + ); + + return ( + +
+ + {HistoryEnterList.map((element) => { + return ( + + {element.title} + {element.image} + + ); + })} + + + ); +} + +export default HistoryEnter; diff --git a/src/Home/components/LecueBookList/LecueBookList.style.ts b/src/Home/components/LecueBookList/LecueBookList.style.ts index d1079d34..4d0958c4 100644 --- a/src/Home/components/LecueBookList/LecueBookList.style.ts +++ b/src/Home/components/LecueBookList/LecueBookList.style.ts @@ -26,6 +26,7 @@ export const Title = styled.header` export const LecueBookList = styled.section` display: grid; gap: 2em 2.4rem; + align-items: start; grid-template-columns: repeat(3, 1fr); width: 100%; diff --git a/src/Home/components/LecueBookList/index.tsx b/src/Home/components/LecueBookList/index.tsx index 4eda671e..31974baa 100644 --- a/src/Home/components/LecueBookList/index.tsx +++ b/src/Home/components/LecueBookList/index.tsx @@ -1,7 +1,4 @@ -import { useNavigate } from 'react-router'; - -import { IcHomeFavorite } from '../../../assets'; -import useDeleteFavorite from '../../../libs/hooks/useDeleteFavorite'; +import LecueBook from '../../../components/common/LecueBook'; import useGetFavorite from '../../../libs/hooks/useGetFavorite'; import useGetLecueBook from '../../hooks/useGetLecueBook'; import NoBookmarkList from '../NoBookmarkList'; @@ -19,19 +16,9 @@ interface LecueBookListProps { } function LecueBookList({ title }: LecueBookListProps) { - const navigate = useNavigate(); - const deleteMutation = useDeleteFavorite(); const isBookmark = title.includes('즐겨찾기'); const { data } = isBookmark ? useGetFavorite() : useGetLecueBook(); - const handleClickLecueBook = (uuid: string) => { - navigate(`/lecue-book/${uuid}`); - }; - - const handleClickFavoriteIcon = (bookId: number) => { - deleteMutation.mutate(bookId); - }; - return ( {title} @@ -39,20 +26,14 @@ function LecueBookList({ title }: LecueBookListProps) { {data.map((book: BookProps) => ( - {isBookmark && ( - handleClickFavoriteIcon(book.bookId)} - > - - - )} - - handleClickLecueBook(book.bookUuid)} + - {book.favoriteName} ))} diff --git a/src/Mypage/api/.gitkeep b/src/Mypage/api/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Mypage/components/LecueList/LecueList.style.ts b/src/Mypage/components/LecueList/LecueList.style.ts deleted file mode 100644 index fb477f7e..00000000 --- a/src/Mypage/components/LecueList/LecueList.style.ts +++ /dev/null @@ -1,56 +0,0 @@ -import styled from '@emotion/styled'; - -export const Wrapper = styled.article` - display: flex; - flex-direction: column; - - width: 100%; - height: 100%; -`; - -export const ButtonWrapper = styled.section` - display: flex; - - width: 100%; -`; - -export const Button = styled.button<{ variant: boolean }>` - width: calc(100vw - 4rem); - height: 3.7rem; - padding: 0.7rem 1.15rem; - - border-radius: 0.4rem 0.4rem 0 0; - background-color: ${({ theme, variant }) => - variant ? theme.colors.black : 'transparent'}; - color: ${({ theme, variant }) => - variant ? theme.colors.background : theme.colors.MG}; - ${({ theme }) => theme.fonts.Title2_M_16} - - text-align: center; - vertical-align: center; -`; - -export const ListWrapper = styled.section<{ variant: string }>` - display: flex; - justify-content: center; - - width: 100%; - height: calc(100dvh - 19.3rem); - padding: 1rem 1rem 1rem ${({ variant }) => (variant === 'note' ? 1.5 : 1)}rem; - - border-radius: ${({ variant }) => (variant === 'note' ? 0 : 0.4)}rem - ${({ variant }) => (variant === 'note' ? 0.4 : 0)}rem 0.4rem 0.4rem; - background-color: ${({ theme }) => theme.colors.black}; -`; - -export const ListContainer = styled.div<{ variant: string }>` - display: flex; - gap: ${({ variant }) => (variant === 'note' ? 1.1 : 0.8)}rem - ${({ variant }) => (variant === 'note' ? 1.1 : 0.95)}rem; - flex-wrap: wrap; - overflow: scroll; - - width: 100%; - height: 100%; - align-content: flex-start; -`; diff --git a/src/Mypage/components/LecueList/index.tsx b/src/Mypage/components/LecueList/index.tsx deleted file mode 100644 index bc4fedf7..00000000 --- a/src/Mypage/components/LecueList/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useState } from 'react'; - -import useGetMyBookList from '../../hooks/useGetMyBookList'; -import useGetNoteList from '../../hooks/useGetMyNoteList'; -import { - LecueBookProps, - LecueBookType, - LecueNoteType, -} from '../../types/myPageType'; -import EmptyView from '../EmptyView'; -import LecueBook from '../LecueBook'; -import LecueNote from '../LecueNote'; -import * as S from './LecueList.style'; - -function LecueList() { - const [clickedBtn, setClickedBtn] = useState('note'); - const [counter, setCounter] = useState([0, 0]); - - const { myBookList } = useGetMyBookList(); - const { myNoteList } = useGetNoteList(); - - const handleClickNoteBtn = () => { - document.getElementById('list-wrapper')!.scrollTo(0, 0); - setClickedBtn('note'); - }; - - const handleClickBookBtn = () => { - document.getElementById('list-wrapper')!.scrollTo(0, 0); - setClickedBtn('book'); - }; - - const numberCount = (NOTE: LecueNoteType[], BOOK: LecueBookType[]) => { - setCounter([NOTE.length, BOOK.length]); - }; - - useEffect(() => { - if (myNoteList && myBookList) { - numberCount(myNoteList, myBookList); - } - }, [myNoteList, myBookList]); - - return ( - - - - 레큐노트 ({counter[0]}개) - - - 레큐북 ({counter[1]}개) - - - - - - {clickedBtn === 'note' ? ( - myNoteList && myNoteList.length !== 0 ? ( - myNoteList.map((note: LecueNoteType) => { - return ( - - ); - }) - ) : ( - - ) - ) : myBookList && myBookList.length !== 0 ? ( - myBookList.map((book: LecueBookProps) => { - return ( - - ); - }) - ) : ( - - )} - - - - ); -} - -export default LecueList; diff --git a/src/Mypage/components/Nickname/Nickname.style.ts b/src/Mypage/components/Nickname/Nickname.style.ts deleted file mode 100644 index 7d7d7823..00000000 --- a/src/Mypage/components/Nickname/Nickname.style.ts +++ /dev/null @@ -1,14 +0,0 @@ -import styled from '@emotion/styled'; - -export const NicknameWrapper = styled.section` - display: flex; - gap: 0.5rem; - align-items: center; - - width: 100%; - padding: 2.7rem 0.6rem; -`; - -export const Nickname = styled.p` - ${({ theme }) => theme.fonts.Head1_B_20}; -`; diff --git a/src/Mypage/components/Nickname/index.tsx b/src/Mypage/components/Nickname/index.tsx deleted file mode 100644 index cd6ba1e6..00000000 --- a/src/Mypage/components/Nickname/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ImgStarPosit } from '../../../assets'; -import useGetMyNickName from '../../hooks/useGetMyNickname'; -import * as S from './Nickname.style'; - -function Nickname() { - const { myNickName } = useGetMyNickName(); - return ( - - - {myNickName} 님 - - ); -} - -export default Nickname; diff --git a/src/Mypage/hooks/.gitkeep b/src/Mypage/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Mypage/page/Mypage.style.ts b/src/Mypage/page/Mypage.style.ts index 49423177..db704f77 100644 --- a/src/Mypage/page/Mypage.style.ts +++ b/src/Mypage/page/Mypage.style.ts @@ -10,15 +10,3 @@ export const Wrapper = styled.article` background-color: ${({ theme }) => theme.colors.background}; `; - -export const InfoWrapper = styled.div` - width: 100%; - padding: 0 0.95rem 1rem; - margin-top: 5.4rem; -`; - -export const ListWrapper = styled.div` - width: 100%; - height: calc(100dvh - 14.6rem); - padding: 0 1rem 1rem; -`; diff --git a/src/Mypage/page/index.tsx b/src/Mypage/page/index.tsx index 6c9d66ef..69dcf7dc 100644 --- a/src/Mypage/page/index.tsx +++ b/src/Mypage/page/index.tsx @@ -1,29 +1,11 @@ -import Header from '../../components/common/Header'; -import LoadingPage from '../../components/common/LoadingPage'; -import LecueList from '../components/LecueList'; -import Nickname from '../components/Nickname'; -import useDeleteMyBook from '../hooks/useDeleteMyBook'; -import useGetMyBookList from '../hooks/useGetMyBookList'; -import useGetMyNickName from '../hooks/useGetMyNickname'; -import useGetNoteList from '../hooks/useGetMyNoteList'; +import { Outlet } from 'react-router-dom'; + import * as S from './Mypage.style'; function Mypage() { - const { isLoading } = - useGetMyBookList() || useGetMyNickName() || useGetNoteList(); - const deleteMutation = useDeleteMyBook(); - - return isLoading || deleteMutation.isLoading ? ( - - ) : ( + return ( -
- - - - - - + ); } diff --git a/src/Mypage/types/myPageType.ts b/src/Mypage/types/myPageType.ts index 6289a6f1..a4c1c428 100644 --- a/src/Mypage/types/myPageType.ts +++ b/src/Mypage/types/myPageType.ts @@ -1,16 +1,3 @@ -export interface LecueBookType { - bookUuid: string; - bookId: number; - favoriteName: string; - title: string; - bookDate: string; - noteNum: number; -} - -export interface LecueBookProps extends LecueBookType { - key: number; -} - export interface LecueNoteType { bookUuid: string; noteId: number; diff --git a/src/Router.tsx b/src/Router.tsx index 22e2e6f7..a93f5d8f 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -9,7 +9,11 @@ import ErrorPage from './components/common/ErrorPage'; import LoadingPage from './components/common/LoadingPage'; import CreateBook from './CreateBook/page'; import DetailPage from './Detail/page/DetailPage'; +import EditNickname from './EditNickname/page'; +import Enter from './Enter/page'; import HealthTest from './HealthTest'; +import History from './History/page'; +import HistoryEnter from './HistoryEnter/page'; import LecueNotePage from './LecueNote/page/LeceuNotePage'; import LoginCallback from './Login/components/LoginCallback/LoginCallback'; import Login from './Login/page'; @@ -51,7 +55,12 @@ function Router() { } /> } /> } /> - } /> + }> + } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/src/assets/icon/ic_arrow_down_black.svg b/src/assets/icon/ic_arrow_down_black.svg new file mode 100644 index 00000000..da4bc6cb --- /dev/null +++ b/src/assets/icon/ic_arrow_down_black.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icon/ic_home_favorite_empty.svg b/src/assets/icon/ic_home_favorite_empty.svg new file mode 100644 index 00000000..c0da1244 --- /dev/null +++ b/src/assets/icon/ic_home_favorite_empty.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icon/ic_home_favorite_filled.svg b/src/assets/icon/ic_home_favorite_filled.svg new file mode 100644 index 00000000..04976ee7 --- /dev/null +++ b/src/assets/icon/ic_home_favorite_filled.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icon/ic_mypage_arrow_right.svg b/src/assets/icon/ic_mypage_arrow_right.svg new file mode 100644 index 00000000..e8bae633 --- /dev/null +++ b/src/assets/icon/ic_mypage_arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/ic_mypage_touchbar.svg b/src/assets/icon/ic_mypage_touchbar.svg new file mode 100644 index 00000000..94d83e90 --- /dev/null +++ b/src/assets/icon/ic_mypage_touchbar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/ic_star.svg b/src/assets/icon/ic_star.svg new file mode 100644 index 00000000..4dd61c3c --- /dev/null +++ b/src/assets/icon/ic_star.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/ic_star_default.svg b/src/assets/icon/ic_star_default.svg new file mode 100644 index 00000000..6612c715 --- /dev/null +++ b/src/assets/icon/ic_star_default.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/img_mypage_favorite_lecuebook.svg b/src/assets/img/img_mypage_favorite_lecuebook.svg new file mode 100644 index 00000000..5310c8f0 --- /dev/null +++ b/src/assets/img/img_mypage_favorite_lecuebook.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/img/img_mypage_letter.svg b/src/assets/img/img_mypage_letter.svg new file mode 100644 index 00000000..1d94426f --- /dev/null +++ b/src/assets/img/img_mypage_letter.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/img_mypage_make_lecuebook.svg b/src/assets/img/img_mypage_make_lecuebook.svg new file mode 100644 index 00000000..506fb6ac --- /dev/null +++ b/src/assets/img/img_mypage_make_lecuebook.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/img_mypage_notexist.svg b/src/assets/img/img_mypage_notexist.svg new file mode 100644 index 00000000..5cba3e1c --- /dev/null +++ b/src/assets/img/img_mypage_notexist.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/index.ts b/src/assets/index.ts index 5d5b41fe..09351371 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -8,6 +8,7 @@ import BtnFloatingWriteOrange from './button/btn_floating_write_orange.svg?react import BtnKakaologin from './button/btn_kakaologin.svg?react'; import IcAlertO from './icon/ic_alert_o.svg?react'; import IcAlertX from './icon/ic_alert_x.svg?react'; +import IcArrowDownBlack from './icon/ic_arrow_down_black.svg?react'; import IcArrowLeftBlack from './icon/ic_arrow_left_black.svg?react'; import IcArrowLeftWhite from './icon/ic_arrow_left_white.svg?react'; import IcCamera from './icon/ic_camera.svg?react'; @@ -18,9 +19,15 @@ import IcCrown from './icon/ic_crown.svg?react'; import IcDate from './icon/ic_date.svg?react'; import IcHome from './icon/ic_home.svg?react'; import IcHomeFavorite from './icon/ic_home_favorite.svg?react'; +import IcHomeFavoriteEmpty from './icon/ic_home_favorite_empty.svg?react'; +import IcHomeFavoriteFilled from './icon/ic_home_favorite_filled.svg?react'; +import IcMypageArrowRight from './icon/ic_mypage_arrow_right.svg?react'; +import IcMypageTouchbar from './icon/ic_mypage_touchbar.svg?react'; import IcNotice from './icon/ic_notice.svg?react'; import IcProfile from './icon/ic_profile.svg?react'; import IcSharing from './icon/ic_sharing.svg?react'; +import IcStar from './icon/ic_star.svg?react'; +import IcStarDefault from './icon/ic_star_default.svg?react'; import IcWaste from './icon/ic_waste.svg?react'; import IcX from './icon/ic_x.svg?react'; import ImgBook from './img/img_book.svg?react'; @@ -46,6 +53,10 @@ import ImgModalLogin from './img/img_modal_login.svg?react'; import ImgModalMypagedelete from './img/img_modal_mypagedelete.svg?react'; import ImgModalNotecomplete from './img/img_modal_notecomplete.svg?react'; import ImgModalNoteexit from './img/img_modal_noteexit.svg?react'; +import ImgMypageFavoriteLecueBook from './img/img_mypage_favorite_lecuebook.svg?react'; +import ImgMypageLetter from './img/img_mypage_letter.svg?react'; +import ImgMypageMakeLecueBook from './img/img_mypage_make_lecuebook.svg?react'; +import ImgMyPageNotexist from './img/img_mypage_notexist.svg?react'; import ImgSaleprice from './img/img_saleprice.svg?react'; import ImgSplashLogo from './img/img_splash_logo.svg?react'; import ImgStarOrangeLine from './img/img_star_orangeline.svg?react'; @@ -62,6 +73,7 @@ export { BtnKakaologin, IcAlertO, IcAlertX, + IcArrowDownBlack, IcArrowLeftBlack, IcArrowLeftWhite, IcCamera, @@ -72,9 +84,15 @@ export { IcDate, IcHome, IcHomeFavorite, + IcHomeFavoriteEmpty, + IcHomeFavoriteFilled, + IcMypageArrowRight, + IcMypageTouchbar, IcNotice, IcProfile, IcSharing, + IcStar, + IcStarDefault, IcWaste, IcX, ImgBook, @@ -100,6 +118,10 @@ export { ImgModalMypagedelete, ImgModalNotecomplete, ImgModalNoteexit, + ImgMyPageNotexist, + ImgMypageFavoriteLecueBook, + ImgMypageLetter, + ImgMypageMakeLecueBook, ImgSaleprice, ImgSplashLogo, ImgStarOrangeLine, diff --git a/src/components/common/LecueBook/LecueBook.style.ts b/src/components/common/LecueBook/LecueBook.style.ts new file mode 100644 index 00000000..c38e13b0 --- /dev/null +++ b/src/components/common/LecueBook/LecueBook.style.ts @@ -0,0 +1,37 @@ +import styled from '@emotion/styled'; + +export const LecueBookWrapper = styled.div` + display: flex; + gap: 1rem; + align-items: center; + flex-direction: column; + position: relative; + + width: 9.8rem; +`; + +export const FavoriteButton = styled.button` + position: absolute; + left: 0.2rem; +`; + +export const BookImage = styled.img` + width: 9.8rem; + height: 9.8rem; + + border-radius: 50%; + + object-fit: cover; +`; + +export const Title = styled.h1` + width: 100%; + + color: ${({ theme }) => theme.colors.BG}; + + text-align: center; + word-wrap: normal; + word-break: break-all; + + ${({ theme }) => theme.fonts.E_Body1_SB_14}; +`; diff --git a/src/components/common/LecueBook/index.tsx b/src/components/common/LecueBook/index.tsx new file mode 100644 index 00000000..ef8d98dd --- /dev/null +++ b/src/components/common/LecueBook/index.tsx @@ -0,0 +1,68 @@ +import { useNavigate } from 'react-router-dom'; + +import { IcHomeFavoriteFilled } from '../../../assets'; +import useDeleteFavorite from '../../../libs/hooks/useDeleteFavorite'; +import * as S from './LecueBook.style'; + +type bookType = 'favorite' | 'normal'; +type deleteType = 'mypage' | 'home'; + +interface LecueBookProps { + bookId: number; + bookUuid: string; + favoriteImage: string; + favoriteName: string; + bookType: bookType; + deleteType?: deleteType; +} + +function LecueBook(props: LecueBookProps) { + const { + bookId, + bookUuid, + favoriteImage, + favoriteName, + deleteType, + bookType, + } = props; + + const navigate = useNavigate(); + + const MypageDeleteMutation = useDeleteFavorite('mypage'); + const HomeDeleteMutation = useDeleteFavorite('home'); + + const handleClickFavoriteBtn = ( + bookId: number, + deleteType: deleteType | undefined, + ) => { + deleteType === 'home' + ? HomeDeleteMutation.mutate(bookId) + : MypageDeleteMutation.mutate(bookId); + }; + + const handleClickBook = (bookUuid: string) => { + navigate(`/lecue-book/${bookUuid}`); + }; + + return ( + + handleClickBook(bookUuid)} + /> + {bookType === 'favorite' && ( + handleClickFavoriteBtn(bookId, deleteType)} + > + + + )} + + {favoriteName} + + ); +} + +export default LecueBook; diff --git a/src/Mypage/api/getMyNickName.ts b/src/libs/api/getMyNickName.ts similarity index 84% rename from src/Mypage/api/getMyNickName.ts rename to src/libs/api/getMyNickName.ts index d555ab39..e4c93ed3 100644 --- a/src/Mypage/api/getMyNickName.ts +++ b/src/libs/api/getMyNickName.ts @@ -2,7 +2,7 @@ import { api } from '../../libs/api'; export async function getMyNickName() { const token = localStorage.getItem('token'); - const data = await api.get(`/api/mypage/note`, { + const data = await api.get(`/api/mypage`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, diff --git a/src/libs/api/postFavorite.ts b/src/libs/api/postFavorite.ts new file mode 100644 index 00000000..fb30f9b2 --- /dev/null +++ b/src/libs/api/postFavorite.ts @@ -0,0 +1,19 @@ +import { api } from '../../libs/api'; + +const postFavorite = async (bookId: number) => { + const token = localStorage.getItem('token'); + const { data } = await api.post( + '/api/favorite', + { bookId: bookId }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }, + ); + + return data; +}; + +export default postFavorite; diff --git a/src/libs/hooks/useDeleteFavorite.ts b/src/libs/hooks/useDeleteFavorite.ts index 81a364e1..086ad981 100644 --- a/src/libs/hooks/useDeleteFavorite.ts +++ b/src/libs/hooks/useDeleteFavorite.ts @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import deleteFavorite from '../api/deleteFavorite'; -const useDeleteFavorite = () => { +const useDeleteFavorite = (state: string) => { const navigate = useNavigate(); const queryClient = useQueryClient(); const mutation = useMutation({ @@ -12,7 +12,13 @@ const useDeleteFavorite = () => { }, onError: () => navigate('/error'), onSuccess: () => { - queryClient.refetchQueries(['get-favorite'], { exact: true }); + state === 'home' + ? queryClient.refetchQueries(['get-favorite'], { + exact: true, + }) + : queryClient.refetchQueries(['get-mypage-favorite'], { + exact: true, + }); }, }); return mutation; diff --git a/src/Mypage/hooks/useGetMyNickname.ts b/src/libs/hooks/useGetMyNickname.ts similarity index 100% rename from src/Mypage/hooks/useGetMyNickname.ts rename to src/libs/hooks/useGetMyNickname.ts diff --git a/src/libs/hooks/usePostFavorite.ts b/src/libs/hooks/usePostFavorite.ts new file mode 100644 index 00000000..b0f1854e --- /dev/null +++ b/src/libs/hooks/usePostFavorite.ts @@ -0,0 +1,17 @@ +import { useMutation } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import postFavorite from '../api/postFavorite'; + +const usePostFavorite = () => { + const navigate = useNavigate(); + const mutation = useMutation({ + mutationFn: (bookId: number) => { + return postFavorite(bookId); + }, + onError: () => navigate('/error'), + }); + return mutation; +}; + +export default usePostFavorite; diff --git a/src/styles/emotion.d.ts b/src/styles/emotion.d.ts index c1451bf5..dbbc2908 100644 --- a/src/styles/emotion.d.ts +++ b/src/styles/emotion.d.ts @@ -5,6 +5,7 @@ type colors = | 'white' | 'background' | 'LG' + | 'LG_2' | 'WG' | 'MG' | 'DG' diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 801f7a21..d2455f27 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -5,6 +5,7 @@ const colors = { white: '#FFFFFF', background: '#F5F5F5', LG: '#DDDDDD', + LG_2: '#E7E7E7', WG: '#BCBCBC', MG: '#9E9E9E', DG: '#494949',