@@ -24,7 +26,16 @@ function Header() {
-
+ {/* React Router v6 이전 버전에서는 NavLink `isActive` prop으로 바로 스타일 정보를 넣어줄 수 있었지만, 최신 버전에서는 className 또는 style을 이용해야 해요 */}
+ {/* /additem 페이지에서도 네이게이션의 '중고마켓' 링크 하이라이트 */}
+
+ location.pathname === "/additem" || isActive
+ ? { color: "var(--blue)" }
+ : {}
+ }
+ >
중고마켓
diff --git a/src/components/UI/DeleteButton.jsx b/src/components/UI/DeleteButton.jsx
new file mode 100644
index 000000000..825c567eb
--- /dev/null
+++ b/src/components/UI/DeleteButton.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import styled from "styled-components";
+import { ReactComponent as CloseIcon } from "../../assets/images/icons/ic_x.svg";
+
+const Button = styled.button`
+ background-color: ${({ theme }) => theme.colors.gray[0]};
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.blue[0]};
+ }
+`;
+
+function DeleteButton({ onClick, label }) {
+ return (
+
+
+
+ );
+}
+
+export default DeleteButton;
diff --git a/src/components/UI/DropdownMenu.css b/src/components/UI/DropdownMenu.css
new file mode 100644
index 000000000..da51c7900
--- /dev/null
+++ b/src/components/UI/DropdownMenu.css
@@ -0,0 +1,33 @@
+.sortButtonWrapper {
+ position: relative;
+}
+
+.sortDropdownTriggerButton {
+ border: 1px solid #e5e7eb;
+ border-radius: 12px;
+ padding: 9px;
+ margin-left: 8px;
+}
+
+.dropdownMenu {
+ position: absolute;
+ top: 110%;
+ right: 0;
+ background: #fff;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ z-index: 99;
+}
+
+.dropdownItem {
+ padding: 12px 44px;
+ border-bottom: 1px solid #e5e7eb;
+ font-size: 16px;
+ color: #1f2937;
+ cursor: pointer;
+}
+
+.dropdownItem:last-child {
+ border-bottom: none;
+}
diff --git a/src/components/UI/DropdownMenu.jsx b/src/components/UI/DropdownMenu.jsx
new file mode 100644
index 000000000..9a883fece
--- /dev/null
+++ b/src/components/UI/DropdownMenu.jsx
@@ -0,0 +1,43 @@
+import React, { useState } from "react";
+import "./DropdownMenu.css";
+import { ReactComponent as SortIcon } from "../../assets/images/icons/ic_sort.svg";
+
+function DropdownMenu({ onSortSelection }) {
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+
+ const toggleDropdown = () => {
+ setIsDropdownVisible(!isDropdownVisible);
+ };
+
+ return (
+
+
+
+
+
+ {isDropdownVisible && (
+
+
{
+ onSortSelection("recent");
+ setIsDropdownVisible(false);
+ }}
+ >
+ 최신순
+
+
{
+ onSortSelection("favorite");
+ setIsDropdownVisible(false);
+ }}
+ >
+ 인기순
+
+
+ )}
+
+ );
+}
+export default DropdownMenu;
diff --git a/src/components/UI/ImageUpload.jsx b/src/components/UI/ImageUpload.jsx
new file mode 100644
index 000000000..4bf12e1c8
--- /dev/null
+++ b/src/components/UI/ImageUpload.jsx
@@ -0,0 +1,122 @@
+import React, { useState } from "react";
+import { Label } from "./InputItem";
+import styled, { css } from "styled-components";
+import { ReactComponent as PlusIcon } from "../../assets/images/icons/ic_plus.svg";
+import DeleteButton from "./DeleteButton";
+
+const ImageUploadContainer = styled.div`
+ display: flex;
+ gap: 8px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ gap: 18px;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ gap: 24px;
+ }
+`;
+
+const squareStyles = css`
+ // 작은 화면에서는 max-width가 되기 전까지는 UploadButton과 ImagePreview가 각각 gap을 포함해 컨테이너 너비의 절반을 차지하도록 함
+ width: calc(50% - 4px);
+ max-width: 200px;
+ aspect-ratio: 1 / 1; // 정사각형 비율 유지
+ border-radius: 12px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ width: 162px;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ width: 282px;
+ }
+`;
+
+// file input과 연관 짓기 위해 버튼이 대신 label로 설정
+const UploadButton = styled.label`
+ background-color: ${({ theme }) => theme.colors.gray[1]};
+ color: ${({ theme }) => theme.colors.gray[0]};
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ font-size: 16px;
+ cursor: pointer; // 버튼이 아닌 label을 사용한 경우 별도로 추가해 주세요
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.gray[2]};
+ }
+
+ ${squareStyles}
+`;
+
+const ImagePreview = styled.div`
+ background-image: url(${({ src }) => src});
+ background-size: cover;
+ background-position: center;
+ position: relative; // DeleteButton 포지셔닝을 위해 추가
+
+ ${squareStyles}
+`;
+
+const DeleteButtonWrapper = styled.div`
+ position: absolute;
+ top: 12px;
+ right: 12px;
+`;
+
+// 브라우저 기본 '파일 선택' 버튼 대신 커스텀 버튼을 사용하기 위해 file input을 숨김 처리
+const HiddenFileInput = styled.input`
+ display: none;
+`;
+
+function ImageUpload({ title }) {
+ const [imagePreviewUrl, setImagePreviewUrl] = useState("");
+
+ const handleImageChange = (event) => {
+ const file = event.target.files[0];
+ if (file) {
+ // 미리보기 주소 값(Object URL) 생성
+ const imageUrl = URL.createObjectURL(file);
+ setImagePreviewUrl(imageUrl);
+ }
+ };
+
+ const handleDelete = () => {
+ setImagePreviewUrl(""); // 미리보기 URL 리셋
+ };
+
+ return (
+
+ {title &&
{title} }
+
+
+ {/* HiddenFileInput의 id와 label의 htmlFor 값을 매칭해 주세요 */}
+
+
+ 이미지 등록
+
+
+
+
+ {/* 업로드된 이미지가 있으면 썸네일 렌더링 */}
+ {imagePreviewUrl && (
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default ImageUpload;
diff --git a/src/components/UI/InputItem.jsx b/src/components/UI/InputItem.jsx
new file mode 100644
index 000000000..5b62734b4
--- /dev/null
+++ b/src/components/UI/InputItem.jsx
@@ -0,0 +1,78 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+// input과 textarea의 스타일이 대부분 중복되기 때문에 styled-components의 css 헬퍼 함수를 사용해 공통 스타일을 정의했어요.
+// `${}`로 정의된 스타일을 삽입하면 여러 styled component 내에서 코드를 재사용할 수 있어요.
+const inputStyle = css`
+ padding: 16px 24px;
+ background-color: ${({ theme }) => theme.colors.gray[1]};
+ color: ${({ theme }) => theme.colors.black};
+ border: none;
+ border-radius: 12px;
+ font-size: 16px;
+ line-height: 24px;
+ width: 100%;
+
+ &::placeholder {
+ color: ${({ theme }) => theme.colors.gray[0]};
+ }
+
+ &:focus {
+ outline-color: ${({ theme }) => theme.colors.blue[0]};
+ }
+`;
+
+export const Label = styled.label`
+ display: block;
+ font-size: 14px;
+ font-weight: bold;
+ margin-bottom: 12px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ font-size: 18px;
+ }
+`;
+
+const InputField = styled.input`
+ ${inputStyle}
+`;
+
+const TextArea = styled.textarea`
+ ${inputStyle}
+ height: 200px; // 디자인에 맞춰 textarea 영역의 기본 높이를 설정해 주세요
+ resize: none; // 우측 하단 코너의 textarea 영역 크기 조절 기능을 없애줍니다
+`;
+
+function InputItem({
+ id,
+ label,
+ value,
+ onChange,
+ placeholder,
+ onKeyDown,
+ isTextArea,
+}) {
+ return (
+
+ {label && {label} }
+ {isTextArea ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default InputItem;
diff --git a/src/components/UI/TagInput.jsx b/src/components/UI/TagInput.jsx
new file mode 100644
index 000000000..09fea6eaa
--- /dev/null
+++ b/src/components/UI/TagInput.jsx
@@ -0,0 +1,83 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import InputItem from "./InputItem";
+import { FlexContainer } from "../../styles/CommonStyles";
+import DeleteButton from "./DeleteButton";
+
+const TagButtonsSection = styled.div`
+ display: flex;
+ gap: 12px;
+ margin-top: 12px;
+ flex-wrap: wrap; // 태그가 길어지면 다음 줄로 넘어가도록 함
+`;
+
+const Tag = styled(FlexContainer)`
+ background-color: ${({ theme }) => theme.colors.gray[2]};
+ color: ${({ theme }) => theme.colors.black};
+ padding: 14px 14px 14px 16px;
+ border-radius: 999px;
+ min-width: 100px;
+`;
+
+const TagText = styled.span`
+ font-size: 16px;
+ line-height: 24px;
+ margin-right: 8px;
+ max-width: calc(100% - 28px); // DeleteButton 너비 및 margin을 제외한 공간
+ /* 태그의 텍스트가 너무 길어 한 줄 내에 표시하기 어려운 경우 말줄임 처리 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+function TagInput({ tags, onAddTag, onRemoveTag }) {
+ const [input, setInput] = useState("");
+
+ // 엔터 키 누르면 tags 배열에 input 값을 추가
+ const onPressEnter = (event) => {
+ // 여러 자모를 결합해 하나의 글자를 만드는 아시아 언어권에서는 IME(입력 메소드 에디터)를 통해 브라우저에 글자를 입력해요.
+ // 사용자가 글자를 완전히 조합하기 전에는 isComposing의 값이 true로 설정됩니다.
+ // 한글 입력 시에 마지막 글자가 하이라이트되는 현상을 보신 적 있을 거예요. 이게 바로 isComposing이 true인 상태로, 아직 입력이 확정되지 않았음을 시각적으로 나타내는 거예요.
+ // 만약 마지막 음절이 태그 배열에 중복으로 추가되는 현상이 있었다면 바로 이 이슈 때문이었을 거예요.
+ // 이 코드를 추가하면 사용자가 아직 입력을 완료하지 않았을 때 함수의 나머지 부분이 실행되지 않도록 하여, 완성되지 않은 입력이 태그로 잘못 추가되는 것을 방지할 수 있어요.
+ if (event.nativeEvent.isComposing) return;
+
+ const inputString = input.trim();
+ if (event.key === "Enter" && inputString) {
+ event.preventDefault(); // 엔터 키 눌렀을 때 form이 제출되지 않도록 꼭 추가해 주세요!
+ onAddTag(inputString);
+ setInput(""); // 태그 추가 후 input field 초기화
+ }
+ };
+
+ return (
+
+ setInput(e.target.value)}
+ onKeyDown={onPressEnter}
+ placeholder="태그를 입력해 주세요"
+ />
+
+ {/* tags 배열이 비어있으면 TagButtonsSection을 렌더링하지 않음 */}
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ onRemoveTag(tag)}
+ label={`${tag} 태그`}
+ />
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default TagInput;
diff --git a/src/index.js b/src/index.js
index 77ca5404e..f916d990e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,10 +2,17 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles/global.css"; // index.js에서 global stylesheet을 import하면 전역적으로 스타일이 적용돼요
+import { ThemeProvider } from "styled-components";
+import theme from "./styles/theme";
+import GlobalStyle from "./styles/GlobalStyle";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
-
+
+ {/* 전역 스타일 관련 코드들은 컴포넌트가 렌더링되기 전에 최상위 수준에서 처리되도록 index.js에서 로드하는 것이 좋아요 */}
+ {/* ThemeProvider를 사용하면 styled-components 내에서 props.theme을 통해 저장된 스타일 정보를 넘겨 받아 간단하게 일관된 스타일을 적용할 수 있어요 */}
+ {/* Styled-components에서는 createGlobalStyle 함수로 생성된 GlobalStyle 컴포넌트를 통해 전역적으로 CSS를 적용할 수 있어요 */}
+
-
+
);
diff --git a/src/pages/AddItemPage/AddItemPage.css b/src/pages/AddItemPage/AddItemPage.css
index 2f671d5a5..e69de29bb 100644
--- a/src/pages/AddItemPage/AddItemPage.css
+++ b/src/pages/AddItemPage/AddItemPage.css
@@ -1,80 +0,0 @@
-.addItems__container {
- width: 1200px;
- max-width: 1200px;
- margin: 0 auto;
- padding-top: 24px;
- padding-bottom: 69px;
-}
-
-.addItems-title__container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-bottom: 24px;
-}
-
-.addItems-title {
- font-size: 20px;
- font-weight: 700;
- line-height: 32px;
-}
-
-.addItems-button {
- background-color: #9ca3af;
- height: 42px;
- border-radius: 8px;
- padding: 0 23px;
- font-size: 16px;
- font-weight: 600;
- line-height: 26px;
-}
-
-.input-form__container {
- display: flex;
- flex-direction: column;
- gap: 32px;
-}
-
-.addItems-img__container,
-.addItems-name__container,
-.addItems-explain__container,
-.addItems-price__container,
-.addItems-tag__container {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.addItems-img__title,
-.addItems-name__title,
-.addItems-explain__title,
-.addItems-price__title,
-.addItems-tag__title {
- font-size: 18px;
- font-weight: 700;
- line-height: 26px;
-}
-
-.addItems-name__input,
-.addItems-price__input,
-.addItems-tag__input {
- height: 56px;
- border: none;
- background-color: #f3f4f6;
- border-radius: 12px;
- padding: 24px 15px;
-}
-
-.addItems-explain__input {
- height: 282px;
- border: none;
- background-color: #f3f4f6;
- border-radius: 12px;
- padding: 24px 15px;
-}
-
-.addItems-tag__flex-container {
- display: flex;
- flex-direction: column;
- gap: 14px;
-}
diff --git a/src/pages/AddItemPage/AddItemPage.jsx b/src/pages/AddItemPage/AddItemPage.jsx
index 90f4a591b..d6699aaf2 100644
--- a/src/pages/AddItemPage/AddItemPage.jsx
+++ b/src/pages/AddItemPage/AddItemPage.jsx
@@ -1,85 +1,91 @@
import React, { useState } from "react";
-import "./AddItemPage.css";
-import Tag from "./components/Tag";
+import {
+ Button,
+ Container,
+ FlexContainer,
+ SectionTitle,
+} from "../../styles/CommonStyles";
+import styled from "styled-components";
+import InputItem from "../../components/UI/InputItem";
+import TagInput from "../../components/UI/TagInput";
+import ImageUpload from "../../components/UI/ImageUpload";
+
+const TitleSection = styled(FlexContainer)`
+ margin-bottom: 16px;
+`;
+
+const InputSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ gap: 24px;
+ }
+`;
+
function AddItemPage() {
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+ const [price, setPrice] = useState("");
const [tags, setTags] = useState([]);
- const [inputValue, setInputValue] = useState("");
- const handleKeyDown = (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- if (inputValue.trim()) {
- setTags((prevTags) => [...prevTags, inputValue.trim()]);
- setInputValue("");
- }
+ // 중복 등록 막기 위해 tags 배열에 없는 것 확인하고 삽입
+ const addTag = (tag) => {
+ if (!tags.includes(tag)) {
+ setTags([...tags, tag]);
}
};
- const handleInputChange = (event) => {
- setInputValue(event.target.value);
- };
- const handleDeleteClick = (tags) => {
- setTags((prevTags) => prevTags.filter((tag) => tag != tags));
+ const removeTag = (tagToRemove) => {
+ setTags(tags.filter((tag) => tag !== tagToRemove));
};
+
+ // form 제출 버튼 활성화 조건: 이미지 제외 모든 input에 값이 입력되어야 함
+ const isSubmitDisabled = !name || !description || !price || !tags.length;
+
return (
-
+
);
}
diff --git a/src/pages/ItemDetailPage/ItemDetailPage.css b/src/pages/ItemDetailPage/ItemDetailPage.css
new file mode 100644
index 000000000..304d8c006
--- /dev/null
+++ b/src/pages/ItemDetailPage/ItemDetailPage.css
@@ -0,0 +1,237 @@
+.itemDetailPage__section {
+ width: 1200px;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding-top: 24px;
+ display: flex;
+ flex-direction: column;
+}
+
+.item-information__container {
+ display: flex;
+ gap: 24px;
+ padding-bottom: 40px;
+}
+
+.item__img {
+ width: 486px;
+ height: 486px;
+ object-fit: cover;
+ border-radius: 16px;
+}
+
+.item-explain__container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.item-explain-title__container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.item-description__container,
+.item-tag__container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.item-description__container {
+ padding: 24px 0;
+}
+.item-description__title,
+.item-tag__title,
+.comment__title {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 26px;
+}
+
+.item-description__text {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 26px;
+}
+
+.item-tag__flex-container {
+ display: flex;
+ gap: 8px;
+}
+
+.item-tag {
+ background-color: #f3f4f6;
+ height: 36px;
+ border-radius: 26px;
+ padding: 5px 16px;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.comment-form__section {
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 24px;
+}
+.comment__title {
+ padding-bottom: 9px;
+}
+
+.comment__input {
+ height: 104px;
+ background-color: #f3f4f6;
+ border: none;
+ border-radius: 12px;
+ padding: 16px 24px;
+ margin-bottom: 16px;
+ text-align: left;
+ vertical-align: top;
+ display: flex;
+ align-items: flex-start;
+ overflow: hidden;
+ resize: none;
+}
+
+.comment__input::placeholder {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 26px;
+}
+
+.comment__button {
+ height: 42px;
+ background-color: #9ca3af;
+ border-radius: 8px;
+ align-self: flex-end;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 12px 23px;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 26px;
+ color: #f3f4f6;
+}
+
+.comment-list__container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ background-color: #fcfcfc;
+}
+
+.comment-list {
+ display: flex;
+ gap: 24px;
+ flex-direction: column;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.comment-list__user-container {
+ display: flex;
+ gap: 8px;
+}
+
+.comment-list__user-img {
+ width: 32px;
+ height: 32px;
+}
+
+.comment-list__user-information {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding-bottom: 12px;
+}
+
+.back-to-list-button__container {
+ display: flex;
+ justify-content: center;
+ align-self: center;
+ margin-top: 64px;
+ margin-bottom: 222px;
+ background-color: #3692ff;
+ border-radius: 40px;
+ width: 240px;
+ height: 48px;
+ gap: 8px;
+ align-items: center;
+ font-size: 18px;
+ font-weight: 600;
+ line-height: 26px;
+ color: #f3f4f6;
+ position: relative;
+}
+
+.comment-list__empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.empty__icon {
+ width: 196px;
+ height: 196px;
+}
+
+.empty__text {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 26px;
+ color: #9ca3af;
+}
+
+.item-explain__user-information {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ justify-self: space-between;
+}
+
+.user__information {
+ display: flex;
+ gap: 16px;
+}
+
+.user__icon {
+ object-fit: contain;
+}
+
+.user__text {
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+}
+
+.user-createdAt__text {
+ color: #9ca3af;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 24px;
+}
+
+.user-nickname__text {
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 24px;
+ color: #4b5563;
+}
+
+.favorite-count__container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 4px 12px;
+ background-color: #ffffff;
+ border: 1px solid #e5e7eb;
+ border-radius: 35px;
+}
diff --git a/src/pages/ItemDetailPage/ItemDetailPage.jsx b/src/pages/ItemDetailPage/ItemDetailPage.jsx
new file mode 100644
index 000000000..01cea83cd
--- /dev/null
+++ b/src/pages/ItemDetailPage/ItemDetailPage.jsx
@@ -0,0 +1,160 @@
+import React, { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { getProductsById, getProductsComments } from "../../api/itemApi";
+import profileIcon from "../../assets/images/icons/ic_profile.png";
+import backButtonIcon from "../../assets/images/icons/ic_back.png";
+import inquiryEmptyIcon from "../../assets/images/icons/Img_inquiry_empty.png";
+import heartIcon from "../../assets/images/icons/ic_heart.png";
+import "./ItemDetailPage.css";
+
+export default function ItemDetailPage() {
+ const { productId } = useParams();
+ const [product, setProduct] = useState(null);
+ const [comments, setComments] = useState({ list: [], nextCursor: null });
+ const [loading, setLoading] = useState(true);
+ const [commentText, setCommentText] = useState("");
+
+ const natigate = useNavigate();
+
+ useEffect(() => {
+ const fetchProduct = async () => {
+ try {
+ const data = await getProductsById(productId);
+ setProduct(data);
+ } catch (error) {
+ console.error("Failed to fetch product details:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchComment = async () => {
+ try {
+ const data = await getProductsComments({ productId, limit: 5 });
+ setComments(data);
+ } catch (error) {
+ console.error("상품 댓글을 가져오지 못했습니다:", error);
+ }
+ };
+
+ fetchProduct();
+ fetchComment();
+ }, [productId]);
+
+ const handleCommentChange = (e) => {
+ setCommentText(e.target.value);
+ };
+
+ const handleBackToList = () => {
+ natigate("/items");
+ };
+
+ if (loading) return
Loading...
;
+ if (!product) return
상품 정보를 불러오는 데 실패했습니다.
;
+
+ return (
+
+
+
+
+
+
{product.name}
+ {product.price}
+
+
+
상품 소개
+
{product.description}
+
+
+
상품 태그
+
+ {product.tags.map((tag) => {
+ return (
+
+ #{tag}
+
+ );
+ })}
+
+
+
+
+
+
+
{product.ownerNickname}
+
+ {product.createdAt.slice(0, 10)}
+
+
+
+
+
+
{product.favoriteCount}
+
+
+
+
+
+ 문의하기
+
+
+ 등록
+
+
+
+ {comments.list.length > 0 ? (
+ comments.list.map((comment) => (
+
+
{comment.content}
+
+
+
+
{comment.writer.nickname}
+
몇 시간전
+
+
+
+ ))
+ ) : (
+
+
+
아직 문의가 없어요
+
+ )}
+
+
+
+ 목록으로 돌아가기
+
+
+
+
+ );
+}
diff --git a/src/pages/MarketPage/MarketPage.css b/src/pages/MarketPage/MarketPage.css
index f304e4a4f..1a1dcbc8d 100644
--- a/src/pages/MarketPage/MarketPage.css
+++ b/src/pages/MarketPage/MarketPage.css
@@ -75,17 +75,6 @@
padding-bottom: 16px;
}
-.sortButtonWrapper {
- position: relative;
-}
-
-.sortDropdownTriggerButton {
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- padding: 9px;
- margin-left: 8px;
-}
-
.searchBarWrapper {
display: flex;
background-color: #f3f4f6;
diff --git a/src/pages/MarketPage/components/AllItemsSection.jsx b/src/pages/MarketPage/components/AllItemsSection.jsx
index a0876a483..995d9acf6 100644
--- a/src/pages/MarketPage/components/AllItemsSection.jsx
+++ b/src/pages/MarketPage/components/AllItemsSection.jsx
@@ -4,7 +4,7 @@ import ItemCard from "./ItemCard";
import { ReactComponent as SortIcon } from "../../../assets/images/icons/ic_sort.svg";
import { ReactComponent as SearchIcon } from "../../../assets/images/icons/ic_search.svg";
import { Link } from "react-router-dom";
-import DropdownList from "../../../components/UI/DropdownList";
+import DropdownMenu from "../../../components/UI/DropdownMenu";
import PaginationBar from "../../../components/UI/PaginationBar";
const getPageSize = () => {
@@ -26,7 +26,6 @@ function AllItemsSection() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(getPageSize());
const [itemList, setItemList] = useState([]);
- const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [totalPageNum, setTotalPageNum] = useState();
const fetchSortedData = async ({ orderBy, page, pageSize }) => {
@@ -37,7 +36,6 @@ function AllItemsSection() {
const handleSortSelection = (sortOption) => {
setOrderBy(sortOption);
- setIsDropdownVisible(false);
};
useEffect(() => {
@@ -55,10 +53,6 @@ function AllItemsSection() {
};
}, [orderBy, page, pageSize]);
- const toggleDropdown = () => {
- setIsDropdownVisible(!isDropdownVisible);
- };
-
const onPageChange = (pageNumber) => {
setPage(pageNumber);
};
@@ -80,17 +74,7 @@ function AllItemsSection() {
placeholder="검색할 상품을 입력해 주세요"
/>
-