diff --git a/package-lock.json b/package-lock.json index 957c9d31..b37997c1 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", + "axios": "^1.7.9", "firebase": "^11.1.0", "lodash.throttle": "^4.1.1", "react": "^18.2.0", @@ -5900,6 +5901,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -15179,6 +15203,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/package.json b/package.json index 5f5b0504..3fbc0b7a 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", + "axios": "^1.7.9", "firebase": "^11.1.0", "lodash.throttle": "^4.1.1", "react": "^18.2.0", diff --git a/src/Hooks/useAutoClose.jsx b/src/Hooks/useAutoClose.jsx new file mode 100644 index 00000000..6041a98e --- /dev/null +++ b/src/Hooks/useAutoClose.jsx @@ -0,0 +1,21 @@ +import { useState, useEffect, useRef } from "react"; + +export function useAutoClose(initialState) { + const [isOpen, setIsOpen] = useState(initialState); + const ref = useRef(null); + + const handleOutsideClick = (e) => { + if (ref.current && !ref.current.contains(e?.target)) { + setIsOpen(false); + } + }; + + useEffect(() => { + document.addEventListener("click", handleOutsideClick, true); + return () => { + document.removeEventListener("click", handleOutsideClick, true); + }; + }, []); + + return { ref, isOpen, setIsOpen }; +} diff --git a/src/Hooks/useFormatting.jsx b/src/Hooks/useFormatting.jsx new file mode 100644 index 00000000..335e24c2 --- /dev/null +++ b/src/Hooks/useFormatting.jsx @@ -0,0 +1,33 @@ +export function useFormatDate(data) { + const date = new Date(data); + const formattedDate = `${date.getFullYear()}.${String( + date.getMonth() + 1 + ).padStart(2, "0")}.${String(date.getDate()).padStart(2, "0")}`; + return formattedDate; +} + +export const useFormatPrice = (data, currency = "KRW") => { + if (typeof data !== "number") return "가격 정보 없음"; + return data.toLocaleString("ko-KR", { + style: "decimal", + currency: currency, + }); +}; + +export const useFormatUpDate = (timestamp) => { + const now = new Date(); + const past = new Date(timestamp); + const diff = (now - past) / 1000; + + if (diff < 60) { + return "1분전"; + } else if (diff < 3600) { + return `${Math.floor(diff / 60)}분 전`; // 1시간 미만 + } else if (diff < 86400) { + return `${Math.floor(diff / 3600)}시간 전`; // 24시간 미만 + } else if (diff < 30 * 86400) { + return `${Math.floor(diff / 86400)}일 전`; // 30일 미만 + } else { + return past.toISOString().split("T")[0]; // YYYY-MM-DD 형식 (한 달 이상 지난 경우) + } +}; diff --git a/src/Main.jsx b/src/Main.jsx new file mode 100644 index 00000000..7ac1c86c --- /dev/null +++ b/src/Main.jsx @@ -0,0 +1,70 @@ +import { Route, BrowserRouter, Routes } from "react-router-dom"; +import { createGlobalStyle } from "styled-components"; +// +import LandingPage from "./pages/LandingPage/LandingPage.jsx"; +import App from "./App.js"; +import HomePage from "./pages/HomePage/HomePage.jsx"; +import AddItem from "./pages/AddItem/AddItem.jsx"; +import Product from "./pages/ProductPage/Product.jsx"; +import Test from "./components/TestPage.jsx"; +// +// +const GlobalStyle = createGlobalStyle` + * { + box-sizing: border-box; + } + body { + font-family: 'Pretendard', sans-serif; + font-display: swap; + margin: 0; + padding: 0; + } + html { + margin: 0; + padding: 0; + } + a { + text-decoration: none; + color: #ffffff; + } + p{ + margin: 0px; + } + @font-face { + font-family: 'Pretendard'; + src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff2') format('woff2'); + src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff'); + font-display: swap; + font-weight: 400; + font-style: normal; +} + +@font-face { +font-family: "Pretendard"; +src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Bold.woff2') format('woff2'); +src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Bold.woff') format('woff'); + font-display: swap; + font-weight: 600; + font-style: normal; +} +`; +// +function Main() { + return ( + <> + + + + } /> + }> + } /> + } /> + } /> + } /> + + + + + ); +} +export default Main; diff --git a/src/api/comment.api.jsx b/src/api/comment.api.jsx new file mode 100644 index 00000000..2c861071 --- /dev/null +++ b/src/api/comment.api.jsx @@ -0,0 +1,17 @@ +import axios from "axios"; +const BASE_URL = "https://panda-market-api.vercel.app"; + +export async function getProductComments(productId, limit = 3) { + try { + const res = await axios.get( + `${BASE_URL}/products/${productId}/comments?limit=${limit}` + ); + if (!res) { + throw new Error("리뷰 불러오기 실패"); + } + return res.data; + } catch (error) { + console.error(error); + return null; + } +} diff --git a/src/api.js b/src/api/product.api.jsx similarity index 74% rename from src/api.js rename to src/api/product.api.jsx index e8001b33..0fc94123 100644 --- a/src/api.js +++ b/src/api/product.api.jsx @@ -1,3 +1,4 @@ +import axios from "axios"; const BASE_URL = "https://panda-market-api.vercel.app"; export async function getProducts({ @@ -28,3 +29,14 @@ export async function bestProducts({ device }) { const body = await response.json(); return body; } + +export async function getProductInfo(productId) { + try { + const response = await axios.get(`${BASE_URL}/products/${productId}`); + if (!response) throw new Error("제품정보 get api 실패"); + return response.data; + } catch (error) { + console.error(error, "제품정보 api 실패"); + return null; + } +} diff --git a/src/assets/btn_sort.svg b/src/assets/btn_sort.svg deleted file mode 100644 index 41751e7b..00000000 --- a/src/assets/btn_sort.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/assets/favoriteLogo.svg b/src/assets/favoriteLogo.svg deleted file mode 100644 index 282d802a..00000000 --- a/src/assets/favoriteLogo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icons/active.heart.icon.svg b/src/assets/icons/active.heart.icon.svg new file mode 100644 index 00000000..a2290cd2 --- /dev/null +++ b/src/assets/icons/active.heart.icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/myLogo.svg b/src/assets/icons/default.profile.icon.svg similarity index 100% rename from src/assets/icons/myLogo.svg rename to src/assets/icons/default.profile.icon.svg diff --git a/src/assets/icons/inactive.heart.icon.svg b/src/assets/icons/inactive.heart.icon.svg new file mode 100644 index 00000000..e7e9f3ed --- /dev/null +++ b/src/assets/icons/inactive.heart.icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/kebab.icon.svg b/src/assets/icons/kebab.icon.svg new file mode 100644 index 00000000..dd7ed7f5 --- /dev/null +++ b/src/assets/icons/kebab.icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/return.icon.svg b/src/assets/icons/return.icon.svg new file mode 100644 index 00000000..a8265375 --- /dev/null +++ b/src/assets/icons/return.icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/select.icon.svg b/src/assets/icons/select.icon.svg new file mode 100644 index 00000000..358e4b87 --- /dev/null +++ b/src/assets/icons/select.icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/no-comments.svg b/src/assets/no-comments.svg new file mode 100644 index 00000000..5444cbbb --- /dev/null +++ b/src/assets/no-comments.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/components/CommentCard/CommentCard.jsx b/src/components/CommentCard/CommentCard.jsx new file mode 100644 index 00000000..486ad262 --- /dev/null +++ b/src/components/CommentCard/CommentCard.jsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +// +import * as S from "./CommentCard.style.jsx"; +import defaultImg from "../../assets/icons/default.profile.icon.svg"; +import { Input } from "../common/Input/Input.jsx"; +import { EditSelect } from "../common/Select/Select.jsx"; +import { button } from "../../constants/globalConstant.jsx"; +import { useFormatUpDate } from "../../hooks/useFormatting.jsx"; +// +export default function CommentCard({ data }) { + const [initialValue, setInitialValue] = useState(data.content); + const [isEditing, setIsEditing] = useState(null); + const [isDelete, setIsDelete] = useState(null); + const formattedUpdate = useFormatUpDate(data.updatedAt); + // + const handleOnChange = (option) => { + if (option === button.edit) { + setIsEditing(data.id); + } + if (option === button.delete) { + setIsDelete(data.id); + } + return; + }; + + useEffect(() => { + //삭제 리퀘스트 예정 + }, [isDelete]); + // + return ( + + + {data.id === isEditing ? ( + setInitialValue(target.value)} + /> + ) : ( + {initialValue} + )} + + + + {data.writer.image ? ( + + ) : ( + + )} + + + {data.writer.nickname} + {formattedUpdate} + + + {data.id === isEditing && ( +
+ setIsEditing("")}> + {button.cancel} + + setIsEditing("")}> + {button.editConfirm} + +
+ )} +
+
+ {data.id !== isEditing && } +
+ ); +} diff --git a/src/components/CommentCard/CommentCard.style.jsx b/src/components/CommentCard/CommentCard.style.jsx new file mode 100644 index 00000000..ab3dfd39 --- /dev/null +++ b/src/components/CommentCard/CommentCard.style.jsx @@ -0,0 +1,68 @@ +import styled from "styled-components"; +import theme from "../../style/theme"; + +export const CommentWrapper = styled.div` + width: 100%; + height: auto; + display: flex; + justify-content: space-between; + border-bottom: 1px solid ${theme.color.gray200}; + padding-bottom: 12px; +`; +export const CommentFlex = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; +`; +export const Content = styled.p` + font: ${theme.font.H7Regular}; +`; +export const ProfileWrapper = styled.div` + display: flex; + justify-content: space-between; + gap: 8px; +`; +export const ProfileDateWrapper = styled.div` + display: flex; + gap: 8px; +`; + +export const ProfileImg = styled.img` + width: 32px; + height: 32px; + border-radius: 99px; + border: none; +`; +export const NickNameDateWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +export const NickName = styled.p` + font: ${theme.font.H8}; +`; +export const Date = styled.p` + font: ${theme.font.H8}; + color: ${theme.color.gray400}; +`; + +export const CancelBtn = styled.button` + width: 68px; + height: 47px; + font: ${theme.font.H5Bold}; + border: none; + background-color: ${theme.color.white}; + color: ${theme.color.gray500}; +`; + +export const EditConfirmBtn = styled.button` + width: 106px; + height: 42px; + border-radius: 8px; + font: ${theme.font.H5Bold}; + border: none; + background-color: ${theme.color.blue}; + color: ${theme.color.white}; +`; diff --git a/src/components/Main.js b/src/components/Main.js deleted file mode 100644 index c04bf732..00000000 --- a/src/components/Main.js +++ /dev/null @@ -1,48 +0,0 @@ -import { Route, BrowserRouter, Routes } from "react-router-dom"; -import { createGlobalStyle } from "styled-components"; -// -import LandingPage from "../pages/LandingPage/LandingPage.jsx"; -import App from "../App"; -import HomePage from "../pages/HomePage"; -import AddItem from "../pages/AddItem/AddItem.jsx"; -// -import useWindowSize from "../hooks/useWindowSize"; -// -const GlobalStyle = createGlobalStyle` - * { - box-sizing: border-box; - } - body { - font-family: 'Pretendard', sans-serif; - font-display: swap; - margin: 0; - padding: 0; - } - html { - margin: 0; - padding: 0; - } - a { - text-decoration: none; - color: #ffffff; - } -`; -// -function Main() { - const deviceType = useWindowSize(); - return ( - <> - - - - } /> - }> - } /> - } /> - - - - - ); -} -export default Main; diff --git a/src/components/SelectBox.js b/src/components/SelectBox.js deleted file mode 100644 index ab586123..00000000 --- a/src/components/SelectBox.js +++ /dev/null @@ -1,75 +0,0 @@ -import Select from "react-select"; -import SelectImg from "../assets/btn_sort.svg"; -import useWindowSize from "../hooks/useWindowSize"; -import styled from "styled-components"; - -// -const CustomSelectWrapper = styled.div` - width: ${({ device }) => (device !== "mobile" ? "130px" : "42px")}; - height: 42px; - order: ${({ device }) => (device === "mobile" ? 3 : 4)}; - padding: "0px 0px"; - border-radius: 12px; -`; -const customStyles = () => ({ - control: (styles) => ({ - ...styles, - padding: "0px 0px", - width: `${({ device }) => (device !== "mobile" ? "130px" : "42px")}`, - backgroundColor: "#ffffff", - border: "none", - }), - valueContainer: (styles) => ({ - ...styles, - width: "100%", - padding: "0px 0px", - }), - indicatorsContainer: (styles) => ({ - ...styles, - display: "none", - }), - menu: (styles) => ({ - ...styles, - borderRadius: "14px", - padding: "0px 0px", - }), - option: (styles) => ({ - ...styles, - backgroundColor: "#ffffff", - color: "#1F2937", - width: "130px", - padding: "0px 0px", - }), - placeholder: (styles) => ({ - ...styles, - textAlign: "center", - padding: "0px 0px", - backgroundImage: `url(${SelectImg}) no-repeat center/cover`, - }), -}); - -const options = [ - { value: "recent", label: "최신순" }, - - { value: "favorite", label: "좋아요" }, -]; -function SelectBox({ value, onChange }) { - const device = useWindowSize(); - - return ( - - - - ); -} -export default SelectBox; diff --git a/src/components/Tag/Tag.jsx b/src/components/Tag/Tag.jsx index fb4f9f2d..f84891cf 100644 --- a/src/components/Tag/Tag.jsx +++ b/src/components/Tag/Tag.jsx @@ -1,15 +1,27 @@ import DeleteButton from "../../assets/icons/DeleteIcon.svg"; import * as S from "./Tag.style"; -export default function Tag({ value, onClick }) { - const handleClickDeleteBtm = () => { - onClick(value); - }; +export default function Tag({ tags, ...props }) { + const { onClick, ...rest } = props; + //...rest : $product(gap8px,items/id페이지) return ( - - - #{value} - - - + + {tags?.length > 0 && + tags.map((tag) => { + return ( + + + #{tag} + {!rest.$product && ( + onClick(tag)} + src={DeleteButton} + /> + )} + + + ); + })} + ); } diff --git a/src/components/Tag/Tag.style.jsx b/src/components/Tag/Tag.style.jsx index 2ad5d95a..2c24e7bf 100644 --- a/src/components/Tag/Tag.style.jsx +++ b/src/components/Tag/Tag.style.jsx @@ -1,6 +1,17 @@ -import styled from "styled-components"; +import styled, { css } from "styled-components"; import theme from "../../style/theme"; +export const TagsContainer = styled.div` + width: 100%; + display: flex; + gap: 12px; + flex-wrap: wrap; + ${(props) => + props.$product && + css` + gap: 8px; + `} +`; export const Container = styled.div` background-color: ${theme.color.gray100}; width: auto; @@ -22,8 +33,12 @@ export const Tag = styled.div` font-weight: 400; font-size: 16px; line-height: 26px; + @media (max-width: 375px) { + } + @media (min-width: 376px) and (max-width: 768px) { + } `; export const DeleteButton = styled.img` width: 22px; - height: 24px; + height: 22px; `; diff --git a/src/components/TestPage.jsx b/src/components/TestPage.jsx new file mode 100644 index 00000000..e6a3ccb4 --- /dev/null +++ b/src/components/TestPage.jsx @@ -0,0 +1,19 @@ +/// 컴포넌트 테스트 페이지 완료후 삭제 예정 + +// import Button from "./common/Button/Button"; +// import BtnHeart from "./common/BtnHeart/BtnHeart"; +// import Tag from "../components/Tag/Tag"; +// import { Input } from "./common/Input/Input"; +export default function TestPage() { + // //태그 테스트 배열 + // const tag3 = []; + // const tag1 = ["태그1", "태그2"]; + // const tags = ["태그1", "태그2", "태그3", "태그4"]; + return ( + <> + {/* + + */} + + ); +} diff --git a/src/components/common/BtnHeart/BtnHeart.jsx b/src/components/common/BtnHeart/BtnHeart.jsx new file mode 100644 index 00000000..4ee40f3e --- /dev/null +++ b/src/components/common/BtnHeart/BtnHeart.jsx @@ -0,0 +1,36 @@ +import activeHeart from "../../../assets/icons/active.heart.icon.svg"; +import inactiveHeart from "../../../assets/icons/inactive.heart.icon.svg"; +// +import * as S from "./BtnHeart.style"; +// +export default function BtnHeart({ value, ...props }) { + const { border, active, onClick, small, ...rest } = props; + const onClickChange = (e) => { + onClick(e); + }; + return ( + <> + {active ? ( + + + {value} + + ) : ( + + + {value} + + )} + + ); +} diff --git a/src/components/common/BtnHeart/BtnHeart.style.jsx b/src/components/common/BtnHeart/BtnHeart.style.jsx new file mode 100644 index 00000000..78db533a --- /dev/null +++ b/src/components/common/BtnHeart/BtnHeart.style.jsx @@ -0,0 +1,70 @@ +import styled, { css } from "styled-components"; +import theme from "../../../style/theme"; +// + +export const InactiveBtnHeart = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + width: 87px; + height: 40px; + border: none; + border-radius: 35px; + font-weight: 500; + font-size: 16px; + line-height: 26px; + + background: ${theme.color.white}; + color: ${theme.color.gray500}; + ${(props) => + props.$small && + css` + width: 79px; + height: 32px; + `}; + ${(props) => + props.$border && + css` + border: 1px solid ${theme.color.gray200}; + `}; +`; + +export const ActiveBtnHeart = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 87px; + height: 40px; + border: none; + border-radius: 35px; + font-weight: 500; + font-size: 16px; + line-height: 26px; + background: ${theme.color.white}; + color: ${theme.color.gray500}; + ${(props) => + props.$small && + css` + width: 79px; + height: 32px; + `}; + ${(props) => + props.$border && + css` + border: 1px solid ${theme.color.gray200}; + `}; +`; + +export const HeartImg = styled.img` + width: 26px; + height: 23px; + + ${(props) => + props.$small && + css` + width: 20px; + height: 17px; + `} +`; diff --git a/src/components/common/Button/Button.jsx b/src/components/common/Button/Button.jsx index 1eb339a0..17e28380 100644 --- a/src/components/common/Button/Button.jsx +++ b/src/components/common/Button/Button.jsx @@ -1,10 +1,39 @@ import * as S from "./Button.style"; // //버튼 컨테이너 필요 -export default function Button({ children, disabled, ...props }) { +export default function Button({ onClick, ...props }) { + const { + $small, + medium, + value, + toggle, + square, + circle, + heart, + children, + disabled, + ...rest + } = props; + //...rest: $square,$small,$medium,toggle,$circle,children,disabled + const onClickChange = (e) => { + onClick(e); + }; return ( - - {children} - + <> + {toggle ? ( + + + + {comments?.list?.length === 0 ? ( + + + + 아직 문의가 없어요 + + + ) : ( + + {comments?.list?.map((data) => ( + + ))} + + )} + + + ); +} diff --git a/src/pages/ProductPage/Comments.style.jsx b/src/pages/ProductPage/Comments.style.jsx new file mode 100644 index 00000000..4affe503 --- /dev/null +++ b/src/pages/ProductPage/Comments.style.jsx @@ -0,0 +1,50 @@ +import styled from "styled-components"; +import theme from "../../style/theme"; +// + +export const CommentWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + @media (max-width: 768px) { + gap: 40px; + } +`; +export const InputWrapper = styled.div` + width: 100%; + + display: flex; + flex-direction: column; + gap: 16px; +`; +export const NoCommentWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 48px; +`; +export const NoCommentImgWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: ${theme.color.gray400}; +`; +export const NoCommentImg = styled.img` + width: 196px; + aspect-ratio: 1/1; +`; + +export const ButtonWrapper = styled.div` + margin-left: auto; + width: 74px; + height: 42px; +`; + +export const CommentCardContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; +`; diff --git a/src/pages/ProductPage/Product.jsx b/src/pages/ProductPage/Product.jsx new file mode 100644 index 00000000..a13ff4ab --- /dev/null +++ b/src/pages/ProductPage/Product.jsx @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +// +import ProductInfo from "./ProductInfo"; +import Comments from "./Comments"; +import * as S from "./Product.style"; +// +export default function Product() { + const { productId } = useParams(); + + return ( + <> + + + + + + + + + + ); +} diff --git a/src/pages/ProductPage/Product.style.jsx b/src/pages/ProductPage/Product.style.jsx new file mode 100644 index 00000000..0560a3e3 --- /dev/null +++ b/src/pages/ProductPage/Product.style.jsx @@ -0,0 +1,25 @@ +import styled from "styled-components"; + +export const PageWrapper = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + //TODO전역 스타일 화 하기 +`; +export const ContentsWrapper = styled.div` + width: 1200px; + padding: 24px; +`; +export const Contents = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 80px; + @media (max-width: 375px) { + gap: 24px; + } + @media (min-width: 376px) and (max-width: 768px) { + gap: 40px; + } +`; diff --git a/src/pages/ProductPage/ProductInfo.jsx b/src/pages/ProductPage/ProductInfo.jsx new file mode 100644 index 00000000..f3089b26 --- /dev/null +++ b/src/pages/ProductPage/ProductInfo.jsx @@ -0,0 +1,71 @@ +import { useState, useEffect } from "react"; +// +import profile from "../../assets/icons/default.profile.icon.svg"; +// +import { getProductInfo } from "../../api/product.api"; +import * as S from "./ProductInfo.style"; +import Tag from "../../components/Tag/Tag"; +import BtnHeart from "../../components/common/BtnHeart/BtnHeart"; +import { EditSelect } from "../../components/common/Select/Select"; +import { useFormatDate, useFormatPrice } from "../../hooks/useFormatting"; +// + +export default function ProductInfo({ productId }) { + const [product, setProduct] = useState({}); + const formattedDate = useFormatDate(product.createdAt); + const formattedPrice = useFormatPrice(product.price, "KRW"); + // + const handleLoad = async () => { + const info = await getProductInfo(productId); + setProduct(info); + }; + + useEffect(() => { + handleLoad(); + }, []); + const handleOnChange = () => {}; + return ( + + {product?.images && product?.images?.length > 0 ? ( + + + + ) : ( + 이미지가 없습니다. + )} + + + + + {product.name} + {formattedPrice}원 + + + +
+ + 상품소개 + {product.description} + + + 상품 태그 + + +
+
+ + + +
+ {product.ownerNickname} + {formattedDate} +
+
+ + + +
+
+
+ ); +} diff --git a/src/pages/ProductPage/ProductInfo.style.jsx b/src/pages/ProductPage/ProductInfo.style.jsx new file mode 100644 index 00000000..671e6105 --- /dev/null +++ b/src/pages/ProductPage/ProductInfo.style.jsx @@ -0,0 +1,144 @@ +import styled from "styled-components"; +import theme from "../../style/theme"; +// +export const ProductInfoWrapper = styled.div` + display: flex; + gap: 24px; + padding-bottom: 40px; + border-bottom: 1px solid ${theme.color.gray200}; + @media (max-width: 375px) { + flex-direction: column; + padding-bottom: 24px; + gap: 16px; + } + @media (min-width: 375px) and (max-width: 590px) { + flex-direction: column; + padding-bottom: 32px; + gap: 16px; + } + @media (min-width: 591px) and (max-width: 768px) { + gap: 16px; + padding-bottom: 32px; + } +`; + +export const ProductImg = styled.img` + width: 100%; + height: 100%; + border-radius: 16px; +`; + +export const ImgDiv = styled.div` + width: 486px; + height: 486px; + aspect-ratio: 1/1; + @media (max-width: 375px) { + min-width: 343px; + min-height: 343px; + } + @media (min-width: 375px) and (max-width: 590px) { + min-width: 343px; + min-height: 343px; + margin: 0px auto; + } + @media (min-width: 376px) and (max-width: 768px) { + max-width: 340px; + max-height: 340px; + } +`; +export const InfoProfileWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 62px; +`; +export const InfoTagWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; + +export const TitleEditBtnWrapper = styled.div` + display: flex; + justify-content: space-between; + padding-bottom: 16px; + border-bottom: 1px solid ${theme.color.gray200}; +`; +export const TitlePriceWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; +export const ProductTitle = styled.p` + font: ${theme.font.H2Bold}; + @media (max-width: 375px) { + font: ${theme.font.H5Bold}; + } + @media (min-width: 376px) and (max-width: 768px) { + font: ${theme.font.H3Bold}; + } +`; + +export const ProductPrice = styled.p` + font: ${theme.font.H1}; + @media (max-width: 375px) { + font: ${theme.font.H2Bold}; + } + @media (min-width: 376px) and (max-width: 768px) { + font-size: 32px; + } +`; + +export const DesTitleContentWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + @media (max-width: 375px) { + gap: 8px; + } +`; +export const DescriptionTitle = styled.p` + font: ${theme.font.H5Bold}; + color: ${theme.color.gray600}; //4b5563 + @media (max-width: 768px) { + font: ${theme.font.H7Bold}; + } +`; + +export const ProductContent = styled.div` + overflow-y: scroll; + height: 104px; + font: ${theme.font.H5Regular}; + @media (max-width: 768px) { + height: 156px; + } +`; +export const ProfileFavorite = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; +export const ProfileWrapper = styled.div` + display: flex; + gap: 16px; +`; +export const BorderLeft = styled.div` + border-left: 1px solid ${theme.color.gray200}; + padding-left: 24px; + height: 34px; +`; +export const ProfileImg = styled.img` + width: 40px; + aspect-ratio: 1/1; +`; +export const NickNameDate = styled.div` + display: flex; +`; +export const NickName = styled.p` + font: ${theme.font.H7Medium}; + color: ${theme.color.gray600}; +`; +export const CreatedAt = styled.p` + font: ${theme.font.H7Regular}; + color: ${theme.color.gray400}; +`; diff --git a/src/style/theme.jsx b/src/style/theme.jsx index 4f5f785e..b9d2a023 100644 --- a/src/style/theme.jsx +++ b/src/style/theme.jsx @@ -4,7 +4,7 @@ const theme = { gray800: "#1f2937", gray700: "#374151", gray600: "#4b5563", - gray500: "#6b7280", + gray500: "#737373", gray400: "#9ca3af", gray200: "#e5e7eb", gray100: "#f3f4f6", @@ -15,5 +15,23 @@ const theme = { backgroundLightGray: "#fcfcfc", inputRed: "#f74747", }, + font: { + //weight , size , height , fontfamily + H1: "600 40px/47px 'Pretendard', sans-serif", + H2Bold: "600 24px/32px 'Pretendard', sans-serif", + H2Regular: "400 24px/36px 'Pretendard', sans-serif", + H3Bold: "600 20px/30px 'Pretendard', sans-serif", + H3Regular: "400 20px/30px 'Pretendard', sans-serif", + H4Bold: "600 18px/28px 'Pretendard', sans-serif", + H4Regular: "400 18px/28px 'Pretendard', sans-serif", + H5Bold: "600 16px/26px 'Pretendard', sans-serif", + H5Regular: "400 16px/26px 'Pretendard', sans-serif", + H6Bold: "600 15px/22px 'Pretendard', sans-serif", + H6Regular: "400 15px/22px 'Pretendard', sans-serif", + H7Bold: "600 14px/20px 'Pretendard', sans-serif", + H7Medium: "500 14px/24px 'Pretendard', sans-serif", + H7Regular: "400 14px/24px 'Pretendard', sans-serif", + H8: "400 12px/18px 'Pretendard', sans-serif", + }, }; export default theme;