Skip to content

Commit

Permalink
Merge pull request #4 from f-lab-edu/feature/3
Browse files Browse the repository at this point in the history
[#3] 도서 목록/검색 화면 개발
  • Loading branch information
2scent authored Jan 21, 2025
2 parents ea2ab9d + eafcf9e commit 3e7e66b
Show file tree
Hide file tree
Showing 24 changed files with 474 additions and 98 deletions.
16 changes: 14 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
/* config options here */
images: {
dangerouslyAllowSVG: true,
remotePatterns: [
{
protocol: 'https',
hostname: 'placehold.co',
},
{
protocol: 'https',
hostname: 'image.aladin.co.kr',
},
],
},
};

export default nextConfig;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
]
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"ky": "^1.7.4",
"lucide-react": "^0.471.0",
"next": "15.1.4",
"react": "^19.0.0",
Expand Down
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file removed src/entities/.gitkeep
Empty file.
3 changes: 3 additions & 0 deletions src/entities/book/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as BookCard } from './ui/BookCard';
export { default as BookList } from './ui/BookList';
export type { Book } from './model/book';
8 changes: 8 additions & 0 deletions src/entities/book/model/book.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Book {
id: number;
title: string;
author: string;
publisher: string;
publishDate: string;
coverImage: string;
}
33 changes: 33 additions & 0 deletions src/entities/book/ui/BookCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Image from 'next/image';
import { Card, CardContent } from '@/shared/ui/card';
import { Book } from '../model/book';

interface BookCardProps {
book: Book;
}

export default function BookCard({ book }: BookCardProps) {
const { coverImage, title, author, publisher, publishDate } = book;

return (
<Card className="overflow-hidden">
<div className="flex flex-row sm:flex-col">
<div className="w-1/3 sm:w-full">
<Image
src={coverImage}
alt={`${title} 표지`}
width={200}
height={300}
className="size-full object-contain sm:h-64 sm:object-cover"
/>
</div>
<CardContent className="w-2/3 p-4 sm:w-full">
<h3 className="mb-1 text-sm font-bold sm:mb-2 sm:text-lg">{title}</h3>
<p className="mb-1 text-xs text-gray-600 sm:text-sm">{author}</p>
<p className="mb-1 text-xs text-gray-600 sm:text-sm">{publisher}</p>
<p className="text-xs text-gray-600 sm:text-sm">{publishDate}</p>
</CardContent>
</div>
</Card>
);
}
19 changes: 19 additions & 0 deletions src/entities/book/ui/BookList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Book } from '../model/book';
import BookCard from './BookCard';

interface BookListProps {
books: Book[];
}

export default function BookList({ books }: BookListProps) {
return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{books.map((book) => (
<BookCard
key={book.id}
book={book}
/>
))}
</div>
);
}
Empty file removed src/features/.gitkeep
Empty file.
2 changes: 2 additions & 0 deletions src/features/book-filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as BookFilter } from './ui/BookFilter';
export type { FilterType } from './model/filter-type';
1 change: 1 addition & 0 deletions src/features/book-filter/model/filter-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type FilterType = 'new' | 'bestseller';
32 changes: 32 additions & 0 deletions src/features/book-filter/ui/BookFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Sparkles, TrendingUp } from 'lucide-react';
import { FilterType } from '../model/filter-type';

interface BookFilterProps {
currentFilter: FilterType;
onFilterChange: (filter: FilterType) => void;
}

export default function BookFilter({ currentFilter, onFilterChange }: BookFilterProps) {
return (
<div className="flex space-x-8 border-b">
<button
className={`flex flex-col items-center pb-2 ${
currentFilter === 'new' ? 'border-b-2 border-primary text-primary' : 'text-gray-500'
}`}
onClick={() => onFilterChange('new')}
>
<Sparkles className="mb-1 size-6" />
<span className="text-xs">신간</span>
</button>
<button
className={`flex flex-col items-center pb-2 ${
currentFilter === 'bestseller' ? 'border-b-2 border-primary text-primary' : 'text-gray-500'
}`}
onClick={() => onFilterChange('bestseller')}
>
<TrendingUp className="mb-1 size-6" />
<span className="text-xs">베스트셀러</span>
</button>
</div>
);
}
1 change: 1 addition & 0 deletions src/features/book-search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as BookSearch } from './ui/BookSearch';
47 changes: 47 additions & 0 deletions src/features/book-search/ui/BookSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';

interface BookSearchProps {
initialSearchTerm?: string;
}

export default function BookSearch({ initialSearchTerm = '' }: BookSearchProps) {
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);

const router = useRouter();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
router.push(`/?q=${searchTerm}`);
};

return (
<form
className="mb-4 flex gap-2"
onSubmit={handleSubmit}
>
<Input
aria-label="도서 검색"
className="w-full"
type="search"
placeholder="제목, 저자로 도서 검색.."
value={searchTerm}
onChange={handleChange}
/>
<Button
aria-label="검색하기"
type="submit"
>
검색
</Button>
</form>
);
}
23 changes: 23 additions & 0 deletions src/pages/home/api/dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export interface BookDTO {
title: string;
link: string;
author: string;
pubDate: string;
description: string;
isbn: string;
isbn13: string;
itemId: number;
priceSales: number;
priceStandard: number;
mallType: string;
stockStatus: string;
mileage: number;
cover: string;
categoryId: number;
categoryName: string;
publisher: string;
salesPoint: number;
adult: boolean;
fixedPrice: boolean;
customerReviewRank: number;
}
54 changes: 54 additions & 0 deletions src/pages/home/api/fetch-books.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { aladinApi } from '@/shared/api/aladin-client';
import { BookDTO } from './dto';
import { adaptBookDTO } from './mapper';

interface Response {
version: string;
title: string;
link: string;
pubDate: string;
totalResults: number;
startIndex: number;
itemsPerPage: number;
query: string;
searchCategoryId: number;
searchCategoryName: string;
item: BookDTO[];
}

interface FetchBooksParams {
q?: string;
}

export async function fetchBooks({ q }: FetchBooksParams) {
const response = q ? fetchSearchBooks(q) : fetchNewBooks();
const data = await response.json<Response>();
return data.item.map(adaptBookDTO);
}

const baseSearchParams = {
MaxResults: '10',
start: '1',
SearchTarget: 'Book',
Cover: 'Big',
output: 'js',
Version: '20131101',
};

function fetchNewBooks() {
const searchParams = new URLSearchParams({
...baseSearchParams,
QueryType: 'ItemNewAll',
});

return aladinApi.get('ItemList.aspx', { searchParams });
}

function fetchSearchBooks(q: string) {
const searchParams = new URLSearchParams({
...baseSearchParams,
Query: q,
});

return aladinApi.get('ItemSearch.aspx', { searchParams });
}
13 changes: 13 additions & 0 deletions src/pages/home/api/mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Book } from '@/entities/book';
import { BookDTO } from './dto';

export function adaptBookDTO(dto: BookDTO): Book {
return {
id: dto.itemId,
title: dto.title,
author: dto.author,
publisher: dto.publisher,
publishDate: dto.pubDate,
coverImage: dto.cover,
};
}
23 changes: 23 additions & 0 deletions src/pages/home/ui/BookControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { useState } from 'react';
import { BookFilter, FilterType } from '@/features/book-filter';
import { BookSearch } from '@/features/book-search';

interface BookControlsProps {
initialSearchTerm?: string;
}

export default function BookControls({ initialSearchTerm }: BookControlsProps) {
const [filter, setFilter] = useState<FilterType>('new');

return (
<div className="mb-8">
<BookSearch initialSearchTerm={initialSearchTerm} />
<BookFilter
currentFilter={filter}
onFilterChange={setFilter}
/>
</div>
);
}
Loading

0 comments on commit 3e7e66b

Please sign in to comment.