From a91ec387aebca861275c2fc94c0075701d25ef90 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 7 Aug 2025 22:07:54 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=ED=85=9C=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 ++ src/components/Product.jsx | 47 +++++++++++++++++++--------------- src/pages/items/ItemDetail.jsx | 11 ++++++++ 3 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 src/pages/items/ItemDetail.jsx diff --git a/src/App.jsx b/src/App.jsx index 3c14e592..54629dbc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import AuthLayout from "./layout/AuthLayout"; import ItemsLayout from "./layout/ItemsLayout"; import Items from "./pages/items/Items"; import AddItem from "./pages/items/AddItem"; +import ItemDetail from "./pages/items/ItemDetail"; function App() { return ( @@ -27,6 +28,7 @@ function App() { }> } /> } /> + } /> diff --git a/src/components/Product.jsx b/src/components/Product.jsx index be536947..ededf8ba 100644 --- a/src/components/Product.jsx +++ b/src/components/Product.jsx @@ -1,35 +1,42 @@ -import { memo } from 'react'; -import ic_heart from '../assets/icons/ic_heart.svg'; +import { memo } from "react"; +import ic_heart from "../assets/icons/ic_heart.svg"; +import { Link } from "react-router"; function Product({ products, style }) { return ( <>
{products && products.map((product) => { return ( -
- -

{product.name}

-

- {product.price - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '원'} -

-
- 하트_아이콘 - - {product.favoriteCount} - + +
+ +

{product.name}

+

+ {product.price + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "원"} +

+
+ 하트_아이콘 + + {product.favoriteCount} + +
-
+ ); })}
diff --git a/src/pages/items/ItemDetail.jsx b/src/pages/items/ItemDetail.jsx new file mode 100644 index 00000000..fca86dca --- /dev/null +++ b/src/pages/items/ItemDetail.jsx @@ -0,0 +1,11 @@ +import { useParams } from "react-router"; + +export default function ItemDetail() { + const params = useParams(); + + return ( + <> +
+ + ); +} From db26af844ef5c7c58b3f21c51d1329a774d11682 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 7 Aug 2025 22:38:30 +0900 Subject: [PATCH 02/20] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f768e33f..e8b281de 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,152 @@ -# React + Vite +# 판다 마켓 - 중고 거래 플랫폼 -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +## 미션 3 요구사항 -Currently, two official plugins are available: +### 기본 -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- [x] 랜딩 페이지의 url path는 루트(‘/’) 입니다. +- [x] title은 “판다마켓”으로 설정해 주세요. +- [x] 화면의 너비가 1920px 이상이면 하늘색 배경색은 너비를 꽉 채우도록 채워지고, 내부 요소들의 위치는 고정되고, 여백만 커지도록 해주세요. +- [x] 화면의 너비가 1920px 보다 작아질 때, “판다마켓” 로고의 왼쪽 여백 200px“로그인" 버튼의 오른쪽 여백 200px이 유지되고, 화면의 너비가 작아질수록 두 요소간 거리가 가까워지도록 해주세요. +- [x] 클릭으로 기능이 동작해야 하는 경우, 사용자가 클릭할 수 있는 요소임을 알 수 있도록 cursor: pointer를 설정해 주세요. +- [x] “판다마켓” 클릭 시 루트 페이지(‘/’)로 이동시켜주세요. +- [x] “구경하러 가기" 클릭 시 (“/items”)페이지로 이동시켜주세요.(빈 페이지) +- [x] “로그인”버튼 클릭 시 로그인 페이지(‘/login’)로 이동합니다 +- [x] “구경하러가기”버튼 클릭 시(’/items’)로 이동합니다 +- [x] 페이스북, 트위터, 유튜브, 인스타그램 아이콘은 클릭 시 각각의 홈페이지로 새로운 창이 열리면서 이동 합니다 +- [x] “Privacy Policy”, “FAQ”는 클릭 시 각각 아래 페이지로 이동합니다- Privacy 페이지(‘/privacy’) - FAQ 페이지(‘/faq’) + +--- + +- [x] 아래로 스크롤 해도 상단 네비게이션 바(Global Navigation Bar)가 최상단에 고정됩니다. +- [x] “판다마켓" 클릭 시 루트 페이지(“/”)로 이동합니다. (새로고침) +- [x] 로그인 페이지, 회원가입 페이지 모두 로고 위 상단 여백이 동일합니다. +- [x] SNS 아이콘들은 클릭시 각각 아래 페이지로 이동합니다.- “https://www.google.com/”, “https://www.kakaocorp.com/page/” +- [x] “회원 가입”버튼 클릭 시 “/signup” 페이지로 이동합니다. + +--- + +- [x] 브라우저에 현재 보이는 화면의 영역(viewport) 너비를 기준으로 분기되는 반응형 디자인을 적용합니다.- PC: 1200px 이상- Tablet: 768px 이상 ~ 1199px 이하- Mobile: 375px 이상 ~ 767px 이하\* 375px 미만 사이즈의 디자인은 고려하지 않습니다 +- [x] Tablet 사이즈로 작아질 때 “판다마켓” 로고의 왼쪽에 여백 24px, “로그인” 버튼 오른쪽 여백 24px을 유지할 수 있도록 “판다마켓” 로고와 “로그인" 버튼의 간격이 가까워집니다. +- [x] Mobile 사이즈로 작아질 때 “판다마켓” 로고의 왼쪽에 여백 16px, “로그인” 버튼 오른쪽 여백 16px을 유지할 수 있도록 “판다마켓” 로고와 “로그인" 버튼의 간격이 가까워집니다. +- [x] Tablet 사이즈에서 내부 디자인은 PC사이즈와 동일합니다. +- [x] Mobile 사이즈에서 좌우 여백 16px 제외하고 내부 요소들이 너비를 모두 차지합니다. +- [x] Mobile 사이즈에서 내부 요소들의 너비는 기기의 너비가 커지는 만큼 커지지만 400px을 넘지 않습니다. +- [x] Mobile 사이즈에서 좌우 여백 16px 제외하고 내부 요소들이 너비를 모두 차지합니다. +- [x] Mobile 사이즈에서 내부 요소들의 너비는 기기의 너비가 커지는 만큼 커지지만 400px을 넘지 않습니다. + +### 심화 + +- [x] 사용자의 브라우저가 크고 작아짐에 따라 페이지의 요소간 간격, 요소의 크기, font-size 등 모든 크기와 관련된 값이 크고 작아지도록 설정해 보세요.(설정값은 자유입니다) + +--- + +- [x] palette에 있는 color값들을 css 변수로 등록하고 사용합니다. + +--- + +- [x] 페이스북, 카카오톡, 디스코드, 트위터 등 SNS에서 Linkbrary 랜딩 페이지(“/”) 공유 시 좌측 예시와 같은 미리보기를 볼 수 있도록 랜딩 페이지 메타 태그를 설정해 주세요. +- [x] 미리보기에서 제목은 “판다 마켓”, 설명은 “일상의 모든 물건을 거래해보세요”로 설정합니다. +- [x] 주소와 이미지는 자유롭게 설정하세요. + +## 주요 변경사항 + +- Netlify에서 로고 이미지가 보이게 수정했습니다. + +## 스크린샷 + +**메인화면** +| PC | Tablet | Mobile | +| :-: | :----: | :----: | +|![pc_main](https://github.com/user-attachments/assets/ab4254e0-c732-49b6-8eb3-becf313e4a92)|![tablet_main](https://github.com/user-attachments/assets/28e8b861-bf77-47b1-9f60-93267f592650)|![mobile_main](https://github.com/user-attachments/assets/fe76d76f-9b93-4403-8205-ec875e364f7f)| + +
+ +**로그인 화면** +| PC | Mobile | +| :-: | :----: | +|![pc_login](https://github.com/user-attachments/assets/aa9ff9df-8eff-4f13-a7e4-e946ad8fe48f)|![mobile_login](https://github.com/user-attachments/assets/8e1662fb-4544-48ce-a791-40e3e5ce0e28)| + +
+ +**회원가입 화면** +| PC | Mobile | +| :-: | :----: | +|![join_pc](https://github.com/user-attachments/assets/52788e32-a174-4441-abe1-957bc36094e7)|![join_mobile](https://github.com/user-attachments/assets/6222a188-d431-433c-959a-07c052ff61a5)| + +## 멘토에게 + +- 저는 CSS를 작성할 때 대부분의 단위를 `px`로 사용했는데, `rem`, `vw`와 같은 단위를 사용하는 것이 더 나은지 궁금합니다 + > 답변 + > [px vs rem vs em 접근성과 관련된 아티클](https://www.joshwcomeau.com/css/surprising-truth-about-pixels-and-accessibility/) + +
+ +## 미션 5 요구사항 + +### 기본 + +- [x] 중고마켓 페이지 주소는 "/items" 입니다. +- [x] 페이지 주소가 "/items" 일 때 상단 네비게이션 바의 "중고마켓" 버튼의 색상은 "3692FF" 입니다. +- [x] 상단 네비게이션 바는 이전 미션에서 구현한 랜딩 페이지와 동일한 스타일로 만들어주세요 +- [x] 전체 상품에서 드롭다운으로 "최신순" 또는 "좋아요순"을 선택해서 정렬할 수 있습니다. +- [ ] "상품 등록하기" 버튼을 누르면 “/additem” 로 이동합니다 ( 빈 페이지 ) +- [x] 미디어 쿼리를 사용하여 반응형 view 마다 물품 개수를 다르게 보여줍니다 (서버로 요청하는 값은 동일) +- 베스트 상품 + - Desktop : 4개 보이기 + - Tablet : 2개 보이기 + - Mobile : 1개 보이기 +- 전체 상품 + - Desktop : 10개 보이기 + - Tablet : 6개 보이기 + - Mobile : 4개 보이기 + +### 심화 + +- [x] 페이지 네이션 기능을 구현합니다. +- [x] 반응형으로 보여지는 물품들의 개수를 다르게 설정할때 서버에 보내는 pageSize값을 적절하게 설정합니다. + +## 주요 변경사항 + +- 스프린트 미션 1부터 4까지의 내용을 React로 변경했습니다. + > form 변경 내용 + > (React-Hook-form을 이용해 유효성 검사 해보기)[https://velog.io/@nudge0613/React-Hook-Form%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-%ED%95%98%EA%B8%B0] + +## 스크린샷 + +| PC | Tablet | Mobile | +| :-------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------: | +| localhost_5173_items | localhost_5173_items (1) | localhost_5173_items (2) | + +
+ +## 미션 6 요구사항 + +### 기본 + +- [x] 상품 등록 페이지 주소는 “/additem” 입니다. +- [x] 페이지 주소가 “/additem” 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 “3692FF”입니다. +- [x] 상품 이미지는 최대 한개 업로드가 가능합니다. +- [x] 각 input의 placeholder 값을 정확히 입력해주세요. +- [x] 이미지를 제외하고 input 에 모든 값을 입력하면 ‘등록' 버튼이 활성화 됩니다. +- [ ] API를 통한 상품 등록은 추후 미션에서 적용합니다. + +### 심화 + +- [x] 이미지 안의 X 버튼을 누르면 이미지가 삭제됩니다. +- [x] 추가된 태그 안의 X 버튼을 누르면 해당 태그는 삭제됩니다. + +## 주요 변경사항 + +- styled component를 통해 스타일링 + +## 스크린샷 + +| PC | Tablet | Mobile | +| :---------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------: | +| localhost_5173_additem | localhost_5173_additem (1) | localhost_5173_additem (2) | + +## 멘토에게 + +- styled component를 사용해서 스타일링을 했는데 한 컴포넌트의 코드가 너무 길어지는 느낌입니다. + 스타일은 따로 빼놓는게 나을까요? From d9fbfe42f0473db9f18db3ee005fffcccae4517c Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 7 Aug 2025 22:39:49 +0900 Subject: [PATCH 03/20] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e8b281de..3d6cf6d0 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ - [x] 미리보기에서 제목은 “판다 마켓”, 설명은 “일상의 모든 물건을 거래해보세요”로 설정합니다. - [x] 주소와 이미지는 자유롭게 설정하세요. -## 주요 변경사항 +### 주요 변경사항 - Netlify에서 로고 이미지가 보이게 수정했습니다. -## 스크린샷 +### 스크린샷 **메인화면** | PC | Tablet | Mobile | @@ -74,7 +74,7 @@ | :-: | :----: | |![join_pc](https://github.com/user-attachments/assets/52788e32-a174-4441-abe1-957bc36094e7)|![join_mobile](https://github.com/user-attachments/assets/6222a188-d431-433c-959a-07c052ff61a5)| -## 멘토에게 +### 멘토에게 - 저는 CSS를 작성할 때 대부분의 단위를 `px`로 사용했는데, `rem`, `vw`와 같은 단위를 사용하는 것이 더 나은지 궁금합니다 > 답변 @@ -106,13 +106,13 @@ - [x] 페이지 네이션 기능을 구현합니다. - [x] 반응형으로 보여지는 물품들의 개수를 다르게 설정할때 서버에 보내는 pageSize값을 적절하게 설정합니다. -## 주요 변경사항 +### 주요 변경사항 - 스프린트 미션 1부터 4까지의 내용을 React로 변경했습니다. > form 변경 내용 > (React-Hook-form을 이용해 유효성 검사 해보기)[https://velog.io/@nudge0613/React-Hook-Form%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-%ED%95%98%EA%B8%B0] -## 스크린샷 +### 스크린샷 | PC | Tablet | Mobile | | :-------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------: | @@ -136,17 +136,17 @@ - [x] 이미지 안의 X 버튼을 누르면 이미지가 삭제됩니다. - [x] 추가된 태그 안의 X 버튼을 누르면 해당 태그는 삭제됩니다. -## 주요 변경사항 +### 주요 변경사항 - styled component를 통해 스타일링 -## 스크린샷 +### 스크린샷 | PC | Tablet | Mobile | | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------: | | localhost_5173_additem | localhost_5173_additem (1) | localhost_5173_additem (2) | -## 멘토에게 +### 멘토에게 - styled component를 사용해서 스타일링을 했는데 한 컴포넌트의 코드가 너무 길어지는 느낌입니다. 스타일은 따로 빼놓는게 나을까요? From b1ca77f7d2c68c33ebbc2e2d2cc41ffb0192559c Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 7 Aug 2025 22:40:56 +0900 Subject: [PATCH 04/20] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3d6cf6d0..9155bef7 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,7 @@ ### 주요 변경사항 - 스프린트 미션 1부터 4까지의 내용을 React로 변경했습니다. - > form 변경 내용 - > (React-Hook-form을 이용해 유효성 검사 해보기)[https://velog.io/@nudge0613/React-Hook-Form%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-%ED%95%98%EA%B8%B0] + > form 변경 내용: [React-Hook-form을 이용해 유효성 검사 해보기](https://velog.io/@nudge0613/React-Hook-Form%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-%ED%95%98%EA%B8%B0) ### 스크린샷 From 7209459ad29391e95966e70c09107ecd33f3b82a Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 8 Aug 2025 17:14:07 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A0=95=EB=B3=B4,=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20textarea=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + src/assets/icons/ic_heart_active_large.svg | 3 + src/assets/icons/ic_heart_inactive_large.svg | 3 + src/components/Product.jsx | 1 + src/pages/items/ItemDetail.jsx | 135 ++++++++++++++++++- src/services/itemsApi.js | 34 +++-- src/styles/commonStyles.js | 5 + src/styles/items/ItemDetailStyle.js | 113 ++++++++++++++++ 8 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 src/assets/icons/ic_heart_active_large.svg create mode 100644 src/assets/icons/ic_heart_inactive_large.svg create mode 100644 src/styles/items/ItemDetailStyle.js diff --git a/README.md b/README.md index 9155bef7..5d919827 100644 --- a/README.md +++ b/README.md @@ -149,3 +149,5 @@ - styled component를 사용해서 스타일링을 했는데 한 컴포넌트의 코드가 너무 길어지는 느낌입니다. 스타일은 따로 빼놓는게 나을까요? + +Copyright 2025 코드잇 Inc. All rights reserved. diff --git a/src/assets/icons/ic_heart_active_large.svg b/src/assets/icons/ic_heart_active_large.svg new file mode 100644 index 00000000..aff4192d --- /dev/null +++ b/src/assets/icons/ic_heart_active_large.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/ic_heart_inactive_large.svg b/src/assets/icons/ic_heart_inactive_large.svg new file mode 100644 index 00000000..72f720d5 --- /dev/null +++ b/src/assets/icons/ic_heart_inactive_large.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Product.jsx b/src/components/Product.jsx index ededf8ba..467d1e1c 100644 --- a/src/components/Product.jsx +++ b/src/components/Product.jsx @@ -16,6 +16,7 @@ function Product({ products, style }) {
diff --git a/src/pages/items/ItemDetail.jsx b/src/pages/items/ItemDetail.jsx index fca86dca..c4383aea 100644 --- a/src/pages/items/ItemDetail.jsx +++ b/src/pages/items/ItemDetail.jsx @@ -1,11 +1,140 @@ -import { useParams } from "react-router"; +import { useLocation, useParams } from "react-router"; +import { requestProductDetail } from "../../services/itemsApi"; +import { useEffect, useState } from "react"; +import { + Heart, + HeartCount, + InquirySubmitButton, + InquiryTextArea, + ProductDate, + ProductImg, + ProductInfoBox, + ProductOwnerName, + ProductPrice, + ProductSubTitle, + ProductTextArea, + ProductTextBox, + ProductTitle, +} from "../../styles/items/ItemDetailStyle"; +import { ItemsTag, palette, ProfileImg } from "../../styles/commonStyles"; +import icProfile from "../../assets/icons/ic_profile.svg"; +import icHeartInactive from "../../assets/icons/ic_heart_inactive_large.svg"; +import icHeartActive from "../../assets/icons/ic_heart_active_large.svg"; export default function ItemDetail() { - const params = useParams(); + /** + * 상품의 id + */ + const { productId } = useParams(); + + const { state } = useLocation(); + + /** + * loading + */ + const [isLoading, setIsLoading] = useState(false); + + /** + * 상품 상세 정보를 받을 state + */ + const [productDetail, setProductDetail] = useState({}); + + /** + * 상품의 상세 정보를 받아온다. + * @param {string} id + */ + const getProductDetail = async (id) => { + try { + setIsLoading(true); + const data = await requestProductDetail(id); + if (!data) { + throw new Error("상품 정보를 불러오지 못했습니다."); + } + console.log(data); + + setProductDetail(data); + } catch (e) { + console.error(e); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + getProductDetail(productId); + }, []); return ( <> -
+
+ + + +
+
+ {state.name} +
+ + {state.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + + "원"} + +
+
+ 상품 소개 + {state.description} +
+
+ 상품 태그 +
+ {state.tags.map((tag) => { + return {`#${tag}`}; + })} +
+
+
+ +
+ + {!isLoading && productDetail.ownerNickname} + + + {state.updatedAt.slice(0, 10).replaceAll("-", ". ")} + +
+
+ + {!isLoading && productDetail.isFavorite ? ( + 좋아요 이미지 + ) : ( + 좋아요 이미지 + )} + {state.favoriteCount} + +
+
+
+
+
+

+ 문의하기 +

+ + 등록 +
+
); } diff --git a/src/services/itemsApi.js b/src/services/itemsApi.js index 50de8d16..1ae2fe45 100644 --- a/src/services/itemsApi.js +++ b/src/services/itemsApi.js @@ -3,20 +3,38 @@ * @param {string} orderBy * @returns {object} */ -const requestProductList = async (querys) => { - const url = new URL('https://panda-market-api.vercel.app/products'); - url.searchParams.append('page', querys.page); - url.searchParams.append('pageSize', querys.pageSize); - url.searchParams.append('orderBy', querys.orderBy); +export const requestProductList = async (querys) => { + const url = new URL("https://panda-market-api.vercel.app/products"); + url.searchParams.append("page", querys.page); + url.searchParams.append("pageSize", querys.pageSize); + url.searchParams.append("orderBy", querys.orderBy); const response = await fetch(url, { - method: 'get', + method: "get", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); return response.json(); }; -export { requestProductList }; +/** + * 상품의 상세 정보를 요청한다. + * @param {string} productId + * @returns {json} response + */ +export const requestProductDetail = async (productId) => { + const url = new URL( + `https://panda-market-api.vercel.app/products/${productId}` + ); + + const response = await fetch(url, { + method: "get", + headers: { + "Content-Type": "application/json", + }, + }); + + return response.json(); +}; diff --git a/src/styles/commonStyles.js b/src/styles/commonStyles.js index bb58912d..c9399260 100644 --- a/src/styles/commonStyles.js +++ b/src/styles/commonStyles.js @@ -30,3 +30,8 @@ export const ItemsTag = styled.div` padding: 15px; margin-right: 10px; `; + +export const ProfileImg = styled.img` + width: 40px; + height: 40px; +`; diff --git a/src/styles/items/ItemDetailStyle.js b/src/styles/items/ItemDetailStyle.js new file mode 100644 index 00000000..bbbc5ed0 --- /dev/null +++ b/src/styles/items/ItemDetailStyle.js @@ -0,0 +1,113 @@ +import styled from "styled-components"; +import { ItemsTag, palette } from "../commonStyles"; + +export const ProductInfoBox = styled.div` + display: flex; + margin-top: 20px; + border-bottom: 1px solid ${palette.gray200}; + padding-bottom: 40px; +`; + +export const ProductTextBox = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +export const ProductImg = styled.img` + width: 486px; + height: 486px; + border-radius: 16px; + border: none; + margin-right: 20px; +`; + +export const ProductTitle = styled.h1` + font-size: 24px; + font-weight: 600; + margin: 0; +`; + +export const ProductPrice = styled.p` + font-size: 40px; + font-weight: 600; + margin: 20px 0px 10px; +`; + +export const ProductSubTitle = styled.p` + font-size: 16px; + font-weight: 600; + color: ${palette.gray600}; + margin: 20px 0px; +`; + +export const ProductTextArea = styled.p` + font-size: 16px; + font-weight: 400; + width: 100%; + color: ${palette.gray600}; + margin: 0; +`; + +export const ProductOwnerName = styled.p` + font-size: 14px; + font-weight: 500; + margin: 0px 0px 10px 0px; + color: ${palette.gray600}; +`; + +export const ProductDate = styled.p` + font-size: 14px; + font-weight: 400; + color: ${palette.gray400}; + margin: 0; +`; + +export const Heart = styled.div` + border: 1px solid ${palette.gray200}; + border-radius: 35px; + background-color: white; + display: flex; + justify-content: space-between; + column-gap: 5px; + padding: 2px 10px; + align-items: center; +`; + +export const HeartCount = styled.p` + font-size: 16px; + font-weight: 500; + color: ${palette.gray500}; + margin: 0; +`; + +export const InquiryTextArea = styled.textarea` + background-color: ${palette.gray100}; + height: 104px; + width: 100%; + border: none; + border-radius: 12px; + resize: none; + font-family: Pretendard-Reqular; + padding: 20px; + font-size: 16px; + font-weight: 400; + margin-bottom: 10px; + &::placeholder { + color: ${palette.gray400}; + } +`; + +export const InquirySubmitButton = styled.button` + width: 74px; + height: 42px; + border: none; + color: white; + padding: 10px; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + background-color: ${({ isActive }) => + isActive ? `${palette.blue}` : `${palette.gray400}`}; + float: right; +`; From 43f3aca6a4cd1db944ab35e322494605689cd839 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 8 Aug 2025 17:40:26 +0900 Subject: [PATCH 06/20] =?UTF-8?q?rename:=20=EB=AC=B8=EC=9D=98=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Inquiry.jsx | 32 ++++++++++++++++++++ src/pages/items/ItemDetail.jsx | 21 ++++--------- src/styles/components/InquiryDetailStyles.js | 0 src/styles/items/ItemDetailStyle.js | 10 ++++++ 4 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 src/components/Inquiry.jsx create mode 100644 src/styles/components/InquiryDetailStyles.js diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx new file mode 100644 index 00000000..9bbceb01 --- /dev/null +++ b/src/components/Inquiry.jsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { palette } from "../styles/commonStyles"; +import { + InquirySubmitButton, + InquiryTextArea, +} from "../styles/items/ItemDetailStyle"; + +export default function Inquiry() { + const [isActive, setIsActive] = useState(false); + + return ( + <> +

+ 문의하기 +

+ { + e.target.value !== "" ? setIsActive(true) : setIsActive(false); + }} + placeholder="개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다." + > + 등록 + + ); +} diff --git a/src/pages/items/ItemDetail.jsx b/src/pages/items/ItemDetail.jsx index c4383aea..9a39c9d6 100644 --- a/src/pages/items/ItemDetail.jsx +++ b/src/pages/items/ItemDetail.jsx @@ -4,8 +4,6 @@ import { useEffect, useState } from "react"; import { Heart, HeartCount, - InquirySubmitButton, - InquiryTextArea, ProductDate, ProductImg, ProductInfoBox, @@ -20,6 +18,7 @@ import { ItemsTag, palette, ProfileImg } from "../../styles/commonStyles"; import icProfile from "../../assets/icons/ic_profile.svg"; import icHeartInactive from "../../assets/icons/ic_heart_inactive_large.svg"; import icHeartActive from "../../assets/icons/ic_heart_active_large.svg"; +import Inquiry from "../../components/Inquiry"; export default function ItemDetail() { /** @@ -27,6 +26,9 @@ export default function ItemDetail() { */ const { productId } = useParams(); + /** + * 목록에서 가져온 상품 정보 + */ const { state } = useLocation(); /** @@ -120,19 +122,8 @@ export default function ItemDetail() {
-
-

- 문의하기 -

- - 등록 +
+
diff --git a/src/styles/components/InquiryDetailStyles.js b/src/styles/components/InquiryDetailStyles.js new file mode 100644 index 00000000..e69de29b diff --git a/src/styles/items/ItemDetailStyle.js b/src/styles/items/ItemDetailStyle.js index bbbc5ed0..8c736402 100644 --- a/src/styles/items/ItemDetailStyle.js +++ b/src/styles/items/ItemDetailStyle.js @@ -110,4 +110,14 @@ export const InquirySubmitButton = styled.button` background-color: ${({ isActive }) => isActive ? `${palette.blue}` : `${palette.gray400}`}; float: right; + + &:hover { + background-color: ${({ isActive }) => + isActive ? `#1967d6` : `${palette.gray400}`}; + } + + &:active { + background-color: ${({ isActive }) => + isActive ? `#1251aa` : `${palette.gray400}`}; + } `; From 28dbbfcf30df732b850329a9fef023556be535e9 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 8 Aug 2025 20:39:09 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9D=98=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Inquiry.jsx | 94 +++++++++++++++----- src/pages/items/ItemDetail.jsx | 5 +- src/services/itemsApi.js | 16 ++++ src/styles/components/InquiryDetailStyles.js | 0 src/styles/components/InquiryStyles.js | 78 ++++++++++++++++ src/styles/items/ItemDetailStyle.js | 41 --------- src/util/formatTimeAgo.js | 37 ++++++++ 7 files changed, 206 insertions(+), 65 deletions(-) delete mode 100644 src/styles/components/InquiryDetailStyles.js create mode 100644 src/styles/components/InquiryStyles.js create mode 100644 src/util/formatTimeAgo.js diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index 9bbceb01..f38666de 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -1,32 +1,86 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { palette } from "../styles/commonStyles"; import { + InquiryContent, + InquiryDate, + InquiryProfileImg, InquirySubmitButton, InquiryTextArea, -} from "../styles/items/ItemDetailStyle"; + InquiryTitle, + InquiryWriter, +} from "../styles/components/InquiryStyles"; +import { requestInquiryLists } from "../services/itemsApi"; +import icProfile from "../assets/icons/ic_profile.svg"; +import { formatTimeAgo } from "../util/formatTimeAgo"; -export default function Inquiry() { +export default function Inquiry({ id }) { + /** + * 버튼 활성화 state + */ const [isActive, setIsActive] = useState(false); + /** + * loading + */ + const [isLoading, setIsLoading] = useState(false); + + /** + * 문의내역을 담을 state + */ + const [inQuiryLists, setInquiryLists] = useState({}); + + const getInquiryLists = async (productId) => { + try { + setIsLoading(true); + const lists = await requestInquiryLists(productId); + if (!lists) { + throw new Error("문의 목록을 받아오지 못했습니다."); + } + + setInquiryLists(lists); + } catch (e) { + console.error(e); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + getInquiryLists(id); + }, []); + return ( <> -

- 문의하기 -

- { - e.target.value !== "" ? setIsActive(true) : setIsActive(false); - }} - placeholder="개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다." - > - 등록 +
+ 문의하기 + { + e.target.value !== "" ? setIsActive(true) : setIsActive(false); + }} + placeholder="개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다." + > + 등록 +
+ {!isLoading && + inQuiryLists.list?.map((el) => { + return ( +
+
+ {el.content} +
+
+ +
+ {el.writer.nickname} + {formatTimeAgo(el.updatedAt)} +
+
+
+ ); + })} ); } diff --git a/src/pages/items/ItemDetail.jsx b/src/pages/items/ItemDetail.jsx index 9a39c9d6..28ebe707 100644 --- a/src/pages/items/ItemDetail.jsx +++ b/src/pages/items/ItemDetail.jsx @@ -52,7 +52,6 @@ export default function ItemDetail() { if (!data) { throw new Error("상품 정보를 불러오지 못했습니다."); } - console.log(data); setProductDetail(data); } catch (e) { @@ -122,9 +121,7 @@ export default function ItemDetail() {
-
- -
+ ); diff --git a/src/services/itemsApi.js b/src/services/itemsApi.js index 1ae2fe45..6e7c4f49 100644 --- a/src/services/itemsApi.js +++ b/src/services/itemsApi.js @@ -38,3 +38,19 @@ export const requestProductDetail = async (productId) => { return response.json(); }; + +export const requestInquiryLists = async (productId) => { + const url = new URL( + `https://panda-market-api.vercel.app/products/${productId}/comments` + ); + url.searchParams.append("limit", 3); + + const response = await fetch(url, { + method: "get", + headers: { + "Content-Type": "application/json", + }, + }); + + return response.json(); +}; diff --git a/src/styles/components/InquiryDetailStyles.js b/src/styles/components/InquiryDetailStyles.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/styles/components/InquiryStyles.js b/src/styles/components/InquiryStyles.js new file mode 100644 index 00000000..1034fda3 --- /dev/null +++ b/src/styles/components/InquiryStyles.js @@ -0,0 +1,78 @@ +import styled from "styled-components"; +import { palette, ProfileImg } from "../commonStyles"; + +export const InquiryTextArea = styled.textarea` + background-color: ${palette.gray100}; + height: 104px; + width: 100%; + border: none; + border-radius: 12px; + resize: none; + font-family: Pretendard-Reqular; + padding: 20px; + font-size: 16px; + font-weight: 400; + margin-bottom: 10px; + &::placeholder { + color: ${palette.gray400}; + } +`; + +export const InquiryTitle = styled.p` + color: ${palette.gray900}; + font-weight: 600; + font-size: 16px; + margin: 40px 0px 10px; + text-align: left; +`; + +export const InquirySubmitButton = styled.button` + width: 74px; + height: 42px; + border: none; + color: white; + padding: 10px; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + background-color: ${({ isActive }) => + isActive ? `${palette.blue}` : `${palette.gray400}`}; + // float: right; + + &:hover { + background-color: ${({ isActive }) => + isActive ? `#1967d6` : `${palette.gray400}`}; + } + + &:active { + background-color: ${({ isActive }) => + isActive ? `#1251aa` : `${palette.gray400}`}; + } +`; + +export const InquiryContent = styled.p` + margin: 0; + color: ${palette.gray800}; + font-size: 14px; + font-weight: 400; +`; + +export const InquiryProfileImg = styled(ProfileImg)` + width: 32px; + height: 32px; +`; + +export const InquiryWriter = styled.p` + margin: 0px 0px 5px; + font-size: 12px; + font-weight: 400; +`; + +export const InquiryDate = styled.p` + margin: 0; + font-size: 12px; + font-weight: 400; + color: ${palette.gray400}; +`; + +export const BackButton = styled.button``; diff --git a/src/styles/items/ItemDetailStyle.js b/src/styles/items/ItemDetailStyle.js index 8c736402..3edfb867 100644 --- a/src/styles/items/ItemDetailStyle.js +++ b/src/styles/items/ItemDetailStyle.js @@ -80,44 +80,3 @@ export const HeartCount = styled.p` color: ${palette.gray500}; margin: 0; `; - -export const InquiryTextArea = styled.textarea` - background-color: ${palette.gray100}; - height: 104px; - width: 100%; - border: none; - border-radius: 12px; - resize: none; - font-family: Pretendard-Reqular; - padding: 20px; - font-size: 16px; - font-weight: 400; - margin-bottom: 10px; - &::placeholder { - color: ${palette.gray400}; - } -`; - -export const InquirySubmitButton = styled.button` - width: 74px; - height: 42px; - border: none; - color: white; - padding: 10px; - border-radius: 8px; - font-size: 16px; - font-weight: 600; - background-color: ${({ isActive }) => - isActive ? `${palette.blue}` : `${palette.gray400}`}; - float: right; - - &:hover { - background-color: ${({ isActive }) => - isActive ? `#1967d6` : `${palette.gray400}`}; - } - - &:active { - background-color: ${({ isActive }) => - isActive ? `#1251aa` : `${palette.gray400}`}; - } -`; diff --git a/src/util/formatTimeAgo.js b/src/util/formatTimeAgo.js new file mode 100644 index 00000000..71c60d4d --- /dev/null +++ b/src/util/formatTimeAgo.js @@ -0,0 +1,37 @@ +/** + * DateTime을 방금전, n분, 일, 월, 년전 형태로 출력한다. + * @param {string} dateString + * @returns {string} + */ +export const formatTimeAgo = (dateString) => { + const now = new Date(); + const past = new Date(dateString); + const diffInMs = now.getTime() - past.getTime(); + + // 시간 단위(밀리초) + const msInSecond = 1000; + const msInMinute = msInSecond * 60; + const msInHour = msInMinute * 60; + const msInDay = msInHour * 24; + const msInMonth = msInDay * 30; // 간단한 계산을 위해 30일로 가정 + const msInYear = msInDay * 365; // 간단한 계산을 위해 365일로 가정 + + if (diffInMs >= msInYear) { + const years = Math.floor(diffInMs / msInYear); + return `${years}년 전`; + } else if (diffInMs >= msInMonth) { + const months = Math.floor(diffInMs / msInMonth); + return `${months}달 전`; + } else if (diffInMs >= msInDay) { + const days = Math.floor(diffInMs / msInDay); + return `${days}일 전`; + } else if (diffInMs >= msInHour) { + const hours = Math.floor(diffInMs / msInHour); + return `${hours}시간 전`; + } else if (diffInMs >= msInMinute) { + const minutes = Math.floor(diffInMs / msInMinute); + return `${minutes}분 전`; + } else { + return "방금 전"; + } +}; From f337c1eb2d2675f0cc6f056bd6612c0b107589e1 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Sat, 9 Aug 2025 22:11:35 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9D=98=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=EC=9D=B4=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EB=B9=88?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/icons/ic_back.svg | 4 + src/assets/images/img_inquiry_empty_md.png | Bin 0 -> 6244 bytes src/components/Inquiry.jsx | 92 ++++++++++++++++----- src/styles/components/InquiryStyles.js | 16 +++- 4 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 src/assets/icons/ic_back.svg create mode 100644 src/assets/images/img_inquiry_empty_md.png diff --git a/src/assets/icons/ic_back.svg b/src/assets/icons/ic_back.svg new file mode 100644 index 00000000..8db5377e --- /dev/null +++ b/src/assets/icons/ic_back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/img_inquiry_empty_md.png b/src/assets/images/img_inquiry_empty_md.png new file mode 100644 index 0000000000000000000000000000000000000000..d941b91a58b0035ccf4624d47170d7f1ed541295 GIT binary patch literal 6244 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 SZ~l7<0JPN&)T&kNk^cdsQ_zM0 literal 0 HcmV?d00001 diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index f38666de..6afd167f 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { palette } from "../styles/commonStyles"; import { + BackButton, InquiryContent, InquiryDate, InquiryProfileImg, @@ -12,8 +13,12 @@ import { import { requestInquiryLists } from "../services/itemsApi"; import icProfile from "../assets/icons/ic_profile.svg"; import { formatTimeAgo } from "../util/formatTimeAgo"; +import { useNavigate } from "react-router"; +import icBack from "../assets/icons/ic_back.svg"; +import imgEmptyMd from "../assets/images/img_inquiry_empty_md.png"; export default function Inquiry({ id }) { + const navigate = useNavigate(); /** * 버튼 활성화 state */ @@ -29,6 +34,10 @@ export default function Inquiry({ id }) { */ const [inQuiryLists, setInquiryLists] = useState({}); + /** + * 상품의 아이디에 맞는 문의 목록을 불러온다 + * @param {string} productId + */ const getInquiryLists = async (productId) => { try { setIsLoading(true); @@ -36,8 +45,13 @@ export default function Inquiry({ id }) { if (!lists) { throw new Error("문의 목록을 받아오지 못했습니다."); } + console.log(lists.list?.length); - setInquiryLists(lists); + if (lists.list?.length) { + setInquiryLists(lists); + } else { + setInquiryLists(null); + } } catch (e) { console.error(e); } finally { @@ -61,26 +75,66 @@ export default function Inquiry({ id }) { > 등록 - {!isLoading && - inQuiryLists.list?.map((el) => { - return ( -
-
- {el.content} -
-
- -
- {el.writer.nickname} - {formatTimeAgo(el.updatedAt)} + {!isLoading ? ( + inQuiryLists ? ( + inQuiryLists.list?.map((el) => { + return ( +
+
+ {el.content} +
+
+ +
+ {el.writer.nickname} + {formatTimeAgo(el.updatedAt)} +
-
- ); - })} + ); + }) + ) : ( +
+ 빈 이미지 +

+ 아직 문의가 없어요 +

+
+ ) + ) : ( +
로딩중
+ )} +
+ navigate(-1)}> + 목록으로 돌아가기 + 뒤로가기 이미지 + +
); } diff --git a/src/styles/components/InquiryStyles.js b/src/styles/components/InquiryStyles.js index 1034fda3..66b2b51c 100644 --- a/src/styles/components/InquiryStyles.js +++ b/src/styles/components/InquiryStyles.js @@ -75,4 +75,18 @@ export const InquiryDate = styled.p` color: ${palette.gray400}; `; -export const BackButton = styled.button``; +export const BackButton = styled.button` + width: 240px; + height: 48px; + border: none; + background-color: ${palette.blue}; + color: white; + padding: 10px; + border-radius: 40px; + font-size: 18px; + font-weight: 600; + justify-content: center; + align-items: center; + display: flex; + gap: 10px; +`; From 44fce53cce879cc518275fa5f31ea11b051bf4f0 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 28 Aug 2025 18:27:12 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20useService=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useMediaQuery.jsx | 6 ++-- src/hooks/useService.jsx | 35 +++++++++++++++++++++ src/pages/items/ItemDetail.jsx | 57 +++++++++------------------------- src/services/itemsApi.js | 8 ++--- 4 files changed, 57 insertions(+), 49 deletions(-) create mode 100644 src/hooks/useService.jsx diff --git a/src/hooks/useMediaQuery.jsx b/src/hooks/useMediaQuery.jsx index 65806367..8196abdc 100644 --- a/src/hooks/useMediaQuery.jsx +++ b/src/hooks/useMediaQuery.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; const getMatches = (query) => { return window.matchMedia(query).matches; @@ -21,10 +21,10 @@ export default function useMediaQuery(query) { useEffect(() => { const media = window.matchMedia(query); - media.addEventListener('change', handleChange); + media.addEventListener("change", handleChange); return () => { - media.removeEventListener('change', handleChange); + media.removeEventListener("change", handleChange); }; }, [query]); diff --git a/src/hooks/useService.jsx b/src/hooks/useService.jsx new file mode 100644 index 00000000..2833084d --- /dev/null +++ b/src/hooks/useService.jsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; + +/** + * 데이터 통신을 위한 공통 커스텀 훅 + * @param {Function} fetchFunction 서버와 직접 통신하는 함수 + * @returns {data: object, isLoading: boolean} + */ +const useService = (fetchFunction) => { + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState(); + + useEffect(() => { + const getService = async (getDataFunction) => { + try { + setIsLoading(true); + const response = await getDataFunction(); + + if (!response) { + throw new Error("데이터를 불러오지 못했습니다."); + } + + setData(response); + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } + }; + + getService(fetchFunction); + }, []); + + return { data, isLoading }; +}; +export default useService; diff --git a/src/pages/items/ItemDetail.jsx b/src/pages/items/ItemDetail.jsx index 28ebe707..c0232444 100644 --- a/src/pages/items/ItemDetail.jsx +++ b/src/pages/items/ItemDetail.jsx @@ -19,6 +19,7 @@ import icProfile from "../../assets/icons/ic_profile.svg"; import icHeartInactive from "../../assets/icons/ic_heart_inactive_large.svg"; import icHeartActive from "../../assets/icons/ic_heart_active_large.svg"; import Inquiry from "../../components/Inquiry"; +import useService from "../../hooks/useService"; export default function ItemDetail() { /** @@ -29,65 +30,37 @@ export default function ItemDetail() { /** * 목록에서 가져온 상품 정보 */ - const { state } = useLocation(); + const { state: productInfo } = useLocation(); /** - * loading + * 상품 정보 받아오기 */ - const [isLoading, setIsLoading] = useState(false); - - /** - * 상품 상세 정보를 받을 state - */ - const [productDetail, setProductDetail] = useState({}); - - /** - * 상품의 상세 정보를 받아온다. - * @param {string} id - */ - const getProductDetail = async (id) => { - try { - setIsLoading(true); - const data = await requestProductDetail(id); - if (!data) { - throw new Error("상품 정보를 불러오지 못했습니다."); - } - - setProductDetail(data); - } catch (e) { - console.error(e); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - getProductDetail(productId); - }, []); + const { data, isLoading } = useService(() => requestProductDetail(productId)); return ( <>
- +
- {state.name} + {productInfo.name}
- {state.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + - "원"} + {productInfo.price + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "원"}
상품 소개 - {state.description} + {productInfo.description}
상품 태그
- {state.tags.map((tag) => { + {productInfo.tags.map((tag) => { return {`#${tag}`}; })}
@@ -102,20 +75,20 @@ export default function ItemDetail() { }} > - {!isLoading && productDetail.ownerNickname} + {!isLoading ? data?.ownerNickname : "..."} - {state.updatedAt.slice(0, 10).replaceAll("-", ". ")} + {productInfo.updatedAt.slice(0, 10).replaceAll("-", ". ")}
- {!isLoading && productDetail.isFavorite ? ( + {!isLoading && data?.isFavorite ? ( 좋아요 이미지 ) : ( 좋아요 이미지 )} - {state.favoriteCount} + {productInfo.favoriteCount}
diff --git a/src/services/itemsApi.js b/src/services/itemsApi.js index 6e7c4f49..b153a1f9 100644 --- a/src/services/itemsApi.js +++ b/src/services/itemsApi.js @@ -3,11 +3,11 @@ * @param {string} orderBy * @returns {object} */ -export const requestProductList = async (querys) => { +export const requestProductList = async (query) => { const url = new URL("https://panda-market-api.vercel.app/products"); - url.searchParams.append("page", querys.page); - url.searchParams.append("pageSize", querys.pageSize); - url.searchParams.append("orderBy", querys.orderBy); + url.searchParams.append("page", query.page); + url.searchParams.append("pageSize", query.pageSize); + url.searchParams.append("orderBy", query.orderBy); const response = await fetch(url, { method: "get", From 889ced0d9254e7a951a78a375286d4dbc10b21c6 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 28 Aug 2025 20:35:28 +0900 Subject: [PATCH 10/20] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Inquiry.jsx | 46 ++--------- src/hooks/useService.jsx | 1 + .../components/ItemDetail/ProductDetails.jsx | 70 +++++++++++++++++ src/pages/items/ItemDetail.jsx | 77 ++----------------- 4 files changed, 84 insertions(+), 110 deletions(-) create mode 100644 src/pages/components/ItemDetail/ProductDetails.jsx diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index 6afd167f..080e3518 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { palette } from "../styles/commonStyles"; import { BackButton, @@ -16,6 +16,7 @@ import { formatTimeAgo } from "../util/formatTimeAgo"; import { useNavigate } from "react-router"; import icBack from "../assets/icons/ic_back.svg"; import imgEmptyMd from "../assets/images/img_inquiry_empty_md.png"; +import useService from "../hooks/useService"; export default function Inquiry({ id }) { const navigate = useNavigate(); @@ -24,44 +25,7 @@ export default function Inquiry({ id }) { */ const [isActive, setIsActive] = useState(false); - /** - * loading - */ - const [isLoading, setIsLoading] = useState(false); - - /** - * 문의내역을 담을 state - */ - const [inQuiryLists, setInquiryLists] = useState({}); - - /** - * 상품의 아이디에 맞는 문의 목록을 불러온다 - * @param {string} productId - */ - const getInquiryLists = async (productId) => { - try { - setIsLoading(true); - const lists = await requestInquiryLists(productId); - if (!lists) { - throw new Error("문의 목록을 받아오지 못했습니다."); - } - console.log(lists.list?.length); - - if (lists.list?.length) { - setInquiryLists(lists); - } else { - setInquiryLists(null); - } - } catch (e) { - console.error(e); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - getInquiryLists(id); - }, []); + const { data, isLoading } = useService(() => requestInquiryLists(id)); return ( <> @@ -76,8 +40,8 @@ export default function Inquiry({ id }) { 등록
{!isLoading ? ( - inQuiryLists ? ( - inQuiryLists.list?.map((el) => { + data ? ( + data.list?.map((el) => { return (
{ return { data, isLoading }; }; + export default useService; diff --git a/src/pages/components/ItemDetail/ProductDetails.jsx b/src/pages/components/ItemDetail/ProductDetails.jsx new file mode 100644 index 00000000..11fa97f3 --- /dev/null +++ b/src/pages/components/ItemDetail/ProductDetails.jsx @@ -0,0 +1,70 @@ +import { ItemsTag, palette, ProfileImg } from "../../../styles/commonStyles"; +import { + Heart, + HeartCount, + ProductDate, + ProductOwnerName, + ProductPrice, + ProductSubTitle, + ProductTextArea, + ProductTextBox, + ProductTitle, +} from "../../../styles/items/ItemDetailStyle"; +import icProfile from "../../../assets/icons/ic_profile.svg"; +import icHeartInactive from "../../../assets/icons/ic_heart_inactive_large.svg"; +import icHeartActive from "../../../assets/icons/ic_heart_active_large.svg"; + +export default function ProductDetails({ data, productInfo, isLoading }) { + return ( + +
+
+ {productInfo.name} +
+ + {productInfo.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + + "원"} + +
+
+ 상품 소개 + {productInfo.description} +
+
+ 상품 태그 +
+ {productInfo.tags.map((tag) => { + return {`#${tag}`}; + })} +
+
+
+ +
+ + {!isLoading ? data?.ownerNickname : "..."} + + + {productInfo.updatedAt.slice(0, 10).replaceAll("-", ". ")} + +
+
+ + {!isLoading && data?.isFavorite ? ( + 좋아요 이미지 + ) : ( + 좋아요 이미지 + )} + {productInfo.favoriteCount} + +
+
+
+ ); +} diff --git a/src/pages/items/ItemDetail.jsx b/src/pages/items/ItemDetail.jsx index c0232444..053bb23b 100644 --- a/src/pages/items/ItemDetail.jsx +++ b/src/pages/items/ItemDetail.jsx @@ -1,25 +1,10 @@ import { useLocation, useParams } from "react-router"; import { requestProductDetail } from "../../services/itemsApi"; -import { useEffect, useState } from "react"; -import { - Heart, - HeartCount, - ProductDate, - ProductImg, - ProductInfoBox, - ProductOwnerName, - ProductPrice, - ProductSubTitle, - ProductTextArea, - ProductTextBox, - ProductTitle, -} from "../../styles/items/ItemDetailStyle"; -import { ItemsTag, palette, ProfileImg } from "../../styles/commonStyles"; -import icProfile from "../../assets/icons/ic_profile.svg"; -import icHeartInactive from "../../assets/icons/ic_heart_inactive_large.svg"; -import icHeartActive from "../../assets/icons/ic_heart_active_large.svg"; +import { ProductImg, ProductInfoBox } from "../../styles/items/ItemDetailStyle"; + import Inquiry from "../../components/Inquiry"; import useService from "../../hooks/useService"; +import ProductDetails from "../components/ItemDetail/ProductDetails"; export default function ItemDetail() { /** @@ -42,57 +27,11 @@ export default function ItemDetail() {
- -
-
- {productInfo.name} -
- - {productInfo.price - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "원"} - -
-
- 상품 소개 - {productInfo.description} -
-
- 상품 태그 -
- {productInfo.tags.map((tag) => { - return {`#${tag}`}; - })} -
-
-
- -
- - {!isLoading ? data?.ownerNickname : "..."} - - - {productInfo.updatedAt.slice(0, 10).replaceAll("-", ". ")} - -
-
- - {!isLoading && data?.isFavorite ? ( - 좋아요 이미지 - ) : ( - 좋아요 이미지 - )} - {productInfo.favoriteCount} - -
-
-
+
From 32a29e2eeb4cea6ade75c488dfe078546d6f7f42 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 28 Aug 2025 22:06:53 +0900 Subject: [PATCH 11/20] =?UTF-8?q?design:=20=EC=BC=80=EB=B0=A5=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EB=B0=8F=20=EC=BC=80=EB=B0=A5=20=EB=93=9C=EB=A1=AD?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20UI=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Inquiry.jsx | 11 ++++- src/components/kebab/KebabDropdown.jsx | 25 ++++++++++ src/components/kebab/KebabMenu.jsx | 29 ++++++++++++ src/hooks/useCloseDropdown.jsx | 19 ++++++++ src/hooks/useToggle.jsx | 22 +++++++++ src/styles/commonStyles.js | 4 ++ src/styles/components/kebab/kebabStyle.js | 57 +++++++++++++++++++++++ 7 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/components/kebab/KebabDropdown.jsx create mode 100644 src/components/kebab/KebabMenu.jsx create mode 100644 src/hooks/useCloseDropdown.jsx create mode 100644 src/hooks/useToggle.jsx create mode 100644 src/styles/components/kebab/kebabStyle.js diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index 080e3518..84f8244c 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -17,6 +17,7 @@ import { useNavigate } from "react-router"; import icBack from "../assets/icons/ic_back.svg"; import imgEmptyMd from "../assets/images/img_inquiry_empty_md.png"; import useService from "../hooks/useService"; +import KebabMenu from "./kebab/KebabMenu"; export default function Inquiry({ id }) { const navigate = useNavigate(); @@ -47,8 +48,16 @@ export default function Inquiry({ id }) { style={{ borderBottom: `1px solid ${palette.gray200}` }} key={el.id} > -
+
{el.content} +
{ + const dropdownRef = useRef(null); + useCloseDropdown(dropdownRef, onClickClose); + + return ( + + {menus?.map((menu) => { + return ( + + {menu.name} + + ); + })} + + ); +}; + +export default memo(KebabDropdown); diff --git a/src/components/kebab/KebabMenu.jsx b/src/components/kebab/KebabMenu.jsx new file mode 100644 index 00000000..bd01b11e --- /dev/null +++ b/src/components/kebab/KebabMenu.jsx @@ -0,0 +1,29 @@ +import useToggle from "../../hooks/useToggle"; +import { + KebabIcon, + KebabMenuContainer, +} from "../../styles/components/kebab/kebabStyle"; +import KebabDropdown from "./KebabDropdown"; + +const menus = [ + { id: "update", name: "수정하기" }, + { id: "delete", name: "삭제하기" }, +]; + +/** + * 케밥 메뉴 + */ +export default function KebabMenu() { + const { isOpen, onClickToggle, onClickClose } = useToggle(false); + + return ( + <> + + + + + {isOpen && } + + + ); +} diff --git a/src/hooks/useCloseDropdown.jsx b/src/hooks/useCloseDropdown.jsx new file mode 100644 index 00000000..7d53bf94 --- /dev/null +++ b/src/hooks/useCloseDropdown.jsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; + +const useCloseDropdown = (ref, callback) => { + useEffect(() => { + const onClickDropdownOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + callback(); + } + }; + + document.addEventListener("mousedown", onClickDropdownOutside); + + return () => { + document.removeEventListener("mousedown", onClickDropdownOutside); + }; + }, []); +}; + +export default useCloseDropdown; diff --git a/src/hooks/useToggle.jsx b/src/hooks/useToggle.jsx new file mode 100644 index 00000000..6fe0d5bf --- /dev/null +++ b/src/hooks/useToggle.jsx @@ -0,0 +1,22 @@ +import { useCallback, useState } from "react"; + +/** + * 토글 커스텀 훅 + * @param {boolean} initialValue 초깃값 + * @returns {isOpen: boolean, onClickToggle: Function} + */ +const useToggle = (initialValue) => { + const [isOpen, setIsOpen] = useState(initialValue); + + const onClickToggle = useCallback(() => { + setIsOpen((prevState) => !prevState); + }, [isOpen]); + + const onClickClose = useCallback(() => { + setIsOpen(false); + }, []); + + return { isOpen, onClickToggle, onClickClose }; +}; + +export default useToggle; diff --git a/src/styles/commonStyles.js b/src/styles/commonStyles.js index c9399260..ce33a4e4 100644 --- a/src/styles/commonStyles.js +++ b/src/styles/commonStyles.js @@ -1,6 +1,9 @@ import styled from "styled-components"; export const palette = { + secondary: { + gray500: "#6B7280", + }, gray900: "#111827", gray800: "#1f2937", gray700: "#374151", @@ -10,6 +13,7 @@ export const palette = { gray200: "#e5e7eb", gray100: "#f3f4f6", gray50: "#f9fafb", + coolGray300: "#D1D5DB", blue: "#3692ff", menu: "#4b5563", }; diff --git a/src/styles/components/kebab/kebabStyle.js b/src/styles/components/kebab/kebabStyle.js new file mode 100644 index 00000000..37c9d153 --- /dev/null +++ b/src/styles/components/kebab/kebabStyle.js @@ -0,0 +1,57 @@ +import styled from "styled-components"; +import { palette } from "../../commonStyles"; + +export const KebabMenuContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 2px; + width: 24px; + height: 24px; + cursor: pointer; + position: relative; +`; + +export const KebabIcon = styled.div` + width: 3px; + height: 3px; + background-color: ${palette.gray400}; + border-radius: 99px; +`; + +export const KebabDropdownContainer = styled.div` + width: 139px; + display: flex; + flex-direction: column; + position: absolute; + top: 100%; + right: 7%; + margin-top: 5px; + border: 1px solid ${palette.coolGray300}; + border-radius: 8px; + z-index: 50; + + & > :first-child { + border-top-right-radius: 8px; + border-top-left-radius: 8px; + } + + & > :last-child { + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + } +`; + +export const KebabDropdownButton = styled.button` + background-color: white; + border: none; + font-weight: 400; + font-size: 16px; + color: ${palette.secondary.gray500}; + padding: 16px 0px 12px; + + &:hover { + background-color: ${palette.gray100}; + } +`; From 0663d775df96ff78dcd620193d490c5a7b2c24c1 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Thu, 28 Aug 2025 22:11:33 +0900 Subject: [PATCH 12/20] =?UTF-8?q?design:=20=EC=A0=9C=ED=92=88=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=EC=97=90=20=EC=BC=80=EB=B0=A5=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/kebab/KebabDropdown.jsx | 5 +++++ src/pages/components/ItemDetail/ProductDetails.jsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/kebab/KebabDropdown.jsx b/src/components/kebab/KebabDropdown.jsx index 6086dfae..1b55f55c 100644 --- a/src/components/kebab/KebabDropdown.jsx +++ b/src/components/kebab/KebabDropdown.jsx @@ -5,6 +5,11 @@ import { } from "../../styles/components/kebab/kebabStyle"; import useCloseDropdown from "../../hooks/useCloseDropdown"; +/** + * 케밥 메뉴 클릭 시 보여지는 드롭다운 + * @param {object[]} menus + * @param {Function} onclickClose + */ const KebabDropdown = ({ menus, onClickClose }) => { const dropdownRef = useRef(null); useCloseDropdown(dropdownRef, onClickClose); diff --git a/src/pages/components/ItemDetail/ProductDetails.jsx b/src/pages/components/ItemDetail/ProductDetails.jsx index 11fa97f3..db34d0ea 100644 --- a/src/pages/components/ItemDetail/ProductDetails.jsx +++ b/src/pages/components/ItemDetail/ProductDetails.jsx @@ -13,13 +13,15 @@ import { import icProfile from "../../../assets/icons/ic_profile.svg"; import icHeartInactive from "../../../assets/icons/ic_heart_inactive_large.svg"; import icHeartActive from "../../../assets/icons/ic_heart_active_large.svg"; +import KebabMenu from "../../../components/kebab/KebabMenu"; export default function ProductDetails({ data, productInfo, isLoading }) { return (
-
+
{productInfo.name} +
{productInfo.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + From affa433ffddd6993f1e10e4a34c10633765a35ee Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 29 Aug 2025 13:46:27 +0900 Subject: [PATCH 13/20] =?UTF-8?q?design:=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ItemDetail/ProductDetails.jsx | 10 +- src/pages/items/ItemDetail.jsx | 10 +- src/styles/components/InquiryStyles.js | 9 +- src/styles/items/ItemDetailStyle.js | 117 ++++++++++++++++-- 4 files changed, 125 insertions(+), 21 deletions(-) diff --git a/src/pages/components/ItemDetail/ProductDetails.jsx b/src/pages/components/ItemDetail/ProductDetails.jsx index db34d0ea..2f71f78b 100644 --- a/src/pages/components/ItemDetail/ProductDetails.jsx +++ b/src/pages/components/ItemDetail/ProductDetails.jsx @@ -6,6 +6,8 @@ import { ProductOwnerName, ProductPrice, ProductSubTitle, + ProductTagContainer, + ProductTagWrapper, ProductTextArea, ProductTextBox, ProductTitle, @@ -32,14 +34,14 @@ export default function ProductDetails({ data, productInfo, isLoading }) { 상품 소개 {productInfo.description}
-
+ 상품 태그 -
+ {productInfo.tags.map((tag) => { return {`#${tag}`}; })} -
-
+ +
-
+ -
+ ); } diff --git a/src/styles/components/InquiryStyles.js b/src/styles/components/InquiryStyles.js index 66b2b51c..a068764c 100644 --- a/src/styles/components/InquiryStyles.js +++ b/src/styles/components/InquiryStyles.js @@ -10,12 +10,18 @@ export const InquiryTextArea = styled.textarea` resize: none; font-family: Pretendard-Reqular; padding: 20px; - font-size: 16px; + font-size: 14px; font-weight: 400; margin-bottom: 10px; &::placeholder { color: ${palette.gray400}; } + + @media (min-width: 744px) { + font-size: 16px; + line-height: 26px; + letter-spacing: 0%; + } `; export const InquiryTitle = styled.p` @@ -37,7 +43,6 @@ export const InquirySubmitButton = styled.button` font-weight: 600; background-color: ${({ isActive }) => isActive ? `${palette.blue}` : `${palette.gray400}`}; - // float: right; &:hover { background-color: ${({ isActive }) => diff --git a/src/styles/items/ItemDetailStyle.js b/src/styles/items/ItemDetailStyle.js index 3edfb867..906d5609 100644 --- a/src/styles/items/ItemDetailStyle.js +++ b/src/styles/items/ItemDetailStyle.js @@ -1,11 +1,27 @@ import styled from "styled-components"; import { ItemsTag, palette } from "../commonStyles"; +export const ProductDetailContainer = styled.div` + width: 100%; + margin: 0 auto; + max-width: 1200px; + padding: 16px 16px 0px 16px; + + @media (min-width: 744px) { + padding: 24px 24px 0px 24px; + } +`; + export const ProductInfoBox = styled.div` display: flex; margin-top: 20px; border-bottom: 1px solid ${palette.gray200}; padding-bottom: 40px; + flex-direction: column; + + @media (min-width: 744px) { + flex-direction: row; + } `; export const ProductTextBox = styled.div` @@ -15,38 +31,115 @@ export const ProductTextBox = styled.div` `; export const ProductImg = styled.img` - width: 486px; - height: 486px; border-radius: 16px; border: none; - margin-right: 20px; + + @media (min-width: 375px) { + width: 343px; + height: 343px; + margin: 0; + margin-bottom: 16px; + } + + @media (min-width: 744px) { + margin-right: 16px; + } + + @media (min-width: 1200px) { + margin-right: 20px; + width: 486px; + height: 486px; + } `; export const ProductTitle = styled.h1` - font-size: 24px; font-weight: 600; margin: 0; + + @media (min-width: 375px) { + font-size: 16px; + line-height: 26px; + } + + @media (min-width: 744px) { + font-size: 20px; + line-height: 32px; + letter-spacing: 0%; + } + + @media (min-width: 1200px) { + font-size: 24px; + line-height: 32px; + letter-spacing: 0%; + } `; export const ProductPrice = styled.p` - font-size: 40px; font-weight: 600; - margin: 20px 0px 10px; + + @media (min-width: 375px) { + font-size: 24px; + line-height: 32px; + margin: 8px 0px 16px; + } + + @media (min-width: 744px) { + font-size: 32px; + line-height: 42px; + letter-spacing: 0%; + } + + @media (min-width: 1200px) { + font-size: 40px; + line-height: 32px; + letter-spacing: 0%; + margin: 20px 0px 10px; + } `; export const ProductSubTitle = styled.p` - font-size: 16px; - font-weight: 600; color: ${palette.gray600}; - margin: 20px 0px; + + @media (min-width: 375px) { + font-size: 14px; + line-height: 24px; + letter-spacing: 0%; + margin: 16px 0px 8px; + } + + @media (min-width: 1200px) { + font-size: 16px; + font-weight: 600; + margin: 20px 0px; + } `; export const ProductTextArea = styled.p` - font-size: 16px; - font-weight: 400; - width: 100%; color: ${palette.gray600}; + width: 100%; margin: 0; + + @media (min-width: 375px) { + font-size: 16px; + line-height: 26px; + letter-spacing: 0%; + } +`; + +export const ProductTagWrapper = styled.div` + @media (min-width: 375px) { + margin-bottom: 40px; + } + + @media (min-width: 1200px) { + margin-bottom: 100px; + } +`; + +export const ProductTagContainer = styled.div` + display: flex; + flex-wrap: wrap; + row-gap: 10px; `; export const ProductOwnerName = styled.p` From 95059765147a87310ffc9c5c3cd346467c227870 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 29 Aug 2025 13:54:31 +0900 Subject: [PATCH 14/20] =?UTF-8?q?design:=20=EB=B2=84=ED=8A=BC=EC=97=90=20h?= =?UTF-8?q?over=20=ED=9A=A8=EA=B3=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/components/InquiryStyles.js | 8 ++++++++ src/styles/items/ItemDetailStyle.js | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/styles/components/InquiryStyles.js b/src/styles/components/InquiryStyles.js index a068764c..5811c5a0 100644 --- a/src/styles/components/InquiryStyles.js +++ b/src/styles/components/InquiryStyles.js @@ -94,4 +94,12 @@ export const BackButton = styled.button` align-items: center; display: flex; gap: 10px; + + &:hover { + background-color: #1967d6; + } + + &:active { + background-color: #1251aa; + } `; diff --git a/src/styles/items/ItemDetailStyle.js b/src/styles/items/ItemDetailStyle.js index 906d5609..9c295cfd 100644 --- a/src/styles/items/ItemDetailStyle.js +++ b/src/styles/items/ItemDetailStyle.js @@ -165,6 +165,12 @@ export const Heart = styled.div` column-gap: 5px; padding: 2px 10px; align-items: center; + transition: transform 0.3s ease; + + &:hover { + cursor: pointer; + transform: scale(1.1); + } `; export const HeartCount = styled.p` From 4c33e07c4383f16c93350db34a0f9aa1c9884f65 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 29 Aug 2025 14:35:44 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9D=98=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Inquiry.jsx | 31 +++++++++++++++++++++--- src/hooks/useService.jsx | 37 +++++++++++++++-------------- src/services/inquiryApi.js | 48 ++++++++++++++++++++++++++++++++++++++ src/services/itemsApi.js | 16 ------------- 4 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 src/services/inquiryApi.js diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index 84f8244c..ea1e8100 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -10,7 +10,10 @@ import { InquiryTitle, InquiryWriter, } from "../styles/components/InquiryStyles"; -import { requestInquiryLists } from "../services/itemsApi"; +import { + requestInquiryLists, + requestPostInquiry, +} from "../services/inquiryApi"; import icProfile from "../assets/icons/ic_profile.svg"; import { formatTimeAgo } from "../util/formatTimeAgo"; import { useNavigate } from "react-router"; @@ -26,7 +29,23 @@ export default function Inquiry({ id }) { */ const [isActive, setIsActive] = useState(false); - const { data, isLoading } = useService(() => requestInquiryLists(id)); + /** + * 문의 내역을 가져온다. + */ + const { data, isLoading, requestServer } = useService(() => + requestInquiryLists(id) + ); + + const onClickUploadInquiry = (e) => { + let data = { + productId: id, + Inquiry: { + content: e.target.value, + }, + }; + + requestServer(() => requestPostInquiry(data)); + }; return ( <> @@ -38,7 +57,13 @@ export default function Inquiry({ id }) { }} placeholder="개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다." > - 등록 + + 등록 +
{!isLoading ? ( data ? ( diff --git a/src/hooks/useService.jsx b/src/hooks/useService.jsx index e0347e1d..5b70c40a 100644 --- a/src/hooks/useService.jsx +++ b/src/hooks/useService.jsx @@ -8,29 +8,30 @@ import { useEffect, useState } from "react"; const useService = (fetchFunction) => { const [isLoading, setIsLoading] = useState(false); const [data, setData] = useState(); + const [error, setError] = useState(false); + const requestServer = async (dataFetch) => { + try { + setIsLoading(true); + const response = await dataFetch(); - useEffect(() => { - const getService = async (getDataFunction) => { - try { - setIsLoading(true); - const response = await getDataFunction(); - - if (!response) { - throw new Error("데이터를 불러오지 못했습니다."); - } - - setData(response); - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); + if (!response) { + throw new Error("서버와의 통신에 실패했습니다."); } - }; - getService(fetchFunction); + setData(response); + } catch (error) { + setError(true); + console.log(error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + requestServer(fetchFunction); }, []); - return { data, isLoading }; + return { data, isLoading, error, requestServer }; }; export default useService; diff --git a/src/services/inquiryApi.js b/src/services/inquiryApi.js new file mode 100644 index 00000000..0e05488b --- /dev/null +++ b/src/services/inquiryApi.js @@ -0,0 +1,48 @@ +/** + * 문의 데이터를 가져온다. + * @param {string} productId + */ +export const requestInquiryLists = async (productId) => { + const url = new URL( + `https://panda-market-api.vercel.app/products/${productId}/comments` + ); + url.searchParams.append("limit", 3); + + const response = await fetch(url, { + method: "get", + headers: { + "Content-Type": "application/json", + }, + }); + + return response.json(); +}; + +/** + * 문의 내용을 등록한다. + * @param {object{}} inquiryData 상품 아이디와 문의 내역 객체 + * @returns + */ +export const requestPostInquiry = async (inquiryData) => { + const url = new URL( + `https://panda-market-api.vercel.app/products/${inquiryData.productId}/comments` + ); + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(inquiryData.inquiry), + }); + + if (!response.ok) { + alert("데이터를 전송하지 못했습니다. 다시 시도해주세요"); + } + + return response.json(); + } catch (error) { + console.error(error); + } +}; diff --git a/src/services/itemsApi.js b/src/services/itemsApi.js index b153a1f9..8148c414 100644 --- a/src/services/itemsApi.js +++ b/src/services/itemsApi.js @@ -38,19 +38,3 @@ export const requestProductDetail = async (productId) => { return response.json(); }; - -export const requestInquiryLists = async (productId) => { - const url = new URL( - `https://panda-market-api.vercel.app/products/${productId}/comments` - ); - url.searchParams.append("limit", 3); - - const response = await fetch(url, { - method: "get", - headers: { - "Content-Type": "application/json", - }, - }); - - return response.json(); -}; From 27e7a37abb152cb308ac0d4bc80d5b838cb2734d Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 29 Aug 2025 16:29:59 +0900 Subject: [PATCH 16/20] =?UTF-8?q?refactor:=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Inquiry.jsx | 40 +++----------------- src/components/InquiryWriteArea.jsx | 57 +++++++++++++++++++++++++++++ src/hooks/usePost.jsx | 9 +++++ src/hooks/useService.jsx | 37 ++++++++++--------- 4 files changed, 90 insertions(+), 53 deletions(-) create mode 100644 src/components/InquiryWriteArea.jsx create mode 100644 src/hooks/usePost.jsx diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index ea1e8100..ddde2ace 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { palette } from "../styles/commonStyles"; import { BackButton, @@ -21,50 +20,21 @@ import icBack from "../assets/icons/ic_back.svg"; import imgEmptyMd from "../assets/images/img_inquiry_empty_md.png"; import useService from "../hooks/useService"; import KebabMenu from "./kebab/KebabMenu"; +import usePost from "../hooks/usePost"; +import InquiryWriteArea from "./InquiryWriteArea"; export default function Inquiry({ id }) { const navigate = useNavigate(); - /** - * 버튼 활성화 state - */ - const [isActive, setIsActive] = useState(false); /** * 문의 내역을 가져온다. */ - const { data, isLoading, requestServer } = useService(() => - requestInquiryLists(id) - ); - - const onClickUploadInquiry = (e) => { - let data = { - productId: id, - Inquiry: { - content: e.target.value, - }, - }; - - requestServer(() => requestPostInquiry(data)); - }; + const { data, isLoading } = useService(() => requestInquiryLists(id)); return ( <> -
- 문의하기 - { - e.target.value !== "" ? setIsActive(true) : setIsActive(false); - }} - placeholder="개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다." - > - - 등록 - -
+ 문의하기 + {!isLoading ? ( data ? ( data.list?.map((el) => { diff --git a/src/components/InquiryWriteArea.jsx b/src/components/InquiryWriteArea.jsx new file mode 100644 index 00000000..be264830 --- /dev/null +++ b/src/components/InquiryWriteArea.jsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import usePost from "../hooks/usePost"; +import { + InquirySubmitButton, + InquiryTextArea, +} from "../styles/components/InquiryStyles"; + +/** + * 문의 내역 작성 공간 + */ +export default function InquiryWriteArea() { + const [isActive, setIsActive] = useState(false); + const [inquiryContent, setInquiryContent] = useState(""); + + const onChangeWrite = (e) => { + if (e.target.value) { + setIsActive(true); + } else { + setIsActive(false); + } + + setInquiryContent(e.target.value); + }; + + const onClickUploadInquiry = () => { + let data = { + productId: id, + Inquiry: { + content: inquiryContent, + }, + }; + + const { data: success } = usePost(requestPostInquiry(data)); + + if (success) { + location.reload(true); + } else { + alert("등록에 실패했습니다."); + } + }; + + return ( +
+ + + 등록 + +
+ ); +} diff --git a/src/hooks/usePost.jsx b/src/hooks/usePost.jsx new file mode 100644 index 00000000..681432e1 --- /dev/null +++ b/src/hooks/usePost.jsx @@ -0,0 +1,9 @@ +import useService from "./useService"; + +const usePost = (postFetching) => { + const { data, isLoading, isError } = useService(() => postFetching); + + return { data, isLoading, isError }; +}; + +export default usePost; diff --git a/src/hooks/useService.jsx b/src/hooks/useService.jsx index 5b70c40a..eb137f8f 100644 --- a/src/hooks/useService.jsx +++ b/src/hooks/useService.jsx @@ -9,29 +9,30 @@ const useService = (fetchFunction) => { const [isLoading, setIsLoading] = useState(false); const [data, setData] = useState(); const [error, setError] = useState(false); - const requestServer = async (dataFetch) => { - try { - setIsLoading(true); - const response = await dataFetch(); - if (!response) { - throw new Error("서버와의 통신에 실패했습니다."); - } + useEffect(() => { + const getService = async (getDataFunction) => { + try { + setIsLoading(true); + const response = await getDataFunction(); - setData(response); - } catch (error) { - setError(true); - console.log(error); - } finally { - setIsLoading(false); - } - }; + if (!response) { + throw new Error("서버와의 통신에 실패했습니다."); + } - useEffect(() => { - requestServer(fetchFunction); + setData(response); + } catch (error) { + setError(true); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + getService(fetchFunction); }, []); - return { data, isLoading, error, requestServer }; + return { data, isLoading, error }; }; export default useService; From bd65187ba9138e8c4230aab1db1e9f7bbc78e82a Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 29 Aug 2025 17:51:33 +0900 Subject: [PATCH 17/20] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Inquiry.jsx | 86 ++----------------- .../components/ItemDetail/InquiryList.jsx | 64 ++++++++++++++ src/styles/components/InquiryStyles.js | 7 ++ src/styles/items/ItemDetailStyle.js | 2 +- 4 files changed, 78 insertions(+), 81 deletions(-) create mode 100644 src/pages/components/ItemDetail/InquiryList.jsx diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index ddde2ace..f1258fca 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -1,27 +1,14 @@ -import { palette } from "../styles/commonStyles"; import { BackButton, - InquiryContent, - InquiryDate, - InquiryProfileImg, - InquirySubmitButton, - InquiryTextArea, + BackButtonWrapper, InquiryTitle, - InquiryWriter, } from "../styles/components/InquiryStyles"; -import { - requestInquiryLists, - requestPostInquiry, -} from "../services/inquiryApi"; -import icProfile from "../assets/icons/ic_profile.svg"; -import { formatTimeAgo } from "../util/formatTimeAgo"; +import { requestInquiryLists } from "../services/inquiryApi"; import { useNavigate } from "react-router"; import icBack from "../assets/icons/ic_back.svg"; -import imgEmptyMd from "../assets/images/img_inquiry_empty_md.png"; import useService from "../hooks/useService"; -import KebabMenu from "./kebab/KebabMenu"; -import usePost from "../hooks/usePost"; import InquiryWriteArea from "./InquiryWriteArea"; +import InquiryList from "../pages/components/ItemDetail/InquiryList"; export default function Inquiry({ id }) { const navigate = useNavigate(); @@ -35,74 +22,13 @@ export default function Inquiry({ id }) { <> 문의하기 - {!isLoading ? ( - data ? ( - data.list?.map((el) => { - return ( -
-
- {el.content} - -
-
- -
- {el.writer.nickname} - {formatTimeAgo(el.updatedAt)} -
-
-
- ); - }) - ) : ( -
- 빈 이미지 -

- 아직 문의가 없어요 -

-
- ) - ) : ( -
로딩중
- )} -
+ {!isLoading ? :
로딩중
} + navigate(-1)}> 목록으로 돌아가기 뒤로가기 이미지 -
+ ); } diff --git a/src/pages/components/ItemDetail/InquiryList.jsx b/src/pages/components/ItemDetail/InquiryList.jsx new file mode 100644 index 00000000..a375b04b --- /dev/null +++ b/src/pages/components/ItemDetail/InquiryList.jsx @@ -0,0 +1,64 @@ +import KebabMenu from "../../../components/kebab/KebabMenu"; +import { palette } from "../../../styles/commonStyles"; +import { + InquiryContent, + InquiryDate, + InquiryProfileImg, + InquiryWriter, +} from "../../../styles/components/InquiryStyles"; +import icProfile from "../../../assets/icons/ic_profile.svg"; +import imgEmptyMd from "../../../assets/images/img_inquiry_empty_md.png"; +import { formatTimeAgo } from "../../../util/formatTimeAgo"; + +const InquiryList = ({ data }) => { + return data ? ( + data.list?.map((el) => { + return ( +
+
+ {el.content} + +
+
+ +
+ {el.writer.nickname} + {formatTimeAgo(el.updatedAt)} +
+
+
+ ); + }) + ) : ( +
+ 빈 이미지 +

+ 아직 문의가 없어요 +

+
+ ); +}; + +export default InquiryList; diff --git a/src/styles/components/InquiryStyles.js b/src/styles/components/InquiryStyles.js index 5811c5a0..cac63e29 100644 --- a/src/styles/components/InquiryStyles.js +++ b/src/styles/components/InquiryStyles.js @@ -80,6 +80,13 @@ export const InquiryDate = styled.p` color: ${palette.gray400}; `; +export const BackButtonWrapper = styled.div` + margin: 60px 0px 60px; + text-align: center; + display: flex; + justify-content: center; +`; + export const BackButton = styled.button` width: 240px; height: 48px; diff --git a/src/styles/items/ItemDetailStyle.js b/src/styles/items/ItemDetailStyle.js index 9c295cfd..e20d3ecc 100644 --- a/src/styles/items/ItemDetailStyle.js +++ b/src/styles/items/ItemDetailStyle.js @@ -93,7 +93,7 @@ export const ProductPrice = styled.p` font-size: 40px; line-height: 32px; letter-spacing: 0%; - margin: 20px 0px 10px; + margin: 20px 0px 16px; } `; From f40f57f37bf99ada70838afaab1c5f3b3bf69ea8 Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 29 Aug 2025 17:58:16 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20=EB=AC=B8=EC=9D=98=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A3=BC=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/components/ItemDetail/InquiryList.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/components/ItemDetail/InquiryList.jsx b/src/pages/components/ItemDetail/InquiryList.jsx index a375b04b..f91de4ef 100644 --- a/src/pages/components/ItemDetail/InquiryList.jsx +++ b/src/pages/components/ItemDetail/InquiryList.jsx @@ -11,7 +11,7 @@ import imgEmptyMd from "../../../assets/images/img_inquiry_empty_md.png"; import { formatTimeAgo } from "../../../util/formatTimeAgo"; const InquiryList = ({ data }) => { - return data ? ( + return data?.length > 0 ? ( data.list?.map((el) => { return (
Date: Fri, 29 Aug 2025 18:05:40 +0900 Subject: [PATCH 19/20] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 5d919827..6370b2bd 100644 --- a/README.md +++ b/README.md @@ -150,4 +150,40 @@ - styled component를 사용해서 스타일링을 했는데 한 컴포넌트의 코드가 너무 길어지는 느낌입니다. 스타일은 따로 빼놓는게 나을까요? +
+ +## 미션 7 요구사항 + +### 기본 + +- [x] 상품 상세 페이지 주소는 “items/{productId}” 입니다. +- [x] response로 받은 아래의 데이터로 화면을 구현합니다. + - favoriteCount : 하트 개수 + - images : 상품 이미지 + - tags : 상품태그 + - name : 상품 이름 + - description : 상품 설명 +- [x] 목록으로 돌아가기 버튼을 클릭하면 중고마켓 페이지 주소인 "/items"로 이동합니다. +- [x] 문의하기에 내용을 입력하면 등록 버튼의 색상은 “3692FF”로 변합니다. +- [x] response 로 받은 아래의 데이터로 화면을 구현합니다. + - image : 작성자 이미지 + - nickname : 작성자 닉네임 + - content : 작성자가 남긴 문구 + - description : 상품 설명 + - updatedAt : 문의글 마지막 업데이트 시간 + +### 심화 + +- [x] 모든 버튼에 자유롭게 Hover효과를 적용하세요. + +### 주요 변경사항 + +### 스크린샷 + +| PC | Tablet | Mobile | +| :---------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------: | +| localhost_5173_additem | localhost_5173_additem (1) | localhost_5173_additem (2) | + +### 멘토에게 + Copyright 2025 코드잇 Inc. All rights reserved. From 462577536d1ca6388c097b58ac765f3e42518dea Mon Sep 17 00:00:00 2001 From: Imhwitae Date: Fri, 29 Aug 2025 18:11:51 +0900 Subject: [PATCH 20/20] =?UTF-8?q?fix:=20=EB=AC=B8=EC=9D=98=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=ED=91=9C=EC=B6=9C=20=EC=A1=B0=EA=B1=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/components/ItemDetail/InquiryList.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/components/ItemDetail/InquiryList.jsx b/src/pages/components/ItemDetail/InquiryList.jsx index f91de4ef..16dfc373 100644 --- a/src/pages/components/ItemDetail/InquiryList.jsx +++ b/src/pages/components/ItemDetail/InquiryList.jsx @@ -11,7 +11,7 @@ import imgEmptyMd from "../../../assets/images/img_inquiry_empty_md.png"; import { formatTimeAgo } from "../../../util/formatTimeAgo"; const InquiryList = ({ data }) => { - return data?.length > 0 ? ( + return data?.list?.length > 0 ? ( data.list?.map((el) => { return (