From dcfa86fc3f87f4f4d1579d46a426f212ac478077 Mon Sep 17 00:00:00 2001 From: jihwooon Date: Wed, 6 Mar 2024 18:19:12 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EB=8F=84=EC=84=9C=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B0=8F=20=EC=84=9C=EB=B2=84=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 변경 사항: - 도서 상세 페이지에 이미지를 표시하도록 기능을 추가했습니다. - 서버 도메인 모델 Book 클래스에 imgId 속성을 추가했습니다. - findWithCategory 함수를 수정하여 이미지 ID 정보를 함께 조회하도록 했습니다. - 개발 서버 실행 스크립트를 tsx watch src/index.ts 에서 tsx watch -r dotenv/config src/index.ts 로 변경했습니다. 이는 .env 파일 변경 시에도 자동으로 서버를 다시 시작하도록 하기 위함입니다. 관련 변경 사항: - server/package.json 파일 수정 - server/src/books/domain/books.repository.ts 파일 수정 --- server/package.json | 2 +- server/src/books/domain/books.repository.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/package.json b/server/package.json index d4d2870..7f4d5ca 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ "test": "jest --runInBand --detectOpenHandles --forceExit", "test:only-changed": "jest --runInBand --onlyChanged", "test:watch": "jest --watch --runInBand --detectOpenHandles --forceExit", - "start": "tsx watch src/index.ts" + "start": "tsx watch -r dotenv/config src/index.ts" }, "keywords": [], "author": "", diff --git a/server/src/books/domain/books.repository.ts b/server/src/books/domain/books.repository.ts index 7e18ab4..d469e53 100644 --- a/server/src/books/domain/books.repository.ts +++ b/server/src/books/domain/books.repository.ts @@ -56,7 +56,7 @@ export const findAll = async ( export const findWithCategory = async (userId: number, bookId: number): Promise => { const [rows] = await doQuery((connection) => connection.execute( - `SELECT b.id, b.title, b.category_id, c.name as category_name, b.form, b.isbn, b.summary, b.detail, b.author, b.pages, b.contents, b.price, b.pub_date, + `SELECT b.id, b.title, b.category_id, c.name as category_name, b.form, b.isbn, b.summary, b.detail, b.author, b.pages, b.contents, b.price, b.pub_date, b.img_id, (SELECT count(*) FROM likes WHERE b.id = liked_book_id) as Likes, (SELECT EXISTS(SELECT * FROM likes WHERE user_id = ? AND liked_book_id)) as liked FROM books b @@ -88,6 +88,7 @@ export const findWithCategory = async (userId: number, bookId: number): Promise< price: row.price, likes: row.likes, pubDate: row.pub_date, + imgId: row.img_id, }); }; From 649f37b81b8e72f979162316bc85a3ce85f8e88f Mon Sep 17 00:00:00 2001 From: jihwooon Date: Wed, 6 Mar 2024 19:28:50 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=8F=84=EC=84=9C=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20?= =?UTF-8?q?UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 변경 사항 - 도서 상세 페이지에 상세 설명과 목차 정보를 추가했습니다. - 상세 설명은 텍스트 넘치는 경우 ellipsis를 활용하여 표시하도록 했습니다. - EllipsisBox 컴포넌트를 추가하여 재사용 가능한 텍스트 넘치기 컴포넌트를 구현했습니다. 관련 변경 사항 - client/src/components/common/EllipisisBox.tsx 파일 생성 (새 파일) - client/src/pages/BookDetail.tsx 파일 수정 --- client/src/components/common/EllipisisBox.tsx | 58 +++++ client/src/pages/BookDetail.tsx | 216 ++++++++++-------- 2 files changed, 174 insertions(+), 100 deletions(-) create mode 100644 client/src/components/common/EllipisisBox.tsx diff --git a/client/src/components/common/EllipisisBox.tsx b/client/src/components/common/EllipisisBox.tsx new file mode 100644 index 0000000..11e945b --- /dev/null +++ b/client/src/components/common/EllipisisBox.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import { FaAngleDown } from "react-icons/fa"; +import { styled } from "styled-components"; +import Button from "./Button"; + +interface Prpos { + children: React.ReactNode; + linelimit: number; +} + +const EllipsisBox = ({ children, linelimit }: Prpos) => { + const [expanded, setExpanded] = useState(false); + + return ( + +

{children}

+
+ +
+
+ ); +}; + +interface EllipsisBoxStyleProps { + linelimit: number; + $expanded: boolean; +} + +const EllipsisBoxStyle = styled.div` + p { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: ${({ linelimit, $expanded }) => ($expanded ? "none" : linelimit)}; + - webkit - box - orient : vertical; + padding: 20px 0 0 0; + margin: 0; + } + + .toggle { + display: flex; + justify-content: end; + } + + svg { + transform: ${({ $expanded }) => ($expanded ? "rotate(180deg)" : "rotate(0)")} + } +`; + +export default EllipsisBox; diff --git a/client/src/pages/BookDetail.tsx b/client/src/pages/BookDetail.tsx index 1bce565..7220e7b 100644 --- a/client/src/pages/BookDetail.tsx +++ b/client/src/pages/BookDetail.tsx @@ -1,5 +1,6 @@ import { Link, useParams } from "react-router-dom"; import styled from "styled-components"; +import EllipsisBox from "../components/common/EllipisisBox"; import Title from "../components/common/Title"; import { useBook } from "../hooks/useBook"; import { BookDetail as IBookDetail } from "../models/book.model"; @@ -7,117 +8,132 @@ import { formatDate, formatNumber } from "../utils/format"; import { getImgSrc } from "../utils/image"; const bookInfoList = [ - { - label: "카테고리", - key: "categoryName", - filter: (book: IBookDetail) => ( - - {book.categoryName} - - ) - }, - { - label: "포맷", - key: "form" - }, - { - label: "페이지", - key: "pages" - }, - { - label: "ISBN", - key: "isbn" - }, - { - label: "출간일", - key: "pubDate", - filter: (book: IBookDetail) => { - return formatDate(book.pubDate) - } - }, - { - label: "가격", - key: "price", - filter: (book: IBookDetail) => { - return `${formatNumber(book.price)} 원` - } - } -] + { + label: "카테고리", + key: "categoryName", + filter: (book: IBookDetail) => ( + + {book.categoryName} + + ), + }, + { + label: "포맷", + key: "form", + }, + { + label: "페이지", + key: "pages", + }, + { + label: "ISBN", + key: "isbn", + }, + { + label: "출간일", + key: "pubDate", + filter: (book: IBookDetail) => { + return formatDate(book.pubDate); + }, + }, + { + label: "가격", + key: "price", + filter: (book: IBookDetail) => { + return `${formatNumber(book.price)} 원`; + }, + }, +]; const BookDetail = () => { - const { bookId } = useParams(); - const { book } = useBook(bookId); + const { bookId } = useParams(); + const { book } = useBook(bookId); - if (!book) return null; + if (!book) return null; - return ( - -
-
- {book.title}/ -
-
- - {book.title} - - { - bookInfoList.map((item) => ( -
-
{item.label}
-
{item.filter ? item.filter(book) : book - [item.key as keyof IBookDetail]}
-
- )) - } -

{book.summary}

+ return ( + +
+
+ {book.title} +
+
+ + {book.title} + + {bookInfoList.map((item) => ( +
+
{item.label}
+
+ {item.filter + ? item.filter(book) + : book[item.key as keyof IBookDetail]} +
+
+ ))} +

{book.summary}

-
- Like -
+
Like
-
- Cart -
-
-
-
- ) -} +
Cart
+
+
+
+ 상세 설명 + {book.detail} + + 목차 +

{book.contents}

+
+
+ ); +}; const BookDetailStyle = styled.div` .header { - display: flex; - align-items: start; - gap: 24px; - padding: 0 0 24px 0; + display: flex; + align-items: start; + gap: 24px; + padding: 0 0 24px 0; + + .img { + flex: 1; + img { + width: 100%; + height: auto; + } + } - .img { - flex: 1; - img { - width: 100%; - height: auto; - } - } + .info { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; - .info { - flex: 1; - display: flex; - flex-direction: column; - gap: 12px; + dl { + display: flex; + margin: 0; + dt { + width: 80px; + color: ${({ theme }) => theme.color.secondary}; + } + a { + color: ${({ theme }) => theme.color.primary}; + } + } - dl { - display: flex; - margin: 0; - dt { - width: 80px; - color: ${({ theme }) => theme.color.secondary} - } - a { - color: ${({ theme }) => theme.color.primary} - } - } - } - } -` + .content { + .detail { + height: 200px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + } + } + } + } +`; export default BookDetail;