diff --git a/package-lock.json b/package-lock.json
index 80568c27..72d9de22 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,8 @@
"react-dom": "^19.0.0",
"react-responsive": "^10.0.1",
"react-router": "^7.5.3",
- "react-router-dom": "^7.5.3"
+ "react-router-dom": "^7.5.3",
+ "styled-components": "^6.1.18"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
@@ -322,6 +323,27 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+ "license": "MIT"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
@@ -1376,6 +1398,12 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
+ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
+ "license": "MIT"
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
@@ -1520,6 +1548,15 @@
"node": ">=6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001715",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
@@ -1616,17 +1653,36 @@
"node": ">= 8"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-mediaquery": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
"integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
"license": "BSD"
},
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -2292,7 +2348,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2417,7 +2472,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -2462,6 +2516,12 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -2665,6 +2725,12 @@
"integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
"license": "MIT"
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2692,7 +2758,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -2711,6 +2776,68 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/styled-components": {
+ "version": "6.1.18",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.18.tgz",
+ "integrity": "sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.2.2",
+ "@emotion/unitless": "0.8.1",
+ "@types/stylis": "4.2.5",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.3",
+ "postcss": "8.4.49",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.2",
+ "tslib": "2.6.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
+ "node_modules/styled-components/node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2741,6 +2868,12 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "license": "0BSD"
+ },
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
diff --git a/package.json b/package.json
index 4796bcc1..8b7645a6 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,8 @@
"react-dom": "^19.0.0",
"react-responsive": "^10.0.1",
"react-router": "^7.5.3",
- "react-router-dom": "^7.5.3"
+ "react-router-dom": "^7.5.3",
+ "styled-components": "^6.1.18"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
diff --git a/public/images/ic_X.png b/public/images/ic_X.png
new file mode 100644
index 00000000..ce355240
Binary files /dev/null and b/public/images/ic_X.png differ
diff --git a/public/images/ic_plus.png b/public/images/ic_plus.png
new file mode 100644
index 00000000..5cd8fc68
Binary files /dev/null and b/public/images/ic_plus.png differ
diff --git a/public/images/icon_password_invisible.png b/public/images/icon_password_invisible.png
index 69a949ba..57e303f5 100644
Binary files a/public/images/icon_password_invisible.png and b/public/images/icon_password_invisible.png differ
diff --git a/public/images/icon_password_visible.png b/public/images/icon_password_visible.png
index e8a0fba8..5263589a 100644
Binary files a/public/images/icon_password_visible.png and b/public/images/icon_password_visible.png differ
diff --git a/src/App.jsx b/src/App.jsx
index eb06a0fd..6b383f85 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,27 +1,29 @@
-import { BrowserRouter, Routes, Route } from 'react-router-dom';
-import Home from './pages/Home/Home';
-import Login from './pages/Auth/Login';
-import Signup from './pages/Auth/Signup';
-import Items from './pages/Items/Items';
-import Privacy from './pages/Privacy/Privacy';
-import Faq from './pages/Faq/Faq';
-import AddItem from './pages/AddItem/AddItem';
-import { LoginStateProvider } from './contexts/LoginStateContext';
-import Board from './pages/Board/Board';
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { LoginStateProvider } from "./contexts/LoginStateContext";
+import HomePage from "./pages/HomePage/HomePage";
+import LoginPage from "./pages/AuthPage/LoginPage";
+import SignupPage from "./pages/AuthPage/SignupPage";
+import ItemsPage from "./pages/ItemsPage/ItemsPage";
+import AddItemPage from "./pages/AddItemPage/AddItemPage";
+import PrivacyPage from "./pages/PrivacyPage/PrivacyPage";
+import FaqPage from "./pages/FaqPage/FaqPage";
+import BoardPage from "./pages/BoardPage/BoardPage";
function App() {
return (
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+ } />
diff --git a/src/components/BestItemsSection.jsx b/src/components/BestItemsSection.jsx
new file mode 100644
index 00000000..7e4665d1
--- /dev/null
+++ b/src/components/BestItemsSection.jsx
@@ -0,0 +1,44 @@
+import { useEffect, useState } from "react";
+import { getItems } from "../utils/api";
+import ItemsContainer from "./ItemsContainer";
+import styles from "./ItemsSection.module.css";
+
+const LIST_TYPE = "best";
+
+const BestItemsSection = ({ pageSize }) => {
+ const [bestItemList, setBestItemList] = useState([]);
+
+ const loadBestItemList = async (options) => {
+ const result = await getItems(options);
+ if (!result) return;
+ const { list } = result;
+ setBestItemList(list);
+ };
+
+ useEffect(() => {
+ if (!pageSize) return;
+ (async () => {
+ await loadBestItemList({
+ offset: 1,
+ pageSize: pageSize,
+ orderBy: "favorite",
+ keyword: "",
+ });
+ })();
+ }, [pageSize]);
+
+ return (
+
+ );
+};
+
+export default BestItemsSection;
diff --git a/src/components/CurrentItemsSection.jsx b/src/components/CurrentItemsSection.jsx
new file mode 100644
index 00000000..20f5179d
--- /dev/null
+++ b/src/components/CurrentItemsSection.jsx
@@ -0,0 +1,148 @@
+import { useEffect, useState } from "react";
+import { getItems } from "../utils/api";
+import ItemsContainer from "./ItemsContainer";
+import styles from "./ItemsSection.module.css";
+import { usePaginationByOffset } from "../hooks/usePaginationByOffset";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import Pagination from "./Pagination";
+import { useIsLogin } from "../contexts/LoginStateContext";
+
+const LIST_TYPE = "current";
+const VISIBLE_PAGE_LENGTH = 5;
+
+const CurrentItemsSection = ({ pageSize }) => {
+ //prettier-ignore
+ const isLogin = useIsLogin();
+
+ const [searchParams, setSearchParams] = useSearchParams();
+ const initKeyword = searchParams.get("keyword") || "";
+ const [keyword, setKeyword] = useState(initKeyword);
+
+ const [offset, setOffset] = useState(1);
+ const [totalDataCount, setTotalDataCount] = useState(1);
+ const [order, setOrder] = useState("recent");
+
+ const [currentItemList, setCurrentItemList] = useState([]);
+
+ const { totalPagesCount, currentPageNumber, visiblePageNumbers } =
+ usePaginationByOffset(
+ offset,
+ pageSize,
+ totalDataCount,
+ VISIBLE_PAGE_LENGTH
+ );
+
+ const onCreateNewItemNavigate = useNavigate();
+
+ const handleSearchOrderChange = (e) => {
+ setOffset(1);
+ setOrder(e.target.value);
+ };
+
+ const loadCurrentItemList = async (option) => {
+ const result = await getItems(option);
+ if (!result) return;
+ const { list, totalCount } = result;
+ setCurrentItemList(list);
+ setTotalDataCount(totalCount);
+ };
+
+ const handleSearchInputChange = (e) => setKeyword(e.target.value);
+ const handleSearchInputEnterPress = (e) => {
+ if (e.key === "Enter") {
+ setOffset(1);
+ setSearchParams(keyword ? { keyword } : {});
+ }
+ };
+
+ const handleCreateNewItemClick = (e) => {
+ e.preventDefault();
+ onCreateNewItemNavigate("/additem");
+ };
+
+ //prettier-ignore
+ const handlePageNumberClick = (e) => onPaginationButtonClick(Number(e.target.value));
+ const handlePagePrev = () => onPaginationButtonClick(visiblePageNumbers[0] - 1); //prettier-ignore
+ const handlePageNext = () => onPaginationButtonClick(visiblePageNumbers[visiblePageNumbers.length - 1] + 1); //prettier-ignore
+ const onPaginationButtonClick = (nextPageNumber) => setOffset((nextPageNumber - 1) * pageSize + 1); //prettier-ignore
+
+ const prevPageEnable = visiblePageNumbers[0] > 1;
+ const nextPageEnable =
+ visiblePageNumbers[visiblePageNumbers.length - 1] < totalPagesCount;
+
+ const handlers = {
+ handlePageNumberClick,
+ handlePagePrev,
+ handlePageNext,
+ };
+
+ const pageControlEnabled = {
+ prevPageEnable,
+ nextPageEnable,
+ };
+
+ useEffect(() => {
+ if (!pageSize) return;
+ (async () => {
+ await loadCurrentItemList({
+ offset: offset,
+ pageSize: pageSize,
+ orderBy: order,
+ keyword: initKeyword,
+ });
+ })();
+ }, [pageSize, order, offset, initKeyword]);
+
+ return (
+ <>
+
+
+
전체 상품
+
+

+
+
+ {isLogin && (
+
+ )}
+
+
+
+
+
+ >
+ );
+};
+
+export default CurrentItemsSection;
diff --git a/src/components/Field.jsx b/src/components/Field.jsx
new file mode 100644
index 00000000..0cbd61e1
--- /dev/null
+++ b/src/components/Field.jsx
@@ -0,0 +1,69 @@
+import { useState } from "react";
+import styles from "./Field.module.css";
+
+const PASSWORD_ICON_CONFIG = {
+ false: {
+ src: "./images/icon_password_invisible.png",
+ className: "password-icon hidden",
+ },
+ true: {
+ src: "./images/icon_password_visible.png",
+ className: "password-icon",
+ },
+};
+
+const INPUT_CONTAINER_CLASSNAME = {
+ null: "",
+ false: "invalid",
+ true: "valid",
+};
+
+const Field = ({
+ fieldConfig,
+ value,
+ valid,
+ hint,
+ handleInputChange,
+ handleInputBlur,
+}) => {
+ //prettier-ignore
+ const [isVisible, setIsVisible] = useState(false);
+ const handlePasswordIconClick = () => setIsVisible((prev) => !prev);
+
+ const inputContainerClass = INPUT_CONTAINER_CLASSNAME[valid];
+
+ return (
+
+ );
+};
+
+export default Field;
diff --git a/src/components/Field.module.css b/src/components/Field.module.css
new file mode 100644
index 00000000..30745c8c
--- /dev/null
+++ b/src/components/Field.module.css
@@ -0,0 +1,69 @@
+.label {
+ display: flex;
+ flex-direction: column;
+ font-weight: 700;
+ font-size: var(--label-font-size);
+ line-height: 24px;
+ margin-bottom: var(--label-margin-bottom);
+}
+
+.input-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 15px 24px;
+ background-color: var(--color-gray200);
+ border-radius: 12px;
+ margin-top: var(--input-container-margin-top);
+ margin-bottom: 8px;
+}
+
+.input-container.valid {
+ border: 1px solid var(--color-blue400);
+}
+.input-container.invalid {
+ border: 1px solid var(--color-error);
+}
+
+.input {
+ flex-grow: 1;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ background-color: var(--color-gray200);
+ border: none;
+ outline: none;
+ color: var(--color-gray800);
+}
+
+.input::placeholder {
+ color: var(--color-gray400);
+}
+
+.password-icon {
+ aspect-ratio: 1 / 1;
+ width: 24px;
+}
+
+.input-hint {
+ color: var(--color-error);
+ margin-left: 16px;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 24px;
+}
+
+:root {
+ --label-margin-bottom: 16px;
+ --label-font-size: 14px;
+ --input-container-margin-top: 8px;
+}
+
+/* Tablet, PC */
+@media (min-width: 768px) {
+ :root {
+ --label-margin-bottom: 24px;
+ --label-font-size: 18px;
+ --input-container-margin-top: 16px;
+ }
+}
diff --git a/src/components/ItemCard.jsx b/src/components/ItemCard.jsx
new file mode 100644
index 00000000..4242c9b8
--- /dev/null
+++ b/src/components/ItemCard.jsx
@@ -0,0 +1,37 @@
+import { useState } from 'react';
+import { formatPriceKRW } from '../utils/formatPrice';
+import styles from './ItemCard.module.css';
+
+const IMAGE_DEFAULT_URL = './images/img_items_default_md.png';
+
+const ItemCard = ({ id, imageUrl, name, price, favoriteCount }) => {
+ const [isImageValid, setIsImageValid] = useState(true);
+
+ const imgSrc = isImageValid && imageUrl ? imageUrl : IMAGE_DEFAULT_URL;
+
+ return (
+
+

setIsImageValid(false)}
+ alt={name}
+ width={282}
+ />
+
+
{name}
+
{formatPriceKRW(price)}
+
+

+
{favoriteCount}
+
+
+
+ );
+};
+
+export default ItemCard;
diff --git a/src/components/ItemCard.module.css b/src/components/ItemCard.module.css
new file mode 100644
index 00000000..6e48859b
--- /dev/null
+++ b/src/components/ItemCard.module.css
@@ -0,0 +1,50 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+}
+
+.context {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.image {
+ aspect-ratio: 1/1;
+ width: 100%;
+ border-radius: 16px;
+}
+
+.title {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 24px;
+ min-height: 24px;
+}
+.price {
+ font-weight: 700;
+ font-size: 16px;
+ line-height: 26px;
+ min-height: 26px;
+}
+
+.favorite-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 3.35px;
+ min-height: 18px;
+}
+
+.favorite-image {
+ aspect-ratio: 1/1;
+ width: 16px;
+}
+
+.favorite-count {
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 18px;
+}
diff --git a/src/components/ItemCardSkeleton.jsx b/src/components/ItemCardSkeleton.jsx
new file mode 100644
index 00000000..ed249076
--- /dev/null
+++ b/src/components/ItemCardSkeleton.jsx
@@ -0,0 +1,16 @@
+import styles from "./ItemCardSkeleton.module.css";
+
+const ItemCardSkeleton = () => {
+ return (
+
+ );
+};
+
+export default ItemCardSkeleton;
diff --git a/src/components/ItemCardSkeleton.module.css b/src/components/ItemCardSkeleton.module.css
new file mode 100644
index 00000000..286f5cb2
--- /dev/null
+++ b/src/components/ItemCardSkeleton.module.css
@@ -0,0 +1,69 @@
+@keyframes skeleton-gradient {
+ 0% {
+ background-position: 0% 50%;
+ }
+ 100% {
+ background-position: 100% 50%;
+ }
+}
+:root {
+ --skeleton-dark: #e3e3e3;
+ --skeleton-light: #f0f0f0;
+}
+.image,
+.title,
+.price,
+.favorite-container {
+ background: linear-gradient(90deg, var(--skeleton-dark), var(--skeleton-dark), var(--skeleton-dark), var(--skeleton-light), var(--skeleton-dark), var(--skeleton-dark), var(--skeleton-dark) );
+ background-size: 800% 800%;
+ animation: skeleton-gradient 1.5s infinite ease-in-out;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+}
+
+.context {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.image {
+ aspect-ratio: 1/1;
+ width: 100%;
+ border-radius: 16px;
+}
+
+.title {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 24px;
+ margin: 4px 0;
+ min-height: 16px;
+ width: 70%;
+ border-radius: 10px;
+}
+.price {
+ font-weight: 700;
+ font-size: 16px;
+ line-height: 26px;
+ margin: 5px 0;
+ min-height: 16px;
+ width: 50%;
+ border-radius: 10px;
+}
+
+.favorite-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 3.35px;
+ width: 40%;
+ margin: 3px 0;
+ min-height: 12px;
+ border-radius: 10px;
+}
\ No newline at end of file
diff --git a/src/components/ItemsContainer.jsx b/src/components/ItemsContainer.jsx
new file mode 100644
index 00000000..21979297
--- /dev/null
+++ b/src/components/ItemsContainer.jsx
@@ -0,0 +1,28 @@
+import ItemCard from "./ItemCard";
+import ItemCardSkeleton from "./ItemCardSkeleton";
+import styles from "./ItemsContainer.module.css";
+
+const ItemsContainer = ({ listName, itemList, pageSize }) => {
+ return (
+
+ {itemList.length === 0
+ ? Array.from({ length: pageSize }, (_, i) => {
+ return ;
+ })
+ : itemList.map((item) => {
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default ItemsContainer;
diff --git a/src/components/ItemsContainer.module.css b/src/components/ItemsContainer.module.css
new file mode 100644
index 00000000..3119412d
--- /dev/null
+++ b/src/components/ItemsContainer.module.css
@@ -0,0 +1,43 @@
+.items-container {
+ display: grid;
+ row-gap: 40px;
+}
+
+.items-container.best {
+ column-gap: 0;
+ grid-template-columns: repeat(1, 1fr);
+ grid-template-rows: repeat(1, 1fr);
+}
+.items-container.current {
+ column-gap: 8px;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+}
+
+/* Tablet */
+@media screen and (min-width: 768px) and (max-width: 1199px) {
+ .items-container.best {
+ column-gap: 10px;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(1, 1fr);
+ }
+ .items-container.current {
+ column-gap: 16px;
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+ }
+}
+
+/* PC */
+@media (min-width: 1200px) {
+ .items-container.best {
+ column-gap: 24px;
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: repeat(1, 1fr);
+ }
+ .items-container.current {
+ column-gap: 24px;
+ grid-template-columns: repeat(5, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+ }
+}
diff --git a/src/components/ItemsSection.module.css b/src/components/ItemsSection.module.css
new file mode 100644
index 00000000..c262c86f
--- /dev/null
+++ b/src/components/ItemsSection.module.css
@@ -0,0 +1,97 @@
+.cards-section {
+ display: flex;
+ flex-direction: column;
+ max-width: 1250px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 0 24px;
+}
+
+.cards-section.best {
+ gap: 16px;
+}
+
+.cards-section.current {
+ gap: 24px;
+}
+
+.section-header-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ gap: 12px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.section-title {
+ font-weight: 700;
+ font-size: 20px;
+ line-height: 32px;
+ color: var(--color-gray900);
+ flex-grow: 1;
+ order: var(--search-title-order);
+ min-width: 200px;
+}
+
+.search-input-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background-color: var(--color-gray200);
+ width: var(--search-input-container-width);
+ padding: 0 9px;
+ height: 42px;
+ border-radius: 12px;
+ order: var(--search-input-order);
+ flex-grow: var(--search-input-flex-grow);
+}
+
+.search-input-icon {
+ aspect-ratio: 1/1;
+}
+
+.search-input {
+ border: none;
+ background-color: var(--color-gray200);
+ flex-grow: 1;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ outline: none;
+}
+
+.search-input::placeholder {
+ color: var(--color-gray400);
+}
+
+.search-submit {
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 16px;
+ line-height: 26px;
+ padding: 0 23px;
+ color: var(--color-gray100);
+ height: 42px;
+ order: var(--search-submit-order);
+}
+
+.search-select {
+ width: var(--search-select-width);
+ padding: 0 20px;
+ border-radius: 12px;
+ border: 1px solid var(--color-gray300);
+ appearance: none; /* 기본 브라우저 UI 제거 */
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ background-image: var(--search-select-image);
+ background-repeat: no-repeat;
+ background-position: var(--search-select-image-position);
+ background-size: 24px;
+ color: var(--color-gray800);
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ height: 42px;
+ order: var(--search-select-order);
+}
diff --git a/src/components/LogoHeader.jsx b/src/components/LogoHeader.jsx
new file mode 100644
index 00000000..725a9bf2
--- /dev/null
+++ b/src/components/LogoHeader.jsx
@@ -0,0 +1,17 @@
+import { Link } from 'react-router-dom';
+import styles from './LogoHeader.module.css';
+
+const LogoHeader = () => {
+ return (
+
+
+ 판다마켓
+
+ );
+};
+
+export default LogoHeader;
diff --git a/src/components/LogoHeader.module.css b/src/components/LogoHeader.module.css
new file mode 100644
index 00000000..05de70c5
--- /dev/null
+++ b/src/components/LogoHeader.module.css
@@ -0,0 +1,43 @@
+.logo-container {
+ text-decoration: none;
+ display: flex;
+ flex-direction: row;
+ gap: var(--logo-container-gap);
+ align-items: center;
+ justify-content: center;
+ height: var(--logo-container-height);
+ margin-bottom: var(--logo-container-margin-bottom);
+}
+
+.logo-image {
+ aspect-ratio: 51.76 / 51.94;
+ width: var(--logo-image-width);
+}
+
+.logo-text {
+ font-family: '';
+ font-family: 'ROKAFSans';
+ font-weight: 700;
+ font-size: var(--logo-text-font-size);
+ line-height: 100%;
+ color: var(--color-blue400);
+}
+
+:root {
+ --logo-text-font-size: 33.17px;
+ --logo-container-margin-bottom: 24px;
+ --logo-image-width: 51.76px;
+ --logo-container-height: 66px;
+ --logo-container-gap: 11.12px;
+}
+
+/* Tablet, PC */
+@media (min-width: 768px) {
+ :root {
+ --logo-text-font-size: 66.34px;
+ --logo-container-margin-bottom: 40px;
+ --logo-image-width: 103.53px;
+ --logo-container-height: 132px;
+ --logo-container-gap: 22.24px;
+ }
+}
diff --git a/src/components/Nav.jsx b/src/components/Nav.jsx
new file mode 100644
index 00000000..7eb5879a
--- /dev/null
+++ b/src/components/Nav.jsx
@@ -0,0 +1,67 @@
+import { Link } from "react-router-dom";
+import styles from "./Nav.module.css";
+import { useIsLogin } from "../contexts/LoginStateContext";
+
+const LINK_CLASSNAME = {
+ false: "",
+ true: "active",
+};
+
+const Nav = ({ currentSection }) => {
+ const isLogin = useIsLogin();
+
+ return (
+
+
+
+ );
+};
+
+export default Nav;
diff --git a/src/pages/components/Header.css b/src/components/Nav.module.css
similarity index 100%
rename from src/pages/components/Header.css
rename to src/components/Nav.module.css
diff --git a/src/components/Pagination.jsx b/src/components/Pagination.jsx
new file mode 100644
index 00000000..2bb2a4c8
--- /dev/null
+++ b/src/components/Pagination.jsx
@@ -0,0 +1,62 @@
+import styles from './Pagination.module.css';
+
+const Pagination = ({
+ visiblePageNumbers,
+ currentPageNumber,
+ handlers,
+ pageControlEnabled,
+}) => {
+ const { handlePageNumberClick, handlePagePrev, handlePageNext } = handlers;
+ const { prevPageEnable, nextPageEnable } = pageControlEnabled;
+
+ return (
+
+ );
+};
+
+export default Pagination;
diff --git a/src/components/Pagination.module.css b/src/components/Pagination.module.css
new file mode 100644
index 00000000..9ea6a535
--- /dev/null
+++ b/src/components/Pagination.module.css
@@ -0,0 +1,38 @@
+.pagination-container {
+ display: flex;
+ flex-direction: row;
+ padding-top: var(--items-pagination-padding-top);
+ padding-bottom: var(--items-pagination-padding-bottom);
+ padding: 43px 0 58px;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--color-gray050);
+ gap: 4px;
+}
+
+.pagination-button {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border: 1px solid var(--color-gray300);
+ font-weight: 600;
+ font-size: 16px;
+ line-height: 26px;
+ border-radius: 40px;
+ color: var(--color-gray500);
+ background-color: var(--color-white);
+ cursor: pointer;
+}
+
+.pagination-button:disabled {
+ cursor: auto;
+}
+
+.pagination-button.selected {
+ border: none;
+ background-color: #2f80ed;
+ color: var(--color-gray100);
+}
diff --git a/src/components/SocialLogin.jsx b/src/components/SocialLogin.jsx
new file mode 100644
index 00000000..a94bc074
--- /dev/null
+++ b/src/components/SocialLogin.jsx
@@ -0,0 +1,33 @@
+import styles from './SocialLogin.module.css';
+
+const SocialLogin = () => {
+ return (
+
+ );
+};
+
+export default SocialLogin;
diff --git a/src/components/SocialLogin.module.css b/src/components/SocialLogin.module.css
new file mode 100644
index 00000000..38432eb9
--- /dev/null
+++ b/src/components/SocialLogin.module.css
@@ -0,0 +1,17 @@
+.social-container {
+ padding: 16px 23px;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: row;
+ background-color: #e6f2ff;
+ align-items: center;
+ gap: 16px;
+ margin: 24px 0;
+}
+
+.social-text {
+ font-weight: 500;
+ font-size: 16px;
+ line-height: 26px;
+ flex-grow: 1;
+}
diff --git a/src/constants/fieldsConfig.js b/src/constants/fieldsConfig.js
new file mode 100644
index 00000000..de88662a
--- /dev/null
+++ b/src/constants/fieldsConfig.js
@@ -0,0 +1,34 @@
+export const FIELDS_CONFIG = {
+ email: {
+ id: 'email',
+ labelText: '이메일',
+ placeholder: '이메일을 입력해주세요',
+ ariaLabel: '이메일 입력 칸',
+ type: 'email',
+ autoComplete: 'email',
+ },
+ nickname: {
+ id: 'nickname',
+ labelText: '닉네임',
+ placeholder: '닉네임을 입력해주세요',
+ ariaLabel: '닉네임 입력 칸',
+ type: 'text',
+ autoComplete: 'on',
+ },
+ password: {
+ id: 'password',
+ labelText: '비밀번호',
+ placeholder: '비밀번호를 입력해주세요',
+ ariaLabel: '비밀번호 입력 칸',
+ type: 'password',
+ autoComplete: 'current-password',
+ },
+ passwordVerify: {
+ id: 'passwordVerify',
+ labelText: '비밀번호 확인',
+ placeholder: '비밀번호를 다시 한 번 입력해주세요',
+ ariaLabel: '비밀번호 확인 입력 칸',
+ type: 'password',
+ autoComplete: 'new-password',
+ },
+};
diff --git a/src/hooks/useFormFields.jsx b/src/hooks/useFormFields.jsx
index b0e52314..cd720a9c 100644
--- a/src/hooks/useFormFields.jsx
+++ b/src/hooks/useFormFields.jsx
@@ -1,24 +1,27 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useState } from "react";
+import { validators } from "../utils/validators";
-export const useFormFields = ({ FIELDS }) => {
+export const useFormFields = (FIELD_KEYS) => {
const createInitialStates = (initialFields, initialValue) => {
const returnFields = {};
- for (const field in initialFields) {
+ for (const field of initialFields) {
returnFields[field] = initialValue;
}
return returnFields;
};
- //prettier-ignore
- const [values, setValues] = useState(createInitialStates(FIELDS, ''));
- //prettier-ignore
- const [valids, setValids] = useState(createInitialStates(FIELDS, null));
- //prettier-ignore
- const [hints, setHints] = useState(createInitialStates(FIELDS, ''));
- //prettier-ignore
- const [isVisibles, setIsVisibles] = useState(createInitialStates(FIELDS, false));
+ const [values, setValues] = useState(createInitialStates(FIELD_KEYS, '')); //prettier-ignore
+ const [valids, setValids] = useState(createInitialStates(FIELD_KEYS, null)); //prettier-ignore
+ const [hints, setHints] = useState(createInitialStates(FIELD_KEYS, '')); //prettier-ignore
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
+ const getValidateResults = {
+ email: validators.email(values.email),
+ nickname: validators.nickname(values.nickname),
+ password: validators.password(values.password, values.passwordVerify),
+ passwordVerify: validators.passwordVerify(values.passwordVerify, values.password), //prettier-ignore
+ };
+
useEffect(() => {
setIsSubmitEnabled(Object.values(valids).every((v) => v === true));
}, [valids]);
@@ -29,8 +32,8 @@ export const useFormFields = ({ FIELDS }) => {
};
const handleInputBlur = (e) => {
- const { name, value } = e.target;
- const validResults = getValidResults(name, value);
+ const { name } = e.target;
+ const validResults = getValidateResults[name];
updateValidResults(validResults);
};
@@ -41,11 +44,6 @@ export const useFormFields = ({ FIELDS }) => {
}));
};
- const getValidResults = (name, value) => {
- const validFunction = FIELDS[name];
- return validFunction(value);
- };
-
const updateValidResults = (validResults) => {
const nextInputValids = {};
const nextHints = {};
@@ -63,21 +61,12 @@ export const useFormFields = ({ FIELDS }) => {
}));
};
- const handlePasswordIconClick = (name) => {
- setIsVisibles((prev) => ({
- ...prev,
- [name]: !isVisibles[name],
- }));
- };
-
return {
values,
valids,
hints,
- isVisibles,
isSubmitEnabled,
handleInputChange,
handleInputBlur,
- handlePasswordIconClick,
};
};
diff --git a/src/hooks/usePageSizeByBreakPoint.jsx b/src/hooks/usePageSizeByBreakPoint.jsx
new file mode 100644
index 00000000..9252fafc
--- /dev/null
+++ b/src/hooks/usePageSizeByBreakPoint.jsx
@@ -0,0 +1,28 @@
+import { useEffect, useState } from 'react';
+import { useScreenBreakpoint } from './useScreenBreakpoint';
+
+const PAGE_SIZE_LIST = {
+ best: { lg: 4, md: 2, sm: 1 },
+ current: { lg: 10, md: 6, sm: 4 },
+};
+
+export const usePageSizeByBreakPoint = () => {
+ const { breakPoint } = useScreenBreakpoint();
+ const [pageSizeList, setPageSizeList] = useState({});
+
+ useEffect(() => {
+ const nextPageSizeList = {};
+
+ for (const [key, sizeList] of Object.entries(PAGE_SIZE_LIST)) {
+ nextPageSizeList[key] = sizeList[breakPoint];
+ }
+
+ setPageSizeList((prev) => {
+ return JSON.stringify(prev) === JSON.stringify(nextPageSizeList)
+ ? prev
+ : nextPageSizeList;
+ });
+ }, [breakPoint]);
+
+ return { pageSizeList };
+};
diff --git a/src/hooks/usePaginationByOffset.jsx b/src/hooks/usePaginationByOffset.jsx
new file mode 100644
index 00000000..c70da2f5
--- /dev/null
+++ b/src/hooks/usePaginationByOffset.jsx
@@ -0,0 +1,39 @@
+const getCurrentPageState = (
+ offset,
+ pageSize = 1,
+ totalDataCount,
+ maxVisiblePagelength
+) => {
+ const nextCurrentPageNumber = Math.ceil(offset / pageSize);
+
+ const nextTotalPagesCount = Math.ceil(totalDataCount / pageSize);
+ //prettier-ignore
+ const paginationStartPage = Math.floor((nextCurrentPageNumber - 1) / maxVisiblePagelength) * maxVisiblePagelength + 1;
+ const remainingPageCount = nextTotalPagesCount - paginationStartPage + 1;
+ const visiblePageCount =
+ remainingPageCount >= maxVisiblePagelength
+ ? maxVisiblePagelength
+ : remainingPageCount;
+ const nextVisiblePageNumbers = new Array(visiblePageCount)
+ .fill(0)
+ .map((v, i) => v + i + paginationStartPage);
+ return { nextCurrentPageNumber, nextVisiblePageNumbers, nextTotalPagesCount };
+};
+
+export const usePaginationByOffset = (
+ offset,
+ pageSize,
+ totalDataCount,
+ visiblePageLength = 5
+) => {
+ //prettier-ignore
+ const { nextTotalPagesCount, nextCurrentPageNumber, nextVisiblePageNumbers } =
+ getCurrentPageState(offset, pageSize, totalDataCount, visiblePageLength);
+
+ //prettier-ignore
+ return {
+ totalPagesCount: nextTotalPagesCount,
+ currentPageNumber: nextCurrentPageNumber,
+ visiblePageNumbers: nextVisiblePageNumbers,
+ };
+};
diff --git a/src/hooks/useDeviceType.jsx b/src/hooks/useScreenBreakpoint.jsx
similarity index 56%
rename from src/hooks/useDeviceType.jsx
rename to src/hooks/useScreenBreakpoint.jsx
index 72b9845e..94c35116 100644
--- a/src/hooks/useDeviceType.jsx
+++ b/src/hooks/useScreenBreakpoint.jsx
@@ -1,21 +1,24 @@
import { useEffect, useState } from 'react';
-export const useDeviceType = () => {
- const getDeviceType = (width) => {
+const DEFAULT_BREAKPOINT = 'sm';
+
+export const useScreenBreakpoint = () => {
+ const getBreakPoint = (width) => {
if (width >= 1200) return 'lg';
else if (width >= 768) return 'md';
else return 'sm';
};
//prettier-ignore
- const [deviceType, setDeviceType] = useState(getDeviceType(window.innerWidth));
+ const [breakPoint, setBreakPoint] = useState(null);
useEffect(() => {
- const handleResize = () => setDeviceType(getDeviceType(window.innerWidth));
+ const handleResize = () => setBreakPoint(getBreakPoint(window.innerWidth));
+ handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
- return { deviceType };
+ return { breakPoint };
};
diff --git a/src/modules/formatPrice.js b/src/modules/formatPrice.js
deleted file mode 100644
index 6fb6374b..00000000
--- a/src/modules/formatPrice.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const formatPriceKRW = (price) => {
- return price.toLocaleString('ko-KR') + '원';
-};
diff --git a/src/pages/AddItem/AddItem.jsx b/src/pages/AddItem/AddItem.jsx
deleted file mode 100644
index daa2c862..00000000
--- a/src/pages/AddItem/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..3cbad1a1
--- /dev/null
+++ b/src/pages/AddItemPage/AddItemPage.jsx
@@ -0,0 +1,197 @@
+import { useEffect, useRef, useState } from "react";
+import Nav from "../../components/Nav";
+import styles from "./AddItemPage.module.css";
+import { formatNumber } from "../../utils/formatPrice";
+
+const AddItemPage = () => {
+ const [preview, setPreview] = useState(null);
+
+ const [hintVisible, setHintVisible] = useState(false);
+ const [tag, setTag] = useState("");
+
+ const [imgUrl, setImgUrl] = useState(null);
+ const [product, setProduct] = useState("");
+ const [description, setDescription] = useState("");
+ const [price, setPrice] = useState("");
+ const [tagList, setTagList] = useState([]);
+
+ //prettier-ignore
+ const isSubmitEnabled = imgUrl && product && description && price && tagList.length;
+
+ const fileInputRef = useRef();
+
+ const handleAddItemClick = () => {
+ if (!imgUrl) fileInputRef.current.click();
+ else setHintVisible(true);
+ };
+
+ const handleInputFileChange = (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+ setImgUrl(file);
+ };
+
+ const handleInputFileDeleteClick = () => {
+ const inputNode = fileInputRef.current;
+ if (!inputNode) return;
+ inputNode.value = "";
+ setImgUrl(null);
+ setHintVisible(false);
+ };
+
+ const handleInputProductChange = (e) => setProduct(e.target.value);
+
+ const handleInputDescriptionChange = (e) => setDescription(e.target.value);
+
+ const handleInputPriceChange = (e) => {
+ const numericValue = e.target.value.replace(/\D/g, "");
+ const formattedValue = formatNumber(numericValue);
+ setPrice(formattedValue);
+ };
+
+ const handleInputTagChange = (e) => setTag(e.target.value);
+
+ const handleTagInputEnterPress = (e) => {
+ if (e.key === "Enter") {
+ if (tagList.indexOf(e.target.value) !== -1) return;
+ setTagList((prev) => [...prev, e.target.value]);
+ setTag("");
+ }
+ };
+
+ const handleTagDelete = (e) => {
+ setTagList((prev) => {
+ const tagIndex = prev.indexOf(e.target.name);
+ return [...prev.slice(0, tagIndex), ...prev.slice(tagIndex + 1)];
+ });
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ };
+
+ useEffect(() => {
+ if (!imgUrl) return;
+ const nextPreview = URL.createObjectURL(imgUrl);
+ setPreview(nextPreview);
+
+ return () => {
+ setPreview();
+ URL.revokeObjectURL(nextPreview);
+ };
+ }, [imgUrl]);
+
+ 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..6c59831e
--- /dev/null
+++ b/src/pages/AddItemPage/AddItemPage.module.css
@@ -0,0 +1,168 @@
+.form {
+ padding: 0 var(--padding-x);
+ max-width: 1224px;
+ display: flex;
+ flex-direction: column;
+ margin: 24px auto;
+ font-family: 'pretendard';
+ gap: 32px;
+}
+
+.title-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.title {
+ font-weight: 700;
+ font-size: 20px;
+ line-height: 32px;
+}
+
+.subtitle {
+ display: flex;
+ flex-direction: column;
+ font-weight: 700;
+ font-size: 18px;
+ line-height: 26px;
+ gap: 16px;
+}
+
+.submit {
+ width: 74px;
+ height: 42px;
+ font-weight: 600;
+ font-size: 16px;
+ line-height: 26px;
+ color: var(--color-gray100);
+ border-radius: 8px;
+}
+
+.input {
+ background-color: var(--color-gray200);
+ border-radius: 12px;
+ border: none;
+ font-family: 'pretendard';
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ padding: 15px 24px;
+ color: var(--color-gray800);
+ outline:none;
+}
+
+.input::placeholder{
+ color: var(--color-gray400);
+}
+
+.button-additems {
+ aspect-ratio: 1/1;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
+
+.button-additems:hover {
+ background-color: var(--color-gray300);
+}
+
+.additems-preview-image {
+ aspect-ratio: 1/1;
+ width: 48px;
+}
+
+.additems-text {
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ color: var(--color-gray400)
+}
+
+.input-textarea {
+ height: 282px;
+ resize: none;
+}
+
+.images-container {
+ display: flex;
+ flex-direction: row;
+ gap: var(--image-container-gap);
+ height: var(--image-size);
+}
+
+.input-additems-hint {
+ color: var(--color-error);
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+}
+
+.image {
+ aspect-ratio: 1/1;
+ height: 100%;
+ border-radius: 18px;
+}
+
+.image-container {
+ position: relative;
+}
+
+.icon-delete {
+ aspect-ratio: 1/1;
+ cursor: pointer;
+ position: absolute;
+ right: 12px;
+ top: 12px;
+}
+
+.tag-list {
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+}
+
+.tag {
+ border-radius: 26px;
+ padding: 5px 12px 5px 16px;
+ background-color: var(--color-gray200);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+}
+
+.icon-delete-tag {
+ aspect-ratio: 1/1;
+ cursor: pointer;
+}
+
+:root {
+ --image-size: 168px;
+ --padding-x: 15px;
+ --image-container-gap: 10px;
+}
+
+@media screen and (min-width: 768px) and (max-width: 1199px) {
+ :root {
+ --image-size: 168px;
+ --padding-x: 24px;
+ --image-container-gap: 10px;
+ }
+}
+
+@media (min-width: 1200px) {
+ :root {
+ --image-size: 282px;
+ --padding-x: 24px;
+ --image-container-gap: 24px;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/Auth/Field.jsx b/src/pages/Auth/Field.jsx
deleted file mode 100644
index a22a186e..00000000
--- a/src/pages/Auth/Field.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-const Field = ({
- id,
- labelText,
- placeholder,
- ariaLabel,
- type = 'text',
- autoComplete = 'on',
- value,
- valid,
- hint,
- isVisible,
- handleInputChange,
- handleInputBlur,
- handlePasswordIconClick,
-}) => {
- const inputContainerClassName = () => {
- if (valid === null) return '';
- if (valid === false) return 'invalid';
- else return 'valid';
- };
-
- const passwordIconClassName = () => {
- return isVisible ? '' : 'hidden';
- };
-
- const passwordIconImgSrc = isVisible
- ? './images/icon_password_visible.png'
- : './images/icon_password_invisible.png';
-
- const inputType = () => {
- return isVisible ? 'text' : type;
- };
-
- return (
-
- );
-};
-
-export default Field;
diff --git a/src/pages/Auth/FormAuth.css b/src/pages/Auth/FormAuth.css
deleted file mode 100644
index d8278d50..00000000
--- a/src/pages/Auth/FormAuth.css
+++ /dev/null
@@ -1,163 +0,0 @@
-.page-form {
- max-width: 672px;
- margin: 0 auto;
- padding: 80px 16px;
- font-family: 'Pretendard';
- display: flex;
- flex-direction: column;
-}
-
-.form-logo-container {
- text-decoration: none;
- display: flex;
- flex-direction: row;
- gap: var(--form-logo-container-gap);
- align-items: center;
- justify-content: center;
- height: var(--form-logo-container-height);
- margin-bottom: var(--form-logo-container-margin-bottom);
-}
-
-.form-logo-image {
- aspect-ratio: 51.76 / 51.94;
- width: var(--form-logo-image-width);
-}
-
-.form-logo-text {
- font-family: '';
- font-family: 'ROKAFSans';
- font-weight: 700;
- font-size: var(--form-logo-text-font-size);
- line-height: 100%;
- color: var(--color-blue400);
-}
-
-.form-label {
- display: flex;
- flex-direction: column;
- font-weight: 700;
- font-size: var(--form-label-font-size);
- line-height: 24px;
- margin-bottom: var(--form-label-margin-bottom);
-}
-
-.form-input-container {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 15px 24px;
- background-color: var(--color-gray200);
- border-radius: 12px;
- margin-top: var(--form-input-container-margin-top);
- margin-bottom: 8px;
-}
-
-.form-input-container.valid {
- border: 1px solid var(--color-blue400);
-}
-.form-input-container.invalid {
- border: 1px solid var(--color-error);
-}
-
-.form-input {
- flex-grow: 1;
- font-weight: 400;
- font-size: 16px;
- line-height: 26px;
- background-color: var(--color-gray200);
- border: none;
- outline: none;
- color: var(--color-gray800);
-}
-
-.form-input::placeholder {
- color: var(--color-gray400);
-}
-
-.form-password-icon {
- aspect-ratio: 20.39 / 14;
- width: 20.39px;
-}
-.form-password-icon.hidden {
- aspect-ratio: 20.47 / 18.07;
- width: 20.47px;
-}
-
-.form-input-hint {
- color: var(--color-error);
- margin-left: 16px;
- font-weight: 600;
- font-size: 14px;
- line-height: 24px;
-}
-
-.form-input-hint.hidden {
- display: none;
-}
-
-#form-submit {
- font-weight: 600;
- font-size: 20px;
- font-size: 20px;
- line-height: 32px;
- width: 100%;
- padding: 12px;
- border-radius: 40px;
- color: var(--color-gray200);
-}
-
-.form-social-container {
- padding: 16px 23px;
- border-radius: 8px;
- display: flex;
- flex-direction: row;
- background-color: #e6f2ff;
- align-items: center;
- gap: 16px;
- margin: 24px 0;
-}
-
-.form-social-text {
- font-weight: 500;
- font-size: 16px;
- line-height: 26px;
- flex-grow: 1;
-}
-
-.form-hint {
- text-align: center;
- font-weight: 500;
- font-size: 14px;
- line-height: 24px;
- color: var(--color-gray800);
-}
-
-.form-hint-link {
- color: var(--color-blue400);
- line-height: 100%;
-}
-
-:root {
- --form-logo-text-font-size: 33.17px;
- --form-logo-container-margin-bottom: 24px;
- --form-logo-image-width: 51.76px;
- --form-logo-container-height: 66px;
- --form-label-margin-bottom: 16px;
- --form-label-font-size: 14px;
- --form-logo-container-gap: 11.12px;
- --form-input-container-margin-top: 8px;
-}
-
-/* Tablet, PC */
-@media (min-width: 768px) {
- :root {
- --form-logo-text-font-size: 66.34px;
- --form-logo-container-margin-bottom: 40px;
- --form-logo-image-width: 103.53px;
- --form-logo-container-height: 132px;
- --form-label-margin-bottom: 24px;
- --form-label-font-size: 18px;
- --form-logo-container-gap: 22.24px;
- --form-input-container-margin-top: 16px;
- }
-}
diff --git a/src/pages/Auth/Login.jsx b/src/pages/Auth/Login.jsx
deleted file mode 100644
index bdd3d801..00000000
--- a/src/pages/Auth/Login.jsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { Link } from 'react-router-dom';
-import './FormAuth.css';
-import { validators } from '../../modules/validators';
-import { useFormFields } from '../../hooks/useFormFields';
-import { useNavigate } from 'react-router';
-import Field from './Field';
-import { useSetIsLogin } from '../../contexts/LoginStateContext';
-
-const Login = () => {
- const onSubmitNavigate = useNavigate();
-
- const setIsLogin = useSetIsLogin();
-
- const FIELDS = {
- email: validators.email,
- password: validators.password,
- };
-
- const onSubmitRedirect = (e) => {
- e.preventDefault();
- onSubmitNavigate('/items');
- setIsLogin(true);
- };
-
- const formDataObject = {
- FIELDS,
- onSubmitRedirect,
- };
-
- const {
- values,
- valids,
- hints,
- isVisibles,
- isSubmitEnabled,
- handleInputChange,
- handleInputBlur,
- handlePasswordIconClick,
- } = useFormFields(formDataObject);
-
- return (
- <>
-
-
-
- 판다마켓
-
-
-
-
- 판다마켓이 처음이신가요?{' '}
-
- 회원가입
-
-
-
- >
- );
-};
-
-export default Login;
diff --git a/src/pages/Auth/Signup.jsx b/src/pages/Auth/Signup.jsx
deleted file mode 100644
index 76629ce5..00000000
--- a/src/pages/Auth/Signup.jsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { Link } from 'react-router-dom';
-import './FormAuth.css';
-import { validators } from '../../modules/validators';
-import { useFormFields } from '../../hooks/useFormFields';
-import { useNavigate } from 'react-router';
-import Field from './Field';
-
-const Signup = () => {
- const onSubmitNavigate = useNavigate();
-
- const FIELDS = {
- email: validators.email,
- nickname: validators.nickname,
- password: (passwordText) => {
- const passwordVerifyText = values.passwordVerify;
- return validators.password(passwordText, passwordVerifyText);
- },
- passwordVerify: (passwordVerifyText) => {
- const passwordText = values.password;
- return validators.passwordVerify(passwordVerifyText, passwordText);
- },
- };
- const onSubmitRedirect = (e) => {
- e.preventDefault();
- onSubmitNavigate('/login');
- };
- const formDataObject = {
- FIELDS,
- };
-
- const {
- values,
- valids,
- hints,
- isVisibles,
- isSubmitEnabled,
- handleInputChange,
- handleInputBlur,
- handlePasswordIconClick,
- } = useFormFields(formDataObject);
-
- return (
- <>
-
-
-
- 판다마켓
-
-
-
-
- 이미 회원이신가요?{' '}
-
- 로그인
-
-
-
- >
- );
-};
-
-export default Signup;
diff --git a/src/pages/AuthPage/FormAuth.css b/src/pages/AuthPage/FormAuth.css
new file mode 100644
index 00000000..dcccfe4f
--- /dev/null
+++ b/src/pages/AuthPage/FormAuth.css
@@ -0,0 +1,32 @@
+.page-form {
+ max-width: 672px;
+ margin: 0 auto;
+ padding: 80px 16px;
+ font-family: 'Pretendard';
+ display: flex;
+ flex-direction: column;
+}
+
+#form-submit {
+ font-weight: 600;
+ font-size: 20px;
+ font-size: 20px;
+ line-height: 32px;
+ width: 100%;
+ padding: 12px;
+ border-radius: 40px;
+ color: var(--color-gray200);
+}
+
+.form-hint {
+ text-align: center;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--color-gray800);
+}
+
+.form-hint-link {
+ color: var(--color-blue400);
+ line-height: 100%;
+}
diff --git a/src/pages/AuthPage/LoginPage.jsx b/src/pages/AuthPage/LoginPage.jsx
new file mode 100644
index 00000000..f268c2d1
--- /dev/null
+++ b/src/pages/AuthPage/LoginPage.jsx
@@ -0,0 +1,77 @@
+import { Link } from "react-router-dom";
+import "./FormAuth.css";
+import { useNavigate } from "react-router";
+import { useSetIsLogin } from "../../contexts/LoginStateContext";
+import Field from "../../components/Field";
+import { FIELDS_CONFIG } from "../../constants/fieldsConfig";
+import SocialLogin from "../../components/SocialLogin";
+import LogoHeader from "../../components/LogoHeader";
+import { useFormFields } from "../../hooks/useFormFields";
+
+const FIELD_KEYS = ["email", "password"];
+
+const LoginPage = () => {
+ const onSubmitNavigate = useNavigate();
+ const setIsLogin = useSetIsLogin();
+
+ const {
+ values,
+ valids,
+ hints,
+ isSubmitEnabled,
+ handleInputChange,
+ handleInputBlur,
+ } = useFormFields(FIELD_KEYS);
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSubmitNavigate("/items");
+ setIsLogin(true);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ 판다마켓이 처음이신가요?{" "}
+
+ 회원가입
+
+
+
+ >
+ );
+};
+
+export default LoginPage;
diff --git a/src/pages/AuthPage/SignupPage.jsx b/src/pages/AuthPage/SignupPage.jsx
new file mode 100644
index 00000000..220290e7
--- /dev/null
+++ b/src/pages/AuthPage/SignupPage.jsx
@@ -0,0 +1,90 @@
+import { Link } from "react-router-dom";
+import "./FormAuth.css";
+import { useNavigate } from "react-router";
+import Field from "../../components/Field";
+import { FIELDS_CONFIG } from "../../constants/fieldsConfig";
+import SocialLogin from "../../components/SocialLogin";
+import LogoHeader from "../../components/LogoHeader";
+import { useFormFields } from "../../hooks/useFormFields";
+
+const FIELD_KEYS = ["email", "nickname", "password", "passwordVerify"];
+
+const Signup = () => {
+ const onSubmitNavigate = useNavigate();
+
+ const {
+ values,
+ valids,
+ hints,
+ isSubmitEnabled,
+ handleInputChange,
+ handleInputBlur,
+ } = useFormFields(FIELD_KEYS);
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSubmitNavigate("/login");
+ };
+
+ return (
+ <>
+
+
+
+
+
+ 이미 회원이신가요?{" "}
+
+ 로그인
+
+
+
+ >
+ );
+};
+
+export default Signup;
diff --git a/src/pages/Board/Board.jsx b/src/pages/Board/Board.jsx
deleted file mode 100644
index cf384939..00000000
--- a/src/pages/Board/Board.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import Header from '../components/Header';
-
-const Board = () => {
- return (
- <>
-
- >
- );
-};
-
-export default Board;
diff --git a/src/pages/BoardPage/BoardPage.jsx b/src/pages/BoardPage/BoardPage.jsx
new file mode 100644
index 00000000..637703f0
--- /dev/null
+++ b/src/pages/BoardPage/BoardPage.jsx
@@ -0,0 +1,11 @@
+import Nav from '../../components/Nav';
+
+const Board = () => {
+ return (
+ <>
+
+ >
+ );
+};
+
+export default Board;
diff --git a/src/pages/Faq/Faq.jsx b/src/pages/Faq/Faq.jsx
deleted file mode 100644
index 16ed36d8..00000000
--- a/src/pages/Faq/Faq.jsx
+++ /dev/null
@@ -1,5 +0,0 @@
-const Faq = () => {
- return <>>;
-};
-
-export default Faq;
diff --git a/src/pages/FaqPage/FaqPage.jsx b/src/pages/FaqPage/FaqPage.jsx
new file mode 100644
index 00000000..7062fb66
--- /dev/null
+++ b/src/pages/FaqPage/FaqPage.jsx
@@ -0,0 +1,5 @@
+const FaqPage = () => {
+ return <>>;
+};
+
+export default FaqPage;
diff --git a/src/pages/Home/Banner.css b/src/pages/HomePage/Banner.css
similarity index 100%
rename from src/pages/Home/Banner.css
rename to src/pages/HomePage/Banner.css
diff --git a/src/pages/Home/BannerBottom.css b/src/pages/HomePage/BannerBottom.css
similarity index 100%
rename from src/pages/Home/BannerBottom.css
rename to src/pages/HomePage/BannerBottom.css
diff --git a/src/pages/Home/Card.css b/src/pages/HomePage/Card.css
similarity index 100%
rename from src/pages/Home/Card.css
rename to src/pages/HomePage/Card.css
diff --git a/src/pages/Home/Cards.css b/src/pages/HomePage/Cards.css
similarity index 100%
rename from src/pages/Home/Cards.css
rename to src/pages/HomePage/Cards.css
diff --git a/src/pages/Home/Footer.css b/src/pages/HomePage/Footer.css
similarity index 100%
rename from src/pages/Home/Footer.css
rename to src/pages/HomePage/Footer.css
diff --git a/src/pages/Home/Home.css b/src/pages/HomePage/Home.css
similarity index 100%
rename from src/pages/Home/Home.css
rename to src/pages/HomePage/Home.css
diff --git a/src/pages/Home/Home.jsx b/src/pages/HomePage/HomePage.jsx
similarity index 97%
rename from src/pages/Home/Home.jsx
rename to src/pages/HomePage/HomePage.jsx
index d2f2585d..73da0504 100644
--- a/src/pages/Home/Home.jsx
+++ b/src/pages/HomePage/HomePage.jsx
@@ -5,12 +5,12 @@ import './Cards.css';
import './Card.css';
import './BannerBottom.css';
import './Footer.css';
-import Header from '../components/Header';
+import Nav from '../../components/Nav';
-const Home = () => {
+const HomePage = () => {
return (
<>
-
+
@@ -163,4 +163,4 @@ const Home = () => {
);
};
-export default Home;
+export default HomePage;
diff --git a/src/pages/Home/Main.css b/src/pages/HomePage/Main.css
similarity index 100%
rename from src/pages/Home/Main.css
rename to src/pages/HomePage/Main.css
diff --git a/src/pages/Items/Header.css b/src/pages/Items/Header.css
deleted file mode 100644
index 97afd9dd..00000000
--- a/src/pages/Items/Header.css
+++ /dev/null
@@ -1,78 +0,0 @@
-.page-header {
- border-bottom: 1px solid #dfdfdf;
-}
-
-.nav-container {
- max-width: 1520px;
- height: 70px;
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 0 var(--nav-padding-x);
- margin: 0 auto;
- gap: 32px;
-}
-
-.nav-logo-container {
- text-decoration: none;
- display: flex;
- flex-direction: row;
- gap: 8.59px;
- align-items: center;
-}
-
-.nav-logo-img {
- aspect-ratio: 40 / 40.14;
- display: var(--display-logo-img);
-}
-
-.nav-logo-text {
- font-weight: 700;
- font-size: 25.63px;
- line-height: 100%;
- color: var(--color-blue400);
- font-family: 'ROKAFSans';
-}
-
-.nav-link-container {
- display: flex;
- flex-direction: row;
- flex-grow: 1;
-}
-
-.nav-link {
- display: inline-block;
- padding: 21px 15px;
- text-decoration: none;
- color: var(--color-gray600);
- font-weight: 600;
- font-size: 18px;
- line-height: 26px;
-}
-
-.nav-button {
- border-radius: 8px;
- padding: 12px 43px;
- line-height: 26px;
- color: var(--color-gray100);
-}
-
-:root {
- --nav-padding-x: 16px;
- --display-logo-img: none;
-}
-
-/* Tablet */
-@media screen and (min-width: 768px) and (max-width: 1199px) {
- :root {
- --nav-padding-x: 24px;
- --display-logo-img: inline;
- }
-}
-/* PC */
-@media (min-width: 1200px) {
- :root {
- --nav-padding-x: 200px;
- --display-logo-img: inline;
- }
-}
diff --git a/src/pages/Items/Items.jsx b/src/pages/Items/Items.jsx
deleted file mode 100644
index 036d8980..00000000
--- a/src/pages/Items/Items.jsx
+++ /dev/null
@@ -1,306 +0,0 @@
-import { Link } from 'react-router-dom';
-import './Items.css';
-import { getItems } from '../../apis/api';
-import { useEffect, useState } from 'react';
-import { useDeviceType } from '../../hooks/useDeviceType';
-import { formatPriceKRW } from '../../modules/formatPrice';
-import { useNavigate } from 'react-router';
-import Header from '../components/Header';
-
-const ItemComponent = ({
- id,
- imageUrl,
- imageDefaultUrl,
- name,
- price,
- favoriteCount,
-}) => {
- return (
-
-

{
- e.target.onError = null;
- e.target.src = imageDefaultUrl;
- }}
- alt={name}
- width={282}
- />
-
-
{name}
-
{formatPriceKRW(price)}
-
-

-
{favoriteCount}
-
-
-
- );
-};
-
-const pageSizeByDevice = {
- best: (deviceType) => {
- return deviceType === 'lg'
- ? 4
- : deviceType === 'md'
- ? 2
- : deviceType === 'sm'
- ? 1
- : 0;
- },
- current: (deviceType) => {
- return deviceType === 'lg'
- ? 10
- : deviceType === 'md'
- ? 6
- : deviceType === 'sm'
- ? 4
- : 0;
- },
-};
-
-const getCurrentPageState = (
- offset,
- pageSize,
- totalDataCount,
- maxVisiblePageCount = 5
-) => {
- //총페이지가 307일때
- //pagesize가 10이고 offset이 13이면?
- //현재페이지: 2, pagenumbers = 1,2,3,4,5
- const currentPageNumber = Math.ceil(offset / pageSize);
- const paginationStartPage = Math.floor((currentPageNumber - 1) / 5) * 5 + 1;
- const lastPageNumber = Math.ceil(totalDataCount / pageSize);
- const remainingPageCount = lastPageNumber - paginationStartPage + 1;
- const visiblePageCount =
- remainingPageCount >= maxVisiblePageCount
- ? maxVisiblePageCount
- : remainingPageCount;
- const visiblePageNumbers = new Array(visiblePageCount)
- .fill(0)
- .map((v, i) => v + i + paginationStartPage);
- return { currentPageNumber, visiblePageNumbers };
-};
-
-const Items = () => {
- const { deviceType } = useDeviceType();
-
- const [offset, setOffset] = useState(1);
- const [order, setOrder] = useState('recent');
- const [keyword, setKeyword] = useState('');
-
- const [bestItemList, setBestItemList] = useState([]);
- const [currentItemList, setCurrentItemList] = useState([]);
- const [pageNumbers, setPageNumbers] = useState([1]);
- const [currentPageNumber, setCurrentPageNumber] = useState(1);
- const [lastPageIndex, setLastPageIndex] = useState(1);
-
- const [searchInputValue, setSearchInputValue] = useState('');
-
- const onCreateNewItemNavigate = useNavigate();
-
- const handleSearchOrderChange = (e) => {
- setOffset(1);
- setOrder(e.target.value);
- };
-
- const loadBestItemList = async (options) => {
- const result = await getItems(options);
- if (!result) return;
- const { list } = result;
- setBestItemList(list);
- };
-
- const loadCurrentItemList = async (option) => {
- const result = await getItems(option);
- if (!result) return;
- const { list, totalCount } = result;
- setCurrentItemList(list);
- setLastPageIndex(Math.ceil(totalCount / option.pageSize));
- const currentPageState = getCurrentPageState(
- option.offset,
- option.pageSize,
- totalCount
- );
- setPageNumbers((prev) => {
- const nextPageNumbers = currentPageState.visiblePageNumbers;
- return JSON.stringify(prev) === JSON.stringify(nextPageNumbers)
- ? prev
- : nextPageNumbers;
- });
- setCurrentPageNumber(currentPageState.currentPageNumber);
- };
-
- const handleSearchInputChange = (e) => setSearchInputValue(e.target.value);
- const handleSearchInputEnterPress = (e) => {
- if (e.key === 'Enter') {
- setOffset(1);
- setKeyword(searchInputValue);
- }
- };
-
- const handleCreateNewItemClick = (e) => {
- e.preventDefault();
- onCreateNewItemNavigate('/additem');
- };
-
- //prettier-ignore
- const handlePageNumberClick = (e) => onPaginationButtonClick(Number(e.target.value));
- const handlePagePrev = () => onPaginationButtonClick(currentPageNumber - 1);
- const handlePageNext = () => onPaginationButtonClick(currentPageNumber + 1);
- const onPaginationButtonClick = (nextPageNumber) =>
- setOffset((nextPageNumber - 1) * pageSizeByDevice.current(deviceType) + 1);
-
- const prevPageEnable = currentPageNumber > 1;
- const nextPageEnable = currentPageNumber < lastPageIndex;
-
- const prevButtonImgSrc = prevPageEnable
- ? './images/ic_prevPageClick_active.png'
- : './images/ic_prevPageClick_inactive.png';
-
- const nextButtonImgSrc = nextPageEnable
- ? './images/ic_nextPageClick_active.png'
- : './images/ic_nextPageClick_inactive.png';
-
- useEffect(() => {
- (async () => {
- await loadBestItemList({
- offset: 1,
- pageSize: pageSizeByDevice.best(deviceType),
- orderBy: 'favorite',
- keyword: '',
- });
- })();
- }, [deviceType]);
-
- useEffect(() => {
- (async () => {
- await loadCurrentItemList({
- offset: offset,
- pageSize: pageSizeByDevice.current(deviceType),
- orderBy: order,
- keyword: keyword,
- });
- })();
- }, [deviceType, order, offset, keyword]);
-
- return (
- <>
-
-
-
-
-
베스트 상품
-
-
- {bestItemList.map((item) => {
- return (
-
- );
- })}
-
-
-
-
-
전체 상품
-
-

-
-
-
-
-
-
- {currentItemList.map((item) => {
- return (
-
- );
- })}
-
-
-
-
- >
- );
-};
-
-export default Items;
diff --git a/src/pages/Items/Items.css b/src/pages/ItemsPage/ItemsPage.css
similarity index 89%
rename from src/pages/Items/Items.css
rename to src/pages/ItemsPage/ItemsPage.css
index 3489fe02..824e3e6a 100644
--- a/src/pages/Items/Items.css
+++ b/src/pages/ItemsPage/ItemsPage.css
@@ -110,14 +110,6 @@
row-gap: 40px;
}
-.item-container {
- display: flex;
- flex-direction: column;
- gap: 16px;
- flex-grow: 1;
- width: 100%;
-}
-
#cards-best .items-container {
column-gap: 0;
grid-template-columns: repeat(1, 1fr);
@@ -129,47 +121,6 @@
grid-template-rows: repeat(2, 1fr);
}
-.item-context {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.item-image {
- aspect-ratio: 1/1;
- width: 100%;
- border-radius: 16px;
-}
-
-.item-title {
- font-weight: 500;
- font-size: 14px;
- line-height: 24px;
-}
-.item-price {
- font-weight: 700;
- font-size: 16px;
- line-height: 26px;
-}
-
-.item-favorite-container {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 3.35px;
-}
-
-.item-favorite-image {
- aspect-ratio: 1/1;
- width: 16px;
-}
-
-.item-favorite-count {
- font-weight: 500;
- font-size: 12px;
- line-height: 18px;
-}
-
.items-pagination {
display: flex;
flex-direction: row;
diff --git a/src/pages/ItemsPage/ItemsPage.jsx b/src/pages/ItemsPage/ItemsPage.jsx
new file mode 100644
index 00000000..3561bbc5
--- /dev/null
+++ b/src/pages/ItemsPage/ItemsPage.jsx
@@ -0,0 +1,21 @@
+import "./ItemsPage.css";
+import Nav from "../../components/Nav";
+import { usePageSizeByBreakPoint } from "../../hooks/usePageSizeByBreakPoint";
+import BestItemsSection from "../../components/BestItemsSection";
+import CurrentItemsSection from "../../components/CurrentItemsSection";
+
+const ItemsPage = () => {
+ const { pageSizeList } = usePageSizeByBreakPoint();
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export default ItemsPage;
diff --git a/src/pages/Privacy/Privacy.jsx b/src/pages/Privacy/Privacy.jsx
deleted file mode 100644
index fb9f1708..00000000
--- a/src/pages/Privacy/Privacy.jsx
+++ /dev/null
@@ -1,5 +0,0 @@
-const Privacy = () => {
- return <>>;
-};
-
-export default Privacy;
diff --git a/src/pages/PrivacyPage/PrivacyPage.jsx b/src/pages/PrivacyPage/PrivacyPage.jsx
new file mode 100644
index 00000000..c53e6304
--- /dev/null
+++ b/src/pages/PrivacyPage/PrivacyPage.jsx
@@ -0,0 +1,5 @@
+const PrivacyPage = () => {
+ return <>>;
+};
+
+export default PrivacyPage;
diff --git a/src/pages/components/Header.jsx b/src/pages/components/Header.jsx
deleted file mode 100644
index 50b035ef..00000000
--- a/src/pages/components/Header.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Link } from 'react-router-dom';
-import './Header.css';
-import { useIsLogin } from '../../contexts/LoginStateContext';
-
-const Header = ({ currentSection }) => {
- const isLogin = useIsLogin();
- const linkClassName = (linkName) => {
- return currentSection === linkName ? 'active' : '';
- };
- return (
-
-
-
- );
-};
-
-export default Header;
diff --git a/src/apis/api.js b/src/utils/api.js
similarity index 100%
rename from src/apis/api.js
rename to src/utils/api.js
diff --git a/src/modules/debounce.js b/src/utils/debounce.js
similarity index 100%
rename from src/modules/debounce.js
rename to src/utils/debounce.js
diff --git a/src/utils/formatPrice.js b/src/utils/formatPrice.js
new file mode 100644
index 00000000..4912f0fa
--- /dev/null
+++ b/src/utils/formatPrice.js
@@ -0,0 +1,7 @@
+export const formatPriceKRW = (price) => {
+ return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + '원';
+};
+
+export const formatNumber = (price) => {
+ return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+}
\ No newline at end of file
diff --git a/src/modules/validators.js b/src/utils/validators.js
similarity index 100%
rename from src/modules/validators.js
rename to src/utils/validators.js