diff --git a/src/App.css b/src/App.css
index 8fa8e28a..c0a39e74 100644
--- a/src/App.css
+++ b/src/App.css
@@ -2,6 +2,7 @@
margin: 0;
padding: 0;
box-sizing: border-box;
+ font-family: "Pretendard", sans-serif;
}
#root {
diff --git a/src/api/commentService.js b/src/api/commentService.js
new file mode 100644
index 00000000..ffae59fa
--- /dev/null
+++ b/src/api/commentService.js
@@ -0,0 +1,15 @@
+const getComments = async (productId, limit = 5) => {
+ const response = await fetch(
+ `https://panda-market-api.vercel.app/products/${productId}/comments?limit=${limit}`
+ );
+
+ if (!response.ok) {
+ throw new Error("상품 목록 조회에 실패하였습니다.");
+ }
+
+ return await response.json();
+};
+
+export const commentServices = {
+ getComments,
+};
diff --git a/src/api/productServices.js b/src/api/productServices.js
index 8809a999..0d1fd6b7 100644
--- a/src/api/productServices.js
+++ b/src/api/productServices.js
@@ -17,6 +17,19 @@ const getProducts = async (
return await response.json();
};
+const getProductDetail = async (productId) => {
+ const response = await fetch(
+ `https://panda-market-api.vercel.app/products/${productId}`
+ );
+
+ if (!response.ok) {
+ throw new Error("상품 목록 조회에 실패하였습니다.");
+ }
+
+ return await response.json();
+};
+
export const productServices = {
getProducts,
+ getProductDetail,
};
diff --git a/src/asset/icon/plus.svg b/src/asset/icon/plus.svg
new file mode 100644
index 00000000..5bb9abf5
--- /dev/null
+++ b/src/asset/icon/plus.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/asset/icon/x.svg b/src/asset/icon/x.svg
new file mode 100644
index 00000000..586f4f4d
--- /dev/null
+++ b/src/asset/icon/x.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/components/common/imageuploader/ImageUploader.jsx b/src/components/common/imageuploader/ImageUploader.jsx
new file mode 100644
index 00000000..46342099
--- /dev/null
+++ b/src/components/common/imageuploader/ImageUploader.jsx
@@ -0,0 +1,68 @@
+import { useRef, useState } from "react";
+import "./imageUploader.css";
+import plus from "../../../asset/icon/plus.svg";
+import x from "../../../asset/icon/x.svg";
+
+export default function ImageUploader({ image, setImage }) {
+ const inputRef = useRef(null);
+ const [showWarning, setShowWarning] = useState(false);
+
+ const handleImageClick = (e) => {
+ if (image) {
+ e.preventDefault(); // 이미지 등록 막기
+ setShowWarning(true);
+ }
+ };
+
+ const handleImageChange = (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setImage(reader.result);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleDelete = () => {
+ setImage(null);
+ setShowWarning(false);
+ inputRef.current.value = "";
+ };
+
+ return (
+
+
상품 이미지
+
+
+
+
+ {image && (
+
+

+
+
+ )}
+
+
+ {showWarning && (
+
+ *이미지 등록은 최대 1개까지 가능합니다.
+
+ )}
+
+ );
+}
diff --git a/src/components/common/imageuploader/imageUploader.css b/src/components/common/imageuploader/imageUploader.css
new file mode 100644
index 00000000..f36459f6
--- /dev/null
+++ b/src/components/common/imageuploader/imageUploader.css
@@ -0,0 +1,102 @@
+/* 전체 wrapper */
+.image-wrapper {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 32px;
+}
+
+.image-title {
+ font-weight: 700;
+ font-size: 18px;
+ color: #1f2937;
+}
+
+.button {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 8px;
+ font-size: 18px;
+ font-weight: 700;
+ border: none;
+}
+
+.image-boxes {
+ display: flex;
+ flex-direction: row;
+ gap: 24px;
+ margin-top: 16px;
+}
+
+.upload-label {
+ width: 282px;
+ height: 282px;
+ border-radius: 12px;
+ background-color: #f3f4f6;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ border: none;
+}
+
+.upload-label img {
+ width: 24px;
+ height: 24px;
+ margin-bottom: 12px;
+}
+
+.upload-label p {
+ font-weight: 400;
+ font-size: 16px;
+ color: #9ca3af;
+}
+
+.preview-box {
+ position: relative;
+ width: 282px;
+ height: 282px;
+ border-radius: 12px;
+}
+
+.preview-box img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 12px;
+}
+
+.delete-button {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ background: none;
+ border: none;
+ cursor: pointer;
+}
+
+.warning-message {
+ font-size: 16px;
+ font-weight: 400;
+ color: #ef4444;
+ margin-top: 16px;
+}
+
+@media (max-width: 768px) {
+}
+
+@media (max-width: 1023px) {
+ .upload-label {
+ width: 168px;
+ height: 168px;
+ }
+
+ .preview-box {
+ width: 168px;
+ height: 168px;
+ }
+
+ .image-boxes {
+ gap: 10px;
+ }
+}
diff --git a/src/components/common/inputbox/InputBox.jsx b/src/components/common/inputbox/InputBox.jsx
new file mode 100644
index 00000000..ae096aca
--- /dev/null
+++ b/src/components/common/inputbox/InputBox.jsx
@@ -0,0 +1,36 @@
+import "./inputBox.css";
+
+export default function InputBox({
+ title,
+ placeholder,
+ value,
+ name,
+ onChange,
+ onKeyDown,
+ isInput = true,
+ height,
+}) {
+ return (
+
+
+ {isInput ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/components/common/inputbox/inputBox.css b/src/components/common/inputbox/inputBox.css
new file mode 100644
index 00000000..470fc62f
--- /dev/null
+++ b/src/components/common/inputbox/inputBox.css
@@ -0,0 +1,42 @@
+.input-wrapper {
+ margin-bottom: 32px;
+ max-width: 1200px;
+}
+
+.input-wrapper label {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 16px;
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.input-wrapper input,
+.input-wrapper textarea {
+ width: 100%;
+
+ background-color: #f5f6f8;
+ border: none;
+ border-radius: 12px;
+ padding: 16px 24px;
+ font-size: 16px;
+ font-weight: 400;
+ color: #1f2937;
+ box-sizing: border-box;
+ resize: none;
+}
+
+.input-wrapper textarea {
+ height: 282px;
+}
+
+.input-wrapper input:focus,
+.input-wrapper textarea:focus {
+ outline: none;
+ border: 2px solid #3692ff;
+}
+
+.input-wrapper input::placeholder,
+.input-wrapper textarea::placeholder {
+ color: #9ca3af;
+}
diff --git a/src/components/common/taginput/TagInput.jsx b/src/components/common/taginput/TagInput.jsx
new file mode 100644
index 00000000..3d33c03c
--- /dev/null
+++ b/src/components/common/taginput/TagInput.jsx
@@ -0,0 +1,46 @@
+import InputBox from "../../common/inputbox/InputBox";
+import { useState } from "react";
+import x from "../../../asset/icon/x.svg";
+
+import "./tagInput.css";
+
+export default function TagInput({ tags, setTags }) {
+ const [input, setInput] = useState("");
+
+ const handleKeyDown = (e) => {
+ if (e.key === "Enter" && input.trim() !== "") {
+ e.preventDefault();
+
+ const newTag = input.trim();
+
+ if (!tags.includes(newTag)) {
+ setTags([...tags, newTag]);
+ }
+ setInput("");
+ }
+ };
+
+ const handleDelete = (tagToDelete) => {
+ setTags(tags.filter((tag) => tag !== tagToDelete));
+ };
+
+ return (
+
+
setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+
+ {tags.map((tag) => (
+
+
#{tag}
+

handleDelete(tag)} />
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/common/taginput/tagInput.css b/src/components/common/taginput/tagInput.css
new file mode 100644
index 00000000..4a4d527c
--- /dev/null
+++ b/src/components/common/taginput/tagInput.css
@@ -0,0 +1,36 @@
+.tag-wrapper {
+ margin-bottom: 32px;
+}
+
+.tag-wrapper > .input-wrapper {
+ margin-bottom: 14px;
+}
+
+.tag-list {
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-top: 14px;
+}
+
+.tag-item {
+ display: flex;
+ align-items: center;
+ background-color: #f3f4f6;
+ color: #1f2937;
+ padding: 5px 12px 5px 16px;
+ border-radius: 30px;
+ font-size: 14px;
+ font-family: "Pretendard";
+}
+
+.tag-item span {
+ margin-right: 8px;
+}
+
+.tag-item img {
+ width: 22px;
+ height: 24px;
+ cursor: pointer;
+}
diff --git a/src/components/items/ItemCard.jsx b/src/components/items/ItemCard.jsx
index f52c3653..e8b27332 100644
--- a/src/components/items/ItemCard.jsx
+++ b/src/components/items/ItemCard.jsx
@@ -7,7 +7,7 @@ export default function ItemCard({ cardInfo, cardType }) {
const { favoriteCount, images, price, name, id } = cardInfo;
return (
-
+
0 ? images[0] : noImage}
diff --git a/src/components/layout/Navbar.jsx b/src/components/layout/Navbar.jsx
index 1713c83b..1b29522b 100644
--- a/src/components/layout/Navbar.jsx
+++ b/src/components/layout/Navbar.jsx
@@ -1,10 +1,12 @@
-import { Link } from "react-router-dom";
+import { Link, useLocation } from "react-router-dom";
import logoImg from "../../asset/icon/panda_market_logo_3.png";
import logoMobileImg from "../../asset/icon/panda_market_logo_no_icon.png";
import profileImg from "../../asset/icon/profile_icon.svg";
import "./navbar.css";
export default function Navbar() {
+ const { pathname } = useLocation();
+
return (
@@ -17,10 +19,20 @@ export default function Navbar() {
/>
-
+
자유게시판
-
+
중고마켓
diff --git a/src/components/layout/navbar.css b/src/components/layout/navbar.css
index c1ee8c0a..d424cf1e 100644
--- a/src/components/layout/navbar.css
+++ b/src/components/layout/navbar.css
@@ -29,13 +29,17 @@
.link {
padding: 25px 15px;
- color: black;
+ color: #4b5563;
text-decoration-line: none;
font-size: 18px;
font-weight: 700;
color: #4b5563;
}
+.link-active {
+ color: #3692ff;
+}
+
@media (max-width: 768px) {
.header-container {
padding-left: 16px;
diff --git a/src/pages/additem/AddItem.jsx b/src/pages/additem/AddItem.jsx
new file mode 100644
index 00000000..60200c2a
--- /dev/null
+++ b/src/pages/additem/AddItem.jsx
@@ -0,0 +1,86 @@
+import { useState } from "react";
+
+import Navbar from "../../components/layout/Navbar";
+import ImageUploader from "../../components/common/imageuploader/ImageUploader";
+import InputBox from "../../components/common/inputbox/InputBox";
+import TagInput from "../../components/common/taginput/TagInput";
+
+import "./addItem.css";
+
+export default function AddItem() {
+ const [image, setImage] = useState(null);
+ const [tags, setTags] = useState([]);
+ const [formValue, setFormValue] = useState({
+ title: "",
+ description: "",
+ price: "",
+ });
+
+ const handleFormValueChange = (e) => {
+ const { name, value } = e.target;
+
+ switch (name) {
+ case "title":
+ setFormValue({ ...formValue, title: value });
+ break;
+ case "description":
+ setFormValue({ ...formValue, description: value });
+ break;
+ case "price":
+ const rawValue = value.replace(/,/g, "").replace(/\D/g, ""); // 문자 막음 -> 숫자만 가능하게
+ const formatted = rawValue.replace(/\B(?=(\d{3})+(?!\d))/g, ","); // 3자리마다 쉼표 추가
+ setFormValue({ ...formValue, price: formatted });
+ break;
+ default:
+ return;
+ }
+ };
+
+ const isFormValid = () => {
+ return (
+ formValue.title.trim() !== "" &&
+ formValue.description.trim() !== "" &&
+ formValue.price.trim() !== "" &&
+ tags.length > 0
+ );
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/pages/additem/addItem.css b/src/pages/additem/addItem.css
new file mode 100644
index 00000000..217ad9dd
--- /dev/null
+++ b/src/pages/additem/addItem.css
@@ -0,0 +1,51 @@
+.additem-container {
+ max-width: 1200px;
+ min-height: 100%;
+ margin: 0 auto;
+ padding: 24px 0 70px 0;
+}
+
+.additem-top-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
+}
+
+.additem-title {
+ line-height: 20px;
+ padding: 0;
+ font-size: 20px;
+ font-weight: 700;
+ color: #1f2937;
+ margin-bottom: 5px;
+}
+
+.submit-button {
+ background-color: #3692ff;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ padding: 12px 23px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.submit-button:disabled {
+ background-color: #9ca3af;
+ cursor: not-allowed;
+}
+
+@media (max-width: 768px) {
+ .additem-container {
+ /* max-width: 346px; */
+ margin: 0 15px;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 1023px) {
+ .additem-container {
+ max-width: 696px;
+ }
+}
diff --git a/src/pages/items/Items.jsx b/src/pages/items/Items.jsx
index f9ec8d52..e2a814cf 100644
--- a/src/pages/items/Items.jsx
+++ b/src/pages/items/Items.jsx
@@ -82,7 +82,7 @@ export default function Items() {
/>
-
+
상품 등록하기
diff --git a/src/routes/AppRouter.jsx b/src/routes/AppRouter.jsx
index d035588a..adeef5f1 100644
--- a/src/routes/AppRouter.jsx
+++ b/src/routes/AppRouter.jsx
@@ -4,6 +4,8 @@ import Login from "../pages/login/Login";
// import Login from "../pages/login/Login";
// import Signup from "./pages/signup/Signup";
import Items from "../pages/items/Items";
+import AddItem from "../pages/additem/AddItem";
+import ItemsDetail from "../pages/items/itemsDetail/ItemsDetail";
// import FAQ from "./pages/faq/FAQ";
// import PrivacyPolicy from "./pages/privacy/PrivacyPolicy";
@@ -14,6 +16,8 @@ export default function AppRouter() {
} />
{/* } /> */}
} />
+ } />
+ } />
{/* } /> */}
{/* } /> */}