diff --git a/README.md b/README.md index 9fa347d00..48f6d97cd 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,23 @@ ## Week 2. 1단계 - 페이지 만들기 [🔗 link](https://edu.nextstep.camp/s/hazAC9xa/ls/QzV1ncxk) + +# STEP3 +📝질문 1. CRA 기반의 SPA프로젝트에서 React Router를 사용하지 않는다면 어떤 문제가 발생하나요? +--- +SPA는 하나의 페이지에서 모든 내용을 제공하는 웹 어플리케이션인데, 경로에 따라 다른 화면을 보여주기 위해 React Router를 사용한다. 그렇기에 React Router를 사용하지 않는다면 특정 페이지로 이동한든 것이 쉽지 않을 것이며 상태관리도 어렵고 페이지별로 컴포넌트를 분리하여 사용하는 것 또한 어려워질 것 이다. + +📝질문 2. 리액트 Context 나 Redux는 언제 사용하면 좋을까요? (로그인을 제외한 예시와 이유를 함께 적어주세요.) +--- + Context 나 Redux는 상태 관리 라이브러리 이다. 그렇기에 + ConText는 테마의 상태를 전역적으로 관리하는데 유용하게 쓰일 수 있을 것이다. 테마는 애플리케이션 전반적으로 영향을 미치고 여러 컴포넌트에서 접근할 필요가 있기 때문에 context를 사용한다면 쉽게 접근할 수 있을 것이다. +Redux는 전자상거래에 유용하게 쓰일 수 잇을 것이다. +대규모의 쇼핑몰을 운영하게 된다면 상태가 매우 복잡해질 것이고 여러 컴포넌트가 서로 다른 상태를 공유하고 업데이트 해야 하기에 Redux를 사용한다면 상태의 일관성을 유지하고 디버깅을 편하게 할 수 있을 것이다. + +📝질문 3. Local Storage, Session Storage 와 Cookies의 차이가 무엇이며 각각 어떨때 사용하면 좋을까요? +--- +- Local Storage
Local Storage는 영구적인 저장소로 브라우저가 닫히고 다시 열려도 데이터가 유지된다. 영구적인 저장소이기에 사용자 개개인의 설정이나 아이디 비밀번호 기억 등의 기능에 사용면 좋을 것이다 +- Session Storage
Session Storage는 브라우저 창이나 탭이 닫히면 데이터도 함께 삭제 되는 저장소이다. +유튜브의 검색창에 입력한 내용 등 일시적인 입력 데이터 등에 사용하면 좋을 것이다. +- Cookies
Cookies는 만료 날짜를 설정할 수 있는 저장소로 설정 기간 동안에만 유지되는 특성이 있다. +쿠키는 특정 기간만 이용할 수 있는 구독 서비스 등에 사용자 인증 정보를 부여하는 곳에 사용한다면 좋을 것이다. diff --git a/package-lock.json b/package-lock.json index 8f100a3a8..581b61b33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.1" }, "devDependencies": { "@craco/craco": "^7.1.0", @@ -6135,6 +6136,14 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@remix-run/router": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.1.tgz", + "integrity": "sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==", + "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", @@ -27856,9 +27865,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -28219,15 +28228,15 @@ "dev": true }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-element-to-jsx-string": { @@ -28318,6 +28327,36 @@ } } }, + "node_modules/react-router": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.1.tgz", + "integrity": "sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==", + "dependencies": { + "@remix-run/router": "1.17.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.1.tgz", + "integrity": "sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==", + "dependencies": { + "@remix-run/router": "1.17.1", + "react-router": "6.24.1" + }, + "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", @@ -29371,9 +29410,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } diff --git a/package.json b/package.json index 8a3e091c7..29331e112 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,11 @@ "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.1" }, "devDependencies": { - "react-scripts": "5.0.1", - "typescript": "^4.9.5", "@craco/craco": "^7.1.0", "@emotion/eslint-plugin": "^11.11.0", "@storybook/addon-essentials": "^7.6.17", @@ -65,8 +64,10 @@ "eslint-plugin-storybook": "^0.8.0", "prettier": "^3.2.5", "prop-types": "^15.8.1", + "react-scripts": "5.0.1", "storybook": "^7.6.17", "tsconfig-paths-webpack-plugin": "^4.1.0", + "typescript": "^4.9.5", "webpack": "^5.90.3" }, "overrides": { diff --git a/src/App.tsx b/src/App.tsx index 1df5ce256..9b6f05533 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,17 @@ -import styled from '@emotion/styled'; +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; -const App = () => { - const name = 'Josh Perez'; +import { AuthProvider } from './context/AuthContext'; +import RoutesPage from './router/Router'; +const App: React.FC = () => { return ( -
- Hello, {name} -
+ + + + + ); }; export default App; - -const Title = styled.h1` - font-size: 1.5em; - color: gray; -`; diff --git a/src/Layout/Footer/index.tsx b/src/Layout/Footer/index.tsx new file mode 100644 index 000000000..66a1808a0 --- /dev/null +++ b/src/Layout/Footer/index.tsx @@ -0,0 +1,25 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +export const Footer: React.FC = () => { + return ( + + 카카오톡 선물하기 + + ); +}; + +export default Footer; + +const Wrapper = styled.footer` + padding: 28px 16px 88px; + width: 80%; + margin: 0 auto; + max-width: 100vw; + background-color: #fafafc; +`; + +const Content = styled.p` + font-size: 20px; + font-weight: bold; +`; diff --git a/src/Layout/Header/index.tsx b/src/Layout/Header/index.tsx new file mode 100644 index 000000000..668b15c41 --- /dev/null +++ b/src/Layout/Header/index.tsx @@ -0,0 +1,54 @@ +import styled from '@emotion/styled'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +interface HeaderProps { + isLoggedIn: boolean; +} + +const Header: React.FC = ({ isLoggedIn }) => { + const navigate = useNavigate(); + + const handleLoginClick = () => { + navigate('/login'); + }; + + const handleAccountClick = () => { + navigate('/my-account'); + }; + + return ( + + 선물하기 + {isLoggedIn ? ( + + ) : ( + + )} + + ); +}; + +export default Header; + +const HeaderWrapper = styled.header` + display: flex; + width: 80%; + margin: 0 auto; + justify-content: space-between; + align-items: center; + padding: 16px; + background-color: #ffffff; +`; + +const Title = styled.h1` + font-size: 30px; + font-weight: bold; +`; + +const Button = styled.button` + background: none; + border: none; + font-size: 16px; + cursor: pointer; +`; diff --git a/src/Layout/index.tsx b/src/Layout/index.tsx new file mode 100644 index 000000000..21ca46deb --- /dev/null +++ b/src/Layout/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; + +import Footer from '@/Layout/Footer'; +import Header from '@/Layout/Header'; + +interface LayoutProps { + isLoggedIn: boolean; +} + +const Layout: React.FC = ({ isLoggedIn }) => { + return ( +
+
+ +
+
+ ); +}; + +export default Layout; diff --git a/src/Pages/Home/index.tsx b/src/Pages/Home/index.tsx new file mode 100644 index 000000000..b9fba68fb --- /dev/null +++ b/src/Pages/Home/index.tsx @@ -0,0 +1,17 @@ +import AiReference from "@/components/common/HomeComponents/AiReference"; +import Banner from "@/components/common/HomeComponents/Banner"; +import Ranking from "@/components/common/HomeComponents/Ranking"; +import ThemeCategory from "@/components/common/HomeComponents/ThemeCategory"; + +export const Home = () => { + return( + <> + + + + + + ) +} + +export default Home \ No newline at end of file diff --git a/src/Pages/Login/index.tsx b/src/Pages/Login/index.tsx new file mode 100644 index 000000000..7ccfe892b --- /dev/null +++ b/src/Pages/Login/index.tsx @@ -0,0 +1,114 @@ +import styled from '@emotion/styled'; +import React, { useState } from 'react'; +import { useLocation,useNavigate } from 'react-router-dom'; + +import { useAuth } from '@/context/AuthContext'; + +const Login: React.FC = () => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); + const location = useLocation(); + const { login } = useAuth(); + + const from = location.state?.from?.pathname || '/'; // 이전 경로 또는 기본 경로 + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!username || !password) { + alert('아이디와 비밀번호를 입력하세요'); + return; + } + login(username); + navigate(from, { replace: true }); // 로그인 후 이전 경로로 리디렉션 + }; + + return ( + + + kakao + +
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> +
+ +
+
+
+
+ ); +}; + +export default Login; + +const Wrapper = styled.div` + height: 500px; + display: flex; + justify-content: center; + align-items: center; + background-color: white; +`; + +const ContentWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +`; + +const Title = styled.h1` + margin-bottom: 20px; + font-size: 32px; + font-weight: bold; +`; + +const FormWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + background-color: white; + width: 500px; + height: 250px; + padding: 40px; + border: 1px solid black; +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 10px; +`; + +const Input = styled.input` + margin-bottom: 10px; + padding: 10px; + width: 100%; + border: none; + border-bottom: 1px solid #dee2e6; + border-radius: 0; + font-size: 16px; +`; + +const Button = styled.button` + padding: 10px; + width: 100%; + background-color: #ffeb00; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + font-weight: bold; +`; \ No newline at end of file diff --git a/src/Pages/MyAccount/index.tsx b/src/Pages/MyAccount/index.tsx new file mode 100644 index 000000000..8b6d1b6a1 --- /dev/null +++ b/src/Pages/MyAccount/index.tsx @@ -0,0 +1,66 @@ +import styled from '@emotion/styled'; +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +// 세션 스토리지에서 사용자 ID를 가져오는 함수 +export const getAuthToken = (): string | null => { + return sessionStorage.getItem('authToken'); +}; + +interface MyAccountProps { + onLogout: () => void; +} + +const MyAccount: React.FC = ({ onLogout }) => { + const navigate = useNavigate(); + const [username, setUsername] = useState(null); + + useEffect(() => { + const token = getAuthToken(); + setUsername(token); + }, []); + + const handleLogoutClick = () => { + onLogout(); + navigate('/'); + }; + + return ( + + {username ? `${username}님 반갑습니다!` : '반갑습니다!'} + 로그아웃 + + ); +}; + +export default MyAccount; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 500px; + background-color: white; +`; + +const Title = styled.h1` + margin-bottom: 20px; + font-size: 32px; + font-weight: bold; + text-align: center; +`; + +const LogoutButton = styled.button` + padding: 10px; + width: 100%; + max-width: 400px; + background-color: #ffeb00; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + font-weight: bold; + text-align: center; + margin: 0 auto; +`; diff --git a/src/Pages/Theme/Goods.tsx b/src/Pages/Theme/Goods.tsx new file mode 100644 index 000000000..d669708a7 --- /dev/null +++ b/src/Pages/Theme/Goods.tsx @@ -0,0 +1,31 @@ +import styled from '@emotion/styled'; + +import {DefaultGoodsItems} from "@/components/common/GoodsItem/Default" +import {itemList} from "@/components/common/HomeComponents/Ranking/ItemList/Items" +import {Grid} from "@/components/common/layouts/Grid" + + +export const ItemList = () =>{ + + return( + + + {itemList.map((item) => ( + + ))} + + + ) + +} + +export default ItemList; + +const Wrapper = styled.div` + width: "80%"; +` \ No newline at end of file diff --git a/src/Pages/Theme/Header.tsx b/src/Pages/Theme/Header.tsx new file mode 100644 index 000000000..a69f5b30e --- /dev/null +++ b/src/Pages/Theme/Header.tsx @@ -0,0 +1,42 @@ +import styled from '@emotion/styled'; + +export const Header = () => { + return ( + + + + 예산은 가볍게, 감동은 무겁게❤️ + 당신의 센스를 뽐내줄 부담 없는 선물 + + + ); +}; + +export default Header; + +const Wrapper = styled.header` + background-color: rgb(75, 77, 80); + padding: 50px 20px; +`; +const Container = styled.div` + max-width: 80%; + margin: 0 auto; + display: flex; + flex-direction: column; +`; +const Label = styled.p` + font-size: 20px; + margin-bottom: 10px; + font-weight: bold; + color: rgba(255, 255, 255, 0.7); +`; +const Title = styled.h1` + font-size: 40px; + margin-bottom: 10px; + font-weight: bold; + color: rgb(255, 255, 255); +`; +const Description = styled.p` + font-size: 20px; + color:rgba(255, 255, 255, 0.55); +`; \ No newline at end of file diff --git a/src/Pages/Theme/index.tsx b/src/Pages/Theme/index.tsx new file mode 100644 index 000000000..9e8077505 --- /dev/null +++ b/src/Pages/Theme/index.tsx @@ -0,0 +1,15 @@ +// import React from 'react'; +import Goods from './Goods'; +import {Header} from "./Header"; + +const Theme = () => { + return ( + <> +
+ + + ); + +}; + +export default Theme; diff --git a/src/components/common/HomeComponents/AiReference/index.tsx b/src/components/common/HomeComponents/AiReference/index.tsx new file mode 100644 index 000000000..5915dff7f --- /dev/null +++ b/src/components/common/HomeComponents/AiReference/index.tsx @@ -0,0 +1,41 @@ +import styled from '@emotion/styled'; +export const AiReference = () => { + return ( + + + + 선물을 추천받고 싶은 친구를 선택해주세요. + + + ); +}; + +export default AiReference; +const Wrapper = styled.section` + padding: 20px; +`; + +const Banner = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + background-color: #feeb00; + padding: 16px; + border-radius: 8px; + +`; + +const Label = styled.p` + font-size: 15px; + font-weight: 500; + color: rgba(0, 0, 0, 0.4); + padding-bottom: 3px; + +`; + +const Title = styled.h1` + font-size: 20px; + font-weight: 700; + color: #000; +`; diff --git a/src/components/common/HomeComponents/Banner/index.tsx b/src/components/common/HomeComponents/Banner/index.tsx new file mode 100644 index 000000000..b71469f98 --- /dev/null +++ b/src/components/common/HomeComponents/Banner/index.tsx @@ -0,0 +1,41 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +import { Image } from '@/components/common/Image'; + +export const Banner: React.FC = () => { + return ( + + + Profile Image{alert("선물 받을 친구 선택하기")}} + /> + 선물 받을 친구를 선택해주세요. + + + ); +}; + +export default Banner; + +const Wrapper = styled.section` + padding: 16px; + background-color: rgb(250, 250, 250); +`; + +const Content = styled.div` + display: flex; + align-items: center; + width: 80%; + margin: 0 auto; +`; + +const Text = styled.p` + padding-left: 16px; + margin: 0; +`; diff --git a/src/components/common/HomeComponents/Ranking/Filter/index.tsx b/src/components/common/HomeComponents/Ranking/Filter/index.tsx new file mode 100644 index 000000000..3af19f2d0 --- /dev/null +++ b/src/components/common/HomeComponents/Ranking/Filter/index.tsx @@ -0,0 +1,80 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; + +import PersonButton from '@/components/common/HomeComponents/Ranking/PersonButton'; +import RankTypeButton from '@/components/common/HomeComponents/Ranking/RankButton'; + +const initialFilterOption = { + targetType: 'ALL', + rankType: 'MANY_WISH', +}; + +export const Filter = () => { + const [filterOption, setFilterOption] = useState(initialFilterOption); + + return ( + + + setFilterOption({ ...filterOption, targetType: 'ALL' })} + /> + setFilterOption({ ...filterOption, targetType: 'FEMALE' })} + /> + setFilterOption({ ...filterOption, targetType: 'MALE' })} + /> + setFilterOption({ ...filterOption, targetType: 'TEEN' })} + /> + + + setFilterOption({ ...filterOption, rankType: 'MANY_WISH' })} + /> + setFilterOption({ ...filterOption, rankType: 'MANY_RECEIVE' })} + /> + setFilterOption({ ...filterOption, rankType: 'MANY_WISH_RECEIVE' })} + /> + + + + ); +}; + +const Wrapper = styled.div` + padding: 20px; +`; +const RankTypeButtons = styled.div` + display: flex; + justify-content: space-around; + background-color:#e6f1ff; + border-radius: 20px; + `; +const PersonButtons = styled.div` + margin-bottom: 10px; + display: flex; + justify-content: space-around; +`; + +export default Filter; diff --git a/src/components/common/HomeComponents/Ranking/ItemList/Items.ts b/src/components/common/HomeComponents/Ranking/ItemList/Items.ts new file mode 100644 index 000000000..d2e18956c --- /dev/null +++ b/src/components/common/HomeComponents/Ranking/ItemList/Items.ts @@ -0,0 +1,15 @@ +type Data= { + imageSrc: string; + subtitle: string; + title: string; + amount: number; +}; + +const item: Data = { + imageSrc: "https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg", + subtitle: "BBQ", + title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", + amount: 29000 +}; + +export const itemList:Data[] = Array.from({ length: 21 }, () => item); \ No newline at end of file diff --git a/src/components/common/HomeComponents/Ranking/ItemList/index.tsx b/src/components/common/HomeComponents/Ranking/ItemList/index.tsx new file mode 100644 index 000000000..b0c246b2d --- /dev/null +++ b/src/components/common/HomeComponents/Ranking/ItemList/index.tsx @@ -0,0 +1,67 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; + +import {Button} from "@/components/common/Button" +import {RankingGoodsItems} from "@/components/common/GoodsItem/Ranking" +import {Grid} from "@/components/common/layouts/Grid" + +import {itemList} from "./Items" + + +export const ItemList = () =>{ + + const [All, setAll] = useState(false); + + const handleToggleShowAll = () => { + setAll(!All); + }; + + const itemsToShow = All ? itemList : itemList.slice(0, 6); + + return( + + + {itemsToShow.map((item, index) => ( + + ))} + + + {All? '접기' : '더보기'} + + + ) + +} + +export default ItemList +const Wrapper = styled.div` + width: "100%"; +` + +const TogleButton = styled(Button)` +width: 800px; +position: relative; +margin: 20px auto; +background-color: white; +border: none; + + &:hover { + background-color:rgb(174, 174, 174); + border: none; + box-shadow: none; + outline: none; + } + + &:focus { + border: none; + box-shadow: none; + outline: none; + } +` diff --git a/src/components/common/HomeComponents/Ranking/PersonButton/index.tsx b/src/components/common/HomeComponents/Ranking/PersonButton/index.tsx new file mode 100644 index 000000000..ac5eb5497 --- /dev/null +++ b/src/components/common/HomeComponents/Ranking/PersonButton/index.tsx @@ -0,0 +1,53 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +type Props = { + label: string; + icon: React.ReactNode; + selected: boolean; + onClick: () => void; +}; + +export const PersonButton = ({ selected, label, icon, onClick }: Props) => { + return ( + + {icon} + + + ); +}; + +const Wrapper = styled.button` + width: 100%; + min-width: 58px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + &:focus { + outline: none; +`; + +const Icon = styled.div>` + display: flex; + justify-content: center; + align-items: center; + width: 50px; + height: 50px; + color: #fff; + background-color: ${({ selected }) => (selected ? '#4684e9' : '#e6f1ff')}; + border-radius: 20px; + + } +`; + +const Label = styled.p>` + padding-top: 5px; + font-size: 20px; + line-height: 16px; + color: ${({ selected }) => (selected ? '#4684e9' : '#666')}; + font-weight: ${({ selected }) => (selected ? 700 : 400)}; + transition: color 200ms, font-weight 200ms; +`; + +export default PersonButton; diff --git a/src/components/common/HomeComponents/Ranking/RankButton/index.tsx b/src/components/common/HomeComponents/Ranking/RankButton/index.tsx new file mode 100644 index 000000000..4db25d060 --- /dev/null +++ b/src/components/common/HomeComponents/Ranking/RankButton/index.tsx @@ -0,0 +1,30 @@ +import styled from '@emotion/styled'; + +type Props = { + label: string; + selected: boolean; + onClick: () => void; +}; + +export const RankTypeButton = ({ selected, label, onClick }: Props) => { + return ( + + {label} + + ); +}; + +export default RankTypeButton + +const Wrapper = styled.button>` + padding: 13px 20px; + font-size: 20px; + line-height: 16px; + color: ${({ selected }) => (selected ? '#4684e9' : 'rgba(70, 132, 233, 0.7)')}; + font-weight: ${({ selected }) => (selected ? 700 : 400)}; + transition: color 200ms, font-weight 200ms; + &:focus { + outline: none; + } +`; +export {}; \ No newline at end of file diff --git a/src/components/common/HomeComponents/Ranking/index.tsx b/src/components/common/HomeComponents/Ranking/index.tsx new file mode 100644 index 000000000..d71f66183 --- /dev/null +++ b/src/components/common/HomeComponents/Ranking/index.tsx @@ -0,0 +1,27 @@ +import styled from '@emotion/styled'; + +import Filter from '@/components/common/HomeComponents/Ranking/Filter'; +import ItemList from '@/components/common/HomeComponents/Ranking/ItemList'; + + + +export const Ranking = () =>{ + return( + + + 실시간 급상승 선물랭킹 + + + + + ); +} +export default Ranking; + +const Wrapper = styled.div` + width: 100%; +`; +const Title = styled.h2` + width: 100%; + font-size: 24px; + text-align: center;` \ No newline at end of file diff --git a/src/components/common/HomeComponents/ThemeCategory/ItemImage.tsx b/src/components/common/HomeComponents/ThemeCategory/ItemImage.tsx new file mode 100644 index 000000000..e33d137e7 --- /dev/null +++ b/src/components/common/HomeComponents/ThemeCategory/ItemImage.tsx @@ -0,0 +1,41 @@ +import styled from '@emotion/styled'; +import { Link } from 'react-router-dom'; + +import { Image } from '@/components/common/Image'; +type Props = { + imageSrc: string; + label: string; + to: string; +} & React.HTMLAttributes; + +export const ItemImage = ({ imageSrc, label, to }: Props) => ( + + + + + + +); + +const Wrapper = styled.div` + width: 100%; + padding: 13px 0 12px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const StyledImage = styled(Image)` + width: 100%; + height: 100%; + max-width: 100px; + max-height: 100px; +`; + +const Label = styled.p` + padding-top: 5px; + font-size: 13px; + line-height: 17px; + color: #333; +`; diff --git a/src/components/common/HomeComponents/ThemeCategory/index.tsx b/src/components/common/HomeComponents/ThemeCategory/index.tsx new file mode 100644 index 000000000..04b90fb10 --- /dev/null +++ b/src/components/common/HomeComponents/ThemeCategory/index.tsx @@ -0,0 +1,52 @@ +import styled from '@emotion/styled'; + +import { Container } from '@/components/common/layouts/Container'; +import { Grid } from '@/components/common/layouts/Grid'; +import { getDynamicPath, ROUTE_PATHS } from '@/router/paths'; + +import { ItemImage } from './ItemImage'; + +export const ThemeCategory = () => { + const themes = [ + { label: '생일', key: 'birthday' }, + { label: '졸업선물', key: 'graduation' }, + { label: '스몰럭셔리', key: 'small-luxury' }, + { label: '명품선물', key: 'luxury' }, + { label: '결혼/집들이', key: 'wedding' }, + { label: '따뜻한선물', key: 'warm-gift' }, + { label: '가벼운선물', key: 'light-gift' }, + { label: '팬심저격', key: 'fan-favorite' }, + { label: '교환권', key: 'exchange' }, + { label: '건강/비타민', key: 'health' }, + { label: '과일/한우', key: 'fruits-meat' }, + { label: '출산/키즈', key: 'kids' }, + ]; + + return ( + + + + {themes.map((theme) => ( + + ))} + + + + ); +}; + +export default ThemeCategory; + +const Wrapper = styled.div` + margin-top: 30px; +`; diff --git a/src/components/common/layouts/Container/index.tsx b/src/components/common/layouts/Container/index.tsx index 06bfca43e..e1181b9aa 100644 --- a/src/components/common/layouts/Container/index.tsx +++ b/src/components/common/layouts/Container/index.tsx @@ -21,7 +21,6 @@ export const Container: React.FC = forwardRef( const Wrapper = styled.div` width: 100%; - display: flex; justify-content: center; align-items: center; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 000000000..5a0617cf6 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,49 @@ +import type { ReactNode } from 'react'; +import React, { createContext, useContext, useEffect, useState } from 'react'; + +interface AuthContextProps { + isLoggedIn: boolean; + login: (username: string) => void; + logout: () => void; +} + +const AuthContext = createContext(undefined); + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + const authToken = sessionStorage.getItem('authToken'); + if (authToken) { + setIsLoggedIn(true); + } + }, []); + + const login = (username: string) => { + sessionStorage.setItem('authToken', username); + setIsLoggedIn(true); + }; + + const logout = () => { + sessionStorage.removeItem('authToken'); + setIsLoggedIn(false); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextProps => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/index.tsx b/src/index.tsx index ab5f7ad6a..59dc14467 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,12 @@ -import '@/styles'; import React from 'react'; -import ReactDOM from 'react-dom/client'; +import ReactDOM from 'react-dom'; -import App from '@/App'; +import App from './App'; -const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); -root.render( +ReactDOM.render( - + , + document.getElementById('root') ); diff --git a/src/router/Router.tsx b/src/router/Router.tsx new file mode 100644 index 000000000..721f682bf --- /dev/null +++ b/src/router/Router.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; + +import { useAuth } from '@/context/AuthContext'; +import Layout from '@/Layout'; +import Home from '@/Pages/Home'; +import Login from '@/Pages/Login'; +import MyAccount from '@/Pages/MyAccount'; +import Theme from '@/Pages/Theme'; + +import { ROUTE_PATHS } from './paths'; + +interface ProtectedRouteProps { + element: React.ReactElement; + onLogout?: () => void; // 추가적인 props를 허용 +} + +const ProtectedRoute: React.FC = ({ element, ...rest }) => { + const { isLoggedIn } = useAuth(); + const location = useLocation(); + return isLoggedIn ? React.cloneElement(element, { ...rest }) : ; +}; + +const RoutesPage: React.FC = () => { + const { isLoggedIn, logout } = useAuth(); + + return ( + + }> + } /> + } /> + } /> + } />} /> + + } /> + + ); +}; + +export default RoutesPage; \ No newline at end of file diff --git a/src/router/paths.tsx b/src/router/paths.tsx new file mode 100644 index 000000000..aa75806e4 --- /dev/null +++ b/src/router/paths.tsx @@ -0,0 +1,11 @@ +export const ROUTE_PATHS = { + ROOT: '/', + HOME: '/', + THEME: '/theme/:themeKey', + MYPAGE: '/my-account', + LOGIN: '/login', +}; + +export const getDynamicPath = (path: string, params: { [key: string]: string }): string => { + return path.replace(/:(\w+)/g, (_, key) => params[key]); +};