From f920d84f8185477cacdfa5f2161bbda5906aa4bd Mon Sep 17 00:00:00 2001 From: sujikim Date: Wed, 6 Sep 2023 18:31:08 +0900 Subject: [PATCH] develop to main (#573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 메인페이지 인기 도서 순위 오류 해결 (#521) * fix: 메인페이지 책 타입 Book이 아나리 BookInfo * fix: 메인페이지 인기도서 rank 속성 추가 * docs: :memo: README에 이름 추가 (#524) * fix: 도서 수정 categoryId를 숫자로 변경 (#525) * refactor: 쓰이지 않는 비표준 css 제거 (#545) * refactor: 검색 바 아이콘 img로 변경 (#546) * refactor: 검색 바 아이콘 img로 변경 * fix: `alt=""` 속성 추가 장식(아이콘)이라는 점을 명시적으로 표시 * feat: 메인페이지 스크롤을 내려주세요 애니메이션 추가 (#539) png 이미지 대신 css로 마우스형태 구현, 동작 추가 * fix: Image 컴포넌트 에러 처리 방식 변경 (#538) * build: 절대경로 import 추가, `MyRentInfo`에 절대경로 적용 (#542) * build: vite-tsconfig-paths 추가 * build: 절대경로 import 추가 * refactor: `MyRentInfo`에 절대경로 적용 --------- Co-authored-by: nocontribute <> * refactor: 대출제한일 계산 로직 통합 (#536) * fix: 불필요한 타입체크 코드 삭제 * fix: addDay 날짜 오류 해결 * feat: string 형태의 날짜 비교 함수 compareDate 추가 * refactor: 대출제한일 계산 로직 변경 및 통합 * fix: 좋아요 취소시 오류 해결 및 리팩토링 (#527) * fix: 불필요한 검증 코드 삭제 * refactor: 재사용되지 않는 ShowLike 컴포넌트 통합 * feat: 유저 권한을 확인하는 usePermission 훅 추가 * refactor: 로컬스토리지 대신 usePermission 훅으로 권한 명시 * refactor: Like JSX 가독성 개선 * refactor: 모호한 변수명 변경 initBookInfoId => bookInfoId * refactor: useGetLike 훅 안팎에 중복된 상태 제거 * refactor: PostLike, DeleteLike 네트워크 성공시에만 뷰가 변경되도록 수정 * refactor: import 절대경로로 변경 * feat: 반응형을 위한 useBreakPoint 훅 추가 * fix: 현재 윈도우 너비에 따라 알맞는 헤더 하나만 표시하도록 * refactor: 기본 헤더에서 LNB 분리 * fix: headerMenu js 파일 ts로 변경 및 절대경로 적용 * refactor: 기본 헤더 불필요한 html 태그 정리 및 css 단순화 * refactor: 모바일 헤더 불필요한 html 태그 정리 및 css 단순화 * refactor: 검색창 컴포넌트 개선 및 드롭다운 UI 추가 (#551) * refactor: SearchBar 용도에 따라 분리 및 합성 방식으로 변경 용도에 따라 BookSearchBar, ManagementSearchBar로 활용하도록 변경 Compound 패턴으로 SearchBar 확장성 개선 * feat: 검색창 드롭다운 UI 추가 검색창 및 드롭다운 Blur 이벤트 추가 * fix: 검색창 드롭다운 배경 투명도 조절 * fix: 검색 결과 변경시 검색창 blur 처리 * feat: 검색창 드롭다운 ESC 키로 끌 수 있도록 * fix: 검색창 드롭다운 padding 설정 삭제 * feat: 최근 검색어 기능 구현 (#553) * feat: 최근 검색어 기능 추가 * fix: 최근검색어 X 버튼 css 수정 * feat: carousel UI 컴포넌트 추가 및 메인페이지에 적용 (#560) * refactor: interval 설정 부분 훅으로 추출 * refactor: useBound 훅에서 event 마다 선택할 수 있도록 props 설정 * feat: useResponsiveWidth 훅 추가 pc, tablet, mobile 환경에 맞는 사이즈 확인용 * refactor: 메인 신규도서 책크기 useResponsiveWidth 훅 적용 * feat: Carousel 컴포넌트 추가 무한 회전하는 슬라이딩 UI, 조합하여 사용할 수 있도록 * refactor: 메인 신규도서 Carousel 형식으로 변경 및 불필요한 태그 depth 제거 * fix: Carousel 사이즈 대신 container 안에 count로 지정할 수 있도록 size나 count 둘 중 하나를 props로 받아서 displayCount를 결정 * fix: Carousel flex 설정으로 너비 보장 안되는 문제 해결 itemSize로 지정한 크기가 조정되지 않도록 flex-grow, flex-shrink 설정 추가 * fix: Carousel 자동 애니메이션 props isAutoAnimated로 제어하도록 - mount 시 useInterval 자동 실행 막음 - props isAutoAnimated True 일때만 carousel 자동으로 돌아가도록 - isAutoAnimated와 isSmoothAnimated 변수 구분 * fix: onPrev 제대로 넘어가지 않던 문제 수정 index가 0이 아닌 1부터 시작하도록 조정 * fix: Carousel 세로형 itemCount = 1일 때 동작오류 수정 translateX와 translateY 설정 및 items이 적을때만 복사 추가 * fix: Carousel 항상 items 뒤에 복사해서 붙이도록 수정 * fix: Carousel list 키 관련 타입오류 수정 * fix: 모바일 화면 메인 신규리스트 동작 오류 수정 * fix: Carousel 가로형 itemCount = 1일 때 UI오류 수정, flexBasis 속성이 필요함 * refactor: 컨테이너와 ContextProvider 분리 및 컴포넌트 파일 독립 - 버튼이나 페이지네이션이 컨테이너 밖에 위치할 수 있도록 Provider와 분리 * fix: 메인 신규도서 컨테이너 컴포넌트 추가 * fix: useApi 임시 활용방법 추가 (#564) url data도 호출 시점에 전달할 수 있도록 * feat: 검색결과 미리보기 기능 구현 (#555) * feat: 검색결과 미리보기 UI 구현 * feat: 검색결과 미리보기에서 검색어와 일치할때 색상 강조 추가 * fix: EmphasisInString default props 설정 * fix: EmphasisInString 강조 단어로 시작할때 제대로 적용되지 않는 문제 수정 index = 0 일 때도 적용되도록 * fix: EmphasisInString 대소문자 구분하지 않도록 * fix: EmphasisInString 원래 문자열의 대소문자 형식 변형되던 문제 수정 wholeString 그대로 보일 수 있도록 수정 * fix: autocomplete api 수정 내용 mockdata 반영 * fix: 검색결과 미리보기 키보드 제어 삭제 및 hover 방식으로 변경 * fix: 검색결과 미리보기 레이아웃 변경 * refactor: 검색결과 미리보기 컴포넌트 구조 단순화 * refactor: Pagination 컴파운드 패턴 적용한 Paginations 방식으로 변경 * fix: 검색결과 미리보기 Pagination 방식 변경 * feat: 검색결과 미리보기에 전체 검색결과 카운트 보여주도록 * fix: 페이지네이션 disabled 인 이동버튼 투명도 적용 * fix: autocomplete api 수정사항 반영 * fix: 검색결과 프리뷰 mock 대신 실제 api 연결 * feat: 검색결과 미리보기 로딩 추가 * fix: 검색결과 미리보기 클릭시 상세로 이동되지 않던 오류 수정 * feat: 메인 추천 도서 기능 구현 (#565) * feat: 추천 도서 기본 UI 구현 * feat: 추천도서 도서 클릭시 해당 도서 상세로 이동 * feat: 메인 추천도서 양 옆 버튼 추가 * fix: 메인 추천도서 모바일 반응형 개선 * feat: 메인 서클별 추천도서 api 연결 * refactor: 메인 신작도서 페이지네이션 추출 * feat: 메인 추천도서에도 원형 페이지네이션 적용 * fix: 메인 서클별 추천도서 반응형 css 수정 * fix: Carousel 길이가 바뀌었을 때 슬라이드 초기화 * feat: 인기검색어 구현 (#566) * feat: 인기검색어 구현 * chore: 오타 수정 * fix: 검색개선 UI 피드백 반영 (#568) * fix: 인기검색어 UI 피드백 반영, 검색어 클릭시 검색결과로 이동 * fix: 검색결과 UI 피드백 반영, 긴 검색어는 줄바꿈 하도록 * fix: 메인 신작도서 모바일 화면 튀어나온 버튼 위치 조정 * fix: 메인 추천도서 UI 피드백 반영, 로딩 추가 및 리스트 하나일때 자동 애니메이션 해제 * fix: 인기검색어 모바일일때 fixed 해제 * fix: 검색결과 미리보기 UI 피드백 반영, 검색결과 없을때 ui 개선 * fix: 검색 개선 UI 피드백 2차 (#570) * fix: 검색결과 미리보기 로딩 중 검색결과없습니다 문구 안보이도록 * fix: 검색결과 더보기 클릭시 미리보기 닫히도록 * fix: 검색창 드롭다운 제대로 focus 안되던 문제 수정 * fix: 검색어 변경시 결과 미리보기 page가 갱신되지 않던 문제 수정 * fix: 검색결과 미리보기 전체 검색결과 더보기를 검색결과 없을때 보이지 않도록 * fix: 공백만 검색했을 때 최근검색어에 추가하지 않도록 * fix: 검색 api요청시 기본 옵션 sort title 설정 * fix: 도서 수정 카테고리 제대로 적용되지 않던 오류 해결 * fix: 추천도서 과격한 애니메이션 수정, 캐러셀 초반에 트랜지션 끌 수 있도록 * fix: 검색결과 미리보기 totalCount 없을때 0 나오던 문제 수정 * fix: 최근 검색어 최대 10개로 제한 (#572) --------- Co-authored-by: mink97 <101685319+mink97@users.noreply.github.com> Co-authored-by: scarf --- README.md | 38 ++- package.json | 1 + pnpm-lock.yaml | 33 ++- src/App.jsx | 2 - src/api/books/useGetBooksInfoNew.ts | 4 +- src/api/books/useGetBooksInfoPopular.ts | 10 +- src/api/books/useGetBooksInfoSearchUrl.ts | 2 +- src/api/books/usePatchBooksUpdate.ts | 5 +- src/api/cursus/useGetCursusRecommendBooks.ts | 43 ++++ src/api/like/index.ts | 3 + src/api/like/useDeleteLike.ts | 33 +-- src/api/like/useGetLike.ts | 40 +-- src/api/like/usePostLike.ts | 33 +-- src/api/searchKeyword/useGetSearchKeyword.ts | 28 +++ .../useGetSearchKeywordsAutocomplete.ts | 37 +++ src/asset/css/BookSearchPreview.css | 119 +++++++++ src/asset/css/BookSearchRecentKeyword.css | 58 +++++ src/asset/css/Carousel.css | 14 ++ src/asset/css/Header.css | 233 ------------------ src/asset/css/HeaderDefault.css | 61 +++++ src/asset/css/HeaderDefaultLNB.css | 78 ++++++ src/asset/css/HeaderMobile.css | 48 ++++ src/asset/css/Loader.css | 99 ++++++++ src/asset/css/MainBanner.css | 26 +- src/asset/css/MainNew.css | 50 +--- src/asset/css/MainRecommend.css | 169 +++++++++++++ src/asset/css/MobileHeader.css | 118 --------- src/asset/css/Pagination.css | 4 + src/asset/css/PaginationCircle.css | 25 ++ src/asset/css/RentModalBookList.css | 16 +- src/asset/css/RentModalUserList.css | 14 +- src/asset/css/Search.css | 5 + src/asset/css/SearchBar.css | 13 +- src/asset/css/SearchBarDropDown.css | 21 ++ src/asset/css/SearchRanking.css | 98 ++++++++ src/asset/css/SubTitle.css | 1 - src/asset/css/Tags.css | 3 - src/asset/img/scroll-icon.svg | 9 - src/component/book/BookDetail.tsx | 20 +- src/component/book/like/Like.tsx | 63 ++--- src/component/book/like/ShowLike.tsx | 50 ---- .../BookManagementModalDetail.tsx | 26 +- src/component/main/Main.tsx | 10 +- src/component/main/MainBanner.tsx | 12 +- src/component/main/MainNewBook.tsx | 29 +-- src/component/main/MainNewBookList.tsx | 155 ++++-------- src/component/main/MainNewBookPagination.tsx | 42 ++-- src/component/main/MainPopularCenter.tsx | 6 +- src/component/main/MainPopularSide.tsx | 4 +- src/component/main/MainRecommend.tsx | 29 +++ src/component/main/MainRecommendList.tsx | 74 ++++++ src/component/main/MainRecommendTitle.tsx | 43 ++++ src/component/mypage/MyRentInfo/MyRent.tsx | 6 +- .../mypage/MyRentInfo/RentHistory.tsx | 6 +- .../mypage/MyRentInfo/RentHistoryTable.tsx | 50 ++-- .../MyRentInfo/RentedOrReservedBooks.tsx | 10 +- src/component/mypage/Mypage.tsx | 37 +-- src/component/search/SearchBanner.tsx | 8 +- .../superTag/SuperTagMergeDefaultTag.tsx | 4 +- .../userManagement/UserBriefInfo.tsx | 35 +-- .../userManagement/UserManagement.tsx | 4 +- src/component/utils/BookSearchBar.tsx | 70 ++++++ src/component/utils/BookSearchPreview.tsx | 67 +++++ src/component/utils/BookSearchPreviewList.tsx | 57 +++++ .../utils/BookSearchRecentKeywords.tsx | 47 ++++ src/component/utils/Carousel.tsx | 50 ++++ src/component/utils/CarouselContainer.tsx | 30 +++ src/component/utils/CarouselList.tsx | 88 +++++++ src/component/utils/CarouselNext.tsx | 19 ++ src/component/utils/CarouselPagination.tsx | 26 ++ src/component/utils/CarouselPrev.tsx | 19 ++ src/component/utils/CarouselRoot.tsx | 108 ++++++++ src/component/utils/EmphasisInString.tsx | 24 +- src/component/utils/Header.tsx | 158 +----------- src/component/utils/HeaderDefault.tsx | 45 ++++ src/component/utils/HeaderDefaultLNB.tsx | 62 +++++ src/component/utils/HeaderMobile.tsx | 58 +++++ src/component/utils/Image.tsx | 5 +- src/component/utils/InquireBoxTitle.tsx | 4 +- src/component/utils/Loader.tsx | 23 ++ src/component/utils/Management.tsx | 4 +- src/component/utils/ManagementSearchBar.tsx | 68 +++++ src/component/utils/MobileHeader.jsx | 81 ------ src/component/utils/Pagination.tsx | 117 ++------- src/component/utils/PaginationCircle.tsx | 35 +++ src/component/utils/Paginations.tsx | 151 ++++++++++++ src/component/utils/PaginationsMove.tsx | 38 +++ src/component/utils/SearchBar.tsx | 95 ++----- src/component/utils/SearchBarButton.tsx | 12 + src/component/utils/SearchBarDropDown.tsx | 56 +++++ src/component/utils/SearchBarInput.tsx | 20 ++ src/component/utils/SearchModal.tsx | 4 +- src/component/utils/SearchRanking.tsx | 22 ++ src/component/utils/SearchRankingItem.tsx | 28 +++ src/component/utils/SearchRankingList.tsx | 62 +++++ src/component/utils/Tooltip.tsx | 7 +- src/constant/breakPoint.ts | 9 + src/constant/{headerMenu.js => headerMenu.ts} | 18 +- src/hook/useApi.ts | 25 +- src/hook/useBound.ts | 19 +- src/hook/useBreakPoint.ts | 28 +++ src/hook/useInterval.ts | 32 +++ src/hook/usePermission.ts | 30 +++ src/hook/useResponsiveWidth.ts | 29 +++ src/type/BookInfo.ts | 10 +- src/type/SearchKeyword.ts | 4 + src/util/date.ts | 43 +++- tsconfig.json | 3 +- vite.config.ts | 3 +- 109 files changed, 2951 insertions(+), 1379 deletions(-) create mode 100644 src/api/cursus/useGetCursusRecommendBooks.ts create mode 100644 src/api/like/index.ts create mode 100644 src/api/searchKeyword/useGetSearchKeyword.ts create mode 100644 src/api/searchKeywords/useGetSearchKeywordsAutocomplete.ts create mode 100644 src/asset/css/BookSearchPreview.css create mode 100644 src/asset/css/BookSearchRecentKeyword.css create mode 100644 src/asset/css/Carousel.css delete mode 100644 src/asset/css/Header.css create mode 100644 src/asset/css/HeaderDefault.css create mode 100644 src/asset/css/HeaderDefaultLNB.css create mode 100644 src/asset/css/HeaderMobile.css create mode 100644 src/asset/css/Loader.css create mode 100644 src/asset/css/MainRecommend.css delete mode 100644 src/asset/css/MobileHeader.css create mode 100644 src/asset/css/PaginationCircle.css create mode 100644 src/asset/css/SearchBarDropDown.css create mode 100644 src/asset/css/SearchRanking.css delete mode 100644 src/asset/img/scroll-icon.svg delete mode 100644 src/component/book/like/ShowLike.tsx create mode 100644 src/component/main/MainRecommend.tsx create mode 100644 src/component/main/MainRecommendList.tsx create mode 100644 src/component/main/MainRecommendTitle.tsx create mode 100644 src/component/utils/BookSearchBar.tsx create mode 100644 src/component/utils/BookSearchPreview.tsx create mode 100644 src/component/utils/BookSearchPreviewList.tsx create mode 100644 src/component/utils/BookSearchRecentKeywords.tsx create mode 100644 src/component/utils/Carousel.tsx create mode 100644 src/component/utils/CarouselContainer.tsx create mode 100644 src/component/utils/CarouselList.tsx create mode 100644 src/component/utils/CarouselNext.tsx create mode 100644 src/component/utils/CarouselPagination.tsx create mode 100644 src/component/utils/CarouselPrev.tsx create mode 100644 src/component/utils/CarouselRoot.tsx create mode 100644 src/component/utils/HeaderDefault.tsx create mode 100644 src/component/utils/HeaderDefaultLNB.tsx create mode 100644 src/component/utils/HeaderMobile.tsx create mode 100644 src/component/utils/Loader.tsx create mode 100644 src/component/utils/ManagementSearchBar.tsx delete mode 100644 src/component/utils/MobileHeader.jsx create mode 100644 src/component/utils/PaginationCircle.tsx create mode 100644 src/component/utils/Paginations.tsx create mode 100644 src/component/utils/PaginationsMove.tsx create mode 100644 src/component/utils/SearchBarButton.tsx create mode 100644 src/component/utils/SearchBarDropDown.tsx create mode 100644 src/component/utils/SearchBarInput.tsx create mode 100644 src/component/utils/SearchRanking.tsx create mode 100644 src/component/utils/SearchRankingItem.tsx create mode 100644 src/component/utils/SearchRankingList.tsx create mode 100644 src/constant/breakPoint.ts rename src/constant/{headerMenu.js => headerMenu.ts} (72%) create mode 100644 src/hook/useBreakPoint.ts create mode 100644 src/hook/useInterval.ts create mode 100644 src/hook/usePermission.ts create mode 100644 src/hook/useResponsiveWidth.ts create mode 100644 src/type/SearchKeyword.ts diff --git a/README.md b/README.md index a3c038d6..d94fe887 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ## 42서울의 도서관, 집현전 +

@@ -19,24 +20,32 @@ ## 🏠 [HOME PAGE](https://42library.kr/) -**1,000명 이상**이 사용하는 42 SEOUL 공식 도서관 웹사이트입니다. +**1,000명 이상**이 사용하는 42 SEOUL 공식 도서관 웹사이트입니다. -사용자와 사서님들에게 피드백을 매주 받는 것은 물론, 사이트에 대한 건의 사항을 **42 Seoul 재단**에서 직접 받기도 합니다. +사용자와 사서님들에게 피드백을 매주 받는 것은 물론, 사이트에 대한 건의 사항을 **42 Seoul 재단**에서 직접 받기도 합니다. 매주 유지보수 팀이 도서관에 상주하여 해당 건의 사항을 해결합니다. ## 📌 집현전 서비스 소개 -### 👩‍👩‍👧‍👦 도서검색 + +### 👩‍👩‍👧‍👦 도서검색 + > - 네이버에서 제공하는 모든 도서를 검색 가능합니다. > - 도서명, ISBN, 저자명으로 검색합니다. + ### 📈 도서 대출과 반납, 등록 가능 + > - 도서의 **QR코드** 및 바코드를 읽어 간단하게 책을 대출해드릴 수 있습니다. > - 같은 방법으로, 집현전에 새로운 도서를 등록하여 DB에 저장 가능합니다. + ### 📆 간편한 마이페이지 제공 + > - 자신의 연체 날짜를 확인하며, 대출하거나 예약한 책의 내역을 봅니다. > - 이메일과 비밀번호를 변경할 수 있습니다. > - 연체 날짜가 잘못된 경우, 사서가 이를 수정해줄 수 있습니다. + ### 📩 42 OAuth 2.0 활용 + > - 이메일과 비밀번호로 회원가입은 물론, [**42의 OAuth API**](https://api.intra.42.fr/apidoc/guides/specification)를 통한 로그인 인증이 가능합니다. ## 📌 시연 영상 @@ -56,6 +65,7 @@ **1. MySQL 다운로드 후 워크벤치에 DB 스키마 설계 후 실행** **2. 백엔드** + - 백엔드 환경 변수 설정 - backend폴더 바로 안에 .env 파일 생성 - .env 예시 @@ -84,17 +94,20 @@ ``` **3. 프론트엔드** + - 프론트엔드 환경 변수 설정 - - 폴더에 .env 파일 생성 - - .env 예시 - ``` - REACT_APP_API=... - REACT_APP_WISH=... - REACT_APP_E_BOOK_LIBRARY=... - PORT=... - REACT_APP_SUGGESTION=... - ``` + + - 폴더에 .env 파일 생성 + - .env 예시 + ``` + REACT_APP_API=... + REACT_APP_WISH=... + REACT_APP_E_BOOK_LIBRARY=... + PORT=... + REACT_APP_SUGGESTION=... + ``` - 실행 + ```jsx pnpm install pnpm dev @@ -118,3 +131,4 @@ - [Jaekim](https://github.com/jae-hwan-kim) - [Chanheki](https://github.com/chanhihi) - [Youkim](https://github.com/scarf005) +- [Mingkang](https://github.com/mink97) diff --git a/package.json b/package.json index ba0222df..62c8352c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-router-dom": "^6.11.2", "recoil": "^0.7.7", "vite": "^4.3.8", + "vite-tsconfig-paths": "^4.2.0", "web-vitals": "^3.3.1" }, "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d783787..0c55ed7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -65,6 +65,9 @@ dependencies: vite: specifier: ^4.3.8 version: 4.3.8 + vite-tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0(vite@4.3.8) web-vitals: specifier: ^3.3.1 version: 3.3.1 @@ -4703,6 +4706,17 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: false + /tsconfck@2.1.2: + resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} + engines: {node: ^14.13.1 || ^16 || >=18} + hasBin: true + peerDependencies: + typescript: ^4.3.5 || ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + dev: false + /tsconfig-paths@3.14.2: resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} dependencies: @@ -4906,6 +4920,23 @@ packages: - supports-color dev: false + /vite-tsconfig-paths@4.2.0(vite@4.3.8): + resolution: {integrity: sha512-jGpus0eUy5qbbMVGiTxCL1iB9ZGN6Bd37VGLJU39kTDD6ZfULTTb1bcc5IeTWqWJKiWV5YihCaibeASPiGi8kw==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + dependencies: + debug: 4.3.4 + globrex: 0.1.2 + tsconfck: 2.1.2 + vite: 4.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /vite@4.2.1: resolution: {integrity: sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src/App.jsx b/src/App.jsx index ca53d438..a7371532 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,7 +6,6 @@ import BookDetail from "./component/book/BookDetail"; import Footer from "./component/utils/Footer"; import NotFound from "./component/utils/NotFound"; import Header from "./component/utils/Header"; -import MobileHeader from "./component/utils/MobileHeader"; import Information from "./component/information/Information"; import Main from "./component/main/Main"; import Search from "./component/search/Search"; @@ -52,7 +51,6 @@ function App() {
- } /> } /> diff --git a/src/api/books/useGetBooksInfoNew.ts b/src/api/books/useGetBooksInfoNew.ts index 95bf0ce5..c2735593 100644 --- a/src/api/books/useGetBooksInfoNew.ts +++ b/src/api/books/useGetBooksInfoNew.ts @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; import { useApi } from "../../hook/useApi"; import { compareExpect } from "../../util/typeCheck"; -import { Book } from "../../type"; +import { BookInfo } from "../../type"; export const useGetBooksInfoNew = () => { - const [bookList, setBookList] = useState([]); + const [bookList, setBookList] = useState([]); const { request } = useApi("get", "books/info/", { sort: "new", diff --git a/src/api/books/useGetBooksInfoPopular.ts b/src/api/books/useGetBooksInfoPopular.ts index 0c20c58b..7d741034 100644 --- a/src/api/books/useGetBooksInfoPopular.ts +++ b/src/api/books/useGetBooksInfoPopular.ts @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; import { useApi } from "../../hook/useApi"; import { compareExpect } from "../../util/typeCheck"; -import { Book } from "../../type"; +import { BookInfo } from "../../type"; export const useGetBooksInfoPopular = () => { - const [docs, setDocs] = useState([]); + const [docs, setDocs] = useState<(BookInfo & { rank: number })[]>([]); const { request } = useApi("get", "books/info", { sort: "popular", @@ -27,7 +27,11 @@ export const useGetBooksInfoPopular = () => { response.data.items, expectedItem, ); - setDocs(books); + const booksWithRank = books.map((book, index) => ({ + ...book, + rank: index + 1, + })); + setDocs(booksWithRank); }; useEffect(() => request(refineResponse), []); diff --git a/src/api/books/useGetBooksInfoSearchUrl.ts b/src/api/books/useGetBooksInfoSearchUrl.ts index a7ea61c6..71767757 100644 --- a/src/api/books/useGetBooksInfoSearchUrl.ts +++ b/src/api/books/useGetBooksInfoSearchUrl.ts @@ -24,7 +24,7 @@ export const useGetBooksInfoSearchUrl = () => { query, page: page ? page - 1 : 0, limit: 20, - sort, + sort: sort ?? "title", category, }); diff --git a/src/api/books/usePatchBooksUpdate.ts b/src/api/books/usePatchBooksUpdate.ts index 13a20bd5..00cdaf0c 100644 --- a/src/api/books/usePatchBooksUpdate.ts +++ b/src/api/books/usePatchBooksUpdate.ts @@ -11,7 +11,10 @@ type Props = { export const usePatchBooksUpdate = ({ bookTitle, closeModal }: Props) => { const [change, setChange] = useState>(); - const { request } = useApi("patch", "books/update", change); + const { request } = useApi("patch", "books/update", { + ...change, + categoryId: change?.categoryId ? change.categoryId + 1 : undefined, // DB에는 1부터 저장되어 있으므로 +1 + }); const { addDialogWithTitleAndMessage } = useNewDialog(); diff --git a/src/api/cursus/useGetCursusRecommendBooks.ts b/src/api/cursus/useGetCursusRecommendBooks.ts new file mode 100644 index 00000000..0f1c9118 --- /dev/null +++ b/src/api/cursus/useGetCursusRecommendBooks.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { useApi } from "~/hook/useApi"; +import type { BookInfoRecommend } from "~/type"; + +export const useGetCursusRecommendBooks = () => { + const [isLoading, setIsLoading] = useState(false); + const [selectedOption, setSelectedOption] = useState(); + const [recommend, setRecommend] = useState({ + books: [], + options: ["사용자 지정"], + }); + const { requestWithUrl } = useApi(); + useEffect(() => { + setIsLoading(true); + const saveRecommend = (response: any) => { + const { items, meta } = response.data; + meta.sort(); + setRecommend({ + books: items, + options: [ + "사용자 지정", + ...meta.filter((item: string) => item !== "사용자 지정"), + ], + }); + setIsLoading(false); + }; + requestWithUrl("get", "/cursus/recommend/books", { + data: { + project: selectedOption + ? selectedOption.split("| ")[1] ?? undefined + : undefined, + }, + onSuccess: saveRecommend, + }); + }, [selectedOption]); + + return { ...recommend, setSelectedOption, isLoading }; +}; + +type Recommend = { + books: BookInfoRecommend[]; + options: string[]; +}; diff --git a/src/api/like/index.ts b/src/api/like/index.ts new file mode 100644 index 00000000..860a486e --- /dev/null +++ b/src/api/like/index.ts @@ -0,0 +1,3 @@ +export * from "./useDeleteLike"; +export * from "./useGetLike"; +export * from "./usePostLike"; diff --git a/src/api/like/useDeleteLike.ts b/src/api/like/useDeleteLike.ts index fff946b3..90a67f53 100644 --- a/src/api/like/useDeleteLike.ts +++ b/src/api/like/useDeleteLike.ts @@ -1,31 +1,22 @@ -import { useEffect, useState } from "react"; -import { compareExpect } from "../../util/typeCheck"; -import { useApi } from "../../hook/useApi"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { useApi } from "~/hook/useApi"; -export const useDeleteLike = (initBookInfoId?: number) => { - const [bookInfoId, setBookInfoId] = useState(initBookInfoId); - const { request } = useApi("delete", `books/info/${bookInfoId}/like`); - const [likeData, setLikeData] = useState({}); +type Props = { + setLike: Dispatch>; +}; - const expectedItem = [ - { key: "bookInfoId", type: "number", isNullable: false }, - { key: "isLiked", type: "bool", isNullable: false }, - { key: "likeNum", type: "number", isNullable: false }, - ]; +export const useDeleteLike = ({ setLike }: Props) => { + const [bookInfoId, setBookInfoId] = useState(); + const { request } = useApi("delete", `books/info/${bookInfoId}/like`); - const refineResponse = (response: any) => { - const [refinelikeData] = compareExpect( - `books/info/${initBookInfoId}/like`, - [response.data], - expectedItem, - ); - setLikeData(refinelikeData); + const onSuccess = () => { + setLike(prev => ({ isLiked: false, likeNum: prev.likeNum - 1 })); }; useEffect(() => { - if (bookInfoId) request(refineResponse); + if (bookInfoId) request(onSuccess); setBookInfoId(undefined); }, [bookInfoId]); - return { setBookInfoId, likeData }; + return { setBookInfoId }; }; diff --git a/src/api/like/useGetLike.ts b/src/api/like/useGetLike.ts index f3b72ec9..27e312b6 100644 --- a/src/api/like/useGetLike.ts +++ b/src/api/like/useGetLike.ts @@ -1,42 +1,24 @@ import { useEffect, useState } from "react"; -import getErrorMessage from "../../constant/error"; -import { useApi } from "../../hook/useApi"; -import { compareExpect } from "../../util/typeCheck"; +import { useApi } from "~/hook/useApi"; type Props = { - initBookInfoId: number; - setCurrentLike: (isLiked: boolean) => void; - setCurrentLikeNum?: (likeNum: number) => void; + bookInfoId: number; }; -export const useGetLike = ({ - initBookInfoId, - setCurrentLike, - setCurrentLikeNum, -}: Props) => { - const { request } = useApi("get", `books/info/${initBookInfoId}/like`); - const [likeData, setLikeData] = useState({}); - - const expectedItem = [ - { key: "bookInfoId", type: "number", isNullable: false }, - { key: "isLiked", type: "bool", isNullable: false }, - { key: "likeNum", type: "number", isNullable: false }, - ]; +export const useGetLike = ({ bookInfoId }: Props) => { + const { request } = useApi("get", `books/info/${bookInfoId}/like`); + const [like, setLike] = useState({ + isLiked: false, + likeNum: 0, + }); const refineResponse = (response: any) => { - const [refinelikeData] = compareExpect( - `books/info/${initBookInfoId}/like`, - [response.data], - expectedItem, - ); - setLikeData(refinelikeData); - setCurrentLike(refinelikeData.isLiked); - setCurrentLikeNum && setCurrentLikeNum(refinelikeData.likeNum); + setLike(response.data); }; useEffect(() => { - if (initBookInfoId) request(refineResponse); + if (bookInfoId) request(refineResponse); }, []); - return { likeData }; + return { like, setLike }; }; diff --git a/src/api/like/usePostLike.ts b/src/api/like/usePostLike.ts index b755385f..1921e02a 100644 --- a/src/api/like/usePostLike.ts +++ b/src/api/like/usePostLike.ts @@ -1,29 +1,22 @@ -import { useEffect, useState } from "react"; -import { useApi } from "../../hook/useApi"; -import { compareExpect } from "../../util/typeCheck"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { useApi } from "~/hook/useApi"; -export const usePostLike = (initBookInfoId?: number) => { - const [bookInfoId, setBookInfoId] = useState(initBookInfoId); - const { request } = useApi("post", `books/info/${bookInfoId}/like`); - const [likeData, setLikeData] = useState({}); +type Props = { + setLike: Dispatch>; +}; - const expectedItem = [ - { key: "userId", type: "number", isNullable: false }, - { key: "bookInfoId", type: "number", isNullable: false }, - ]; +export const usePostLike = ({ setLike }: Props) => { + const [bookInfoId, setBookInfoId] = useState(); + const { request } = useApi("post", `books/info/${bookInfoId}/like`); - const refineResponse = (response: any) => { - const [refinelikeData] = compareExpect( - `books/info/${initBookInfoId}/like`, - [response.data], - expectedItem, - ); - setLikeData(refinelikeData); + const onSuccess = () => { + setLike(prev => ({ isLiked: true, likeNum: prev.likeNum + 1 })); }; + useEffect(() => { - if (bookInfoId) request(refineResponse); + if (bookInfoId) request(onSuccess); setBookInfoId(undefined); }, [bookInfoId]); - return { setBookInfoId, likeData }; + return { setBookInfoId }; }; diff --git a/src/api/searchKeyword/useGetSearchKeyword.ts b/src/api/searchKeyword/useGetSearchKeyword.ts new file mode 100644 index 00000000..c9aee944 --- /dev/null +++ b/src/api/searchKeyword/useGetSearchKeyword.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; +import { useApi } from "~/hook/useApi"; +import { SearchKeyword } from "~/type/SearchKeyword"; + +export const useGetSearchKeyword = () => { + const [keywords, setKeywords] = useState<(SearchKeyword & { id: number })[]>( + [], + ); + const { requestWithUrl } = useApi(); + + useEffect(() => { + const saveKeywordsWithKey = (response: any) => { + const keywords = response.data.items as SearchKeyword[]; + setKeywords( + keywords.map((keyword, index) => ({ + ...keyword, + id: index, + })), + ); + }; + + requestWithUrl("get", "/search-keywords/popular", { + onSuccess: saveKeywordsWithKey, + }); + }, []); + + return { keywords }; +}; diff --git a/src/api/searchKeywords/useGetSearchKeywordsAutocomplete.ts b/src/api/searchKeywords/useGetSearchKeywordsAutocomplete.ts new file mode 100644 index 00000000..993abecb --- /dev/null +++ b/src/api/searchKeywords/useGetSearchKeywordsAutocomplete.ts @@ -0,0 +1,37 @@ +import { useDeferredValue, useEffect, useState } from "react"; +import { useApi } from "~/hook/useApi"; +import { useDebounce } from "~/hook/useDebounce"; +import { type BookPreviewType } from "~/type"; + +export const useGetSearchKeywordsAutocomplete = () => { + const [isLoading, setIsLoading] = useState(true); + const [keyword, setKeyword] = useState(""); + const defferedKeyword = useDeferredValue(keyword); + const [data, setData] = useState<{ + books: BookPreviewType[]; + totalCount: number; + }>({ + books: [], + totalCount: 0, + }); + + const { requestWithUrl } = useApi(); + const debounce = useDebounce(); + + useEffect(() => { + setIsLoading(true); + debounce(() => { + requestWithUrl("get", "/search-keywords/autocomplete", { + data: { keyword }, + onSuccess: (response: any) => { + setData({ + books: response.data.items, + totalCount: response.data.meta.totalCount, + }); + setIsLoading(false); + }, + }); + }, 300); + }, [defferedKeyword]); + return { ...data, keyword, setKeyword, isLoading }; +}; diff --git a/src/asset/css/BookSearchPreview.css b/src/asset/css/BookSearchPreview.css new file mode 100644 index 00000000..5f7a3a86 --- /dev/null +++ b/src/asset/css/BookSearchPreview.css @@ -0,0 +1,119 @@ +.search-preview__wrapper { + position: relative; + background-color: white; + z-index: 2; + display: block; +} + +.search-preview__wrapper.loading { + background-color: rgba(200, 200, 200, 0.8); +} + +.search-preview__loader { + position: absolute !important; + top: 40%; + left: 50%; +} + +.search-preview__list { + display: flex; + overflow: hidden; + width: 100%; +} + +.search-preview__books { + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; +} + +.search-preview__book { + text-align: left; + font-size: 1.4rem; + height: 6rem; + cursor: pointer; + margin: 0; + padding: 0 2rem; + background-color: rgba(255, 255, 255, 0.8); + border: none; + border-bottom: solid 1px gray; + padding: 1rem; + box-sizing: border-box; +} + +.search-preview__book.selected { + background-color: rgba(200, 200, 200, 0.6); +} + +.search-preview__book__title { + white-space: nowrap; + width: 100%; + overflow: hidden; + font-size: 1.6rem; + font-weight: bold; + margin-bottom: 0.4rem; + text-overflow: ellipsis; + box-sizing: border-box; + flex: 0 1 auto; +} + +.search-preview__book__author:after { + content: "|"; + margin: 0 1rem; +} + +.search-preview__no-result { + margin: auto; +} + +.search-preview__image { + flex-shrink: 0; +} + +.search-preview__pagination.pagination { + display: flex; + align-items: center; + margin-top: 0.4rem; + padding: 0; + min-width: auto; +} +.search-preview__pagination.pagination > span { + font-size: 1.4rem; + margin-top: 0.4rem; +} + +.search-preview__pagination.pagination > span:before { + content: "0"; +} + +.search-preview__page { + font-weight: bold; +} +.search-preview__last-page { + color: #5d5d5d; +} + +.search-preview__page:after { + content: "|"; + margin: 0 1rem; + font-weight: normal; +} + +.search-preview__more { + padding: 1rem; + display: block; + text-align: center; + font-size: 1.4rem; + margin: auto; +} + +@media screen and (max-width: 767px) { + .search-preview__book { + font-size: 1.2rem; + } + .search-preview__book__title { + font-size: 1.4rem; + } +} diff --git a/src/asset/css/BookSearchRecentKeyword.css b/src/asset/css/BookSearchRecentKeyword.css new file mode 100644 index 00000000..45ca6d5d --- /dev/null +++ b/src/asset/css/BookSearchRecentKeyword.css @@ -0,0 +1,58 @@ +.recent-keyword__wrapper { + padding: 0.8rem 0.4rem; + display: flex; +} + +.recent-keyword__wrapper > h3 { + font-size: 1.6rem; + font-weight: bold; + margin-right: 2rem; + flex-shrink: 0; + margin: 1.6rem 1.2rem; +} + +.recent-keyword__list { + display: flex; + flex-wrap: wrap; + width: 100%; + font-size: 1.4rem; + gap: 0.8rem; + padding-right: 1rem; +} + +.recent-keyword__keyword { + position: relative; + white-space: wrap; + word-break: break-all; + display: flex; + align-items: center; +} + +.recent-keyword__keyword > a { + display: inline-block; + padding: 0.8rem 3.2rem 0.8rem 1.6rem; + border-radius: 1.6rem; + background: #d9d9d9; +} + +.recent-keyword__remove { + background: none; + border: none; + cursor: pointer; + position: absolute; + right: 0; + padding: 1rem; +} +.recent-keyword__remove > img { + width: 1rem; + height: 1rem; +} +.recent-keyword__remove:hover:after { + content: " "; + width: 20px; + height: 20px; + background: #d9d9d9; +} +.recet-keyword__no-record { + margin: 0.8rem; +} diff --git a/src/asset/css/Carousel.css b/src/asset/css/Carousel.css new file mode 100644 index 00000000..c8508f00 --- /dev/null +++ b/src/asset/css/Carousel.css @@ -0,0 +1,14 @@ +.carousel__container { + overflow: hidden; + position: relative; +} + +.carousel__list { + display: flex; +} + +.carousel__list > * { + display: block; + box-sizing: border-box; + flex: 0 0; +} diff --git a/src/asset/css/Header.css b/src/asset/css/Header.css deleted file mode 100644 index 9310fe10..00000000 --- a/src/asset/css/Header.css +++ /dev/null @@ -1,233 +0,0 @@ -@media screen and (max-width: 1199px) { - .gnb__text { - font-size: 1.6rem !important; - } - - .lnb__text { - font-size: 1.4rem !important; - } -} - -a:hover { - text-decoration: none; -} - -a:active { - text-decoration: none; -} - -.header { - width: 100%; - margin: auto; - position: absolute; - z-index: 5; - top: 0; - left: 50%; - transform: translate(-50%); -} - -@media screen and (max-width: 767px) { - .header { - display: none; - } -} - -.header-wrapper { - padding: 7rem 15rem 0 15rem; - display: flex; - max-width: 120rem; - min-width: 88rem; - margin: auto; - flex-grow: 1; -} - -.header__logo { - flex: none; - margin: 0 0 0 0; -} - -.logo_img { - width: 12.4rem; - height: 4.2rem; - object-fit: contain; -} - -.header__gnb { - margin-left: auto; -} - -.gnb__menu { - list-style: none; - display: flex; - padding: 0 0 0 0; - margin: 1rem 0 0 0; -} - -.gnb__text { - margin: 0 0 0 0; - font-weight: normal; - font-stretch: normal; - font-style: normal; - line-height: 1.5; - letter-spacing: normal; - text-align: left; - color: #fff; - word-break: keep-all; -} - -.gnb__user__text { - max-width: 10rem; - overflow: hidden; - text-overflow: ellipsis; -} - -.gnb__icon { - margin: 0 1rem 0 0; - object-fit: contain; -} - -.gnb__book__icon { - width: 1.65rem; - height: 2.2rem; -} - -.gnb__info__icon { - width: 2.2rem; - height: 2.2rem; -} - -.gnb__user__button { - all: unset; - margin: 0 0 0 5rem; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; -} - -.gnb__button { - margin: 0 0 0 5rem; - display: flex; - align-items: center; - justify-content: center; -} - -.gnb__user__icon { - width: 2.2rem; - height: 2.2rem; -} - -.gnb__dropdown__icon { - width: 1.3rem; - height: 1.3rem; - margin: 0.5rem 0 0 0.8rem; -} - -.gnb__user__lnb { - display: flex; - flex-direction: row; - justify-content: flex-start; - position: relative; - flex-direction: row-reverse; - right: 0.3rem; -} - -.lnb__line { - position: relative; - width: 0; - border-right: solid 0.1rem #fff; - top: -1rem; - right: 0.3rem; -} - -.lnb__circle { - width: 0.7rem; - height: 0.7rem; - border: 0; - padding: 0; - background-color: #fff; - border-radius: 100%; -} - -.lnb__menu { - list-style: none; - padding-inline-start: 0; - display: flex; - flex-direction: column; - align-items: flex-end; - position: relative; - left: 0.1rem; -} - -.lnb__text { - word-break: keep-all; - white-space: nowrap; - font-weight: normal; - font-stretch: normal; - font-style: normal; - letter-spacing: normal; - text-align: right; - opacity: 0.8; - margin-right: 1rem; -} - -.lnb__text:hover { - opacity: 1; - font-weight: bold; -} - -.lnb__menu_button { - margin-top: 1.6rem; - display: flex; - align-items: center; -} - -@media screen and (max-width: 1199px) { - .header-wrapper { - padding: 5rem 8rem 0 8rem; - min-width: 58.8rem; - } - - .gnb__user__button { - margin: 0 0 0 4rem; - } - - .gnb__button { - margin: 0 0 0 4rem; - } - - .logo_img { - width: 11.16rem; - height: 3.78rem; - } - - .gnb__book__icon { - width: 1.5rem; - height: 2rem; - } - - .gnb__info__icon { - width: 2rem; - height: 2rem; - } - - .gnb__user__icon { - width: 2rem; - height: 2rem; - } - - .gnb__dropdown__icon { - width: 1.3rem; - height: 1.3rem; - margin: 0.6rem 0 0 0.8rem; - } - - .lnb__circle { - width: 0.7rem; - height: 0.7rem; - } - - .lnb__menu_button { - margin-top: 1.4rem; - } -} diff --git a/src/asset/css/HeaderDefault.css b/src/asset/css/HeaderDefault.css new file mode 100644 index 00000000..14475c55 --- /dev/null +++ b/src/asset/css/HeaderDefault.css @@ -0,0 +1,61 @@ +.header__wrapper { + position: absolute; + max-width: 150rem; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + left: 0; + right: 0; + margin: auto; + box-sizing: border-box; + padding: 5rem 8rem 0; +} + +.header__logo > img { + width: 11.16rem; + height: 3.78rem; +} + +.header__gnb__wrapper { + display: flex; + align-items: center; + position: relative; +} + +.header__gnb__menu { + position: relative; + display: flex; + align-items: center; + margin-left: 4rem; + font-size: 1.6rem; + color: white; + line-height: 1.5; +} + +.header__gnb__icon { + width: 2rem; + height: 2rem; + margin-right: 1rem; + object-fit: contain; +} + +@media screen and (min-width: 1200px) { + .header__wrapper { + padding: 7rem 15rem 0; + } + + .header__logo > img { + width: 12.4rem; + height: 4.2rem; + } + + .header__gnb__menu { + font-size: 1.8rem; + margin-left: 5rem; + } + .header__gnb__icon { + width: 2.2rem; + height: 2.2rem; + } +} diff --git a/src/asset/css/HeaderDefaultLNB.css b/src/asset/css/HeaderDefaultLNB.css new file mode 100644 index 00000000..ee21bc4e --- /dev/null +++ b/src/asset/css/HeaderDefaultLNB.css @@ -0,0 +1,78 @@ +.header__lnb-toggle { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + appearance: none; + background-color: transparent; + border: none; + margin: 0; + padding: 0; + color: white; + font-size: inherit; +} + +.header__gnb__icon.dropdown { + width: 1.3rem; + height: 1.3rem; + margin: 0.5rem 0 0 0.8rem; +} + +.header__lnb-menu__wrapper { + position: absolute; + top: 3rem; + display: flex; + flex-direction: column; + align-items: end; + width: 100%; + box-sizing: border-box; + margin-top: -0.8rem; +} + +.header__lnb-menu__wrapper:after { + content: " "; + position: absolute; + top: -1.2rem; + bottom: -2rem; + right: 0.6rem; + width: 0.1rem; + height: 100%; + background-color: #fff; +} + +.header__lnb-menu__item { + position: relative; + color: white; + padding-right: 2rem; + margin-top: 0.8rem; +} + +.header__lnb-menu__item::after { + content: " "; + position: absolute; + top: 40%; + right: 0.3rem; + width: 0.7rem; + height: 0.7rem; + border-radius: 100%; + background-color: #fff; + opacity: 1; +} + +.header__lnb-menu__text { + font-size: 1.4rem; + line-height: 1; + opacity: 0.8; +} + +@media screen and (min-width: 1200px) { + .header__lnb-menu__wrapper { + margin-top: -0.3rem; + } + .header__lnb-menu__item { + margin-top: 0.9피rem; + } + .header__lnb-menu__text { + font-size: 1.6rem; + } +} diff --git a/src/asset/css/HeaderMobile.css b/src/asset/css/HeaderMobile.css new file mode 100644 index 00000000..23d2cf05 --- /dev/null +++ b/src/asset/css/HeaderMobile.css @@ -0,0 +1,48 @@ +.header-mobile__wrapper { + width: 100%; + margin: auto; + position: fixed; + z-index: 3; + top: 0; + display: flex; + justify-content: space-between; + padding: 3.5rem 3rem 0 3rem; + box-sizing: border-box; +} + +.header-mobile__wrapper.fixed { + z-index: 5; + background-color: rgba(45, 45, 45, 0.97); + padding: 1.8rem 2rem; +} + +.header-mobile__logo > img { + width: 9.3rem; + height: 3.15rem; + object-fit: contain; +} + +.header-mobile__gnb__wrapper { + display: flex; + align-items: center; + position: relative; +} + +.header-mobile__search > img { + width: 2rem; + height: 2rem; + object-fit: contain; +} + +.header-mobile__hamburger { + background-color: transparent; + border: none; + margin: 0 0 0 3.5rem; + padding: 0; +} + +.header-mobile__hamburger > img { + width: 3.15rem; + height: 3.15rem; + object-fit: contain; +} diff --git a/src/asset/css/Loader.css b/src/asset/css/Loader.css new file mode 100644 index 00000000..daeb549c --- /dev/null +++ b/src/asset/css/Loader.css @@ -0,0 +1,99 @@ +.loader__backdrop { + position: absolute; + background-color: rgba(100, 100, 100, 0.5); + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 20; +} + +.loader__loader { + font-size: 10px; + width: 1em; + height: 1em; + border-radius: 50%; + position: relative; + text-indent: -9999em; + animation: mulShdSpin 1.1s infinite ease; + transform: translateZ(0); + z-index: 10; +} +@keyframes mulShdSpin { + 0%, + 100% { + box-shadow: 0em -2.6em 0em 0em #ffffff, + 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), + 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), + 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), + 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), + -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), + -2.6em 0em 0 0em rgba(255, 255, 255, 0.5), + -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7); + } + 12.5% { + box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7), + 1.8em -1.8em 0 0em #ffffff, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), + 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), + 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), + -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), + -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), + -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5); + } + 25% { + box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5), + 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #ffffff, + 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), + 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), + -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), + -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), + -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); + } + 37.5% { + box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), + 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5), + 2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em #ffffff, + 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), + -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), + -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), + -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); + } + 50% { + box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), + 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), + 2.5em 0em 0 0em rgba(255, 255, 255, 0.5), + 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #ffffff, + -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), + -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), + -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); + } + 62.5% { + box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), + 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), + 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), + 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5), + 0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #ffffff, + -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), + -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); + } + 75% { + box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), + 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), + 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), + 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), + 0em 2.5em 0 0em rgba(255, 255, 255, 0.5), + -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #ffffff, + -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); + } + 87.5% { + box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), + 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), + 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), + 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), + 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), + -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5), + -2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #ffffff; + } +} + +/* 출처 https://cssloaders.github.io/ */ diff --git a/src/asset/css/MainBanner.css b/src/asset/css/MainBanner.css index 82daab55..885c5ead 100644 --- a/src/asset/css/MainBanner.css +++ b/src/asset/css/MainBanner.css @@ -63,8 +63,32 @@ font-size: 1.2rem; } -.main-banner__scroll_icon { +.main-banner__mouse { margin-top: 1.6rem; + width: 2rem; + height: 3.4rem; + border: 0.2rem solid rgba(255, 255, 255, 0.5); + border-radius: 6rem; + position: relative; + &::before { + content: ""; + width: 0.6rem; + height: 0.6rem; + position: absolute; + top: 0.3rem; + left: 50%; + transform: translateX(-50%); + background-color: white; + border-radius: 50%; + animation: wheel 3s infinite; + } +} + +@keyframes wheel { + to { + opacity: 0; + top: 2.4rem; + } } @media screen and (max-width: 1199px) { diff --git a/src/asset/css/MainNew.css b/src/asset/css/MainNew.css index 2b5bb8b6..aa1decf5 100644 --- a/src/asset/css/MainNew.css +++ b/src/asset/css/MainNew.css @@ -12,14 +12,13 @@ .main-new__arrow { margin: 0; position: absolute; - top: 27.5rem; + bottom: 10rem; left: 0; background-color: rgba(0, 0, 0, 0.4); border: none; z-index: 1; padding: 0; transition: all 0.3s ease-in-out; - -webkit-transition: 0.3s; } .main-new__arrow.right { @@ -35,15 +34,9 @@ position: relative; z-index: 0; width: 100%; - overflow: hidden; margin-top: 10rem; } -.main-new__books { - transition: all 0.3s ease-in-out; - -webkit-transition: 0.3s; -} - .main-new__book { text-align: none; display: inline-block; @@ -53,13 +46,8 @@ .main-new__book:hover { margin-top: 1rem; - -webkit-transform: scale(1.05); - -moz-transform: scale(1.05); - -ms-transform: scale(1.05); - -o-transform: scale(1.05); transform: scale(1.05); transition: all 0.3s ease-in-out; - -webkit-transition: 0.3s; } .main-new__book:first-child { @@ -85,39 +73,21 @@ } .main-new__books_pagination { - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); -} - -.main-new__books_pag_circle { - width: 1.2rem; - height: 1.2rem; - border-radius: 50%; - background-color: #a4a4a4; - display: inline-block; - margin-left: 1.2rem; - border: none; - padding: 0; - cursor: pointer; - color: transparent; -} - -.main-new__books_pag_circle:first-child { - margin: 0; -} - -.main-new__books_pag_circle.selected { - background-color: #2d2d2d; + display: flex; + justify-content: center; + margin-top: 10rem; } -@media screen and (max-width:767px) { +@media screen and (max-width: 767px) { .main-new { height: 52rem; } .main-new__book { - margin-left:1rem; + margin-left: 1rem; + } + + .main-new__arrow { + bottom: 7.2rem; } } diff --git a/src/asset/css/MainRecommend.css b/src/asset/css/MainRecommend.css new file mode 100644 index 00000000..8518c2a6 --- /dev/null +++ b/src/asset/css/MainRecommend.css @@ -0,0 +1,169 @@ +.main__recommend__wrapper { + position: relative; + font: inherit; + margin: 24rem auto 0; + width: 80rem; + min-height: 60rem; + min-width: 36rem; +} + +.main__recommend__title__wrapper { + margin-bottom: 4rem; +} + +.main__recommend__title__title { + display: flex; + justify-content: center; + align-items: center; +} + +.main__recommend__title__select { + font: inherit; + font-size: 2.4rem; + font-weight: bold; + margin: 0 0 0 2.4rem; + padding: 0.4rem 0.8rem; + border-radius: 1rem; +} + +.main__recommend-list__container, +.main__recommend__loader { + background-color: #b2b2b2; + + margin: 0; + border-radius: 3rem; + box-sizing: border-box; + outline: #f2f2f2 solid 2rem; + min-height: 30rem; +} + +.main__recommend__loader { + display: flex; + align-items: center; + justify-content: center; + background-color: #a4a4a4; +} + +.main__recommend-list__book { + background-color: white; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 4rem; +} + +.main__recommend-list__cover { + width: 20rem; + height: 24rem; + margin: 4rem 0 4rem 2rem; + flex: 0 0 20rem; +} + +.main__recommend-list__detail { + display: block; + flex-direction: column; + justify-content: flex-start; + font-size: 1.8rem; + line-height: 2; + text-align: left; + overflow: hidden; + padding: 4rem 2rem; + padding-left: 0; + box-sizing: border-box; +} + +.main__recommend-list__detail > p { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} +.main__recommend-list__title { + max-height: 10rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre-wrap; + font-size: 2.4rem; + font-weight: bold; + margin-bottom: 1.6rem; + line-height: 1.4; +} + +.main__recommend-list__arrow-button { + position: absolute; + bottom: 20rem; + transform: translateY(-50%); + width: 4rem; + height: 4rem; + background-color: transparent; + border: none; + right: -10%; + cursor: pointer; +} + +.main__recommend-list__arrow-button.left { + left: -10%; + transform: translateY(-50%) rotate(180deg); +} + +.main__recommend-list__arrow-button > img { + width: 100%; + height: 100%; +} +.main__recommend-list__pagination { + margin: 8rem auto 0; +} + +@media screen and (max-width: 1200px) { + .main__recommend__wrapper { + width: 80%; + } +} + +@media screen and (max-width: 767px) { + .main__recommend__title__wrapper { + height: auto; + margin-bottom: 6rem; + } + .main__recommend__title__title { + flex-direction: column; + align-items: start; + } + .main__recommend__title__select { + margin: 1rem 0; + } + .main__recommend-list__book { + display: block; + line-height: 1.2; + } + + .main__recommend-list__cover { + margin: 4rem auto 0; + display: block; + } + + .main__recommend-list__detail { + margin-left: 0; + margin-top: 2rem; + text-align: center; + align-items: center; + font-size: 1.4rem; + line-height: 1.4; + padding: 2rem 8rem 4rem; + } + + .main__recommend-list__title { + font-size: 1.6rem; + margin-bottom: 1.2rem; + } + .main__recommend-list__arrow-button { + bottom: 16rem; + } + + .main__recommend-list__arrow-button { + right: 4%; + } + .main__recommend-list__arrow-button.left { + left: 4%; + } +} diff --git a/src/asset/css/MobileHeader.css b/src/asset/css/MobileHeader.css deleted file mode 100644 index 13bac30e..00000000 --- a/src/asset/css/MobileHeader.css +++ /dev/null @@ -1,118 +0,0 @@ -.mobile-header { - width: 100%; - margin: auto; - position: fixed; - z-index: 3; - top: 0; - left: 50%; - transform: translate(-50%); - display: none; -} - -.fixed-header { - width: 100%; - margin: auto; - position: fixed; - z-index: 5; - top: 0; - left: 50%; - transform: translate(-50%); - display: none; - background-color: rgba(45, 45, 45, 0.97); -} - -@media screen and (max-width: 767px) { - .mobile-header { - display: block; - } - .fixed-header { - display: block; - } - .fixed-header-none { - display: block; - } -} - -.f-header { - padding: 1.8rem 2rem 1.8rem 2rem; - display: flex; - max-width: 71.7rem; - min-width: 31rem; - margin: auto; - flex-grow: 1; - align-items: center; -} - -.f-header-none { - padding: 1.8rem 2rem 1.8rem 2rem; - display: none; - max-width: 71.7rem; - min-width: 31rem; - margin: auto; - flex-grow: 1; - align-items: center; - /* visibility: hidden; */ -} - -.m-header { - padding: 3.5rem 3rem 0rem 3rem; - display: flex; - max-width: 70.7rem; - min-width: 30rem; - margin: auto; - flex-grow: 1; - align-items: center; -} - -.m-header__logo { - flex: none; - margin: 0 0 0 0; -} - -.m-header__logo-icon { - width: 9.3rem; - height: 3.15rem; - object-fit: contain; -} - -.m-header__gnb { - margin-left: auto; -} - -.m-header__ul { - list-style: none; - display: flex; - padding: 0 0 0 0; - margin: 0 0 0 0; - align-items: center; -} - -.m-header__gnb__search-icon { - width: 2rem; - height: 2rem; - margin: 0 0 0 0; - object-fit: contain; -} - -.gnb__hamburger__icon { - width: 3.15rem; - height: 3.15rem; - margin: 0 0 0 0; - object-fit: contain; -} - -.m-header__button { - margin: 0 0 0 0rem; - display: flex; - align-items: center; - justify-content: center; -} - -.m-header__hamburger-button { - all: unset; - margin: 0 0 0 3.5rem; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; -} diff --git a/src/asset/css/Pagination.css b/src/asset/css/Pagination.css index abc6baa9..8e95bc85 100644 --- a/src/asset/css/Pagination.css +++ b/src/asset/css/Pagination.css @@ -31,6 +31,10 @@ cursor: pointer; } +.pagination__page-range-button:disabled { + opacity: 0.4; +} + .pagination__page-range-button > img { width: 0.8rem; height: 1.6rem; diff --git a/src/asset/css/PaginationCircle.css b/src/asset/css/PaginationCircle.css new file mode 100644 index 00000000..b396b505 --- /dev/null +++ b/src/asset/css/PaginationCircle.css @@ -0,0 +1,25 @@ +.pagination-circle__wrapper { + display: flex; + justify-content: center; +} + +.pagination-circle__circle { + width: 1.2rem; + height: 1.2rem; + border-radius: 50%; + background-color: #a4a4a4; + display: inline-block; + margin-left: 1.2rem; + border: none; + padding: 0; + cursor: pointer; + color: transparent; +} + +.pagination-circle__circle:first-child { + margin: 0; +} + +.pagination-circle__circle.selected { + background-color: #2d2d2d; +} diff --git a/src/asset/css/RentModalBookList.css b/src/asset/css/RentModalBookList.css index 9555c510..7bf28162 100644 --- a/src/asset/css/RentModalBookList.css +++ b/src/asset/css/RentModalBookList.css @@ -10,29 +10,18 @@ border-bottom: 1px solid #a4a4a4; cursor: pointer; background: none; - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); transform: scale(1); - -webkit-transition: 0.3s; - -moz-transition: 0.3s; - -ms-transition: 0.3s; - -o-transition: 0.3s; transition: 0.3s; word-break: break-all; white-space: inherit; } + .rent__modal-book-list.disabled { opacity: 0.7; cursor: unset; } .rent__modal-book-list:hover { - -webkit-transform: scale(1.01); - -moz-transform: scale(1.01); - -ms-transform: scale(1.01); - -o-transform: scale(1.01); transform: scale(1.01); } @@ -96,9 +85,11 @@ .rent__modal-book-list { height: 13rem; } + .rent__modal-book-list__name { margin-bottom: 1.4rem; } + .rent__modal-book-list__title { display: inline-block; margin: 0; @@ -109,6 +100,7 @@ font-size: 1.5rem; font-weight: bold; } + .rent__modal-book-list__valid { float: right; margin-right: 1rem; diff --git a/src/asset/css/RentModalUserList.css b/src/asset/css/RentModalUserList.css index c0c3740a..e662a91c 100644 --- a/src/asset/css/RentModalUserList.css +++ b/src/asset/css/RentModalUserList.css @@ -1,6 +1,5 @@ .rent__user-list { border: 0; - margin: 0; display: flex; width: 100%; padding: 2rem 2rem 2rem 0; @@ -8,15 +7,7 @@ border-bottom: 0.1rem solid #a4a4a4; cursor: pointer; background: none; - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); transform: scale(1); - -webkit-transition: 0.3s; - -moz-transition: 0.3s; - -ms-transition: 0.3s; - -o-transition: 0.3s; transition: 0.3s; } @@ -38,10 +29,6 @@ } .rent__user-list:hover { - -webkit-transform: scale(1.01); - -moz-transform: scale(1.01); - -ms-transform: scale(1.01); - -o-transform: scale(1.01); transform: scale(1.01); } @@ -87,6 +74,7 @@ width: 40%; font-size: 1.6rem; } + .rent__user-list__penalty.font-16 { margin-left: 0; font-size: 1.4rem; diff --git a/src/asset/css/Search.css b/src/asset/css/Search.css index 40970998..8f8c7a38 100644 --- a/src/asset/css/Search.css +++ b/src/asset/css/Search.css @@ -11,6 +11,7 @@ padding: 0 15rem 0 15rem; max-width: 120rem; margin: auto; + overflow: hidden; } .search-title { @@ -29,6 +30,10 @@ padding: 10rem 0 0 0; } +.search-subtitle > * > .subtitle__title { + word-break: break-all; +} + .search-sort { display: flex; margin-bottom: 8rem; diff --git a/src/asset/css/SearchBar.css b/src/asset/css/SearchBar.css index 400c4d79..ef68ab2e 100644 --- a/src/asset/css/SearchBar.css +++ b/src/asset/css/SearchBar.css @@ -4,6 +4,7 @@ display: flex; border-radius: 6.1rem; background-color: #f2f2f2; + position: relative; } .search-bar__wrapper.short { @@ -56,21 +57,13 @@ .search-bar__button { all: unset; margin-left: auto; - background-image: url("../img/search_icon_black.svg"); - background-repeat: no-repeat; - background-size: cover; width: 2.8rem; height: 2.8rem; margin: auto; - color: transparent; -} - -.search-bar__button.submit { - background-image: url("../img/search_icon_black.svg"); + cursor: pointer; } .search-bar__button.barcode { - background-image: url("../img/barcode.svg"); margin-right: 2rem; } @@ -100,9 +93,11 @@ .search-bar__wrapper.center { padding: 0 2rem 0 2rem; } + .search-bar__wrapper.long { padding: 0 2.5rem 0 2.2rem; } + .search-bar__wrapper.short { margin-right: 1.8rem; padding: 0 2.5rem 0 2.2rem; diff --git a/src/asset/css/SearchBarDropDown.css b/src/asset/css/SearchBarDropDown.css new file mode 100644 index 00000000..0810264e --- /dev/null +++ b/src/asset/css/SearchBarDropDown.css @@ -0,0 +1,21 @@ +.search-bar__dropdown { + text-align: left; + background-color: rgba(255, 255, 255, 0.6) !important; + position: absolute; + top: 6.4rem; + white-space: wrap; + width: calc(100% - 5.4rem); + box-sizing: border-box; +} + +@media screen and (max-width: 1200px) { + .search-bar__dropdown { + top: 5.5rem; + } +} + +@media screen and (max-width: 767px) { + .search-bar__dropdown { + top: 5rem; + } +} diff --git a/src/asset/css/SearchRanking.css b/src/asset/css/SearchRanking.css new file mode 100644 index 00000000..75a66e6a --- /dev/null +++ b/src/asset/css/SearchRanking.css @@ -0,0 +1,98 @@ +.search-ranking__wrapper { + position: absolute; + top: 6rem; + left: 4.4rem; + width: 20rem; + z-index: 2; + appearance: none; + padding: 0; + border: 0; + box-sizing: border-box; + cursor: pointer; + border-radius: 0.5rem; +} + +.search-ranking__container { + background-color: white; + outline: solid 3px white; + border-radius: 0.5rem; + font-size: 1.4rem; + font-family: inherit; +} + +.search-ranking__container.carousel__container { + height: 100%; +} + +.search-ranking__keyword__wrapper { + display: flex !important; + justify-content: space-between; + align-items: center; +} + +.search-ranking__keyword__wrapper.title { + padding: 0 1rem; + font-weight: bold; +} + +.search-ranking__keyword__wrapper.title > img { + width: 1rem; + margin-right: 0.4rem; + color: black; + filter: opacity(0.5) drop-shadow(0 0 0 black); + transform: rotate(180deg); +} + +.search-ranking__keyword__rank { + font-weight: bold; + flex: 0 0 1rem; + margin: 0 1rem; +} + +.search-ranking__keyword__keyword { + flex: 1 0; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; +} + +.search-ranking__keyword__change { + flex: 0 0 2rem; + margin-right: 1rem; +} + +.search-ranking__keyword__change.new { + color: green; + font-size: 1rem; +} +.search-ranking__keyword__change.▲ { + color: red; +} +.search-ranking__keyword__change.▼ { + color: blue; +} +@media screen and (min-width: 1200px) { + .search-ranking__wrapper { + top: 6rem; + left: 5rem; + width: 24rem; + } +} +@media screen and (max-width: 768px) { + .search-ranking__wrapper { + position: absolute; + top: 9rem; + left: unset; + right: 3rem; + font-size: 1.2rem; + } + .search-ranking__keyword__wrapper { + font-size: 1.2rem; + } +} + +@media screen and (max-width: 360px) { + .search-ranking__wrapper { + left: 13rem; + } +} diff --git a/src/asset/css/SubTitle.css b/src/asset/css/SubTitle.css index cbb12491..7bb62947 100644 --- a/src/asset/css/SubTitle.css +++ b/src/asset/css/SubTitle.css @@ -13,7 +13,6 @@ align-items: flex-start; text-align: left; margin-left: 4rem; - height: 17.5rem; } .subtitle__line { diff --git a/src/asset/css/Tags.css b/src/asset/css/Tags.css index ee45d765..0561c8ce 100644 --- a/src/asset/css/Tags.css +++ b/src/asset/css/Tags.css @@ -1,9 +1,6 @@ /* default */ .none-drag { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; user-select: none; } diff --git a/src/asset/img/scroll-icon.svg b/src/asset/img/scroll-icon.svg deleted file mode 100644 index 251d3b75..00000000 --- a/src/asset/img/scroll-icon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/component/book/BookDetail.tsx b/src/component/book/BookDetail.tsx index c52b01c8..aa5f0502 100644 --- a/src/component/book/BookDetail.tsx +++ b/src/component/book/BookDetail.tsx @@ -1,14 +1,14 @@ import { useEffect, useRef } from "react"; import { useLocation, useParams } from "react-router-dom"; -import { useGetBooksInfoId } from "../../api/books/useGetBooksInfoId"; -import BookReservation from "./BookReservation"; -import BookStatus from "./BookStatus"; -import Review from "./review/Review"; -import Banner from "../utils/Banner"; -import Image from "../utils/Image"; -import Like from "./like/Like"; -import TagWrapper from "./tag/TagWrapper"; -import "../../asset/css/BookDetail.css"; +import { useGetBooksInfoId } from "~/api/books/useGetBooksInfoId"; +import BookReservation from "~/component/book/BookReservation"; +import BookStatus from "~/component/book/BookStatus"; +import Review from "~/component/book/review/Review"; +import Banner from "~/component/utils/Banner"; +import Image from "~/component/utils/Image"; +import Like from "~/component/book/like/Like"; +import TagWrapper from "~/component/book/tag/TagWrapper"; +import "~/asset/css/BookDetail.css"; const BookDetail = () => { const id = useParams().id || ""; @@ -58,7 +58,7 @@ const BookDetail = () => { {bookDetailInfo.title}
- +
diff --git a/src/component/book/like/Like.tsx b/src/component/book/like/Like.tsx index bc8c5715..ab658397 100644 --- a/src/component/book/like/Like.tsx +++ b/src/component/book/like/Like.tsx @@ -1,44 +1,45 @@ -import { useState } from "react"; -import { usePostLike } from "../../../api/like/usePostLike"; -import { useDeleteLike } from "../../../api/like/useDeleteLike"; -import { useGetLike } from "../../../api/like/useGetLike"; -import ShowLike from "./ShowLike"; -import "../../../asset/css/BookDetail.css"; +import { useGetLike, usePostLike, useDeleteLike } from "~/api/like"; +import { usePermission } from "~/hook/usePermission"; + +import Image from "~/component/utils/Image"; +import FilledLike from "~/asset/img/like_filled.svg"; +import EmptyLike from "~/asset/img/like_empty.svg"; type Props = { - initBookInfoId: string; + bookInfoId: string; }; -const Like = ({ initBookInfoId }: Props) => { - const [currentLike, setCurrentLike] = useState(false); - const [currentLikeNum, setCurrentLikeNum] = useState(0); +const Like = ({ bookInfoId }: Props) => { + const { is42Authenticated } = usePermission(); + + const { like, setLike } = useGetLike({ bookInfoId: +bookInfoId }); + const { setBookInfoId: requestdelete } = useDeleteLike({ setLike }); + const { setBookInfoId: requestPost } = usePostLike({ setLike }); - useGetLike({ - initBookInfoId: +initBookInfoId, - setCurrentLike, - setCurrentLikeNum, - }); - const { setBookInfoId: setDeleteLike } = useDeleteLike(); - const { setBookInfoId: setPostLike } = usePostLike(); const deleteLike = () => { - setCurrentLike(false); - setDeleteLike(+initBookInfoId); - setCurrentLikeNum(currentLikeNum - 1); + requestdelete(+bookInfoId); }; const postLike = () => { - setCurrentLike(true); - setPostLike(+initBookInfoId); - setCurrentLikeNum(currentLikeNum + 1); + requestPost(+bookInfoId); }; + return ( - <> - - +
+ {is42Authenticated ? ( + + ) : null} + {`좋아요 ${like.likeNum}`} +
); }; diff --git a/src/component/book/like/ShowLike.tsx b/src/component/book/like/ShowLike.tsx deleted file mode 100644 index c177fef7..00000000 --- a/src/component/book/like/ShowLike.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import Image from "../../utils/Image"; -import FilledLike from "../../../asset/img/like_filled.svg"; -import EmptyLike from "../../../asset/img/like_empty.svg"; -import "../../../asset/css/BookDetail.css"; - -type Props = { - deleteLike(...args: unknown[]): unknown; - postLike(...args: unknown[]): unknown; - currentLike: boolean; - currentLikeNum: number; -}; - -const ShowLike = ({ - deleteLike, - postLike, - currentLike, - currentLikeNum, -}: Props) => { - const permission = JSON.parse(window.localStorage.getItem("user") || "{}"); - const clickLikeHandler = () => { - if (currentLike) { - deleteLike(); - } else { - postLike(); - } - }; - - return ( - <> -
- {permission !== null ? ( - - ) : null} - {`좋아요 ${currentLikeNum}`} -
- - ); -}; - -export default ShowLike; diff --git a/src/component/bookManagement/BookManagementModalDetail.tsx b/src/component/bookManagement/BookManagementModalDetail.tsx index 502a5787..29414ee3 100644 --- a/src/component/bookManagement/BookManagementModalDetail.tsx +++ b/src/component/bookManagement/BookManagementModalDetail.tsx @@ -20,15 +20,15 @@ const BookManagementModalDetail = ({ book, closeModal }: Props) => { const [image, setImage] = useState(book.image); const [reset, setReset] = useState(false); - const titleRef = useRef(null); - const authorRef = useRef(null); - const publisherRef = useRef(null); - const publishedAtRef = useRef(null); - const isbnRef = useRef(null); - const imageRef = useRef(null); - const callSignRef = useRef(null); - const statusRef = useRef(null); - const categoryRef = useRef(null); + const titleRef = useRef(null); + const authorRef = useRef(null); + const publisherRef = useRef(null); + const publishedAtRef = useRef(null); + const isbnRef = useRef(null); + const imageRef = useRef(null); + const callSignRef = useRef(null); + const statusRef = useRef(null); + const categoryRef = useRef(null); const { setChange } = usePatchBooksUpdate({ bookTitle: book.title, @@ -57,7 +57,8 @@ const BookManagementModalDetail = ({ book, closeModal }: Props) => { modifyFromRef("callSign", callSignRef); modifyFromRef("categoryId", categoryRef); modifyFromRef("status", statusRef); - change.categoryId += 1; // select option의 index는 0부터 시작하므로 +1 + change.categoryId = Number(change.categoryId); + change.category = category[change.categoryId].name; return change; }; @@ -71,7 +72,8 @@ const BookManagementModalDetail = ({ book, closeModal }: Props) => { const isValidDate = change.publishedAt && dateRegex.test(change.publishedAt); const isValidCallSign = callSignRegex.test(change.callSign); - const isValidCategory = category.find(x => x.code === categoryToChange) !== undefined; + const isValidCategory = + category.find(x => x.code === categoryToChange) !== undefined; const isValidISBN = change.isbn && isbnRegex.test(change.isbn); let errorMessage = ""; @@ -166,7 +168,7 @@ const BookManagementModalDetail = ({ book, closeModal }: Props) => { ref={categoryRef} resetDependency={reset} optionList={category.map(i => i.name)} - initialSelectedIndex={book.categoryId - 1} + initialSelectedIndex={Number(book.categoryId) - 1} />
diff --git a/src/component/main/Main.tsx b/src/component/main/Main.tsx index 0edfbc53..ab6bb63d 100644 --- a/src/component/main/Main.tsx +++ b/src/component/main/Main.tsx @@ -1,13 +1,15 @@ -import MainBanner from "./MainBanner"; -import MainNew from "./MainNew"; -import MainPopular from "./MainPopular"; -import "../../asset/css/Main.css"; +import MainBanner from "~/component/main/MainBanner"; +import MainNew from "~/component/main/MainNew"; +import MainRecommend from "~/component/main/MainRecommend"; +import MainPopular from "~/component/main/MainPopular"; +import "~/asset/css/Main.css"; const Main = () => { return (
+
); diff --git a/src/component/main/MainBanner.tsx b/src/component/main/MainBanner.tsx index d58481bc..66645fe9 100644 --- a/src/component/main/MainBanner.tsx +++ b/src/component/main/MainBanner.tsx @@ -1,6 +1,4 @@ -import Image from "../utils/Image"; -import SearchBar from "../utils/SearchBar"; -import ScrollIcon from "../../asset/img/scroll-icon.svg"; +import BookSearchBar from "../utils/BookSearchBar"; import "../../asset/css/Banner.css"; import "../../asset/css/MainBanner.css"; @@ -20,15 +18,11 @@ const MainBanner = () => { 검색창에 원하는 도서를 입력해주세요. - +

스크롤을 내려주세요

- scroll-icon +
diff --git a/src/component/main/MainNewBook.tsx b/src/component/main/MainNewBook.tsx index 579e0790..49b0a970 100644 --- a/src/component/main/MainNewBook.tsx +++ b/src/component/main/MainNewBook.tsx @@ -1,30 +1,27 @@ import { Link } from "react-router-dom"; -import Image from "../utils/Image"; +import type { BookInfo } from "~/type"; +import Image from "~/component/utils/Image"; type Props = { - book: { - id?: number; - image?: string; - title?: string; - }; + book: BookInfo; bookWidth: number; }; const MainNewBook = ({ book, bookWidth }: Props) => { return ( -
- - new - -
+ new + ); }; diff --git a/src/component/main/MainNewBookList.tsx b/src/component/main/MainNewBookList.tsx index 302a742d..391caddf 100644 --- a/src/component/main/MainNewBookList.tsx +++ b/src/component/main/MainNewBookList.tsx @@ -1,124 +1,67 @@ -import { useState, useEffect, useRef } from "react"; -import MainNewBook from "./MainNewBook"; -import MainNewBookPagination from "./MainNewBookPagination"; -import Image from "../utils/Image"; -import ArrLeft from "../../asset/img/arrow_left.svg"; -import ArrRight from "../../asset/img/arrow_right.svg"; - -const mobileWidth = 100; -const pcWidth = 200; +import type { BookInfo } from "~/type"; +import { useResponsiveWidth } from "~/hook/useResponsiveWidth"; +import Carousel from "~/component/utils/Carousel"; +import Image from "~/component/utils/Image"; +import MainNewBook from "~/component/main/MainNewBook"; +import MainNewBookPagination from "~/component/main/MainNewBookPagination"; +import ArrLeft from "~/asset/img/arrow_left.svg"; +import ArrRight from "~/asset/img/arrow_right.svg"; type Props = { - docs: object[]; + docs: BookInfo[]; }; const MainNewBookList = ({ docs }: Props) => { - const [page, setPage] = useState(1); - const [bookWidth, setBookWidth] = useState(pcWidth); - const [transition, setTransition] = useState(true); - const [displayCount, setDisplayCount] = useState(0); - const intervalId = useRef(); - - useEffect(() => { - function handleSize() { - const width = window.innerWidth < 767 ? mobileWidth : pcWidth; - if (width !== bookWidth) setBookWidth(width); - const count = Math.ceil(window.innerWidth / (width * 1.1)); - if (count !== displayCount) setDisplayCount(count); - } - window.addEventListener("resize", handleSize); - handleSize(); - return () => window.removeEventListener("resize", handleSize); - }, [bookWidth, displayCount]); + const { width: bookWidth } = useResponsiveWidth({ + pcWidth: 200, + mobileWidth: 100, + }); - const books = [...docs.slice(-1), ...docs, ...docs.slice(0, displayCount)]; - const onNext = () => { - const index = page; - if (index === books.length - displayCount - 1) { - setTransition(false); - setPage(0); - setTimeout(() => { - setTransition(true); - setPage(1); - }, 3); - } else setPage(index + 1); + const margin = bookWidth * 0.1; + const moveButton = { + width: bookWidth / 4, + height: bookWidth * 1.5 + 20, }; - const onPrev = () => { - let index = page; - if (index === 1) { - index = books.length - displayCount; - setTransition(false); - setPage(index); - setTimeout(() => { - setTransition(true); - setPage(index - 1); - }, 3); - } else setPage(index - 1); - }; - - const pauseInterval = () => { - clearInterval(intervalId.current); - }; - const startInterval = () => { - clearInterval(intervalId.current); - intervalId.current = setInterval(onNext, 2000); - }; - - useEffect(() => { - clearInterval(intervalId.current); - intervalId.current = setInterval(onNext, 2000); - return () => clearInterval(intervalId.current); - }, [page]); - return ( -
- - -
-
- {books.map(book => ( - - ))} -
-
- -
+ + ( + + )} + /> + ); }; diff --git a/src/component/main/MainNewBookPagination.tsx b/src/component/main/MainNewBookPagination.tsx index ff4920da..4a9dd1fd 100644 --- a/src/component/main/MainNewBookPagination.tsx +++ b/src/component/main/MainNewBookPagination.tsx @@ -1,36 +1,28 @@ -import { MouseEventHandler } from "react"; +import PaginationCircle from "~/component/utils/PaginationCircle"; type Props = { page: number; setPage: (page: number) => void; }; +/* + * 신작도서를 5권씩 4챕터로 나누어서 표시 + * page: 1 ~ 20 + * chapter: 1 ~ 4 + */ +const pageSize = 5; +const chapterSize = 4; const MainNewBookPagination = ({ page, setPage }: Props) => { - const onChapter: MouseEventHandler = e => { - setPage(+e.currentTarget.value * 5); - }; - const chapter = [0, 1, 2, 3]; - function isSelected(n: number) { - if (Math.floor(page / 5) === n) return true; - if (Math.floor(page / 5) === 4 && !n) return true; - return false; - } + const currentChapter = Math.ceil(page / pageSize); + const setCurrentChapter = (chapter: number) => setPage(chapter * pageSize); + return ( -
- {chapter.map(i => ( - - ))} -
+ ); }; diff --git a/src/component/main/MainPopularCenter.tsx b/src/component/main/MainPopularCenter.tsx index 03669825..f9094392 100644 --- a/src/component/main/MainPopularCenter.tsx +++ b/src/component/main/MainPopularCenter.tsx @@ -1,10 +1,10 @@ import { MouseEventHandler, TouchEventHandler, useState } from "react"; import Image from "../utils/Image"; -import { Book } from "../../type"; +import { BookInfo } from "../../type"; import { useNavigate } from "react-router-dom"; type Props = { - docs: Book[]; + docs: (BookInfo & { rank: number })[]; centerTop: number; onLeft: () => void; onRight: () => void; @@ -100,7 +100,7 @@ const MainPopularCenter = ({ docs, centerTop, onLeft, onRight }: Props) => { />
- {index + 1} + {book.rank}

#{book.category}

{book.title}

diff --git a/src/component/main/MainPopularSide.tsx b/src/component/main/MainPopularSide.tsx index 2114c545..fbc90bf3 100644 --- a/src/component/main/MainPopularSide.tsx +++ b/src/component/main/MainPopularSide.tsx @@ -1,9 +1,9 @@ import { MouseEventHandler } from "react"; -import { Book } from "../../type"; +import { BookInfo } from "../../type"; import Image from "../utils/Image"; type Props = { - books: Book[]; + books: BookInfo[]; onClick: MouseEventHandler; side: "left" | "right"; }; diff --git a/src/component/main/MainRecommend.tsx b/src/component/main/MainRecommend.tsx new file mode 100644 index 00000000..dfba013a --- /dev/null +++ b/src/component/main/MainRecommend.tsx @@ -0,0 +1,29 @@ +import MainRecommendTitle from "~/component/main/MainRecommendTitle"; +import MainRecommendList from "~/component/main/MainRecommendList"; +import "~/asset/css/MainRecommend.css"; +import { useGetCursusRecommendBooks } from "~/api/cursus/useGetCursusRecommendBooks"; +import Loader from "../utils/Loader"; + +const MainRecommend = () => { + const { books, options, setSelectedOption, isLoading } = + useGetCursusRecommendBooks(); + return ( +
+ + {isLoading ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export default MainRecommend; diff --git a/src/component/main/MainRecommendList.tsx b/src/component/main/MainRecommendList.tsx new file mode 100644 index 00000000..7378327c --- /dev/null +++ b/src/component/main/MainRecommendList.tsx @@ -0,0 +1,74 @@ +import { Link } from "react-router-dom"; +import type { BookInfoRecommend } from "~/type"; +import { dateFormat } from "~/util/date"; +import Carousel from "~/component/utils/Carousel"; +import Image from "~/component/utils/Image"; +import Arr from "~/asset/img/arrow_right_gray.svg"; +import PaginationCircle from "../utils/PaginationCircle"; + +type Props = { + isLoading: boolean; + books: BookInfoRecommend[]; +}; + +const MainRecommendList = ({ books, isLoading }: Props) => { + return ( + 1} + initailSmoothAnimated={false} + > + + ( + + {item.title} +
+

{item.title}

+

저자 | {item.author}

+

출판사 | {item.publisher}

+

발행일 | {dateFormat(item.publishedAt ?? "")}

+

관련과제 | {item.project.join(", ")}

+
+ + )} + /> +
+ { + if (!lastPage) return <>; + return ( + + ); + }} + /> + {!isLoading && ( + <> + + + + + + + + )} +
+ ); +}; + +export default MainRecommendList; diff --git a/src/component/main/MainRecommendTitle.tsx b/src/component/main/MainRecommendTitle.tsx new file mode 100644 index 00000000..00681da7 --- /dev/null +++ b/src/component/main/MainRecommendTitle.tsx @@ -0,0 +1,43 @@ +import { ChangeEventHandler, Dispatch, SetStateAction } from "react"; + +type Props = { + isLoading: boolean; + description: string; + options: string[]; + setSelectedOption: (option: string) => void; +}; +const MainRecommendTitle = ({ + isLoading, + description, + options, + setSelectedOption, +}: Props) => { + const changeOption: ChangeEventHandler = e => { + setSelectedOption(e.target.selectedOptions[0].value); + }; + + return ( +
+
+

+ 추천도서 + +

+ + {description} + +
+ ); +}; + +export default MainRecommendTitle; diff --git a/src/component/mypage/MyRentInfo/MyRent.tsx b/src/component/mypage/MyRentInfo/MyRent.tsx index 4ab0b47d..09c43651 100644 --- a/src/component/mypage/MyRentInfo/MyRent.tsx +++ b/src/component/mypage/MyRentInfo/MyRent.tsx @@ -1,8 +1,8 @@ -import { useGetUsersSearchId } from "../../../api/users/useGetUsersSearchId"; +import { useGetUsersSearchId } from "~/api/users/useGetUsersSearchId"; import RentHistory from "./RentHistory"; import RentedOrReservedBooks from "./RentedOrReservedBooks"; -import InquireBoxTitle from "../../utils/InquireBoxTitle"; -import Book from "../../../asset/img/admin_icon.svg"; +import InquireBoxTitle from "~/component/utils/InquireBoxTitle"; +import Book from "~/asset/img/admin_icon.svg"; const MyRent = () => { const user = window.localStorage.getItem("user"); diff --git a/src/component/mypage/MyRentInfo/RentHistory.tsx b/src/component/mypage/MyRentInfo/RentHistory.tsx index 151b5275..c6baaa95 100644 --- a/src/component/mypage/MyRentInfo/RentHistory.tsx +++ b/src/component/mypage/MyRentInfo/RentHistory.tsx @@ -1,7 +1,7 @@ -import Pagination from "../../utils/Pagination"; -import { useGetHistories } from "../../../api/histories/useGetHistories"; +import Pagination from "~/component/utils/Pagination"; +import { useGetHistories } from "~/api/histories/useGetHistories"; import RentHistoryTable from "./RentHistoryTable"; -import "../../../asset/css/RentHistory.css"; +import "~/asset/css/RentHistory.css"; const RentHistory = () => { const { historiesList, lastPage, page, setPage } = useGetHistories({ diff --git a/src/component/mypage/MyRentInfo/RentHistoryTable.tsx b/src/component/mypage/MyRentInfo/RentHistoryTable.tsx index a6d0b86f..68099cc7 100644 --- a/src/component/mypage/MyRentInfo/RentHistoryTable.tsx +++ b/src/component/mypage/MyRentInfo/RentHistoryTable.tsx @@ -1,40 +1,26 @@ -import { useState } from "react"; -import { useGetLike } from "../../../api/like/useGetLike"; -import { usePostLike } from "../../../api/like/usePostLike"; -import { useDeleteLike } from "../../../api/like/useDeleteLike"; -import Image from "../../utils/Image"; -import FilledLike from "../../../asset/img/like_filled.svg"; -import EmptyLike from "../../../asset/img/like_empty.svg"; -import { History } from "../../../type"; -import "../../../asset/css/RentHistory.css"; +import { useGetLike, usePostLike, useDeleteLike } from "~/api/like"; +import Image from "~/component/utils/Image"; +import FilledLike from "~/asset/img/like_filled.svg"; +import EmptyLike from "~/asset/img/like_empty.svg"; +import type { History } from "~/type"; +import "~/asset/css/RentHistory.css"; type Props = { factor: History; }; const RentHistoryTable = ({ factor }: Props) => { - const [currentLike, setCurrentLike] = useState(false); - useGetLike({ - initBookInfoId: factor.bookInfoId, - setCurrentLike, + const { like, setLike } = useGetLike({ + bookInfoId: factor.bookInfoId, }); - const { setBookInfoId: setBookInfoIdPost } = usePostLike(); - const postLike = (bookInfoId: number) => { - setBookInfoIdPost(bookInfoId); - }; - const { setBookInfoId: setBookInfoIdDelete } = useDeleteLike(); - const deleteLike = (bookInfoId: number) => { - setBookInfoIdDelete(bookInfoId); - }; + const { setBookInfoId: requestPost } = usePostLike({ setLike }); + const { setBookInfoId: requestDelete } = useDeleteLike({ setLike }); - const clickLikeHandler = (bookInfoId: number) => { - if (currentLike) { - deleteLike(bookInfoId); - setCurrentLike(false); - } else { - postLike(bookInfoId); - setCurrentLike(true); - } + const postLike = () => { + requestPost(factor.bookInfoId); + }; + const deleteLike = () => { + requestDelete(factor.bookInfoId); }; return ( @@ -49,11 +35,9 @@ const RentHistoryTable = ({ factor }: Props) => {
- + ); diff --git a/src/component/superTag/SuperTagMergeDefaultTag.tsx b/src/component/superTag/SuperTagMergeDefaultTag.tsx index 14f5bd37..8904fb02 100644 --- a/src/component/superTag/SuperTagMergeDefaultTag.tsx +++ b/src/component/superTag/SuperTagMergeDefaultTag.tsx @@ -2,7 +2,7 @@ import { DragEventHandler, useState } from "react"; import { usePatchTagsBookInfoIdMerge } from "../../api/tags/usePatchTagsBookInfoIdMerge"; import { Tag } from "../../type"; import Accordion from "../utils/Accordion"; -import SearchBar from "../utils/SearchBar"; +import ManagementSearchBar from "../utils/ManagementSearchBar"; import Droppable from "../utils/Droppable"; import SuperTagMergeSubTag from "./SuperTagMergeSubTag"; @@ -49,7 +49,7 @@ const SuperTagMergeDefaultTag = ({ format="text/plain" onDrop={addNewListAndMergeIfMoved} > - { setModal(EDIT); }; - const concatDate = (day: Date) => { - let overDueDate = ""; - - day.setDate(day.getDate() + user.overDueDay); - overDueDate += day.getFullYear(); - overDueDate += "-"; - overDueDate += day.getMonth() + 1 < 10 ? "0" : ""; - overDueDate += day.getMonth() + 1; - overDueDate += "-"; - overDueDate += day.getDate() < 10 ? "0" : ""; - overDueDate += day.getDate(); - return overDueDate; - }; - - const getOverDueDate = () => { - if ( - !user.penaltyEndDate || - new Date(user.penaltyEndDate).setHours(0, 0, 0, 0) < - new Date().setHours(0, 0, 0, 0) - ) { - return concatDate(nowDay); - } - return concatDate(new Date(user.penaltyEndDate)); - }; + const { isRestricted, restrictionDate } = lendingRestriction(user); return (
@@ -68,12 +46,7 @@ const UserBriefInfo = ({ user, line, setModal, setSelectedUser }: Props) => { )}
{user.email}
- {user.overDueDay || - (user.penaltyEndDate && - new Date(user.penaltyEndDate).setHours(0, 0, 0, 0) >= - new Date().setHours(0, 0, 0, 0)) - ? getOverDueDate() - : "-"} + {isRestricted ? restrictionDate : "-"}
{user.nickname ? ( + + ))} + {keywords.length === 0 && ( +
  • + 최근 검색된 기록이 없습니다 +
  • + )} + +
    + ); +}; + +export default BookSearchRecentKeyword; diff --git a/src/component/utils/Carousel.tsx b/src/component/utils/Carousel.tsx new file mode 100644 index 00000000..b398630e --- /dev/null +++ b/src/component/utils/Carousel.tsx @@ -0,0 +1,50 @@ +import { RefObject, createContext } from "react"; +import CarouselRoot from "~/component/utils/CarouselRoot"; +import CarouselList from "~/component/utils/CarouselList"; +import CarouselContainer from "~/component/utils/CarouselContainer"; +import CarouselPagination from "~/component/utils/CarouselPagination"; +import CarouselPrev from "./CarouselPrev"; +import CarouselNext from "./CarouselNext"; +import "~/asset/css/Carousel.css"; + +type CarouselContextType = { + index: number; + isSmoothAnimated: boolean; + onPrev: () => void; + onNext: () => void; + setIndex: (index: number) => void; + startAutoAnimation: () => void; + pauseAutoAnimation: () => void; + displayCount: number; + itemSize: number; + length: number; + direction: "row" | "column"; + targetRef: RefObject | null; +}; + +// 내부 컴포넌트에서 사용하는 context +// Root 컴포넌트로 감싼 후 다양한 컴포넌트를 조합해서 활용 가능 +export const CarouselContext = createContext({ + index: 0, + isSmoothAnimated: true, + onPrev: () => {}, + onNext: () => {}, + setIndex: (index: number) => {}, + startAutoAnimation: () => {}, + pauseAutoAnimation: () => {}, + displayCount: 0, + itemSize: 0, + length: 0, + direction: "row", + targetRef: null, +}); + +const Carousel = () => {}; + +export default Carousel; +Carousel.Root = CarouselRoot; +Carousel.Container = CarouselContainer; +Carousel.List = CarouselList; +Carousel.Prev = CarouselPrev; +Carousel.Next = CarouselNext; +Carousel.Pagination = CarouselPagination; diff --git a/src/component/utils/CarouselContainer.tsx b/src/component/utils/CarouselContainer.tsx new file mode 100644 index 00000000..308e86ca --- /dev/null +++ b/src/component/utils/CarouselContainer.tsx @@ -0,0 +1,30 @@ +import { ComponentProps, useContext } from "react"; +import { CarouselContext } from "~/component/utils/Carousel"; + +/** + * 슬라이드 리스트를 감싸는 컴포넌트 + * item의 크기를 결정하고, 슬라이드 애니메이션을 위한 컨테이너 역할을 한다. + * 반드시 Carousel.Root 컴포넌트로 감싸져야 한다. + */ + +const CarouselContainer = ({ + className, + children, + ...rest +}: ComponentProps<"div">) => { + const { targetRef, startAutoAnimation, pauseAutoAnimation } = + useContext(CarouselContext); + return ( +
    + {children} +
    + ); +}; + +export default CarouselContainer; diff --git a/src/component/utils/CarouselList.tsx b/src/component/utils/CarouselList.tsx new file mode 100644 index 00000000..a8843444 --- /dev/null +++ b/src/component/utils/CarouselList.tsx @@ -0,0 +1,88 @@ +import { CSSProperties, ComponentProps, useContext, useMemo } from "react"; +import { CarouselContext } from "~/component/utils/Carousel"; + +/** + * 슬라이드 될 item을 연속으로 늘어놓은 컴포넌트 + * 슬라이드 효과를 위해 조금씩 이동한다. + * 반드시 Carousel.Root 와 Carousel.Container 컴포넌트로 감싸져야 한다. + * Root : 필요한 상태 및 제어 설정을 공유받기 위함 + * Container : 슬라이드 될 item의 크기를 결정하기 위함 + * + * @param items - 슬라이드 될 item들의 배열 + * @param renderItem - 슬라이드 될 item의 렌더링 UI, 파라미터로 item을 받아서 렌더링한다. + * @param showPreviousItem - 이전 item을 보여줄지 여부 (half | none) + * @example ~/component/main/MainNewBookList.tsx + */ + +type Props = ComponentProps<"ul"> & { + items: T[]; + renderItem: (props: { + item: T & { key: string }; + key: string; + style: CSSProperties; + }) => JSX.Element; + showPreviousItem?: "half" | "none"; +}; + +const CarouselList = ({ + items, + renderItem, + showPreviousItem = "none", + className, + ...rest +}: Props) => { + const { + index, + startAutoAnimation, + pauseAutoAnimation, + displayCount, + itemSize, + direction, + isSmoothAnimated, + } = useContext(CarouselContext); + + // 슬라이드 될 item들을 복사해서 앞뒤로 붙여준다. + // 애니메이션 효과를 위한 앞 마지막 1개와 컨테이너 크기에 맞춰서 복사한 item들을 붙여준다. + const displayItems = useMemo(() => { + if (items.length === 0) return []; + const lastItem = items.slice(-1)[0]; + const copySize = displayCount / items.length + 1; + return [ + { ...lastItem, key: "last" + lastItem.id }, + ...items.map(i => ({ ...i, key: `${i.id}` })), + ...Array.from({ length: copySize }).flatMap((_, i) => + items.map((_, index) => ({ + ...items[index % items.length], + key: "copy" + i + index, // key 중복을 피하기 위해 임의로 가공 + })), + ), + ]; + }, [items, displayCount]); + + const prevItemSize = showPreviousItem === "half" ? itemSize / 2 : 0; + const translate = direction === "row" ? "translateX" : "translateY"; + + return ( +
      + {displayItems.map(item => + renderItem({ + item, + key: item.key, + style: { flexBasis: itemSize, width: itemSize, overflow: "hidden" }, + }), + )} +
    + ); +}; + +export default CarouselList; diff --git a/src/component/utils/CarouselNext.tsx b/src/component/utils/CarouselNext.tsx new file mode 100644 index 00000000..23ef0861 --- /dev/null +++ b/src/component/utils/CarouselNext.tsx @@ -0,0 +1,19 @@ +import { ComponentProps, useContext } from "react"; +import { CarouselContext } from "~/component/utils/Carousel"; + +const CarouselNext = ({ className, ...rest }: ComponentProps<"button">) => { + const { onNext, startAutoAnimation, pauseAutoAnimation } = + useContext(CarouselContext); + + return ( + - {toggleLNB && ( - /* 토글 시작 */ -
    -
    -
      - {user.isAdmin && - adminLnbMenu.map(menu => { - return ( -
    • - - {menu.text} - -
      -
    • - ); - })} - {loginLnbMenu.map(menu => { - return ( -
    • - - {menu.text} - -
      -
    • - ); - })} -
    -
    - /* 토글 끝 */ - )} -
    - - - - - - ); + return isMobile ? : ; }; export default Header; diff --git a/src/component/utils/HeaderDefault.tsx b/src/component/utils/HeaderDefault.tsx new file mode 100644 index 00000000..b26950d0 --- /dev/null +++ b/src/component/utils/HeaderDefault.tsx @@ -0,0 +1,45 @@ +import { useRecoilValue } from "recoil"; +import { Link } from "react-router-dom"; +import { basicGnbMenu } from "~/constant/headerMenu"; +import userState from "~/atom/userState"; +import Image from "./Image"; +import HeaderDefaultLNB from "./HeaderDefaultLNB"; +import Logo from "~/asset/img/jiphyeonjeon_logo.svg"; +import "~/asset/css/HeaderDefault.css"; +import SearchRanking from "./SearchRanking"; + +const HeaderDefault = () => { + const user = useRecoilValue(userState); + + const gnbMenu = user.isLogin + ? basicGnbMenu.slice(0, basicGnbMenu.length - 1) // basicGnbMenu의 마지막 요소는 "로그인", 이미 로그인 상태면 제외 + : basicGnbMenu; + + return ( +
    + + 집현전 로고 + + +
    + ); +}; + +export default HeaderDefault; diff --git a/src/component/utils/HeaderDefaultLNB.tsx b/src/component/utils/HeaderDefaultLNB.tsx new file mode 100644 index 00000000..bf90ee6b --- /dev/null +++ b/src/component/utils/HeaderDefaultLNB.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from "react"; +import { useRecoilValue } from "recoil"; +import { Link, useLocation } from "react-router-dom"; +import { adminLnbMenu, loginLnbMenu } from "~/constant/headerMenu"; +import userState from "~/atom/userState"; +import Image from "./Image"; +import User from "~/asset/img/Uniconlabs.png"; +import ToggleUser from "~/asset/img/UniconlabsFill.png"; +import ToggleDownArrow from "~/asset/img/caret-down_DaveGandy.png"; +import DownArrow from "~/asset/img/drop-down_Freepik.png"; +import "~/asset/css/HeaderDefaultLNB.css"; + +const HeaderDefaultLNB = () => { + const [isLNBOpened, setIsLNBOpened] = useState(false); + const user = useRecoilValue(userState); + const location = useLocation(); + + useEffect(() => { + setIsLNBOpened(false); + }, [location.pathname]); + + const lnbMenu = user.isAdmin + ? [...adminLnbMenu, ...loginLnbMenu] + : loginLnbMenu; + + return ( +
    + + {isLNBOpened && ( +
    + {lnbMenu.map(menu => ( + + {menu.text} + + ))} +
    + )} +
    + ); +}; + +export default HeaderDefaultLNB; diff --git a/src/component/utils/HeaderMobile.tsx b/src/component/utils/HeaderMobile.tsx new file mode 100644 index 00000000..82d12aef --- /dev/null +++ b/src/component/utils/HeaderMobile.tsx @@ -0,0 +1,58 @@ +import { useState, useEffect } from "react"; +import { Link, useLocation } from "react-router-dom"; +import Image from "./Image"; +import Logo from "~/asset/img/jiphyeonjeon_logo.svg"; +import Hamburger from "~/asset/img/Hamburger_OwlDsgnr.png"; +import SearchBook from "~/asset/img/Search_VectorsMarket.png"; +import HeaderModal from "./HeaderModal"; +import "~/asset/css/HeaderMobile.css"; +import SearchRanking from "./SearchRanking"; + +const HeaderMobile = () => { + const [isDrawerOpened, setIsDrawerOpened] = useState(false); + const [isFixed, setFixed] = useState(false); + const location = useLocation(); + + useEffect(() => { + const handleScroll = () => setFixed(window.scrollY > 0); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + useEffect(() => { + setIsDrawerOpened(false); + }, [location.pathname]); + + useEffect(() => { + if (isDrawerOpened) document.body.style.overflow = "hidden"; + else document.body.style.overflow = "unset"; + }, [isDrawerOpened]); + + return ( + <> +
    + + logo + + +
    + {isDrawerOpened && ( + setIsDrawerOpened(false)} /> + )} + + + ); +}; + +export default HeaderMobile; diff --git a/src/component/utils/Image.tsx b/src/component/utils/Image.tsx index bc4a3566..8f7cb206 100644 --- a/src/component/utils/Image.tsx +++ b/src/component/utils/Image.tsx @@ -5,14 +5,13 @@ type Size = number; type Props = HTMLProps & { width?: Size; height?: Size }; const Image = ({ src, alt, width, height, ...props }: Props) => { - const [error, setError] = useState(false); return ( {alt} setError(true)} + onError={e => (e.currentTarget.src = fallback)} loading="lazy" /> ); diff --git a/src/component/utils/InquireBoxTitle.tsx b/src/component/utils/InquireBoxTitle.tsx index b00afff0..e273e309 100644 --- a/src/component/utils/InquireBoxTitle.tsx +++ b/src/component/utils/InquireBoxTitle.tsx @@ -1,5 +1,5 @@ import Image from "./Image"; -import SearchBar from "./SearchBar"; +import ManagementSearchBar from "./ManagementSearchBar"; import "../../asset/css/InquireBoxTitle.css"; type Props = { @@ -51,7 +51,7 @@ const InquireBoxTitle = ({ {placeHolder ? ( - & { + hasBackdrop?: boolean; +}; + +const Loader = ({ className, hasBackdrop, ...rest }: Props) => { + const BackDrop = ({ children }: PropsWithChildren) => + hasBackdrop ? ( +
    {children}
    + ) : ( + <>{children} + ); + + return ( + +
    + + ); +}; + +export default Loader; diff --git a/src/component/utils/Management.tsx b/src/component/utils/Management.tsx index 9748f452..4b8b6814 100644 --- a/src/component/utils/Management.tsx +++ b/src/component/utils/Management.tsx @@ -1,4 +1,4 @@ -import SearchBar from "./SearchBar"; +import ManagementSearchBar from "./ManagementSearchBar"; import Pagination from "./Pagination"; import "../../asset/css/Management.css"; @@ -23,7 +23,7 @@ const Management = ({ }: Props) => { return (
    - void; + placeHolder?: string; + wrapperClassName?: string; + isWithBarcodeButton?: boolean; + isFocusedOnMount?: boolean; + onClickBarcodeButton?: MouseEventHandler; +}; + +const ManagementSearchBar = ({ + width, + setQuery = () => {}, + placeHolder, + wrapperClassName = "", + isWithBarcodeButton = false, + isFocusedOnMount = true, + onClickBarcodeButton = () => {}, + ...rest +}: Props) => { + const [keyword, setKeyword] = useState(""); + const searchRef = useRef(null); + + useEffect(() => { + if (isFocusedOnMount && searchRef.current) { + searchRef.current.focus(); + } + }, []); + + const changeKeyword: ChangeEventHandler = e => { + const changed = e.currentTarget.value; + setKeyword(changed); + setQuery(changed); + }; + + return ( + + + {isWithBarcodeButton ? ( + + ) : null} + + + ); +}; + +export default ManagementSearchBar; diff --git a/src/component/utils/MobileHeader.jsx b/src/component/utils/MobileHeader.jsx deleted file mode 100644 index ac16c94e..00000000 --- a/src/component/utils/MobileHeader.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useState, useEffect } from "react"; -import { Link, useLocation } from "react-router-dom"; -import Image from "./Image"; -import HeaderModal from "./HeaderModal"; -import Logo from "../../asset/img/jiphyeonjeon_logo.svg"; -import Hamburger from "../../asset/img/Hamburger_OwlDsgnr.png"; -import SearchBook from "../../asset/img/Search_VectorsMarket.png"; -import "../../asset/css/MobileHeader.css"; - -const MobileHeader = () => { - const [headerModal, setHeaderModal] = useState(false); - const [isFixed, setFixed] = useState(false); - const location = useLocation(); - - const openHeaderModal = () => { - setHeaderModal(true); - }; - - const stickyHeader = () => { - if (window.pageYOffset > 0) { - setFixed(true); - } else { - setFixed(false); - } - }; - - window.onscroll = stickyHeader; - - const closeHeader = () => { - setHeaderModal(false); - }; - - useEffect(closeHeader, [location.pathname]); - - return ( -
    -
    -
    - - logo - -
    - -
    - {headerModal ? : ``} -
    - ); -}; - -export default MobileHeader; diff --git a/src/component/utils/Pagination.tsx b/src/component/utils/Pagination.tsx index e7edb12d..5e79a277 100644 --- a/src/component/utils/Pagination.tsx +++ b/src/component/utils/Pagination.tsx @@ -1,9 +1,6 @@ -import { MouseEventHandler, RefObject } from "react"; +import { RefObject } from "react"; import { useSearchParams } from "react-router-dom"; -import Image from "./Image"; -import ArrRight from "../../asset/img/arrow_right_black.svg"; -import ArrRightDouble from "../../asset/img/arrow_right_black_double.svg"; -import "../../asset/css/Pagination.css"; +import Paginations from "~/component/utils/Paginations"; type Props = { className?: string; @@ -24,11 +21,6 @@ const Pagination = ({ scrollRef, count = 5, }: Props) => { - const startNum = Math.floor((page - 1) / count) * count + 1; - const pageRange = []; - for (let i = 0; i < count; i += 1) { - if (startNum + i <= lastPage) pageRange.push(startNum + i); - } const isPrevAvailable = page > 1; const isNextAvailable = page < lastPage; const [searchParams, setSearchParams] = useSearchParams(); @@ -41,97 +33,24 @@ const Pagination = ({ if (scrollRef?.current) scrollRef.current.scrollIntoView(); // 페이지 전환시 돔이 참조하고 있는 곳으로 현재 스크롤 이동 } }; - const onClickPage: MouseEventHandler = e => { - const { value } = e.currentTarget; - changePage(parseInt(value, 10)); - }; - - const onClickPageRange: MouseEventHandler = e => { - const type = e.currentTarget.value; - if (type === "previous" && page > 1) changePage(page - 1); - else if (type.includes("prev")) changePage(1); - else if (type === "next" && page < lastPage) changePage(page + 1); - else if (type.includes("next")) changePage(lastPage); - }; return ( -
    - {/* 왼쪽으로 넘기기 버튼 */} -
    - {isPrevAvailable && ( - <> - - - - )} -
    - {/* 페이지 번호 */} -
    - {pageRange.map(pageNum => ( - - ))} -
    - - {/* 오른쪽으로 넘기기 버튼 */} -
    - {isNextAvailable && ( - <> - - - - )} -
    -
    + + + + + + + + + + + ); }; diff --git a/src/component/utils/PaginationCircle.tsx b/src/component/utils/PaginationCircle.tsx new file mode 100644 index 00000000..daa0fdc0 --- /dev/null +++ b/src/component/utils/PaginationCircle.tsx @@ -0,0 +1,35 @@ +import { ComponentProps } from "react"; +import "~/asset/css/PaginationCircle.css"; + +type Props = ComponentProps<"div"> & { + page: number; + setPage: (page: number) => void; + lastPage: number; +}; + +const PaginationCircle = ({ + page, + setPage, + lastPage, + className, + ...rest +}: Props) => { + const pages = Array.from({ length: lastPage }, (_, i) => i + 1); + + return ( +
    + {pages.map(p => ( + + ))} +
    + ); +}; + +export default PaginationCircle; diff --git a/src/component/utils/Paginations.tsx b/src/component/utils/Paginations.tsx new file mode 100644 index 00000000..db95f8f4 --- /dev/null +++ b/src/component/utils/Paginations.tsx @@ -0,0 +1,151 @@ +import { + ComponentProps, + ReactNode, + createContext, + useContext, + useState, +} from "react"; +import Move from "~/component/utils/PaginationsMove"; +import "~/asset/css/Pagination.css"; + +// 내부 컴포넌트에서 사용하는 context +const PaginationContext = createContext({ + page: 1, + setPage: (_: number) => {}, + lastPage: 10, +}); + +type Props = { + children: ReactNode; + className?: string; + page?: number; + setPage?: (_: number) => void; + lastPage: number; +}; + +const Root = ({ + page = 1, + setPage, + lastPage, + className = "", + children, +}: Props) => { + const [currentPage, setCurrentPage] = useState(page); + + const changePage = (page: number) => { + if (page < 1 || page > lastPage) return; + setCurrentPage(page); + setPage && setPage(page); + }; + + return ( + +
    {children}
    +
    + ); +}; + +const Pages = (props: { length: number }) => { + const { page, lastPage } = useContext(PaginationContext); + const startPage = Math.floor((page - 1) / props.length) * props.length + 1; + const range = Array.from( + { length: Math.min(startPage + props.length, lastPage + 1) - startPage }, + (_, i) => startPage + i, + ); + + return ( +
    + {range.map(i => ( + + ))} +
    + ); +}; + +const Page = ({ + number, + ...rest +}: ComponentProps<"button"> & { number: number }) => { + const { page, setPage } = useContext(PaginationContext); + return ( + + ); +}; + +const ConditionalMove = (props: { + isVisible: boolean; + children: ReactNode; +}) => ( +
    + {props.isVisible ? props.children : null} +
    +); + +const First = (props: ComponentProps<"button">) => { + const { page, setPage } = useContext(PaginationContext); + return ( + setPage(1)} + isDoubled + disabled={props.disabled || page === 1} + /> + ); +}; + +const Prev = (props: ComponentProps<"button">) => { + const { page, setPage } = useContext(PaginationContext); + return ( + setPage(page - 1)} + disabled={props.disabled || page === 1} + /> + ); +}; + +const Next = (props: ComponentProps<"button">) => { + const { page, setPage, lastPage } = useContext(PaginationContext); + return ( + setPage(page + 1)} + disabled={props.disabled || page === lastPage} + /> + ); +}; + +const Last = (props: ComponentProps<"button">) => { + const { page, setPage, lastPage } = useContext(PaginationContext); + return ( + setPage(lastPage)} + isDoubled + disabled={props.disabled || page === lastPage} + /> + ); +}; + +export default { Root, Pages, Page, ConditionalMove, First, Prev, Next, Last }; diff --git a/src/component/utils/PaginationsMove.tsx b/src/component/utils/PaginationsMove.tsx new file mode 100644 index 00000000..40684e10 --- /dev/null +++ b/src/component/utils/PaginationsMove.tsx @@ -0,0 +1,38 @@ +import { ComponentProps } from "react"; +import Image from "~/component/utils/Image"; +import ArrRight from "~/asset/img/arrow_right_black.svg"; +import ArrRightDouble from "~/asset/img/arrow_right_black_double.svg"; + +type Props = ComponentProps<"button"> & { + alt?: string; + isDoubled?: boolean; + direction?: "left" | "right"; +}; + +const PaginationsMove = ({ + alt, + isDoubled = false, + direction = "left", + className, + ...rest +}: Props) => { + return ( + + ); +}; + +export default PaginationsMove; diff --git a/src/component/utils/SearchBar.tsx b/src/component/utils/SearchBar.tsx index 8ce8ae71..b792fbec 100644 --- a/src/component/utils/SearchBar.tsx +++ b/src/component/utils/SearchBar.tsx @@ -1,89 +1,28 @@ -import { - useRef, - useState, - useEffect, - ChangeEventHandler, - FormEventHandler, - MouseEventHandler, -} from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import "../../asset/css/SearchBar.css"; +import { ComponentProps } from "react"; +import SearchBarInput from "~/component/utils/SearchBarInput"; +import SearchBarButton from "~/component/utils/SearchBarButton"; +import SearchBarDropDown from "~/component/utils/SearchBarDropDown"; +import "~/asset/css/SearchBar.css"; -type Props = { - setQuery?: (query: string) => void; - placeHolder?: string; - wrapperClassName?: string; - width: "banner" | "center" | "short" | "long"; - isWithBarcodeButton?: boolean; - onClickBarcodeButton?: MouseEventHandler; - isNavigate?: boolean; - isFocusedOnMount?: boolean; +export type Props = ComponentProps<"form"> & { + width?: "banner" | "center" | "short" | "long"; }; const SearchBar = ({ - setQuery, - placeHolder = "", - wrapperClassName = "", - width, - isWithBarcodeButton = false, - onClickBarcodeButton = () => {}, - isNavigate = false, - isFocusedOnMount = true, + width = "banner", + className = "", + children, + ...rest }: Props) => { - const [urlParams] = useSearchParams(); - const [searchWord, setSearchWord] = useState(urlParams.get("search") || ""); - const navigate = useNavigate(); - const searchRef = useRef(null); - - const onChange: ChangeEventHandler = event => { - const { value } = event.currentTarget; - if (typeof setQuery === "function") { - if (!isNavigate) setQuery(value); - } - setSearchWord(value); - }; - - const onSubmit: FormEventHandler = event => { - event.preventDefault(); - const encodedSearchWord = encodeURIComponent(searchWord); - if (isNavigate) navigate(`/search?search=${encodedSearchWord}`); - }; - - useEffect(() => { - if (isFocusedOnMount && searchRef.current) { - searchRef.current.focus(); - } - }, []); - return ( -
    - - {isWithBarcodeButton ? ( - - ) : null} - + + {children}
    ); }; export default SearchBar; + +SearchBar.Input = SearchBarInput; +SearchBar.Button = SearchBarButton; +SearchBar.DropDown = SearchBarDropDown; diff --git a/src/component/utils/SearchBarButton.tsx b/src/component/utils/SearchBarButton.tsx new file mode 100644 index 00000000..74f29c94 --- /dev/null +++ b/src/component/utils/SearchBarButton.tsx @@ -0,0 +1,12 @@ +import SearchIcon from "~/asset/img/search_icon_black.svg"; +import Image from "~/component/utils/Image"; + +const SearchBarButton = () => { + return ( + + ); +}; + +export default SearchBarButton; diff --git a/src/component/utils/SearchBarDropDown.tsx b/src/component/utils/SearchBarDropDown.tsx new file mode 100644 index 00000000..6c94fdab --- /dev/null +++ b/src/component/utils/SearchBarDropDown.tsx @@ -0,0 +1,56 @@ +import { + Dispatch, + ReactNode, + RefObject, + SetStateAction, + useEffect, + useRef, +} from "react"; +import "~/asset/css/SearchBarDropDown.css"; + +type Props = { + isOpened: boolean; + setIsOpened: Dispatch>; + searchBarRef: RefObject; + children: ReactNode; +}; +const SearchBarDropDown = ({ + isOpened, + setIsOpened, + searchBarRef, + children, +}: Props) => { + const dropDownRef = useRef(null); + + useEffect(() => { + const updateDropdownVisibility = (e: MouseEvent) => { + const clickedNode = e.target as Node; + const isInsideClicked = + searchBarRef.current?.contains(clickedNode) || + dropDownRef.current?.contains(clickedNode); + + setIsOpened(isInsideClicked ?? false); + }; + + const closeWithEsc = (e: KeyboardEvent) => { + if (e.key === "Escape") setIsOpened(false); + }; + + document.addEventListener("click", updateDropdownVisibility); + document.addEventListener("keydown", closeWithEsc); + return () => { + document.removeEventListener("click", updateDropdownVisibility); + document.removeEventListener("keydown", closeWithEsc); + }; + }, []); + + if (isOpened) + return ( +
    + {children} +
    + ); + return null; +}; + +export default SearchBarDropDown; diff --git a/src/component/utils/SearchBarInput.tsx b/src/component/utils/SearchBarInput.tsx new file mode 100644 index 00000000..2825e349 --- /dev/null +++ b/src/component/utils/SearchBarInput.tsx @@ -0,0 +1,20 @@ +import { ComponentProps, forwardRef } from "react"; + +export type Props = Omit, "onSubmit">; +const SearchBarInput = forwardRef( + ({ ...rest }, ref) => { + return ( + + ); + }, +); + +export default SearchBarInput; diff --git a/src/component/utils/SearchModal.tsx b/src/component/utils/SearchModal.tsx index ef030cd0..3975d19f 100644 --- a/src/component/utils/SearchModal.tsx +++ b/src/component/utils/SearchModal.tsx @@ -1,5 +1,5 @@ import { MouseEventHandler, ReactNode, useRef } from "react"; -import SearchBar from "./SearchBar"; +import ManagementSearchBar from "./ManagementSearchBar"; import Pagination from "./Pagination"; import TextWithLabel from "./TextWithLabel"; import "../../asset/css/SearchModal.css"; @@ -37,7 +37,7 @@ const SearchModal = ({ mainText={titleText} wrapperClassName="search-modal__header__title" /> - { + const isAdminPath = adminLnbMenu.some( + menu => menu.linkTo === useLocation().pathname, + ); + if (isAdminPath) { + return null; + } + const { keywords } = useGetSearchKeyword(); + if (keywords.length === 0) { + return null; + } + + return ; +}; + +export default SearchRanking; diff --git a/src/component/utils/SearchRankingItem.tsx b/src/component/utils/SearchRankingItem.tsx new file mode 100644 index 00000000..537f556f --- /dev/null +++ b/src/component/utils/SearchRankingItem.tsx @@ -0,0 +1,28 @@ +import { type SearchKeyword } from "~/type/SearchKeyword"; +import Tooltip from "./Tooltip"; + +type Props = { item: SearchKeyword & { id: number }; height: number }; + +const SearchRankingItem = ({ item, height }: Props) => { + const changeWord = (() => { + if (item.rankingChange === null) return "new"; + if (item.rankingChange > 0) return "▲"; + if (item.rankingChange < 0) return "▼"; + return "-"; + })(); + + return ( +
    +

    {item.id}

    +

    {item.searchKeyword}

    +

    + {changeWord} +

    +
    + ); +}; + +export default SearchRankingItem; diff --git a/src/component/utils/SearchRankingList.tsx b/src/component/utils/SearchRankingList.tsx new file mode 100644 index 00000000..b34175e0 --- /dev/null +++ b/src/component/utils/SearchRankingList.tsx @@ -0,0 +1,62 @@ +import { ComponentProps, useState } from "react"; +import { type SearchKeyword } from "~/type/SearchKeyword"; +import Carousel from "./Carousel"; +import SearchRankingItem from "./SearchRankingItem"; +import ToggleDownArrow from "~/asset/img/caret-down_DaveGandy.png"; +import Image from "./Image"; +import { Link } from "react-router-dom"; + +type Props = ComponentProps<"div"> & { + list: (SearchKeyword & { id: number })[]; +}; + +const SearchRankingList = ({ list }: Props) => { + const [isOpened, setIsOpened] = useState(false); + const HEIGHT = 24; + + const toggleOpened = () => setIsOpened(!isOpened); + + return ( + + ); +}; + +export default SearchRankingList; diff --git a/src/component/utils/Tooltip.tsx b/src/component/utils/Tooltip.tsx index 33feb7ef..8dca182a 100644 --- a/src/component/utils/Tooltip.tsx +++ b/src/component/utils/Tooltip.tsx @@ -1,6 +1,6 @@ import { ReactNode, useState } from "react"; import { createPortal } from "react-dom"; -import { useBound } from "../../hook/useBound"; +import { useBound } from "~/hook/useBound"; type TooltipProps = { className?: string; @@ -10,7 +10,10 @@ type TooltipProps = { const Tooltip = ({ children, description, className }: TooltipProps) => { const [isDisplayed, setDisplayed] = useState(false); - const { boundInfo, targetRef } = useBound(); + const { boundInfo, targetRef } = useBound({ + hasResizeEvent: true, + hasScrollEvent: true, + }); const displayTooltip = () => setDisplayed(true); const hiddenTooltip = () => setDisplayed(false); diff --git a/src/constant/breakPoint.ts b/src/constant/breakPoint.ts new file mode 100644 index 00000000..78b5bbd4 --- /dev/null +++ b/src/constant/breakPoint.ts @@ -0,0 +1,9 @@ +/** + * @description 반응형을 위한 breakpoint + */ + +export const breakPoint= { + mobile: 360, + tablet: 768, + laptop: 1200, +}; diff --git a/src/constant/headerMenu.js b/src/constant/headerMenu.ts similarity index 72% rename from src/constant/headerMenu.js rename to src/constant/headerMenu.ts index 45675754..de08fff8 100644 --- a/src/constant/headerMenu.js +++ b/src/constant/headerMenu.ts @@ -1,12 +1,12 @@ -import Information from "../asset/img/information_icon.svg"; -import InformationMobile from "../asset/img/information_icon_black.svg"; -import Book from "../asset/img/admin_icon.svg"; -import User from "../asset/img/Uniconlabs.png"; -import UserMobile from "../asset/img/login_feen.png"; -import Rent from "../asset/img/admin_icon_black.svg"; -import DB from "../asset/img/database.svg"; -import Mypage from "../asset/img/login_icon.svg"; -import Logout from "../asset/img/logout_IconsBox.png"; +import Information from "~/asset/img/information_icon.svg"; +import InformationMobile from "~/asset/img/information_icon_black.svg"; +import Book from "~/asset/img/admin_icon.svg"; +import User from "~/asset/img/Uniconlabs.png"; +import UserMobile from "~/asset/img/login_feen.png"; +import Rent from "~/asset/img/admin_icon_black.svg"; +import DB from "~/asset/img/database.svg"; +import Mypage from "~/asset/img/login_icon.svg"; +import Logout from "~/asset/img/logout_IconsBox.png"; export const basicGnbMenu = [ { diff --git a/src/hook/useApi.ts b/src/hook/useApi.ts index a84e9c57..6de2573e 100644 --- a/src/hook/useApi.ts +++ b/src/hook/useApi.ts @@ -4,12 +4,12 @@ import axiosPromise from "../util/axios"; type Method = "get" | "post" | "put" | "patch" | "delete"; -export const useApi = (method: Method, url: string, data?: unknown) => { +export const useApi = (method?: Method, url?: string, data?: unknown) => { const { addErrorDialog } = useNewDialog(); const request = useCallback( (resolve: (response: any) => void, reject?: (error: any) => void) => { - axiosPromise(method, url, data) + axiosPromise(method ?? "GET", url ?? "/", data) ?.then(response => { resolve(response); }) @@ -22,5 +22,24 @@ export const useApi = (method: Method, url: string, data?: unknown) => { [method, url, data], ); - return { request }; + const requestWithUrl = ( + method: Method, + url: string, + options?: { + data?: unknown; + onSuccess?: (response: any) => void; + onError?: (error: any) => void; + }, + ) => { + axiosPromise(method, url, options?.data) + ?.then(response => { + if (options?.onSuccess) options.onSuccess(response); + }) + ?.catch(error => { + if (options?.onError) options.onError(error); + else addErrorDialog(error); + }); + }; + + return { request, requestWithUrl }; }; diff --git a/src/hook/useBound.ts b/src/hook/useBound.ts index 07cf75df..fb8f8f80 100644 --- a/src/hook/useBound.ts +++ b/src/hook/useBound.ts @@ -1,6 +1,14 @@ import { useEffect, useRef, useState } from "react"; -export const useBound = () => { +type Props = { + hasResizeEvent?: boolean; + hasScrollEvent?: boolean; +}; + +export const useBound = ({ + hasResizeEvent = true, + hasScrollEvent = false, +}: Props) => { const [boundInfo, setBoundInfo] = useState({ top: 0, bottom: 0, @@ -18,14 +26,13 @@ export const useBound = () => { useEffect(() => { getBound(); - window.addEventListener("resize", getBound); - window.addEventListener("scroll", getBound); + if (hasResizeEvent) window.addEventListener("resize", getBound); + if (hasScrollEvent) window.addEventListener("scroll", getBound); return () => { - window.removeEventListener("scroll", getBound); - window.removeEventListener("resize", getBound); + if (hasScrollEvent) window.removeEventListener("scroll", getBound); + if (hasResizeEvent) window.removeEventListener("resize", getBound); }; }, []); return { targetRef, boundInfo }; }; - diff --git a/src/hook/useBreakPoint.ts b/src/hook/useBreakPoint.ts new file mode 100644 index 00000000..9afcbfe5 --- /dev/null +++ b/src/hook/useBreakPoint.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; + +/** + * 현재 width가 breakPoint에 해당하는지 확인하는 hook + * @param breakPoint 기준이 되는 width + * @param compareFunction 현재 width와 비교하는 함수 default : < (less than) + * @returns window.innerWidth와 breakPoint를 비교한 결과 + * @example const isTablet = useBreakPoint(768) + */ + +export const useBreakPoint = ( + breakPoint: number, + compareFunction?: (A: number, B: number) => boolean, +) => { + const compare = compareFunction || ((A: number, B: number) => A < B); + + const [isBreaked, setBreaked] = useState( + compare(window.innerWidth, breakPoint), + ); + + useEffect(() => { + const resize = () => setBreaked(compare(window.innerWidth, breakPoint)); + window.addEventListener("resize", resize); + return () => window.removeEventListener("resize", resize); + }, [breakPoint, compare]); + + return isBreaked; +}; diff --git a/src/hook/useInterval.ts b/src/hook/useInterval.ts new file mode 100644 index 00000000..59a0ab97 --- /dev/null +++ b/src/hook/useInterval.ts @@ -0,0 +1,32 @@ +import { useEffect, useRef } from "react"; + +/** + * useInterval SetInterval을 사용하기 위한 Hook + * + * @param callback 간격마다 실행할 함수 + * @param delay 실행 간격 (ms) + * @returns startInterval: setInterval 재시작을 위한 함수, stopInterval: setInterval 종료 함수 + * @example const { startInterval, stopInterval } = useInterval(() => { console.log("interval") }, 1000); + */ + +export const useInterval = (callback: () => void, delay: number) => { + const intervalId = useRef(null); + + const stopInterval = () => { + if (intervalId.current) { + clearInterval(intervalId.current); + intervalId.current = null; + } + }; + + const startInterval = () => { + stopInterval(); + intervalId.current = setInterval(callback, delay); + }; + + useEffect(() => { + return stopInterval; + }, [callback, delay]); + + return { startInterval, stopInterval }; +}; diff --git a/src/hook/usePermission.ts b/src/hook/usePermission.ts new file mode 100644 index 00000000..04f4b5b2 --- /dev/null +++ b/src/hook/usePermission.ts @@ -0,0 +1,30 @@ +import { useRecoilValue } from "recoil"; +import { useNewDialog } from "~/hook/useNewDialog"; +import userState from "~/atom/userState"; + +export const usePermission = () => { + const user = useRecoilValue(userState); + + const isLoggined = user !== null; + const isAdmin = user?.isAdmin; + const is42Authenticated = user.email !== user.userName; + + const { addDialogWithTitleAndMessage } = useNewDialog(); + + const excuteOnly42Authenticated = ( + callback: () => void, + forbiddenMessage?: string, + ) => { + if (is42Authenticated) { + callback(); + return; + } + addDialogWithTitleAndMessage( + "not authenticated", + forbiddenMessage || "42 인증 유저만 접근할 수 있는 기능입니다.", + "42 인증은 마이페이지에서 진행하실 수 있습니다.", + ); + }; + + return { isLoggined, isAdmin, is42Authenticated, excuteOnly42Authenticated }; +}; diff --git a/src/hook/useResponsiveWidth.ts b/src/hook/useResponsiveWidth.ts new file mode 100644 index 00000000..806df8d8 --- /dev/null +++ b/src/hook/useResponsiveWidth.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; + +type Props = { + pcWidth: number; + tabletWidth?: number; + mobileWidth: number; +}; + +export const useResponsiveWidth = ({ + pcWidth, + tabletWidth, + mobileWidth, +}: Props) => { + const [width, setSize] = useState(pcWidth); + + const resize = () => { + if (window.innerWidth <= 767) setSize(tabletWidth ?? mobileWidth); + else if (window.innerWidth <= 600) setSize(mobileWidth); + else setSize(pcWidth); + }; + + useEffect(() => { + resize(); + window.addEventListener("resize", resize); + return () => window.removeEventListener("resize", resize); + }, []); + + return { width }; +}; diff --git a/src/type/BookInfo.ts b/src/type/BookInfo.ts index e844918f..2c555af2 100644 --- a/src/type/BookInfo.ts +++ b/src/type/BookInfo.ts @@ -4,7 +4,7 @@ export type BookInfo = { id: number; title: string; author: string; - category: string; + category?: string; categoryId?: number; isbn?: string; publisher: string; @@ -14,3 +14,11 @@ export type BookInfo = { donator?: string; books?: Book[]; }; + +export type BookPreviewType = BookInfo & { + bookInfoId: number; +}; + +export type BookInfoRecommend = BookInfo & { + project: string[]; +}; diff --git a/src/type/SearchKeyword.ts b/src/type/SearchKeyword.ts new file mode 100644 index 00000000..bf6a03df --- /dev/null +++ b/src/type/SearchKeyword.ts @@ -0,0 +1,4 @@ +export type SearchKeyword = { + searchKeyword: string; + rankingChange: number | null; +}; diff --git a/src/util/date.ts b/src/util/date.ts index 42267704..ae9ea114 100644 --- a/src/util/date.ts +++ b/src/util/date.ts @@ -1,11 +1,10 @@ -import { isNumber, isString } from "./typeCheck"; -/* 기본적인 날짜표시 형식 20yy-mm-dd */ +import { User } from "../type"; +/* 기본적인 날짜표시 형식 20yy-mm-dd */ const dateReg = /^(20\d{2})-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])$/; export const isFormattedDate = (string: string) => RegExp(dateReg).test(string); export const dateFormat = (string: string) => { - if (isFormattedDate(string)) return string; return string?.slice(0, 10)?.replace(".", "-") || ""; }; @@ -22,9 +21,16 @@ export const splitDate = (string: string) => ?.map(v => parseInt(v)) || []; /* string 형식의 날짜 비교 */ +export const compareDate = ( + date1: string, + date2 = nowDate, + compare: (date1: string, date2: string) => boolean, +) => { + return compare(dateFormat(date1), dateFormat(date2)); +}; + export const dateLessThan = (date: string, now = nowDate) => { - if (!isString(date) || !isString(now)) return undefined; - return dateFormat(date) < now; + return compareDate(date, now, (date1, date2) => date1 < date2); }; /* 날짜 및 시간 계산 */ @@ -44,10 +50,27 @@ export const isExpiredDate = (expireDateString: string) => { }; export const addDay = (num: number, date = nowDate) => { - if (!isString(date) || !isNumber(num)) return date; - const splited = splitDate(dateFormat(date)); - if (!splited) return date; - const [year, month, day] = splited; - const dateObj = new Date(year, month - 1, day); + if (!isFormattedDate(date)) return date; + const dateObj = new Date(date); return dateFormat(addDayDateObject(dateObj, num).toISOString()); }; + +/* lending 관련 날짜 함수 */ + +export const lendingRestriction = (user: User) => { + // 대출제한날짜는 이미 반납한 대출건의 연체제한 + 대출중인 도서의 연체일로 계산 + const restrictionDate = + !user.penaltyEndDate || dateLessThan(user.penaltyEndDate) + ? addDay(user.overDueDay) // 오늘 날짜 + 대출중인 도서를 오늘 반납시 받게 될 연체일 + : addDay(user.overDueDay, user.penaltyEndDate); + // 이미 반납한 대출건의 연체 제한날짜 + 대출중인 도서를 오늘 반납시 받게 될 연체일 + + // 대출제한날짜가 현재 날짜보다 크면 대출제한 + const isRestricted = compareDate( + restrictionDate, + nowDate, + (d1, d2) => d1 > d2, + ); + + return { isRestricted, restrictionDate }; +}; diff --git a/tsconfig.json b/tsconfig.json index 0c3e76a0..bd5c5220 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "paths": { "~/*": ["./src/*"] } }, "include": ["src"], "references": [{ "path": "./tsconfig.vite.json" }] diff --git a/vite.config.ts b/vite.config.ts index 91f0b40d..101cd41a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig, loadEnv } from "vite"; // eslint-disable-next-line import/no-extraneous-dependencies import react from "@vitejs/plugin-react-swc"; +import tsconfigPaths from "vite-tsconfig-paths"; /** * REACT_APP_ 으로 시작하는 환경변수를 @@ -21,7 +22,7 @@ export default defineConfig(({ mode }) => { return { /** @see https://vitejs.dev/plugins/ */ - plugins: [react()], + plugins: [react(), tsconfigPaths()], envPrefix,