diff --git a/package-lock.json b/package-lock.json
index a1e590ee..5d1b4f78 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,9 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.30.0",
"react-scripts": "5.0.1",
+ "sass": "^1.89.0",
"web-vitals": "^2.1.4"
}
},
@@ -3192,6 +3194,288 @@
"node": ">= 8"
}
},
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+ "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz",
@@ -3241,6 +3525,14 @@
}
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -6791,6 +7083,18 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "optional": true,
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -9181,6 +9485,11 @@
"url": "https://opencollective.com/immer"
}
},
+ "node_modules/immutable": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
+ "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -12472,6 +12781,12 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "optional": true
+ },
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -14671,6 +14986,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.30.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
+ "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==",
+ "dependencies": {
+ "@remix-run/router": "1.23.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz",
+ "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==",
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -15223,6 +15568,25 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
},
+ "node_modules/sass": {
+ "version": "1.89.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz",
+ "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
"node_modules/sass-loader": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@@ -15260,6 +15624,32 @@
}
}
},
+ "node_modules/sass/node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/sass/node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
diff --git a/package.json b/package.json
index 7ff0d6b5..5a00eb52 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,9 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.30.0",
"react-scripts": "5.0.1",
+ "sass": "^1.89.0",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/public/assets/images/common/og_img.png b/public/assets/images/common/og_img.png
new file mode 100644
index 00000000..093e85dc
Binary files /dev/null and b/public/assets/images/common/og_img.png differ
diff --git a/public/assets/images/main/bottom_banner_img.png b/public/assets/images/main/bottom_banner_img.png
new file mode 100644
index 00000000..17b6df69
Binary files /dev/null and b/public/assets/images/main/bottom_banner_img.png differ
diff --git a/public/assets/images/main/bottom_banner_img_mo.png b/public/assets/images/main/bottom_banner_img_mo.png
new file mode 100644
index 00000000..1dcc0f82
Binary files /dev/null and b/public/assets/images/main/bottom_banner_img_mo.png differ
diff --git a/public/assets/images/main/service_hot_img.png b/public/assets/images/main/service_hot_img.png
new file mode 100644
index 00000000..2254efbe
Binary files /dev/null and b/public/assets/images/main/service_hot_img.png differ
diff --git a/public/assets/images/main/service_hot_img_mo.png b/public/assets/images/main/service_hot_img_mo.png
new file mode 100644
index 00000000..129c373e
Binary files /dev/null and b/public/assets/images/main/service_hot_img_mo.png differ
diff --git a/public/assets/images/main/service_register_img.png b/public/assets/images/main/service_register_img.png
new file mode 100644
index 00000000..dc949e56
Binary files /dev/null and b/public/assets/images/main/service_register_img.png differ
diff --git a/public/assets/images/main/service_register_img_mo.png b/public/assets/images/main/service_register_img_mo.png
new file mode 100644
index 00000000..bba8468c
Binary files /dev/null and b/public/assets/images/main/service_register_img_mo.png differ
diff --git a/public/assets/images/main/service_search_img.png b/public/assets/images/main/service_search_img.png
new file mode 100644
index 00000000..a7f3a4f6
Binary files /dev/null and b/public/assets/images/main/service_search_img.png differ
diff --git a/public/assets/images/main/service_search_img_mo.png b/public/assets/images/main/service_search_img_mo.png
new file mode 100644
index 00000000..bbc1155c
Binary files /dev/null and b/public/assets/images/main/service_search_img_mo.png differ
diff --git a/public/assets/images/main/top_banner_img.png b/public/assets/images/main/top_banner_img.png
new file mode 100644
index 00000000..07b45ec5
Binary files /dev/null and b/public/assets/images/main/top_banner_img.png differ
diff --git a/public/assets/images/main/top_banner_img_mo.png b/public/assets/images/main/top_banner_img_mo.png
new file mode 100644
index 00000000..86a64aff
Binary files /dev/null and b/public/assets/images/main/top_banner_img_mo.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index a11777cc..00000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/index.html b/public/index.html
index aa069f27..d81391ab 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,43 +1,34 @@
-
+
-
-
-
-
+
+
+
+
+
+
-
-
-
-
- React App
+
+
+
+
+
+
+ 판다마켓
-
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a3..00000000
Binary files a/public/logo192.png and /dev/null differ
diff --git a/public/logo512.png b/public/logo512.png
deleted file mode 100644
index a4e47a65..00000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/public/manifest.json b/public/manifest.json
deleted file mode 100644
index 080d6c77..00000000
--- a/public/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "short_name": "React App",
- "name": "Create React App Sample",
- "icons": [
- {
- "src": "favicon.ico",
- "sizes": "64x64 32x32 24x24 16x16",
- "type": "image/x-icon"
- },
- {
- "src": "logo192.png",
- "type": "image/png",
- "sizes": "192x192"
- },
- {
- "src": "logo512.png",
- "type": "image/png",
- "sizes": "512x512"
- }
- ],
- "start_url": ".",
- "display": "standalone",
- "theme_color": "#000000",
- "background_color": "#ffffff"
-}
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e053..00000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/src/App.js b/src/App.js
index 37845757..a3a5801a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,24 +1,15 @@
-import logo from './logo.svg';
-import './App.css';
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import routes from "./routes";
+import "./styles/style.scss";
function App() {
+ const router = createBrowserRouter(routes, {
+ future: {
+ v7_relativeSplatPath: true,
+ },
+ });
return (
-
+
);
}
diff --git a/src/App.test.js b/src/App.test.js
deleted file mode 100644
index 1f03afee..00000000
--- a/src/App.test.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/assets/images/common/logo_lg.svg b/src/assets/images/common/logo_lg.svg
new file mode 100644
index 00000000..a71db940
--- /dev/null
+++ b/src/assets/images/common/logo_lg.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/assets/images/common/logo_md.svg b/src/assets/images/common/logo_md.svg
new file mode 100644
index 00000000..9842d949
--- /dev/null
+++ b/src/assets/images/common/logo_md.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/assets/images/common/logo_sm.svg b/src/assets/images/common/logo_sm.svg
new file mode 100644
index 00000000..a0c381b1
--- /dev/null
+++ b/src/assets/images/common/logo_sm.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/assets/images/common/logo_sx.svg b/src/assets/images/common/logo_sx.svg
new file mode 100644
index 00000000..55a63efc
--- /dev/null
+++ b/src/assets/images/common/logo_sx.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_facebook.svg b/src/assets/images/icons/ic_facebook.svg
new file mode 100644
index 00000000..b9c9d493
--- /dev/null
+++ b/src/assets/images/icons/ic_facebook.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_favorite.svg b/src/assets/images/icons/ic_favorite.svg
new file mode 100644
index 00000000..cca65053
--- /dev/null
+++ b/src/assets/images/icons/ic_favorite.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_instagram.svg b/src/assets/images/icons/ic_instagram.svg
new file mode 100644
index 00000000..0b9337b0
--- /dev/null
+++ b/src/assets/images/icons/ic_instagram.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_pagination_arrow.svg b/src/assets/images/icons/ic_pagination_arrow.svg
new file mode 100644
index 00000000..378fecdb
--- /dev/null
+++ b/src/assets/images/icons/ic_pagination_arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_pw_hide.svg b/src/assets/images/icons/ic_pw_hide.svg
new file mode 100644
index 00000000..f039d5cc
--- /dev/null
+++ b/src/assets/images/icons/ic_pw_hide.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/icons/ic_pw_show.svg b/src/assets/images/icons/ic_pw_show.svg
new file mode 100644
index 00000000..35a75305
--- /dev/null
+++ b/src/assets/images/icons/ic_pw_show.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_search.svg b/src/assets/images/icons/ic_search.svg
new file mode 100644
index 00000000..52241e6d
--- /dev/null
+++ b/src/assets/images/icons/ic_search.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_select_arrow.svg b/src/assets/images/icons/ic_select_arrow.svg
new file mode 100644
index 00000000..8308690f
--- /dev/null
+++ b/src/assets/images/icons/ic_select_arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_sns_google.svg b/src/assets/images/icons/ic_sns_google.svg
new file mode 100644
index 00000000..5c5d1f2a
--- /dev/null
+++ b/src/assets/images/icons/ic_sns_google.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/icons/ic_sns_kakao.svg b/src/assets/images/icons/ic_sns_kakao.svg
new file mode 100644
index 00000000..a9c381ff
--- /dev/null
+++ b/src/assets/images/icons/ic_sns_kakao.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/images/icons/ic_sort.svg b/src/assets/images/icons/ic_sort.svg
new file mode 100644
index 00000000..e4542d39
--- /dev/null
+++ b/src/assets/images/icons/ic_sort.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/icons/ic_twitter.svg b/src/assets/images/icons/ic_twitter.svg
new file mode 100644
index 00000000..14a6069a
--- /dev/null
+++ b/src/assets/images/icons/ic_twitter.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/ic_user_thumbnail.svg b/src/assets/images/icons/ic_user_thumbnail.svg
new file mode 100644
index 00000000..94d40a49
--- /dev/null
+++ b/src/assets/images/icons/ic_user_thumbnail.svg
@@ -0,0 +1,24 @@
+
diff --git a/src/assets/images/icons/ic_youtube.svg b/src/assets/images/icons/ic_youtube.svg
new file mode 100644
index 00000000..c65eceb5
--- /dev/null
+++ b/src/assets/images/icons/ic_youtube.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/components/AllProductArea/AllProductArea.js b/src/components/AllProductArea/AllProductArea.js
new file mode 100644
index 00000000..d8c44bf1
--- /dev/null
+++ b/src/components/AllProductArea/AllProductArea.js
@@ -0,0 +1,135 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import Pagination from "../Pagination/Pagination";
+import ProductList from "../ProductList/ProductList";
+import { getData } from "../../data/api";
+import styles from "./AllProductArea.module.scss";
+import { getItemCount } from "../../utils/getItemCount";
+
+const SORT_TYPE = {
+ recent: "최신순",
+ favorite: "좋아요순",
+};
+const SortDropdown = ({ orderBy, setOrderBy }) => {
+ const [sortOpen, setSortOpen] = useState(false);
+ const handleClickSort = () => setSortOpen(!sortOpen);
+ const handleSelectSort = (sort) => {
+ setOrderBy(sort);
+ setSortOpen(!sortOpen);
+ };
+
+ return (
+ <>
+
+ {sortOpen && (
+
+
+ {Object.keys(SORT_TYPE).map((sort) => (
+ -
+
+
+ ))}
+
+
+ )}
+ >
+ );
+};
+
+const ITEM_COUNT = {
+ WEB: 10,
+ TABLET: 6,
+ MOBILE: 4,
+};
+
+const INIT_PAGE_SIZE = getItemCount(ITEM_COUNT);
+
+const AllProductArea = () => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [orderBy, setOrderBy] = useState("recent");
+ const [pageSize, setPageSize] = useState(INIT_PAGE_SIZE);
+ const [productList, setProductList] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+
+ // 요구 정의서
+ // 1. orderby="recent", 10가지 상품을 전체 상품 리스트에 렌더링
+ // 2. 반응형에 따라 웹에선 10, 타블렛에선 6, 모바일에선 4 보여주기 (미디어 쿼리 사용하기)
+ // 3. 전체 상품에서 드롭다운으로 최신순/좋아요순 정렬 기능 추가
+ // 4. [심화] 페이지네이션 기능 구현
+
+ const getProductList = async (options) => {
+ try {
+ const data = await getData(options);
+ if (!data) return;
+ setProductList(data.list);
+ setTotalCount(data.totalCount);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const updatePageSize = () => {
+ const itemCount = getItemCount(ITEM_COUNT);
+ setPageSize(itemCount);
+ };
+
+ useEffect(() => {
+ updatePageSize();
+ window.addEventListener("resize", updatePageSize);
+
+ return () => {
+ window.removeEventListener("resize", updatePageSize);
+ };
+ }, []);
+
+ useEffect(() => {
+ getProductList({
+ page: currentPage,
+ pageSize: pageSize,
+ orderBy: orderBy,
+ });
+ }, [currentPage, pageSize, orderBy]);
+
+ return (
+ <>
+
+
전체 상품
+
+
+
+
+ 상품 등록하기
+
+
+
+
+
+
+ >
+ );
+};
+
+export default AllProductArea;
diff --git a/src/components/AllProductArea/AllProductArea.module.scss b/src/components/AllProductArea/AllProductArea.module.scss
new file mode 100644
index 00000000..a00dfaa9
--- /dev/null
+++ b/src/components/AllProductArea/AllProductArea.module.scss
@@ -0,0 +1,151 @@
+@use "../../styles/variables" as var;
+@use "../../styles/mixin" as mixin;
+
+.allProductArea {
+ &__title {
+ font-size: 20px;
+ font-weight: 700;
+ color: var.$gray900;
+ }
+
+ &__utils {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ @include mixin.mobile {
+ flex-wrap: wrap;
+ gap: 8px 14px;
+ }
+
+ .utils {
+ $height: 42px;
+
+ &__searchBox {
+ width: 325px;
+ margin-left: auto;
+
+ @include mixin.tablet {
+ width: 242px;
+ }
+
+ @include mixin.mobile {
+ margin: 0;
+ width: calc(100% - 56px);
+ order: 3;
+ }
+
+ input {
+ display: block;
+ width: 100%;
+ height: $height;
+ padding: 0 12px 0 44px;
+ background: var.$gray100
+ url("../../assets/images/icons/ic_search.svg") 16px center no-repeat;
+ border-radius: 12px;
+ }
+ }
+
+ &__productAdd {
+ display: block;
+ width: 132px;
+ line-height: $height;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: var.$gray100;
+ background: var.$primary-color;
+ border-radius: 8px;
+
+ @include mixin.mobile {
+ margin-left: auto;
+ }
+ }
+
+ &__sortSelectBox {
+ position: relative;
+ width: 130px;
+
+ @include mixin.mobile {
+ width: auto;
+ order: 4;
+ }
+
+ .sortSelectBox {
+ &__current {
+ display: block;
+ width: 100%;
+ padding: 0 42px 0 20px;
+ height: $height;
+ border: 1px solid var.$gray200;
+ font-size: 16px;
+ text-align: left;
+ color: var.$gray800;
+ background: url("../../assets/images/icons/ic_select_arrow.svg")
+ right 20px center no-repeat;
+ border-radius: 12px;
+
+ @include mixin.mobile {
+ width: $height;
+ padding: 0;
+ background: url("../../assets/images/icons/ic_sort.svg") center
+ center no-repeat;
+
+ span {
+ display: none;
+ }
+ }
+ }
+
+ &__list {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ width: 130px;
+ margin-top: 8px;
+ z-index: 1;
+ overflow: hidden;
+
+ ul {
+ border: 1px solid var.$gray200;
+ background: var.$white;
+ border-radius: 12px;
+ transform: translateY(-100%);
+ animation: showSortList 0.3s ease forwards;
+
+ @keyframes showSortList {
+ 100% {
+ transform: translateY(0);
+ }
+ }
+
+ li {
+ border-bottom: 1px solid var.$gray200;
+
+ &:last-of-type {
+ border-bottom: none;
+ }
+
+ button {
+ display: block;
+ width: 100%;
+ height: $height;
+ font-size: 16px;
+ color: var.$gray800;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ &__content {
+ margin-top: 24px;
+
+ @include mixin.mobile {
+ margin-top: 16px;
+ }
+ }
+}
diff --git a/src/components/AuthGuide/AuthGuide.js b/src/components/AuthGuide/AuthGuide.js
new file mode 100644
index 00000000..f14ed6f3
--- /dev/null
+++ b/src/components/AuthGuide/AuthGuide.js
@@ -0,0 +1,18 @@
+import { Link } from "react-router-dom";
+
+const AuthGuide = ({ guideTxt, linkTxt, linkUrl }) => {
+ return (
+
+
{guideTxt}
+
+ {linkTxt}
+
+
+ );
+};
+
+export default AuthGuide;
diff --git a/src/components/AuthSns/AuthSns.js b/src/components/AuthSns/AuthSns.js
new file mode 100644
index 00000000..983bbeb7
--- /dev/null
+++ b/src/components/AuthSns/AuthSns.js
@@ -0,0 +1,31 @@
+import { Link } from "react-router-dom";
+import snsGoogle from "../../assets/images/icons/ic_sns_google.svg";
+import snsKakao from "../../assets/images/icons/ic_sns_kakao.svg";
+
+const AuthSns = () => {
+ return (
+
+
간편 로그인하기
+
+ -
+
+
+
+
+ -
+
+
+
+
+
+
+ );
+};
+
+export default AuthSns;
diff --git a/src/components/BestProductArea/BestProductArea.js b/src/components/BestProductArea/BestProductArea.js
new file mode 100644
index 00000000..6759a4ba
--- /dev/null
+++ b/src/components/BestProductArea/BestProductArea.js
@@ -0,0 +1,58 @@
+import { useEffect, useState } from "react";
+import ProductList from "../ProductList/ProductList";
+import { getData } from "../../data/api";
+import styles from "./BestProductArea.module.scss";
+import { getItemCount } from "../../utils/getItemCount";
+
+const INIT_PAGE_SIZE = 4;
+const ITEM_COUNT = {
+ WEB: 4,
+ TABLET: 2,
+ MOBILE: 1,
+};
+
+const BestProductArea = () => {
+ const [bestList, setBestList] = useState([]);
+ const [pageSize, setPageSize] = useState(INIT_PAGE_SIZE);
+
+ // 요구 정의서
+ // 1. orderby="favorite", 4가지 상품을 베스트 상품 리스트에 렌더링
+ // 2. 반응형에 따라 웹에선 4, 타블렛에선 2, 모바일에선 1 보여주기 (미디어 쿼리 사용하기)
+
+ const getProductList = async (options) => {
+ try {
+ const data = await getData(options);
+ if (!data) return;
+ setBestList(data.list);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const updatePageSize = () => {
+ const itemCount = getItemCount(ITEM_COUNT);
+ setPageSize(itemCount);
+ };
+
+ useEffect(() => {
+ getProductList({ orderBy: "favorite", pageSize: INIT_PAGE_SIZE, page: 1 });
+
+ updatePageSize();
+ window.addEventListener("resize", updatePageSize);
+
+ return () => {
+ window.removeEventListener("resize", updatePageSize);
+ };
+ }, []);
+
+ return (
+ <>
+ 베스트 상품
+
+ >
+ );
+};
+
+export default BestProductArea;
diff --git a/src/components/BestProductArea/BestProductArea.module.scss b/src/components/BestProductArea/BestProductArea.module.scss
new file mode 100644
index 00000000..394a2702
--- /dev/null
+++ b/src/components/BestProductArea/BestProductArea.module.scss
@@ -0,0 +1,13 @@
+@use "../../styles/variables" as var;
+
+.bestProductArea {
+ &__title {
+ font-size: 20px;
+ font-weight: 700;
+ color: var.$gray900;
+ }
+
+ &__content {
+ margin-top: 16px;
+ }
+}
diff --git a/src/components/Pagination/Pagination.js b/src/components/Pagination/Pagination.js
new file mode 100644
index 00000000..f93c87ba
--- /dev/null
+++ b/src/components/Pagination/Pagination.js
@@ -0,0 +1,89 @@
+import { useCallback, useMemo } from "react";
+import arrow from "../../assets/images/icons/ic_pagination_arrow.svg";
+import styles from "./Pagination.module.scss";
+
+const LIMIT = 5;
+const Pagination = ({ pageSize, totalCount, currentPage, setCurrentPage }) => {
+ // 페이지네이션 5개(LIMIT)로 끊어서 2차원 배열로 생성
+ const calcPager = useMemo(() => {
+ if (!totalCount) return;
+
+ const totalPager = Math.ceil(totalCount / pageSize);
+
+ const pagerArr = [];
+ let pagerCounter = 0;
+ for (let i = 0; i < totalPager; i++) {
+ if (!pagerArr[pagerCounter]) pagerArr[pagerCounter] = [];
+ pagerArr[pagerCounter].push(i + 1);
+ if (pagerArr[pagerCounter].length === LIMIT) pagerCounter++;
+ }
+
+ return { pagerArr, totalPager };
+ }, [pageSize, totalCount]);
+
+ // currentPage 값이 있는 배열 반환
+ const getCurrentGroup = useCallback(() => {
+ if (!calcPager) return;
+
+ return calcPager.pagerArr.filter((item) => item.includes(currentPage))[0];
+ }, [calcPager, currentPage]);
+
+ const currentGroup = getCurrentGroup();
+
+ // prev 버튼 클릭
+ const handleClickPrev = () => {
+ const changeCurrentPage = currentPage - 1 <= 0 ? 1 : currentPage - 1;
+ setCurrentPage(changeCurrentPage);
+ };
+
+ // next 버튼 클릭
+ const handleClickNext = () => {
+ const totalPageNum = calcPager.totalPager;
+ const changeCurrentPage =
+ currentPage + 1 >= totalPageNum ? totalPageNum : currentPage + 1;
+ setCurrentPage(changeCurrentPage);
+ };
+
+ return (
+
+ {currentGroup && (
+ <>
+
+
+ {currentGroup.map((pager) => (
+ -
+
+
+ ))}
+
+
+ >
+ )}
+
+ );
+};
+
+export default Pagination;
diff --git a/src/components/Pagination/Pagination.module.scss b/src/components/Pagination/Pagination.module.scss
new file mode 100644
index 00000000..75ca0da8
--- /dev/null
+++ b/src/components/Pagination/Pagination.module.scss
@@ -0,0 +1,47 @@
+@use "../../styles/variables" as var;
+
+$size: 40px;
+
+.pagination {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ margin-top: 40px;
+
+ &__list {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ }
+
+ &__button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $size;
+ height: $size;
+ border: 1px solid var.$gray200;
+ font-size: 16px;
+ font-weight: 600;
+ color: var.$gray500;
+ border-radius: 50%;
+
+ &:disabled {
+ opacity: 0.5;
+ }
+
+ &-current {
+ border-color: #2f80ed;
+ color: #fff;
+ background: #2f80ed;
+ }
+
+ &-next {
+ img {
+ transform: rotate(180deg);
+ }
+ }
+ }
+}
diff --git a/src/components/ProductItem/ProductItem.js b/src/components/ProductItem/ProductItem.js
new file mode 100644
index 00000000..b3808a2c
--- /dev/null
+++ b/src/components/ProductItem/ProductItem.js
@@ -0,0 +1,23 @@
+import { Link } from "react-router-dom";
+import style from "./ProductItem.module.scss";
+
+const ProductItem = ({ product }) => {
+ const { id, images, name, price, favoriteCount } = product;
+
+ return (
+
+
+
+
+
+
{name}
+
+ {price.toLocaleString("ko-KR")}원
+
+
{favoriteCount}
+
+
+ );
+};
+
+export default ProductItem;
diff --git a/src/components/ProductItem/ProductItem.module.scss b/src/components/ProductItem/ProductItem.module.scss
new file mode 100644
index 00000000..69d45db0
--- /dev/null
+++ b/src/components/ProductItem/ProductItem.module.scss
@@ -0,0 +1,52 @@
+@use "../../styles/variables" as var;
+@use "../../styles/mixin" as mixin;
+
+.productItem {
+ display: block;
+
+ &__image {
+ position: relative;
+ padding-top: 100%;
+ overflow: hidden;
+ border-radius: 16px;
+
+ img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ &__content {
+ margin-top: 16px;
+ color: var.$gray800;
+ }
+
+ &__subject {
+ font-size: 14px;
+ font-weight: 500;
+ @include mixin.ellipsis();
+ }
+
+ &__price {
+ display: block;
+ margin-top: 6px;
+ font-size: 16px;
+ font-weight: 700;
+ }
+
+ &__favorite {
+ display: block;
+ margin-top: 6px;
+ padding-left: 20px;
+ font-size: 12px;
+ font-weight: 500;
+ color: var.$gray600;
+ line-height: 1.5;
+ background: url("../../assets/images/icons/ic_favorite.svg") left center
+ no-repeat;
+ }
+}
diff --git a/src/components/ProductList/ProductList.js b/src/components/ProductList/ProductList.js
new file mode 100644
index 00000000..65f1ce12
--- /dev/null
+++ b/src/components/ProductList/ProductList.js
@@ -0,0 +1,18 @@
+import ProductItem from "../ProductItem/ProductItem";
+import styles from "./ProductList.module.scss";
+
+const ProductList = ({ list, type = "all" }) => {
+ return (
+
+ {list.map((product) => (
+ -
+
+
+ ))}
+
+ );
+};
+
+export default ProductList;
diff --git a/src/components/ProductList/ProductList.module.scss b/src/components/ProductList/ProductList.module.scss
new file mode 100644
index 00000000..46040f20
--- /dev/null
+++ b/src/components/ProductList/ProductList.module.scss
@@ -0,0 +1,32 @@
+@use "../../styles/mixin" as mixin;
+
+.productList {
+ display: grid;
+ gap: 40px 24px;
+
+ &__best {
+ grid-template-columns: repeat(4, 1fr);
+
+ @include mixin.tablet {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 10px;
+ }
+ @include mixin.mobile {
+ grid-template-columns: repeat(1, 1fr);
+ }
+ }
+
+ &__all {
+ grid-template-columns: repeat(5, 1fr);
+
+ @include mixin.tablet {
+ gap: 16px;
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ @include mixin.mobile {
+ gap: 8px;
+ grid-template-columns: repeat(2, 1fr);
+ }
+ }
+}
diff --git a/src/data/api.js b/src/data/api.js
new file mode 100644
index 00000000..b0c9d7a1
--- /dev/null
+++ b/src/data/api.js
@@ -0,0 +1,17 @@
+const BASE_URL = "https://panda-market-api.vercel.app";
+
+export const getData = async ({
+ page = 1,
+ pageSize = 10,
+ orderBy = "recent",
+}) => {
+ const query = `page=${page}&pageSize=${pageSize}&orderBy=${orderBy}`;
+ const res = await fetch(`${BASE_URL}/products?${query}`);
+
+ if (!res.ok) {
+ throw new Error("상품 리스트를 불러오는데 실패했습니다.");
+ }
+
+ const data = await res.json();
+ return data;
+};
diff --git a/src/hooks/useAllValid.js b/src/hooks/useAllValid.js
new file mode 100644
index 00000000..59e6408d
--- /dev/null
+++ b/src/hooks/useAllValid.js
@@ -0,0 +1,17 @@
+import { useEffect, useState } from "react";
+
+const useAllValid = (valueValids) => {
+ const [isAllValid, setIsAllValid] = useState(false);
+
+ useEffect(() => {
+ // auth 관련 페이지에서만 검증 요소가 전부 true인지 확인
+ const allValid = Object.keys(valueValids).every((valid) => {
+ return valueValids[valid].isValid;
+ });
+ setIsAllValid(allValid);
+ }, [valueValids]);
+
+ return isAllValid;
+};
+
+export default useAllValid;
diff --git a/src/hooks/usePasswordToggle.js b/src/hooks/usePasswordToggle.js
new file mode 100644
index 00000000..106e63d8
--- /dev/null
+++ b/src/hooks/usePasswordToggle.js
@@ -0,0 +1,21 @@
+import { useState } from "react";
+import pwShow from "../assets/images/icons/ic_pw_show.svg";
+import pwHide from "../assets/images/icons/ic_pw_hide.svg";
+
+const usePasswordToggle = () => {
+ const [toggle, setToggle] = useState(false);
+
+ const handleClickToggle = () => {
+ setToggle(() => !toggle);
+ };
+
+ const toggleImg = toggle ? pwShow : pwHide;
+
+ return {
+ toggle,
+ handleClickToggle,
+ toggleImg,
+ };
+};
+
+export default usePasswordToggle;
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index ec2585e8..00000000
--- a/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
diff --git a/src/index.js b/src/index.js
index d563c0fb..8db5acb8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,17 +1,10 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
-const root = ReactDOM.createRoot(document.getElementById('root'));
+const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
diff --git a/src/layouts/Footer.js b/src/layouts/Footer.js
new file mode 100644
index 00000000..b56d11ef
--- /dev/null
+++ b/src/layouts/Footer.js
@@ -0,0 +1,70 @@
+import { Link } from "react-router-dom";
+import icFacebook from "../assets/images/icons/ic_facebook.svg";
+import icTwitter from "../assets/images/icons/ic_twitter.svg";
+import icYoutube from "../assets/images/icons/ic_youtube.svg";
+import icInstagram from "../assets/images/icons/ic_instagram.svg";
+
+const snsImgList = {
+ facebook: icFacebook,
+ twitter: icTwitter,
+ youtube: icYoutube,
+ instagram: icInstagram,
+};
+
+const Footer = () => {
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/src/layouts/Header.js b/src/layouts/Header.js
new file mode 100644
index 00000000..03191757
--- /dev/null
+++ b/src/layouts/Header.js
@@ -0,0 +1,64 @@
+import { Link, useLocation } from "react-router-dom";
+import getLogo from "../utils/getLogo";
+import userThumbnail from "../assets/images/icons/ic_user_thumbnail.svg";
+import { useState } from "react";
+
+const GNB_MENU = [
+ { path: "/free", title: "자유게시판" },
+ { path: "/items", title: "중고마켓" },
+];
+
+const Header = () => {
+ const location = useLocation();
+ const [isLogin, setIsLogin] = useState(true);
+
+ return (
+
+ );
+};
+
+export default Header;
diff --git a/src/layouts/Layout.js b/src/layouts/Layout.js
new file mode 100644
index 00000000..ae93189d
--- /dev/null
+++ b/src/layouts/Layout.js
@@ -0,0 +1,13 @@
+import { Outlet } from "react-router";
+import Header from "./Header";
+
+const Layout = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default Layout;
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c05..00000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/pages/AddItemPage/AddItemPage.js b/src/pages/AddItemPage/AddItemPage.js
new file mode 100644
index 00000000..77d2c394
--- /dev/null
+++ b/src/pages/AddItemPage/AddItemPage.js
@@ -0,0 +1,5 @@
+const AddItemPage = () => {
+ return AddItemPage
;
+};
+
+export default AddItemPage;
diff --git a/src/pages/FaqPage/FaqPage.js b/src/pages/FaqPage/FaqPage.js
new file mode 100644
index 00000000..58da441a
--- /dev/null
+++ b/src/pages/FaqPage/FaqPage.js
@@ -0,0 +1,5 @@
+const FaqPage = () => {
+ return faq 페이지
;
+};
+
+export default FaqPage;
diff --git a/src/pages/ItemsPage/ItemsPage.js b/src/pages/ItemsPage/ItemsPage.js
new file mode 100644
index 00000000..a03dac54
--- /dev/null
+++ b/src/pages/ItemsPage/ItemsPage.js
@@ -0,0 +1,20 @@
+import styles from "./ItemsPage.module.scss";
+import AllProductArea from "../../components/AllProductArea/AllProductArea";
+import BestProductArea from "../../components/BestProductArea/BestProductArea";
+
+const ItemsPage = () => {
+ return (
+
+ );
+};
+
+export default ItemsPage;
diff --git a/src/pages/ItemsPage/ItemsPage.module.scss b/src/pages/ItemsPage/ItemsPage.module.scss
new file mode 100644
index 00000000..0f4c86fd
--- /dev/null
+++ b/src/pages/ItemsPage/ItemsPage.module.scss
@@ -0,0 +1,27 @@
+@use "../../styles/variables" as var;
+@use "../../styles/mixin" as mixin;
+
+.itemsPage {
+ &__inner {
+ width: calc(100% - 48px);
+ max-width: 1200px;
+ margin: 24px auto;
+
+ @include mixin.mobile {
+ width: calc(100% - 32px);
+ margin-top: 17px;
+ }
+ }
+
+ &__section {
+ margin-bottom: 40px;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+
+ @include mixin.mobile {
+ margin-bottom: 24px;
+ }
+ }
+}
diff --git a/src/pages/LoginPage/LoginPage.js b/src/pages/LoginPage/LoginPage.js
new file mode 100644
index 00000000..0fd537c9
--- /dev/null
+++ b/src/pages/LoginPage/LoginPage.js
@@ -0,0 +1,149 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import useAllValid from "../../hooks/useAllValid";
+import usePasswordToggle from "../../hooks/usePasswordToggle";
+import {
+ checkValidEmail,
+ checkValidPassword,
+ getAuthValidClassName,
+} from "../../utils/authUtils";
+import getLogo from "../../utils/getLogo";
+import AuthSns from "../../components/AuthSns/AuthSns";
+import AuthGuide from "../../components/AuthGuide/AuthGuide";
+import "../../styles/auth.scss";
+import styles from "./LoginPage.module.scss";
+
+const INIT_VALUE = { email: "", password: "" };
+const INIT_VALID = {
+ email: {
+ isValid: null,
+ msg: "",
+ },
+ password: {
+ isValid: null,
+ msg: "",
+ },
+};
+
+const VALIDATOR = {
+ email: checkValidEmail,
+ password: checkValidPassword,
+};
+
+const LoginPage = () => {
+ const nav = useNavigate();
+ const [userValues, setUserValues] = useState(INIT_VALUE);
+ const [valueValids, setValueValids] = useState(INIT_VALID);
+ const pwToggle = usePasswordToggle();
+ const isAllValid = useAllValid(valueValids);
+
+ const handleChangeUserValues = (e) => {
+ setUserValues({ ...userValues, [e.target.name]: e.target.value });
+ };
+
+ const handleFocusOut = (e) => {
+ const { name, value } = e.target;
+
+ // 검증할 요소인지 확인
+ if (!VALIDATOR[name]) return;
+
+ setValueValids(() => ({
+ ...valueValids,
+ [name]: VALIDATOR[name](value),
+ }));
+ };
+
+ const handleClickSubmit = (e) => {
+ e.preventDefault();
+ nav("/");
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/src/pages/LoginPage/LoginPage.module.scss b/src/pages/LoginPage/LoginPage.module.scss
new file mode 100644
index 00000000..62fdafba
--- /dev/null
+++ b/src/pages/LoginPage/LoginPage.module.scss
@@ -0,0 +1,13 @@
+@use "../../styles/mixin" as mixin;
+
+.loginPage {
+ padding: 231px 0;
+
+ @include mixin.tablet {
+ padding: 190px 0;
+ }
+
+ @include mixin.mobile {
+ padding: 80px 0;
+ }
+}
diff --git a/src/pages/MainPage/MainPage.js b/src/pages/MainPage/MainPage.js
new file mode 100644
index 00000000..62fb633e
--- /dev/null
+++ b/src/pages/MainPage/MainPage.js
@@ -0,0 +1,163 @@
+import { Link } from "react-router-dom";
+import styles from "./MainPage.module.scss";
+import Footer from "../../layouts/Footer";
+
+const MainPage = () => {
+ return (
+ <>
+
+ {/* sec__top-banner */}
+
+
+
+
+ 일상의 모든 물건을
+ 거래해 보세요
+
+
+ 구경하러 가기
+
+
+
+
+
+
+
+ {/* sec__service */}
+
+
+ {/* service__item-hot */}
+
+
+
+
+
+
+
Hot item
+
+ 인기 상품을
+
확인해 보세요
+
+
+ 가장 HOT한 중고거래 물품을 판다 마켓에서 확인해 보세요
+
+
+
+
+ {/* service__item-search */}
+
+
+
+
+
+
+
Search
+
+ 구매를 원하는
+ 상품을 검색하세요
+
+
+ 구매하고 싶은 물품은 검색해서 쉽게 찾아보세요
+
+
+
+
+ {/* service__item-register */}
+
+
+
+
+
+
+
Register
+
+ 판매를 원하는
+ 상품을 등록하세요
+
+
+ 어떤 물건이든 판매하고 싶은 상품을 쉽게 등록하세요
+
+
+
+
+
+
+ {/* sec__bottom-banner */}
+
+
+
+
+ 믿을 수 있는
+ 판다마켓 중고 거래
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default MainPage;
diff --git a/src/pages/MainPage/MainPage.module.scss b/src/pages/MainPage/MainPage.module.scss
new file mode 100644
index 00000000..25c9aabe
--- /dev/null
+++ b/src/pages/MainPage/MainPage.module.scss
@@ -0,0 +1,279 @@
+@use "../../styles/variables" as var;
+@use "../../styles/mixin" as mixin;
+
+.mainPage {
+ &__title {
+ font-size: 40px;
+ font-weight: 700;
+ line-height: 1.4;
+
+ @include mixin.mobile {
+ font-size: 32px;
+ }
+ }
+
+ .inner {
+ width: calc(100% - 30px);
+ max-width: 1110px;
+ margin: 0 auto;
+
+ @include mixin.tablet {
+ width: 100%;
+ }
+ }
+
+ .sec {
+ &__top-banner {
+ background-color: var.$primary-bg;
+
+ .inner {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ padding-top: 200px;
+
+ @include mixin.tablet {
+ display: block;
+ padding-top: 84px;
+ text-align: center;
+ }
+
+ @include mixin.mobile {
+ padding-top: 48px;
+ }
+ }
+
+ .top-banner {
+ &__textArea {
+ padding-bottom: 100px;
+
+ @include mixin.tablet {
+ padding-bottom: 211px;
+ }
+
+ @include mixin.mobile {
+ padding-bottom: 132px;
+ }
+
+ .mainPage__title {
+ br {
+ @include mixin.tablet {
+ display: none;
+ }
+ @include mixin.mobile {
+ display: block;
+ }
+ }
+ }
+
+ .textArea {
+ &__link {
+ margin-top: 32px;
+
+ @include mixin.tablet {
+ margin-top: 24px;
+ }
+
+ @include mixin.mobile {
+ margin-top: 18px;
+ }
+ }
+ }
+ }
+
+ @include mixin.tablet {
+ &__imgArea {
+ img {
+ margin: 0 auto;
+ }
+ }
+ }
+ }
+ }
+
+ &__service {
+ background-color: var.$white;
+
+ @include mixin.tablet {
+ padding: 24px 24px 4px;
+ }
+
+ @include mixin.mobile {
+ padding: 52px 16px 16px;
+ }
+
+ .service {
+ &__article {
+ padding: 138px 0;
+
+ @include mixin.tablet {
+ padding: 0;
+ margin-bottom: 52px;
+ }
+
+ @include mixin.mobile {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ max-width: 988px;
+ margin: 0 auto;
+ gap: 64px;
+ background: #fcfcfc;
+ overflow: hidden;
+ border-radius: 12px;
+
+ @include mixin.tablet {
+ display: block;
+ background: none;
+ }
+
+ &--hot {
+ padding-left: 24px;
+
+ @include mixin.tablet {
+ padding: 0;
+ }
+ }
+ &--search {
+ padding-right: 14px;
+ flex-direction: row-reverse;
+ padding-left: 24px;
+ text-align: right;
+
+ @include mixin.tablet {
+ padding: 0;
+ }
+ }
+ }
+
+ &__textArea {
+ flex: 1;
+
+ @include mixin.tablet {
+ margin-top: 24px;
+ }
+
+ .textArea {
+ &__cat {
+ display: block;
+ margin-bottom: 12px;
+ font-size: 18px;
+ font-weight: 700;
+ color: var.$primary-color;
+
+ @include mixin.tablet {
+ margin-bottom: 16px;
+ }
+
+ @include mixin.mobile {
+ margin-bottom: 8px;
+ font-size: 16px;
+ }
+ }
+
+ &__title {
+ @extend .mainPage__title;
+
+ @include mixin.tablet {
+ font-size: 32px;
+ }
+
+ @include mixin.mobile {
+ font-size: 24px;
+ }
+
+ br {
+ @include mixin.tablet {
+ display: none;
+ }
+ }
+ }
+
+ &__desc {
+ margin-top: 24px;
+ font-size: 24px;
+ font-weight: 500;
+ word-break: keep-all;
+
+ @include mixin.tablet {
+ font-size: 18px;
+ }
+
+ @include mixin.mobile {
+ margin-top: 16px;
+ font-size: 16px;
+ }
+ }
+ }
+ }
+
+ &__imgArea {
+ img {
+ @include mixin.tablet {
+ width: 100%;
+ border-radius: 14px;
+ }
+
+ @include mixin.mobile {
+ border-radius: 7px;
+ }
+ }
+ }
+ }
+ }
+
+ &__bottom-banner {
+ margin-top: 138px;
+ padding-top: 143px;
+ background-color: var.$primary-bg;
+
+ @include mixin.tablet {
+ margin-top: 0;
+ padding-top: 201px;
+ }
+
+ @include mixin.mobile {
+ padding-top: 121px;
+ }
+
+ .inner {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 30px;
+
+ @include mixin.tablet {
+ display: block;
+ text-align: center;
+ }
+ }
+
+ .bottom-banner {
+ &__textArea {
+ padding-bottom: 172px;
+
+ @include mixin.tablet {
+ padding-bottom: 217px;
+ }
+
+ @include mixin.mobile {
+ padding-bottom: 131px;
+ }
+ }
+
+ &__imgArea {
+ img {
+ @include mixin.tablet {
+ margin: 0 auto;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/pages/PrivacyPage/PrivacyPage.js b/src/pages/PrivacyPage/PrivacyPage.js
new file mode 100644
index 00000000..3e32c9c3
--- /dev/null
+++ b/src/pages/PrivacyPage/PrivacyPage.js
@@ -0,0 +1,7 @@
+import React from "react";
+
+const privacyPage = () => {
+ return ;
+};
+
+export default privacyPage;
diff --git a/src/pages/SignupPage/SignupPage.js b/src/pages/SignupPage/SignupPage.js
new file mode 100644
index 00000000..6b5242f5
--- /dev/null
+++ b/src/pages/SignupPage/SignupPage.js
@@ -0,0 +1,257 @@
+import { useCallback, useMemo, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import useAllValid from "../../hooks/useAllValid";
+import usePasswordToggle from "../../hooks/usePasswordToggle";
+import {
+ checkValidEmail,
+ checkValidNickname,
+ checkValidPassword,
+ checkValidPasswordConfirm,
+ getAuthValidClassName,
+} from "../../utils/authUtils";
+import getLogo from "../../utils/getLogo";
+import AuthSns from "../../components/AuthSns/AuthSns";
+import AuthGuide from "../../components/AuthGuide/AuthGuide";
+import "../../styles/auth.scss";
+import styles from "./SignupPage.module.scss";
+
+const INIT_VALUE = {
+ nickname: "",
+ email: "",
+ password: "",
+ passwordConfirm: "",
+};
+const INIT_VALID = {
+ nickname: {
+ isValid: null,
+ msg: "",
+ },
+ email: {
+ isValid: null,
+ msg: "",
+ },
+ password: {
+ isValid: null,
+ msg: "",
+ },
+ passwordConfirm: {
+ isValid: null,
+ msg: "",
+ },
+};
+
+const SignupPage = () => {
+ const nav = useNavigate();
+ const [userValues, setUserValues] = useState(INIT_VALUE);
+ const [valueValids, setValueValids] = useState(INIT_VALID);
+ const pwToggle = usePasswordToggle();
+ const pwConfirmToggle = usePasswordToggle();
+ const isAllValid = useAllValid(valueValids);
+
+ // 비밀번호 확인 필드 함수 재정의
+ // 렌더링마다 함수 재생성 방지를 위해 useCallback 사용
+ const redefinePasswordConfirm = useCallback(
+ (value) => {
+ return checkValidPasswordConfirm(value, userValues.password);
+ },
+ [userValues]
+ );
+
+ // 비밀번호 필드 함수 재정의
+ // 렌더링마다 함수 재생성 방지를 위해 useCallback 사용
+ const redefinePassword = useCallback(
+ (value) => {
+ const checkPassword = checkValidPassword(value);
+ // 비밀번호 확인 필드에 값이 있는 상태에서 비밀번호 필드를 바꾸면 양쪽 모두 검사
+ if (userValues.passwordConfirm.length) {
+ setValueValids(() => ({
+ ...valueValids,
+ password: checkPassword, // 여기에도 써주어야 정상적으로 변경됨...
+ passwordConfirm: redefinePasswordConfirm(userValues.passwordConfirm),
+ }));
+ }
+
+ return checkPassword;
+ },
+ [redefinePasswordConfirm, valueValids, userValues]
+ );
+
+ // 함수 재정의와 useValues 참조를 위해 컴포넌트 내부에 정의
+ // 렌더링마다 객체 재생성 방지를 위해 useMemo 사용
+ const VALIDATOR = useMemo(
+ () => ({
+ nickname: checkValidNickname,
+ email: checkValidEmail,
+ password: redefinePassword,
+ passwordConfirm: redefinePasswordConfirm,
+ }),
+ [redefinePassword, redefinePasswordConfirm]
+ );
+
+ const handleChangeUserValues = (e) => {
+ setUserValues({ ...userValues, [e.target.name]: e.target.value });
+ };
+
+ const handleFocusOut = (e) => {
+ const { name, value } = e.target;
+
+ // 검증할 요소인지 확인
+ if (!VALIDATOR[name]) return;
+
+ setValueValids(() => ({
+ ...valueValids,
+ [name]: VALIDATOR[name](value),
+ }));
+ };
+
+ const handleClickSubmit = (e) => {
+ e.preventDefault();
+ nav("/login");
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SignupPage;
diff --git a/src/pages/SignupPage/SignupPage.module.scss b/src/pages/SignupPage/SignupPage.module.scss
new file mode 100644
index 00000000..b921de0b
--- /dev/null
+++ b/src/pages/SignupPage/SignupPage.module.scss
@@ -0,0 +1,13 @@
+@use "../../styles/mixin" as mixin;
+
+.signupPage {
+ padding: 60px 0;
+
+ @include mixin.tablet {
+ padding: 48px 0;
+ }
+
+ @include mixin.mobile {
+ padding: 24px 0;
+ }
+}
diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js
deleted file mode 100644
index 5253d3ad..00000000
--- a/src/reportWebVitals.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const reportWebVitals = onPerfEntry => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/src/routes.js b/src/routes.js
new file mode 100644
index 00000000..6af0c538
--- /dev/null
+++ b/src/routes.js
@@ -0,0 +1,32 @@
+import Layout from "./layouts/Layout";
+import MainPage from "./pages/MainPage/MainPage";
+import LoginPage from "./pages/LoginPage/LoginPage";
+import SignupPage from "./pages/SignupPage/SignupPage";
+import ItemsPage from "./pages/ItemsPage/ItemsPage";
+import PrivacyPage from "./pages/PrivacyPage/PrivacyPage";
+import FaqPage from "./pages/FaqPage/FaqPage";
+import AddItemPage from "./pages/AddItemPage/AddItemPage";
+
+const routes = [
+ {
+ path: "/",
+ element: ,
+ children: [
+ { path: "/", element: },
+ { path: "/items", element: },
+ { path: "/additem", element: },
+ { path: "/privacy", element: },
+ { path: "/faq", element: },
+ ],
+ },
+ {
+ path: "/login",
+ element: ,
+ },
+ {
+ path: "/signup",
+ element: ,
+ },
+];
+
+export default routes;
diff --git a/src/setupTests.js b/src/setupTests.js
deleted file mode 100644
index 8f2609b7..00000000
--- a/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
diff --git a/src/styles/_button.scss b/src/styles/_button.scss
new file mode 100644
index 00000000..3186dc9d
--- /dev/null
+++ b/src/styles/_button.scss
@@ -0,0 +1,60 @@
+@use "variables" as var;
+@use "mixin" as mixin;
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 42px;
+ padding: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ color: #fff;
+ background-color: var.$btn-primary;
+ border-radius: 8px;
+ transition: 0.2s ease;
+
+ &:hover {
+ background-color: var.$btn-hover;
+ }
+
+ &:active {
+ background-color: var.$btn-click;
+ }
+
+ &:disabled {
+ background-color: var.$btn-disabled;
+ }
+
+ &.sm {
+ min-width: 88px;
+ }
+
+ &.md {
+ min-width: 240px;
+ height: 48px;
+ font-size: 18px;
+ border-radius: 40px;
+ }
+
+ &.lg {
+ min-width: 357px;
+ height: 56px;
+ font-size: 20px;
+ border-radius: 40px;
+ }
+
+ &.h48 {
+ height: 48px;
+ }
+}
+
+@include mixin.mobile {
+ .btn {
+ &.lg {
+ min-width: 240px;
+ height: 48px;
+ font-size: 18px;
+ }
+ }
+}
diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss
new file mode 100644
index 00000000..585ad47a
--- /dev/null
+++ b/src/styles/_fonts.scss
@@ -0,0 +1,2 @@
+/* Pretendard Variable */
+@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css");
diff --git a/src/styles/_layout.scss b/src/styles/_layout.scss
new file mode 100644
index 00000000..e16da7b7
--- /dev/null
+++ b/src/styles/_layout.scss
@@ -0,0 +1,163 @@
+@use "variables" as var;
+@use "mixin" as mixin;
+
+@mixin layoutInner {
+ max-width: 1120px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+#wrap {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background: #fcfcfc;
+}
+
+// header
+#header {
+ position: sticky;
+ top: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 70px;
+ padding: 0 200px;
+ border-bottom: 1px solid #dfdfdf;
+ background-color: var.$white;
+ z-index: 100;
+ flex: 0 0 auto;
+
+ .inner {
+ display: flex;
+ align-items: center;
+ @include layoutInner;
+ }
+
+ @include mixin.tablet {
+ padding: 0 24px;
+ }
+
+ @include mixin.mobile {
+ padding: 0 16px;
+ }
+
+ .header {
+ &__logo {
+ a {
+ display: block;
+
+ @include mixin.mobile {
+ width: 81px;
+ }
+ }
+ }
+
+ &__gnb {
+ margin: 0 32px;
+
+ @include mixin.tablet {
+ margin: 0 20px;
+ }
+
+ @include mixin.mobile {
+ margin: 0 8px;
+ }
+
+ ul {
+ display: flex;
+ align-items: center;
+
+ @include mixin.mobile {
+ gap: 8px;
+ }
+
+ li {
+ a {
+ display: block;
+ padding: 20px 15px;
+ font-size: 18px;
+ font-weight: 700;
+ color: var.$gray600;
+
+ &.current {
+ color: var.$primary-color;
+ }
+
+ @include mixin.mobile {
+ padding: 20px 0;
+ }
+ }
+ }
+ }
+ }
+
+ &__member {
+ margin-left: auto;
+ }
+
+ &__login-link {
+ width: 128px;
+ }
+ }
+}
+// container
+#container {
+ flex: 1;
+}
+
+// footer
+#footer {
+ padding: 32px 104px 108px;
+ font-weight: 300;
+ background-color: var.$footer-bg;
+ flex: 0 0 auto;
+
+ @include mixin.mobile {
+ padding: 32px 32px 30px;
+ }
+
+ .inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ @include layoutInner;
+
+ @include mixin.mobile {
+ flex-wrap: wrap;
+ gap: 60px 36px;
+ }
+ }
+
+ .footer {
+ &__copy {
+ color: #9ca3af;
+
+ @include mixin.mobile {
+ width: 100%;
+ order: 3;
+ }
+ }
+
+ &__menu-list {
+ display: flex;
+ align-items: center;
+ color: #e5e7eb;
+ gap: 30px;
+
+ a {
+ display: block;
+ }
+ }
+
+ &__sns-list {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ a {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/src/styles/_mixin.scss b/src/styles/_mixin.scss
new file mode 100644
index 00000000..0a90577d
--- /dev/null
+++ b/src/styles/_mixin.scss
@@ -0,0 +1,21 @@
+@use "variables" as var;
+
+@mixin tablet {
+ @media (max-width: var.$tablet-size) {
+ @content;
+ }
+}
+
+@mixin mobile {
+ @media (max-width: var.$mobile-size) {
+ @content;
+ }
+}
+
+@mixin ellipsis($line: 1) {
+ display: -webkit-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: $line;
+ -webkit-box-orient: vertical;
+}
diff --git a/src/styles/_reset.scss b/src/styles/_reset.scss
new file mode 100644
index 00000000..8a425bb4
--- /dev/null
+++ b/src/styles/_reset.scss
@@ -0,0 +1,50 @@
+@use "variables" as var;
+
+/* ====== reset ====== */
+* {
+ margin: 0;
+ padding: 0;
+ font: inherit;
+ box-sizing: border-box;
+ word-break: keep-all;
+ outline: none;
+}
+
+html,
+body {
+ font-family: "Pretendard Variable", Pretendard, sans-serif;
+ color: #374151;
+ font-size: 16px;
+ line-height: 1.4;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+ul,
+ol {
+ list-style: none;
+}
+
+input {
+ border: none;
+ outline: none;
+
+ &::placeholder {
+ color: var.$gray400;
+ }
+}
+
+button {
+ background: none;
+ border: none;
+ cursor: pointer;
+}
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
new file mode 100644
index 00000000..98498077
--- /dev/null
+++ b/src/styles/_variables.scss
@@ -0,0 +1,26 @@
+// colors
+$primary-color: #3692ff;
+$primary-bg: #cfe5ff;
+$white: #fff;
+$gray50: #f9fafb;
+$gray100: #f3f4f6;
+$gray200: #e5e7eb;
+$gray400: #9ca3af;
+$gray500: #6b7280;
+$gray600: #4b5563;
+$gray700: #374151;
+$gray800: #1f2937;
+$gray900: #111827;
+$error: #f74747;
+
+$footer-bg: #111827;
+
+// btn bg color
+$btn-primary: $primary-color;
+$btn-hover: #1967d6;
+$btn-click: #1251aa;
+$btn-disabled: #9ca3af;
+
+// responsive
+$tablet-size: 1199px;
+$mobile-size: 767px;
diff --git a/src/styles/auth.scss b/src/styles/auth.scss
new file mode 100644
index 00000000..5340df14
--- /dev/null
+++ b/src/styles/auth.scss
@@ -0,0 +1,140 @@
+@use "variables" as var;
+@use "mixin" as mixin;
+
+.auth {
+ &-container {
+ max-width: 640px;
+ width: calc(100% - 30px);
+ margin: 0 auto;
+
+ @include mixin.mobile {
+ max-width: 400px;
+ }
+
+ .logo {
+ img {
+ margin: 0 auto;
+
+ @include mixin.mobile {
+ width: 198px;
+ }
+ }
+ }
+ }
+
+ &-form {
+ margin-top: 40px;
+
+ @include mixin.mobile {
+ margin-top: 24px;
+ }
+
+ &__item {
+ margin-bottom: 24px;
+
+ @include mixin.mobile {
+ font-size: 16px;
+ }
+ }
+
+ &__label {
+ display: block;
+ margin-bottom: 16px;
+ font-size: 18px;
+ font-weight: 700;
+ color: var.$gray800;
+
+ @include mixin.mobile {
+ font-size: 14px;
+ }
+ }
+
+ &__input-box {
+ position: relative;
+
+ input {
+ display: block;
+ width: 100%;
+ height: 56px;
+ padding: 0 24px;
+ font-size: 16px;
+ font-weight: 400;
+ color: var.$gray800;
+ background: var.$gray100;
+ border-radius: 12px;
+
+ &.isError {
+ border: 1px solid var.$error;
+ }
+
+ &.isPass {
+ border: 1px solid var.$primary-color;
+ }
+ }
+
+ &--pw {
+ input {
+ padding-right: 60px;
+ }
+ }
+ }
+
+ &__toggle-btn {
+ position: absolute;
+ top: 0;
+ right: 12px;
+ height: 100%;
+ padding: 0 12px;
+ }
+
+ &__submit-btn {
+ width: 100%;
+ }
+
+ &__error-msg {
+ padding: 8px 16px 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: var.$error;
+ }
+ }
+
+ &-sns {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 24px;
+ padding: 16px 23px;
+ background-color: #e6f2ff;
+ border-radius: 8px;
+
+ &__label {
+ display: block;
+ font-size: 16px;
+ font-weight: 500;
+ }
+
+ &__list {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+ }
+
+ &-guide {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 24px;
+ font-size: 14px;
+ font-weight: 500;
+ gap: 5px;
+
+ &__link {
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ text-decoration-thickness: 2px;
+ color: var.$primary-color;
+ }
+ }
+}
diff --git a/src/styles/style.scss b/src/styles/style.scss
new file mode 100644
index 00000000..bcc6c4e1
--- /dev/null
+++ b/src/styles/style.scss
@@ -0,0 +1,4 @@
+@use "fonts";
+@use "reset";
+@use "button";
+@use "layout";
diff --git a/src/utils/authUtils.js b/src/utils/authUtils.js
new file mode 100644
index 00000000..e5367792
--- /dev/null
+++ b/src/utils/authUtils.js
@@ -0,0 +1,61 @@
+// 닉네임 검사
+export function checkValidNickname(value) {
+ // 빈값 확인
+ if (!value.trim().length)
+ return { isValid: false, msg: "닉네임을 입력해주세요." };
+
+ // 유효성 검사 통과시
+ return { isValid: true, msg: "" };
+}
+
+// 이메일 검사
+export function checkValidEmail(value) {
+ // 빈값 확인
+ if (!value.trim().length)
+ return { isValid: false, msg: "이메일을 입력해주세요." };
+
+ // 유효성 검사
+ const PATTERN =
+ /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;
+ if (!PATTERN.test(value))
+ return { isValid: false, msg: "잘못된 이메일 형식입니다." };
+
+ // 유효성 검사 통과시
+ return { isValid: true, msg: "" };
+}
+
+// 비밀번호 검사
+export function checkValidPassword(value) {
+ // 빈값 확인
+ if (!value.trim().length)
+ return { isValid: false, msg: "비밀번호를 입력해주세요." };
+
+ // 유효성 검사
+ const PATTERN = /^[0-9a-zA-Z]{8}/;
+ if (!PATTERN.test(value))
+ return { isValid: false, msg: "비밀번호를 8자 이상 입력해주세요." };
+
+ // 유효성 검사 통과시
+ return { isValid: true, msg: "" };
+}
+
+// 비밀번호 확인 검사
+export function checkValidPasswordConfirm(value, password) {
+ // 빈값 확인
+ if (!value.trim().length)
+ return { isValid: false, msg: "비밀번호를 입력해주세요." };
+
+ // 유효성 검사
+ if (value !== password)
+ return { isValid: false, msg: "비밀번호가 일치하지 않습니다." };
+
+ // 유효성 검사 통과시
+ return { isValid: true, msg: "" };
+}
+
+// 인풋 유효성 검사 결과에 따라 클래스명 전달
+export function getAuthValidClassName(isValid) {
+ if (isValid === null) return "";
+
+ return isValid ? "isPass" : "isError";
+}
diff --git a/src/utils/getItemCount.js b/src/utils/getItemCount.js
new file mode 100644
index 00000000..7a650ba1
--- /dev/null
+++ b/src/utils/getItemCount.js
@@ -0,0 +1,12 @@
+export const getItemCount = (itemCountInfo) => {
+ const viewWidth = window.innerWidth;
+ if (viewWidth <= 767) {
+ // mobile 0 ~ 767
+ return itemCountInfo["MOBILE"];
+ } else if (viewWidth <= 1199) {
+ // tablet 768 ~ 1199
+ return itemCountInfo["TABLET"];
+ }
+ // web 1200 ~
+ return itemCountInfo["WEB"];
+};
diff --git a/src/utils/getLogo.js b/src/utils/getLogo.js
new file mode 100644
index 00000000..c293b011
--- /dev/null
+++ b/src/utils/getLogo.js
@@ -0,0 +1,15 @@
+import logoLg from "../assets/images/common/logo_lg.svg";
+import logoMd from "../assets/images/common/logo_md.svg";
+import logoSm from "../assets/images/common/logo_sm.svg";
+import logoSx from "../assets/images/common/logo_sx.svg";
+
+const logoSize = {
+ lg: logoLg,
+ md: logoMd,
+ sm: logoSm,
+ sx: logoSx,
+};
+
+const getLogo = (size) => logoSize[size];
+
+export default getLogo;