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.css b/src/App.css
new file mode 100644
index 00000000..9dfa9354
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,3 @@
+:root {
+ background-color: var(--content-bg);
+}
\ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index 02c611c9..e80336e4 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,106 +1,18 @@
-import Nav from "./components/Nav";
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import Items from './Pages/Items/Items.jsx';
+import AddItem from './Pages/AddItem/AddItem.jsx';
-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 './App.css'
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/AddItem/AddItem.jsx b/src/Pages/AddItem/AddItem.jsx
new file mode 100644
index 00000000..4e83bff8
--- /dev/null
+++ b/src/Pages/AddItem/AddItem.jsx
@@ -0,0 +1,144 @@
+import DeleteImg from '../../assets/input/delete.svg'
+
+import { useState } from "react";
+
+import Nav from "../../components/Nav/Nav";
+import Content from "../../components/Content/Content";
+import Header from '../../components/Header/Header';
+import Button from '../../components/Button/Button';
+import FormImg from "../../components/Form/FormImg";
+import FormInput from "../../components/Form/FormInput";
+
+import styles from './AddItem.module.css';
+
+const INITIAL_VALUES = {
+ name: '',
+ description: '',
+ price: '',
+ tags:[],
+ tagInput:'',
+ images: null,
+};
+
+
+function AddItem() {
+ const [values, setValues] = useState(INITIAL_VALUES)
+ const [isComposing, setIsComposing] = useState(false);
+
+ const handleChange = (name, value) => {
+ setValues((prevValues) => ({
+ ...prevValues,
+ [name]: value,
+ }));
+ };
+
+
+ const handleTagInputKeyDown = (e) => {
+ if (e.key === 'Enter' && !isComposing) {
+ e.preventDefault();
+
+ const newTag = values.tagInput.trim();
+
+ if (newTag !== '') {
+
+ if (!values.tags.includes(`#${newTag}`)) {
+ setValues((prev) => ({
+ ...prev,
+ tags: [...prev.tags, `#${newTag}`],
+ tagInput:'',
+ }))
+ } else {
+ setValues((prev) => ({
+ ...prev,
+ tagInput:'',
+ }))
+ }
+ }}
+ };
+
+
+ const handleRemoveTag = (tagToRemove) => {
+ setValues((prevValues) => ({
+ ...prevValues,
+ tags: prevValues.tags.filter((tag) => tag !== tagToRemove),
+ }));
+ };
+
+ const handleCompositionStart = () => {
+ setIsComposing(true);
+ };
+
+
+ const handleCompositionEnd = () => {
+ setIsComposing(false);
+ };
+
+ const isSubmitDisabled = !values.name || !values.description || values.price <= 0 || !values.images;
+
+ return(
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default AddItem
\ No newline at end of file
diff --git a/src/Pages/AddItem/AddItem.module.css b/src/Pages/AddItem/AddItem.module.css
new file mode 100644
index 00000000..85fcdf1b
--- /dev/null
+++ b/src/Pages/AddItem/AddItem.module.css
@@ -0,0 +1,33 @@
+.form {
+ margin-bottom: 100px;
+}
+
+.headers {
+ margin: 16px 0 26px 0;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.taglistContainer {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.tagItem {
+ display: flex;
+ align-items: center;
+ text-align: center;
+ background-color: var(--cool-gray-100);
+ padding: 6px 10px 6px 16px;
+ border-radius: 26px;
+}
+
+.removeTagButton {
+ display: flex;
+ border: 0;
+ background-color: inherit;
+}
\ 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..86c2f93c
--- /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..9f15564e
--- /dev/null
+++ b/src/Pages/Items/Items.module.css
@@ -0,0 +1,7 @@
+.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/assets/input/delete.svg b/src/assets/input/delete.svg
new file mode 100644
index 00000000..87a00547
--- /dev/null
+++ b/src/assets/input/delete.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/input/img.svg b/src/assets/input/img.svg
new file mode 100644
index 00000000..e56d43e1
--- /dev/null
+++ b/src/assets/input/img.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/components/Button.jsx b/src/components/Button.jsx
deleted file mode 100644
index 0e981fb2..00000000
--- a/src/components/Button.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import styles from "./Button.module.css";
-
-function Button({ href, buttonText }) {
- return (
-
- {buttonText}
-
- );
-}
-
-export default Button;
diff --git a/src/components/Button/Button.jsx b/src/components/Button/Button.jsx
new file mode 100644
index 00000000..52abb6ee
--- /dev/null
+++ b/src/components/Button/Button.jsx
@@ -0,0 +1,15 @@
+import { Link } from "react-router-dom";
+import styles from "./Button.module.css";
+
+function Button({ href, buttonText, type = 'button', disabled }) {
+
+ const buttonClasses = `${styles.button}`;
+ return (
+
+
+ );
+}
+
+export default Button;
diff --git a/src/components/Button.module.css b/src/components/Button/Button.module.css
similarity index 75%
rename from src/components/Button.module.css
rename to src/components/Button/Button.module.css
index 20bcca07..9493febc 100644
--- a/src/components/Button.module.css
+++ b/src/components/Button/Button.module.css
@@ -5,6 +5,8 @@
display: flex;
align-items: center;
justify-content: center;
+ font-size: var(--font-size-xs);
+ font-weight: 600;
padding: 12px 23px;
border-radius: 8px;
order: 1;
@@ -14,6 +16,10 @@
background-color: var(--primary-300);
}
+.button:disabled {
+ background-color: var(--cool-gray-400);
+}
+
/* Tablet Desktop*/
@media (min-width: 768px) {
.button {
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" ? (