diff --git a/src/pages/items/components/ProductItem/styles.module.scss b/src/pages/items/components/ProductItem.module.scss
similarity index 93%
rename from src/pages/items/components/ProductItem/styles.module.scss
rename to src/pages/items/components/ProductItem.module.scss
index 91c618015..7e567a41b 100644
--- a/src/pages/items/components/ProductItem/styles.module.scss
+++ b/src/pages/items/components/ProductItem.module.scss
@@ -1,4 +1,8 @@
.item {
+ .thumbnail {
+ margin-bottom: 1.6rem;
+ }
+
&:hover {
figure {
transform: translateY(-4px);
diff --git a/src/pages/items/components/Product/index.jsx b/src/pages/items/components/ProductList.jsx
similarity index 84%
rename from src/pages/items/components/Product/index.jsx
rename to src/pages/items/components/ProductList.jsx
index 29564f4a6..d247e0b3e 100644
--- a/src/pages/items/components/Product/index.jsx
+++ b/src/pages/items/components/ProductList.jsx
@@ -1,7 +1,7 @@
import clsx from "clsx";
-import List from "@components/List";
-import ProductItem from "../ProductItem";
-import styles from "./styles.module.scss";
+import { List } from "@components/List";
+import ProductItem from "./ProductItem";
+import styles from "./ProductList.module.scss";
export default function ProductList({
items,
diff --git a/src/pages/items/components/Product/styles.module.scss b/src/pages/items/components/ProductList.module.scss
similarity index 100%
rename from src/pages/items/components/Product/styles.module.scss
rename to src/pages/items/components/ProductList.module.scss
diff --git a/src/pages/items/components/ProductThumbnail/index.jsx b/src/pages/items/components/ProductThumbnail/index.jsx
deleted file mode 100644
index f2e9a303c..000000000
--- a/src/pages/items/components/ProductThumbnail/index.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useState } from "react";
-import clsx from "clsx";
-import defaultImg from "@assets/img/icon/icon_placeholder.svg";
-import styles from "./styles.module.scss";
-
-export default function ProductThumbnail({
- src: initialSrc = defaultImg,
- alt = "",
-}) {
- const [src, setSrc] = useState(initialSrc);
- const [isLoaded, setIsLoaded] = useState(false);
-
- let imgCss = clsx({
- [styles.default]: src === defaultImg,
- [styles.loaded]: isLoaded,
- });
-
- return (
-
- );
-}
diff --git a/src/pages/items/components/ProductThumbnail/styles.module.scss b/src/pages/items/components/ProductThumbnail/styles.module.scss
deleted file mode 100644
index 46b202615..000000000
--- a/src/pages/items/components/ProductThumbnail/styles.module.scss
+++ /dev/null
@@ -1,41 +0,0 @@
-.cover {
- overflow: hidden;
- position: relative;
- display: block;
- margin-bottom: 1.6rem;
- border-radius: 1.6rem;
- background: var(--color-secondary-100);
- transition: all 0.2s;
-
- &:before {
- content: "";
- display: block;
- width: 100%;
- height: 0;
- padding-bottom: 100%;
- }
-
- img {
- position: absolute;
- left: 0;
- top: 0;
- max-width: 100%;
- width: 100%;
- height: 100%;
- object-fit: cover;
- opacity: 0;
- transition: opacity 0.2s;
-
- &.default {
- max-width: 11.6rem;
- height: auto;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- }
-
- &.loaded {
- opacity: 1;
- }
- }
-}
diff --git a/src/pages/items/components/schema.js b/src/pages/items/components/schema.js
new file mode 100644
index 000000000..0246e2ba0
--- /dev/null
+++ b/src/pages/items/components/schema.js
@@ -0,0 +1,50 @@
+import { PRODUCT_VALIDATION_MESSAGE as MESSAGE } from "@util/validation";
+
+export const addItemSchema = {
+ images: {
+ value: undefined,
+ rule: {
+ required: MESSAGE.PRODUCT_IMAGE_REQUIRED,
+ },
+ },
+ tags: {
+ value: [],
+ rule: {
+ required: MESSAGE.PRODUCT_TAGS_REQUIRED,
+ patterns: [
+ {
+ regex: /^[^!@#$%^&*(),.?":{}|<>_\-+=\[\]\\\/`~';]*$/,
+ message: MESSAGE.INVALID_STRING,
+ },
+ ],
+ },
+ },
+ name: {
+ value: "",
+ rule: {
+ required: MESSAGE.PRODUCT_NAME_REQUIRED,
+ },
+ },
+ description: {
+ value: "",
+ rule: {
+ required: MESSAGE.PRODUCT_DESCRIPTION_REQUIRED,
+ },
+ },
+ price: {
+ value: undefined,
+ rule: {
+ required: MESSAGE.PRODUCT_PRICE_REQUIRED,
+ patterns: [
+ {
+ regex: /^[0-9]*$/,
+ message: MESSAGE.INVALID_NUMBER,
+ },
+ ],
+ custom: {
+ validate: (value) => value >= 100,
+ message: "최소 100원이상 작성해주세요.",
+ },
+ },
+ },
+};
diff --git a/src/pages/items/components/useRecentSearch.js b/src/pages/items/components/useRecentSearch.js
new file mode 100644
index 000000000..85c8dd5ae
--- /dev/null
+++ b/src/pages/items/components/useRecentSearch.js
@@ -0,0 +1,64 @@
+import { useState } from "react";
+import useLocalStorage from "@hooks/useLocalStorage";
+
+const RECENT_SEARCH_SIZE = 5;
+
+export default function useRecentSearch({ initialKeyword, onChange }) {
+ const [searchInput, setSearchInput] = useState(initialKeyword || "");
+ const [recentSearh, setRecentSearch] = useLocalStorage("keyword", []);
+
+ // 검색창 핸들러 (submit, change, clear)
+ function handleSearchSubmit() {
+ if (searchInput.trim()) {
+ setRecentSearch((prev) =>
+ [
+ searchInput,
+ ...prev.filter((keyword) => keyword !== searchInput),
+ ].slice(0, RECENT_SEARCH_SIZE)
+ );
+ }
+ //검색인풋 초기화가 좋은 UX일까? (검색취소 버튼이 나오도록, 검색어가 떠있는게 좋은것 같음)
+ //setSearchInput("");
+ onChange(searchInput);
+ }
+
+ function handleSearchChange(e) {
+ setSearchInput(e.target.value);
+ }
+
+ function handleSearchClear() {
+ setSearchInput("");
+ onChange("");
+ }
+
+ // 최근검색 이벤트 핸들러 (click, remove, clear)
+ function handleRecentSearchClick(value) {
+ setRecentSearch((prev) =>
+ [value, ...prev.filter((keyword) => keyword !== value)].slice(
+ 0,
+ RECENT_SEARCH_SIZE
+ )
+ );
+ setSearchInput(value);
+ onChange(value);
+ }
+
+ function handleRecentSearchRemove(value) {
+ setRecentSearch((prev) => prev.filter((keyword) => keyword !== value));
+ }
+
+ function handleRecentSearchClear() {
+ setRecentSearch([]);
+ }
+
+ return {
+ searchInput,
+ recentSearh,
+ handleSearchSubmit,
+ handleSearchChange,
+ handleSearchClear,
+ handleRecentSearchClick,
+ handleRecentSearchRemove,
+ handleRecentSearchClear,
+ };
+}
diff --git a/src/pages/items/detail/index.jsx b/src/pages/items/detail/index.jsx
deleted file mode 100644
index a11145919..000000000
--- a/src/pages/items/detail/index.jsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import Temporary from "@components/Temp";
-
-export default function ItemDetail() {
- return
;
-}
diff --git a/src/pages/items/index.jsx b/src/pages/items/index.jsx
deleted file mode 100644
index e671f93d5..000000000
--- a/src/pages/items/index.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import Container from "@components/Container";
-import BestItems from "./components/BestItems";
-import AllItems from "./components/AllItems";
-
-export default function Items() {
- return (
-
-
-
-
- );
-}
diff --git a/src/pages/landing/index.jsx b/src/pages/landing/LandingPage.jsx
similarity index 55%
rename from src/pages/landing/index.jsx
rename to src/pages/landing/LandingPage.jsx
index 3023a261d..ae5ac3835 100644
--- a/src/pages/landing/index.jsx
+++ b/src/pages/landing/LandingPage.jsx
@@ -1,12 +1,12 @@
-import Banner from "./components/LandingBanner";
-import Feature from "./components/LandingFeature";
+import Banner from "./components/Banner";
+import Feature from "./components/Feature";
import {
heroBannerData,
footerBannerData,
featureList,
-} from "./landingContents";
+} from "./components/landingContents";
-export default function Landing() {
+export default function LandingPage() {
return (
<>
diff --git a/src/pages/landing/components/LandingBanner/index.jsx b/src/pages/landing/components/Banner.jsx
similarity index 88%
rename from src/pages/landing/components/LandingBanner/index.jsx
rename to src/pages/landing/components/Banner.jsx
index f10992159..53d37ee08 100644
--- a/src/pages/landing/components/LandingBanner/index.jsx
+++ b/src/pages/landing/components/Banner.jsx
@@ -1,5 +1,5 @@
-import Button from "@components/Button";
-import styles from "./styles.module.scss";
+import { Button } from "@components/ui";
+import styles from "./Banner.module.scss";
export default function Banner({
title,
diff --git a/src/pages/landing/components/LandingBanner/styles.module.scss b/src/pages/landing/components/Banner.module.scss
similarity index 100%
rename from src/pages/landing/components/LandingBanner/styles.module.scss
rename to src/pages/landing/components/Banner.module.scss
diff --git a/src/pages/landing/components/LandingFeature/index.jsx b/src/pages/landing/components/Feature.jsx
similarity index 96%
rename from src/pages/landing/components/LandingFeature/index.jsx
rename to src/pages/landing/components/Feature.jsx
index 637702b05..86c21fa14 100644
--- a/src/pages/landing/components/LandingFeature/index.jsx
+++ b/src/pages/landing/components/Feature.jsx
@@ -1,4 +1,4 @@
-import styles from "./styles.module.scss";
+import styles from "./Feature.module.scss";
export default function Feature({ list }) {
return (
diff --git a/src/pages/landing/components/LandingFeature/styles.module.scss b/src/pages/landing/components/Feature.module.scss
similarity index 100%
rename from src/pages/landing/components/LandingFeature/styles.module.scss
rename to src/pages/landing/components/Feature.module.scss
diff --git a/src/pages/landing/landingContents.js b/src/pages/landing/components/landingContents.js
similarity index 100%
rename from src/pages/landing/landingContents.js
rename to src/pages/landing/components/landingContents.js
diff --git a/src/router.jsx b/src/router.jsx
index 90991d191..9d1f89954 100644
--- a/src/router.jsx
+++ b/src/router.jsx
@@ -1,71 +1,87 @@
import { createBrowserRouter } from "react-router-dom";
-import Layout from "./components/Layout";
import App from "./App";
-import Landing from "./pages/landing";
-import Login from "./pages/auth/login";
-import Signup from "./pages/auth/signup";
-import Privacy from "./pages/privacy";
-import Faq from "./pages/faq";
-import Items from "./pages/items";
-import ItemDetail from "./pages/items/detail";
-import ItemAdd from "./pages/items/add";
-import Boards from "./pages/boards";
-import ErrorPage from "./pages/error";
+import { Layout } from "@components/Layout";
+import ProtectedRoute from "@components/routing/ProtectedRoute";
+import ErrorPage from "./pages/error/ErrorPage";
+import LandingPage from "./pages/landing/LandingPage";
+import LoginPage from "./pages/auth/LoginPage";
+import SignupPage from "./pages/auth/SignupPage";
+import PrivacyPage from "./pages/etc/PrivacyPage";
+import FaqPage from "./pages/etc/FaqPage";
+import ItemsPage from "./pages/items/ItemsPage";
+import ItemDetailPage from "./pages/items/ItemDetailPage";
+import ItemAddPage from "./pages/items/ItemAddPage";
+import BoardPage from "./pages/boards/BoardPage";
-export const router = createBrowserRouter([
+export const router = createBrowserRouter(
+ [
+ {
+ path: "/",
+ errorElement:
,
+ children: [
+ {
+ element:
,
+ children: [
+ {
+ index: true,
+ element:
,
+ },
+ ],
+ },
+ {
+ children: [
+ {
+ path: "login",
+ element:
,
+ },
+ {
+ path: "signup",
+ element:
,
+ },
+ {
+ path: "privacy",
+ element:
,
+ },
+ {
+ path: "faq",
+ element:
,
+ },
+ ],
+ },
+ {
+ element:
,
+ children: [
+ {
+ path: "items",
+ children: [
+ { index: true, element:
},
+ { path: ":id", element:
},
+ ],
+ },
+ {
+ path: "addItem",
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: "boards",
+ element:
,
+ },
+ ],
+ },
+ ],
+ },
+ ],
{
- element:
,
- errorElement:
,
- children: [
- {
- element:
,
- children: [
- {
- index: true,
- element:
,
- },
- ],
- },
- {
- children: [
- {
- path: "login",
- element:
,
- },
- {
- path: "signup",
- element:
,
- },
- {
- path: "privacy",
- element:
,
- },
- {
- path: "faq",
- element:
,
- },
- ],
- },
- {
- element:
,
- children: [
- {
- path: "items",
- children: [
- { index: true, element:
},
- { path: ":id", element:
},
- ],
- },
- {
- path: "addItem",
- element:
,
- },
- {
- path: "boards",
- element:
,
- },
- ],
- },
- ],
- },
-]);
+ future: {
+ v7_relativeSplatPath: true,
+ v7_fetcherPersist: true,
+ v7_normalizeFormMethod: true,
+ v7_partialHydration: true,
+ v7_skipActionErrorRevalidation: true,
+ },
+ }
+);
diff --git a/src/service/auth.js b/src/service/auth.js
index 6ff7e272a..80261be76 100644
--- a/src/service/auth.js
+++ b/src/service/auth.js
@@ -15,7 +15,10 @@ export async function login({ email, password }) {
const data = await res.json();
if (!res.ok) {
- throw new Error(data.message || "에러가 발생했습니다.");
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
}
return data;
@@ -42,7 +45,10 @@ export async function signUp({
const data = await res.json();
if (!res.ok) {
- throw new Error(data.message || "에러가 발생했습니다.");
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
}
return data;
@@ -61,7 +67,10 @@ export async function refreshAccessToken(refreshToken) {
const data = await res.json();
if (!res.ok) {
- throw new Error(data.message || "에러가 발생했습니다.");
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
}
return data;
@@ -76,9 +85,11 @@ export async function getUser(accessToken) {
const data = await res.json();
if (!res.ok) {
- throw new Error(data.message || "에러가 발생했습니다.");
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
}
return data;
- s;
}
diff --git a/src/service/product.js b/src/service/product.js
index 2bc5692f0..dc178d3d1 100644
--- a/src/service/product.js
+++ b/src/service/product.js
@@ -10,20 +10,90 @@ export async function getProducts(
const data = await res.json();
if (!res.ok) {
- throw new Error(data.message || "에러가 발생했습니다.");
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
}
return data;
}
-export async function getBestProducts({ pageSize }, { signal }) {
- const query = `page=1&pageSize=${pageSize}&orderBy=favorite`;
- const res = await fetch(`${VITE_API_URL}/products?${query}`, { signal });
+export async function uploadProductImage(formData, accessToken) {
+ const res = await fetch(`${VITE_API_URL}/images/upload`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "multipart/form-data",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ body: formData,
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
+ }
+
+ return data;
+}
+
+export async function addProduct(productData, accessToken) {
+ const res = await fetch(`${VITE_API_URL}/products`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ body: JSON.stringify(productData),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
+ }
+
+ return data;
+}
+
+export async function deleteProduct(productId, accessToken) {
+ const res = await fetch(`${VITE_API_URL}/products/${productId}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
+ }
+
+ return data;
+}
+
+export async function getProduct(productId) {
+ const res = await fetch(`${VITE_API_URL}/products/${productId}`);
const data = await res.json();
if (!res.ok) {
- throw new Error(data.message || "에러가 발생했습니다.");
+ throw {
+ status: res.status,
+ message: data.message || "에러가 발생했습니다.",
+ };
}
return data;
diff --git a/src/util/validation.js b/src/util/validation.js
index dd0e36729..b530a868e 100644
--- a/src/util/validation.js
+++ b/src/util/validation.js
@@ -1,4 +1,4 @@
-export const VALIDATION_MESSAGES = {
+export const AUTH_VALIDATION_MESSAGES = {
EMAIL_REQUIRED: "이메일을 입력해주세요",
INVALID_EMAIL: "잘못된 이메일 형식입니다.",
PASSWORD_REQUIRED: "비밀번호를 입력해주세요.",
@@ -7,8 +7,18 @@ export const VALIDATION_MESSAGES = {
USERNAME_REQUIRED: "닉네임을 입력해주세요",
};
-export const VALIDATION_REGEX = {
+export const AUTH_VALIDATION_REGEX = {
EMAIL:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i,
PASSWORD: /^.{8,}$/,
};
+
+export const PRODUCT_VALIDATION_MESSAGE = {
+ PRODUCT_IMAGE_REQUIRED: "이미지를 업로드해주세요",
+ PRODUCT_TAGS_REQUIRED: "태그를 입력해주세요",
+ PRODUCT_NAME_REQUIRED: "상품명을 입력해주세요",
+ PRODUCT_DESCRIPTION_REQUIRED: "상품소개를 입력해주세요",
+ PRODUCT_PRICE_REQUIRED: "판매 가격을 입력해주세요",
+ INVALID_STRING: "특수문자는 사용할 수 없습니다.",
+ INVALID_NUMBER: "숫자만 입력해주세요",
+};
diff --git a/vite.config.js b/vite.config.js
index 1e44f6ab2..f763edca3 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -6,7 +6,6 @@ export default defineConfig({
plugins: [react()],
resolve: {
alias: [
- { find: "@", replacement: path.resolve(__dirname, "src") },
{
find: "@components",
replacement: path.resolve(__dirname, "src/components"),