상품 목록
키워드로 상품을 검색하고, 카테고리로 좁히고, 정렬하고, 한 페이지에 6개씩 나눠 표시합니다.
상품 목록
핵심 포인트
- 1
원본 데이터는 그대로 두고 보여 줄 부분만 만들기
전체 상품에서 시작해 filter(좁히기) → sort(정렬) → slice(페이지 나누기) 순서로 화면에 보여 줄 부분을 만듭니다. 원래 목록은 그대로입니다.
- 2
조건이 바뀌면 1페이지로 돌아가기
2페이지를 보다가 검색하면 결과가 1페이지뿐이라 화면이 비어 버릴 수 있습니다. 조건이 바뀌면 페이지 번호를 1로 되돌립니다.
- 3
"결과 없음" 화면 준비하기
0개일 때는 "조건에 맞는 상품이 없습니다"를 표시하고 조건을 지우는 버튼을 둡니다. 아무것도 표시하지 않으면 고장 난 것처럼 보입니다.
사용한 도구
누르면 제2부의 비교 페이지로
코드 고치기
코드를 고치면 오른쪽 미리보기가 바로 바뀝니다.
import { useState } from "react" import { SearchIcon } from "lucide-react" import { Button } from "./components/ui/button" import { Input } from "./components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/ui/select" type Product = { id: number name: string category: string price: number rating: number color: string // 상품 사진 대신 쓰는 색 } // 상품 데이터 (12개) const products: Product[] = [ { id: 1, name: "무선 이어폰", category: "가전", price: 110500, rating: 4.5, color: "#0ea5e9" }, { id: 2, name: "스테인리스 텀블러", category: "주방", price: 26000, rating: 4.2, color: "#10b981" }, { id: 3, name: "백팩 20L", category: "패션", price: 65000, rating: 4.7, color: "#f59e0b" }, { id: 4, name: "데스크 조명", category: "가전", price: 52000, rating: 4.0, color: "#8b5cf6" }, { id: 5, name: "커피 그라인더", category: "주방", price: 39000, rating: 4.6, color: "#ef4444" }, { id: 6, name: "스니커즈", category: "패션", price: 84500, rating: 4.3, color: "#ec4899" }, { id: 7, name: "보조배터리", category: "가전", price: 33800, rating: 4.4, color: "#14b8a6" }, { id: 8, name: "프라이팬 26cm", category: "주방", price: 29900, rating: 4.1, color: "#f97316" }, { id: 9, name: "볼캡", category: "패션", price: 20800, rating: 3.9, color: "#6366f1" }, { id: 10, name: "전기 주전자", category: "가전", price: 42900, rating: 4.5, color: "#84cc16" }, { id: 11, name: "도마", category: "주방", price: 16900, rating: 4.0, color: "#a855f7" }, { id: 12, name: "손목시계", category: "패션", price: 136500, rating: 4.8, color: "#64748b" }, ] const categories = ["전체", "가전", "주방", "패션"] // 한 페이지에 보여 줄 개수 const pageSize = 6 // 상품 하나의 카드 function ProductCard({ product }: { product: Product }) { return ( <div className="overflow-hidden rounded-lg border bg-card"> <div className="h-24" style={{ backgroundColor: product.color }} /> <div className="p-3"> <p className="text-xs text-muted-foreground">{product.category}</p> <p className="mt-0.5 text-sm font-medium">{product.name}</p> <div className="mt-2 flex items-center justify-between"> <p className="text-sm font-semibold">₩{product.price.toLocaleString()}</p> <p className="text-xs text-amber-500">★ {product.rating}</p> </div> </div> </div> ) } export default function App() { // ① 화면이 기억할 값 (state) const [query, setQuery] = useState("") const [category, setCategory] = useState("전체") const [sort, setSort] = useState("recommended") const [page, setPage] = useState(1) // ② 화면에 보여 줄 상품: 필터 → 정렬 → 페이지 나누기 const filtered = products.filter((product) => product.name.includes(query) && (category === "전체" || product.category === category)) // 낮은 가격순 let sorted = filtered if (sort === "cheap") sorted = [...filtered].sort((a, b) => a.price - b.price) // 높은 가격순 if (sort === "expensive") sorted = [...filtered].sort((a, b) => b.price - a.price) // 평점 높은순 if (sort === "rating") sorted = [...filtered].sort((a, b) => b.rating - a.rating) // 필터한 개수로 전체 페이지 수 계산 const totalPages = Math.ceil(sorted.length / pageSize) // 1페이지 → slice(0, 6), 2페이지 → slice(6, 12) const shown = sorted.slice((page - 1) * pageSize, page * pageSize) //console.log(query, category, shown) return ( <div className="mx-auto my-6 w-full max-w-3xl rounded-xl border bg-card p-6 text-card-foreground"> {/* 검색과 정렬 */} <div className="flex flex-wrap items-center gap-2"> {/* 입력란 왼쪽의 선으로 된 돋보기 아이콘 */} <div className="relative flex-1"> <SearchIcon className="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <Input value={query} onChange={(e) => { setQuery(e.target.value) setPage(1) // 검색하면 1페이지로 돌아가기 }} className="pl-9" placeholder="상품명으로 검색" /> </div> <Select value={sort} onValueChange={(value) => { setSort(value) setPage(1) // 정렬을 바꾸면 1페이지로 돌아가기 }}> <SelectTrigger className="w-40"> <SelectValue /> </SelectTrigger> <SelectContent position="popper"> <SelectItem value="recommended">추천순</SelectItem> <SelectItem value="cheap">낮은 가격순</SelectItem> <SelectItem value="expensive">높은 가격순</SelectItem> <SelectItem value="rating">평점 높은순</SelectItem> </SelectContent> </Select> </div> {/* 카테고리 */} <div className="mt-3 flex flex-wrap gap-2"> {categories.map((name) => ( <Button key={name} size="sm" variant={ category === name ? "default" : "outline" } onClick={() => { setCategory(name) setPage(1) // 카테고리를 바꾸면 1페이지로 돌아가기 }} className="rounded-full"> {name} </Button> ))} </div> <p className="mt-4 text-xs text-muted-foreground">총 {sorted.length}개</p> {/* 상품 카드 */} {sorted.length === 0 ? ( <p className="py-10 text-center text-sm text-muted-foreground">검색 결과가 없습니다</p> ) : ( <div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-3"> {shown.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div> )} {/* 페이지 이동 */} <div className="mt-5 flex items-center justify-center gap-1"> <Button size="sm" variant="outline" disabled={page === 1} onClick={page === 1 ? undefined : () => setPage(page - 1)}> 이전 </Button> {Array.from({ length: totalPages }, (_, i) => i + 1).map ((number) => ( <Button key={number} size="sm" variant={page === number ? "default" : "outline"} onClick={() => setPage(number)}> {number} </Button> ))} <Button size="sm" variant="outline" disabled={page >= totalPages} onClick={() => setPage(page + 1)}> 다음 </Button> </div> </div> ) }