diff --git a/.env b/.env new file mode 100644 index 0000000..2d9f028 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_API_URL=https://api.bookpharmacy.store \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4a1a14c..ffa8947 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,12 @@ # production /build +localhost-key.pem +localhost.pem + # misc .DS_Store +.env .env.local .env.development .env.development.local diff --git a/package.json b/package.json index 091d215..b35ec9c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "react-hook-form": "^7.49.3", "react-router-dom": "6", "react-scripts": "5.0.1", + "react-slick": "^0.30.2", + "slick-carousel": "^1.8.1", "styled-components": "^6.1.6", "styled-reset": "^4.5.1", "swiper": "^11.0.5", diff --git a/src/App.js b/src/App.js index 13f630c..52d1358 100644 --- a/src/App.js +++ b/src/App.js @@ -29,7 +29,8 @@ import LoginFindResult from './pages/IdFindResult'; import PasswordFind from './pages/PasswordFind'; import PasswordFindResult from './pages/PasswordFindResult'; import IdFind from './pages/IdFind'; -import LoginContextProvider, { LoginContext } from './contexts/LoginContextProvider'; +import LoginContextProvider from './contexts/LoginContextProvider'; +import SearchResult from './pages/SearchResult'; function App() { // 브라우저 새로고침 스크롤 이벤트 @@ -40,47 +41,48 @@ function App() { }, []); https: return ( - - -
- - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + +
+ + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - }> - } /> - } /> - } /> - } - /> - - } /> - } /> - } /> - } /> - } /> - {/* } /> */} - } /> + }> + } /> + } /> + } /> + } + /> + + } /> + } /> + } /> + } /> + } /> + {/* } /> */} + } /> - } /> - } /> - } /> - -
-
-
- ); + } /> + } /> + } /> +
+
+
+
+ ); } export default App; diff --git a/src/assets/closeIconRound.svg b/src/assets/closeIconRound.svg new file mode 100644 index 0000000..1615930 --- /dev/null +++ b/src/assets/closeIconRound.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git "a/src/assets/icons8-\353\263\204-30 (1).png" "b/src/assets/icons8-\353\263\204-30 (1).png" new file mode 100644 index 0000000..62fcd11 Binary files /dev/null and "b/src/assets/icons8-\353\263\204-30 (1).png" differ diff --git "a/src/assets/icons8-\353\271\210\353\263\204-30 (2).png" "b/src/assets/icons8-\353\271\210\353\263\204-30 (2).png" new file mode 100644 index 0000000..757c13d Binary files /dev/null and "b/src/assets/icons8-\353\271\210\353\263\204-30 (2).png" differ diff --git "a/src/assets/icons8-\354\212\244\355\203\200-\353\260\230-\353\271\210-30.png" "b/src/assets/icons8-\354\212\244\355\203\200-\353\260\230-\353\271\210-30.png" new file mode 100644 index 0000000..4a23802 Binary files /dev/null and "b/src/assets/icons8-\354\212\244\355\203\200-\353\260\230-\353\271\210-30.png" differ diff --git a/src/components/BookCard.js b/src/components/BookCard.js index 54e7bf5..965897c 100644 --- a/src/components/BookCard.js +++ b/src/components/BookCard.js @@ -1,16 +1,22 @@ +import { Link } from 'react-router-dom'; + // Styles import styles from '../styles/BookCard.module.css'; -const BookCard = ({ title, author }) => { +const BookCard = ({ title, author, img, isbn }) => { return ( <> -
-
-
-
{title}
-
{author}
+ +
+
+ 썸네일 +
+
+
{title}
+
{author}
+
-
+ ); }; diff --git a/src/components/BookDetailCard.jsx b/src/components/BookDetailCard.jsx index 3d40b50..3e2f88e 100644 --- a/src/components/BookDetailCard.jsx +++ b/src/components/BookDetailCard.jsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react'; -import { Link, useSearchParams } from 'react-router-dom'; +import React from 'react'; +import { Link } from 'react-router-dom'; //COMPONENTS import HashTag from '../components/HashTag'; @@ -7,46 +7,93 @@ import HashTag from '../components/HashTag'; //STYLES import '../styles/BookDetailCard.css'; -const BookDetailCard = ({ title, author, imageUrl }) => { - const [searchParams, setSearchParams] = useSearchParams(); - const bookTitle = searchParams.get('title'); - // console.log('title:', bookTitle); +const BookDetailCard = ({ + title, + author, + imageUrl, + isbn, + bookKeywordList, + type, +}) => { + let cardType = ['expModal'].includes(type) ? type : ''; - // useEffect(() => { - // setSearchParams({ who: 'bb' }); - // }); + const renderCard = (type) => { + if (type === 'expModal') { + return ( + <> +
+
+
+
+ 썸네일 +
+
- return ( - <> - -
-
-
-
- 썸네일 +
+
+
+ {title} +
+
{author}
+
+ +
-
-
-
- {title} +
+ + ); + } else { + return ( + <> + +
+
+
+
+ 썸네일 +
+
+ +
+
+
+ {title} +
+
{author}
+
+ +
+ {bookKeywordList.map((keyword, idx) => { + return ( + + ); + })} +
-
{author}
-
-
- - - -
-
-
- + + + ); + } + }; + + return ( + <> +
{renderCard(cardType)}
); }; diff --git a/src/components/BookListSlide.js b/src/components/BookListSlide.js index cc11def..b5babc9 100644 --- a/src/components/BookListSlide.js +++ b/src/components/BookListSlide.js @@ -1,4 +1,6 @@ import { useState, useEffect } from 'react'; +import Slider from 'react-slick'; +import { Swiper, SwiperSlide } from 'swiper/react'; import axios from 'axios'; // COMPONENTS @@ -7,23 +9,57 @@ import BookCard from './BookCard'; // STYLES import styles from '../styles/BookListSlide.module.css'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; +import 'swiper/css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import { set } from 'react-hook-form'; + +const BookListSlide = ({ + list, + bigCategory, + midCategoryTitle, + title, + author, + imageUrl, +}) => { + var settings = { + dots: true, + infinite: false, + speed: 500, + slidesToShow: 1, + slidesToScroll: 1, + }; -const BookListSlide = ({ bigCategory, midCategoryTitle, author }) => { return ( <> -
- - <div className={styles['slide']}> - <BookCard title={'책 제목'} author={author} /> - <BookCard title={'책 제목'} author={author} /> - <BookCard title={'책 제목'} author={'저자'} /> - <BookCard title={'책 제목'} author={'저자'} /> - <BookCard title={'책 제목'} author={'저자'} /> - <BookCard title={'책 제목'} author={'저자'} /> - <BookCard title={'책 제목'} author={'저자'} /> - <BookCard title={'책 제목'} author={'저자'} /> + <section className={styles['swiper-container']}> + <div className={styles['container']}> + <Swiper + spaceBetween={20} + slidesPerView={7} + slidesOffsetBefore={0} + id={'my-swiper'} + > + <div className={styles['slide']}> + {list.map((item) => { + return ( + <SwiperSlide key={item.isbn}> + <BookCard + key={item.isbn} + title={item.title} + author={item.author} + img={item.imageUrl} + isbn={item.isbn} + /> + </SwiperSlide> + ); + })} + </div> + </Swiper> </div> - </div> + </section> </> ); }; diff --git a/src/components/Button.jsx b/src/components/Button.jsx index f1934ba..487e0be 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -39,7 +39,7 @@ const Button = ({ text, type }) => { }; const renderButton = (url, type) => { - if (type === 'add' || type === 'delete') { + if (type === 'add' || type === 'delete' || type === 'exp') { return <button className={styles[`Btn-${type}`]}>{text}</button>; } else { if (type === 'logout') { diff --git a/src/components/FeedCard.js b/src/components/FeedCard.js index c9d17f2..cbd42c1 100644 --- a/src/components/FeedCard.js +++ b/src/components/FeedCard.js @@ -3,23 +3,23 @@ import React from 'react'; //STYLES import '../styles/FeedCard.css'; -const FeedCard = ({ book_name, book_author, user_nickname }) => { +const FeedCard = ({ title, author, nickname, imgUrl, comment }) => { return ( <> <div className="FeedCardContainer"> <div className="feed_up_wrapper"> <div className="feed_review_wrapper"> <div className="feed_review_text_wrapper"> - <p className="feed_review_text"> - 죽는 날까지 하늘을 우러러 한 점 부끄럼이 없기를 - </p> + <p className="feed_review_text">{comment}</p> </div> <div className="review_book_wrapper"> - <div className="feed_book_img"></div> + <div className="feed_book_img_wrapper"> + <img className="feed_book_img" src={imgUrl} alt="" /> + </div> <div className="feed_book_text_wrapper"> - <div className="feed_book_title">서시</div> - <div className="feed_book_author">윤동주</div> + <div className="feed_book_title">{title}</div> + <div className="feed_book_author">{author}</div> </div> </div> </div> @@ -27,7 +27,7 @@ const FeedCard = ({ book_name, book_author, user_nickname }) => { <div className="feed_bottom_wrapper"> <div className="user_wrapper"> <div className="user_profile"></div> - <div className="user_nicknameText">별헤는밤</div> + <div className="user_nicknameText">{nickname}</div> </div> </div> </div> diff --git a/src/components/Modal/Experience.jsx b/src/components/Modal/Experience.jsx new file mode 100644 index 0000000..beec330 --- /dev/null +++ b/src/components/Modal/Experience.jsx @@ -0,0 +1,75 @@ +import React, { useState, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +// SERVICE +import api from '../../services/api'; + +// COMPONENTS +import BookInfoCard from '../BookDetailCard'; + +// STYLE +import '../../styles/Experience.css'; + +const Experience = ({ onClose }) => { + // 모달 창 닫는 함수 + const handleClose = () => { + onClose?.(); + }; + + const [bookInfo, setBookInfo] = useState([]); + const [searchParams, setSearchParams] = useSearchParams(); + + const getIsbn = () => { + let isbn = searchParams.get('isbn'); + + api.get(`/api/book/detail?isbn=${isbn}`).then((res) => { + setBookInfo(res.data); + }); + }; + + useEffect(() => { + getIsbn(); + }, []); + + return ( + <> + <div className="expModal_overlay"> + <div className="expModal_wrapper"> + <div className="expModal_title_wrapper"> + 리뷰작성 + <div className="close_btn" onClick={handleClose}> + X + </div> + </div> + <div className="expModal_content_wrapper"> + <div className="bookInfo_content_wrapper"> + <BookInfoCard + type="expModal" + title={bookInfo.title} + author={bookInfo.author} + imageUrl={bookInfo.imageUrl} + isbn={bookInfo.isbn} + /> + </div> + <div className="inputReview_wrapper"> + <div className="inputReview_title">리뷰작성</div> + <textarea + name="contents" + rows="10" + cols="50" + type="text" + className="inputReview_box" + /> + </div> + <div className="expModal_button_wrapper"> + <button className="review_btn">리뷰 남기기</button> + <button className="later_btn">피드만 나중에</button> + </div> + </div> + </div> + </div> + </> + ); +}; + +export default Experience; diff --git a/src/components/Pagination.jsx b/src/components/Pagination.jsx new file mode 100644 index 0000000..4b25e64 --- /dev/null +++ b/src/components/Pagination.jsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import styled from "styled-components"; + +const Pagination = ({ booksPerPage, totalBooks, paginate, bookTitle }) => { + const pageNumbers = []; + console.log(totalBooks, booksPerPage); + for (let i = 1; i <= Math.ceil(totalBooks / booksPerPage); i++) { + pageNumbers.push(i); + console.log(pageNumbers); + } + + return ( + <nav> + <PaginationContainer className="pagination"> + {pageNumbers.map((number) => { + return ( + <li + key={number} + className="page-item" + style={{ + textAlign: "center", + marginTop: "30px", + marginBottom: "60px", + padding: "4px", + }} + > + {/* <a onClick={()=>paginate(number)} href={bookTitle} className="page-link"> */} + <Link + onClick={() => { + window.scrollTo(0, 0); + paginate(number); + }} + > + {number} + </Link> + {/* </a> */} + </li> + ); + })} + </PaginationContainer> + </nav> + ); +}; + +export default Pagination; + +const PaginationContainer = styled.ul` + display: flex; + justify-content: center; +`; diff --git a/src/components/SearchBox.jsx b/src/components/SearchBox.jsx new file mode 100644 index 0000000..6eb091b --- /dev/null +++ b/src/components/SearchBox.jsx @@ -0,0 +1,145 @@ +import React, { useState } from 'react' +import SearchResultListModal from './SearchResultListModal'; + +// style +import "../styles/SearchStyles.css"; +import styled from 'styled-components'; + +const SearchBox = ({ + input, + setInput, + searchType, + setSearchType, + searchBook, + isShow, + setIsShow, + searchData, +}) => { + + + return ( + <section + className="search-wrapper" + onClick={(e) => { + e.stopPropagation(); + // handleSearchResultShow(); + setIsShow(true); + }} + > + <label> + <SearchInputWrap> + {/* 검색창에 라벨 적용해보기 */} + {/* 책 렌더링했던 유튜브 영상을 활용해서 검색창 누르면 밑에 책보여주는 방법으로 활용하기 */} + {/* <button + className="search-button" + onClick={searchBook} + name="search-button" + /> */} + <SelectMenu + value={searchType} + onChange={(e) => setSearchType(e.target.value)} + name="" + id="" + // className="search-select" + > + <option value="title" selected> + 책제목 + </option> + <option value="author">작가</option> + <option value="keyword">키워드</option> + </SelectMenu> + {searchType === "keyword" ? ( + <> + <SearchInput + type="text" + placeholder="검색어를 입력하세요" + // className="search-input" + value={input} + onChange={(e) => { + setInput(e.target.value); + }} + onKeyPress={searchBook} + /> + </> + ) : ( + <> + <SearchInput + type="text" + placeholder="검색어를 입력하세요" + // className="search-input" + value={input} + onChange={(e) => { + setInput(e.target.value); + }} + onKeyPress={searchBook} + /> + </> + )} + {input.length > 0 ? ( + <button + className="search-close-button" + onClick={(e) => { + setInput(""); + }} + > + X + </button> + ) : null} + </SearchInputWrap> + </label> + {input.length > 0 && isShow && searchData.length > 0 ? ( + // <SearchResultList + // book={searchData} + // onClick={(e) => { + // e.stopPropagation(); + // }} + // /> + <SearchResultListModal + book={searchData} + addInput={setInput} + onClick={(e) => { + e.stopPropagation(); + }} + /> + ) : null} + </section> + ); +}; + +export default SearchBox; + +const SearchInputWrap = styled.div` + width: 100%; + height: 60px; + box-shadow: 0px 2px 4px #00000033; + padding: 10px 0px 10px 1rem; + border-radius: 5px; + border: 1px solid #b0b0b0; + display: flex; + align-items: center; + font-size: 20px; + /* margin-bottom: 40px; */ +`; + +const SelectMenu = styled.select` + width: 140px; + font-size: 20px; + border: none; + /* padding-left: 10px; */ + text-align: center; + &:focus { + outline: none; + } +`; + +const SearchInput = styled.input` + border: none; + border-left: 1px solid #c0c0c0; + margin-left: 1rem; + padding-left: 1rem; + font-size: 20px; + width: 100%; + &:focus { + outline: none; + } +`; \ No newline at end of file diff --git a/src/components/SearchResultList.jsx b/src/components/SearchResultList.jsx index 252e637..67741ae 100644 --- a/src/components/SearchResultList.jsx +++ b/src/components/SearchResultList.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import '../styles/SearchResultList.css'; +// 구글 책 검색 const SearchResultList = ({ book, type, updateBook }) => { let listType = ['myBook'].includes(type) ? type : 'search'; // console.log('book: ',book); @@ -49,6 +50,7 @@ const SearchResultList = ({ book, type, updateBook }) => { </ul> ); } + return null; })} </div> </> diff --git a/src/components/SearchResultListModal.jsx b/src/components/SearchResultListModal.jsx new file mode 100644 index 0000000..c77de4f --- /dev/null +++ b/src/components/SearchResultListModal.jsx @@ -0,0 +1,66 @@ +import React from 'react' +import "../styles/SearchResultList.css"; +import { Link } from 'react-router-dom'; + +const SearchResultListModal = ({ book, type, updateBook, addInput }) => { + let listType = ["myBook"].includes(type) ? type : "search"; + + const updateBookTitle = (pickTitle) => { + updateBook(pickTitle); + }; + + return ( + <> + <div className={`${listType}-modal-container`}> + {book.map((item) => { + console.log(item); + // 작가에서 누구 지음 이걸 빼주도록 하기 -> 작가 이름을 어떻게 필터링 처리를 할까? + + let title = item && item.title; + let author = item && item.author; + let thumbnail = item && item.imageUrl; + let keyword = item && item.name; + + // 책 제목 && 작가 UI + if (title !== undefined) { + return ( + <ul key={item.id} className={`${listType}-result-container`}> + <Link to={`/book-detial/${title}`}> + <li className={`${listType}-result-list`}> + <img src={thumbnail} alt="" /> + <div className={`${listType}-result-item`}> + <h1 className={`${listType}-result-item-title`}> + {title} + </h1> + <h1 className={`${listType}-result-item-author`}> + {author} + </h1> + </div> + </li> + </Link> + </ul> + ); + } + + // 키워드 UI + if (keyword !== undefined) { + return ( + <p + style={{ fontSize: "20px", marginBottom: "10px" }} + onClick={()=>addInput(`#${keyword}`)} + > + {keyword} + </p> + ); + } + })} + </div> + </> + ); +} + +SearchResultListModal.defaultProps = { + type: "search", +}; + +export default SearchResultListModal \ No newline at end of file diff --git a/src/components/Slider.jsx b/src/components/Slider.jsx index 9549acb..e420533 100644 --- a/src/components/Slider.jsx +++ b/src/components/Slider.jsx @@ -1,49 +1,50 @@ -import React from "react"; -import { Swiper, SwiperSlide } from "swiper/react"; -import "swiper/css"; -import "../styles/Slider.css"; +import React from 'react'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import '../styles/Slider.css'; -import { Navigation, Pagination, Scrollbar, A11y } from "swiper/modules"; +import { Navigation, Pagination, Scrollbar, A11y } from 'swiper/modules'; -import "swiper/css"; -import "swiper/css/navigation"; -import "swiper/css/pagination"; -import "swiper/css/scrollbar"; +import 'swiper/css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import 'swiper/css/scrollbar'; const Slider = ({ title, subtitle, isBestSeller, bookTitle, bookAuthor }) => { - const numberOfSlides = 10; + const numberOfSlides = 10; - const slides = Array.from({ length: numberOfSlides }, (_, index) => ( - <SwiperSlide key={index}> - <div className="item-wrapper"> - <div className="item-image"></div> - <div className="item-detail"> - <div className="item-title">{bookTitle}</div> - <div className="item-author">{bookAuthor}</div> - </div> - </div> - {isBestSeller && <div className="item-rank">{index + 1}</div>} - </SwiperSlide> - )); + const slides = Array.from({ length: numberOfSlides }, (_, index) => ( + <SwiperSlide key={index}> + <div className="item-wrapper"> + <div className="item-image"></div> + <div className="item-detail"> + <div className="item-title">{bookTitle}</div> + <div className="item-author">{bookAuthor}</div> + </div> + </div> + {isBestSeller && <div className="item-rank">{index + 1}</div>} + </SwiperSlide> + )); - return ( - <div className="best"> - <div className="best-wrapper"> - <div className="title-wrapper"> - <div className="best-title">{title}</div> - <div className="best-subtitle">{subtitle}</div> - </div> - <Swiper - modules={[Navigation, Pagination, Scrollbar, A11y]} - spaceBetween={0} - slidesPerView={7} - navigation - > - {slides} - </Swiper> - </div> - </div> - ); -} + return ( + <div className="best"> + <div className="best-wrapper"> + <div className="title-wrapper"> + <div className="best-title">{title}</div> + <div className="best-subtitle">{subtitle}</div> + </div> + <Swiper + id="slider-swiper" + modules={[Navigation, Pagination, Scrollbar, A11y]} + spaceBetween={0} + slidesPerView={7} + navigation + > + {slides} + </Swiper> + </div> + </div> + ); +}; export default Slider; diff --git a/src/pages/BookDetail.jsx b/src/pages/BookDetail.jsx index bbf6405..266b1ba 100644 --- a/src/pages/BookDetail.jsx +++ b/src/pages/BookDetail.jsx @@ -1,4 +1,8 @@ -import React, { useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; + +// SERVICE +import api from '../services/api'; // COMPONENTS import Header from '../components/Header'; @@ -8,10 +12,12 @@ import Review from '../components/Review'; import Title from '../components/ArrowTitle'; import BookCard from '../components/BookCard'; import Footer from '../components/Footer'; +import ModalPortal from '../components/Modal/Portal'; +import ExpModal from '../components/Modal/Experience'; +import MyListModal from '../components/Modal/MyListModal'; // STYLES import '../styles/BookDetail.css'; -import { Link } from 'react-router-dom'; const BookDetail = () => { const scrollRef = useRef([]); @@ -19,6 +25,32 @@ const BookDetail = () => { ref.scrollIntoView({ behavior: 'smooth' }); }; + const [modalOn, setModalOn] = useState(false); + + const handleModal = () => { + setModalOn(!modalOn); + }; + + const [searchParams, setSearchParams] = useSearchParams(); + const [bookInfo, setBookInfo] = useState([]); + const [bookKeywordList, setBookKeywordList] = useState([]); + + const getIsbn = () => { + let isbn = searchParams.get('isbn'); + // console.log(isbn); + + api.get(`/api/book/detail?isbn=${isbn}`).then((res) => { + // console.log(res.data.title); + // console.log(res.data); + setBookInfo(res.data); + setBookKeywordList(res.data.bookKeywordList); + }); + }; + + useEffect(() => { + getIsbn(); + }, []); + return ( <> <Header /> @@ -26,25 +58,38 @@ const BookDetail = () => { <section className="bookSummary"> <div className="bookSummary_wrapper"> <div className="summary_left_wrapper"> - <div className="summary_left_img"></div> + <div> + <img + className="summary_left_img" + src={bookInfo.imageUrl} + alt={`${bookInfo.title}썸네일`} + /> + </div> </div> <div className="summary_right_wrapper"> <div className="summary_right_up_wrapper"> <div className="right_up_left_wrapper"> - <div className="up_left_book_title">해리포터와 불의 잔</div> - <div className="up_left_book_author">J. K. 롤링 </div> + <div className="up_left_book_title">{bookInfo.title}</div> + <div className="up_left_book_author">{bookInfo.author}</div> </div> <div className="right_up_right_wrapper"> <div className="up_right_exp"> - <Btn text={'경험 추가하기'} type="exp" /> + {/* <Btn + text={'경험 추가하기'} + type="exp" + onClick={handleModal} + /> */} + <div onClick={handleModal}>경험추가하기</div> </div> + <ModalPortal> + {modalOn && <ExpModal onClose={handleModal} />} + </ModalPortal> </div> </div> <div className="summary_left_mid_wrapper"> - <HashTag text={'#마법학교'} /> - <HashTag text={'#판타지'} /> - <HashTag text={'#해리포터'} /> - <HashTag text={'#소설원작'} /> + {bookKeywordList.map((item) => { + return <HashTag key={item.name} text={item.name} />; + })} </div> <div className="summary_left_bottom_wrapper"> <nav className="left_bottom_text_wrapper"> @@ -88,7 +133,8 @@ const BookDetail = () => { > <div className="bookInfo_title">책 정보</div> <div className="bookInfo_content"> - <p className="content_bold"> + <p className="content_normal">{bookInfo.content}</p> + {/* <p className="content_bold"> 해리 포터 세대의, 해리 포터 세대를 위한, 해리 포터 세대에 의한 새 번역!‘ 21세기 대표 아이콘’에 걸맞은 완성도 높은 작품으로 재탄생하다! @@ -139,7 +185,7 @@ const BookDetail = () => { 말투의 미세한 뉘앙스까지 점검했다. 『해리 포터』의 세계에 처음 발을 들여놓는 독자는 물론, 그동안 『해리 포터』의 세계를 즐겨 찾아왔던 독자 모두에게 완성도 높은 만족과 감동을 선사할 것이다. - </p> + </p> */} </div> </div> </section> diff --git a/src/pages/BookList.jsx b/src/pages/BookList.jsx index 93f91b5..51d9e2a 100644 --- a/src/pages/BookList.jsx +++ b/src/pages/BookList.jsx @@ -1,7 +1,11 @@ import React, { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import Slider from 'react-slick'; import axios from 'axios'; +// SERVICE +import api from '../services/api'; + // COMPONENTS import Header from '../components/Header'; import Title from '../components/ArrowTitle'; @@ -11,157 +15,40 @@ import BookListSlide from '../components/BookListSlide'; // STYLES import '../styles/BookList.css'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; const BookList = () => { + var settings = { + dots: true, + infinite: true, + speed: 500, + slidesToShow: 1, + slidesToScroll: 1, + }; + // 대분류 let { title } = useParams(); const [bigCategory, setBigCategory] = useState(''); // 중분류 const [midCategory, setMidCategory] = useState([]); - - const categories = [ - { - 총류: [ - '도서학·서지학', - '문헌정보학', - '백과사전', - '강연집·수필집·연설문집', - '일반연속간행물', - '일반학회·단체·협회·기관', - '신문·언론·저널리즘', - '일반전집·총서', - '향토자료', - ], - 철학: [ - '형이상학', - '인식론·인과론·인간학', - '철학의 체계', - '경학', - '동양철학·7사상', - '서양철학', - '논리학', - '심리학', - '윤리학·도덕철학', - ], - 종교: [ - '비교종교', - '불교', - '기독교', - '도교', - '천도교', - '신도', - '힌두교·브라만교', - '이슬람교(회교)', - '기타 제종교', - ], - 사회과학: [ - '통계학', - '경제학', - '사회학·사회문제', - '정치학', - '행정학', - '법학', - '교육학', - '풍속·예절·민속학', - '국방·군사학', - ], - 자연과학: [ - '수학', - '물리학', - '화학', - '천문학', - '지학', - '광물학', - '생명과학', - '식물학', - '동물학', - ], - 기술과학: [ - '의학', - '농업·농학', - '공학·공업일반·토목공학·환경', - '건축공학', - '기계공학', - '전기공학·전자공학', - '화학공학', - '제조업', - '생활과학', - ], - 예술: [ - '건축물', - '조각·조형예술', - '공예·장식미술', - '서예', - '회화·도화', - '사진예술', - '음악', - '공연예술·매체예술', - '오락·스포츠', - ], - 언어: [ - '한국어', - '중국어', - '일본어·기타아시아제어', - '영어', - '독일어', - '프랑스어', - '스페인어·포르투갈어', - '이탈리아어', - '기타제어', - ], - 문학: [ - // smallCategory 페이지 api 테스트 위해서 한국문학 -> 한국소설로 바꿈 - '한국소설', - '중국문학', - '일본문학·기타아시아문학', - '영미문학', - '독일문학', - '프랑스문학', - '스페인·포르투갈문학', - '이탈리아문학', - '기타제문학', - ], - 역사: [ - '아시아', - '유럽', - '아프리카', - '북아프리카', - '남아메리카', - '오세아니아', - '양극지방', - '지리', - '전기', - ], - }, - ]; + const [resMidBookList, setResMidBookList] = useState([]); // 초기에 랜더링될 때 한 번만 실행 useEffect(() => { // 대분류 지정 setBigCategory(title); - // { - // smallCategory.map((e) => { - // console.log(e); - // }); - // } - // setCategory(category[title]); - // console.log(smallCategory); - // 중분류 가져오기 + api.get('/api/category/big').then((res) => { + setMidCategory(res.data[title]); + }); - // categories.map((res) => { - // console.log('대분류:', title); - // setMidCategory(res[title]); - // }); - axios - .get( - 'https://port-0-backend-book-pharmacy-umnqdut2blqqhv7sd.sel5.cloudtype.app/api/category/big', - ) - .then((res) => { - console.log(res.data[title]); - setMidCategory(res.data[title]); + api.get(`/api/book/list/big?name=${title}`).then((res) => { + res.data.map(() => { + setResMidBookList(res.data); }); + }); }, []); return ( @@ -171,23 +58,43 @@ const BookList = () => { <Header /> <div className="bookList_title">{bigCategory}</div> <Title + key={bigCategory} bigCategory={bigCategory} title={`${bigCategory} 전체보기`} type={'shadow'} /> - <div className="bookList_wrapper"> - {midCategory?.map((e) => { - // console.log(e); - // setNameList(e.name); - return ( - <BookListSlide - key={e} - bigCategory={bigCategory} - midCategoryTitle={e} - /> - ); - })} - </div> + + {resMidBookList.map((list, idx) => { + return ( + // 중분류 타이틀 렌더링 + <div className="bookList_wrapper" key={idx}> + <div className="bookList_title_wrapper"> + <Title + key={list[idx]} + bigCategory={bigCategory} + title={list.categoryName} + /> + </div> + + <div className="bookList_slide_wrapper"> + <BookListSlide list={list.bookList} /> + + {/* 중분류에 해당하는 책 리스트 데이터 바인딩 */} + {/* {list.bookList.map((item) => { + return ( + <BookListSlide + key={item.isbn} + title={item.title} + author={item.author} + bigCategory={bigCategory} + imageUrl={item.imageUrl} + /> + ); + })} */} + </div> + </div> + ); + })} </div> <Footer /> diff --git a/src/pages/Feed.jsx b/src/pages/Feed.jsx index 4013942..f194a65 100644 --- a/src/pages/Feed.jsx +++ b/src/pages/Feed.jsx @@ -1,13 +1,91 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; + +// SERVICES +import api from '../services/api'; // COMPONENTS import Header from '../components/Header'; import FeedGrid from '../components/FeedGrid'; +import FeedCard from '../components/FeedCard'; // STYLES import '../styles/Feed.css'; const Feed = () => { + const [dogArr, setDogArr] = useState([]); + const [feedArr, setFeedArr] = useState([]); + const [page, setPage] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + // Intersection Observer 설정 + const handleObserver = (entries) => { + // console.log(entries); + const target = entries[0]; + if (target.isIntersecting && !isLoading) { + setPage((prevPage) => prevPage + 1); + } + }; + + useEffect(() => { + const observer = new IntersectionObserver(handleObserver, { + threshold: 0, + }); + // 최하단 요소를 관찰 대상으로 지정함 + const observerTarget = document.getElementById('observer'); + // 관찰 시작 + if (observerTarget) { + observer.observe(observerTarget); + } + }, []); + + // page 변경 감지에 따른 API호출 + useEffect(() => { + fetchData(); + console.log(page); + }, [page]); + + let feedFetchData = { page: { page }, size: 9 }; + + // API를 호출하는 부분 + const fetchData = async () => { + setIsLoading(true); + try { + // api.get('/api/feeds/all', feedFetchData).then((res) => { + // // console.log(res.data.content); + // setFeedArr(res.data.content); + // setFeedArr((prevData) => [...prevData, ...res.data.content]); + // }); + const API_URL = + 'https://port-0-backend-book-pharmacy-umnqdut2blqqhv7sd.sel5.cloudtype.app/api/feeds/all?page=0&size=9'; + + const response = await axios.get(API_URL, feedFetchData); + // .then((res) => { + // // console.log(res.data.content); + // setFeedArr(res.data.content); + // }); + + // const newData = response.data.map((dogImg) => ({ + // id: dogImg.id, + // dogUrl: dogImg.url, + // })); + + const newData = response.data.content.map((item) => ({ + title: item.bookTitle, + author: item.bookAuthor, + comment: item.comment, + image: item.imgUrl, + nickName: item.clientNickname, + })); + + // //불러온 데이터를 배열에 추가 + setFeedArr((prevData) => [...prevData, ...newData]); + } catch (error) { + console.log(error); + } + setIsLoading(false); + }; + return ( <> <Header /> @@ -16,8 +94,20 @@ const Feed = () => { <div className="feed_title">추천 피드</div> </div> <div className="feed_content_wrapper"> - <FeedGrid /> + {feedArr.map((item, idx) => { + return ( + <FeedCard + key={`${idx}-item.id`} + title={item.title} + author={item.author} + comment={item.comment} + imgUrl={item.image} + nickname={item.nickName} + /> + ); + })} </div> + <div id="observer" style={{ height: '10px' }}></div> </div> </> ); diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index a493183..6ceaa52 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -201,8 +201,6 @@ const ImageContent = styled.div` flex: 1; max-width: 50%; box-sizing: border-box; - /* background-image: url('https://d3udu241ivsax2.cloudfront.net/v3/images/login/promotion_intro_bg.ac5237a5bed49b864cccee5224a464e4.jpg'); */ - /* background-image: url('https://www.flybook.kr/FlyBookSitePublishing/assets/img/main/top-banner.jpg'); */ background-image: url('../../public/Login-Banner.png'); background: url(${banner}); background-size: cover; diff --git a/src/pages/Search.jsx b/src/pages/Search.jsx index 098dad0..d1bacbc 100644 --- a/src/pages/Search.jsx +++ b/src/pages/Search.jsx @@ -1,311 +1,329 @@ -import { useContext, useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; -import axios from 'axios'; +import { useContext, useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import axios from "axios"; // COMPONENTS -import Header from '../components/Header'; -import SearchResultList from '../components/SearchResultList'; +import Header from "../components/Header"; +import SearchResultListModal from "../components/SearchResultListModal"; // STYLES -import '../styles/SearchStyles.css'; -import bookImg1 from '../assets/category-book-총류.jpg'; -import bookImg2 from '../assets/category-book-철학.jpg'; -import bookImg3 from '../assets/category-book-종교.jpg'; -import bookImg4 from '../assets/category-book-사회과학.jpg'; -import bookImg5 from '../assets/category-book-자연과학.jpg'; -import bookImg6 from '../assets/category-book-기술과학.jpg'; -import bookImg7 from '../assets/category-book-예술.jpg'; -import bookImg8 from '../assets/category-book-언어.jpg'; -import bookImg9 from '../assets/category-book-문학.png'; -import bookImg10 from '../assets/category-book-역사.jpg'; -import { LoginContext } from '../contexts/LoginContextProvider'; +import "../styles/SearchStyles.css"; +import bookImg1 from "../assets/category-book-총류.jpg"; +import bookImg2 from "../assets/category-book-철학.jpg"; +import bookImg3 from "../assets/category-book-종교.jpg"; +import bookImg4 from "../assets/category-book-사회과학.jpg"; +import bookImg5 from "../assets/category-book-자연과학.jpg"; +import bookImg6 from "../assets/category-book-기술과학.jpg"; +import bookImg7 from "../assets/category-book-예술.jpg"; +import bookImg8 from "../assets/category-book-언어.jpg"; +import bookImg9 from "../assets/category-book-문학.png"; +import bookImg10 from "../assets/category-book-역사.jpg"; +import { LoginContext } from "../contexts/LoginContextProvider"; +import api from "../services/api"; + const Search = () => { - const baseURL = 'https://api.bookpharmacy.store/api'; - const [input, setInput] = useState(''); // 검색 데이터 - const [searchData, setSearchData] = useState([]); // 검색 결과 데이터 - const [categories, setCategories] = useState([]); // 카테고리 데이터 - const [isShow, setIsShow] = useState(false); // 검색창 모달창 + const baseURL = "https://api.bookpharmacy.store/api"; + const navigate = useNavigate(); + const [input, setInput] = useState(""); // 검색 데이터 + const [inputKeyword, setInputKeyword] = useState([]); // 키워드 검색 데이터 + const [searchData, setSearchData] = useState([]); // 검색 결과 데이터 + const [categories, setCategories] = useState([]); // 카테고리 데이터 + const [isShow, setIsShow] = useState(false); // 검색창 모달창 + const [searchType, setSearchType] = useState("title"); // 검색 유형 상태 + + const { userId, userPwd } = useContext(LoginContext); + const loginData = { username: userId, password: userPwd }; + + // 카테고리 배경 색상(10개) && 카테고리별 대표 책 이미지 정보 + const categoriesInfo = [ + { color: "#D4F4FF", image: bookImg1 }, + { color: "#FFF2EC", image: bookImg2 }, + { color: "#FFE3B5", image: bookImg3 }, + { color: "#FFF4B6", image: bookImg4 }, + { color: "#D6D6D6", image: bookImg5 }, + { color: "#C2E2FF", image: bookImg6 }, + { color: "#FFCACD", image: bookImg7 }, + { color: "#DFFFF8", image: bookImg8 }, + { color: "#CBD4F0", image: bookImg9 }, + { color: "#D6CABC", image: bookImg10 }, + ]; - const { userId, userPwd } = useContext(LoginContext); - const loginData = { username: userId, password: userPwd }; - // 카테고리 배경 색상(10개) && 카테고리별 대표 책 이미지 정보 - const categoriesInfo = [ - { color: '#D4F4FF', image: bookImg1 }, - { color: '#FFF2EC', image: bookImg2 }, - { color: '#FFE3B5', image: bookImg3 }, - { color: '#FFF4B6', image: bookImg4 }, - { color: '#D6D6D6', image: bookImg5 }, - { color: '#C2E2FF', image: bookImg6 }, - { color: '#FFCACD', image: bookImg7 }, - { color: '#DFFFF8', image: bookImg8 }, - { color: '#CBD4F0', image: bookImg9 }, - { color: '#D6CABC', image: bookImg10 }, - ]; - const setCategory = async (res) => { - const fetchedCategories = res.data; - const transformedCategories = Object.keys(fetchedCategories).map( - (key, index) => { - const { color, image } = categoriesInfo[index % categoriesInfo.length]; // 객체에서 색상과 이미지를 가져옴 - return { - title: key, - subtitle: fetchedCategories[key].join(', '), - image: image, - color: color, - }; - }, - ); - setCategories(transformedCategories); - }; + // 카테고리 대분류, 중분류 GET 요청 및 요청 데이터 사용하기 쉽게 처리 + useEffect(() => { + let username = localStorage.getItem("id"); + let password = localStorage.getItem("password"); - // 카테고리 대분류, 중분류 GET 요청 및 요청 데이터 사용하기 쉽게 처리 - useEffect(() => { - let username = localStorage.getItem('id'); - let password = localStorage.getItem('password'); + const fetchCategories = async () => { + try { + axios + .post( + "https://api.bookpharmacy.store/login", + { username: username, password: password }, + { withCredentials: true } + ) + .then(async () => { + // console.log('성공'); + axios + .get("https://api.bookpharmacy.store/api/category/big", { + withCredentials: true, + }) + .then((res) => { + setCategories(res.data); + }); + }) + .catch((err) => { + console.log(err); + }); + axios + .get("https://api.bookpharmacy.store/api/category/big", { + withCredentials: true, + }) + .then((res) => { + setCategories(res.data); + }); + } catch (error) { + console.error("Error fetching categories:", error); + } + }; - const fetchCategories = async () => { - try { - axios - .post( - 'https://api.bookpharmacy.store/login', - { username: username, password: password }, - { withCredentials: true }, - ) - .then(async () => { - // console.log('성공'); - axios - .get('https://api.bookpharmacy.store/api/category/big', { - // withCredentials: true, - }) - .then((res) => { - setCategories(res.data); - }); - }) - .catch((err) => { - console.log(err); - }); - } catch (error) { - console.error('Error fetching categories:', error); - } - }; + fetchCategories(); + }, []); - fetchCategories(); - }, []); + // 카테고리 아이템을 렌더링 함수 + const renderCategoryItem = ({ title, subtitle, image, color }, index) => ( + <Link to={`/book/list/${title}`} key={index}> + <div className="category-item-wrapper"> + <div + className="category-grid-item" + style={{ + backgroundColor: color, // 여기서 색상 적용 + }} + > + <div className="category-grid-description"> + <h2 className="category-grid-item-title">{title}</h2> + <h3 className="category-grid-item-subtitle">{subtitle}</h3> + </div> + <img + src={image} + alt="카테고리 대표 이미지" + className="category-grid-item-image" + /> + </div> + </div> + </Link> + ); - // 카테고리 아이템을 렌더링 함수 - const renderCategoryItem = ({ title, subtitle, image, color }, index) => ( - <Link to={`/book/list/${title}`} key={index}> - <div className="category-item-wrapper"> - <div - className="category-grid-item" - style={{ - backgroundColor: color, // 여기서 색상 적용 - }} - > - <div className="category-grid-description"> - <h2 className="category-grid-item-title">{title}</h2> - <h3 className="category-grid-item-subtitle">{subtitle}</h3> - </div> - <img - src={image} - alt="카테고리 대표 이미지" - className="category-grid-item-image" - /> - </div> - </div> - </Link> - ); + // 함수로 추천 키워드 리스트를 생성하는 함수 + const renderKeywordList = (title, keywords) => ( + <section className="recommend-word-wrapper"> + <h2 className="recommend-title">{title}</h2> + <ul className="recommend-keyword-wrapper"> + {keywords.map((keyword, index) => ( + <li key={index}> + <Link to={`/result/${keyword}-책목록-페이지`}>{keyword}</Link> + </li> + ))} + </ul> + </section> + ); - // 함수로 추천 키워드 리스트를 생성하는 함수 - const renderKeywordList = (title, keywords) => ( - <section className="recommend-word-wrapper"> - <h2 className="recommend-title">{title}</h2> - <ul className="recommend-keyword-wrapper"> - {keywords.map((keyword, index) => ( - <li key={index}> - <Link to={`/result/${keyword}-책목록-페이지`}>{keyword}</Link> - </li> - ))} - </ul> - </section> - ); + // 추천 검색어 리스트 + const recommendedSearchKeywords = [ + "감정", + "해리포터", + "화장품", + "하늘 높이 비상", + "감정", + // Add more keywords as needed + ]; - // 추천 검색어 리스트 - const recommendedSearchKeywords = [ - '감정', - '해리포터', - '화장품', - '하늘 높이 비상', - '감정', - '해리포터', - '화장품', - '하늘 높이 비상', - '감정', - '해리포터', - '화장품', - '하늘 높이 비상', - '감정', - '해리포터', - '화장품', - '하늘 높이 비상', - '감정', - '해리포터', - '화장품', - '하늘 높이 비상', - '감정', - '해리포터', - '화장품', - '하늘 높이 비상', - // Add more keywords as needed - ]; + // 사용자 추천 키워드 리스트 + const userRecommendedKeywords = [ + "#감정", + "#해리포터", + "#화장품", + "#하늘 높이 비상", + // Add more keywords as needed + ]; - // 사용자 추천 키워드 리스트 - const userRecommendedKeywords = [ - '#감정', - '#해리포터', - '#화장품', - '#하늘 높이 비상', - '#감정', - '#해리포터', - '#화장품', - '#하늘 높이 비상', - '#감정', - '#해리포터', - '#화장품', - '#하늘 높이 비상', - '#감정', - '#해리포터', - '#화장품', - '#하늘 높이 비상', - // Add more keywords as needed - ]; + // 검색할 때, 0.1초 딜레이 걸기 -> 끊기는 느낌을 방지 + useEffect(() => { + const timer = setTimeout(() => { + fetchBooks(input); + }, 100); - // 검색할 때, 0.1초 딜레이 걸기 -> 끊기는 느낌을 방지 - useEffect(() => { - const timer = setTimeout(() => { - fetchBooks(input); - }, 100); + // cleanup 함수를 반환하여 컴포넌트가 언마운트될 때 타이머를 해제합니다. + return () => clearTimeout(timer); + }, [input]); - // cleanup 함수를 반환하여 컴포넌트가 언마운트될 때 타이머를 해제합니다. - return () => clearTimeout(timer); - }, [input]); + const fetchBooks = async (searchInput) => { + if (input.trim() === "") return; // 빈 문자열일 때 API 호출 방지 - const fetchBooks = async (searchInput) => { - if (input.trim() === '') return; // 빈 문자열일 때 API 호출 방지 + let endpoint = ""; + if (searchType === "title") { + endpoint = `/api/search/book?title=${searchInput}&target=modal`; + } + if (searchType === "author") { + endpoint = `/api/search/book?author=${searchInput}&target=modal`; + } + if (searchType === "keyword") { + endpoint = `/api/search/keyword?name=${searchInput}&target=modal`; + } - try { - const response = await axios.get( - `https://www.googleapis.com/books/v1/volumes?q=${searchInput}&key=AIzaSyDUtFpAVpNPHCEW-pxSxpTHSACNjko_MCc&maxResults=10`, - ); - const booksData = response.data.items - ? response.data.items.slice(0, 6) - : []; // 검색창에서 6개의 데이터만 보여줌 + try { + const response = await api.get( + endpoint + ); + console.log("test", searchType,response.data); + setSearchData(response.data); + } catch (error) { + console.error("Failed to fetch books:", error); + } + }; - setSearchData(booksData); - } catch (error) { - console.error('Failed to fetch books:', error); - } - }; + // 검색창 엔터 및 버튼 이벤트 처리 - // 검색창 엔터 및 버튼 이벤트 처리 + const searchBook = (evt) => { + if (evt.key === "Enter") { + fetchBooks(input); + navigate(`/search/result/${input}`) + } + }; - const searchBook = (evt) => { - if (evt.key === 'Enter' || evt.target.name === 'search-button') { - fetchBooks(input); - } - }; + const handleSearchResultClose = () => { + setIsShow(false); + }; - const handleSearchResultClose = () => { - setIsShow(false); - }; + const handleSearchResultShow = () => { + setIsShow(true); + }; - const handleSearchResultShow = () => { - setIsShow(true); - }; + const handleSelectChange = (e) => { + setSearchType(e.target.value); + setInput(""); + }; - return ( - <div onClick={handleSearchResultClose}> - <Header /> + return ( + <div onClick={handleSearchResultClose}> + <Header /> - {/* 검색 페이지 전체 */} - <section className="search-container"> - {/* 검색 창 */} - <section - className="search-wrapper" - onClick={(e) => { - e.stopPropagation(); - handleSearchResultShow(); - }} - > - <label> - <div className="search-wrap-inner"> - {/* 검색창에 라벨 적용해보기 */} - {/* 책 렌더링했던 유튜브 영상을 활용해서 검색창 누르면 밑에 책보여주는 방법으로 활용하기 */} - <button - className="search-button" - onClick={searchBook} - name="search-button" - /> - <input - type="text" - placeholder="검색어를 입력하세요" - className="search-input" - value={input} - onChange={(e) => { - setInput(e.target.value); - }} - onKeyPress={searchBook} - /> - {input.length > 0 ? ( - <button - className="search-close-button" - onClick={(e) => { - setInput(''); - }} - > - X - </button> - ) : null} - </div> - </label> - {input.length > 0 && isShow ? ( - <SearchResultList - book={searchData} - onClick={(e) => { - e.stopPropagation(); - }} - /> - ) : null} - </section> + {/* 검색 페이지 전체 */} + <section className="search-container"> + {/* 검색 창 */} + <section + className="search-wrapper" + onClick={(e) => { + e.stopPropagation(); + handleSearchResultShow(); + }} + > + <label> + <div className="search-wrap-inner"> + {/* 검색창에 라벨 적용해보기 */} + {/* 책 렌더링했던 유튜브 영상을 활용해서 검색창 누르면 밑에 책보여주는 방법으로 활용하기 */} + {/* <button + className="search-button" + onClick={searchBook} + name="search-button" + /> */} + <select + value={searchType} + onChange={handleSelectChange} + name="" + id="" + className="search-select" + > + <option value="title" selected> + 책제목 + </option> + <option value="author">작가</option> + <option value="keyword">키워드</option> + </select> + {searchType === "keyword" ? ( + <> + <input + type="text" + placeholder="검색어를 입력하세요" + className="search-input" + value={input} + onChange={(e) => { + setInput(e.target.value); + }} + onKeyPress={searchBook} + /> + </> + ) : ( + <> + <input + type="text" + placeholder="검색어를 입력하세요" + className="search-input" + value={input} + onChange={(e) => { + setInput(e.target.value); + }} + onKeyPress={searchBook} + /> + {input.length > 0 ? ( + <button + className="search-close-button" + onClick={(e) => { + setInput(""); + }} + > + X + </button> + ) : null} + </> + )} + </div> + </label> + {input.length > 0 && isShow && searchData.length > 0 ? ( + // <SearchResultList + // book={searchData} + // onClick={(e) => { + // e.stopPropagation(); + // }} + // /> + <SearchResultListModal + book={searchData} + addInput={setInput} + onClick={(e) => { + e.stopPropagation(); + }} + /> + ) : null} + </section> - {/* 추천 검색어 */} - {renderKeywordList('추천검색어', recommendedSearchKeywords)} + {/* 추천 검색어 */} + {renderKeywordList("추천검색어", recommendedSearchKeywords)} - {/* 사용자 추천 키워드 */} - {renderKeywordList('사용자 추천 키워드', userRecommendedKeywords)} + {/* 사용자 추천 키워드 */} + {renderKeywordList("사용자 추천 키워드", userRecommendedKeywords)} - {/* 카테고리 */} - <section className="category-wrapper"> - <h2 className="recommend-title">카테고리</h2> - <div className="category-items"> - {Object.keys(categories).map((key, index) => { - const title = key; - const subtitle = categories[key].join(', '); - const infoIndex = index % categoriesInfo.length; // 나머지로 0~9만 접근하도록 길이제한 - const color = categoriesInfo[infoIndex].color; - const image = categoriesInfo[infoIndex].image; - return renderCategoryItem( - { title, subtitle, color, image }, - index, - ); - })} - </div> - </section> - </section> - </div> - ); + {/* 카테고리 */} + <section className="category-wrapper"> + <h2 className="recommend-title">카테고리</h2> + <div className="category-items"> + {Object.keys(categories).map((key, index) => { + const title = key; + const subtitle = categories[key].join(", "); + const infoIndex = index % categoriesInfo.length; // 나머지로 0~9만 접근하도록 길이제한 + const color = categoriesInfo[infoIndex].color; + const image = categoriesInfo[infoIndex].image; + return renderCategoryItem( + { title, subtitle, color, image }, + index + ); + })} + </div> + </section> + </section> + </div> + ); }; export default Search; diff --git a/src/pages/SearchResult.jsx b/src/pages/SearchResult.jsx new file mode 100644 index 0000000..ca55c50 --- /dev/null +++ b/src/pages/SearchResult.jsx @@ -0,0 +1,584 @@ +import React, { useEffect, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import Header from "../components/Header"; +import styled from "styled-components"; +import closeIcon from "../assets/closeIconRound.svg"; +import starIcon from "../assets/icons8-별-30 (1).png"; +import api from "./../services/api"; +import Pagination from "../components/Pagination"; +import SearchBox from "../components/SearchBox"; + +const SearchResult = () => { + const navigate = useNavigate(); + const { title } = useParams(); // path로 책 제목 가져오기 + const [viewMode, setViewMode] = useState(true); // 책 뷰 선택(리스트/카드) + const [books, setBooks] = useState([]); // 책 정보 + const [currentPage, setCurrentPage] = useState(1); // 현재 페이지 + const [booksPerPage, setBooksPerPage] = useState(10); // 페이지당 책 수 + const [loading, setLoading] = useState(false); // 로딩 + + const [input, setInput] = useState(""); // 검색 데이터 + const [searchType, setSearchType] = useState("title"); // 검색 유형 상태 + const [searchData, setSearchData] = useState([]); // 검색 결과 데이터 + const [isShow, setIsShow] = useState(false); // 검색창 모달창 + + useEffect(() => { + const timer = setTimeout(() => { + fetchBooks(input); + }, 100); + + // cleanup 함수를 반환하여 컴포넌트가 언마운트될 때 타이머를 해제합니다. + return () => clearTimeout(timer); + }, [input]); + + const fetchBooks = async (searchInput) => { + if (input.trim() === "") return; // 빈 문자열일 때 API 호출 방지 + + let endpoint = ""; + if (searchType === "title") { + endpoint = `/api/search/book?title=${searchInput}&target=modal`; + } + if (searchType === "author") { + endpoint = `/api/search/book?author=${searchInput}&target=modal`; + } + if (searchType === "keyword") { + endpoint = `/api/search/keyword?name=${searchInput}&target=modal`; + } + + try { + const response = await api.get(endpoint); + console.log("test", searchType, response.data); + setSearchData(response.data); + } catch (error) { + console.error("Failed to fetch books:", error); + } + }; + + const searchBook = (evt) => { + if (evt.key === "Enter") { + fetchBooks(input); + if (input.length > 0) navigate(`/search/result/${input}`); + setIsShow(false); + } + }; + + // 현재 책들 정보 + const indexOfLastBook = currentPage * booksPerPage; + const indexOfFirstBook = indexOfLastBook - booksPerPage; + console.log(indexOfFirstBook, indexOfLastBook); + const currentBooks = books.slice(indexOfFirstBook, indexOfLastBook); + + // 페이지 변경 + const paginate = (pageNumber) => setCurrentPage(pageNumber); + + console.log(books.length); + console.log(currentBooks); + + useEffect(() => { + const getSearchResults = async () => { + setLoading(true); + try { + const response = await api.get( + // `/api/search/book?title=${title}&target=page&page=${currentPage-1}&size=${booksPerPage}` + `/api/search/book?title=${title}&target=page&page=0&size=999` + ); + console.log(response.data); + setBooks(response.data); + setLoading(false); + } catch (error) { + console.error("책 데이터 GET 요청 실패", error); + } + }; + getSearchResults(); + }, [title]); + + let searchResultsCount = books.length; + searchResultsCount = searchResultsCount.toLocaleString(); + let searchResultsKeywordCount = 123; + let reviewCount = 123; + + const handleSizeChange = (event) => { + setBooksPerPage(event.target.value); + }; + + return ( + <> + <Header /> + <Main onClick={() => setIsShow(false)}> + {/* 검색창 컴포넌트 만들어야함 */} + {/* <SearchInputWrap> + <SelectMenu> + <option value="title" selected> + 제목 + </option> + <option value="author">저자</option> + <option value="keyword">키워드</option> + </SelectMenu> + <SearchInput type="text" placeholder="검색어를 입력하세요" /> + </SearchInputWrap> */} + <SearchBox + input={input} + setInput={setInput} + searchType={searchType} + setSearchType={setSearchType} + isShow={isShow} + setIsShow={setIsShow} + searchBook={searchBook} + searchData={searchData} + /> + + <section id="search-title" style={{ marginBottom: "80px" }}> + <h1 style={{ fontSize: "30px", fontWeight: "bold" }}> + <span style={{ color: "#67B6C1" }}>"{title}"</span> 에 대한 + <span style={{ color: "#67B6C1" }}> + {" "} + {searchResultsCount} 개의 검색 결과 + </span> + </h1> + </section> + + <ContentsWrap> + <aside> + <ContentTitle style={{ marginBottom: "20px" }}> + 키워드 검색 + </ContentTitle> + <input + type="text" + placeholder="키워드 추가" + style={{ + borderRadius: "10px", + border: "1px solid #6B6565", + padding: "8px 8px 8px 12px", + fontSize: "1rem", + }} + /> + </aside> + + <section style={{ paddingLeft: "2.5rem", width: "100%" }}> + {/* 헤더 영역 */} + <div + style={{ + width: "100%", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + paddingBottom: "20px", + borderBottom: "1px solid #c4bebe", + }} + > + <ContentTitle> + 전체{" "} + <span style={{ color: "#67B6C1" }}> + {searchResultsKeywordCount}건 + </span> + </ContentTitle> + <div> + <div style={{ display: "flex" }}> + <select + name="" + id="" + style={{ + width: "130px", + border: "1px solid #C0C0C0 ", + padding: "0px 10px", + borderRadius: "5px", + // marginRight: "10px", + }} + > + <option value="" selected> + 인기순 + </option> + <option value="">평점순</option> + </select> + <select + name="" + id="" + style={{ + width: "130px", + border: "1px solid #C0C0C0 ", + padding: "0px 10px", + borderRadius: "5px", + marginLeft: "10px", + }} + onChange={handleSizeChange} + value={booksPerPage} + > + <option value="10" selected> + 10개씩 보기 + </option> + <option value="50">50개씩 보기</option> + <option value="100">100개씩 보기</option> + </select> + <div + style={{ + display: "flex", + width: "75px", + border: "1px solid #C0C0C0", + borderRadius: "5px", + marginLeft: "10px", + }} + > + <button + onClick={() => setViewMode(true)} + style={{ + padding: "10px", + backgroundColor: "white", + borderRight: "1px solid #C0C0C0", + }} + > + <img + src="https://contents.kyobobook.co.kr/resources/fo/images/common/ink/ico_view_list_active.png" + alt="" + /> + </button> + <button + onClick={() => setViewMode(false)} + style={{ + padding: "10px", + backgroundColor: "white", + }} + > + <img + src="https://contents.kyobobook.co.kr/resources/fo/images/common/ink/ico_view_img_active.png" + alt="" + /> + </button> + </div> + </div> + </div> + </div> + + {/* 키워드 영역 */} + <div + style={{ + padding: "15px 0px 15px 0px", + borderBottom: "1px solid #c4bebe", + }} + > + <ul style={{ display: "flex" }}> + <SearchKeyword> + {/* <div>저주</div> */} + 저주 + <button + style={{ + backgroundColor: "#c8edf2", + borderRadius: "999px", + lineHeight: "5px", + }} + > + <img src={closeIcon} alt="" /> + </button> + </SearchKeyword> + </ul> + </div> + + {/* 콘텐츠 영역 */} + <div> + {viewMode ? ( + <ListUIWrap> + {/* {books.map((book, index) => ( */} + {currentBooks.map((book, index) => ( + <Link to={`/book-detail/${book.title}`}> + <li + key={index} + style={{ + height: "310px", + display: "flex", + padding: "36px 20px", + borderBottom: "1px solid #A1A1A1", + }} + > + <img + src={book.imageUrl} + alt="책 표지 이미지" + style={{ + height: "240px", + width: "170px", + backgroundColor: "gray", + borderRadius: "5px", + // objectFit: "cover", + border: "1px solid #c0c0c0", + }} + /> + <div style={{ padding: "1rem 0px 0px 1rem" }}> + <div> + <h3 + style={{ + fontSize: "20px", + fontWeight: "bold", + marginBottom: "8px", + }} + > + {book.title} + </h3> + <h4 + style={{ + fontSize: "1rem", + color: "gray", + marginBottom: "40px", + }} + > + {book.author} + </h4> + </div> + <div style={{ marginBottom: "40px" }}> + <ul style={{ display: "flex", flexWrap: "wrap" }}> + <BookKeyword + onClick={(e) => { + e.preventDefault(); + navigate( + `/search/result/${book.middleCategoryName}` + ); + }} + > + {book.middleCategoryName} + </BookKeyword> + {book.bookKeywordList.map((keyword, index) => { + return ( + <BookKeyword + key={index} + onClick={(e) => { + e.preventDefault(); + navigate( + `/search/result/${keyword.name}` + ); + }} + > + {keyword.name} + </BookKeyword> + ); + })} + </ul> + </div> + <h1 + style={{ + fontWeight: "bold", + marginBottom: "10px", + }} + > + 평균 ★{book.rating}{" "} + <span style={{ color: "gray" }}> + ({reviewCount}) + </span> + </h1> + <div + style={{ display: "flex", alignItems: "center" }} + > + <img + src={starIcon} + alt="" + style={{ marginRight: "5px" }} + /> + <img + src={starIcon} + alt="" + style={{ marginRight: "5px" }} + /> + <img + src={starIcon} + alt="" + style={{ marginRight: "5px" }} + /> + <img + src={starIcon} + alt="" + style={{ marginRight: "5px" }} + /> + <img + src={starIcon} + alt="" + style={{ marginRight: "5px" }} + /> + </div> + </div> + </li> + </Link> + ))} + </ListUIWrap> + ) : ( + <CardUIWrap> + {/* {books.map((book, index) => ( */} + {currentBooks.map((book, index) => ( + <Link to={`/book-detail/${book.title}`}> + <li key={index} style={{ width: "170px" }}> + <div + style={{ + height: "240px", + width: "170px", + borderRadius: "5px", + backgroundColor: "gray", + marginBottom: "10px", + display: "inline-block", + }} + > + <img + src={book.imageUrl} + alt="책 표지 이미지" + style={{ + width: "100%", + height: "100%", + // objectFit: "cover", + border: "1px solid #c0c0c0", + }} + /> + </div> + <BookTitle>{book.title}</BookTitle> + <h3 style={{ color: "#6B6B6B", marginBottom: "10px" }}> + {book.author} + </h3> + <h1 style={{ marginBottom: "10px" }}> + 평균 ★{book.rating}{" "} + <span style={{ color: "gray" }}>({reviewCount})</span> + </h1> + <div + style={{ + display: "flex", + alignItems: "center", + marginBottom: "40px", + }} + > + <img + src={starIcon} + alt="" + style={{ + width: "20px", + marginRight: "5px", + }} + /> + <img + src={starIcon} + alt="" + style={{ width: "20px", marginRight: "5px" }} + /> + <img + src={starIcon} + alt="" + style={{ width: "20px", marginRight: "5px" }} + /> + <img + src={starIcon} + alt="" + style={{ width: "20px", marginRight: "5px" }} + /> + <img + src={starIcon} + alt="" + style={{ width: "20px", marginRight: "5px" }} + /> + </div> + + {/* 별 컴포넌트 */} + </li> + </Link> + ))} + </CardUIWrap> + )} + </div> + <Pagination + booksPerPage={booksPerPage} + totalBooks={books.length} + paginate={paginate} + bookTitle={title} + /> + </section> + </ContentsWrap> + </Main> + </> + ); +}; + +export default SearchResult; + +const Main = styled.main` + padding: 48px 52px 0px 52px; + max-width: 1440px; + margin: 0 auto; +`; + +const SearchInputWrap = styled.div` + width: 100%; + height: 60px; + box-shadow: 0px 2px 4px #00000033; + padding: 10px 0px 10px 1rem; + border-radius: 5px; + border: 1px solid #b0b0b0; + display: flex; + align-items: center; + font-size: 20px; + margin-bottom: 40px; +`; + +const SelectMenu = styled.select` + width: 140px; + font-size: 20px; + border: none; + /* padding-left: 10px; */ + text-align: center; + &:focus { + outline: none; + } +`; + +const SearchInput = styled.input` + border: none; + border-left: 1px solid #c0c0c0; + margin-left: 1rem; + padding-left: 1rem; + font-size: 20px; + width: 100%; + &:focus { + outline: none; + } +`; + +const ContentsWrap = styled.div` + display: flex; +`; + +const ContentTitle = styled.h2` + font-size: 20px; + font-weight: bold; +`; + +const SearchKeyword = styled.li` + display: flex; + align-items: center; + justify-content: space-between; + height: 29px; + line-height: 27px; + padding: 0px 0px 0px 12px; + background-color: #c8edf2; + border-radius: 15px; + margin-right: 10px; +`; + +const BookKeyword = styled.li` + display: flex; + align-items: center; + justify-content: space-between; + height: 29px; + padding: 0px 12px; + background-color: #c8edf2; + border-radius: 15px; + margin-right: 10px; +`; + +const ListUIWrap = styled.ul``; + +const CardUIWrap = styled.ul` + padding: 36px 20px; + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-gap: 10px; +`; + +const BookTitle = styled.h2` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; /* 원하는 라인 수 */ + overflow: hidden; + text-overflow: ellipsis; + font-size: 20px; + font-weight: bold; + margin-bottom: 5px; +`; diff --git a/src/pages/Signup2.jsx b/src/pages/Signup2.jsx index 6671eae..145b884 100644 --- a/src/pages/Signup2.jsx +++ b/src/pages/Signup2.jsx @@ -2,6 +2,9 @@ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { useForm, rules } from 'react-hook-form'; +// SERVICE +import api from '../services/api'; + // ASSETS import banner from '../assets/Login-Banner.png'; @@ -274,6 +277,9 @@ const Signup2 = () => { const [emailDomain, setEmailDomain] = useState(''); const [isInputEnabled, setIsInputEnabled] = useState(false); + // 직업 정보 + const [job, setJob] = useState(''); + // 모든 것을 작성해야 가입하기 버튼 클릭 활성화 const [isButtonEnabled, setIsButtonEnabled] = useState(true); @@ -323,6 +329,11 @@ const Signup2 = () => { setBirthDate(e.target.value); }; + const handleJobChange = (e) => { + // console.log(e.target.value); + setJob(e.target.value); + }; + const handleGenderButtonClick = (gender) => { if (gender === 'male') { setIsMaleClicked(true); @@ -358,7 +369,27 @@ const Signup2 = () => { // setEmailDomain이 변경될 때마다 실행 setEmail(`${emailUsername}@${emailDomain}`); }, [emailUsername, emailDomain]); - console.log(emailDomain); + // console.log(emailDomain); + + const signUpData = { + username: id, + password: pwd, + name: name, + nickname: nickname, + email: email, + gender: gender, + occupation: job, + }; + + const postSignup = () => { + api + .post('/signup', signUpData, { + withCredentials: true, + }) + .then((res) => { + console.log(res.data); + }); + }; return ( <LoginContainer> @@ -541,7 +572,7 @@ const Signup2 = () => { <InputWrap> <p>직업 선택</p> - <JobSelect name="" id=""> + <JobSelect name="job" id="" onChange={handleJobChange}> <option value="0" selected> 선택 없음 </option> @@ -554,8 +585,8 @@ const Signup2 = () => { </JobSelect> </InputWrap> - <LoginButton isButtonEnabled={isButtonEnabled}> - <Link to={`/signup/2`}>네, 동의합니다</Link> + <LoginButton isButtonEnabled={isButtonEnabled} onClick={postSignup}> + {/* <Link to={`/signup/2`}>네, 동의합니다</Link> */} </LoginButton> </LoginContent> </LoginContainer> diff --git a/src/pages/SmallCategory.jsx b/src/pages/SmallCategory.jsx index 0d06234..464322c 100644 --- a/src/pages/SmallCategory.jsx +++ b/src/pages/SmallCategory.jsx @@ -1,11 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { useParams, useSearchParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import axios from 'axios'; // COMPONENTS import Header from '../components/Header'; -import Footer from '../components/Footer'; -import Btn from '../components/Button'; import Card from '../components/BookDetailCard'; // STYLES @@ -28,12 +26,9 @@ const SmallCategory = () => { ) .then((res) => { setSmCategoryBookList(res.data); - console.log(res.data); }); }, []); - const [searchParams, setSearchParams] = useSearchParams(); - return ( <> <Header /> @@ -48,33 +43,18 @@ const SmallCategory = () => { <div className="smCategory_card_wrapper"> <div className="smCategory_card_slide"> {smCategoryBookList.map((data) => { - // console.log(data); return ( <Card + isbn={data.isbn} title={data.title} author={data.author} key={data.isbn} imageUrl={data.imageUrl} - // onClick={() => { - // setSearchParams({ - // title: data[title], - // author: data[author], - // }); - // }} + bookKeywordList={data.bookKeywordList} /> ); })} </div> - {/* <div className="smCategory_card_slide"> - <Card /> - <Card /> - <Card /> - </div> - <div className="smCategory_card_slide"> - <Card /> - <Card /> - <Card /> - </div> */} </div> </div> </> diff --git a/src/services/books.js b/src/services/books.js new file mode 100644 index 0000000..b3a770c --- /dev/null +++ b/src/services/books.js @@ -0,0 +1,7 @@ +import React from 'react' + +export const books = (page, limit) => { + let array = []; + + return +} diff --git a/src/services/login.js b/src/services/login.js index 1c46c06..830f7ed 100644 --- a/src/services/login.js +++ b/src/services/login.js @@ -4,7 +4,7 @@ export const login = async (username, password) => { const result = await axios.post( "https://port-0-backend-book-pharmacy-umnqdut2blqqhv7sd.sel5.cloudtype.app/login", { username, password }, // 키와 값이 같아서 생략함 - { withCredentials: true } + { withCredentials: true } // 쿠키를 브라우저에 자동 저장 및 보내기 ); console.log("result", result); return result; diff --git a/src/styles/ArrowTitle.module.css b/src/styles/ArrowTitle.module.css index dc344ef..f16f105 100644 --- a/src/styles/ArrowTitle.module.css +++ b/src/styles/ArrowTitle.module.css @@ -33,7 +33,7 @@ } .default_wrapper { - width: 1338px; + width: 100%; height: 55px; display: flex; justify-content: space-between; diff --git a/src/styles/BookCard.module.css b/src/styles/BookCard.module.css index 39cdba0..4d55c2d 100644 --- a/src/styles/BookCard.module.css +++ b/src/styles/BookCard.module.css @@ -12,6 +12,7 @@ display: flex; flex-direction: column; align-items: center; + cursor: pointer; } .book_img { @@ -21,6 +22,11 @@ background: #d9d9d9; } +.book_thumbnail { + width: 148px; + height: 210px; +} + .book_info_wrapper { width: 148px; display: flex; diff --git a/src/styles/BookDetailCard.css b/src/styles/BookDetailCard.css index c15183b..0e45a7e 100644 --- a/src/styles/BookDetailCard.css +++ b/src/styles/BookDetailCard.css @@ -84,3 +84,88 @@ padding-top: 60px; overflow: auto; } + +/* 경험 추가하기 모달창 */ + +.expModal_bookCard_container { + width: 639px; + height: 200px; + background: #ffffff; + margin: 10px; + + /* Drop Shadow */ + /* box-shadow: 6px 6px 30px rgba(119, 119, 119, 0.466667); */ + box-shadow: 0px 1px 13px rgba(119, 119, 119, 0.466667); + border-radius: 15px; +} + +/* .expModal_bookCard_container:hover { + background: #a4d6dd; +} */ + +.expModal_bookCard_wrapper { + display: flex; + align-items: center; + padding: 14px 10px; +} + +.expModal_bookCard_left_wrapper { + padding-left: 5px; +} + +.expModal_left_img_wrapper { + height: 170px; + box-shadow: 2px 2px 4px rgba(119, 119, 119, 0.25); + border-radius: 5px; +} + +.expModal_img_wrapper_thumbnail { + width: 120px; + height: 170px; + border-radius: 5px; +} + +.expModal_bookCard_right_wrapper { + height: 170px; + display: flex; + flex-direction: column; + margin-left: 20px; +} + +.expModal_bookCard_right_up_wrapper { + display: flex; + flex-direction: column; +} + +.expModal_right_up_title { + width: 260px; + + /* Font */ + font-family: var(--basic-font); + font-style: normal; + font-weight: 700; + font-size: 18px; + line-height: 21px; + color: #000; + + /* 말 줄임 */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.expModal_right_up_author { + font-family: var(--basic-font); + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 29px; + color: #868686; +} + +.expModal_bookCard_right_bottom_wrapper { + width: 260px; + height: 130px; + padding-top: 60px; + overflow: auto; +} diff --git a/src/styles/BookList.css b/src/styles/BookList.css index 5ab75da..d40d3d0 100644 --- a/src/styles/BookList.css +++ b/src/styles/BookList.css @@ -32,3 +32,11 @@ width: 1338px; margin: 0 auto; } + +.bookList_title_wrapper { + margin-top: 30px; +} + +.bookList_slide_wrapper { + /* overflow-x: scroll; */ +} diff --git a/src/styles/BookListSlide.module.css b/src/styles/BookListSlide.module.css index a81bbdb..bf38d89 100644 --- a/src/styles/BookListSlide.module.css +++ b/src/styles/BookListSlide.module.css @@ -8,14 +8,25 @@ .container { padding: 0; - margin-top: 56px; - width: 1338px; + margin-top: 16px; + width: 200px; font-family: 'Pretendard-Regular'; font-weight: 700; + /* display: grid; + grid-template-columns: repeat(8, 1fr); */ + width: 100%; } .slide { - display: flex; justify-content: space-around; margin-top: 20px; + width: 500px; +} + +.swiper-container .swiper { + padding: 0; +} + +#my-swiper .swiper { + padding-left: 0; } diff --git a/src/styles/Experience.css b/src/styles/Experience.css new file mode 100644 index 0000000..bede7c7 --- /dev/null +++ b/src/styles/Experience.css @@ -0,0 +1,141 @@ +.expModal_overlay { + position: fixed; + width: 100%; + height: 100%; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.2); + z-index: 9999; +} + +.expModal_wrapper { + width: 690px; + height: 600px; + display: flex; + flex-direction: column; + align-items: center; + padding: 30px 30px; + border-radius: 15px; + background-color: #fff; + position: absolute; + top: 53%; + left: 50%; + transform: translate(-50%, -50%); +} + +.expModal_title_wrapper { + display: flex; + width: 630px; + justify-content: space-between; + + /* Font */ + font-family: var(--basic-font); + font-style: normal; + font-weight: 600; + font-size: 24px; + line-height: 0px; +} + +.expModal_title_wrapper .close_btn { + cursor: pointer; + + /* Font */ + font-family: var(--basic-font); + font-style: normal; + font-weight: 200; + font-size: 24px; + line-height: 0px; + color: #575757; +} + +.expModal_content_wrapper { + display: flex; + flex-direction: column; + align-items: center; + width: 660px; + height: 570px; + margin-top: 20px; +} + +.bookInfo_content_wrapper { + width: 100%; +} + +.inputReview_wrapper { + display: flex; + flex-direction: column; + margin-top: 35px; +} + +.inputReview_title { + /* Font */ + font-family: var(--basic-font); + font-style: normal; + font-weight: 600; + font-size: 18px; + line-height: 21px; +} + +.inputReview_box { + width: 640px; + height: 125px; + border: 1px solid #868686; + border-radius: 5px; + margin-top: 15px; + padding: 10px 10px; + resize: none; + overflow: auto; + + /* Font */ + font-family: var(--basic-font); + font-style: normal; + font-weight: 700; + font-size: 14px; + line-height: 1.5em; +} + +.inputReview_box::-webkit-scrollbar { + width: 10px; +} + +.inputReview_box::-webkit-scrollbar-button { + display: none; +} + +.inputReview_box::-webkit-scrollbar-thumb { + background-color: #67b6c1; + border-radius: 20px; + background-clip: padding-box; + border: 2px solid transparent; +} + +.inputReview_box::-webkit-scrollbar-track { + background-color: #fff; + border-radius: 20px; + box-shadow: inset 0px 0px 5px #fff; +} + +.expModal_button_wrapper { + display: flex; + justify-content: space-around; + width: 100%; + margin-top: 40px; +} + +.review_btn, +.later_btn { + width: 300px; + height: 60px; + background-color: #c8edf2; + border-radius: 10px; + + /* Font */ + text-align: center; + font-family: var(--basic-font); + font-size: 20px; + font-weight: 600; + font-style: normal; + line-height: 0px; +} diff --git a/src/styles/Feed.css b/src/styles/Feed.css index 175c5c7..d1a024d 100644 --- a/src/styles/Feed.css +++ b/src/styles/Feed.css @@ -10,20 +10,20 @@ margin: 0 auto; max-width: 1440px; padding: 24px; - height: 1000vh; + /* height: 108vh; */ display: flex; flex-direction: column; } .feed_title_wrapper { - width: auto; + width: 100%; height: 50px; padding: 10px; } .feed_title { color: #000; - font-family: 'Pretendard-Regular'; + font-family: var(--basic-font); font-size: 24px; font-style: normal; font-weight: 700; @@ -31,8 +31,10 @@ } .feed_content_wrapper { - max-width: 1440px; - height: 500px; + display: grid; + grid-template-columns: repeat(4, 1fr); + width: 1440px; + /* height: 860px; */ -ms-flex-align: center; align-items: center; margin-left: auto; diff --git a/src/styles/FeedCard.css b/src/styles/FeedCard.css index a7d612b..ed0da25 100644 --- a/src/styles/FeedCard.css +++ b/src/styles/FeedCard.css @@ -9,7 +9,7 @@ .FeedCardContainer { position: static; box-sizing: border-box; - width: 310px; + width: 340px; height: 280px; /* border-radius: 10px; background: linear-gradient(111deg, #cafadf 0%, #d5e0ff 100%); @@ -20,17 +20,17 @@ /* Drop Shadow */ box-shadow: 2px 2px 4px 0px rgba(119, 119, 119, 0.25); + margin-top: 20px; } .feed_up_wrapper { display: flex; - width: 310px; + width: 340px; height: 240px; } .feed_review_wrapper { display: flex; - flex-direction: column; align-items: center; } @@ -55,7 +55,7 @@ .review_book_wrapper { position: absolute; display: flex; - width: 310px; + width: 340px; height: 115px; background: rgba(255, 255, 255, 0.35); margin-top: 160px; @@ -64,12 +64,19 @@ z-index: 1; } +.feed_book_img_wrapper { + width: 74px; + height: 106px; + background: #6f6f6f; + border-radius: 5px; +} + .feed_book_img { width: 74px; height: 106px; flex-shrink: 0; border-radius: 5px; - background: #6f6f6f; + object-fit: fill; } .feed_book_text_wrapper { @@ -166,7 +173,7 @@ position: absolute; z-index: 2; display: flex; - width: 311px; + width: 341px; height: 40px; padding: 4px 0; align-items: center; diff --git a/src/styles/HomeStyles.css b/src/styles/HomeStyles.css index 001bef6..f4e7b8a 100644 --- a/src/styles/HomeStyles.css +++ b/src/styles/HomeStyles.css @@ -16,7 +16,7 @@ width: 100%; z-index: 100; background-color: hsla(0, 0%, 100%, 0.9); - /* top: 0; */ + top: 0; } .home_header { diff --git a/src/styles/SearchResultList.css b/src/styles/SearchResultList.css index 9380bf1..a0f8d59 100644 --- a/src/styles/SearchResultList.css +++ b/src/styles/SearchResultList.css @@ -6,7 +6,7 @@ box-sizing: border-box; border-radius: 16px; width: 1440px; - padding: 29px 29px 39px; + padding: 29px 29px; /* border: 1px solid #000; */ margin-top: 10px; background-color: #fff; @@ -25,6 +25,8 @@ } .search-result-list img { + width: 70px; + background-color: rgb(192, 192, 192); border: 1px solid #d6d6d6; } diff --git a/src/styles/SearchStyles.css b/src/styles/SearchStyles.css index 2f8aff7..428c1bd 100644 --- a/src/styles/SearchStyles.css +++ b/src/styles/SearchStyles.css @@ -26,11 +26,20 @@ border-radius: 0.5rem; } -.search-button { +/* .search-button { width: 32px; height: 32px; background: url("../assets/search-icon.svg"); margin-top: 4px; +} */ + +.search-select { + border: none; + font-size: large; +} + +.search-select:focus { + outline: none; } .search-input { @@ -49,8 +58,8 @@ width: 50px; height: 40px; font-size: 1.5rem; - position: relative; - right: 0; + position: absolute; + right: 70px; background-color: #fff; } diff --git a/src/styles/Slider.css b/src/styles/Slider.css index a603afd..c98ad52 100644 --- a/src/styles/Slider.css +++ b/src/styles/Slider.css @@ -39,27 +39,27 @@ /* background-color: aqua; */ } -.swiper-button-prev, -.swiper-button-next { +#slider-swiper .swiper-button-prev, +#slider-swiper .swiper-button-next { color: #575757 !important; } -.swiper-button-prev:after, -.swiper-button-next:after { +#slider-swiper .swiper-button-prev:after, +#slider-swiper .swiper-button-next:after { font-size: 2rem !important; font-weight: 1000 !important; margin-bottom: 40px; } -.swiper-button-prev:after { +#slider-swiper .swiper-button-prev:after { margin-left: -25px; } -.swiper-button-next:after { +#slider-swiper .swiper-button-next:after { margin-right: -20px; } -.swiper { +#slider-swiper .swiper { padding-left: 30px; cursor: grab; -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index d073109..77ec5ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3543,6 +3543,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== +classnames@^2.2.5: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^5.2.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -4408,6 +4413,11 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enquire.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/enquire.js/-/enquire.js-2.1.6.tgz#3e8780c9b8b835084c3f60e166dbc3c2a3c89814" + integrity sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw== + entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" @@ -6758,6 +6768,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" + integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA== + dependencies: + string-convert "^0.2.0" + json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -8599,6 +8616,17 @@ react-scripts@5.0.1: optionalDependencies: fsevents "^2.3.2" +react-slick@^0.30.2: + version "0.30.2" + resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.30.2.tgz#b28e992f9c519bb516a0af8d37e82cb59fee08ce" + integrity sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg== + dependencies: + classnames "^2.2.5" + enquire.js "^2.1.6" + json2mq "^0.2.0" + lodash.debounce "^4.0.8" + resize-observer-polyfill "^1.5.0" + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -8768,6 +8796,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -9156,6 +9189,11 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +slick-carousel@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/slick-carousel/-/slick-carousel-1.8.1.tgz#a4bfb29014887bb66ce528b90bd0cda262cc8f8d" + integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA== + sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" @@ -9296,6 +9334,11 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" + integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"