diff --git a/package-lock.json b/package-lock.json index d9442d2a..4f242d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,15 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.15.21", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^7.6.0", "react-scripts": "5.0.1", + "typescript": "^5.8.3", "web-vitals": "^2.1.4" } }, @@ -3775,6 +3780,26 @@ "node": ">=12" } }, + "node_modules/@testing-library/react/node_modules/@types/react": { + "version": "18.3.22", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", + "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@testing-library/react/node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@testing-library/react/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4047,9 +4072,10 @@ } }, "node_modules/@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -4298,9 +4324,13 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "node_modules/@types/node": { - "version": "20.5.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", - "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==" + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4313,9 +4343,11 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT", + "peer": true }, "node_modules/@types/q": { "version": "1.5.6", @@ -4333,21 +4365,21 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", - "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", + "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", - "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", - "dependencies": { - "@types/react": "*" + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", + "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/resolve": { @@ -4363,11 +4395,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" - }, "node_modules/@types/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", @@ -16678,16 +16705,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -16709,6 +16736,12 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/package.json b/package.json index cc88c710..44c55f5a 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,15 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.15.21", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^7.6.0", "react-scripts": "5.0.1", + "typescript": "^5.8.3", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/src/App.css b/src/App.css index c0a39e74..55705eab 100644 --- a/src/App.css +++ b/src/App.css @@ -10,457 +10,9 @@ height: 100%; } -header { - display: flex; - justify-content: center; - align-items: center; - background-color: #ffffff; - width: 100%; - height: 70px; - font-family: "Pretendard", sans-serif; -} - -.header-inner { - width: 100%; - max-width: 1120px; /* 최대 간격 제한 */ - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-logo img { - width: 153px; - height: 51px; -} - -.login-button { - display: flex; /* 플렉스 박스를 사용 */ - justify-content: center; /* 가로 방향 중앙 정렬 */ - align-items: center; /* 세로 방향 중앙 정렬 */ - text-align: center; /* 글자 중앙 정렬 */ - background-color: #3692ff; - width: 128px; - height: 48px; - padding: 12px 23px; - gap: 10px; - border-radius: 8px; - color: #f3f4f6; - font-size: 16px; - font-weight: 600; - line-height: 32px; - letter-spacing: 0%; - text-decoration: none; -} - body { - margin: 0; - padding: 0; box-sizing: border-box; - word-break: keep-all; /* 띄어쓰기 기준으로 줄바꿈하도록 설정 */ - font-family: "Pretendard", sans-serif; + word-break: keep-all; overflow-x: hidden; -} - -.banner { - position: relative; - display: flex; - justify-content: center; - align-items: end; - background-color: #cfe5ff; - width: 100vw; /* 화면 꽉 채우기 */ - height: 540px; - box-sizing: border-box; - gap: 7px; - background-position: 80% bottom; -} - -.img-panda { - width: 100%; - height: auto; - min-width: 448px; - max-width: 746px; /* 적절한 최대 너비 */ - object-fit: contain; -} - -#features { - padding: 24px 24px 56px 24px; -} - -.feature { background-color: #fcfcfc; - padding: 138px 0; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - border-radius: 12px; - padding: 24px 0; - gap: 52px; -} - -.feature-title { - font-size: 28px; - line-height: 40px; - white-space: normal; - word-break: keep-all; - max-width: 205px; -} - -.feature-content { - display: flex; - flex-direction: column; - gap: 8px; - text-align: left; - align-items: flex-start; -} - -.feature:nth-child(2) .feature-content { - text-align: right; - align-items: flex-end; -} - -.feature-content h1 { - font-size: 28px; - line-height: 40px; - white-space: normal; -} - -.feature-description { - font-size: 16px; - line-height: 24px; -} - -/* - .feature-content { - flex: 1; - } -*/ - -/* 요소 최대 넓이보다 큰 경우에 가로 중앙 정렬 해주는 inner container */ -.wrapper { - max-width: 1200px; - margin: 0 auto; - width: 100%; - display: flex; - justify-content: center; -} - -#top-banner-title { - padding: 0; -} - -#bottom-banner-title { - padding: 112.5px 0 0; -} - -h1 { - color: #374151; - font-size: 40px; - font-weight: 700; - line-height: 56px; - letter-spacing: 0%; - padding: 12px 0; -} - -.feature-tag { - font-family: "Pretendard", sans-serif; - color: #3692ff; - font-weight: 700; - font-size: 18px; - line-height: 26px; - letter-spacing: 0%; -} - -.feature-description { - font-family: "Pretendard"; - color: #374151; - font-weight: 500; - font-size: 24px; - line-height: 32px; - letter-spacing: 2%; - padding: 12px 0 0 0; -} - -.hero-button { - font-family: "Pretendard", sans-serif; - background-color: #3692ff; - color: #f9fafb; - font-size: 20px; - font-weight: 600; - text-decoration: none; - border: none; - border-radius: 40px; - padding: 16px 124px; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 10px; - margin-top: 32px; -} - -footer { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - background-color: #111827; - font-family: "Pretendard"; - line-height: 100%; - padding: 32px 300px 108px; - gap: 10px; -} - -.copyright { - text-align: left; - font-size: 16px; - font-weight: 400; - color: #e5e7eb; - letter-spacing: 0%; - line-height: 19.09px; -} - -.footerMenu { - display: flex; - gap: 30px; -} - -.footer-link { - color: #e5e7eb; - text-decoration: none; - font-size: 16px; - font-weight: 400; - transition: color 0.3s ease; -} - -.footer-link:hover, -.footer-link:visited, -.footer-link:active { - color: #e5e7eb; - text-decoration: none; -} - -.socialMedia { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 12px; -} - -.socialMedia > img { - width: 20px; - height: 20px; -} - -/* Mobile, Tablet 공통 (최대 1199px) */ -@media screen and (max-width: 1199px) { - .wrapper { - flex-direction: column; - align-items: center; - text-align: center; - } - - .feature { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding: 0; - gap: 24px; - } - - .feature img { - width: 100%; - height: auto; - object-fit: cover; - } - - .feature-content { - text-align: left; - align-items: flex-start; - width: 100%; - margin-bottom: 52px; - } - - .feature-content h1 { - white-space: nowrap; - max-width: 100%; - } - - .feature:nth-child(2) { - flex-direction: column-reverse; - } - - .feature:nth-child(2) .feature-content { - align-items: flex-end; - text-align: right; - } -} - -/* Tablet: 768px 이상 ~ 1199px 이하 */ -@media (min-width: 768px) and (max-width: 1199px) { - .header-inner { - padding: 0 24px; - } - - .hero-button { - white-space: nowrap; - width: 357px; - min-height: 56px; - } - - .banner { - height: auto; - padding: 84px 0 0 0; - background-size: 120%; - } - - .banner h1 { - font-size: 40px; - line-height: 56px; - padding-top: 84px; - padding-bottom: 24px; - } - - .img-panda { - width: 100%; - height: auto; - max-width: 100%; - margin-top: 211px; - } - - #features { - padding-bottom: 0; - } - - .feature-content h2 { - font-size: 18px; - line-height: 26px; - padding: 0; - } - - .feature-content h1 { - font-size: 32px; - line-height: 42px; - padding: 16px 0 24px; - } - - .feature-description { - font-size: 18px; - line-height: 26px; - padding: 0; - } - - #bottomBanner .wrapper { - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - gap: 6px; - } - - #bottom-banner-title { - padding: 0; - } - - footer { - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 32px 104px 108px; - text-align: center; - } - - .footerMenu { - flex-direction: row; - justify-content: center; - flex-wrap: wrap; - } - - .socialMedia { - justify-content: center; - } -} - -/* Mobile: 375px 이상 ~ 767px 이하 */ -@media (min-width: 375px) and (max-width: 767px) { - .header-inner { - padding: 0 16px; - } - - .hero-button { - white-space: nowrap; - width: 240px; - min-height: 48px; - } - - .banner { - height: auto; - padding: 48px 0 0 0; - background-size: 120%; - } - - .img-panda { - width: 100%; - height: auto; - max-width: 100%; - margin-top: 132px; - } - - #features { - padding: 52px 15px 0; - } - - .feature-content h2 { - font-size: 16px; - line-height: 26px; - padding: 0; - } - - .feature-content h1 { - font-size: 24px; - line-height: 32px; - padding: 8px 0 16px; - } - - .feature-description { - font-size: 16px; - line-height: 26px; - padding: 0; - } - - footer { - padding: 32px; - flex-direction: row; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - } - - .footerMenu, - .socialMedia { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - margin-bottom: 60px; - gap: 12px; - } - - .footerMenu { - display: flex; - flex-direction: row; - gap: 30px; - } - - .socialMedia { - display: flex; - flex-direction: row; - gap: 12px; - } - - .copyright { - order: 2; - width: 100%; - text-align: left; - } } diff --git a/src/components/layout/Navbar.jsx b/src/components/layout/Navbar.jsx index 1b29522b..9b4057ff 100644 --- a/src/components/layout/Navbar.jsx +++ b/src/components/layout/Navbar.jsx @@ -10,14 +10,14 @@ export default function Navbar() { return (
-
+ 판다 로고 판다 모바일 로고 -
+
-
- -
-
- -
-
- Hot item -
-

Hot item

-

인기 상품을 확인해 보세요

-

- 가장 HOT한 중고거래 물품을 -
- 판다 마켓에서 확인해 보세요 -

-
-
-
-
-

Search

-

구매를 원하는 상품을 검색하세요

-

- 구매하고 싶은 물품은 검색해서 -
- 쉽게 찾아보세요 -

-
- Search -
-
- Register -
-

Register

-

판매를 원하는 상품을 등록하세요

-

- 어떤 물건이든 판매하고 싶은 상품을 -
- 쉽게 등록하세요 -

-
-
-
- -
- - - ); -} diff --git a/src/pages/landing/Landing.tsx b/src/pages/landing/Landing.tsx new file mode 100644 index 00000000..0747a742 --- /dev/null +++ b/src/pages/landing/Landing.tsx @@ -0,0 +1,148 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import "./landing.css"; + +import mainLogo from "../../asset/icon/panda_market_logo_3.png"; +import landingImg1 from "../../asset/image/Img_home_01.png"; +import landingImg2 from "../../asset/image/Img_home_02.png"; +import landingImg3 from "../../asset/image/Img_home_03.png"; +import landingImgTop from "../../asset/image/Img_home_top.png"; +import landingImgBottom from "../../asset/image/Img_home_bottom.png"; + +export default function Home(): JSX.Element { + return ( + <> +
+
+ + 판다마켓 로고 + + + 로그인 + +
+
+
+
+
+
+

+ 일상의 모든 물건을 +
+ 거래해 보세요 +

+ + 구경하러 가기 + +
+ 랜딩페이지 상단 이미지 +
+
+ +
+
+ Hot item +
+

Hot item

+

인기 상품을 확인해 보세요

+

+ 가장 HOT한 중고거래 물품을 +
+ 판다 마켓에서 확인해 보세요 +

+
+
+ +
+
+

Search

+

구매를 원하는 상품을 검색하세요

+

+ 구매하고 싶은 물품은 검색해서 +
+ 쉽게 찾아보세요 +

+
+ Search +
+ +
+ Register +
+

Register

+

판매를 원하는 상품을 등록하세요

+

+ 어떤 물건이든 판매하고 싶은 상품을 +
+ 쉽게 등록하세요 +

+
+
+
+ +
+
+
+

+ 믿을 수 있는 +
+ 판다마켓 중고 거래 +

+
+ 랜딩페이지 하단 이미지 +
+
+
+ + + + ); +} diff --git a/src/pages/landing/landing.css b/src/pages/landing/landing.css new file mode 100644 index 00000000..7edaa096 --- /dev/null +++ b/src/pages/landing/landing.css @@ -0,0 +1,445 @@ +header { + display: flex; + justify-content: center; + align-items: center; + background-color: #ffffff; + width: 100%; + height: 70px; + font-family: "Pretendard", sans-serif; +} + +.header-inner { + width: 100%; + max-width: 1120px; /* 최대 간격 제한 */ + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-logo img { + width: 153px; + height: 51px; +} + +.login-button { + display: flex; /* 플렉스 박스를 사용 */ + justify-content: center; /* 가로 방향 중앙 정렬 */ + align-items: center; /* 세로 방향 중앙 정렬 */ + text-align: center; /* 글자 중앙 정렬 */ + background-color: #3692ff; + width: 128px; + height: 48px; + padding: 12px 23px; + gap: 10px; + border-radius: 8px; + color: #f3f4f6; + font-size: 16px; + font-weight: 600; + line-height: 32px; + letter-spacing: 0%; + text-decoration: none; +} + +.banner { + position: relative; + display: flex; + justify-content: center; + align-items: end; + background-color: #cfe5ff; + width: 100vw; /* 화면 꽉 채우기 */ + height: 540px; + box-sizing: border-box; + gap: 7px; + background-position: 80% bottom; +} + +.img-panda { + width: 100%; + height: auto; + min-width: 448px; + max-width: 746px; /* 적절한 최대 너비 */ + object-fit: contain; +} + +#features { + padding: 24px 24px 56px 24px; +} + +.feature { + background-color: #fcfcfc; + padding: 138px 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + border-radius: 12px; + padding: 24px 0; + gap: 52px; +} + +.feature-title { + font-size: 28px; + line-height: 40px; + white-space: normal; + word-break: keep-all; + max-width: 205px; +} + +.feature-content { + display: flex; + flex-direction: column; + gap: 8px; + text-align: left; + align-items: flex-start; +} + +.feature:nth-child(2) .feature-content { + text-align: right; + align-items: flex-end; +} + +.feature-content h1 { + font-size: 28px; + line-height: 40px; + white-space: normal; +} + +.feature-description { + font-size: 16px; + line-height: 24px; +} + +/* + .feature-content { + flex: 1; + } +*/ + +/* 요소 최대 넓이보다 큰 경우에 가로 중앙 정렬 해주는 inner container */ +.wrapper { + max-width: 1200px; + margin: 0 auto; + width: 100%; + display: flex; + justify-content: center; +} + +#top-banner-title { + padding: 0; +} + +#bottom-banner-title { + padding: 112.5px 0 0; +} + +h1 { + color: #374151; + font-size: 40px; + font-weight: 700; + line-height: 56px; + letter-spacing: 0%; + padding: 12px 0; +} + +.feature-tag { + font-family: "Pretendard", sans-serif; + color: #3692ff; + font-weight: 700; + font-size: 18px; + line-height: 26px; + letter-spacing: 0%; +} + +.feature-description { + font-family: "Pretendard"; + color: #374151; + font-weight: 500; + font-size: 24px; + line-height: 32px; + letter-spacing: 2%; + padding: 12px 0 0 0; +} + +.hero-button { + font-family: "Pretendard", sans-serif; + background-color: #3692ff; + color: #f9fafb; + font-size: 20px; + font-weight: 600; + text-decoration: none; + border: none; + border-radius: 40px; + padding: 16px 124px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 32px; +} + +footer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background-color: #111827; + font-family: "Pretendard"; + line-height: 100%; + padding: 32px 300px 108px; + gap: 10px; +} + +.copyright { + text-align: left; + font-size: 16px; + font-weight: 400; + color: #e5e7eb; + letter-spacing: 0%; + line-height: 19.09px; +} + +.footerMenu { + display: flex; + gap: 30px; +} + +.footer-link { + color: #e5e7eb; + text-decoration: none; + font-size: 16px; + font-weight: 400; + transition: color 0.3s ease; +} + +.footer-link:hover, +.footer-link:visited, +.footer-link:active { + color: #e5e7eb; + text-decoration: none; +} + +.socialMedia { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 12px; +} + +.socialMedia > img { + width: 20px; + height: 20px; +} + +/* Mobile, Tablet 공통 (최대 1199px) */ +@media screen and (max-width: 1199px) { + .wrapper { + flex-direction: column; + align-items: center; + text-align: center; + } + + .feature { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 0; + gap: 24px; + } + + .feature img { + width: 100%; + height: auto; + object-fit: cover; + } + + .feature-content { + text-align: left; + align-items: flex-start; + width: 100%; + margin-bottom: 52px; + } + + .feature-content h1 { + white-space: nowrap; + max-width: 100%; + } + + .feature:nth-child(2) { + flex-direction: column-reverse; + } + + .feature:nth-child(2) .feature-content { + align-items: flex-end; + text-align: right; + } +} + +/* Tablet: 768px 이상 ~ 1199px 이하 */ +@media (min-width: 768px) and (max-width: 1199px) { + .header-inner { + padding: 0 24px; + } + + .hero-button { + white-space: nowrap; + width: 357px; + min-height: 56px; + } + + .banner { + height: auto; + padding: 84px 0 0 0; + background-size: 120%; + } + + .banner h1 { + font-size: 40px; + line-height: 56px; + padding-top: 84px; + padding-bottom: 24px; + } + + .img-panda { + width: 100%; + height: auto; + max-width: 100%; + margin-top: 211px; + } + + #features { + padding-bottom: 0; + } + + .feature-content h2 { + font-size: 18px; + line-height: 26px; + padding: 0; + } + + .feature-content h1 { + font-size: 32px; + line-height: 42px; + padding: 16px 0 24px; + } + + .feature-description { + font-size: 18px; + line-height: 26px; + padding: 0; + } + + #bottomBanner .wrapper { + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 6px; + } + + #bottom-banner-title { + padding: 0; + } + + footer { + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 32px 104px 108px; + text-align: center; + } + + .footerMenu { + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + } + + .socialMedia { + justify-content: center; + } +} + +/* Mobile: 375px 이상 ~ 767px 이하 */ +@media (min-width: 375px) and (max-width: 767px) { + .header-inner { + padding: 0 16px; + } + + .hero-button { + white-space: nowrap; + width: 240px; + min-height: 48px; + } + + .banner { + height: auto; + padding: 48px 0 0 0; + background-size: 120%; + } + + .img-panda { + width: 100%; + height: auto; + max-width: 100%; + margin-top: 132px; + } + + #features { + padding: 52px 15px 0; + } + + .feature-content h2 { + font-size: 16px; + line-height: 26px; + padding: 0; + } + + .feature-content h1 { + font-size: 24px; + line-height: 32px; + padding: 8px 0 16px; + } + + .feature-description { + font-size: 16px; + line-height: 26px; + padding: 0; + } + + footer { + padding: 32px; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + } + + .footerMenu, + .socialMedia { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin-bottom: 60px; + gap: 12px; + } + + .footerMenu { + display: flex; + flex-direction: row; + gap: 30px; + } + + .socialMedia { + display: flex; + flex-direction: row; + gap: 12px; + } + + .copyright { + order: 2; + width: 100%; + text-align: left; + } +} diff --git a/src/pages/login/Login.jsx b/src/pages/login/Login.tsx similarity index 82% rename from src/pages/login/Login.jsx rename to src/pages/login/Login.tsx index f7cae645..b3bd9e1a 100644 --- a/src/pages/login/Login.jsx +++ b/src/pages/login/Login.tsx @@ -17,7 +17,7 @@ export default function Login() { const [emailError, setEmailError] = useState(""); const [passwordError, setPasswordError] = useState(""); - const validateEmail = (value) => { + const validateEmail = (value: string) => { const trimmed = value.trim(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -26,7 +26,7 @@ export default function Login() { return ""; }; - const validatePassword = (value) => { + const validatePassword = (value: string) => { const trimmed = value.trim(); if (!trimmed) return "비밀번호를 입력해주세요."; if (trimmed.length < 8) return "비밀번호를 8자 이상 입력해주세요."; @@ -48,7 +48,7 @@ export default function Login() { const isFormValid = validateEmail(email) === "" && validatePassword(password) === ""; - const handleLogin = async (e) => { + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); if (!isFormValid) return; @@ -129,18 +129,24 @@ export default function Login() {
- 간편 로그인하기 -
- - 구글 간편 로그인 - - - 카카오톡 간편 로그인 - +
+ 간편 로그인하기 +
diff --git a/src/pages/login/login.css b/src/pages/login/login.css index c3f2416b..583a23e7 100644 --- a/src/pages/login/login.css +++ b/src/pages/login/login.css @@ -1,12 +1,3 @@ -body { - font-family: "Pretendard", sans-serif; - margin: 0; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; -} - .header { display: flex; justify-content: center; @@ -20,9 +11,10 @@ body { height: 132px; } -main { +.login-page { width: 640px; height: 618px; + margin: auto; } .login-container { @@ -103,7 +95,23 @@ button:focus { outline-color: #3692ff; } -.easy-login span { +.easy-login { + background-color: #e6f2ff; + border-radius: 8px; + padding: 16px 23px; + margin-bottom: 24px; + min-height: 42px; +} + +.easy-login-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.easy-login-text { + display: inline-block; margin-top: 0; margin-bottom: 0; font-size: 16px; @@ -113,18 +121,7 @@ button:focus { white-space: nowrap; } -.easy-login { - display: flex; - justify-content: space-between; - align-items: center; - background-color: #e6f2ff; - border-radius: 8px; - padding: 16px 23px; - margin-bottom: 24px; -} - .login-icon { - box-sizing: border-box; display: flex; justify-content: center; align-items: center; @@ -132,7 +129,6 @@ button:focus { } .login-icon img { - box-sizing: border-box; width: 42px; height: 42px; } diff --git a/src/pages/privacy/privacy.html b/src/pages/privacy/Privacy.tsx similarity index 100% rename from src/pages/privacy/privacy.html rename to src/pages/privacy/Privacy.tsx diff --git a/src/pages/signup/Signup.tsx b/src/pages/signup/Signup.tsx new file mode 100644 index 00000000..9d66fd44 --- /dev/null +++ b/src/pages/signup/Signup.tsx @@ -0,0 +1,207 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { Link } from "react-router-dom"; +import "./signup.css"; + +import mainLogo from "../../asset/icon/panda_market_logo_3.png"; +import eyeOffIcon from "../../asset/icon/btn_visibility_off.png"; +import eyeOnIcon from "../../asset/icon/btn_visibility_on.png"; + +export default function SignupPage() { + const emailRef = useRef(null); + const nicknameRef = useRef(null); + const passwordRef = useRef(null); + const passwordConfirmRef = useRef(null); + + const [emailError, setEmailError] = useState(""); + const [nicknameError, setNicknameError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [passwordCheckError, setPasswordCheckError] = useState(""); + + const [showPassword, setShowPassword] = useState(false); + const [showPasswordConfirm, setShowPasswordConfirm] = useState(false); + + const [isButtonEnabled, setIsButtonEnabled] = useState(false); + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + const validateAll = useCallback(() => { + const email = emailRef.current?.value.trim() ?? ""; + const nickname = nicknameRef.current?.value.trim() ?? ""; + const password = passwordRef.current?.value.trim() ?? ""; + const passwordConfirm = passwordConfirmRef.current?.value.trim() ?? ""; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const emailValid = !!email && emailRegex.test(email); + const nicknameValid = !!nickname; + const passwordValid = password.length >= 8; + const confirmValid = passwordConfirm === password && passwordConfirm !== ""; + + const noErrors = + !emailError && !nicknameError && !passwordError && !passwordCheckError; + + setIsButtonEnabled( + emailValid && nicknameValid && passwordValid && confirmValid && noErrors + ); + }, [emailError, nicknameError, passwordError, passwordCheckError]); + + useEffect(() => { + validateAll(); + }, [validateAll]); + + const handleBlur = (type: "email" | "nickname" | "password" | "confirm") => { + const email = emailRef.current?.value.trim() ?? ""; + const nickname = nicknameRef.current?.value.trim() ?? ""; + const password = passwordRef.current?.value.trim() ?? ""; + const confirm = passwordConfirmRef.current?.value.trim() ?? ""; + + switch (type) { + case "email": + if (!email) setEmailError("이메일을 입력해주세요."); + else if (!emailRegex.test(email)) + setEmailError("잘못된 이메일 형식입니다."); + else setEmailError(""); + break; + case "nickname": + if (!nickname) setNicknameError("닉네임을 입력해주세요."); + else setNicknameError(""); + break; + case "password": + if (!password) setPasswordError("비밀번호를 입력해주세요."); + else if (password.length < 8) + setPasswordError("비밀번호를 8자 이상 입력해주세요."); + else setPasswordError(""); + break; + case "confirm": + if (confirm !== password) + setPasswordCheckError("비밀번호가 일치하지 않습니다."); + else setPasswordCheckError(""); + break; + } + }; + + const handleSignup = () => { + if (isButtonEnabled) { + window.location.href = "/login/login.html"; + } + }; + + return ( +
+
+
+ + 판다마켓 로고 + +
+
+
+
{ + e.preventDefault(); + handleSignup(); + }} + > + + handleBlur("email")} + onInput={validateAll} + /> +
{emailError}
+ + handleBlur("nickname")} + onInput={validateAll} + /> +
{nicknameError}
+ +
+ handleBlur("password")} + onInput={validateAll} + /> + +
+
{passwordError}
+ +
+ handleBlur("confirm")} + onInput={validateAll} + /> + +
+
{passwordCheckError}
+ +
+ +
+ 간편 로그인하기 + +
+

+ 이미 회원이신가요? 로그인 +

+
+
+ ); +} diff --git a/src/pages/signup/signup.css b/src/pages/signup/signup.css index f5fa269b..ee5859ad 100644 --- a/src/pages/signup/signup.css +++ b/src/pages/signup/signup.css @@ -1,13 +1,3 @@ -body { - font-family: "Pretendard", sans-serif; - margin: 0; - background-color: #fcfcfc; - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; -} - .header { display: block; text-align: center; @@ -20,7 +10,7 @@ body { height: 132px; } -main { +.signup-page { width: 640px; height: 857px; margin: auto; @@ -117,7 +107,7 @@ button:focus { align-items: center; background-color: #e6f2ff; border-radius: 8px; - height: 42px; + min-height: 42px; padding: 16px 23px; margin-bottom: 24px; } @@ -159,9 +149,9 @@ button:focus { #toggle-password, #toggle-password-confirm { position: absolute; - right: 16px; + right: 24px; top: 50%; - transform: translateY(-110%); + transform: translateY(-50%); background: none; border: none; width: 24px; diff --git a/src/pages/signup/signup.html b/src/pages/signup/signup.html deleted file mode 100644 index b147723a..00000000 --- a/src/pages/signup/signup.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - 회원가입 페이지 - - - - -
-
- -
- -
- - - diff --git a/src/pages/signup/signup.js b/src/pages/signup/signup.js deleted file mode 100644 index c29696dc..00000000 --- a/src/pages/signup/signup.js +++ /dev/null @@ -1,171 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - // 요소 선택 - const emailInput = document.getElementById("email"); - const nicknameInput = document.getElementById("nickname"); - const passwordInput = document.getElementById("password"); - const passwordCheckInput = document.getElementById("password-confirm"); - const signupBtn = document.querySelector(".button-wide"); - - // 에러 발생 시, 에러 메시지를 보여주는 div 요소 생성 함수 - const createErrorMessage = (input, id) => { - let error = document.createElement("div"); - error.className = "error-message"; - error.id = id; - // insertAdjacentElement : 기준이 되는 요소 줍녀에 새 노드를 정확한 위치에 삽입 - // 기준요소.insertAdjacentElement(삽입위치문자열로, 삽입할노드) - input.insertAdjacentElement("afterend", error); - return error; - }; - - // 에러 메시지 DOM 만들기 - const emailError = createErrorMessage(emailInput, "email-error"); - const nicknameError = createErrorMessage(nicknameInput, "nickname-error"); - const passwordError = createErrorMessage(passwordInput, "password-error"); - const passwordCheckError = createErrorMessage( - passwordCheckInput, - "password-confirm-error" - ); - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - // 에러 보여주는 함수 - const showError = (input, errorElement, message) => { - errorElement.textContent = message; // 에러 메세지 div에 추가 - input.classList.add("error"); - }; - - // 에러 지우는 함수 - const clearError = (input, errorElement) => { - errorElement.textContent = ""; - input.classList.remove("error"); - }; - - // 버튼 활성화 상태 체크 - const updateButtonState = () => { - const emailValid = - emailInput.value.trim() && emailRegex.test(emailInput.value.trim()); - const nicknameValid = nicknameInput.value.trim(); - const passwordValid = passwordInput.value.trim().length >= 8; - const confirmValid = - !!passwordCheckInput.value.trim() && - passwordCheckInput.value.trim() === passwordInput.value.trim(); - - // 에러 메세지 유무 검사 - const noErrors = - !emailError.textContent && - !nicknameError.textContent && - !passwordError.textContent && - !passwordCheckError.textContent; - - if ( - emailValid && - nicknameValid && - passwordValid && - confirmValid && - noErrors - ) { - signupBtn.disabled = false; // 활성화 - } else { - signupBtn.disabled = true; // 하나라도 어긋날 시 비활성화 - } - }; - - // 이메일 blur - emailInput.addEventListener("blur", () => { - const value = emailInput.value.trim(); - if (!value) { - showError(emailInput, emailError, "이메일을 입력해주세요."); - } else if (!emailRegex.test(value)) { - showError(emailInput, emailError, "잘못된 이메일 형식입니다."); - } else { - clearError(emailInput, emailError); - } - updateButtonState(); // 버튼 활성화 여부 재판단 - }); - - // 닉네임 blur - nicknameInput.addEventListener("blur", () => { - const value = nicknameInput.value.trim(); - if (!value) { - showError(nicknameInput, nicknameError, "닉네임을 입력해주세요."); - } else { - clearError(nicknameInput, nicknameError); - } - updateButtonState(); - }); - - // 비밀번호 blur - passwordInput.addEventListener("blur", () => { - const value = passwordInput.value.trim(); - if (!value) { - showError(passwordInput, passwordError, "비밀번호를 입력해주세요."); - } else if (value.length < 8) { - showError( - passwordInput, - passwordError, - "비밀번호를 8자 이상 입력해주세요." - ); - } else { - clearError(passwordInput, passwordError); - } - updateButtonState(); - }); - - // 비밀번호 확인 blur - passwordCheckInput.addEventListener("blur", () => { - const pw = passwordInput.value.trim(); - const pwCheck = passwordCheckInput.value.trim(); - if (pw !== pwCheck) { - showError( - passwordCheckInput, - passwordCheckError, - "비밀번호가 일치하지 않습니다." - ); - } else { - clearError(passwordCheckInput, passwordCheckError); - } - updateButtonState(); - }); - - // 실시간 입력 시 버튼 상태 체크 - [emailInput, nicknameInput, passwordInput, passwordCheckInput].forEach( - (input) => input.addEventListener("input", updateButtonState) - ); - - // 버튼 클릭 시 로그인 페이지 이동 - signupBtn.addEventListener("click", () => { - if (!signupBtn.disabled) { - window.location.href = "/login/login.html"; - } - }); - - // 비밀번호 보기/숨기기 토글 (비밀번호) - const togglePassword = document.getElementById("toggle-password"); - togglePassword.addEventListener("click", () => { - const icon = togglePassword.querySelector("img"); - const isHidden = passwordInput.type === "password"; - - passwordInput.type = isHidden ? "text" : "password"; - icon.src = isHidden - ? "/images/btn_visibility_on_24px.png" - : "/images/btn_visibility_off_24px.png"; - }); - - // 비밀번호 확인 보기/숨기기 토글 - const togglePasswordConfirm = document.getElementById( - "toggle-password-confirm" - ); - togglePasswordConfirm.addEventListener("click", () => { - const icon = togglePasswordConfirm.querySelector("img"); - const confirmInput = document.getElementById("password-confirm"); - const isHidden = confirmInput.type === "password"; - - confirmInput.type = isHidden ? "text" : "password"; - icon.src = isHidden - ? "/images/btn_visibility_on_24px.png" - : "/images/btn_visibility_off_24px.png"; - }); - - // 초기 상태는 비활성화 - signupBtn.disabled = true; -}); diff --git a/src/routes/AppRouter.jsx b/src/routes/AppRouter.tsx similarity index 62% rename from src/routes/AppRouter.jsx rename to src/routes/AppRouter.tsx index adeef5f1..82598817 100644 --- a/src/routes/AppRouter.jsx +++ b/src/routes/AppRouter.tsx @@ -1,25 +1,24 @@ import { Routes, Route } from "react-router-dom"; import Login from "../pages/login/Login"; -// import LandingPage from "./pages/LandingPage"; -// import Login from "../pages/login/Login"; -// import Signup from "./pages/signup/Signup"; +import Landing from "../pages/landing/Landing"; +import Signup from "../pages/signup/Signup"; import Items from "../pages/items/Items"; import AddItem from "../pages/additem/AddItem"; import ItemsDetail from "../pages/items/itemsDetail/ItemsDetail"; // import FAQ from "./pages/faq/FAQ"; -// import PrivacyPolicy from "./pages/privacy/PrivacyPolicy"; +// import Privacy from "../pages/privacy/Privacy"; export default function AppRouter() { return ( - {/* } /> */} + } /> } /> - {/* } /> */} + } /> } /> } /> } /> {/* } /> */} - {/* } /> */} + {/* } /> */} ); } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..dc431fc3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "es5", + "lib": ["dom", "es2015"], + "allowJs": true, + "checkJs": false, + "skipLibCheck": true, + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", // or "react-jsx" for React 17+ + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +}