diff --git a/vite-project/eslint.config.js b/vite-project/eslint.config.js
index ec2b712d..8cf17d62 100644
--- a/vite-project/eslint.config.js
+++ b/vite-project/eslint.config.js
@@ -1,33 +1,34 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
export default [
- { ignores: ['dist'] },
+ { ignores: ["dist"] },
{
- files: ['**/*.{js,jsx}'],
+ files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
- ecmaVersion: 'latest',
+ ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
- sourceType: 'module',
+ sourceType: "module",
},
},
plugins: {
- 'react-hooks': reactHooks,
- 'react-refresh': reactRefresh,
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
- 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
- 'react-refresh/only-export-components': [
- 'warn',
+ "no-unused-vars": ["warn", { varsIgnorePattern: "^[A-Z_]" }],
+ "react-refresh/only-export-components": [
+ "warn",
{ allowConstantExport: true },
],
+ "no-console": ["warn", { allow: ["warn", "error"] }],
},
},
-]
+];
diff --git a/vite-project/package-lock.json b/vite-project/package-lock.json
index 7f1e275a..58b22412 100644
--- a/vite-project/package-lock.json
+++ b/vite-project/package-lock.json
@@ -8,12 +8,14 @@
"name": "vite-project",
"version": "0.0.0",
"dependencies": {
+ "@tanstack/react-query": "^5.85.5",
"axios": "^1.11.0",
"dotenv": "^17.2.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-responsive": "^10.0.1",
- "react-router": "^7.7.0"
+ "react-router": "^7.7.0",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
@@ -1311,6 +1313,32 @@
"win32"
]
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.85.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz",
+ "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.85.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz",
+ "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.85.5"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1374,7 +1402,7 @@
"version": "19.1.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -1683,7 +1711,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -3205,6 +3233,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
+ "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/vite-project/package.json b/vite-project/package.json
index f1c069c4..596ed67d 100644
--- a/vite-project/package.json
+++ b/vite-project/package.json
@@ -10,12 +10,14 @@
"preview": "vite preview"
},
"dependencies": {
+ "@tanstack/react-query": "^5.85.5",
"axios": "^1.11.0",
"dotenv": "^17.2.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-responsive": "^10.0.1",
- "react-router": "^7.7.0"
+ "react-router": "^7.7.0",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
diff --git a/vite-project/public/images/ic_plus.svg b/vite-project/public/images/ic_plus.svg
new file mode 100644
index 00000000..5bb9abf5
--- /dev/null
+++ b/vite-project/public/images/ic_plus.svg
@@ -0,0 +1,4 @@
+
diff --git a/vite-project/public/images/ic_x.svg b/vite-project/public/images/ic_x.svg
new file mode 100644
index 00000000..5e2e3e92
--- /dev/null
+++ b/vite-project/public/images/ic_x.svg
@@ -0,0 +1,4 @@
+
diff --git a/vite-project/src/App.css b/vite-project/src/App.css
index 8152f4e9..cd559b3c 100644
--- a/vite-project/src/App.css
+++ b/vite-project/src/App.css
@@ -1,5 +1,17 @@
:root {
--primary100: #3692ff;
+ --primary200: #1967d6;
+ --primary300: #1251aa;
+
+ --gray50: #f9fafb;
+ --gray100: #f3f4f6;
+ --gray200: #e5e7eb;
+ --gray400: #9ca3af;
+ --gray500: #6b7280;
+ --gray600: #4b5563;
+ --gray700: #374151;
+ --gray800: #1f2937;
+ --gray900: #111827;
}
* {
@@ -45,12 +57,69 @@ p {
.wrap {
padding: 0 240px;
+}
+
+.btn-large {
+ height: 56px;
+ padding: 16px 124px;
+ border-radius: 40px;
+ border: none;
+ font-weight: 600;
+ font-size: 20px;
+ color: var(--gray100);
+}
+
+.btn-medium {
+ height: 48px;
+ padding: 12px 71px;
+ border-radius: 40px;
+ border: none;
+ font-weight: 600;
+ font-size: 18px;
+ color: var(--gray100);
+}
+
+.btn-small-40 {
+ height: 42px;
+ padding: 12px 23px;
+ border-radius: 8px;
+ border: none;
+ font-weight: 600;
+ font-size: 16px;
+ color: var(--gray100);
+}
+
+.btn-small-48 {
+ height: 48px;
+ padding: 12px 23px;
+ border-radius: 8px;
+ border: none;
+ font-weight: 600;
+ font-size: 16px;
+ color: var(--gray100);
+}
+
+.btn-large:disabled,
+.btn-medium:disabled,
+.btn-small-40:disabled,
+.btn-small-48:disabled {
+ background-color: #9ca3af;
+}
+
+.ic-x-btn {
+ background-color: var(--gray400);
+ color: #f9fafb;
+ width: 20px;
+ height: 20px;
+ border-radius: 100%;
display: flex;
justify-content: center;
+ align-items: center;
}
.container {
max-width: 1200px;
+ margin: 0 auto;
}
@media screen and (max-width: 1199px) {
diff --git a/vite-project/src/App.jsx b/vite-project/src/App.jsx
index b43d95c5..8ad11bfe 100644
--- a/vite-project/src/App.jsx
+++ b/vite-project/src/App.jsx
@@ -1,10 +1,9 @@
-import { useState } from "react";
import "./App.css";
import { Route, Routes, Navigate } from "react-router";
-import ItemsPage from "./pages/Items-Page/ItemsPage";
-import AddItemPage from "./pages/AddItem-Page/AddItemPage";
+import ItemsPage from "./pages/ItemsPage";
+import AddItemPage from "./pages/AddItemPage";
import Layout from "./layout/Layout";
-import FreeBoard from "./pages/FreeBoard-Page/FreeBoard";
+import FreeBoard from "./pages/FreeBoardPage";
function App() {
return (
diff --git a/vite-project/src/utils/api.js b/vite-project/src/api/axiosInstance.js
similarity index 75%
rename from vite-project/src/utils/api.js
rename to vite-project/src/api/axiosInstance.js
index 00661b44..6fcdc49a 100644
--- a/vite-project/src/utils/api.js
+++ b/vite-project/src/api/axiosInstance.js
@@ -2,9 +2,11 @@ import axios from "axios";
const VITE_BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
-export const instance = axios.create({
+const instance = axios.create({
baseURL: VITE_BACKEND_URL,
headers: {
"Content-Type": "application/json",
},
});
+
+export default instance;
diff --git a/vite-project/src/common/Dropdown/DropDown.jsx b/vite-project/src/common/Dropdown/index.jsx
similarity index 100%
rename from vite-project/src/common/Dropdown/DropDown.jsx
rename to vite-project/src/common/Dropdown/index.jsx
diff --git a/vite-project/src/common/ErrorMessage/ErrorMessage.style.css b/vite-project/src/common/ErrorMessage/ErrorMessage.style.css
new file mode 100644
index 00000000..732cd942
--- /dev/null
+++ b/vite-project/src/common/ErrorMessage/ErrorMessage.style.css
@@ -0,0 +1,4 @@
+.error-message {
+ color: #f74747;
+ padding: 6px 8px;
+}
diff --git a/vite-project/src/common/ErrorMessage/index.jsx b/vite-project/src/common/ErrorMessage/index.jsx
new file mode 100644
index 00000000..a51f4e39
--- /dev/null
+++ b/vite-project/src/common/ErrorMessage/index.jsx
@@ -0,0 +1,7 @@
+import "./ErrorMessage.style.css";
+
+const ErrorMessage = ({ errorMessage }) => {
+ return
{errorMessage}
;
+};
+
+export default ErrorMessage;
diff --git a/vite-project/src/common/LoadingSpinner/LoadingSpinner.style.css b/vite-project/src/common/LoadingSpinner/LoadingSpinner.style.css
new file mode 100644
index 00000000..8bddf0c0
--- /dev/null
+++ b/vite-project/src/common/LoadingSpinner/LoadingSpinner.style.css
@@ -0,0 +1,21 @@
+.spinner-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100px;
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid rgba(0, 0, 0, 0.1);
+ border-left-color: var(--primary100);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/vite-project/src/common/LoadingSpinner/index.jsx b/vite-project/src/common/LoadingSpinner/index.jsx
new file mode 100644
index 00000000..c33e26eb
--- /dev/null
+++ b/vite-project/src/common/LoadingSpinner/index.jsx
@@ -0,0 +1,11 @@
+import "./LoadingSpinner.style.css";
+
+const LoadingSpinner = () => {
+ return (
+
+ );
+};
+
+export default LoadingSpinner;
diff --git a/vite-project/src/common/Navbar/Navbar.style.css b/vite-project/src/common/Navbar/Navbar.style.css
index bb52a84a..379a9b7b 100644
--- a/vite-project/src/common/Navbar/Navbar.style.css
+++ b/vite-project/src/common/Navbar/Navbar.style.css
@@ -58,3 +58,9 @@
padding: 0 16px;
}
}
+
+@media screen and (max-width: 767px) {
+ .nav-logo {
+ width: 100px;
+ }
+}
diff --git a/vite-project/src/common/Navbar/Navbar.jsx b/vite-project/src/common/Navbar/index.jsx
similarity index 72%
rename from vite-project/src/common/Navbar/Navbar.jsx
rename to vite-project/src/common/Navbar/index.jsx
index 2014eb8e..de2624a3 100644
--- a/vite-project/src/common/Navbar/Navbar.jsx
+++ b/vite-project/src/common/Navbar/index.jsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Link, NavLink } from "react-router";
+import { Link, NavLink, useLocation } from "react-router";
import "./Navbar.style.css";
import { useMediaQuery } from "react-responsive";
@@ -10,6 +10,7 @@ const navLinks = [
const Navbar = () => {
const isMobile = useMediaQuery({ maxWidth: 767 });
+ const location = useLocation();
return (
@@ -27,7 +28,16 @@ const Navbar = () => {
{navLinks.map((link) => (
-
- {link.title}
+
+ {link.title}
+
))}
diff --git a/vite-project/src/common/Pagination/Pagination.jsx b/vite-project/src/common/Pagination/index.jsx
similarity index 100%
rename from vite-project/src/common/Pagination/Pagination.jsx
rename to vite-project/src/common/Pagination/index.jsx
diff --git a/vite-project/src/common/Product-Card/ProductCard.style.css b/vite-project/src/common/ProductCard/ProductCard.style.css
similarity index 100%
rename from vite-project/src/common/Product-Card/ProductCard.style.css
rename to vite-project/src/common/ProductCard/ProductCard.style.css
diff --git a/vite-project/src/common/Product-Card/ProductCard.jsx b/vite-project/src/common/ProductCard/index.jsx
similarity index 100%
rename from vite-project/src/common/Product-Card/ProductCard.jsx
rename to vite-project/src/common/ProductCard/index.jsx
diff --git a/vite-project/src/common/Product-Card/ProductList.jsx b/vite-project/src/common/ProductList/index.jsx
similarity index 69%
rename from vite-project/src/common/Product-Card/ProductList.jsx
rename to vite-project/src/common/ProductList/index.jsx
index 3fe23879..10308393 100644
--- a/vite-project/src/common/Product-Card/ProductList.jsx
+++ b/vite-project/src/common/ProductList/index.jsx
@@ -1,10 +1,10 @@
import React from "react";
import { Link } from "react-router";
-import ProductCard from "./ProductCard";
-const ProductList = ({ allProducts, className }) => {
+import ProductCard from "../ProductCard";
+const ProductList = ({ products, className }) => {
return (
- {allProducts?.map((product) => (
+ {products?.map((product) => (
-
diff --git a/vite-project/src/common/TagBadge/TagBadge.style.css b/vite-project/src/common/TagBadge/TagBadge.style.css
new file mode 100644
index 00000000..f5a936b8
--- /dev/null
+++ b/vite-project/src/common/TagBadge/TagBadge.style.css
@@ -0,0 +1,16 @@
+.tag-badge {
+ background-color: var(--gray100);
+ padding: 6px 12px;
+ border-radius: 26px;
+ height: 36px;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+
+.tag-badge > span {
+ color: #1f2937;
+ font-weight: 400;
+ font-size: 16px;
+}
diff --git a/vite-project/src/common/TagBadge/index.jsx b/vite-project/src/common/TagBadge/index.jsx
new file mode 100644
index 00000000..ddda26e9
--- /dev/null
+++ b/vite-project/src/common/TagBadge/index.jsx
@@ -0,0 +1,14 @@
+import "./TagBadge.style.css";
+
+const TagBadge = ({ name, onDelete }) => {
+ return (
+
+
#{name}
+
+
+ );
+};
+
+export default TagBadge;
diff --git a/vite-project/src/common/TagList/TagList.style.css b/vite-project/src/common/TagList/TagList.style.css
new file mode 100644
index 00000000..91455908
--- /dev/null
+++ b/vite-project/src/common/TagList/TagList.style.css
@@ -0,0 +1,4 @@
+.tag-area {
+ display: flex;
+ gap: 16px;
+}
diff --git a/vite-project/src/common/TagList/index.jsx b/vite-project/src/common/TagList/index.jsx
new file mode 100644
index 00000000..f5443253
--- /dev/null
+++ b/vite-project/src/common/TagList/index.jsx
@@ -0,0 +1,16 @@
+import TagBadge from "../TagBadge";
+import "./TagList.style.css";
+
+const TagList = ({ tags, onDelete }) => {
+ return (
+
+ {tags?.map((tag, index) => (
+ -
+ onDelete(index)} />
+
+ ))}
+
+ );
+};
+
+export default TagList;
diff --git a/vite-project/src/constants/PRODUCTS.js b/vite-project/src/constants/PRODUCTS.js
new file mode 100644
index 00000000..c08eb7c1
--- /dev/null
+++ b/vite-project/src/constants/PRODUCTS.js
@@ -0,0 +1,5 @@
+export const ORDER_BYS = {
+ 최신순: "recent",
+ 좋아요순: "favorite",
+};
+export const GROUP_SIZE = 5;
diff --git a/vite-project/src/hooks/useGetProduct.js b/vite-project/src/hooks/useGetProduct.js
deleted file mode 100644
index b79b9224..00000000
--- a/vite-project/src/hooks/useGetProduct.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { instance } from "../utils/api";
-
-export const useGetProduct = async (params) => {
- try {
- const { data } = await instance.get("/products", { params });
- console.log(data);
- return [data.list, data.totalCount];
- } catch (error) {
- console.error(error);
- }
-};
diff --git a/vite-project/src/hooks/useGetProducts.js b/vite-project/src/hooks/useGetProducts.js
new file mode 100644
index 00000000..13c36521
--- /dev/null
+++ b/vite-project/src/hooks/useGetProducts.js
@@ -0,0 +1,17 @@
+import { useQuery } from "@tanstack/react-query";
+import instance from "../api/axiosInstance";
+
+const getProducts = async ({ page = 1, orderBy = "recent", pageSize = 10 }) => {
+ return await instance.get("/products", {
+ params: { page, orderBy, pageSize },
+ });
+};
+
+export const useGetProductsQuery = ({ page, orderBy, pageSize }) => {
+ return useQuery({
+ queryKey: ["getProducts", page, pageSize, orderBy],
+ queryFn: () => getProducts({ page, orderBy, pageSize }),
+ staleTime: 300000,
+ select: (response) => response.data,
+ });
+};
diff --git a/vite-project/src/hooks/useResponsivePageSize.js b/vite-project/src/hooks/useResponsivePageSize.js
index a0a6ea14..bf989cbe 100644
--- a/vite-project/src/hooks/useResponsivePageSize.js
+++ b/vite-project/src/hooks/useResponsivePageSize.js
@@ -5,11 +5,20 @@ export const useResponsivePageSize = () => {
const isMobile = useMediaQuery({ maxWidth: 767 });
if (isMobile) {
- return [1, 4]; // [베스트상품 pageSize, 전체상품 pageSize]
+ return {
+ bestProductPageSize: 1,
+ allProductPageSize: 4,
+ };
}
if (isTablet) {
- return [2, 6];
+ return {
+ bestProductPageSize: 2,
+ allProductPageSize: 6,
+ };
}
- return [4, 10];
+ return {
+ bestProductPageSize: 4,
+ allProductPageSize: 10,
+ };
};
diff --git a/vite-project/src/layout/Layout.jsx b/vite-project/src/layout/Layout.jsx
index ff3bf49e..56058b81 100644
--- a/vite-project/src/layout/Layout.jsx
+++ b/vite-project/src/layout/Layout.jsx
@@ -1,6 +1,6 @@
import React from "react";
import { Outlet } from "react-router";
-import Navbar from "../common/Navbar/Navbar";
+import Navbar from "../common/Navbar";
const Layout = () => {
return (
diff --git a/vite-project/src/main.jsx b/vite-project/src/main.jsx
index e50388ce..4025443e 100644
--- a/vite-project/src/main.jsx
+++ b/vite-project/src/main.jsx
@@ -1,9 +1,14 @@
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App.jsx";
+const queryClient = new QueryClient();
+
createRoot(document.getElementById("root")).render(
-
-
-
+
+
+
+
+
);
diff --git a/vite-project/src/pages/AddItem-Page/AddItemPage.jsx b/vite-project/src/pages/AddItem-Page/AddItemPage.jsx
deleted file mode 100644
index c2069567..00000000
--- a/vite-project/src/pages/AddItem-Page/AddItemPage.jsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from "react";
-
-const AddItemPage = () => {
- return AddItemPage
;
-};
-
-export default AddItemPage;
diff --git a/vite-project/src/pages/AddItemPage/AddItemPage.style.css b/vite-project/src/pages/AddItemPage/AddItemPage.style.css
new file mode 100644
index 00000000..dc13d896
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/AddItemPage.style.css
@@ -0,0 +1,96 @@
+.addItem-page-layout {
+ margin-top: 90px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding-bottom: 50px;
+}
+
+.addItem-form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.addItem-form-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.addItem-form-header-title {
+ font-weight: 700;
+ font-size: 20px;
+ color: var(--gray800);
+}
+
+.addItem-form-submit-btn {
+ background-color: var(--primary100);
+}
+
+.addItem-form-submit-btn:disabled {
+ background-color: var(--gray400);
+}
+
+.addItem-input,
+.addItem-textArea,
+.addItem-image-input {
+ background-color: var(--gray100);
+ padding: 16px 24px;
+ border-radius: 12px;
+ border: none;
+ font-weight: 400;
+ font-size: 16px;
+ color: #1f2937;
+}
+
+.addItem-input,
+.addItem-textArea {
+ width: 100%;
+}
+
+.addItem-image-input {
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ color: var(--gray400);
+}
+
+.addItem-image-input,
+.addItem-image-card {
+ aspect-ratio: 1;
+ width: 282px;
+}
+
+.addItem-textArea {
+ height: 282px;
+ resize: none;
+}
+
+.addItem-image-area {
+ display: flex;
+ gap: 16px;
+}
+
+.addItem-image-card {
+ position: relative;
+}
+
+.addItem-image-card > button {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+}
+
+@media screen and (max-width: 767px) {
+ .addItem-image-input,
+ .addItem-image-card {
+ width: 160px;
+ }
+ .addItem-image-area {
+ gap: 10px;
+ }
+}
diff --git a/vite-project/src/pages/AddItemPage/components/AddItemDescription/index.jsx b/vite-project/src/pages/AddItemPage/components/AddItemDescription/index.jsx
new file mode 100644
index 00000000..d7a0ec23
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/components/AddItemDescription/index.jsx
@@ -0,0 +1,16 @@
+const AddItemDescription = ({ value, onChange }) => {
+ return (
+
+
상품 소개
+
+
+ );
+};
+
+export default AddItemDescription;
diff --git a/vite-project/src/pages/AddItemPage/components/AddItemFormHeader/index.jsx b/vite-project/src/pages/AddItemPage/components/AddItemFormHeader/index.jsx
new file mode 100644
index 00000000..53e28e2a
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/components/AddItemFormHeader/index.jsx
@@ -0,0 +1,22 @@
+const AddItemFormHeader = ({ formData }) => {
+ const isDisabled =
+ formData.name &&
+ formData.price &&
+ formData.description &&
+ formData.tags.length > 0;
+
+ return (
+
+
상품 등록하기
+
+
+ );
+};
+
+export default AddItemFormHeader;
diff --git a/vite-project/src/pages/AddItemPage/components/AddItemImage/index.jsx b/vite-project/src/pages/AddItemPage/components/AddItemImage/index.jsx
new file mode 100644
index 00000000..d3e87556
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/components/AddItemImage/index.jsx
@@ -0,0 +1,38 @@
+import ErrorMessage from "../../../../common/ErrorMessage";
+
+const AddItemImage = ({ ref, error, image, onChange, onDelete }) => {
+ return (
+
+
상품 이미지
+
+
+
+ {image && (
+
+
+

+
+ )}
+
+ {error &&
}
+
+ );
+};
+
+export default AddItemImage;
diff --git a/vite-project/src/pages/AddItemPage/components/AddItemName/index.jsx b/vite-project/src/pages/AddItemPage/components/AddItemName/index.jsx
new file mode 100644
index 00000000..e91ad2b7
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/components/AddItemName/index.jsx
@@ -0,0 +1,16 @@
+const AddItemName = ({ value, onChange }) => {
+ return (
+
+
상품명
+
+
+ );
+};
+
+export default AddItemName;
diff --git a/vite-project/src/pages/AddItemPage/components/AddItemPrice/index.jsx b/vite-project/src/pages/AddItemPage/components/AddItemPrice/index.jsx
new file mode 100644
index 00000000..338e7ae6
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/components/AddItemPrice/index.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+
+const AddItemPrice = ({ value, onChange }) => {
+ return (
+
+
판매가격
+
+
+ );
+};
+
+export default AddItemPrice;
diff --git a/vite-project/src/pages/AddItemPage/components/AddItemTag/index.jsx b/vite-project/src/pages/AddItemPage/components/AddItemTag/index.jsx
new file mode 100644
index 00000000..5c640ace
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/components/AddItemTag/index.jsx
@@ -0,0 +1,19 @@
+import ErrorMessage from "../../../../common/ErrorMessage";
+
+const AddItemTag = ({ value, onChange, error }) => {
+ return (
+
+
태그
+
+ {error && }
+
+ );
+};
+
+export default AddItemTag;
diff --git a/vite-project/src/pages/AddItemPage/components/AddItemTagList/index.jsx b/vite-project/src/pages/AddItemPage/components/AddItemTagList/index.jsx
new file mode 100644
index 00000000..e9f81799
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/components/AddItemTagList/index.jsx
@@ -0,0 +1,9 @@
+import TagBadge from "../../../../common/TagBadge";
+
+const AddItemTagList = ({ tags }) => {
+ return (
+
+ );
+};
+
+export default AddItemTagList;
diff --git a/vite-project/src/pages/AddItemPage/index.jsx b/vite-project/src/pages/AddItemPage/index.jsx
new file mode 100644
index 00000000..3dff4efa
--- /dev/null
+++ b/vite-project/src/pages/AddItemPage/index.jsx
@@ -0,0 +1,121 @@
+import { useRef, useState } from "react";
+import "./AddItemPage.style.css";
+import AddItemDescription from "./components/AddItemDescription";
+import AddItemFormHeader from "./components/AddItemFormHeader";
+import AddItemImage from "./components/AddItemImage";
+import AddItemName from "./components/AddItemName";
+import AddItemPrice from "./components/AddItemPrice";
+import AddItemTag from "./components/AddItemTag";
+import TagList from "../../common/TagList";
+
+const AddItemPage = () => {
+ const [formData, setFormData] = useState({
+ images: [], // required 지만 빈 배열이어도 되는듯
+ name: "", // required
+ description: "", // required
+ price: "", // required
+ tags: [], // required
+ });
+
+ const [inputValueTag, setInputValueTag] = useState("");
+ const [previewImage, setPreviewImage] = useState("");
+ const [errors, setErrors] = useState({ image: "", tag: "" }); // POST요청 에러까지 추후에 들어갈지도
+
+ const fileInputRef = useRef(null);
+
+ const handleChangeImage = (e) => {
+ if (previewImage) {
+ setErrors((prev) => ({
+ ...prev,
+ image: "이미지 파일은 1개만 등록할 수 있습니다",
+ }));
+ return;
+ }
+ const file = e.target.files[0];
+ const url = URL.createObjectURL(file);
+ setPreviewImage(url);
+ const newImages = [url];
+ setFormData((prev) => ({ ...prev, images: newImages }));
+ };
+
+ const handleDeleteImage = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ setPreviewImage("");
+ setFormData((prev) => ({ ...prev, images: [] }));
+ setErrors((prev) => ({ ...prev, image: "" }));
+ };
+
+ const handleChange = (e, category) => {
+ setFormData((prev) => ({ ...prev, [category]: e.target.value }));
+ };
+
+ const handleSubmitAddItem = (e) => {
+ e.preventDefault();
+ console.log(formData);
+ };
+
+ const handleSubmitTag = (e) => {
+ e.preventDefault();
+
+ if (!inputValueTag) {
+ setErrors((prev) => ({ ...prev, tag: "태그를 입력하세요" }));
+ return;
+ }
+
+ if (formData.tags.includes(inputValueTag)) {
+ setErrors((prev) => ({ ...prev, tag: "태그가 이미 존재합니다" }));
+ return;
+ }
+
+ const newTags = [...formData.tags, inputValueTag];
+ setFormData((prev) => ({ ...prev, tags: newTags }));
+ setInputValueTag("");
+ setErrors((prev) => ({ ...prev, tag: "" }));
+ };
+
+ const handleDeleteTag = (index) => {
+ const newTags = formData.tags.filter((_, i) => i !== index);
+ setFormData((prev) => ({ ...prev, tags: newTags }));
+ };
+
+ return (
+
+ );
+};
+
+export default AddItemPage;
diff --git a/vite-project/src/pages/FreeBoard-Page/FreeBoard.jsx b/vite-project/src/pages/FreeBoardPage/index.jsx
similarity index 100%
rename from vite-project/src/pages/FreeBoard-Page/FreeBoard.jsx
rename to vite-project/src/pages/FreeBoardPage/index.jsx
diff --git a/vite-project/src/pages/Items-Page/components/ProductAll/index.jsx b/vite-project/src/pages/Items-Page/components/ProductAll/index.jsx
deleted file mode 100644
index 437dd034..00000000
--- a/vite-project/src/pages/Items-Page/components/ProductAll/index.jsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router";
-import { useGetProduct } from "../../../../hooks/useGetProduct";
-import DropDown from "../../../../common/Dropdown/DropDown";
-import Pagination from "../../../../common/Pagination/Pagination";
-import ProductList from "../../../../common/Product-Card/ProductList";
-import { useResponsivePageSize } from "../../../../hooks/useResponsivePageSize";
-import { useMediaQuery } from "react-responsive";
-
-const DROPDOWN_MENUS = {
- 최신순: "recent",
- 좋아요순: "favorite",
-};
-
-const GROUP_SIZE = 5;
-
-const ProductAll = () => {
- const navigate = useNavigate();
- const [allProducts, setAllProducts] = useState([]);
- const [orderBy, setOrderBy] = useState("최신순");
- const [page, setPage] = useState(1);
- const [totalCount, setTotalCount] = useState(0);
- const [isOpenDropdown, setIsOpenDropdown] = useState(false);
- const [_, pageSize] = useResponsivePageSize();
- const menus = Object.keys(DROPDOWN_MENUS);
-
- const isMobile = useMediaQuery({ maxWidth: 767 });
-
- const onClickMenu = (orderBy) => {
- setOrderBy(orderBy);
- setIsOpenDropdown(false);
- };
-
- useEffect(() => {
- const fetchData = async () => {
- const [response, totalCount] = await useGetProduct({
- orderBy: DROPDOWN_MENUS[orderBy],
- pageSize,
- page,
- });
- setAllProducts(response);
- setTotalCount(totalCount);
- };
- fetchData();
- console.log("data", allProducts);
- }, [orderBy, page, pageSize]);
- return (
- <>
-
-
전체 상품
-
-
-
-
-
-
- setIsOpenDropdown(!isOpenDropdown)}>
- {isMobile ? (
-
- ) : (
- <>
- {orderBy}
-
- >
- )}
-
-
-
- {menus.map((orderBy, index) => (
- -
-
-
- ))}{" "}
-
-
-
-
-
-
- >
- );
-};
-
-export default ProductAll;
diff --git a/vite-project/src/pages/Items-Page/components/ProductBest/index.jsx b/vite-project/src/pages/Items-Page/components/ProductBest/index.jsx
deleted file mode 100644
index f32bca52..00000000
--- a/vite-project/src/pages/Items-Page/components/ProductBest/index.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { useGetProduct } from "../../../../hooks/useGetProduct";
-import ProductList from "../../../../common/Product-Card/ProductList";
-import { useResponsivePageSize } from "../../../../hooks/useResponsivePageSize";
-
-const ProductBest = () => {
- const [bestProducts, setBestProducts] = useState([]);
- const [pageSize, _] = useResponsivePageSize();
-
- useEffect(() => {
- const fetchData = async () => {
- const [response, totalCount] = await useGetProduct({
- orderBy: "favorite",
- pageSize,
- });
- setBestProducts(response);
- };
- fetchData();
- }, [pageSize]);
-
- return (
-
- );
-};
-
-export default ProductBest;
diff --git a/vite-project/src/pages/Items-Page/ItemsPage.style.css b/vite-project/src/pages/ItemsPage/ItemsPage.style.css
similarity index 95%
rename from vite-project/src/pages/Items-Page/ItemsPage.style.css
rename to vite-project/src/pages/ItemsPage/ItemsPage.style.css
index 7e2e0a34..d08794c0 100644
--- a/vite-project/src/pages/Items-Page/ItemsPage.style.css
+++ b/vite-project/src/pages/ItemsPage/ItemsPage.style.css
@@ -26,12 +26,6 @@
.product-all-menus-btn {
background-color: var(--primary100);
- color: white;
- padding: 12px 23px;
- font-weight: 600;
- font-size: 16px;
- height: 42px;
- border-radius: 8px;
}
.product-search-form {
diff --git a/vite-project/src/pages/ItemsPage/components/ProductAll/ProductAllMenuBar.jsx b/vite-project/src/pages/ItemsPage/components/ProductAll/ProductAllMenuBar.jsx
new file mode 100644
index 00000000..074edae1
--- /dev/null
+++ b/vite-project/src/pages/ItemsPage/components/ProductAll/ProductAllMenuBar.jsx
@@ -0,0 +1,79 @@
+import { useNavigate } from "react-router";
+import DropDown from "../../../../common/Dropdown";
+import { useMediaQuery } from "react-responsive";
+import { ORDER_BYS } from "../../../../constants/PRODUCTS";
+
+const ProductAllMenuBar = ({
+ isOpenDropdown,
+ setIsOpenDropdown,
+ onClickMenu,
+ orderBy,
+}) => {
+ const navigate = useNavigate();
+ const isMobile = useMediaQuery({ maxWidth: 767 });
+ const DROPDOWN_MENUS = Object.keys(ORDER_BYS);
+ const selectedDropdownValue = DROPDOWN_MENUS.find(
+ (key) => ORDER_BYS[key] === orderBy
+ );
+
+ return (
+
+
전체 상품
+
+
+
+
+
+
+ setIsOpenDropdown(!isOpenDropdown)}>
+ {isMobile ? (
+
+ ) : (
+ <>
+ {selectedDropdownValue}
+
+ >
+ )}
+
+
+
+ {DROPDOWN_MENUS.map((menu, index) => (
+ -
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default ProductAllMenuBar;
diff --git a/vite-project/src/pages/ItemsPage/components/ProductAll/index.jsx b/vite-project/src/pages/ItemsPage/components/ProductAll/index.jsx
new file mode 100644
index 00000000..20222012
--- /dev/null
+++ b/vite-project/src/pages/ItemsPage/components/ProductAll/index.jsx
@@ -0,0 +1,57 @@
+import { useState } from "react";
+import Pagination from "../../../../common/Pagination";
+import ProductList from "../../../../common/ProductList";
+import ProductAllMenuBar from "./ProductAllMenuBar";
+import { useResponsivePageSize } from "../../../../hooks/useResponsivePageSize";
+import { useGetProductsQuery } from "../../../../hooks/useGetProducts";
+import { ORDER_BYS, GROUP_SIZE } from "../../../../constants/PRODUCTS";
+import LoadingSpinner from "../../../../common/LoadingSpinner";
+import ErrorMessage from "../../../../common/ErrorMessage";
+
+const ProductAll = () => {
+ const [orderBy, setOrderBy] = useState(ORDER_BYS["최신순"]);
+ const [page, setPage] = useState(1);
+ const [isOpenDropdown, setIsOpenDropdown] = useState(false);
+ const { _, allProductPageSize } = useResponsivePageSize();
+
+ const {
+ data: allProducts,
+ isLoading,
+ isError,
+ error,
+ } = useGetProductsQuery({
+ page,
+ orderBy,
+ pageSize: allProductPageSize,
+ });
+
+ const totalCount = allProducts?.totalCount;
+
+ const onClickMenu = (menu) => {
+ setOrderBy(menu);
+ setIsOpenDropdown(false);
+ };
+
+ if (isLoading) return ;
+
+ if (isError) return ;
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default ProductAll;
diff --git a/vite-project/src/pages/ItemsPage/components/ProductBest/index.jsx b/vite-project/src/pages/ItemsPage/components/ProductBest/index.jsx
new file mode 100644
index 00000000..d53f1f4e
--- /dev/null
+++ b/vite-project/src/pages/ItemsPage/components/ProductBest/index.jsx
@@ -0,0 +1,30 @@
+import ProductList from "../../../../common/ProductList";
+import { useResponsivePageSize } from "../../../../hooks/useResponsivePageSize";
+import { useGetProductsQuery } from "../../../../hooks/useGetProducts";
+import LoadingSpinner from "../../../../common/LoadingSpinner";
+import ErrorMessage from "../../../../common/ErrorMessage";
+
+const ProductBest = () => {
+ const { bestProductPageSize, _ } = useResponsivePageSize();
+ const {
+ data: bestProducts,
+ isLoading,
+ isError,
+ error,
+ } = useGetProductsQuery({
+ orderBy: "favorite",
+ pageSize: bestProductPageSize,
+ });
+
+ if (isLoading) return ;
+
+ if (isError) return ;
+ return (
+
+ );
+};
+
+export default ProductBest;
diff --git a/vite-project/src/pages/Items-Page/ItemsPage.jsx b/vite-project/src/pages/ItemsPage/index.jsx
similarity index 100%
rename from vite-project/src/pages/Items-Page/ItemsPage.jsx
rename to vite-project/src/pages/ItemsPage/index.jsx