diff --git a/apps/member/src/api/book.ts b/apps/member/src/api/book.ts index 195e1e96..4c85e2ba 100644 --- a/apps/member/src/api/book.ts +++ b/apps/member/src/api/book.ts @@ -1,9 +1,8 @@ import { END_POINT } from '@constants/api'; -import { createCommonPagination } from '@utils/api'; +import { createCommonPagination, createPath } from '@utils/api'; import type { BaseResponse, - PaginationPramsType, PaginationType, WithPaginationPrams, } from '@type/api'; @@ -15,12 +14,12 @@ import type { import { server } from './server'; -export interface BorrowerBookInfo { +export interface BookLoanRequestData { bookId: number; borrowerId: string; } -export interface GetBookLoanRecordConditionsPrams extends PaginationPramsType { +export interface BookLoanRecordSearchOptions extends WithPaginationPrams { bookId?: number; borrowerId?: string; isReturned?: boolean; @@ -28,79 +27,74 @@ export interface GetBookLoanRecordConditionsPrams extends PaginationPramsType { /** * 도서 목록 조회 */ -export const getBooks = async (page: number, size: number) => { - const params = { page, size }; +export async function getBooks(page: number, size: number) { const { data } = await server.get>({ - url: createCommonPagination(END_POINT.BOOK, params), + url: createCommonPagination(END_POINT.BOOK, { page, size }), }); return data; -}; +} /** * 도서 상세 조회 */ -export const getBookDetail = async (id: number) => { +export async function getBookDetail(id: number) { const { data } = await server.get>({ url: END_POINT.BOOK_DETAIL(id), }); return data; -}; +} /** * 나의 대출내역 조회 */ -export const getMyBooks = async (id: string, page: number, size: number) => { - const params = { page, size }; +export async function getMyBooks(id: string, page: number, size: number) { const { data } = await server.get>({ - url: createCommonPagination(END_POINT.BOOK, params), + url: createCommonPagination(END_POINT.BOOK, { page, size }), }); return data.items.filter((book) => book.borrowerId === id); -}; +} /** * 도서 대출 */ -export const postBorrowBook = async (body: BorrowerBookInfo) => { - const borrowUrl = END_POINT.BOOK_LOAN_BORROW; - const { data } = await server.post>({ - url: borrowUrl, +export async function postBorrowBook(body: BookLoanRequestData) { + return server.post>({ + url: END_POINT.BOOK_LOAN_BORROW, body, }); - - return data; -}; +} /** * 도서 반납 */ -export const postReturnBook = async (body: BorrowerBookInfo) => { - const { data } = await server.post>({ - url: END_POINT.BOOK_LOAN_RETURN, - body, - }); +export async function postReturnBook(body: BookLoanRequestData) { + const { data } = await server.post>( + { + url: END_POINT.BOOK_LOAN_RETURN, + body, + }, + ); return data; -}; +} /** * 도서 연장 */ -export const postExtendBook = async (body: BorrowerBookInfo) => { - const { data } = await server.post>({ +export function postExtendBook(body: BookLoanRequestData) { + return server.post>({ url: END_POINT.BOOK_LOAN_EXTEND, body, }); - - return data; -}; +} /** * 도서 대출 내역 조회 */ -export const getBookLoanRecordConditions = async ({ +export async function getBookLoanRecordConditions({ bookId, borrowerId, isReturned, page = 0, size = 20, -}: GetBookLoanRecordConditionsPrams) => { +}: BookLoanRecordSearchOptions) { const { data } = await server.get< PaginationType >({ @@ -114,14 +108,14 @@ export const getBookLoanRecordConditions = async ({ }); return data; -}; +} /** * 도서 연체자 조회 */ -export const getBookLoanRecordOverdue = async ({ +export async function getBookLoanRecordOverdue({ page, size, -}: WithPaginationPrams) => { +}: WithPaginationPrams) { const { data } = await server.get< PaginationType >({ @@ -132,4 +126,12 @@ export const getBookLoanRecordOverdue = async ({ }); return data; -}; +} +/** + * 도서 대출 승인 + */ +export function patchBookLoanRecordApprove(id: number) { + return server.patch>({ + url: createPath(END_POINT.BOOK_LOAN_RECORD_APPROVE, id), + }); +} diff --git a/apps/member/src/components/common/Header/Header.tsx b/apps/member/src/components/common/Header/Header.tsx index 6fa3f5e8..477cf303 100644 --- a/apps/member/src/components/common/Header/Header.tsx +++ b/apps/member/src/components/common/Header/Header.tsx @@ -1,22 +1,27 @@ import { Fragment } from 'react'; import { GrNext } from 'react-icons/gr'; +import { Link } from 'react-router-dom'; interface HeaderProps { title: string | string[]; + path?: string | string[]; children?: React.ReactNode; } -const Header = ({ title, children }: HeaderProps) => { +const Header = ({ title, path, children }: HeaderProps) => { const RenderTitle = () => { if (Array.isArray(title)) { // 배열일 경우, 제목이 여러 개일 경우 return (
{title.map((name, index) => ( - - + + {name} - + {index !== title.length - 1 && } ))} diff --git a/apps/member/src/components/common/Image/Image.tsx b/apps/member/src/components/common/Image/Image.tsx index ecfa4ab5..845374a6 100644 --- a/apps/member/src/components/common/Image/Image.tsx +++ b/apps/member/src/components/common/Image/Image.tsx @@ -1,59 +1,57 @@ -import { SyntheticEvent, useState } from 'react'; +import { ComponentPropsWithRef, SyntheticEvent, useState } from 'react'; import { NOT_FOUND_IMG } from '@constants/path'; -import classNames from 'classnames'; +import { cn } from '@utils/string'; -interface ImageProps { - src?: string; - alt: string; +interface ImageProps extends ComponentPropsWithRef<'img'> { width?: string; height?: string; - className?: string; - onClick?: () => void; overflow?: boolean; } +type Status = 'loading' | 'error' | 'loaded'; + const Image = ({ - src, - alt, width, height, + src, + overflow, className, onClick, - overflow, + ...rest }: ImageProps) => { - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - - if (!src) src = NOT_FOUND_IMG; + const [status, setStatus] = useState('loading'); - const _width = width ? width : 'w-full'; - const _height = height ? height : 'h-full'; + const _width = width ?? 'w-full'; + const _height = height ?? 'h-full'; const handleError = (e: SyntheticEvent) => { e.currentTarget.src = NOT_FOUND_IMG; - setError(true); - setLoading(false); + setStatus('error'); }; return (
{alt} setLoading(false)} + className={cn( + { + 'animate-pulse bg-gray-200': status === 'loading', + 'bg-gray-50': status === 'error', + }, + _width, + _height, + className, + )} + src={src ?? NOT_FOUND_IMG} + onLoad={() => setStatus('loaded')} onError={handleError} loading="lazy" + {...rest} />
); diff --git a/apps/member/src/components/common/Nav/Nav.tsx b/apps/member/src/components/common/Nav/Nav.tsx index 1c3c64ea..6b0d9457 100644 --- a/apps/member/src/components/common/Nav/Nav.tsx +++ b/apps/member/src/components/common/Nav/Nav.tsx @@ -13,9 +13,7 @@ const Nav = () => { const navigate = useNavigate(); const { openModal } = useModal(); - const isSelected = (path: string) => { - return location.pathname.endsWith(path); - }; + const pathName = location.pathname; const handleMenubarItemClick = useCallback( (path: string) => { @@ -40,44 +38,44 @@ const Nav = () => { handleMenubarItemClick(PATH.MAIN)} > 홈 handleMenubarItemClick(PATH.CALENDER)} > 일정 handleMenubarItemClick(PATH.ACTIVITY)} > 활동 handleMenubarItemClick(PATH.COMMUNITY)} > 커뮤니티 handleMenubarItemClick(PATH.LIBRARY)} > 도서관 handleMenubarItemClick(PATH.SUPPORT)} > 회비 {MODE !== 'production' && ( handleMenubarItemClick(PATH.MANAGE)} > 관리 diff --git a/apps/member/src/components/library/BookDetailSection/BookDetailSection.tsx b/apps/member/src/components/library/BookDetailSection/BookDetailSection.tsx index 2e9e3e79..7e590bd0 100644 --- a/apps/member/src/components/library/BookDetailSection/BookDetailSection.tsx +++ b/apps/member/src/components/library/BookDetailSection/BookDetailSection.tsx @@ -5,17 +5,18 @@ import { Badge, Button, DetailsList, Grid, Tabs } from '@clab/design-system'; import Image from '@components/common/Image/Image'; import Section from '@components/common/Section/Section'; +import { BOOK_STORE_URL } from '@constants/path'; import { SELECT_DEFAULT_OPTION } from '@constants/select'; import { BOOK_STATE } from '@constants/state'; -import { useBookLoanBorrowMutation } from '@hooks/queries/useBookLoanBorrowMutation'; -import { useMyProfile } from '@hooks/queries/useMyProfile'; +import { useBookLoanBorrowMutation, useMyProfile } from '@hooks/queries'; import { createImageUrl } from '@utils/api'; +import { bookReviewParser, toBookstore } from '@utils/string'; import yes24Icon from '@assets/svg/yes24.svg'; import aladinIcon from '@assets/webp/aladin.webp'; import kyoboIcon from '@assets/webp/kyobobook.webp'; -import type { BookItem } from '@type/book'; +import type { BookItem, BookstoreKorean } from '@type/book'; interface BookDetailSectionProps { data: BookItem; @@ -31,7 +32,7 @@ const options = [ value: '예스24', }, { - icon: 알라딘, + icon: 알라딘, value: '알라딘', }, ] as const; @@ -39,7 +40,16 @@ const options = [ const BookDetailSection = ({ data }: BookDetailSectionProps) => { const { data: myInfo } = useMyProfile(); const { bookBorrowMutate } = useBookLoanBorrowMutation(); - const { id, borrowerId, category, title, author, publisher, imageUrl } = data; + const { + id, + borrowerId, + category, + title, + author, + publisher, + imageUrl, + reviewLinks, + } = data; const handleBorrowClick = useCallback( (bookId: number) => { @@ -50,6 +60,17 @@ const BookDetailSection = ({ data }: BookDetailSectionProps) => { }, [bookBorrowMutate, myInfo.id], ); + /** + * 온라인 서점에서 책의 정보를 검색하는 함수입니다. + * 서버에 저장된 온라인 서점 URL이 존재할 경우 해당 URL로 이동합니다. + * 존재하지 않을 경우, 책 제목을 검색하여 검색 결과를 보여줍니다. + */ + const handleTabsChange = (value: string) => { + const bookStore = toBookstore(value as BookstoreKorean); + const url = bookReviewParser(reviewLinks); + const targetUrl = url[bookStore] ?? `${BOOK_STORE_URL[bookStore]}${title}`; + window.open(targetUrl, '_blank'); + }; return (
@@ -62,7 +83,7 @@ const BookDetailSection = ({ data }: BookDetailSectionProps) => { />
-

{title}

+

{title}

{author} {publisher} @@ -73,13 +94,13 @@ const BookDetailSection = ({ data }: BookDetailSectionProps) => { - +
-