diff --git a/.gitignore b/.gitignore
index 4d29575d..d600b6c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,23 +1,25 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
-# production
-/build
+node_modules
+dist
+dist-ssr
+*.local
-# misc
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
.DS_Store
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
diff --git a/README.md b/README.md
index 58beeacc..b6e9958b 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
@@ -68,3 +81,5 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/d
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
+
+> > > > > > > be83c09a32b62eaaad6a248020d263bc5717e59e
diff --git a/package-lock.json b/package-lock.json
index 8178319b..933e9713 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,8 @@
"classnames": "^2.5.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-error-boundary": "^5.0.0",
+ "react-hook-form": "^7.55.0",
"react-router-dom": "^7.2.0"
},
"devDependencies": {
@@ -279,6 +281,18 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
+ "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
@@ -3255,6 +3269,34 @@
"react": "^19.0.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz",
+ "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.55.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz",
+ "integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -3328,6 +3370,12 @@
"node": ">=8.10.0"
}
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
diff --git a/package.json b/package.json
index 33c1fdcf..b07b2d2b 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,8 @@
"classnames": "^2.5.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-error-boundary": "^5.0.0",
+ "react-hook-form": "^7.55.0",
"react-router-dom": "^7.2.0"
},
"devDependencies": {
@@ -28,6 +30,5 @@
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"vite": "^6.2.0"
-
}
}
diff --git a/src/App.jsx b/src/App.jsx
index 9b6ddd00..d2d2b8fe 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -5,7 +5,9 @@ import ItemsPage from "./pages/ItemsPage";
import FaqPage from "./pages/FaqPage";
import PrivacyPage from "./pages/PrivacyPage";
import SignupPage from "./pages/SignupPage";
+import AddItemPage from "./pages/AddItemPage";
import ErrorPage from "./pages/ErrorPage";
+import BoardsPage from "./pages/BoardsPage";
function App() {
return (
@@ -15,8 +17,10 @@ function App() {
} />
} />
} />
+ } />
} />
} />
+ } />
} />
diff --git a/src/Components/Additem/FormField.jsx b/src/Components/Additem/FormField.jsx
new file mode 100644
index 00000000..c2857925
--- /dev/null
+++ b/src/Components/Additem/FormField.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+import FormInput from "../Common/FormInput";
+
+const FormField = ({ label, id, type = "text", placeholder, register }) => {
+ return (
+
+
+
+
+ );
+};
+
+export default FormField;
diff --git a/src/Components/Additem/FormHeader.jsx b/src/Components/Additem/FormHeader.jsx
new file mode 100644
index 00000000..53e6e077
--- /dev/null
+++ b/src/Components/Additem/FormHeader.jsx
@@ -0,0 +1,20 @@
+const FormHeader = ({ title, isSubmitEnabled }) => {
+ return (
+
+ );
+};
+
+export default FormHeader;
diff --git a/src/Components/Additem/ImageUploader.jsx b/src/Components/Additem/ImageUploader.jsx
new file mode 100644
index 00000000..3f7cb5d2
--- /dev/null
+++ b/src/Components/Additem/ImageUploader.jsx
@@ -0,0 +1,77 @@
+import addItem from "../../assets/add-item.png";
+import xIcon from "../../assets/x-icon.png";
+
+import FormInput from "../Common/FormInput";
+
+const ImageUploader = ({
+ previewUrl,
+ setPreviewUrl,
+ showWarning,
+ setShowWarning,
+ imageInputRef,
+}) => {
+ const handleUploadClick = () => {
+ imageInputRef.current.click();
+ };
+
+ const handleFileChange = (e) => {
+ const file = e.target.files[0];
+ if (previewUrl) {
+ setShowWarning(true);
+ } else if (file) {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setPreviewUrl(reader.result);
+ setShowWarning(false);
+ };
+ reader.readAsDataURL(file);
+ }
+ e.target.value = "";
+ };
+ return (
+
+
+ 상품 이미지
+
+
+
+
+
+ {previewUrl && (
+ <>
+

+

setPreviewUrl("")}
+ />
+ >
+ )}
+
+
+ {showWarning && (
+
+ *이미지 등록은 최대 1개까지 가능합니다.
+
+ )}
+
+ );
+};
+
+export default ImageUploader;
diff --git a/src/Components/Additem/TagInput.jsx b/src/Components/Additem/TagInput.jsx
new file mode 100644
index 00000000..33cb6621
--- /dev/null
+++ b/src/Components/Additem/TagInput.jsx
@@ -0,0 +1,56 @@
+import { useState } from "react";
+
+const TagInput = ({ tags, setTags }) => {
+ const [tagInput, setTagInput] = useState("");
+
+ const handleKeyDown = (e) => {
+ if (e.nativeEvent.isComposing) return;
+
+ if (e.key === "Enter") {
+ e.preventDefault();
+ const trimmed = tagInput.trim();
+ if (trimmed && !tags.includes(trimmed)) {
+ setTags([...tags, trimmed]);
+ }
+ setTagInput("");
+ }
+ };
+ const removeTag = (index) => {
+ setTags(tags.filter((_, i) => i !== index));
+ };
+ return (
+ <>
+
+ setTagInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="태그를 입력해주세요"
+ className="input-primary"
+ />
+
+
+ {tags.map((tag, i) => (
+
+ #{tag}
+
+
+ ))}
+
+ >
+ );
+};
+
+export default TagInput;
diff --git a/src/Components/Common/ErrorMessage.jsx b/src/Components/Common/ErrorMessage.jsx
index 5bf6a81c..34eecc67 100644
--- a/src/Components/Common/ErrorMessage.jsx
+++ b/src/Components/Common/ErrorMessage.jsx
@@ -1,19 +1,38 @@
const ErrorMessage = ({
- message = "데이터를 불러오는데 실패했어요 😥",
+ message = "데이터를 불러오는데 실패했어요 🐼",
onRetry,
+ categori,
}) => {
return (
-
-
{message}
- {onRetry && (
-
+ >
);
};
diff --git a/src/Components/Common/FormInput.jsx b/src/Components/Common/FormInput.jsx
new file mode 100644
index 00000000..3d49d549
--- /dev/null
+++ b/src/Components/Common/FormInput.jsx
@@ -0,0 +1,65 @@
+import React from "react";
+
+const FormInput = ({
+ id,
+ type = "text",
+ label,
+ placeholder,
+ onBlur,
+ error,
+ hidden = false,
+ className = "",
+ rightIcon,
+ accept,
+ ref,
+ ...rest
+}) => {
+ return (
+
+ {label && !hidden && (
+
+ )}
+
+ {type === "textarea" ? (
+
+ ) : (
+
+ )}
+ {rightIcon && (
+
+ {rightIcon}
+
+ )}
+
+ {error && !hidden && (
+
{error}
+ )}
+
+ );
+};
+
+export default FormInput;
diff --git a/src/Components/Common/Loading.jsx b/src/Components/Common/Loading.jsx
index 6c47531a..07ac216e 100644
--- a/src/Components/Common/Loading.jsx
+++ b/src/Components/Common/Loading.jsx
@@ -1,9 +1,29 @@
-const Loading = ({ message = "로딩 중입니다..." }) => {
+const Loading = ({ categori }) => {
return (
-
+ <>
+ {categori === "best" ? (
+
+
+

+
+ ) : (
+
+
+

+
+ )}
+ >
+
);
};
diff --git a/src/Components/Common/MyErrorBoundary.jsx b/src/Components/Common/MyErrorBoundary.jsx
new file mode 100644
index 00000000..33237752
--- /dev/null
+++ b/src/Components/Common/MyErrorBoundary.jsx
@@ -0,0 +1,16 @@
+import { ErrorBoundary } from "react-error-boundary";
+import ErrorMessage from "./ErrorMessage";
+
+const MyErrorBoundary = ({ children }) => {
+ return (
+ (
+
+ )}
+ >
+ {children}
+
+ );
+};
+
+export default MyErrorBoundary;
diff --git a/src/Components/Common/Nav.jsx b/src/Components/Common/Nav.jsx
index 1af672a1..5956d455 100644
--- a/src/Components/Common/Nav.jsx
+++ b/src/Components/Common/Nav.jsx
@@ -1,8 +1,12 @@
import pandaFace from "../../assets/panda-face.png";
import pandaFaceLogo from "../../assets/panda-logo.png";
-import { Link } from "react-router-dom";
+import { Link, useLocation } from "react-router-dom";
const Nav = ({ darkMode }) => {
+ const location = useLocation();
+ const isItemsPage = location.pathname === "/items";
+ const isBoardsPage = location.pathname === "/boards";
+ const isAddItemPage = location.pathname === "/additem";
return (