diff --git a/.github/delete-merged-branch-config.yml b/.github/delete-merged-branch-config.yml deleted file mode 100644 index 2a6d27bef..000000000 --- a/.github/delete-merged-branch-config.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: delete branch on close pr - -on: - pull_request: - types: [closed] - -permissions: - pull-requests: write - -jobs: - delete-branch: - runs-on: ubuntu-latest - steps: - - name: delete branch - uses: SvanBoxel/delete-merged-branch@main diff --git a/src/Main.js b/src/Main.js index f0f815d2e..93cad085c 100644 --- a/src/Main.js +++ b/src/Main.js @@ -3,6 +3,7 @@ import App from "./components/App"; import Items from './pages/Items'; import AddItems from './pages/AddItem'; import NotFound from './pages/NotFound'; +import Products from './pages/Products'; function Main() { @@ -11,8 +12,12 @@ function Main() { } > - }> + + }> + }> + }> + }> diff --git a/src/api.js b/src/api.js index 7b36c0cf1..f5fa63ef9 100644 --- a/src/api.js +++ b/src/api.js @@ -1,16 +1,28 @@ const BASE_URL = 'https://panda-market-api.vercel.app'; -export async function getProducts(order = 'recent') { +export async function getProducts({ order = 'recent', page = 1, pageSize }) { const query = `orderBy=${order}`; - const response = await fetch(`${BASE_URL}/products?${query}`); + const response = await fetch(`${BASE_URL}/products?${query}&page=${page}&pageSize=${pageSize}`); const body = await response.json(); return body; +} +export async function getProduct(productSlug) { + const response = await fetch(`${BASE_URL}/products/${productSlug}`); + const body = await response.json(); + return body; } -export async function createProducts(formData) { +export async function getComment(productSlug) { + const response = await fetch(`${BASE_URL}/products/${productSlug}/comments?limit=10`) + const body = await response.json(); + return body; + +} + +export async function createProducts(formData) { const response = await fetch(`${BASE_URL}/products?`, { @@ -21,5 +33,4 @@ export async function createProducts(formData) { const body = await response.json(); return body; -} - +} \ No newline at end of file diff --git a/src/components/App.js b/src/components/App.js index 9036050e7..119747d51 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,5 +1,5 @@ -import Topbar from './Topbar'; +import Topbar from './Topbar/Topbar' import './App.css'; import { Outlet } from 'react-router-dom'; diff --git a/src/components/BestProductList.js b/src/components/BestProductList.js deleted file mode 100644 index 4e47e36be..000000000 --- a/src/components/BestProductList.js +++ /dev/null @@ -1,32 +0,0 @@ -import './BestProductList.css'; - -function BestProductListItem({ item }) { - - return ( -
- {item.name} -

{item.name}

-

{item.price}

-

{item.favoriteCount}

-
- ); - -} - -function BestProductList({ items }) { - - const displayedItems = items.slice(0, 4); - return ( - -
- {displayedItems.map((item) => { - return ( - - - ); - })} -
- ) -} - -export default BestProductList; \ No newline at end of file diff --git a/src/components/BestProductList.css b/src/components/BestProductList/BestProductList.css similarity index 88% rename from src/components/BestProductList.css rename to src/components/BestProductList/BestProductList.css index 6a7768ac8..ed71340ac 100644 --- a/src/components/BestProductList.css +++ b/src/components/BestProductList/BestProductList.css @@ -15,6 +15,8 @@ display: flex; flex-direction: column; padding: 24px 0; + border: 0; + background-color: transparent; } .BestProductListItem p, diff --git a/src/components/BestProductList/BestProductList.js b/src/components/BestProductList/BestProductList.js new file mode 100644 index 000000000..2c642174b --- /dev/null +++ b/src/components/BestProductList/BestProductList.js @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; +import { getProducts } from '../../api'; +import { Link, useNavigate } from 'react-router-dom'; +import './BestProductList.css'; + +function BestProductListItem({ item }) { + const navigate = useNavigate(); + const handleAddItemClick = () => { + navigate(`/items/${item.id}`); + } + + return ( + + ); + +} + +function BestProductList() { + + + const [items, setItems] = useState([]); + + const handleLoad = async () => { + const { list } = await getProducts({ order: 'favorite', page: 1, pageSize: 10 }); + setItems(list); + } + + + useEffect(() => { + handleLoad(); + }, []); + + const displayedItems = items.slice(0, 4); + return ( + +
+ {displayedItems.map((item) => { + return ( + + + ); + })} +
+ ) +} + +export default BestProductList; \ No newline at end of file diff --git a/src/components/Button.css b/src/components/Button/Button.css similarity index 76% rename from src/components/Button.css rename to src/components/Button/Button.css index 795ab07ea..9bf3517a7 100644 --- a/src/components/Button.css +++ b/src/components/Button/Button.css @@ -22,3 +22,13 @@ font-size: 18px; font-weight: 700; } + +.disable { + background-color: #9ca3af; + color: #ffffff; +} + +.inquiry { + width: 74px; + align-self: flex-end; +} diff --git a/src/components/Button.js b/src/components/Button/Button.js similarity index 57% rename from src/components/Button.js rename to src/components/Button/Button.js index f30d36f5e..b47740fe0 100644 --- a/src/components/Button.js +++ b/src/components/Button/Button.js @@ -3,8 +3,8 @@ import './Button.css'; -function Button({ children, onClick, select = '' }) { - const classNames = `Button ${select}`; +function Button({ children, onClick, select = '', width = '' }) { + const classNames = `Button ${select} ${width}`; return ( ); } function ProductList({ items }) { + return (
diff --git a/src/components/Topbar.css b/src/components/Topbar/Topbar.css similarity index 100% rename from src/components/Topbar.css rename to src/components/Topbar/Topbar.css diff --git a/src/components/Topbar.js b/src/components/Topbar/Topbar.js similarity index 86% rename from src/components/Topbar.js rename to src/components/Topbar/Topbar.js index c66e1da04..c6bdeca5d 100644 --- a/src/components/Topbar.js +++ b/src/components/Topbar/Topbar.js @@ -1,6 +1,6 @@ -import pandaLogo from '../img/logo.png'; +import pandaLogo from '../../img/logo.png'; import './Topbar.css'; -import Button from './Button' +import Button from '../Button/Button'; import { Link } from 'react-router-dom'; function Topbar() { diff --git a/src/components/pageNation/PageNation.css b/src/components/pageNation/PageNation.css new file mode 100644 index 000000000..524ec334b --- /dev/null +++ b/src/components/pageNation/PageNation.css @@ -0,0 +1,12 @@ +.pageButton { + width: 40px; + height: 40px; + border-radius: 40px; + background-color: #ffffff; + border: #e5e7eb 1px solid; +} + +.active { + background-color: #3692ff; + color: #ffffff; +} diff --git a/src/components/pageNation/PageNation.js b/src/components/pageNation/PageNation.js new file mode 100644 index 000000000..63555e2ff --- /dev/null +++ b/src/components/pageNation/PageNation.js @@ -0,0 +1,22 @@ +import './PageNation.css'; + +function PageNation({ totalPages, currentPage, handlePageChange }) { + + return ( +
+ + {Array.from({ length: totalPages }, (_, i) => ( + + ))} + +
+ ) +} + +export default PageNation; \ No newline at end of file diff --git a/src/img/Img_inquiry_empty.png b/src/img/Img_inquiry_empty.png new file mode 100644 index 000000000..20f0821bb Binary files /dev/null and b/src/img/Img_inquiry_empty.png differ diff --git a/src/img/ic_back.png b/src/img/ic_back.png new file mode 100644 index 000000000..69ce99c35 Binary files /dev/null and b/src/img/ic_back.png differ diff --git a/src/img/ic_heart.png b/src/img/ic_heart.png new file mode 100644 index 000000000..587b90cd9 Binary files /dev/null and b/src/img/ic_heart.png differ diff --git a/src/pages/AddItem.js b/src/pages/AddItem.js index 4b2a62dfe..2b28708ed 100644 --- a/src/pages/AddItem.js +++ b/src/pages/AddItem.js @@ -1,6 +1,6 @@ import './Additem.css'; import { useEffect, useState } from "react"; -import FileInput from "../components/FileInput"; +import FileInput from '../components/FileInput/FileInput'; function AddItem() { @@ -12,15 +12,15 @@ function AddItem() { imgFile: null, }); - const [isButtonDisabled, SetisButtonDisabled] = useState(true); + const [isButtonDisabled, setIsButtonDisabled] = useState(true); useEffect(() => { const allInput = values.introduction && values.price && values.productName && values.tag; if (allInput) { - SetisButtonDisabled(false); + setIsButtonDisabled(false); } else { - SetisButtonDisabled(true); + setIsButtonDisabled(true); } }, [values]) @@ -39,7 +39,6 @@ function AddItem() { const handleSubmit = (e) => { e.preventDefault(); - console.log(values); } diff --git a/src/pages/Items.css b/src/pages/Items.css index 506124967..40561b91b 100644 --- a/src/pages/Items.css +++ b/src/pages/Items.css @@ -23,3 +23,9 @@ padding: 9px 20px 9px 16px; outline: none; } + +.pagination { + display: flex; + justify-content: center; + margin-top: 20px; +} diff --git a/src/pages/Items.js b/src/pages/Items.js index 1ad46922a..2e79b458f 100644 --- a/src/pages/Items.js +++ b/src/pages/Items.js @@ -1,43 +1,59 @@ -import ProductList from '../components/ProductList'; -import BestProductList from '../components/BestProductList'; -import Button from '../components/Button'; +import ProductList from '../components/ProductList/ProductList'; +import BestProductList from '../components/BestProductList/BestProductList'; +import Button from '../components/Button/Button'; import { useEffect, useState } from 'react'; import { getProducts } from '../api'; import './Items.css'; import { Link, useNavigate } from 'react-router-dom'; +import PageNation from '../components/pageNation/PageNation'; function Items() { + const navigate = useNavigate(); const [order, setOrder] = useState('recent'); const [item, setItem] = useState([]); + const [totalCount, setTotalCount] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [totalPages, setTotalPages] = useState(0); - const SortedItems = item.sort((a, b) => b[order] - a[order]); + useEffect(() => { + handleLoadMore({ order, page: currentPage, pageSize }); + }, [order, currentPage]) - const bestItems = item.sort((a, b) => b['favorite'] - a['favorite']); + useEffect(() => { + setTotalPages(Math.ceil(totalCount / pageSize)); + //Math.ceil(전체 컨텐츠 개수 / 한 페이지에 보여주고자 하는 컨텐츠의 개수) + }, [item]) - const handleFavorite = () => setOrder('favorite'); + const SortedItems = item.sort((a, b) => b[order] - a[order]); + const handleFavorite = () => setOrder('favorite'); const handleRecent = () => setOrder('recent'); - const navigate = useNavigate(); + const handleLoadMore = async () => { + await handleLoad({ order, page: currentPage, pageSize }); + } const handleLoad = async (orderQuery) => { - const { list } = await getProducts(orderQuery); + const { list, totalCount } = await getProducts(orderQuery); setItem(list); + setTotalCount(totalCount); } - useEffect(() => { - handleLoad(order); - }, [order]) const handleAddItemClick = () => { navigate('/additem'); } + const handlePageChange = (page) => { + setCurrentPage(page); + } + return (

베스트 상품

- +

판매 중인 상품

@@ -54,6 +70,7 @@ function Items() { +
) diff --git a/src/pages/Products.css b/src/pages/Products.css new file mode 100644 index 000000000..9851bf4e0 --- /dev/null +++ b/src/pages/Products.css @@ -0,0 +1,124 @@ +.itemWrapper { + display: flex; + gap: 24px; + padding-bottom: 30px; + border-bottom: 1px solid #e5e7eb; +} + +.itemImg { + width: 486px; + height: 486px; + border-radius: 16px; +} + +.itemLetter { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; +} + +.itemLetterTop { + display: flex; + flex-direction: column; + gap: 24px; + padding-bottom: 10px; + border-bottom: 1px solid #e5e7eb; +} + +.itemLetterBottom { + display: flex; + flex-direction: column; + flex: 1; +} + +.itemName { + font-size: 24px; + font-weight: 600; + line-height: 28.64px; +} + +.itemTitle { + font-size: 14px; + font-weight: 500; + line-height: 16.71px; + color: #4b5563; +} + +.itemPrice { + font-size: 40px; + font-weight: 600; + line-height: 47.73px; +} + +.itemitemDescription { + font-size: 14px; + font-weight: 400; + line-height: 16.71px; + color: #1f2937; +} + +.itemTags { + display: flex; + gap: 10px; + padding: 10px 0; +} + +.itemTag { + display: flex; + justify-content: center; + align-items: center; + padding: 6px 16px; + height: 36px; + border-radius: 26px; + background-color: #f3f4f6; +} + +.itemFavorite { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + padding: 6px 16px; + height: 36px; + width: 87px; + border-radius: 26px; + border: 1px solid #e5e7eb; + margin: auto 0 0; +} + +.inquiryWrapper { + display: flex; + width: auto; + flex-direction: column; + gap: 16px; + margin: 24px 0; +} + +.inquiryInput { + width: 100%; + height: 104px; + border-radius: 12px; + background-color: #f3f4f6; + border: none; + color: #9ca3af; + font-size: 16px; + font-weight: 400; + line-height: 24px; + padding: 0 16px; + outline: none; +} + +.backButton { + display: flex; + margin: 40px auto; + gap: 10px; + background-color: #3692ff; + color: #ffffff; + border-radius: 40px; + padding: 12px 71px; + font-size: 18px; + font-weight: 600; + text-decoration: none; + border: none; +} diff --git a/src/pages/Products.js b/src/pages/Products.js new file mode 100644 index 000000000..f906ef42f --- /dev/null +++ b/src/pages/Products.js @@ -0,0 +1,77 @@ +import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { getProduct } from '../api'; +import './Products.css'; +import Button from "../components/Button/Button"; +import { useNavigate } from 'react-router-dom'; +import favoriteImg from '../img/ic_heart.png' +import backImg from '../img/ic_back.png' +import CommentList from "../components/CommentList/CommentList"; + + +function Products() { + const navigate = useNavigate(); + const { productSlug } = useParams(); + const [item, setItem] = useState([]); + const [inquiry, setInquiry] = useState(''); + + useEffect(() => { + handleLoad(productSlug); + }, [productSlug]); + + + const handleLoad = async (productSlug) => { + const list = await getProduct(productSlug); + setItem(list); + } + + const handleBackClick = () => { + navigate('/items'); + } + + const handleInquriyChange = (e) => { + setInquiry(e.target.value); + } + + return (
+
+ {item.name} +
+
+ {item.name} + {item.price}원 +
+
+ 상품소개 +

{item.description}

+ + 상품태그 +
+ {item.tags && item.tags.map((tag, index) => ( +
#{tag}
+ ))} +
+
+ + {item.favoriteCount} +
+
+
+
+ +
+ + + +
+ + + + +
) +} + +export default Products; \ No newline at end of file