diff --git a/package-lock.json b/package-lock.json
index 4fc1e10d..b9b3f056 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "vite-project",
"version": "0.0.0",
"dependencies": {
+ "clsx": "^2.1.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1"
@@ -1567,6 +1568,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
diff --git a/package.json b/package.json
index 22318774..510acef8 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "clsx": "^2.1.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1"
diff --git a/public/common.css b/public/common.css
index 1e3f736f..98ced021 100644
--- a/public/common.css
+++ b/public/common.css
@@ -2,18 +2,26 @@
cursor: pointer;
border: none;
white-space: nowrap;
+ text-decoration: none;
+ background: none;
+ text-align: center;
+}
+
+.primary-btn {
background-color: var(--color-primary);
+ color: var(--color-white);
}
-.btn:hover {
+.primary-btn:not(:disabled):hover {
background-color: var(--color-primary-dark);
}
-.btn:active {
+.primary-btn:active {
background-color: var(--color-primary-darker);
}
-.btn .disabled {
+.disabled {
+ cursor: default;
background-color: var(--color-disabled);
}
diff --git a/src/App.jsx b/src/App.jsx
index d231cf3f..c9758dec 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,7 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
-import Nav from "./components/Nav";
-import Items from "./pages/Items";
-import AddItem from "./pages/AddItem";
+import Nav from "./layouts/Nav";
+import ItemsPage from "./pages/ItemsPage/ItemsPage";
+import AddItemPage from "./pages/AddItemPage/AddItemPage";
const App = () => {
return (
@@ -9,8 +9,8 @@ const App = () => {
} />
- } />
- } />
+ } />
+ } />
);
diff --git a/src/api.jsx b/src/api.jsx
index 0d0aa841..dde3600b 100644
--- a/src/api.jsx
+++ b/src/api.jsx
@@ -6,13 +6,22 @@ export const getProducts = async ({
pageSize = 4,
keyword = "",
}) => {
- const query = `orderBy=${orderBy}&page=${currentPage}&pageSize=${pageSize}${
- keyword ? `&keyword=${keyword}` : ""
- }`;
- const response = await fetch(`${BASE_URL}products?${query}`);
+ const params = new URLSearchParams({
+ orderBy,
+ page: currentPage,
+ pageSize,
+ });
+
+ if (keyword) {
+ params.append("keyword", keyword);
+ }
+
+ const response = await fetch(`${BASE_URL}products?${params.toString()}`);
+
if (!response.ok) {
throw new Error("항목을 불러오는데 실패했습니다.");
}
+
const body = await response.json();
return body;
};
diff --git a/src/assets/fonts/ROKAF-Sans-Bold.ttf b/src/assets/fonts/ROKAF-Sans-Bold.ttf
new file mode 100644
index 00000000..8889aba4
Binary files /dev/null and b/src/assets/fonts/ROKAF-Sans-Bold.ttf differ
diff --git a/src/assets/icons/ic_X.svg b/src/assets/icons/ic_X.svg
new file mode 100644
index 00000000..1af75bea
--- /dev/null
+++ b/src/assets/icons/ic_X.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/components/ArrowButton.jsx b/src/common/ArrowButton.jsx
similarity index 100%
rename from src/components/ArrowButton.jsx
rename to src/common/ArrowButton.jsx
diff --git a/src/components/PageButton.jsx b/src/common/PageButton.jsx
similarity index 100%
rename from src/components/PageButton.jsx
rename to src/common/PageButton.jsx
diff --git a/src/components/PageButton.module.css b/src/common/PageButton.module.css
similarity index 100%
rename from src/components/PageButton.module.css
rename to src/common/PageButton.module.css
diff --git a/src/common/XButton.jsx b/src/common/XButton.jsx
new file mode 100644
index 00000000..37400dd3
--- /dev/null
+++ b/src/common/XButton.jsx
@@ -0,0 +1,17 @@
+import XIcon from "../assets/icons/ic_X.svg";
+
+const XButton = ({ onClick, value = null, className = "btn" }) => {
+ return (
+
+ );
+};
+
+export default XButton;
diff --git a/src/components/hooks/useBreakpoint.jsx b/src/hooks/useBreakpoint.js
similarity index 100%
rename from src/components/hooks/useBreakpoint.jsx
rename to src/hooks/useBreakpoint.js
diff --git a/src/hooks/useField.js b/src/hooks/useField.js
new file mode 100644
index 00000000..867812ef
--- /dev/null
+++ b/src/hooks/useField.js
@@ -0,0 +1,65 @@
+import { useEffect, useState } from "react";
+import formatNumber from "../utils/formatNumber";
+
+const KEY_MAP = {
+ productName: "name",
+ productInfo: "description",
+ productPrice: "price",
+};
+
+const useField = (id, onSaveData) => {
+ const [value, setValue] = useState("");
+ const [displayedValue, setDisplayedValue] = useState("");
+ const [isFocused, SetIsFocused] = useState(false);
+
+ const handleChangeValue = (e) => {
+ setValue((prev) => e.target.value);
+ };
+
+ const handleChangePrice = (e) => {
+ let input = e.target.value;
+ input = input.replace(/[^0-9]/g, "");
+
+ if (input.length > 1 && input.startsWith("0")) {
+ input = input.replace(/^0+/, "");
+ }
+
+ setValue((prev) => (input === "" ? "" : Number(input)));
+ };
+
+ const handleFocus = () => {
+ SetIsFocused((prev) => true);
+ };
+
+ const handleBlurDisplayedValue = () => {
+ setDisplayedValue((prev) => formatNumber(value));
+ SetIsFocused((prev) => false);
+ };
+
+ useEffect(() => {
+ onSaveData((prev) => {
+ return {
+ ...prev,
+ [KEY_MAP[`${id}`]]: value,
+ };
+ });
+ }, [value]);
+
+ switch (id) {
+ case "productPrice": {
+ return {
+ value,
+ displayedValue,
+ isFocused,
+ handleChangePrice,
+ handleFocus,
+ handleBlurDisplayedValue,
+ };
+ }
+ default: {
+ return { value, handleChangeValue };
+ }
+ }
+};
+
+export default useField;
diff --git a/src/components/PaginationNav.jsx b/src/hooks/usePagination.js
similarity index 51%
rename from src/components/PaginationNav.jsx
rename to src/hooks/usePagination.js
index f2f18c83..b7841914 100644
--- a/src/components/PaginationNav.jsx
+++ b/src/hooks/usePagination.js
@@ -1,27 +1,14 @@
import { useCallback, useEffect, useState } from "react";
-import ArrowButton from "./ArrowButton";
-import PageButton from "./PageButton";
-import styles from "./PaginationNav.module.css";
-const PaginationNav = ({
- signalSearch,
- onClickPage,
+const usePagination = (
+ BUTTONS_PER_PAGE,
totalPage,
- currentPage,
breakpoint,
-}) => {
+ signalSearch,
+ onClickPage
+) => {
const [pageList, setPageList] = useState([[1]]);
const [pageListIndex, setPageListIndex] = useState(0);
- const PrevArrowButton = {
- shape: "<",
- direction: -1,
- endIndex: 0,
- };
- const NextArrowButton = {
- shape: ">",
- direction: 1,
- endIndex: pageList.length - 1,
- };
const resetPagination = useCallback(() => {
setPageListIndex((prev) => 0);
@@ -33,7 +20,7 @@ const PaginationNav = ({
onClickPage((prev) => pageList[pageListIndex + direction][0]);
};
- const calcPageList = (totalPage) => {
+ const calcPageList = useCallback((totalPage) => {
if (!totalPage) {
setPageList((prev) => [[1]]);
return;
@@ -43,7 +30,7 @@ const PaginationNav = ({
let count = 0;
for (let i = 1; i <= totalPage; i++) {
- if (count === 5) {
+ if (count === BUTTONS_PER_PAGE) {
wholePageList.push([...dividedPageList]);
dividedPageList.splice(0);
count = 0;
@@ -55,40 +42,17 @@ const PaginationNav = ({
}
}
setPageList((prev) => [...wholePageList]);
- };
+ }, []);
useEffect(() => {
calcPageList(totalPage);
- }, [totalPage]);
+ }, [totalPage, calcPageList]);
useEffect(() => {
resetPagination();
}, [signalSearch, breakpoint, resetPagination]);
- return (
-
-
- {pageList[pageListIndex].map((pageNumber) => {
- return (
-
- );
- })}
-
-
- );
+ return { pageList, pageListIndex, onMovePageList };
};
-export default PaginationNav;
+export default usePagination;
diff --git a/src/index.css b/src/index.css
index 59ca5436..f9609798 100644
--- a/src/index.css
+++ b/src/index.css
@@ -6,7 +6,7 @@
font-style: normal;
font-family: "ROKAF SANS";
- src: url("assets/fonts/ROKAF\ Sans\ Bold.ttf") format("truetype");
+ src: url("./assets/fonts/ROKAF-Sans-Bold.ttf") format("truetype");
}
:root {
@@ -45,6 +45,10 @@
/* Fonts */
--font-weight-bold: 700;
--font-weight-regular: 400;
+
+ /* Z-index */
+ --z-index-base: 0;
+ --z-index-navigation: 500;
}
* {
@@ -56,3 +60,8 @@ body {
font-family: var(--font-primary);
word-break: keep-all;
}
+
+button {
+ padding-inline: 0;
+ padding-block: 0;
+}
diff --git a/src/components/Nav.jsx b/src/layouts/Nav.jsx
similarity index 64%
rename from src/components/Nav.jsx
rename to src/layouts/Nav.jsx
index c52e16db..51e8140c 100644
--- a/src/components/Nav.jsx
+++ b/src/layouts/Nav.jsx
@@ -4,6 +4,8 @@ import { useLocation } from "react-router-dom";
const Nav = () => {
const location = useLocation();
+ const isMarketPage =
+ location.pathname === "/items" || location.pathname === "/additem";
return (
diff --git a/src/components/Nav.module.css b/src/layouts/Nav.module.css
similarity index 95%
rename from src/components/Nav.module.css
rename to src/layouts/Nav.module.css
index bfdeddc6..439b9634 100644
--- a/src/components/Nav.module.css
+++ b/src/layouts/Nav.module.css
@@ -10,6 +10,7 @@
margin: 0 auto;
border-bottom: 1px solid var(--color-border-grey);
gap: var(--space-sm);
+ z-index: var(--z-index-navigation);
}
.link-container {
@@ -56,10 +57,7 @@
display: inline-block;
width: 128px;
height: 48px;
- text-decoration: none;
- color: var(--color-white);
padding: 12px 20px;
- text-align: center;
border-radius: 8px;
font-weight: 500;
font-size: 16px;
diff --git a/src/pages/AddItem.jsx b/src/pages/AddItem.jsx
deleted file mode 100644
index daa2c862..00000000
--- a/src/pages/AddItem.jsx
+++ /dev/null
@@ -1,5 +0,0 @@
-const AddItem = () => {
- return <>>;
-};
-
-export default AddItem;
diff --git a/src/pages/AddItemPage/AddItemPage.jsx b/src/pages/AddItemPage/AddItemPage.jsx
new file mode 100644
index 00000000..35e8b456
--- /dev/null
+++ b/src/pages/AddItemPage/AddItemPage.jsx
@@ -0,0 +1,115 @@
+import { useEffect, useState } from "react";
+import FileField from "./components/FileField";
+import Field from "./components/Field";
+import TagField from "./components/TagField";
+import isEmpty from "../../utils/isEmpty";
+import styles from "./AddItemPage.module.css";
+import clsx from "clsx";
+
+const DATA_FORMAT = {
+ images: [],
+ tags: [],
+ price: 0,
+ description: "",
+ name: "",
+};
+
+const FIELD_MAP = {
+ productImg: {
+ label: "상품 이미지",
+ id: "productImg",
+ type: "file",
+ },
+ productName: {
+ label: "상품명",
+ id: "productName",
+ type: "text",
+ placeholder: "상품명을 입력해주세요",
+ },
+ productInfo: {
+ label: "상품 소개",
+ id: "productInfo",
+ type: "textarea",
+ placeholder: "상품 소개를 입력해주세요",
+ },
+ productPrice: {
+ label: "판매 가격",
+ id: "productPrice",
+ type: "text",
+ placeholder: "판매 가격을 입력해주세요",
+ },
+ productTag: {
+ label: "태그",
+ id: "productTag",
+ type: "text",
+ placeholder: "태그를 입력해주세요",
+ },
+};
+
+const AddItemPage = () => {
+ const [formData, setFormData] = useState(DATA_FORMAT);
+ const [disabled, setDisabled] = useState(true);
+
+ useEffect(() => {
+ for (const key in formData) {
+ if (isEmpty(formData[key])) {
+ setDisabled((prev) => true);
+ return;
+ }
+ setDisabled((prev) => false);
+ }
+ }, [formData]);
+
+ return (
+
+ );
+};
+
+export default AddItemPage;
diff --git a/src/pages/AddItemPage/AddItemPage.module.css b/src/pages/AddItemPage/AddItemPage.module.css
new file mode 100644
index 00000000..af4ae54f
--- /dev/null
+++ b/src/pages/AddItemPage/AddItemPage.module.css
@@ -0,0 +1,75 @@
+.formContainer {
+ min-width: 375px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ padding: var(--space-lg) var(--space-md) 70px var(--space-md);
+}
+
+.submitContainer {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.formContainer h2 {
+ font-size: 20px;
+ font-weight: var(--font-weight-bold);
+}
+
+.formContainer label {
+ font-size: 18px;
+ font-weight: var(--font-weight-bold);
+}
+
+.submitButton {
+ width: 74px;
+ height: 42px;
+ border-radius: 8px;
+ padding: 12px 23px;
+ font-weight: 500;
+ font-size: 16px;
+ line-height: 1;
+}
+
+.formContainer input {
+ width: 100%;
+ height: 56px;
+ border: none;
+ border-radius: 12px;
+ padding: var(--space-md) var(--space-lg);
+ margin-top: var(--space-md);
+ background-color: var(--color-input);
+}
+
+.formContainer input::placeholder {
+ font-size: 16px;
+ color: var(--color-text-grey400);
+}
+
+.formContainer textarea {
+ width: 100%;
+ height: 282px;
+ border: none;
+ border-radius: 12px;
+ padding: var(--space-md) var(--space-lg);
+ margin-top: var(--space-md);
+ background-color: var(--color-input);
+ resize: none;
+ font-family: var(--font-primary);
+}
+
+.formContainer textarea::placeholder {
+ font-family: var(--font-primary);
+ font-size: 16px;
+ color: var(--color-text-grey400);
+}
+
+/* desktop size */
+@media screen and (min-width: 1200px) {
+ .formContainer {
+ width: 1200px;
+ margin: 0 auto;
+ }
+}
diff --git a/src/pages/AddItemPage/components/Field.jsx b/src/pages/AddItemPage/components/Field.jsx
new file mode 100644
index 00000000..9559dadf
--- /dev/null
+++ b/src/pages/AddItemPage/components/Field.jsx
@@ -0,0 +1,41 @@
+import useField from "../../../hooks/useField";
+
+const Field = ({ label, id, type, placeholder, onSaveData }) => {
+ const {
+ value,
+ displayedValue,
+ isFocused,
+ handleChangeValue,
+ handleChangePrice,
+ handleFocus,
+ handleBlurDisplayedValue,
+ } = useField(id, onSaveData);
+
+ return (
+
+
+ {type === "textarea" ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default Field;
diff --git a/src/pages/AddItemPage/components/FileField.jsx b/src/pages/AddItemPage/components/FileField.jsx
new file mode 100644
index 00000000..d3525430
--- /dev/null
+++ b/src/pages/AddItemPage/components/FileField.jsx
@@ -0,0 +1,83 @@
+import { useEffect, useState } from "react";
+import XButton from "../../../common/XButton";
+import styles from "./FileField.module.css";
+import clsx from "clsx";
+
+const ERROR_MESSAGE = "*이미지 등록은 최대 1개까지 가능합니다.";
+
+const FileField = ({ label, id, type, onSaveData }) => {
+ const [selectedFiled, setSelectedFile] = useState(null);
+ const [preview, setPreview] = useState(null);
+ const [hasError, setHasError] = useState(false);
+
+ const handleChangeImg = (e) => {
+ const file = e.target.files[0];
+ e.target.value = null;
+ const preview = URL.createObjectURL(file);
+ setSelectedFile((prev) => file);
+ setPreview((prev) => preview);
+ };
+
+ const handleDeleteImg = () => {
+ setSelectedFile((prev) => null);
+ setPreview((prev) => {
+ URL.revokeObjectURL(prev);
+ return null;
+ });
+ setHasError((prev) => false);
+ };
+
+ const handleErrorMessage = (e) => {
+ if (selectedFiled) {
+ e.preventDefault();
+ setHasError((prev) => true);
+ }
+ return;
+ };
+
+ useEffect(() => {
+ onSaveData((prev) => {
+ return {
+ ...prev,
+ images: [selectedFiled],
+ };
+ });
+ }, [selectedFiled]);
+
+ return (
+
+
{label}
+
+
+
+ {selectedFiled ? (
+
+

+
+
+ ) : (
+
+ )}
+
+ {hasError ? (
+
{ERROR_MESSAGE}
+ ) : undefined}
+
+ );
+};
+
+export default FileField;
diff --git a/src/pages/AddItemPage/components/FileField.module.css b/src/pages/AddItemPage/components/FileField.module.css
new file mode 100644
index 00000000..38314d2a
--- /dev/null
+++ b/src/pages/AddItemPage/components/FileField.module.css
@@ -0,0 +1,73 @@
+.labelReplacer {
+ font-size: 18px;
+ font-weight: var(--font-weight-bold);
+}
+
+.imgContainer {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: var(--space-md) 0;
+}
+
+.uploadBtn {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ flex: 1 1 50%;
+ aspect-ratio: 1/1;
+ border-radius: 12px;
+ padding: 32px 32px;
+ background-color: var(--color-input);
+ color: var(--color-text-grey400);
+ gap: 10px;
+}
+
+.plusIcon {
+ line-height: 1;
+ font-size: 60px;
+}
+
+.uploadText {
+ font-size: 16px;
+ font-weight: var(--font-weight-regular);
+}
+
+.imgContainer input {
+ display: none;
+}
+
+.img {
+ width: 100%;
+ height: auto;
+ object-fit: cover;
+ border-radius: 12px;
+ aspect-ratio: 1/1;
+}
+
+.deleteBtn {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+}
+
+.imgSpace {
+ position: relative;
+ flex: 1 1 50%;
+ aspect-ratio: 1/1;
+}
+
+.errorMessage {
+ color: var(--color-error-red);
+}
+
+/* tablet size */
+@media screen and (min-width: 768px) {
+ .uploadBtn {
+ flex: 0 0 25%;
+ }
+
+ .imgSpace {
+ flex: 0 0 25%;
+ }
+}
diff --git a/src/pages/AddItemPage/components/TagField.jsx b/src/pages/AddItemPage/components/TagField.jsx
new file mode 100644
index 00000000..b4a15608
--- /dev/null
+++ b/src/pages/AddItemPage/components/TagField.jsx
@@ -0,0 +1,68 @@
+import { useEffect, useRef, useState } from "react";
+import styles from "./TagField.module.css";
+import XButton from "../../../common/XButton";
+
+const TagField = ({ label, id, type, placeholder, onSaveData }) => {
+ const [value, setValue] = useState("");
+ const [tags, setTags] = useState([]);
+ const listKey = useRef(0);
+
+ const handleChangeValue = (e) => {
+ setValue((prev) => e.target.value);
+ };
+
+ const handleAddTag = (e) => {
+ if (e.key !== "Enter") {
+ return;
+ }
+ if (!e.target.value.trim()) {
+ return;
+ }
+ if (tags.includes(`#${e.target.value}`)) {
+ alert("중복태그는 불가능합니다!");
+ return;
+ }
+ setTags((prev) => [...prev, `#${value}`]);
+ setValue((prev) => "");
+ };
+
+ const handleDeleteTag = (e) => {
+ const tagContent = e.currentTarget.value;
+ setTags((prev) => prev.filter((tag) => tag !== tagContent));
+ };
+
+ useEffect(() => {
+ onSaveData((prev) => {
+ return {
+ ...prev,
+ tags: [...tags],
+ };
+ });
+ }, [tags]);
+
+ return (
+
+
+
+ {tags
+ ? tags?.map((tag) => {
+ return (
+
+ {tag}
+
+
+ );
+ })
+ : undefined}
+
+ );
+};
+
+export default TagField;
diff --git a/src/pages/AddItemPage/components/TagField.module.css b/src/pages/AddItemPage/components/TagField.module.css
new file mode 100644
index 00000000..27b5419b
--- /dev/null
+++ b/src/pages/AddItemPage/components/TagField.module.css
@@ -0,0 +1,13 @@
+.tag {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ height: auto;
+ border: none;
+ border-radius: 26px;
+ padding: 6px 12px;
+ margin: 14px 12px 0 0;
+ background-color: var(--color-input);
+ gap: var(--space-sm);
+ word-break: break-all;
+}
diff --git a/src/pages/Items.jsx b/src/pages/ItemsPage/ItemsPage.jsx
similarity index 79%
rename from src/pages/Items.jsx
rename to src/pages/ItemsPage/ItemsPage.jsx
index 6b5dedb0..1c547b51 100644
--- a/src/pages/Items.jsx
+++ b/src/pages/ItemsPage/ItemsPage.jsx
@@ -1,12 +1,11 @@
-import Nav from "../components/Nav";
-import BestProductList from "../components/BestProductList";
-import AllProductList from "../components/AllProductList";
-import useBreakpoint from "../components/hooks/useBreakpoint";
-import { getProducts } from "../api";
+import BestProductList from "./components/BestProductList";
+import AllProductList from "./components/AllProductList";
+import useBreakpoint from "../../hooks/useBreakpoint";
+import { getProducts } from "../../api";
import { useState, useEffect, useCallback, useRef } from "react";
-import styles from "./Items.module.css";
+import styles from "./ItemsPage.module.css";
-function Items() {
+const ItemsPage = () => {
const [products, setProducts] = useState([]);
const [bestProducts, setBestProducts] = useState([]);
const [totalCount, setTotalCount] = useState(0);
@@ -16,6 +15,9 @@ function Items() {
const { breakpoint, pageSize, bestProductsPageSize } = useBreakpoint();
const keyword = useRef("");
+ const calcTotalPage = (totalCount, pageSize) =>
+ Math.ceil(totalCount / pageSize);
+
const onSearch = () => {
handleLoad({ orderBy, pageSize, keyword: keyword.current });
};
@@ -52,8 +54,7 @@ function Items() {
);
-}
+};
-export default Items;
+export default ItemsPage;
diff --git a/src/pages/Items.module.css b/src/pages/ItemsPage/ItemsPage.module.css
similarity index 100%
rename from src/pages/Items.module.css
rename to src/pages/ItemsPage/ItemsPage.module.css
diff --git a/src/components/AllProductList.jsx b/src/pages/ItemsPage/components/AllProductList.jsx
similarity index 88%
rename from src/components/AllProductList.jsx
rename to src/pages/ItemsPage/components/AllProductList.jsx
index 2b5f09db..8f11763b 100644
--- a/src/components/AllProductList.jsx
+++ b/src/pages/ItemsPage/components/AllProductList.jsx
@@ -4,13 +4,9 @@ import PaginationNav from "./PaginationNav";
import styles from "./AllProductList.module.css";
import { useState } from "react";
-const calcTotalPage = (totalCount, pageSize) =>
- Math.ceil(totalCount / pageSize);
-
const AllProductList = ({
products,
- totalCount,
- pageSize,
+ totalPage,
currentPage,
breakpoint,
inputValue,
@@ -45,7 +41,7 @@ const AllProductList = ({
diff --git a/src/components/AllProductList.module.css b/src/pages/ItemsPage/components/AllProductList.module.css
similarity index 100%
rename from src/components/AllProductList.module.css
rename to src/pages/ItemsPage/components/AllProductList.module.css
diff --git a/src/components/BestProductList.jsx b/src/pages/ItemsPage/components/BestProductList.jsx
similarity index 100%
rename from src/components/BestProductList.jsx
rename to src/pages/ItemsPage/components/BestProductList.jsx
diff --git a/src/components/BestProductList.module.css b/src/pages/ItemsPage/components/BestProductList.module.css
similarity index 100%
rename from src/components/BestProductList.module.css
rename to src/pages/ItemsPage/components/BestProductList.module.css
diff --git a/src/pages/ItemsPage/components/PaginationNav.jsx b/src/pages/ItemsPage/components/PaginationNav.jsx
new file mode 100644
index 00000000..56cda123
--- /dev/null
+++ b/src/pages/ItemsPage/components/PaginationNav.jsx
@@ -0,0 +1,59 @@
+import ArrowButton from "../../../common/ArrowButton";
+import PageButton from "../../../common/PageButton";
+import styles from "./PaginationNav.module.css";
+import usePagination from "../../../hooks/usePagination";
+
+const BUTTONS_PER_PAGE = 5;
+
+const PaginationNav = ({
+ signalSearch,
+ onClickPage,
+ totalPage,
+ currentPage,
+ breakpoint,
+}) => {
+ const { pageList, pageListIndex, onMovePageList } = usePagination(
+ BUTTONS_PER_PAGE,
+ totalPage,
+ breakpoint,
+ signalSearch,
+ onClickPage
+ );
+ const PrevArrowButton = {
+ shape: "<",
+ direction: -1,
+ endIndex: 0,
+ };
+ const NextArrowButton = {
+ shape: ">",
+ direction: 1,
+ endIndex: pageList.length - 1,
+ };
+
+ return (
+
+
+ {pageList[pageListIndex].map((pageNumber) => {
+ return (
+
+ );
+ })}
+
+
+ );
+};
+
+export default PaginationNav;
diff --git a/src/components/PaginationNav.module.css b/src/pages/ItemsPage/components/PaginationNav.module.css
similarity index 100%
rename from src/components/PaginationNav.module.css
rename to src/pages/ItemsPage/components/PaginationNav.module.css
diff --git a/src/components/ProductCard.jsx b/src/pages/ItemsPage/components/ProductCard.jsx
similarity index 100%
rename from src/components/ProductCard.jsx
rename to src/pages/ItemsPage/components/ProductCard.jsx
diff --git a/src/components/ProductCard.module.css b/src/pages/ItemsPage/components/ProductCard.module.css
similarity index 100%
rename from src/components/ProductCard.module.css
rename to src/pages/ItemsPage/components/ProductCard.module.css
diff --git a/src/components/SearchBar.jsx b/src/pages/ItemsPage/components/SearchBar.jsx
similarity index 91%
rename from src/components/SearchBar.jsx
rename to src/pages/ItemsPage/components/SearchBar.jsx
index 4c247cf0..a4d7a8da 100644
--- a/src/components/SearchBar.jsx
+++ b/src/pages/ItemsPage/components/SearchBar.jsx
@@ -1,6 +1,6 @@
import { Link } from "react-router-dom";
import styles from "./SearchBar.module.css";
-import searchIcon from "../assets/icons/ic_search.svg";
+import searchIcon from "../../../assets/icons/ic_search.svg";
const SearchBar = ({
inputValue,
@@ -30,7 +30,7 @@ const SearchBar = ({
전체 상품
-
+
상품 등록하기
@@ -41,7 +41,7 @@ const SearchBar = ({
placeholder="검색할 상품을 입력해주세요"
onChange={handleChangeInputValue}
onKeyDown={handleSearch}
- >
+ />
{
+ const num = Number(value);
+ if (isNaN(num)) {
+ return "";
+ }
+ return num.toLocaleString("ko-kr");
+};
+
+export default formatNumber;
diff --git a/src/utils/isEmpty.js b/src/utils/isEmpty.js
new file mode 100644
index 00000000..006fc745
--- /dev/null
+++ b/src/utils/isEmpty.js
@@ -0,0 +1,14 @@
+const isEmpty = (value) => {
+ if (
+ value === undefined ||
+ value === null ||
+ value === "" ||
+ value?.length === 0
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+};
+
+export default isEmpty;
diff --git a/vite.config.js b/vite.config.js
index 8b0f57b9..bc29fd49 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,7 +1,8 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
-})
+ assetsInclude: ["**/*.ttf"],
+});