diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..526b5163
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+VITE_API_URL=https://api.example.com
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a547bf36..ba835be4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,10 @@ dist
dist-ssr
*.local
+# env
+.env
+!.env.example
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/README.md b/README.md
index df5484b2..a3dd6f7c 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,11 @@
10. React 기반 중고 마켓 페이지 구현
+### 09.28
+
+11. 제품 등록 페이지 구현
+12. express, mongo 기반 백엔드와 연결 구현
+
## 기술 스택
### 지금까지 사용 된 스택
diff --git a/package.json b/package.json
index 512d4f62..5466614c 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"dependencies": {
"clsx": "^2.1.1",
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router": "^7.9.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 07abb125..10a9bd0a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
react-dom:
specifier: ^19.1.1
version: 19.1.1(react@19.1.1)
+ react-router:
+ specifier: ^7.9.1
+ version: 7.9.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
devDependencies:
'@eslint/js':
specifier: ^9.33.0
@@ -556,6 +559,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -837,6 +844,16 @@ packages:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
+ react-router@7.9.1:
+ resolution: {integrity: sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react@19.1.1:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'}
@@ -857,6 +874,9 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -1380,6 +1400,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie@1.0.2: {}
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -1655,6 +1677,14 @@ snapshots:
react-refresh@0.17.0: {}
+ react-router@7.9.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ dependencies:
+ cookie: 1.0.2
+ react: 19.1.1
+ set-cookie-parser: 2.7.1
+ optionalDependencies:
+ react-dom: 19.1.1(react@19.1.1)
+
react@19.1.1: {}
resolve-from@4.0.0: {}
@@ -1690,6 +1720,8 @@ snapshots:
semver@6.3.1: {}
+ set-cookie-parser@2.7.1: {}
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
diff --git a/src/App.jsx b/src/App.jsx
index 28b5b850..f98be659 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,19 +1,21 @@
-import { BestItemsSection } from './pages/ItemPage/BestItemsSection';
-import { SalesItemsSection } from './pages/ItemPage/SalesItemsSection';
-import { ItemProvider } from './providers/ItemProvider';
+import { Routes, Route } from 'react-router';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
+import { LandingPage } from './pages/LandingPage';
+import { ItemsPage } from './pages/ItemsPage';
+import { RegistItemPage } from './pages/RegistItemPage';
function App() {
return (
-
+ <>
-
-
-
-
+
+ } />
+ } />
+ } />
+
-
+ >
);
}
diff --git a/src/api/ProductService.js b/src/api/ProductService.js
index 45fb0b20..e747a8f8 100644
--- a/src/api/ProductService.js
+++ b/src/api/ProductService.js
@@ -1,15 +1,41 @@
-const API_URL = "https://panda-market-api.vercel.app/products";
+const API_URL = import.meta.env.VITE_API_URL + '/products';
const DEFULT_PAGE = 1;
const DEFULT_PAGE_SIZE = 1;
const DEFULR_ORDERBY = 'recent';
-export const getProductList = async function({page = DEFULT_PAGE, pageSize = DEFULT_PAGE_SIZE, orderBy = DEFULR_ORDERBY, keyword = ""}) {
+export const getProductList = async function ({
+ page = DEFULT_PAGE,
+ pageSize = DEFULT_PAGE_SIZE,
+ orderBy = DEFULR_ORDERBY,
+ keyword = '',
+}) {
+ console.log(API_URL);
const url = `${API_URL}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&keyword=${keyword}`;
const response = await fetch(url);
if (!response.ok) {
- throw new Error(`리퀘스트 에러: ${response.status}, 에러 메시지: ${response.statusText}`);
+ throw new Error(
+ `리퀘스트 에러: ${response.status}, 에러 메시지: ${response.statusText}`,
+ );
}
const data = await response.json();
return data;
-}
\ No newline at end of file
+};
+
+export const createProduct = async function (contents) {
+ const url = `${API_URL}`;
+ const response = await fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(contents),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ if (!response.ok) {
+ throw new Error(
+ `리퀘스트 에러: ${response.status}, 에러 메시지: ${response.statusText}`,
+ );
+ }
+ const data = await response.json();
+ return data;
+};
diff --git a/src/assets/img/ic_close.svg b/src/assets/img/ic_close.svg
new file mode 100644
index 00000000..18a86e83
--- /dev/null
+++ b/src/assets/img/ic_close.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/img/img_default.svg b/src/assets/img/img_default.svg
new file mode 100644
index 00000000..1f155779
--- /dev/null
+++ b/src/assets/img/img_default.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/components/Footer/Footer.css b/src/components/Footer/Footer.css
new file mode 100644
index 00000000..ec6df647
--- /dev/null
+++ b/src/components/Footer/Footer.css
@@ -0,0 +1,74 @@
+#footer {
+ justify-content: center;
+ align-items: flex-start;
+ background: var(--secondary-900);
+ text-align: center;
+}
+
+#footer-box {
+ display: flex;
+ width: 120rem;
+ height: 10rem;
+ padding: 2rem 12.5rem;
+ justify-content: space-between;
+ align-items: flex-start;
+ text-align: center;
+}
+
+#footer-box > ul {
+ display: flex;
+ align-items: flex-start;
+}
+
+#footer-box > ul > li {
+ display: inline-block;
+}
+
+#copyright {
+ color: var(--secondary-400);
+ font-size: 1rem;
+ font-weight: 400;
+}
+
+#cs-list {
+ gap: 1.875rem;
+}
+
+#cs-list a {
+ color: var(--secondary-200);
+ font-size: 1rem;
+ font-weight: 400;
+}
+
+#sns-list {
+ gap: 0.75rem;
+}
+
+#sns-list img {
+ width: 1.25rem;
+}
+
+@media screen and (max-width: 120rem) {
+ #footer-box {
+ width: 100%;
+ }
+}
+
+@media screen and (max-width: 74.9rem) {
+ #footer-box {
+ padding: 2rem 1.5rem;
+ }
+}
+
+@media screen and (max-width: 46.4rem) {
+ #footer-box {
+ padding: 2rem 1rem;
+ flex-wrap: wrap-reverse;
+ }
+
+ #copyright {
+ width: 100%;
+ margin: 1.5rem 0 2rem;
+ text-align: start;
+ }
+}
diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx
index 06954ca9..be3398a5 100644
--- a/src/components/Footer/Footer.jsx
+++ b/src/components/Footer/Footer.jsx
@@ -2,6 +2,7 @@ import facebookIcon from '@/assets/img/ic_facebook.png';
import twitterIcon from '@/assets/img/ic_twitter.png';
import youtubeIcon from '@/assets/img/ic_youtube.png';
import instagramIcon from '@/assets/img/ic_instagram.png';
+import './Footer.css';
export function Footer() {
return (
diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css
new file mode 100644
index 00000000..d33f1d24
--- /dev/null
+++ b/src/components/Header/Header.css
@@ -0,0 +1,83 @@
+#header {
+ position: fixed;
+ height: 4.4rem;
+ justify-content: center;
+ align-items: center;
+ background-color: white;
+ border-bottom: 1px solid #dfdfdf;
+}
+
+#title {
+ width: 9.6rem;
+ height: 3.2rem;
+}
+
+.logo {
+ width: 100%;
+ height: 100%;
+}
+
+#nav {
+ display: flex;
+ width: 120rem;
+ height: 4.4rem;
+ padding: 0px 25rem;
+ justify-content: space-between;
+ align-items: center;
+}
+
+#nav-left {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+}
+
+.nav-menu {
+ padding: 0 0.94rem;
+ color: var(--secondary-600);
+ text-align: center;
+ font-size: 1.125rem;
+ font-weight: 700;
+ line-height: 1.625rem;
+}
+
+.intro-nav-btn {
+ width: 8rem;
+}
+
+.nav-menu-active {
+ color: var(--primary-100);
+}
+
+@media screen and (max-width: 120rem) {
+ #nav {
+ width: 100%;
+ padding: 0 12.5rem;
+ }
+}
+
+@media screen and (max-width: 74.9rem) {
+ #nav {
+ padding: 0 1.5rem;
+ }
+}
+
+@media screen and (max-width: 46.4rem) {
+ #nav {
+ padding: 0 1rem;
+ }
+
+ #title {
+ width: 5rem;
+ height: auto;
+ }
+
+ #nav-left {
+ gap: 0.5rem;
+ }
+
+ .nav-menu {
+ padding: 0 0.5rem 0 0;
+ font-size: 1rem;
+ }
+}
diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx
index c9a6c500..d78075da 100644
--- a/src/components/Header/Header.jsx
+++ b/src/components/Header/Header.jsx
@@ -1,24 +1,38 @@
+import { Link } from 'react-router';
import logo from '@/assets/img/logo.png';
import logoText from '@/assets/img/logo_text.png';
import { NavMenuWrap } from './NavMenuWrap';
+import './Header.css';
+import { useLocation } from 'react-router';
+import clsx from 'clsx';
export function Header() {
+ const { pathname } = useLocation();
+ const isIndexPage = pathname === '/' ? true : false;
+
return (