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 ? ( + + ) : ( +