diff --git a/src/assets/icons/delete.svg b/src/assets/icons/delete.svg
new file mode 100644
index 00000000..a85baced
--- /dev/null
+++ b/src/assets/icons/delete.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg
new file mode 100644
index 00000000..5bb9abf5
--- /dev/null
+++ b/src/assets/icons/plus.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/FileInput/FileInput.jsx b/src/components/FileInput/FileInput.jsx
new file mode 100644
index 00000000..1d16453a
--- /dev/null
+++ b/src/components/FileInput/FileInput.jsx
@@ -0,0 +1,62 @@
+import * as S from "./FileInput.styles";
+import x from "../../assets/icons/delete.svg";
+import plus from "../../assets/icons/plus.svg";
+import { useState, useRef } from "react";
+
+export default function FileInput({ lable, images, setValues }) {
+ const [preview, setPreview] = useState(null);
+ const inputRef = useRef();
+
+ const handleImageChange = (e) => {
+ const file = e.target.files[0];
+
+ if (file) {
+ setValues((prevState) => ({
+ ...prevState,
+ images: file,
+ }));
+
+ const fileUrl = URL.createObjectURL(file);
+ setPreview(fileUrl);
+ }
+ };
+
+ const handleImageDelete = () => {
+ if (!inputRef.current) return;
+ inputRef.current.value = "";
+ setValues((prevState) => ({
+ ...prevState,
+ images: null,
+ }));
+ setPreview(null);
+ };
+
+ return (
+ <>
+
+ {lable}
+
+
+
+ 이미지 등록
+
+
+
+
+ {images && (
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/FileInput/FileInput.styles.jsx b/src/components/FileInput/FileInput.styles.jsx
new file mode 100644
index 00000000..938a0a11
--- /dev/null
+++ b/src/components/FileInput/FileInput.styles.jsx
@@ -0,0 +1,76 @@
+import { styled } from "styled-components";
+
+export const FileContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+export const Label = styled.div`
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 26px;
+ color: var(--gray800);
+`;
+
+export const File = styled.label`
+ width: 282px;
+ height: 282px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--gray100);
+ border-radius: 12px;
+ cursor: pointer;
+
+ @media (max-width: 1199px) {
+ width: 168px;
+ height: 168px;
+ }
+`;
+
+export const Div = styled.div`
+ width: 74px;
+ height: 86px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+export const PlusIcon = styled.img`
+ width: 48px;
+ height: 48px;
+`;
+
+export const AddImg = styled.div`
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 26px;
+ color: var(--gray400);
+`;
+
+export const Preview = styled.div`
+ display: flex;
+ position: relative;
+`;
+
+export const PreviewImg = styled.img`
+ width: 282px;
+ height: 282px;
+ border-radius: 12px;
+
+ @media (max-width: 1199px) {
+ width: 168px;
+ height: 168px;
+ }
+`;
+
+export const DeleteImg = styled.img`
+ width: 22px;
+ height: 24px;
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ cursor: pointer;:
+`;
diff --git a/src/components/Items/AllItems/AllItems.jsx b/src/components/Items/AllItems/AllItems.jsx
index 26fa56bf..32e3043a 100644
--- a/src/components/Items/AllItems/AllItems.jsx
+++ b/src/components/Items/AllItems/AllItems.jsx
@@ -1,39 +1,92 @@
import * as S from "./AllItems.styles";
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
import ItemCard from "../ItemCard/ItemCard";
import Dropdown from "../../common/Dropdown/Dropdown";
-import { Link } from "react-router-dom";
import Search from "../../Search/Search";
import NoneItem from "../../NoneItem/NoneItem";
+import { getProducts } from "../../../api/products";
+import Paging from "../../Paging/Paging";
+
+const LIST = ["최신순", "좋아요순"];
+
+export default function AllItems() {
+ const [items, setItems] = useState([]);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const [sortOption, setSortOption] = useState("최신순");
+ const [keyword, setKeyword] = useState("");
+ const [totalItems, setTotalItems] = useState(0);
+
+ const orderByValue = sortOption === "최신순" ? "recent" : "favorite";
+
+ const updateItems = () => {
+ if (window.innerWidth <= 767) {
+ setPageSize(4);
+ } else if (window.innerWidth >= 768 && window.innerWidth <= 1199) {
+ setPageSize(6);
+ } else {
+ setPageSize(10);
+ }
+ };
+
+ useEffect(() => {
+ getProducts({
+ page: currentPage,
+ pageSize: pageSize,
+ orderBy: orderByValue,
+ keyword: keyword,
+ }).then((result) => {
+ if (!result) return;
+ setItems(result.list);
+ setTotalItems(result.totalCount);
+ });
+ }, [currentPage, pageSize, orderByValue, keyword]);
+
+ useEffect(() => {
+ updateItems();
+ window.addEventListener("resize", updateItems);
+
+ return () => {
+ window.removeEventListener("resize", updateItems);
+ };
+ }, []);
-const list = ["최신순", "좋아요순"];
+ const handleChangeClick = (sortOption) => {
+ setSortOption(sortOption);
+ };
-export default function AllItems({ items, sortOption, onChange, setKeyword }) {
return (
-
-
-
- 전체 상품
-
- 상품 등록하기
-
-
-
-
-
- 상품 등록하기
-
-
-
-
- {items.length !== 0 ? (
-
- {items.map((items, idx) => (
-
- ))}
-
- ) : (
-
+
+
+
+
+ 전체 상품
+
+ 상품 등록하기
+
+
+
+
+
+ 상품 등록하기
+
+
+
+
+ {items.length !== 0 ? (
+
+ {items.map((items, idx) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ {items.length !== 0 && (
+
)}
-
+
);
}
diff --git a/src/components/Items/AllItems/AllItems.styles.jsx b/src/components/Items/AllItems/AllItems.styles.jsx
index 30effde3..8ef5ff9d 100644
--- a/src/components/Items/AllItems/AllItems.styles.jsx
+++ b/src/components/Items/AllItems/AllItems.styles.jsx
@@ -1,5 +1,11 @@
import styled from "styled-components";
+export const AllItems = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+`;
+
export const AllContainer = styled.div`
display: flex;
flex-direction: column;
diff --git a/src/components/Items/BestItems/BestItems.jsx b/src/components/Items/BestItems/BestItems.jsx
index f5db3640..fd179934 100644
--- a/src/components/Items/BestItems/BestItems.jsx
+++ b/src/components/Items/BestItems/BestItems.jsx
@@ -1,8 +1,37 @@
import * as S from "./BestItems.styles";
+import { useState, useEffect, useCallback } from "react";
import ItemCard from "../ItemCard/ItemCard";
-import { useEffect } from "react";
+import { getProducts } from "../../../api/products";
+
+export default function BestItems() {
+ const [bestItems, setBestItems] = useState([]);
+ const [showItems, setShowItems] = useState(4);
+
+ useEffect(() => {
+ getProducts({
+ page: 1,
+ pageSize: showItems,
+ orderBy: "favorite",
+ keyword: "",
+ }).then((result) => {
+ if (!result) return;
+ const sortedBestItems = [...result.list].slice(0, 4);
+ setBestItems(sortedBestItems);
+ });
+ }, [showItems]);
+
+ const updateBestItems = useCallback(() => {
+ if (window.innerWidth <= 767) {
+ setShowItems(1);
+ } else if (window.innerWidth >= 768 && window.innerWidth <= 1199) {
+ setShowItems(2);
+ } else {
+ setShowItems(4);
+ }
+ }, []);
+
+ const responsiveItems = bestItems.slice(0, showItems);
-export default function BestItems({ bestItems, updateBestItems }) {
useEffect(() => {
updateBestItems();
window.addEventListener("resize", updateBestItems);
@@ -16,7 +45,7 @@ export default function BestItems({ bestItems, updateBestItems }) {
베스트 상품
- {bestItems.map((items, idx) => (
+ {responsiveItems.map((items, idx) => (
))}
diff --git a/src/components/Items/ItemCard/ItemCard.jsx b/src/components/Items/ItemCard/ItemCard.jsx
index bfa18f92..eaab547a 100644
--- a/src/components/Items/ItemCard/ItemCard.jsx
+++ b/src/components/Items/ItemCard/ItemCard.jsx
@@ -1,12 +1,18 @@
import * as S from "./ItemCard.styles";
import heart from "../../../assets/icons/heart.svg";
import NoneImage from "../../NoneImage/NoneImage";
+import { useState } from "react";
-// images 값이 없으면 NoneImage 컴포넌트가 보이도록 구현했는데 images 배열 안에 값이 있지만 사진이 안 불러와지는 경우에는 어떻게 처리해야 하는지 고민입니다.
export default function ItemCard({ list = "best", images, name, price, favoriteCount }) {
+ const [isImgError, setIsImgError] = useState(false);
+
return (
- {images[0] ? : }
+ {images[0] && !isImgError ? (
+ setIsImgError(true)} />
+ ) : (
+
+ )}
{name}
{price.toLocaleString()}원
diff --git a/src/components/Paging/Paging.styles.jsx b/src/components/Paging/Paging.styles.jsx
index af75a280..79b05ea8 100644
--- a/src/components/Paging/Paging.styles.jsx
+++ b/src/components/Paging/Paging.styles.jsx
@@ -3,7 +3,6 @@ import { styled } from "styled-components";
export const PagingContainer = styled.div`
width: 100%;
display: flex;
- margin-bottom: 40px;
.pagination {
display: flex;
diff --git a/src/components/Tag/Tag.jsx b/src/components/Tag/Tag.jsx
new file mode 100644
index 00000000..322bc5fc
--- /dev/null
+++ b/src/components/Tag/Tag.jsx
@@ -0,0 +1,13 @@
+import * as S from "./Tag.styles";
+import x from "../../assets/icons/delete.svg";
+
+export default function Tag({ tag, onClick }) {
+ return (
+
+
+ #{tag}
+
+
+
+ );
+}
diff --git a/src/components/Tag/Tag.styles.jsx b/src/components/Tag/Tag.styles.jsx
new file mode 100644
index 00000000..2a227c66
--- /dev/null
+++ b/src/components/Tag/Tag.styles.jsx
@@ -0,0 +1,27 @@
+import { styled } from "styled-components";
+
+export const TagContainer = styled.li`
+ height: 36px;
+ background-color: var(--gray100);
+ border-radius: 26px;
+ padding: 6px 12px;
+`;
+
+export const Tag = styled.div`
+ width: 100%;
+ display: flex;
+ gap: 8px;
+`;
+
+export const TagName = styled.div`
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 26px;
+ color: var(--gray800);
+`;
+
+export const DeleteTag = styled.img`
+ width: 22px;
+ height: 24px;
+ cursor: pointer;
+`;
diff --git a/src/components/common/Dropdown/Dropdown.styles.jsx b/src/components/common/Dropdown/Dropdown.styles.jsx
index cb919d54..b0821856 100644
--- a/src/components/common/Dropdown/Dropdown.styles.jsx
+++ b/src/components/common/Dropdown/Dropdown.styles.jsx
@@ -46,7 +46,6 @@ export const PresentValue = styled.div`
}
`;
-// UI component에서 style 작업을 하고 싶지 않아 content를 이용해서 icon 변경했는데 content를 쓰는 게 맞는지 모르겠습니다.
export const Arrow = styled.img`
width: 24px;
height: 24px;
diff --git a/src/components/common/Header/Header.jsx b/src/components/common/Header/Header.jsx
index e8bd96be..aff14729 100644
--- a/src/components/common/Header/Header.jsx
+++ b/src/components/common/Header/Header.jsx
@@ -1,15 +1,28 @@
-import { NavLink } from "react-router-dom";
import * as S from "./Header.styles";
+import { NavLink, useLocation } from "react-router-dom";
import logo from "../../../assets/icons/panda.svg";
import user from "../../../assets/icons/user.svg";
-const activeLink = ({ isActive }) => {
- return {
- color: isActive ? "var(--primary)" : "var(--gray600)",
+export default function Header() {
+ const location = useLocation().pathname;
+
+ const activeLink = ({ isActive }) => {
+ const isItemsOrAddItem = isActive || location.startsWith("/addItem");
+
+ return {
+ color: isItemsOrAddItem ? "var(--primary)" : "var(--gray600)",
+ };
};
-};
-export default function Header() {
+ const navLink = [
+ {
+ to: "/freeBoard",
+ style: ({ isActive }) => ({ color: isActive ? "var(--primary)" : "var(--gray600)" }),
+ name: "자유게시판",
+ },
+ { to: "/items", style: activeLink, name: "중고마켓" },
+ ];
+
return (
@@ -18,16 +31,13 @@ export default function Header() {
판다마켓
-
-
- 자유게시판
-
-
-
-
- 중고마켓
-
-
+ {navLink.map((l, idx) => (
+
+
+ {l.name}
+
+
+ ))}
diff --git a/src/components/common/Input/Input.jsx b/src/components/common/Input/Input.jsx
new file mode 100644
index 00000000..cd7d98f1
--- /dev/null
+++ b/src/components/common/Input/Input.jsx
@@ -0,0 +1,10 @@
+import * as S from "./Input.styles";
+
+export default function Input({ label, style, isTextarea, onChange, ...rest }) {
+ return (
+
+ {label}
+
+
+ );
+}
diff --git a/src/components/common/Input/Input.styles.jsx b/src/components/common/Input/Input.styles.jsx
new file mode 100644
index 00000000..bafff8d2
--- /dev/null
+++ b/src/components/common/Input/Input.styles.jsx
@@ -0,0 +1,35 @@
+import { styled } from "styled-components";
+
+export const InputContainer = styled.div`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+export const Label = styled.label`
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 26px;
+ color: var(--gray800);
+`;
+
+export const StyledInput = styled.input`
+ width: 100%;
+ background-color: var(--gray100);
+ padding: 16px 24px;
+ border-radius: 12px;
+ resize: none;
+
+ &:focus {
+ outline: none;
+ }
+
+ &::placeholder {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 26px;
+ color: var(--gray400);
+ }
+`;
diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx
index 116f504d..a1813c3a 100644
--- a/src/components/pages/AddItemPage/AddItemPage.jsx
+++ b/src/components/pages/AddItemPage/AddItemPage.jsx
@@ -1,3 +1,152 @@
+import * as S from "./AddItemPage.styles";
+import Input from "../../common/Input/Input";
+import FileInput from "../../FileInput/FileInput";
+import Tag from "../../Tag/Tag";
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { isValidAddItem } from "../../../utils/addValidate";
+
+const INITIAL_VALUE = {
+ images: null,
+ name: "",
+ description: "",
+ price: 0,
+ tags: [],
+};
+
export default function AddItemPage() {
- return <>>;
+ const navigate = useNavigate();
+ const [values, setValues] = useState(INITIAL_VALUE);
+ const [tag, setTag] = useState("");
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+
+ setValues((prevState) => ({
+ ...prevState,
+ [name]: value,
+ }));
+ };
+
+ const handleTagChange = (e) => {
+ // IME composition
+ if (e.nativeEvent.isComposing) return;
+
+ const eqaulTag = values.tags.some((existing) => existing === tag);
+ const enterEvent = e.key === "Enter";
+
+ if (!eqaulTag) {
+ if (enterEvent && tag.trim() !== "") {
+ setValues((prevState) => ({
+ ...prevState,
+ tags: [...prevState.tags, tag],
+ }));
+ setTag("");
+ }
+ } else if (enterEvent) {
+ e.preventDefault();
+ alert("이미 존재하는 태그입니다!");
+ }
+ };
+
+ const handleTagDelete = (deleteTag) => {
+ setValues((prevState) => ({
+ ...prevState,
+ tags: prevState.tags.filter((tag) => tag !== deleteTag),
+ }));
+ };
+
+ const handleSubmit = (e) => {
+ if (isValidAddItem(values)) {
+ e.preventDefault();
+ // const formData = new FormData();
+ // formData.append("images", values.images);
+ // formData.append("name", values.name);
+ // formData.append("description", values.description);
+ // formData.append("price", values.price);
+ // formData.append("tags", values.tags);
+ navigate("/items");
+ }
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ }
+ };
+
+ const INPUT = [
+ {
+ label: "상품명",
+ name: "name",
+ type: "text",
+ placeholder: "상품명을 입력해주세요",
+ value: values.name,
+ onChange: handleInputChange,
+ },
+ {
+ label: "상품 소개",
+ name: "description",
+ type: "text",
+ placeholder: "상품 소개를 입력해주세요",
+ style: { height: "282px" },
+ value: values.description,
+ isTextarea: true,
+ onChange: handleInputChange,
+ },
+ {
+ label: "판매가격",
+ name: "price",
+ type: "number",
+ placeholder: "판매 가격을 입력해주세요",
+ value: values.price ? values.price : "",
+ onChange: handleInputChange,
+ },
+ {
+ label: "태그",
+ name: "tags",
+ type: "text",
+ placeholder: "태그를 입력해주세요",
+ value: tag,
+ onChange: (e) => setTag(e.target.value),
+ onKeyDown: handleTagChange,
+ },
+ ];
+
+ return (
+
+
+
+ 상품 등록하기
+
+ 등록
+
+
+
+
+
+
+ {INPUT.map((i, idx) => (
+
+ ))}
+
+
+ {values.tags.map((t, idx) => (
+ handleTagDelete(t)} />
+ ))}
+
+
+
+ );
}
diff --git a/src/components/pages/AddItemPage/AddItemPage.styles.jsx b/src/components/pages/AddItemPage/AddItemPage.styles.jsx
new file mode 100644
index 00000000..7d372d8f
--- /dev/null
+++ b/src/components/pages/AddItemPage/AddItemPage.styles.jsx
@@ -0,0 +1,79 @@
+import { styled } from "styled-components";
+
+export const AddItemContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const AddItem = styled.form`
+ max-width: 1200px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ padding: 10px 0;
+
+ @media (max-width: 767px) {
+ min-width: 346px;
+ padding: 0 16px;
+ }
+
+ @media (min-width: 768px) and (max-width: 1199px) {
+ min-width: 696px;
+ padding: 0 24px;
+ }
+`;
+
+export const AddItemHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+`;
+
+export const Add = styled.h1`
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 32px;
+ color: var(--gray800);
+`;
+
+export const AddBtn = styled.button`
+ width: 72px;
+ height: 42px;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 26px;
+ background-color: var(--primary);
+ color: var(--gray100);
+ cursor: pointer;
+
+ &:disabled {
+ background-color: var(--gray400);
+ }
+`;
+
+export const InputContainer = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+`;
+
+export const AddImg = styled.div`
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-end;
+ gap: 24px;
+`;
+
+export const TagList = styled.ul`
+ width: 100%;
+ flex-wrap: wrap;
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ margin-top: 14px;
+`;
diff --git a/src/components/pages/ItemPage/ItemPage.jsx b/src/components/pages/ItemPage/ItemPage.jsx
index 04e17fe1..8b5fe90d 100644
--- a/src/components/pages/ItemPage/ItemPage.jsx
+++ b/src/components/pages/ItemPage/ItemPage.jsx
@@ -1,90 +1,12 @@
import * as S from "./ItemPage.styles";
import BestItems from "../../Items/BestItems/BestItems";
import AllItems from "../../Items/AllItems/AllItems";
-import { useEffect, useState } from "react";
-import { getProducts } from "../../../api/products";
-import Paging from "../../Paging/Paging";
export default function ItemPage() {
- const [items, setItems] = useState([]);
- const [bestItems, setBestItems] = useState([]);
- const [currentPage, setCurrentPage] = useState(1);
- const [pageSize, setPageSize] = useState(10);
- const [sortOption, setSortOption] = useState("최신순");
- const [keyword, setKeyword] = useState("");
- const [totalItems, setTotalItems] = useState(0);
- const [showItems, setShowItems] = useState(4);
-
- const orderByValue = sortOption === "최신순" ? "recent" : "favorite";
-
- const updateItems = () => {
- if (window.innerWidth <= 767) {
- setPageSize(4);
- } else if (window.innerWidth >= 768 && window.innerWidth <= 1199) {
- setPageSize(6);
- } else {
- setPageSize(10);
- }
- };
-
- const updateBestItems = () => {
- if (window.innerWidth <= 767) {
- setShowItems(1);
- } else if (window.innerWidth >= 768 && window.innerWidth <= 1199) {
- setShowItems(2);
- } else {
- setShowItems(4);
- }
- };
-
- useEffect(() => {
- getProducts({
- page: 1,
- pageSize: showItems,
- orderBy: "favorite",
- keyword: "",
- }).then((result) => {
- if (!result) return;
- const sortedBestItems = [...result.list].slice(0, 4);
- setBestItems(sortedBestItems);
- });
- }, [showItems]);
-
- useEffect(() => {
- getProducts({
- page: currentPage,
- pageSize: pageSize,
- orderBy: orderByValue,
- keyword: keyword,
- }).then((result) => {
- if (!result) return;
- setItems(result.list);
- setTotalItems(result.totalCount);
- });
- }, [currentPage, pageSize, orderByValue, keyword]);
-
- useEffect(() => {
- updateItems();
- window.addEventListener("resize", updateItems);
-
- return () => {
- window.removeEventListener("resize", updateItems);
- };
- }, []);
-
- const handleChangeClick = (sortOption) => {
- setSortOption(sortOption);
- };
-
- const responsiveItems = bestItems.slice(0, showItems);
-
return (
-
-
- {items.length !== 0 && (
-
- )}
+
+
);
}
diff --git a/src/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx
index 50bdafcf..de8f6a39 100644
--- a/src/styles/GlobalStyle.jsx
+++ b/src/styles/GlobalStyle.jsx
@@ -7,10 +7,19 @@ const GlobalStyle = createGlobalStyle`
list-style: none;
}
-body {
+body, ul, li {
margin: 0;
}
+ul, li{
+ margin:0;
+ padding:0;
+}
+
+button, input, textarea {
+ border:none;
+}
+
:root {
--primary: #3692ff;
--primary-hover: #1967d6;
diff --git a/src/utils/addValidate.js b/src/utils/addValidate.js
new file mode 100644
index 00000000..b50e8354
--- /dev/null
+++ b/src/utils/addValidate.js
@@ -0,0 +1,5 @@
+export const isValidAddItem = (values) => {
+ const isValid = values.name.trim() && values.description.trim() && values.price > 0 && values.tags.length > 0;
+
+ return isValid;
+};