diff --git a/client/src/api/books.api.ts b/client/src/api/books.api.ts index fdf3b82..ece9433 100644 --- a/client/src/api/books.api.ts +++ b/client/src/api/books.api.ts @@ -36,3 +36,16 @@ export const fetchBook = async (bookId: string) => { return response.data; } + +export const likeBook = async (bookId: number) => { + const response = await httpClient.post(`/likes/${bookId}`) + + return response.data; + +} + +export const unlikeBook = async (bookId: number) => { + const response = await httpClient.delete(`/likes/${bookId}`) + + return response.data; +} \ No newline at end of file diff --git a/client/src/components/book/LikeButton.tsx b/client/src/components/book/LikeButton.tsx new file mode 100644 index 0000000..76ab422 --- /dev/null +++ b/client/src/components/book/LikeButton.tsx @@ -0,0 +1,34 @@ +import { FaHeart } from "react-icons/fa"; +import styled from "styled-components"; +import { BookDetail } from "../../models/book.model"; +import Button from "../common/Button"; + +interface Props { + book: BookDetail; + onClick: () => void; +} + +const LikeButton = ({book, onClick}: Props) => { + return ( + + + + ); +} + +const LikeButtonStyle = styled.div` + display: flex; + gap: 6px; + + svg { + color: inherit; + * { + color: inherit; + } + } +`; + +export default LikeButton; diff --git a/client/src/components/common/Header.tsx b/client/src/components/common/Header.tsx index 606414c..e3ab2de 100644 --- a/client/src/components/common/Header.tsx +++ b/client/src/components/common/Header.tsx @@ -1,13 +1,13 @@ -import styled from "styled-components"; -import { FaSignInAlt, FaRegUser } from "react-icons/fa"; -import logo from '../../assets/images/logo.png' +import { FaRegUser, FaSignInAlt } from "react-icons/fa"; import { Link } from "react-router-dom"; +import styled from "styled-components"; +import logo from '../../assets/images/logo.png'; import { useCategory } from "../../hooks/useCategory"; import { useAuthStore } from "../../store/authStore"; const Header = () => { const { category } = useCategory(); - const { isloggedIn, storeLogout } = useAuthStore(); + const { isLoggedIn: isloggedIn, storeLogout } = useAuthStore(); return ( @@ -60,7 +60,7 @@ const HeaderStyle = styled.header` width: 100%; margin: 0 auto; max-width: ${({ theme }) => theme.layout.width.large}; - + display: flex; justify-content: space-between; padding: 20px 0; diff --git a/client/src/hooks/useBook.ts b/client/src/hooks/useBook.ts index 06f466a..88fd7db 100644 --- a/client/src/hooks/useBook.ts +++ b/client/src/hooks/useBook.ts @@ -1,9 +1,40 @@ import { useEffect, useState } from "react"; -import { fetchBook } from "../api/books.api"; +import { fetchBook, likeBook, unlikeBook } from "../api/books.api"; import { BookDetail } from "../models/book.model"; +import { useAuthStore } from "../store/authStore"; +import { useAlert } from "./useAlert"; export const useBook = (bookId: string | undefined) => { const [book, setBook] = useState(null); + const { isLoggedIn } = useAuthStore(); + const showAlert = useAlert(); + + const likeToggle = () => { + if (!isLoggedIn) { + showAlert('로그인이 필요합니다.') + return; + } + + if (!book) return; + + if (book.liked) { + unlikeBook(book.id).then(() => { + setBook({ + ...book, + liked: false, + likes: book.likes - 1, + }) + }) + } else { + likeBook(book.id).then(() => { + setBook({ + ...book, + liked: true, + likes: book.likes + 1, + }) + }) + } + } useEffect(() => { if (!bookId) return; @@ -13,5 +44,5 @@ export const useBook = (bookId: string | undefined) => { }) }, [bookId]) - return { book } + return { book, likeToggle } } diff --git a/client/src/pages/BookDetail.tsx b/client/src/pages/BookDetail.tsx index 7220e7b..95fbd19 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 LikeButton from "../components/book/LikeButton"; import EllipsisBox from "../components/common/EllipisisBox"; import Title from "../components/common/Title"; import { useBook } from "../hooks/useBook"; @@ -47,7 +48,7 @@ const bookInfoList = [ const BookDetail = () => { const { bookId } = useParams(); - const { book } = useBook(bookId); + const { book, likeToggle } = useBook(bookId); if (!book) return null; @@ -73,8 +74,9 @@ const BookDetail = () => { ))}

{book.summary}

-
Like
- +
+ +
Cart
diff --git a/client/src/pages/Signin.tsx b/client/src/pages/Signin.tsx index a0d9a21..5d9dc04 100644 --- a/client/src/pages/Signin.tsx +++ b/client/src/pages/Signin.tsx @@ -1,12 +1,12 @@ import { useForm } from 'react-hook-form' -import Title from "../components/common/Title" -import { InputText } from "../components/common/InputText" -import Button from "../components/common/Button" import { Link, useNavigate } from "react-router-dom" -import { useAlert } from '../hooks/useAlert' -import { SingupStyle } from './Signup' import { signin } from '../api/auth.api' +import Button from "../components/common/Button" +import { InputText } from "../components/common/InputText" +import Title from "../components/common/Title" +import { useAlert } from '../hooks/useAlert' import { useAuthStore } from '../store/authStore' +import { SingupStyle } from './Signup' export interface SigninProps { email: string; @@ -18,8 +18,8 @@ const Signin = () => { const showAlert = useAlert(); const { register, handleSubmit, formState: { errors } } = useForm(); - const { isloggedIn, storeLogin } = useAuthStore(); - + const { isLoggedIn: isloggedIn, storeLogin } = useAuthStore(); + const onSubmit = (data: SigninProps) => { signin(data).then((res) => { storeLogin(res.data) @@ -36,16 +36,16 @@ const Signin = () => {
- {errors.email &&

이메일을 입력해주세요.

}
- diff --git a/client/src/store/authStore.ts b/client/src/store/authStore.ts index 3a611a1..08538f4 100644 --- a/client/src/store/authStore.ts +++ b/client/src/store/authStore.ts @@ -1,9 +1,9 @@ import { create } from "zustand"; interface StoreState { - isloggedIn: boolean - storeLogin: (token: string) => void - storeLogout: () => void; + isLoggedIn: boolean + storeLogin: (token: string) => void + storeLogout: () => void; } export const getToken = () => { @@ -20,13 +20,14 @@ export const removeToken = () => { } export const useAuthStore = create((set) => ({ - isloggedIn: getToken() ? true : false, - storeLogin: (token: string) => { - set({ isloggedIn: true }) - setToken(token); - }, - storeLogout: () => { - set({ isloggedIn: false }) - removeToken() - } + isLoggedIn: getToken() ? true : false, + + storeLogin: (token: string) => { + set({ isLoggedIn: true }) + setToken(token); + }, + storeLogout: () => { + set({ isLoggedIn: false }) + removeToken() + } })) diff --git a/client/src/style/theme.ts b/client/src/style/theme.ts index 3fde17b..419e89b 100644 --- a/client/src/style/theme.ts +++ b/client/src/style/theme.ts @@ -8,7 +8,7 @@ export type ColorKey = | "text"; export type HeadingSize = "large" | "medium" | "small"; export type ButtonSize = "large" | "medium" | "small"; -export type ButtonScheme = "primary" | "normal"; +export type ButtonScheme = "primary" | "normal" | "like"; export type LayoutWith = "large" | "medium" | "small"; interface Theme { @@ -85,6 +85,10 @@ export const light: Theme = { color: "black", backgroundColor: "lightgrey", }, + like: { + color: "while", + backgroundColor: "coral" + } }, borderRadius: { default: "4px", diff --git a/server/src/books/domain/book.ts b/server/src/books/domain/book.ts index f67dd44..1a20eb5 100644 --- a/server/src/books/domain/book.ts +++ b/server/src/books/domain/book.ts @@ -29,6 +29,8 @@ export default class Book { private pubDate: Date; + private liked: boolean; + constructor({ id = 0, title = '', @@ -45,6 +47,7 @@ export default class Book { price = 0, likes = 0, pubDate = new Date(), + liked = false, }: { id?: number; title?: string; @@ -61,6 +64,7 @@ export default class Book { price?: number; likes?: number; pubDate?: Date; + liked?: boolean; }) { this.id = id; this.title = title; @@ -77,6 +81,7 @@ export default class Book { this.price = price; this.likes = likes; this.pubDate = pubDate; + this.liked = liked; } getId() { @@ -138,4 +143,8 @@ export default class Book { getPubDate() { return this.pubDate; } + + getLiked() { + return this.liked; + } } diff --git a/server/src/books/domain/books.repository.ts b/server/src/books/domain/books.repository.ts index d469e53..81249dd 100644 --- a/server/src/books/domain/books.repository.ts +++ b/server/src/books/domain/books.repository.ts @@ -57,7 +57,7 @@ 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, b.img_id, - (SELECT count(*) FROM likes WHERE b.id = liked_book_id) as Likes, + (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 LEFT JOIN category c @@ -89,6 +89,7 @@ export const findWithCategory = async (userId: number, bookId: number): Promise< likes: row.likes, pubDate: row.pub_date, imgId: row.img_id, + liked: row.liked, }); };