From e0095ffada29fdf8262d1c7e6ceb05f4da66f87c Mon Sep 17 00:00:00 2001 From: heejin Date: Fri, 17 Jan 2025 21:32:36 +0900 Subject: [PATCH 01/12] Feat : input common component & global style add --- src/components/common/Input/Input.jsx | 10 ++++++ src/components/common/Input/Input.styles.jsx | 35 ++++++++++++++++++++ src/styles/GlobalStyle.jsx | 4 +++ 3 files changed, 49 insertions(+) create mode 100644 src/components/common/Input/Input.jsx create mode 100644 src/components/common/Input/Input.styles.jsx 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/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx index 50bdafcf..2eae4814 100644 --- a/src/styles/GlobalStyle.jsx +++ b/src/styles/GlobalStyle.jsx @@ -11,6 +11,10 @@ body { margin: 0; } +button, input, textarea { + border:none; +} + :root { --primary: #3692ff; --primary-hover: #1967d6; From 5b3348b424ec31fad09c433242f940735b64b257 Mon Sep 17 00:00:00 2001 From: heejin Date: Fri, 17 Jan 2025 22:24:49 +0900 Subject: [PATCH 02/12] Feat : file input component --- src/assets/icons/plus.svg | 4 ++ src/components/FileInput/FileInput.jsx | 17 +++++++ src/components/FileInput/FileInput.styles.jsx | 46 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 src/assets/icons/plus.svg create mode 100644 src/components/FileInput/FileInput.jsx create mode 100644 src/components/FileInput/FileInput.styles.jsx 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..f3f593ad --- /dev/null +++ b/src/components/FileInput/FileInput.jsx @@ -0,0 +1,17 @@ +import * as S from "./FileInput.styles"; +import plus from "../../assets/icons/plus.svg"; + +export default function FileInput({ lable, onChange }) { + return ( + + {lable} + + + + 이미지 등록 + + + + + ); +} diff --git a/src/components/FileInput/FileInput.styles.jsx b/src/components/FileInput/FileInput.styles.jsx new file mode 100644 index 00000000..8a10ebd2 --- /dev/null +++ b/src/components/FileInput/FileInput.styles.jsx @@ -0,0 +1,46 @@ +import { styled } from "styled-components"; + +export const FileContainer = styled.div` + 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 File = styled.div` + width: 282px; + height: 282px; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--gray100); + border-radius: 12px; + cursor: pointer; +`; + +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); +`; From a556efc4ffa8107ba3e180dd9f987bd35e77ff91 Mon Sep 17 00:00:00 2001 From: heejin Date: Fri, 17 Jan 2025 23:27:01 +0900 Subject: [PATCH 03/12] Feat : tag component --- src/assets/icons/delete.svg | 5 +++++ src/components/Tag/Tag.jsx | 13 +++++++++++++ src/components/Tag/Tag.styles.jsx | 26 ++++++++++++++++++++++++++ src/styles/GlobalStyle.jsx | 7 ++++++- 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/assets/icons/delete.svg create mode 100644 src/components/Tag/Tag.jsx create mode 100644 src/components/Tag/Tag.styles.jsx 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/components/Tag/Tag.jsx b/src/components/Tag/Tag.jsx new file mode 100644 index 00000000..f970a137 --- /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 }) { + return ( + + + #{tag} + + + + ); +} diff --git a/src/components/Tag/Tag.styles.jsx b/src/components/Tag/Tag.styles.jsx new file mode 100644 index 00000000..4fd3e0b4 --- /dev/null +++ b/src/components/Tag/Tag.styles.jsx @@ -0,0 +1,26 @@ +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; +`; diff --git a/src/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx index 2eae4814..de8f6a39 100644 --- a/src/styles/GlobalStyle.jsx +++ b/src/styles/GlobalStyle.jsx @@ -7,10 +7,15 @@ const GlobalStyle = createGlobalStyle` list-style: none; } -body { +body, ul, li { margin: 0; } +ul, li{ + margin:0; + padding:0; +} + button, input, textarea { border:none; } From 3b5ce2fb2f68a6ae0dc459954cbea1c9099a069c Mon Sep 17 00:00:00 2001 From: heejin Date: Sat, 18 Jan 2025 01:39:59 +0900 Subject: [PATCH 04/12] Feat : add item component UI --- .../pages/AddItemPage/AddItemPage.jsx | 82 +++++++++++++++++- .../pages/AddItemPage/AddItemPage.styles.jsx | 84 +++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/components/pages/AddItemPage/AddItemPage.styles.jsx diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx index 116f504d..157b958b 100644 --- a/src/components/pages/AddItemPage/AddItemPage.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.jsx @@ -1,3 +1,83 @@ +import * as S from "./AddItemPage.styles"; +import Input from "../../common/Input/Input"; +import FileInput from "../../FileInput/FileInput"; +import x from "../../../assets/icons/delete.svg"; +import Tag from "../../Tag/Tag"; + export default function AddItemPage() { - return <>; + const INPUT = [ + { + label: "상품명", + name: "name", + type: "text", + placeholder: "상품명을 입력해주세요", + value: "", + isTextarea: false, + }, + { + label: "상품 소개", + name: "description", + type: "text", + placeholder: "상품 소개를 입력해주세요", + style: { height: "282px" }, + value: "", + isTextarea: true, + }, + { + label: "판매가격", + name: "price", + type: "number", + placeholder: "판매 가격을 입력해주세요", + value: "", + isTextarea: false, + }, + { + label: "태그", + name: "tag", + type: "text", + placeholder: "태그를 입력해주세요", + value: "", + isTextarea: false, + }, + ]; + + const tag = ["티셔츠", "상의"]; + + return ( + + + + 상품 등록하기 + 등록 + + + + console.log("")} /> + + + + + + {INPUT.map((i, idx) => ( + console.log("")} + /> + ))} + + + {tag.map((t, idx) => ( + + ))} + + + + ); } diff --git a/src/components/pages/AddItemPage/AddItemPage.styles.jsx b/src/components/pages/AddItemPage/AddItemPage.styles.jsx new file mode 100644 index 00000000..85f1778f --- /dev/null +++ b/src/components/pages/AddItemPage/AddItemPage.styles.jsx @@ -0,0 +1,84 @@ +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.div` + width: 1200px; + display: flex; + flex-direction: column; + padding: 10px 0; +`; + +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; +`; + +export const InputContainer = styled.form` + 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 Preview = styled.div` + display: flex; + position: relative; +`; + +export const PreviewImg = styled.img` + width: 282px; + height: 282px; + background-color: black; + border-radius: 12px; +`; + +export const DeleteImg = styled.img` + width: 22px; + height: 24px; + position: absolute; + right: 10px; + top: 10px; + cursor: pointer;: +`; + +export const TagList = styled.ul` + width: 100%; + display: flex; + align-items: flex-start; + gap: 12px; + margin-top: 14px; +`; From ceaba3b5d222100e02eb33916793c8142c1123ed Mon Sep 17 00:00:00 2001 From: heejin Date: Sat, 18 Jan 2025 01:41:51 +0900 Subject: [PATCH 05/12] =?UTF-8?q?Feat=20:=20if=20pathname=20is=20addItem,?= =?UTF-8?q?=20highlighting=20=EC=A4=91=EA=B3=A0=EB=A7=88=EC=BC=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Header/Header.jsx | 42 +++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) 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} + + + ))} From d66cbf8f2d6f188d08343ff485bb4ce0174a076c Mon Sep 17 00:00:00 2001 From: heejin Date: Sun, 19 Jan 2025 03:57:03 +0900 Subject: [PATCH 06/12] Feat : form data change handler & image file delete handler --- src/components/FileInput/FileInput.jsx | 67 ++++++++++++++++--- src/components/FileInput/FileInput.styles.jsx | 24 ++++++- .../pages/AddItemPage/AddItemPage.jsx | 63 ++++++++++++----- .../pages/AddItemPage/AddItemPage.styles.jsx | 21 ------ 4 files changed, 123 insertions(+), 52 deletions(-) diff --git a/src/components/FileInput/FileInput.jsx b/src/components/FileInput/FileInput.jsx index f3f593ad..1d16453a 100644 --- a/src/components/FileInput/FileInput.jsx +++ b/src/components/FileInput/FileInput.jsx @@ -1,17 +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); + }; -export default function FileInput({ lable, onChange }) { return ( - - {lable} - - - - 이미지 등록 - - - - + <> + + {lable} + + + + 이미지 등록 + + + + + {images && ( + + + + + )} + ); } diff --git a/src/components/FileInput/FileInput.styles.jsx b/src/components/FileInput/FileInput.styles.jsx index 8a10ebd2..4ac82775 100644 --- a/src/components/FileInput/FileInput.styles.jsx +++ b/src/components/FileInput/FileInput.styles.jsx @@ -6,14 +6,14 @@ export const FileContainer = styled.div` gap: 16px; `; -export const Label = styled.label` +export const Label = styled.div` font-size: 18px; font-weight: 700; line-height: 26px; color: var(--gray800); `; -export const File = styled.div` +export const File = styled.label` width: 282px; height: 282px; display: flex; @@ -44,3 +44,23 @@ export const AddImg = styled.div` 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; +`; + +export const DeleteImg = styled.img` + width: 22px; + height: 24px; + position: absolute; + right: 10px; + top: 10px; + cursor: pointer;: +`; diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx index 157b958b..a8f274b9 100644 --- a/src/components/pages/AddItemPage/AddItemPage.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.jsx @@ -1,18 +1,48 @@ import * as S from "./AddItemPage.styles"; import Input from "../../common/Input/Input"; import FileInput from "../../FileInput/FileInput"; -import x from "../../../assets/icons/delete.svg"; import Tag from "../../Tag/Tag"; +import { useState } from "react"; + +const INITIAL_VALUE = { + images: null, + productName: "", + description: "", + price: 0, + tag: [], +}; export default function AddItemPage() { + const [values, setValues] = useState(INITIAL_VALUE); + const [tag, setTag] = useState(""); + + const handleTagChange = (e) => { + if (e.key === "Enter" && tag.trim() !== "") { + setValues((prevState) => ({ + ...prevState, + tag: [...prevState.tag, tag], + })); + setTag(""); + } + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + + setValues((prevState) => ({ + ...prevState, + [name]: value, + })); + }; + const INPUT = [ { label: "상품명", - name: "name", + name: "productName", type: "text", placeholder: "상품명을 입력해주세요", - value: "", - isTextarea: false, + value: values.productName, + onChange: handleInputChange, }, { label: "상품 소개", @@ -20,29 +50,29 @@ export default function AddItemPage() { type: "text", placeholder: "상품 소개를 입력해주세요", style: { height: "282px" }, - value: "", + value: values.description, isTextarea: true, + onChange: handleInputChange, }, { label: "판매가격", name: "price", type: "number", placeholder: "판매 가격을 입력해주세요", - value: "", - isTextarea: false, + value: values.price ? values.price : "", + onChange: handleInputChange, }, { label: "태그", name: "tag", type: "text", placeholder: "태그를 입력해주세요", - value: "", - isTextarea: false, + value: tag, + onChange: (e) => setTag(e.target.value), + onKeyDown: handleTagChange, }, ]; - const tag = ["티셔츠", "상의"]; - return ( @@ -52,11 +82,7 @@ export default function AddItemPage() { - console.log("")} /> - - - - + {INPUT.map((i, idx) => ( console.log("")} + onChange={i.onChange} + onKeyDown={i.onKeyDown} /> ))} - {tag.map((t, idx) => ( + {values.tag.map((t, idx) => ( ))} diff --git a/src/components/pages/AddItemPage/AddItemPage.styles.jsx b/src/components/pages/AddItemPage/AddItemPage.styles.jsx index 85f1778f..cd07237a 100644 --- a/src/components/pages/AddItemPage/AddItemPage.styles.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.styles.jsx @@ -54,27 +54,6 @@ export const AddImg = styled.div` gap: 24px; `; -export const Preview = styled.div` - display: flex; - position: relative; -`; - -export const PreviewImg = styled.img` - width: 282px; - height: 282px; - background-color: black; - border-radius: 12px; -`; - -export const DeleteImg = styled.img` - width: 22px; - height: 24px; - position: absolute; - right: 10px; - top: 10px; - cursor: pointer;: -`; - export const TagList = styled.ul` width: 100%; display: flex; From 038cd0fa8538f59ec643cbae93c41931abd02662 Mon Sep 17 00:00:00 2001 From: heejin Date: Sun, 19 Jan 2025 19:02:00 +0900 Subject: [PATCH 07/12] Feat - tag delete handler - submit handler(prevent submit using enter) - when submitting successfully, navigate '/items' --- src/components/Tag/Tag.jsx | 4 +- src/components/Tag/Tag.styles.jsx | 1 + .../pages/AddItemPage/AddItemPage.jsx | 50 ++++++++++++++----- .../pages/AddItemPage/AddItemPage.styles.jsx | 4 +- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/components/Tag/Tag.jsx b/src/components/Tag/Tag.jsx index f970a137..322bc5fc 100644 --- a/src/components/Tag/Tag.jsx +++ b/src/components/Tag/Tag.jsx @@ -1,12 +1,12 @@ import * as S from "./Tag.styles"; import x from "../../assets/icons/delete.svg"; -export default function Tag({ tag }) { +export default function Tag({ tag, onClick }) { return ( #{tag} - + ); diff --git a/src/components/Tag/Tag.styles.jsx b/src/components/Tag/Tag.styles.jsx index 4fd3e0b4..2a227c66 100644 --- a/src/components/Tag/Tag.styles.jsx +++ b/src/components/Tag/Tag.styles.jsx @@ -23,4 +23,5 @@ export const TagName = styled.div` export const DeleteTag = styled.img` width: 22px; height: 24px; + cursor: pointer; `; diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx index a8f274b9..03088c34 100644 --- a/src/components/pages/AddItemPage/AddItemPage.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.jsx @@ -3,42 +3,68 @@ 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"; const INITIAL_VALUE = { images: null, - productName: "", + name: "", description: "", price: 0, - tag: [], + tags: [], }; export default function AddItemPage() { + 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 handleSubmit = (e) => { + 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 handleTagChange = (e) => { if (e.key === "Enter" && tag.trim() !== "") { setValues((prevState) => ({ ...prevState, - tag: [...prevState.tag, tag], + tags: [...prevState.tags, tag], })); setTag(""); } }; - const handleInputChange = (e) => { - const { name, value } = e.target; + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + } + }; + const handleTagDelete = (deleteTag) => { setValues((prevState) => ({ ...prevState, - [name]: value, + tags: prevState.tags.filter((tag) => tag !== deleteTag), })); }; const INPUT = [ { label: "상품명", - name: "productName", + name: "name", type: "text", placeholder: "상품명을 입력해주세요", value: values.productName, @@ -64,7 +90,7 @@ export default function AddItemPage() { }, { label: "태그", - name: "tag", + name: "tags", type: "text", placeholder: "태그를 입력해주세요", value: tag, @@ -75,10 +101,10 @@ export default function AddItemPage() { return ( - + 상품 등록하기 - 등록 + 등록 @@ -100,8 +126,8 @@ export default function AddItemPage() { ))} - {values.tag.map((t, 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 index cd07237a..f78666a0 100644 --- a/src/components/pages/AddItemPage/AddItemPage.styles.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.styles.jsx @@ -7,7 +7,7 @@ export const AddItemContainer = styled.div` align-items: center; `; -export const AddItem = styled.div` +export const AddItem = styled.form` width: 1200px; display: flex; flex-direction: column; @@ -40,7 +40,7 @@ export const AddBtn = styled.button` cursor: pointer; `; -export const InputContainer = styled.form` +export const InputContainer = styled.div` width: 100%; display: flex; flex-direction: column; From d66f007317939d520e9d7e0356f8dfeccaa6fe9a Mon Sep 17 00:00:00 2001 From: heejin Date: Mon, 20 Jan 2025 03:37:18 +0900 Subject: [PATCH 08/12] Feat - addItem validate - submit button disabled - tag IME problem fix --- .../pages/AddItemPage/AddItemPage.jsx | 46 +++++++++++-------- .../pages/AddItemPage/AddItemPage.styles.jsx | 4 ++ src/utils/addValidate.js | 5 ++ 3 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 src/utils/addValidate.js diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx index 03088c34..a63f2100 100644 --- a/src/components/pages/AddItemPage/AddItemPage.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.jsx @@ -4,6 +4,7 @@ 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, @@ -27,18 +28,10 @@ export default function AddItemPage() { })); }; - const handleSubmit = (e) => { - 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 handleTagChange = (e) => { + // IME composition + if (e.nativeEvent.isComposing) return; + if (e.key === "Enter" && tag.trim() !== "") { setValues((prevState) => ({ ...prevState, @@ -48,12 +41,6 @@ export default function AddItemPage() { } }; - const handleKeyDown = (e) => { - if (e.key === "Enter") { - e.preventDefault(); - } - }; - const handleTagDelete = (deleteTag) => { setValues((prevState) => ({ ...prevState, @@ -61,13 +48,32 @@ export default function AddItemPage() { })); }; + 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.productName, + value: values.name, onChange: handleInputChange, }, { @@ -104,7 +110,9 @@ export default function AddItemPage() { 상품 등록하기 - 등록 + + 등록 + diff --git a/src/components/pages/AddItemPage/AddItemPage.styles.jsx b/src/components/pages/AddItemPage/AddItemPage.styles.jsx index f78666a0..610c8b72 100644 --- a/src/components/pages/AddItemPage/AddItemPage.styles.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.styles.jsx @@ -38,6 +38,10 @@ export const AddBtn = styled.button` background-color: var(--primary); color: var(--gray100); cursor: pointer; + + &:disabled { + background-color: var(--gray400); + } `; export const InputContainer = styled.div` 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; +}; From 911bb745b0a27d39434f5ae1cc22915b749ce184 Mon Sep 17 00:00:00 2001 From: heejin Date: Mon, 20 Jan 2025 12:23:54 +0900 Subject: [PATCH 09/12] Feat : If values.tags has same value, alret & e.preventDefault --- .../pages/AddItemPage/AddItemPage.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx index a63f2100..ac95dd96 100644 --- a/src/components/pages/AddItemPage/AddItemPage.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.jsx @@ -32,12 +32,19 @@ export default function AddItemPage() { // IME composition if (e.nativeEvent.isComposing) return; - if (e.key === "Enter" && tag.trim() !== "") { - setValues((prevState) => ({ - ...prevState, - tags: [...prevState.tags, tag], - })); - setTag(""); + const eqaulTag = values.tags.some((existing) => existing === tag); + + if (!eqaulTag) { + if (e.key === "Enter" && tag.trim() !== "") { + setValues((prevState) => ({ + ...prevState, + tags: [...prevState.tags, tag], + })); + setTag(""); + } + } else { + e.preventDefault(); + alert("이미 존재하는 태그입니다!"); } }; From b0399b3453e249835b1e8d7be2d1024296f8a1bc Mon Sep 17 00:00:00 2001 From: heejin Date: Tue, 21 Jan 2025 19:58:59 +0900 Subject: [PATCH 10/12] Feat : responsive css --- src/components/FileInput/FileInput.styles.jsx | 10 ++++++++++ .../pages/AddItemPage/AddItemPage.styles.jsx | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/FileInput/FileInput.styles.jsx b/src/components/FileInput/FileInput.styles.jsx index 4ac82775..938a0a11 100644 --- a/src/components/FileInput/FileInput.styles.jsx +++ b/src/components/FileInput/FileInput.styles.jsx @@ -22,6 +22,11 @@ export const File = styled.label` background-color: var(--gray100); border-radius: 12px; cursor: pointer; + + @media (max-width: 1199px) { + width: 168px; + height: 168px; + } `; export const Div = styled.div` @@ -54,6 +59,11 @@ 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` diff --git a/src/components/pages/AddItemPage/AddItemPage.styles.jsx b/src/components/pages/AddItemPage/AddItemPage.styles.jsx index 610c8b72..092eef99 100644 --- a/src/components/pages/AddItemPage/AddItemPage.styles.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.styles.jsx @@ -8,10 +8,21 @@ export const AddItemContainer = styled.div` `; export const AddItem = styled.form` - width: 1200px; + 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` From 243851dad3af4f00f3d6aee4c1bebf39903a82b4 Mon Sep 17 00:00:00 2001 From: heejin Date: Tue, 21 Jan 2025 20:04:39 +0900 Subject: [PATCH 11/12] Feat & Fix - tag list responsive css - equal tag enterEvent error fix --- src/components/pages/AddItemPage/AddItemPage.jsx | 5 +++-- src/components/pages/AddItemPage/AddItemPage.styles.jsx | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx index ac95dd96..a1813c3a 100644 --- a/src/components/pages/AddItemPage/AddItemPage.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.jsx @@ -33,16 +33,17 @@ export default function AddItemPage() { if (e.nativeEvent.isComposing) return; const eqaulTag = values.tags.some((existing) => existing === tag); + const enterEvent = e.key === "Enter"; if (!eqaulTag) { - if (e.key === "Enter" && tag.trim() !== "") { + if (enterEvent && tag.trim() !== "") { setValues((prevState) => ({ ...prevState, tags: [...prevState.tags, tag], })); setTag(""); } - } else { + } else if (enterEvent) { e.preventDefault(); alert("이미 존재하는 태그입니다!"); } diff --git a/src/components/pages/AddItemPage/AddItemPage.styles.jsx b/src/components/pages/AddItemPage/AddItemPage.styles.jsx index 092eef99..7d372d8f 100644 --- a/src/components/pages/AddItemPage/AddItemPage.styles.jsx +++ b/src/components/pages/AddItemPage/AddItemPage.styles.jsx @@ -71,6 +71,7 @@ export const AddImg = styled.div` export const TagList = styled.ul` width: 100%; + flex-wrap: wrap; display: flex; align-items: flex-start; gap: 12px; From 81bb4ca1f1e30dc1f6f5e60fbebb1707e8199e66 Mon Sep 17 00:00:00 2001 From: heejin Date: Tue, 21 Jan 2025 20:30:48 +0900 Subject: [PATCH 12/12] Refactor - image onError - itemsPage : separation of responsibility --- src/components/Items/AllItems/AllItems.jsx | 109 +++++++++++++----- .../Items/AllItems/AllItems.styles.jsx | 6 + src/components/Items/BestItems/BestItems.jsx | 35 +++++- src/components/Items/ItemCard/ItemCard.jsx | 10 +- src/components/Paging/Paging.styles.jsx | 1 - .../common/Dropdown/Dropdown.styles.jsx | 1 - src/components/pages/ItemPage/ItemPage.jsx | 82 +------------ 7 files changed, 129 insertions(+), 115 deletions(-) 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/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/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 && ( - - )} + + ); }