diff --git a/eslint.config 2.mjs b/eslint.config 2.mjs deleted file mode 100644 index bb72d115b..000000000 --- a/eslint.config 2.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import pluginReact from "eslint-plugin-react"; - - -/** @type {import('eslint').Linter.Config[]} */ -export default [ - {files: ["**/*.{js,mjs,cjs,jsx}"]}, - {languageOptions: { globals: globals.browser }}, - pluginJs.configs.recommended, - pluginReact.configs.flat.recommended, -]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cfb3d924f..76f7b680e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "date-fns": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.3.0", @@ -6757,6 +6758,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", diff --git a/package.json b/package.json index 558ff1e35..cfa4ea1c7 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "date-fns": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.3.0", diff --git a/public/index.html b/public/index.html index df028d963..82aef376f 100644 --- a/public/index.html +++ b/public/index.html @@ -7,8 +7,7 @@ 판다마켓 diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 000000000..8fc4743b3 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,20 @@ +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import ProductPage from "./components/ProductPage/ProductPage"; +import ItemPage from "./components/ItemPage/ItemPage"; +import AddItem from "./components/AddItemPage/AddItemPage"; +import NavBar from "./components/Nav/NavBar"; + +function App() { + return ( + + + + } /> + } /> + } /> + + + ); +} + +export default App; diff --git a/src/Main.jsx b/src/Main.jsx deleted file mode 100644 index bb812e82b..000000000 --- a/src/Main.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import { BrowserRouter, Route, Routes } from "react-router-dom"; -import App from "./components/App"; -import Item from "./components/ItemPage/ItemPage"; -import AddItem from "./components/AddItemPage/AddItemPage"; - -function Main() { - return ( - - - } /> {/* 기본 경로에 App 연결 */} - } /> - } /> - - - ); -} - -export default Main; diff --git a/src/api.js b/src/api.js index adf2e13f5..b796ec141 100644 --- a/src/api.js +++ b/src/api.js @@ -1,7 +1,38 @@ const BASE_URL = "https://panda-market-api.vercel.app"; + export async function getData({ page = 1, pageSize = 4, orderBy = "recent" }) { - const query = `page=${page}&pageSize=${pageSize}&orderBy=${orderBy}`; - const response = await fetch(`${BASE_URL}/products?${query}`); - const body = response.json(); - return body; + const url = new URL("/products", BASE_URL); + url.searchParams.append("page", page); + url.searchParams.append("pageSize", pageSize); + url.searchParams.append("orderBy", orderBy); + + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.statusText}`); + } + return response.json(); +} + +//GET product ID +export async function getProductDetail(id) { + const url = new URL(`/products/${id}`, BASE_URL); + // url.searchParams.append("{id}", id); + + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.statusText}`); + } + return response.json(); +} + +//GET 상세페이지 댓글들 + +export async function getProductComment(id, limit = 3) { + const url = new URL(`/products/${id}/comments`, BASE_URL); + url.searchParams.append("limit", limit); + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.statusText}`); + } + return response.json(); } diff --git a/src/components/AddItemPage/AddItemPage.jsx b/src/components/AddItemPage/AddItemPage.jsx index e69d1da01..3e2d0ace2 100644 --- a/src/components/AddItemPage/AddItemPage.jsx +++ b/src/components/AddItemPage/AddItemPage.jsx @@ -1,33 +1,34 @@ -import NavBar from "../Nav/NavBar"; import "../AddItemPage/AddItemPage.scss"; -import InputForm from "../AddItemPage/InputForm"; -import InputImage from "../AddItemPage/InputImage"; -import InputTag from "./InputTag"; -import { useRef, useState } from "react"; +import InputForm from "./components/InputForm"; +import InputImage from "./components/InputImage"; +import InputTag from "./components/InputTag"; +import { useState } from "react"; function AddItemPage() { const [values, setValues] = useState({ title: "", description: "", price: "", - tag: "", }); - - const handleInputChange = (field) => (e) => { - setValues((preValues) => ({ - ...preValues, - [field]: e.target.value, - })); - }; + const [tags, setTags] = useState([]); const isSubmitDisabled = - !values.title || !values.description || !values.price || !values.tag; + !values.title || !values.description || !values.price || tags.length === 0; + + console.log(isSubmitDisabled); + return ( <> - -
+
상품 등록하기
- @@ -36,28 +37,35 @@ function AddItemPage() { value={values.title} height={56} placeholder={"상품명을 입력해주세요"} + onChange={(e) => setValues({ ...values, title: e.target.value })} // title 값 변경 /> + setValues({ ...values, description: e.target.value }) + } // description 값 변경 /> setValues({ ...values, price: e.target.value })} // price 값 변경 /> ); } -export default AddItemPage; // default export +export default AddItemPage; diff --git a/src/components/AddItemPage/AddItemPage.scss b/src/components/AddItemPage/AddItemPage.scss index 17522152e..5c950fbf5 100644 --- a/src/components/AddItemPage/AddItemPage.scss +++ b/src/components/AddItemPage/AddItemPage.scss @@ -29,4 +29,16 @@ font-size: 12px; font-weight: 600; line-height: 26px; + cursor: pointer; + + &.active { + background-color: #007bff; /* Blue when enabled */ + color: white; + } + + &.disabled { + background-color: #ccc; /* Gray when disabled */ + color: #888; /* Gray text */ + cursor: not-allowed; + } } diff --git a/src/components/AddItemPage/InputTag.jsx b/src/components/AddItemPage/InputTag.jsx deleted file mode 100644 index b48d56799..000000000 --- a/src/components/AddItemPage/InputTag.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; -import { useState, useEffect } from "react"; -import "./InputTag.scss"; -import deleteButton from "../images/ic_X.svg"; - -function InputTag({ title, height, placeholder, type = "text" }) { - const [inputValue, setInputValue] = useState(""); - const [tags, setTags] = useState([]); // 태그 리스트 관리 - - const handleInputChange = (e) => { - setInputValue(e.target.value); - }; - - const handleAddTag = (e) => { - e.preventDefault(); - console.log(e.target.value); - // console.log("ddd", [...tags, inputValue.trim()]); - setTags([...tags, inputValue.trim()]); - setInputValue(""); - }; - - const handleDeleteTag = (tag) => { - setTags(tags.filter((t) => t !== tag)); - }; - useEffect(() => { - console.log("tags", tags); - }, [tags]); - return ( - <> -
- {title} -
- { - if (e.key === "Enter") { - e.preventDefault(); - handleAddTag(e); - } - }} - /> -
- {tags.map((tag, index) => ( - - #{tag} - - - ))} -
- - ); -} - -export default InputTag; diff --git a/src/components/AddItemPage/InputForm.jsx b/src/components/AddItemPage/components/InputForm.jsx similarity index 81% rename from src/components/AddItemPage/InputForm.jsx rename to src/components/AddItemPage/components/InputForm.jsx index bf2d90760..a3adbcb96 100644 --- a/src/components/AddItemPage/InputForm.jsx +++ b/src/components/AddItemPage/components/InputForm.jsx @@ -1,4 +1,11 @@ -function InputForm({ title, height, placeholder, type = "text", onChange }) { +function InputForm({ + title, + value = "", + height, + placeholder, + type = "text", + onChange, +}) { return ( <>
{ @@ -21,20 +20,8 @@ function InputImage({ title, value, placeholder }) { if (!inputNode) return; inputNode.value = ""; - // onChange(title, null); }; - // useEffect(() => { - // if (!value) return; - // const nextPreview = URL.createObjectURL(value); - // setPreview(nextPreview); - - // return () => { - // setPreview(); - // URL.revokeObjectURL(nextPreview); - // }; - // }, [value]); - return (
{ + if (e) e.preventDefault(); // 이벤트 객체가 있을 경우에만 preventDefault 호출 + if (inputValue.trim() !== "") { + setTags([...tags, inputValue.trim()]); + setInputValue(""); // 입력 필드 초기화 + } + }; + + const handleDeleteTag = (tag) => { + setTags(tags.filter((t) => t !== tag)); + }; + + useEffect(() => { + console.log("tags", tags); + }, [tags]); + + return ( + <> +
+ {title} +
+ { + setInputValue(e.target.value); // 내부 상태 업데이트 + if (onChange) onChange(e); // 부모 컴포넌트로 변경 전달 + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleAddTag(); // 이벤트 객체 없이 태그 추가 처리 + } + }} + /> +
+ {tags.map((tag, index) => ( + + #{tag} + + + ))} +
+ + ); +} diff --git a/src/components/AddItemPage/InputTag.scss b/src/components/AddItemPage/components/InputTag.scss similarity index 100% rename from src/components/AddItemPage/InputTag.scss rename to src/components/AddItemPage/components/InputTag.scss diff --git a/src/components/App.jsx b/src/components/App.jsx deleted file mode 100644 index c9c6b1318..000000000 --- a/src/components/App.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import "./App.css"; - -import NavBar from "./Nav/NavBar"; -import BestProductContainer from "./ProuductListPage/BestProductContainer"; -import AllProductContainer from "./ProuductListPage/AllProductContainer"; - -function App() { - return ( -
- -
- - -
-
- ); -} - -export default App; diff --git a/src/components/InputImage 2.scss b/src/components/InputImage 2.scss deleted file mode 100644 index 0dc931532..000000000 --- a/src/components/InputImage 2.scss +++ /dev/null @@ -1,41 +0,0 @@ -.ImgUploadButton { - display: block; - width: 282px; - height: 282px; - background-color: #f3f4f6; - border-radius: 12px; - margin: 24px; - div { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - height: 100%; - } -} - -.imgPreview { - display: block; - width: 282px; - height: 282px; - background-color: #f3f4f6; - border-radius: 12px; - margin-top: 24px; -} - -.uploadImageContent { - display: flex; - gap: 24px; -} - -.imgDelete { - background-color: transparent; - border-style: none; - position: absolute; - top: 35px; - right: 5px; -} - -.imgContents { - position: relative; -} diff --git a/src/components/ItemPage/ItemPage.jsx b/src/components/ItemPage/ItemPage.jsx index 9c1c5fa0d..6392fdef5 100644 --- a/src/components/ItemPage/ItemPage.jsx +++ b/src/components/ItemPage/ItemPage.jsx @@ -1,5 +1,34 @@ -function ItemPage() { - return
Item Page
; +import React from "react"; +import { Link, useParams } from "react-router-dom"; +import ItemPageDescription from "./components/ItemPageDescription"; +import style from "./ItemPage.module.scss"; +import ItemComment from "./components/ItemComment"; +import Comments from "./components/Comments"; +import backBtn from "../../img/ic_back.svg"; + +function Itempage() { + const { id } = useParams(); // URL에서 id 파라미터를 추출 + return ( +
+ +
+ + + + 목록으로 돌아가기 + 뒤로 가기 + +
+ ); } -export default ItemPage; // default export +export default Itempage; diff --git a/src/components/ItemPage/ItemPage.module.scss b/src/components/ItemPage/ItemPage.module.scss new file mode 100644 index 000000000..f25883434 --- /dev/null +++ b/src/components/ItemPage/ItemPage.module.scss @@ -0,0 +1,26 @@ +.container { + width: 1200px; + margin: 0 auto; + padding-top: 29px; +} + +.link { + display: flex; + justify-content: center; + align-items: center; + margin: 40px auto; + width: 240px; + height: 48px; + + color: white; + font-size: 18px; + font-weight: 600; + line-height: 26px; + text-align: center; + text-underline-position: from-font; + text-decoration: none; + + background-color: #3692ff; + padding: 4px 10px; + border-radius: 40px; +} diff --git a/src/components/ItemPage/components/Comments.jsx b/src/components/ItemPage/components/Comments.jsx new file mode 100644 index 000000000..44368bbec --- /dev/null +++ b/src/components/ItemPage/components/Comments.jsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { getProductComment } from "../../../api"; +import profile from "../../../img/Group 33728.svg"; +import { differenceInHours, differenceInDays } from "date-fns"; + +export default function Comments({ id }) { + const [commentList, setCommentList] = useState([]); + + useEffect(() => { + getProductComment(id).then((data) => { + setCommentList(data); + console.log(data); + }); + }, [id]); + + const isValidDate = (date) => date && !isNaN(new Date(date).getTime()); + + return ( + <> +
+ {commentList?.list?.map((comment) => { + console.log("Comment data:", comment); + + const createdAt = isValidDate(comment.createdAt) + ? new Date(comment.createdAt) + : null; + + const updatedAt = isValidDate(comment.updatedAt) + ? new Date(comment.updatedAt) + : null; + + let timeDiff = ""; + + if ( + createdAt && + updatedAt && + createdAt.getTime() !== updatedAt.getTime() + ) { + const daysDiff = differenceInDays(updatedAt, createdAt); + + if (daysDiff > 0) { + timeDiff = `${daysDiff}일 전`; + } else { + const hoursDiff = differenceInHours(updatedAt, createdAt); + timeDiff = `${hoursDiff}시간 전`; + } + } + + return ( +
+
{comment.content}
+
+ 개인 프로필 +
+
{comment.writer.nickname}
+ {timeDiff &&
{timeDiff}
} +
+
+
+
+ ); + })} +
+ + ); +} diff --git a/src/components/ItemPage/components/ItemComment.jsx b/src/components/ItemPage/components/ItemComment.jsx new file mode 100644 index 000000000..cd0967498 --- /dev/null +++ b/src/components/ItemPage/components/ItemComment.jsx @@ -0,0 +1,55 @@ +import { useState } from "react"; + +export default function ItemComment() { + const [inputValue, setInputValue] = useState(""); + const handleInputChange = (e) => { + setInputValue(e.target.value); + }; + + return ( +
+