diff --git a/src/components/User/ItemList/index.tsx b/src/components/User/ItemList/index.tsx new file mode 100644 index 0000000..4003e49 --- /dev/null +++ b/src/components/User/ItemList/index.tsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import * as S from './style'; +import axiosInstance from 'utils/Axios'; + +interface Item { + itemId: number; + itemName: string; + itemCode: string; + itemPrice: number; + category?: string; + isNew?: boolean; + isHot?: boolean; +} + +const categories = [ + '전체보기', + '과자', + '아이스크림', + '음료', + '냉동식품', + '빵류', + '식품', + '잡화' +]; + +const ItemList: React.FC = () => { + const [items, setItems] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); + const [selectedCategory, setSelectedCategory] = useState('전체보기'); + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(true); + + const filterItems = useCallback(() => { + let filtered = items; + + // 카테고리 필터 + if (selectedCategory !== '전체보기') { + filtered = filtered.filter(item => item.category === selectedCategory); + } + + // 검색어 필터 + if (searchTerm.trim() !== '') { + filtered = filtered.filter(item => + item.itemName.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + setFilteredItems(filtered); + }, [items, selectedCategory, searchTerm]); + + useEffect(() => { + fetchItems(); + }, []); + + useEffect(() => { + filterItems(); + }, [filterItems]); + + const fetchItems = async () => { + try { + setLoading(true); + const response = await axiosInstance.get('v2/item/'); + + if (response.status === 204 || !response.data.itemList || response.data.itemList.length === 0) { + // API 데이터가 없으면 목 데이터 사용 + alert("반환된 상품이 없습니다."); + } else { + const remappedData: Item[] = response.data.itemList.map((item: any) => ({ + itemId: item.itemId, + itemName: item.itemName, + itemCode: item.itemCode, + itemPrice: item.itemPrice, + category: item.category || '기타', + isNew: item.isNew || false, + isHot: item.isHot || false, + })); + setItems(remappedData); + setFilteredItems(remappedData); + } + } catch (error) { + console.error('Error fetching items:', error); + } finally { + setLoading(false); + } + }; + + const handleCategoryClick = (category: string) => { + setSelectedCategory(category); + }; + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); + }; + + return ( + + 매점상품 보기 + + + + {categories.map((category) => ( + handleCategoryClick(category)} + > + {category} + + ))} + + + + + + + + + + + + + {loading ? ( + 로딩 중... + ) : filteredItems.length === 0 ? ( + 상품이 없습니다. + ) : ( + + {filteredItems.map((item) => ( + + {(item.isNew || item.isHot) && ( + + {item.isNew ? 'NEW' : 'HOT'} + + )} + + {item.itemName} + + {item.itemPrice.toLocaleString()} 원 + + + + + ))} + + )} + + ); +}; + +export default ItemList; + diff --git a/src/components/User/ItemList/style.ts b/src/components/User/ItemList/style.ts new file mode 100644 index 0000000..46e3210 --- /dev/null +++ b/src/components/User/ItemList/style.ts @@ -0,0 +1,209 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + width: 100%; + max-width: 1560px; + margin: 0 auto; + padding: 34px 20px 100px; + min-height: calc(100vh - 200px); +`; + +export const Title = styled.h1` + font-family: 'Pretendard', sans-serif; + font-size: 32px; + font-weight: 600; + color: #111111; + margin: 0 0 40px 0; +`; + +export const FilterSection = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 50px; + gap: 20px; + + @media (max-width: 1200px) { + flex-direction: column; + align-items: stretch; + } +`; + +export const CategoryTabBar = styled.div` + display: flex; + align-items: center; + height: 40px; + flex: 1; + max-width: 780px; + + @media (max-width: 1200px) { + max-width: 100%; + overflow-x: auto; + } +`; + +export const CategoryTab = styled.button<{ active: boolean }>` + flex: 1; + min-width: fit-content; + padding: 10px 15px; + font-family: 'Pretendard', sans-serif; + font-size: 18px; + font-weight: 400; + color: ${props => props.active ? '#111111' : '#666666'}; + background: transparent; + border-radius: 0; + border: none; + border-bottom: ${props => props.active ? '3px solid #F49E15' : 'none'}; + cursor: pointer; + transition: all 0.3s ease; + white-space: nowrap; + + &:hover { + color: #111111; + } +`; + +export const SearchBar = styled.div` + position: relative; + width: 500px; + height: 40px; + background: #F3F3F3; + border-radius: 8px; + display: flex; + align-items: center; + padding: 0 20px; + gap: 10px; + + @media (max-width: 1200px) { + width: 100%; + } +`; + +export const SearchInput = styled.input` + flex: 1; + font-family: 'Pretendard', sans-serif; + font-size: 18px; + color: #111111; + background: transparent; + border: none; + outline: none; + + &::placeholder { + color: #666666; + } +`; + +export const SearchIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex-shrink: 0; +`; + +export const ProductGrid = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + width: 100%; + + @media (max-width: 1024px) { + grid-template-columns: 1fr; + } +`; + +export const ProductCard = styled.div` + position: relative; + height: 190px; + background: white; + border: 1px solid #CCCCCC; + border-radius: 16px; + overflow: hidden; + transition: box-shadow 0.3s ease; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } +`; + +export const Badge = styled.div<{ type: 'new' | 'hot' }>` + position: absolute; + left: 20px; + top: 60px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 5px; + border-radius: 8px; + background: ${props => props.type === 'new' ? '#FCC800' : '#F49E15'}; + font-family: 'Pretendard', sans-serif; + font-size: 18px; + font-weight: 400; + color: ${props => props.type === 'new' ? '#111111' : '#FFFFFF'}; + z-index: 2; +`; + +export const ProductInfo = styled.div` + position: absolute; + bottom: 30px; + left: 20px; + display: flex; + flex-direction: column; + gap: 5px; + z-index: 2; + max-width: 273px; +`; + +export const ProductTitle = styled.h3` + font-family: 'Pretendard', sans-serif; + font-size: 32px; + font-weight: 600; + color: #111111; + margin: 0; + line-height: normal; +`; + +export const ProductPrice = styled.div` + display: flex; + align-items: center; + gap: 5px; + font-family: 'Pretendard', sans-serif; + font-size: 18px; + font-weight: 400; + color: #666666; + margin: 0; + line-height: normal; +`; + +export const ProductImagePlaceholder = styled.div` + position: absolute; + right: -0.5px; + top: 50%; + transform: translateY(-50%); + width: 300px; + height: 190px; + background: linear-gradient(270deg, #D9D9D9 0%, rgba(115, 115, 115, 0.00) 100%); + z-index: 1; +`; + +export const LoadingMessage = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 400px; + font-family: 'Pretendard', sans-serif; + font-size: 20px; + color: #666666; +`; + +export const EmptyMessage = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 400px; + font-family: 'Pretendard', sans-serif; + font-size: 20px; + color: #666666; +`; + diff --git a/src/routers/UserRouter.tsx b/src/routers/UserRouter.tsx index 3f0ac8f..23d5872 100644 --- a/src/routers/UserRouter.tsx +++ b/src/routers/UserRouter.tsx @@ -9,6 +9,7 @@ import PwChangeEX from 'components/User/pwChangeEX'; import Register from 'components/User/Register'; import Update from 'components/User/Update'; import UserLog from 'components/User/UserLog'; +import ItemList from 'components/User/ItemList'; const UserRoutes: React.FC = () => { return ( @@ -22,6 +23,7 @@ const UserRoutes: React.FC = () => { } /> } /> } /> + } /> } />