diff --git a/public/ic_X.svg b/public/ic_X.svg new file mode 100644 index 00000000..f6674f7f --- /dev/null +++ b/public/ic_X.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/ic_back.svg b/public/ic_back.svg new file mode 100644 index 00000000..a8265375 --- /dev/null +++ b/public/ic_back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/ic_kebab.svg b/public/ic_kebab.svg new file mode 100644 index 00000000..dd7ed7f5 --- /dev/null +++ b/public/ic_kebab.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/ic_profile.svg b/public/ic_profile.svg new file mode 100644 index 00000000..834caf76 --- /dev/null +++ b/public/ic_profile.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img-preview.png b/public/img_preview.png similarity index 100% rename from public/img-preview.png rename to public/img_preview.png diff --git a/src/App.jsx b/src/App.jsx index 121e62d4..07462a3d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,30 +1,13 @@ -import { Routes, Route, BrowserRouter } from "react-router-dom"; - -import Home from "./pages/Home"; -import Login from "./pages/Login"; -import Signup from "./pages/Signup"; -import Item from "./pages/Item"; -import Privacy from "./pages/Privacy"; -import FAQ from "./pages/FAQ"; -import Community from "./pages/Community"; +import { BrowserRouter as Router, Routes } from "react-router-dom"; +import routes from "./routes"; import "./styles/common.css"; -import AddItem from "./pages/AddItem"; function App() { return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + {routes} + ); } diff --git a/src/api/getItemById.js b/src/api/getItemById.js new file mode 100644 index 00000000..18fed4da --- /dev/null +++ b/src/api/getItemById.js @@ -0,0 +1,16 @@ +export default async function getProductById(id) { + try { + const response = await fetch( + `https://panda-market-api.vercel.app/products/${id}` + ); + if (!response.ok) { + throw new Error("상품 정보를 불러오는데 실패했습니다."); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("상품 상세 에러:", error); + throw error; + } +} diff --git a/src/api/getItemComment.js b/src/api/getItemComment.js new file mode 100644 index 00000000..ea96d7f3 --- /dev/null +++ b/src/api/getItemComment.js @@ -0,0 +1,15 @@ +export default function getItemComment({ id, limit = 10 }) { + return fetch( + `https://panda-market-api.vercel.app/products/${id}/comments?limit=${limit}` + ) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to fetch comments"); + } + return response.json(); + }) + .catch((error) => { + console.error("Error fetching comments:", error); + throw error; + }); +} diff --git a/src/api/itemAPI.js b/src/api/itemAPI.js index 295a60e9..59315b17 100644 --- a/src/api/itemAPI.js +++ b/src/api/itemAPI.js @@ -1,14 +1,18 @@ export async function getProducts(params = {}) { - const query = new URLSearchParams(params).toString(); + try { + const query = new URLSearchParams(params).toString(); + const response = await fetch( + `https://panda-market-api.vercel.app/products?${query}` + ); - const response = await fetch( - `https://panda-market-api.vercel.app/products?${query}` - ); - if (!response.ok) { - throw new Error("데이터를 불러오는데 실패했습니다."); - } - - const data = await response.json(); + if (!response.ok) { + throw new Error("데이터를 불러오는데 실패했습니다."); + } - return data; + const data = await response.json(); + return data; + } catch (error) { + console.error("에러 발생:", error); + throw error; + } } diff --git a/src/components/AddItem/AddItem.jsx b/src/components/AddItem/AddItem.jsx new file mode 100644 index 00000000..749260c6 --- /dev/null +++ b/src/components/AddItem/AddItem.jsx @@ -0,0 +1,107 @@ +import { useState } from "react"; + +import ProductImg from "./components/ProductImg"; +import styles from "./styles/AddItem.module.css"; + +export default function AddItemContent() { + const [productName, setProductName] = useState(""); + const [description, setDescription] = useState(""); + const [price, setPrice] = useState(""); + const [tagInput, setTagInput] = useState(""); + const [tags, setTags] = useState([]); + const [imagePreview, setImagePreview] = useState(null); + + const isFormValid = + productName && description && price && tags.length > 0 && imagePreview; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + const trimmed = tagInput.trim(); + if (trimmed && !tags.includes(trimmed)) { + setTags((prev) => [...prev, trimmed]); + setTagInput(""); + } + } + }; + + const handleDelete = (targetTag) => { + setTags((prevTags) => prevTags.filter((tag) => tag !== targetTag)); + }; + + return ( +
+
+

상품 등록하기

+ +
+ +
+

상품 이미지

+ +
+ +
+

상품명

+ setProductName(e.target.value)} + > +
+ +
+

상품 소개

+ +
+ +
+

판매가격

+ setPrice(e.target.value)} + > +
+ +
+

태그

+ setTagInput(e.target.value)} + onKeyDown={handleKeyDown} + > + +
+ {tags.map((tag, idx) => ( + + #{tag} + + + ))} +
+
+
+ ); +} diff --git a/src/components/AddItem/components/ProductImg.jsx b/src/components/AddItem/components/ProductImg.jsx new file mode 100644 index 00000000..b9a1a176 --- /dev/null +++ b/src/components/AddItem/components/ProductImg.jsx @@ -0,0 +1,54 @@ +import { useRef, useState } from "react"; +import styles from "./styles/ProductImg.module.css"; + +export default function ProductImg({ preview, setPreview }) { + const inputRef = useRef(null); + + const handleImageChange = (e) => { + const file = e.target.files?.[0]; + + if (preview) { + alert("이미지는 한 개만 업로드할 수 있습니다."); + return; + } + + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreview(reader.result); + }; + reader.readAsDataURL(file); + } + }; + + const handleDelete = () => { + setPreview(null); + }; + + return ( +
+ + + {preview && ( +
+ + 미리보기 +
+ )} +
+ ); +} diff --git a/src/components/AddItem/components/styles/ProductImg.module.css b/src/components/AddItem/components/styles/ProductImg.module.css new file mode 100644 index 00000000..4106c706 --- /dev/null +++ b/src/components/AddItem/components/styles/ProductImg.module.css @@ -0,0 +1,77 @@ +.wrapper { + display: flex; + gap: 2.4rem; +} + +.uploadBox { + width: 28.2rem; + height: 28.2rem; + background-color: var(--gray100); + border-radius: 1.2rem; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + cursor: pointer; + overflow: hidden; + + flex-shrink: 0; +} + +.textContainer { + display: inline-flex; + flex-direction: column; + align-items: center; +} + +.plus { + font-size: 5rem; + font-weight: 100; + color: var(--gray400); +} + +.text { + font-size: 1rem; + color: var(--gray400); + font-weight: 400; +} + +.imagePreviewBox { + width: 28.2rem; + height: 28.2rem; + border-radius: 1.2rem; + overflow: hidden; + position: relative; + + flex-shrink: 0; +} + +.previewImg { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.deleteButton { + position: absolute; + top: 8px; + right: 15px; + + width: 20px; + height: 20px; +} + +@media (max-width: 768px) { + .uploadBox { + width: 16.8rem; + height: 16.8rem; + } + + .imagePreviewBox { + width: 16.8rem; + height: 16.8rem; + } +} diff --git a/src/components/AddItem/styles/AddItem.module.css b/src/components/AddItem/styles/AddItem.module.css new file mode 100644 index 00000000..9faf8c31 --- /dev/null +++ b/src/components/AddItem/styles/AddItem.module.css @@ -0,0 +1,144 @@ +.container { + margin-top: 2.4rem; + margin-bottom: 6.9rem; + + width: 120rem; + height: 100%; + + margin-inline: auto; + + display: flex; + flex-direction: column; + gap: 3.2rem; +} + +.addContainer { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; +} + +.addTitle { + font-size: 2rem; + font-weight: 700; + line-height: 3.2rem; +} + +.addBtn { + display: flex; + height: 42px; + padding: 1.2rem 2.3rem; + justify-content: center; + align-items: center; + gap: 1rem; + + border-radius: 0.8rem; + background: var(--blue); + + font-size: 1.6rem; + font-weight: 400; + color: var(--white); +} + +.addBtn:hover { + background: var(--hover-button); +} + +.addBtn:disabled { + background: var(--gray400); + + font-size: 1.6rem; + font-weight: 400; + color: var(--gray100); + + cursor: not-allowed; +} + +.contentContainer { + display: flex; + flex-direction: column; + gap: 1.6rem; +} + +.title { + font-size: 1.8rem; + font-weight: 700; +} + +.input { + width: 100%; + height: 5.6rem; + + border-radius: 1.2rem; +} + +.input::placeholder { + font-size: 1.6rem; + color: var(--gray400); +} + +.inputDescription { + resize: none; + width: 100%; + height: 28.2rem; + border-radius: 1.2rem; + border: none; + background-color: var(--gray100); + + font-size: 1.6rem; + line-height: 2.4rem; + + padding: 1.5rem 2.3rem; +} + +.inputDescription::placeholder { + font-size: 1.6rem; + color: var(--gray400); +} + +.tagList { + display: flex; + gap: 1.2rem; +} + +.tag { + width: 11rem; + height: 3.6rem; + border-radius: 2.6rem; + + position: relative; + + background-color: var(--gray100); + + display: flex; + justify-content: center; + align-items: center; + + gap: 0.8rem; + + font-size: 1.6rem; + font-weight: 400; +} + +.deleteBtn { + width: 2rem; + height: 2rem; +} + +.deleteImg { + width: 100%; + height: 100%; +} + +@media (max-width: 768px) { + .container { + width: 69.6rem; + } +} + +@media (max-width: 426px) { + .container { + width: 34.4rem; + } +} diff --git a/src/components/Home/Banner.jsx b/src/components/Home/Header.jsx similarity index 92% rename from src/components/Home/Banner.jsx rename to src/components/Home/Header.jsx index 504b18da..c1281995 100644 --- a/src/components/Home/Banner.jsx +++ b/src/components/Home/Header.jsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import logo from "/logo.svg"; import logo_title from "/logo_title.svg"; -function Banner() { +function Header() { return (
@@ -21,4 +21,4 @@ function Banner() { ); } -export default Banner; +export default Header; diff --git a/src/components/Item/AllItems.jsx b/src/components/Item/AllItems.jsx index 0f0fe909..202a39a2 100644 --- a/src/components/Item/AllItems.jsx +++ b/src/components/Item/AllItems.jsx @@ -1,11 +1,10 @@ import { useState, useEffect, useCallback } from "react"; -import debounce from "lodash/debounce"; import { getProducts } from "../../api/itemAPI"; import { Link } from "react-router-dom"; -import ItemList from "./ItemList"; -import PageNation from "./PageNation"; +import ItemList from "./component/ItemList"; +import PageNation from "./component/PageNation"; import "./AllItem.css"; import Dropdown from "./component/Dropdown.jsx"; @@ -30,33 +29,34 @@ function AllItem() { const [totalPage, setTotalPage] = useState(); const [pageSize, setPageSize] = useState(getPageSize()); const [page, setPage] = useState(1); + const [keyword, setKeyword] = useState(""); const handleResize = useCallback(() => { const newSize = getPageSize(); setPageSize((prevSize) => (prevSize !== newSize ? newSize : prevSize)); }, []); - const debouncedResize = useCallback(debounce(handleResize, 300), [ - handleResize, - ]); - useEffect(() => { - window.addEventListener("resize", debouncedResize); - + window.addEventListener("resize", handleResize); return () => { - window.removeEventListener("resize", debouncedResize); - debouncedResize.cancel(); + window.removeEventListener("resize", handleResize); }; - }, [debouncedResize]); + }, []); useEffect(() => { const fetchItems = async () => { try { - const data = await getProducts({ + const params = { orderBy: sort, - pageSize: pageSize, - page: page, - }); + pageSize, + page, + }; + + if (keyword.trim() !== "") { + params.keyword = keyword; + } + + const data = await getProducts(params); setItems(data.list); setTotalPage(Math.ceil(data.totalCount / pageSize)); } catch (error) { @@ -65,7 +65,12 @@ function AllItem() { }; fetchItems(); - }, [sort, pageSize, page]); + }, [sort, pageSize, page, keyword]); + + const handleKeywordChange = (e) => { + setKeyword(e.target.value); + setPage(1); + }; return (
@@ -76,8 +81,10 @@ function AllItem() { - + 상품 등록하기 diff --git a/src/components/Item/BestItem.jsx b/src/components/Item/BestItem.jsx index 9bedb83a..b8925b95 100644 --- a/src/components/Item/BestItem.jsx +++ b/src/components/Item/BestItem.jsx @@ -1,7 +1,6 @@ import { useState, useEffect, useCallback } from "react"; -import debounce from "lodash/debounce"; -import ItemList from "./ItemList"; +import ItemList from "./component/ItemList"; import { getProducts } from "../../api/itemAPI"; const getPageSize = () => { @@ -27,18 +26,12 @@ function BestItem() { setPageSize((prevSize) => (prevSize !== newSize ? newSize : prevSize)); }, []); - const debouncedResize = useCallback(debounce(handleResize, 300), [ - handleResize, - ]); - useEffect(() => { - window.addEventListener("resize", debouncedResize); - + window.addEventListener("resize", handleResize); return () => { - window.removeEventListener("resize", debouncedResize); - debouncedResize.cancel(); // 꼭 필요! + window.removeEventListener("resize", handleResize); }; - }, [debouncedResize]); + }, []); useEffect(() => { const fetchItems = async () => { diff --git a/src/components/Item/Banner.css b/src/components/Item/Header.css similarity index 100% rename from src/components/Item/Banner.css rename to src/components/Item/Header.css diff --git a/src/components/Item/Banner.jsx b/src/components/Item/Header.jsx similarity index 69% rename from src/components/Item/Banner.jsx rename to src/components/Item/Header.jsx index 708d2a5f..d2f60b4c 100644 --- a/src/components/Item/Banner.jsx +++ b/src/components/Item/Header.jsx @@ -3,7 +3,7 @@ import { Link, NavLink } from "react-router-dom"; import logo from "/logo.svg"; import logo_title from "/logo_title.svg"; -import "./Banner.css"; +import "./Header.css"; function getLinkStyle({ isActive }) { return { @@ -11,19 +11,16 @@ function getLinkStyle({ isActive }) { }; } -function Banner() { +function Header() { return (
-
- - 판다마켓 로고 이미지 - - - 판다마켓 - -
+ + 판다마켓 로고 이미지 + 판다마켓 + +