diff --git a/package-lock.json b/package-lock.json index 933e9713..771d2f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "classnames": "^2.5.1", + "dayjs": "^1.11.13", "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^5.0.0", @@ -17,8 +18,8 @@ }, "devDependencies": { "@eslint/js": "^9.21.0", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "eslint": "^9.21.0", @@ -27,6 +28,7 @@ "globals": "^15.15.0", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", "vite": "^6.2.0" } }, @@ -1435,9 +1437,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", - "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "dev": true, "license": "MIT", "dependencies": { @@ -1445,9 +1447,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", - "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", + "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1883,6 +1885,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3815,6 +3823,20 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/package.json b/package.json index b07b2d2b..11495592 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "classnames": "^2.5.1", + "dayjs": "^1.11.13", "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^5.0.0", @@ -19,8 +20,8 @@ }, "devDependencies": { "@eslint/js": "^9.21.0", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "eslint": "^9.21.0", @@ -29,6 +30,7 @@ "globals": "^15.15.0", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", "vite": "^6.2.0" } } diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777cc..00000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/index.html b/public/index.html deleted file mode 100644 index aa069f27..00000000 --- a/public/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - React App - - - -
- - - diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a3..00000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c77..00000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e053..00000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.js b/src/App.js deleted file mode 100644 index 37845757..00000000 --- a/src/App.js +++ /dev/null @@ -1,25 +0,0 @@ -import logo from './logo.svg'; -import './App.css'; - -function App() { - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ); -} - -export default App; diff --git a/src/App.jsx b/src/App.jsx index d2d2b8fe..a84ab818 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ import SignupPage from "./pages/SignupPage"; import AddItemPage from "./pages/AddItemPage"; import ErrorPage from "./pages/ErrorPage"; import BoardsPage from "./pages/BoardsPage"; +import ItemDetailPage from "./pages/ItemDetailPage"; function App() { return ( @@ -17,6 +18,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afee..00000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/Components/Common/FormInput.jsx b/src/Components/Common/FormInput.tsx similarity index 71% rename from src/Components/Common/FormInput.jsx rename to src/Components/Common/FormInput.tsx index 3d49d549..0803f649 100644 --- a/src/Components/Common/FormInput.jsx +++ b/src/Components/Common/FormInput.tsx @@ -1,5 +1,21 @@ import React from "react"; +export interface FormInputType { + id: string; + type?: string; + label?: string; + value?: string; + placeholder?: string; + onBlur?: (e: React.FocusEvent) => void; + error?:string; + hidden?: boolean; + className?: string; + rightIcon?: React.ReactNode; + accept?: string; + onChange?: (e:React.ChangeEvent) => void; + ref?:React.Ref; +} + const FormInput = ({ id, type = "text", @@ -13,7 +29,7 @@ const FormInput = ({ accept, ref, ...rest -}) => { +}:FormInputType) => { return (
{label && !hidden && ( @@ -25,11 +41,9 @@ const FormInput = ({ {type === "textarea" ? ( + + +
+ ); +}; + +export default InquirySubmit; diff --git a/src/Components/itemDetail/ProductDetail.jsx b/src/Components/itemDetail/ProductDetail.jsx new file mode 100644 index 00000000..64c2c87c --- /dev/null +++ b/src/Components/itemDetail/ProductDetail.jsx @@ -0,0 +1,86 @@ +import { useParams } from "react-router-dom"; +import useProductById from "../../hooks/useProductById"; +import pandaFace from "../../assets/panda-face.png"; +import heartIcon from "../../assets/heart-Icon.png"; + +const ProductDetail = () => { + const { id } = useParams(); + const { product, isLoading } = useProductById(id); + + if (isLoading || !product) return

상품 불러오는중. . .

; + + return ( +
+
+
+ {product.name} +
+
+

+ {product.name} +

+
+ {product.price.toLocaleString()}원 +
+
+
+ 상품소개 +
+
{product.description}
+
+ 상품태그 +
+
+
    + {product.tags && product.tags.length > 0 + ? product.tags.map((tag, idx) => ( +
  • + #{tag} +
  • + )) + : null} +
+
+
+
+
+ panda-face-logo +
+
{product.ownerNickname}
+
+ {new Date(product.createdAt).toLocaleDateString()} +
+
+
+
+
+
+ heartIcon + {product.favoriteCount} +
+
+
+
+
+ +
+
+ ); +}; + +export default ProductDetail; diff --git a/src/Components/items/Pagination.jsx b/src/Components/items/Pagination.jsx index 3c8e61d7..c30fb53d 100644 --- a/src/Components/items/Pagination.jsx +++ b/src/Components/items/Pagination.jsx @@ -1,17 +1,8 @@ import btnRight from "../../assets/btn-right.png"; import btnLeft from "../../assets/btn-left.png"; -const Pagination = ({ - currentPage, - onPageClick, - totalCount, - visibleCount, - pageSize, -}) => { +const Pagination = ({ currentPage, onPageClick, totalCount, pageSize }) => { const totalPages = Math.ceil(totalCount / pageSize); - console.log(totalCount); - console.log(visibleCount); - console.log(totalPages); const pageGroupSize = 5; const currentGroup = Math.floor((currentPage - 1) / pageGroupSize); diff --git a/src/Components/items/ProductCard.jsx b/src/Components/items/ProductCard.jsx index 70b2e362..a09b29e6 100644 --- a/src/Components/items/ProductCard.jsx +++ b/src/Components/items/ProductCard.jsx @@ -1,8 +1,9 @@ import heartIcon from "../../assets/heart-Icon.png"; +import { Link } from "react-router-dom"; const ProductCard = ({ product, cardType = "best" }) => { return ( -
+ {
{product.favoriteCount}
- + ); }; diff --git a/src/assets/back-to-items-icon.png b/src/assets/back-to-items-icon.png new file mode 100644 index 00000000..01a3cee3 Binary files /dev/null and b/src/assets/back-to-items-icon.png differ diff --git a/src/assets/dropbox-icon.png b/src/assets/dropbox-icon.png new file mode 100644 index 00000000..aea87347 Binary files /dev/null and b/src/assets/dropbox-icon.png differ diff --git a/src/assets/inquiry-empty.png b/src/assets/inquiry-empty.png new file mode 100644 index 00000000..a26fb88c Binary files /dev/null and b/src/assets/inquiry-empty.png differ diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 00000000..3dd4d063 --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1,14 @@ +declare module "*.png" { + const value: string; + export default value; +} + +declare module "*.jpg" { + const value: string; + export default value; +} + +declare module "*.svg" { + const content: any; + export default content; +} diff --git a/src/hooks/useInquiries.js b/src/hooks/useInquiries.js new file mode 100644 index 00000000..36716310 --- /dev/null +++ b/src/hooks/useInquiries.js @@ -0,0 +1,27 @@ +import { useEffect, useState, useCallback } from "react"; + +const useInquiries = (productId) => { + const [inquiries, setInquiries] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const fetchInquiries = useCallback(async () => { + const res = await fetch( + `https://panda-market-api.vercel.app/products/${productId}/comments?cursor=0&limit=10` + ); + if (!res.ok) { + throw new Error(`요청 실패: ${res.status}`); + } + const data = await res.json(); + console.log(data); + setInquiries(data.list ?? []); + setIsLoading(false); + }, [productId]); + + useEffect(() => { + fetchInquiries(); + }, [fetchInquiries]); + + return { inquiries, isLoading, refetch: fetchInquiries }; +}; + +export default useInquiries; diff --git a/src/hooks/useProductById.js b/src/hooks/useProductById.js new file mode 100644 index 00000000..e29b9289 --- /dev/null +++ b/src/hooks/useProductById.js @@ -0,0 +1,19 @@ +import { useEffect, useState } from "react"; +import { fetchProductById } from "../utils/fetchProducts"; + +const useProductById = (id) => { + const [product, setProduct] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + const data = await fetchProductById(id); + setProduct(data); + setIsLoading(false); + }; + fetchData(); + }, [id]); + return { product, isLoading }; +}; + +export default useProductById; diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.tsx similarity index 100% rename from src/pages/HomePage.jsx rename to src/pages/HomePage.tsx diff --git a/src/pages/ItemDetailPage.jsx b/src/pages/ItemDetailPage.jsx new file mode 100644 index 00000000..92d7e46a --- /dev/null +++ b/src/pages/ItemDetailPage.jsx @@ -0,0 +1,15 @@ +import Nav from "../Components/Common/Nav"; +import ProductDetail from "../Components/itemDetail/ProductDetail"; +import InquiryBoard from "../Components/itemDetail/InquiryBoard"; + +const ItemDetailPage = () => { + return ( + <> +