From c5c6efc14e277eabb052dcf415ddfaefb7a619ff Mon Sep 17 00:00:00 2001
From: 3rdflr <3rdflrhtl@gmail.com>
Date: Fri, 23 May 2025 15:51:52 +0900
Subject: [PATCH 1/5] Refactoring ItemsPage
---
package-lock.json | 63 +++++++-
package.json | 4 +-
src/App.jsx | 108 +-------------
src/Pages/Items/Items.jsx | 138 ++++++++++++++++++
src/Pages/Items/Items.module.css | 11 ++
src/components/{ => Button}/Button.jsx | 0
src/components/{ => Button}/Button.module.css | 0
src/components/Content/Content.jsx | 9 ++
.../Content/Content.module.css} | 11 --
src/components/{ => DropDown}/DropDown.jsx | 13 +-
.../{ => DropDown}/DropDown.module.css | 0
src/components/DropDown/DropDownItems.jsx | 24 +++
.../{ => DropDown}/DropDownItems.module.css | 0
src/components/DropDownItems.jsx | 27 ----
src/components/{ => Header}/Header.jsx | 0
src/components/{ => Header}/Header.module.css | 0
src/components/{ => Nav}/Nav.jsx | 8 +-
src/components/{ => Nav}/Nav.module.css | 0
.../{ => Pagination}/Pagination.css | 0
.../{ => Pagination}/Pagination.jsx | 0
.../{ => Pagination}/Pagination.module.css | 0
.../{ => ProductLIst}/ProductItem.jsx | 20 ++-
.../{ => ProductLIst}/ProductItem.module.css | 8 +-
.../{ => ProductLIst}/ProductList.jsx | 17 ++-
.../{ => ProductLIst}/ProductList.module.css | 12 +-
.../{ => SearchItem}/SearchItem.jsx | 2 +-
.../{ => SearchItem}/SearchItem.module.css | 0
src/main.jsx | 4 +-
src/{css => }/reset.css | 0
src/{css => }/variables.css | 0
30 files changed, 302 insertions(+), 177 deletions(-)
create mode 100644 src/Pages/Items/Items.jsx
create mode 100644 src/Pages/Items/Items.module.css
rename src/components/{ => Button}/Button.jsx (100%)
rename src/components/{ => Button}/Button.module.css (100%)
create mode 100644 src/components/Content/Content.jsx
rename src/{css/App.module.css => components/Content/Content.module.css} (64%)
rename src/components/{ => DropDown}/DropDown.jsx (69%)
rename src/components/{ => DropDown}/DropDown.module.css (100%)
create mode 100644 src/components/DropDown/DropDownItems.jsx
rename src/components/{ => DropDown}/DropDownItems.module.css (100%)
delete mode 100644 src/components/DropDownItems.jsx
rename src/components/{ => Header}/Header.jsx (100%)
rename src/components/{ => Header}/Header.module.css (100%)
rename src/components/{ => Nav}/Nav.jsx (73%)
rename src/components/{ => Nav}/Nav.module.css (100%)
rename src/components/{ => Pagination}/Pagination.css (100%)
rename src/components/{ => Pagination}/Pagination.jsx (100%)
rename src/components/{ => Pagination}/Pagination.module.css (100%)
rename src/components/{ => ProductLIst}/ProductItem.jsx (59%)
rename src/components/{ => ProductLIst}/ProductItem.module.css (93%)
rename src/components/{ => ProductLIst}/ProductList.jsx (56%)
rename src/components/{ => ProductLIst}/ProductList.module.css (92%)
rename src/components/{ => SearchItem}/SearchItem.jsx (88%)
rename src/components/{ => SearchItem}/SearchItem.module.css (100%)
rename src/{css => }/reset.css (100%)
rename src/{css => }/variables.css (100%)
diff --git a/package-lock.json b/package-lock.json
index 6cffc66d..0b3681f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,11 @@
"name": "sprintmisson5",
"version": "0.0.0",
"dependencies": {
+ "lodash": "^4.17.21",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-icons": "^5.5.0"
+ "react-icons": "^5.5.0",
+ "react-router-dom": "^7.6.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
@@ -2719,6 +2721,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3179,6 +3187,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
+ "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
+ "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-router/node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3329,6 +3384,12 @@
"node": ">= 18"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
diff --git a/package.json b/package.json
index 0fda482a..a4ec8f44 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
+ "lodash": "^4.17.21",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-icons": "^5.5.0"
+ "react-icons": "^5.5.0",
+ "react-router-dom": "^7.6.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
diff --git a/src/App.jsx b/src/App.jsx
index 02c611c9..fcb19062 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,106 +1,14 @@
-import Nav from "./components/Nav";
-
-import styles from "./css/App.module.css";
-import Header from "./components/Header";
-import DropDown from "./components/DropDown";
-import ProductList from "./components/ProductList";
-import Button from "./components/Button";
-import SearchItem from "./components/SearchItem";
-import Pagination from "./components/Pagination";
-
-import { getProducts } from "./api/ProductApi";
-import { useScreenSize } from "./utils/useScreenSize";
-import { useState, useEffect } from "react";
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import Items from './Pages/Items/Items.jsx';
function App() {
- const [search, setSearch] = useState("");
- const [order, setOrder] = useState("recent");
- const [currPage, setCurrPage] = useState(1);
- const [totalProducts, setTotalProducts] = useState(0);
- const [bestProduct, setBestProduct] = useState(4);
- const [allProduct, setAllProduct] = useState(10);
- const [isBestProduct, setIsBestProduct] = useState(true);
-
- const screenSize = useScreenSize();
-
- const handleSearch = (e) => {
- setSearch(e.target.value);
- };
-
- const handleOrder = (selectedOrder) => {
- setOrder(selectedOrder);
- };
-
- const handlePage = (newPage) => {
- setCurrPage(newPage);
- };
-
- const getTotalProducts = async () => {
- try {
- const { totalCount } = await getProducts({
- orderBy: order,
- keyword: search,
- });
- setTotalProducts(totalCount);
- } catch (error) {
- console.error("전체 상품 수를 불러오는 중 오류:", error);
- }
- };
-
- useEffect(() => {
- let newPageSize;
- if (screenSize === "lg") {
- setBestProduct(4);
- newPageSize = 10;
- } else if (screenSize === "md") {
- setBestProduct(2);
- newPageSize = 6;
- } else {
- setBestProduct(1);
- newPageSize = 4;
- }
- if (newPageSize !== allProduct) {
- const firstItemOfCurrentPage = (currPage - 1) * allProduct;
- const newPage = Math.floor(firstItemOfCurrentPage / newPageSize) + 1;
- setCurrPage(newPage);
- setAllProduct(newPageSize);
- }
- }, [screenSize, currPage, allProduct]);
-
- useEffect(() => {
- getTotalProducts();
- }, []);
return (
- <>
-
-
- >
+
+
+ } />
+
+
);
}
-export default App;
+export default App
\ No newline at end of file
diff --git a/src/Pages/Items/Items.jsx b/src/Pages/Items/Items.jsx
new file mode 100644
index 00000000..bc84306c
--- /dev/null
+++ b/src/Pages/Items/Items.jsx
@@ -0,0 +1,138 @@
+import Nav from "../../components/Nav/Nav.jsx";
+
+import styles from "./Items.module.css";
+import Content from "../../components/Content/Content.jsx";
+import Header from "../../components/Header/Header.jsx";
+import DropDown from "../../components/DropDown/DropDown.jsx";
+import ProductList from "../../components/ProductLIst/ProductList.jsx";
+import Button from "../../components/Button/Button.jsx";
+import SearchItem from "../../components/SearchItem/SearchItem.jsx";
+import Pagination from "../../components/Pagination/Pagination.jsx";
+
+import { getProducts } from "../../api/ProductApi.jsx";
+import { useScreenSize } from "../../utils/useScreenSize.jsx";
+import { useState, useEffect, useCallback } from "react";
+import { debounce } from "lodash";
+
+const PAGE_SIZES = {
+ lg: { best: 4, all: 10 },
+ md: { best: 2, all: 6 },
+ sm: { best: 1, all: 4 },
+};
+
+const MAX_SEARCH_LENGTH = 100;
+const DEBOUNCE_DELAY = 300;
+
+function Items() {
+ const [search, setSearch] = useState("");
+ const [order, setOrder] = useState("recent");
+ const [currPage, setCurrPage] = useState(1);
+ const [totalProducts, setTotalProducts] = useState(0);
+ const [bestProductCount, setBestProductCount] = useState(0);
+ const [allProductCount, setAllProductCount] = useState(0);
+
+ const screenSize = useScreenSize();
+
+ useEffect(() => {
+ const { best, all } = PAGE_SIZES[screenSize];
+ setBestProductCount(best);
+ setAllProductCount(all);
+ }, [screenSize]);
+
+ useEffect(() => {
+ const updateProducts = async () => {
+ try {
+ const { totalCount } = await getProducts({
+ orderBy: order,
+ keyword: search,
+ });
+ setTotalProducts(totalCount);
+
+ const { all } = PAGE_SIZES[screenSize];
+ if (all !== allProductCount) {
+ const firstItemOfCurrentPage = Math.max(
+ 0,
+ (currPage - 1) * allProductCount
+ );
+ const newPage = Math.max(
+ 1,
+ Math.floor(firstItemOfCurrentPage / all) + 1
+ );
+ const totalPages = Math.ceil(totalCount / all);
+ const validPage = Math.min(newPage, totalPages || 1);
+
+ setCurrPage(validPage);
+ setAllProductCount(all);
+ } else if (currPage > Math.ceil(totalCount / allProductCount)) {
+ setCurrPage(1);
+ }
+ } catch (error) {
+ console.error("상품 정보를 불러오는 중 오류:", error);
+ setTotalProducts(0);
+ }
+ };
+
+ updateProducts();
+ }, [screenSize, order, search, currPage, allProductCount]);
+
+ const debouncedSearch = useCallback(
+ debounce((value) => {
+ setSearch(value);
+ }, DEBOUNCE_DELAY),
+ []
+ );
+
+ const handleSearch = (e) => {
+ const value = e.target.value;
+ if (value.length <= MAX_SEARCH_LENGTH) {
+ debouncedSearch(value);
+ }
+ };
+
+ const handleOrder = (selectedOrder) => {
+ setOrder(selectedOrder);
+ setCurrPage(1);
+ };
+
+ const handlePage = (newPage) => {
+ if (newPage >= 1 && newPage <= Math.ceil(totalProducts / allProductCount)) {
+ setCurrPage(newPage);
+ window.scrollTo({ top: 0, behavior: "smooth" });
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default Items;
diff --git a/src/Pages/Items/Items.module.css b/src/Pages/Items/Items.module.css
new file mode 100644
index 00000000..ec3ac244
--- /dev/null
+++ b/src/Pages/Items/Items.module.css
@@ -0,0 +1,11 @@
+:root {
+ background-color: var(--content-bg);
+}
+
+.headers {
+ margin: 16px 0 10px 0;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+}
\ No newline at end of file
diff --git a/src/components/Button.jsx b/src/components/Button/Button.jsx
similarity index 100%
rename from src/components/Button.jsx
rename to src/components/Button/Button.jsx
diff --git a/src/components/Button.module.css b/src/components/Button/Button.module.css
similarity index 100%
rename from src/components/Button.module.css
rename to src/components/Button/Button.module.css
diff --git a/src/components/Content/Content.jsx b/src/components/Content/Content.jsx
new file mode 100644
index 00000000..60612cd3
--- /dev/null
+++ b/src/components/Content/Content.jsx
@@ -0,0 +1,9 @@
+import styles from './Content.module.css';
+
+function Content({ children }) {
+ return (
+ {children}
+ );
+}
+
+export default Content;
diff --git a/src/css/App.module.css b/src/components/Content/Content.module.css
similarity index 64%
rename from src/css/App.module.css
rename to src/components/Content/Content.module.css
index 57f746e0..cc790ae9 100644
--- a/src/css/App.module.css
+++ b/src/components/Content/Content.module.css
@@ -1,19 +1,8 @@
-:root {
- background-color: var(--content-bg);
-}
.content {
width: 344px;
margin: 20px auto;
}
-.headers {
- margin: 16px 0 10px 0;
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 8px;
-}
-
/* Tablet */
@media screen and (min-width: 768px) and (max-width: 1199px) {
.content {
diff --git a/src/components/DropDown.jsx b/src/components/DropDown/DropDown.jsx
similarity index 69%
rename from src/components/DropDown.jsx
rename to src/components/DropDown/DropDown.jsx
index b3f1e089..787098d0 100644
--- a/src/components/DropDown.jsx
+++ b/src/components/DropDown/DropDown.jsx
@@ -1,23 +1,28 @@
import { FaSortAmountDown } from "react-icons/fa";
import { FaSortDown } from "react-icons/fa6";
-import DropDownItems from "./DropDownItems";
-import { useScreenSize } from "../utils/useScreenSize";
+import DropDownItems from "./DropDownItems.jsx";
+import { useScreenSize } from "../../utils/useScreenSize";
import { useState } from "react";
import styles from "./DropDown.module.css";
+const DEFAULT_VALUE = "최신순";
+
function DropDown({ onChangeOrder }) {
- const [currentValue, setCurrentValue] = useState("최신순");
+ const [currentValue, setCurrentValue] = useState(DEFAULT_VALUE);
const [isOpen, setIsOpen] = useState(false);
const screenSize = useScreenSize();
+ const handleToggle = () => setIsOpen((prev) => !prev);
+
const handleOnChangeValue = (value, label) => {
setCurrentValue(label);
onChangeOrder(value);
+ setIsOpen(false);
};
return (
- setIsOpen((prev) => !prev)}>
+
{screenSize === "sm" ? (