From 2af81393038bbd5100e1fa70d0921081b2846520 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Fri, 6 Dec 2024 03:43:42 +0900 Subject: [PATCH 01/32] =?UTF-8?q?=E2=9C=A8=20Feature(#67):=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8/=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20useAuth=20=ED=9B=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useAuth.ts | 28 ++++++++++++++++++++++++++++ src/utils/{cookie.ts => cookies.ts} | 0 2 files changed, 28 insertions(+) create mode 100644 src/hooks/useAuth.ts rename src/utils/{cookie.ts => cookies.ts} (100%) diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..5cce3e8 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,28 @@ +import { getCookie } from '@utils/cookies'; + +/** + * @description + * 이 커스텀 훅은 사용자의 로그인 상태를 판별합니다. + * JWT 토큰이 쿠키에 존재하는지를 확인하여 로그인 여부를 반환합니다. + * + * @returns + * `isLoggedIn`: 사용자가 로그인 상태인지 여부 + * + * @example + * const { isLoggedIn } = useAuth(); + * + * if (isLoggedIn) { + * console.log("사용자가 로그인 상태입니다."); + * } else { + * console.log("로그인이 필요합니다."); + * } + */ +const useAuth = () => { + const jwt = getCookie('jwt'); + + return { + isLoggedIn: !!jwt, // JWT가 존재하면 true, 없으면 false + }; +}; + +export default useAuth; diff --git a/src/utils/cookie.ts b/src/utils/cookies.ts similarity index 100% rename from src/utils/cookie.ts rename to src/utils/cookies.ts From be2c0787bb76c214770afbba27054bb678de356b Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Fri, 6 Dec 2024 04:44:08 +0900 Subject: [PATCH 02/32] =?UTF-8?q?=E2=9C=A8=20Feature(#67):=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20fake=20jwt=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/login/service/handleLogin.ts | 14 ++++++++++++++ src/features/login/service/handleLogout.ts | 8 ++++++++ src/features/login/service/handleSocialLogin.ts | 8 -------- src/features/login/ui/Login.tsx | 8 ++++---- src/pages/learn/Learn.tsx | 1 + 5 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 src/features/login/service/handleLogin.ts create mode 100644 src/features/login/service/handleLogout.ts delete mode 100644 src/features/login/service/handleSocialLogin.ts diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts new file mode 100644 index 0000000..6106fc2 --- /dev/null +++ b/src/features/login/service/handleLogin.ts @@ -0,0 +1,14 @@ +const BASE_URL = import.meta.env.VITE_BASE_URL; +import { setCookie } from '@utils/cookies'; + +const handleLogin = (provider: 'google' | 'kakao' | 'github') => { + const redirectUrl = `${BASE_URL}/auth/${provider}`; + window.location.href = redirectUrl; + + // 테스트용 JWT 생성 및 쿠키에 저장 + const fakeJwt = 'test.jwt.token'; + setCookie('jwt', fakeJwt, { path: '/', maxAge: 3600 }); // 1시간 유효 + alert('JWT 생성 완료: ' + fakeJwt); +}; + +export default handleLogin; diff --git a/src/features/login/service/handleLogout.ts b/src/features/login/service/handleLogout.ts new file mode 100644 index 0000000..affa32f --- /dev/null +++ b/src/features/login/service/handleLogout.ts @@ -0,0 +1,8 @@ +import { removeCookie } from '@utils/cookies'; + +const handleLogout = () => { + removeCookie('jwt'); + window.location.reload(); // 로그아웃 후 새로고침 +}; + +export default handleLogout; diff --git a/src/features/login/service/handleSocialLogin.ts b/src/features/login/service/handleSocialLogin.ts deleted file mode 100644 index 13565d9..0000000 --- a/src/features/login/service/handleSocialLogin.ts +++ /dev/null @@ -1,8 +0,0 @@ -const BASE_URL = import.meta.env.VITE_BASE_URL; - -const handleSocialLogin = (provider: 'google' | 'kakao' | 'github') => { - const redirectUrl = `${BASE_URL}/auth/${provider}`; - window.location.href = redirectUrl; -}; - -export default handleSocialLogin; diff --git a/src/features/login/ui/Login.tsx b/src/features/login/ui/Login.tsx index 89860bd..2cd6005 100644 --- a/src/features/login/ui/Login.tsx +++ b/src/features/login/ui/Login.tsx @@ -8,7 +8,7 @@ import { DashLineHr, } from '../styles'; import { LogoImg } from '@common/ui/style'; -import handleSocialLogin from '../service/handleSocialLogin'; +import handleLogin from '../service/handleLogin'; interface LoginProps { openModal: () => void; @@ -28,7 +28,7 @@ export default function Login({ closeModal }: LoginProps) { handleSocialLogin('google')} + onClick={() => handleLogin('google')} > 구글 로그인 Google 로그인 @@ -36,7 +36,7 @@ export default function Login({ closeModal }: LoginProps) { handleSocialLogin('kakao')} + onClick={() => handleLogin('kakao')} > 카카오 로그인 Kakao 로그인 @@ -44,7 +44,7 @@ export default function Login({ closeModal }: LoginProps) { handleSocialLogin('github')} + onClick={() => handleLogin('github')} > 깃허브 로그인 GitHub 로그인 diff --git a/src/pages/learn/Learn.tsx b/src/pages/learn/Learn.tsx index 82e8064..e68fdd2 100644 --- a/src/pages/learn/Learn.tsx +++ b/src/pages/learn/Learn.tsx @@ -13,6 +13,7 @@ import PartNavContainer from '@features/quiz/ui/PartNavContainer'; import usePreloadImages from '@hooks/usePreloadImages'; import useUserStore from '@store/useUserStore'; import { useEffect } from 'react'; +import { setCookie } from '@utils/cookies'; export default function Learn() { const { setUser } = useUserStore(); From aec6afb780ee120b36f5dc8e58aecd00c140965c Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Fri, 6 Dec 2024 04:53:25 +0900 Subject: [PATCH 03/32] =?UTF-8?q?=E2=9A=99=20Setting(#67):=20CI-CD=20Actio?= =?UTF-8?q?n=20Test=20-=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 3f47c45..952f5c2 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,7 +2,7 @@ name: CI/CD Docker on: push: - branches: [main, develop] + branches: [main, develop, feature/#67/OAuth_2_0] env: DOCKER_IMAGE: ghcr.io/${{ github.actor }}/react-auto-deploy @@ -76,5 +76,6 @@ jobs: # Docker Compose로 서비스 배포 - name: Docker run run: | + docker rmi $(docker images -f "dangling=true" -q) docker compose -f /home/ubuntu/docker-compose.yml down docker compose -f /home/ubuntu/docker-compose.yml up -d --pull always From 7bf40310693e5c056ed214266248d75ca7f72449 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Fri, 6 Dec 2024 05:03:07 +0900 Subject: [PATCH 04/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20Service=EC=97=90=EB=8A=94=20fake=20jwt=20=EC=95=88?= =?UTF-8?q?=20=EC=98=AC=EB=9D=BC=EA=B0=80=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/login/service/handleLogin.ts | 8 ++++---- src/pages/learn/Learn.tsx | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts index 6106fc2..289f05f 100644 --- a/src/features/login/service/handleLogin.ts +++ b/src/features/login/service/handleLogin.ts @@ -1,14 +1,14 @@ const BASE_URL = import.meta.env.VITE_BASE_URL; -import { setCookie } from '@utils/cookies'; +// import { setCookie } from '@utils/cookies'; const handleLogin = (provider: 'google' | 'kakao' | 'github') => { const redirectUrl = `${BASE_URL}/auth/${provider}`; window.location.href = redirectUrl; // 테스트용 JWT 생성 및 쿠키에 저장 - const fakeJwt = 'test.jwt.token'; - setCookie('jwt', fakeJwt, { path: '/', maxAge: 3600 }); // 1시간 유효 - alert('JWT 생성 완료: ' + fakeJwt); + // const fakeJwt = 'test.jwt.token'; + // setCookie('jwt', fakeJwt, { path: '/', maxAge: 3600 }); // 1시간 유효 + // alert('JWT 생성 완료: ' + fakeJwt); }; export default handleLogin; diff --git a/src/pages/learn/Learn.tsx b/src/pages/learn/Learn.tsx index e68fdd2..82e8064 100644 --- a/src/pages/learn/Learn.tsx +++ b/src/pages/learn/Learn.tsx @@ -13,7 +13,6 @@ import PartNavContainer from '@features/quiz/ui/PartNavContainer'; import usePreloadImages from '@hooks/usePreloadImages'; import useUserStore from '@store/useUserStore'; import { useEffect } from 'react'; -import { setCookie } from '@utils/cookies'; export default function Learn() { const { setUser } = useUserStore(); From 3b05363350ff0bc628e3881ce0c4ac5d84b86a31 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Fri, 6 Dec 2024 05:10:51 +0900 Subject: [PATCH 05/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=EB=90=98=EB=8A=94=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=EC=A7=80=20=ED=99=95=EC=9D=B8=20=ED=9B=84=20?= =?UTF-8?q?=20=ED=83=9C=EA=B7=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EA=B0=80=20=EC=9E=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 952f5c2..5ede711 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -76,6 +76,8 @@ jobs: # Docker Compose로 서비스 배포 - name: Docker run run: | - docker rmi $(docker images -f "dangling=true" -q) + if [ -n "$(docker images -f "dangling=true" -q)" ]; then + docker rmi $(docker images -f "dangling=true" -q) + fi docker compose -f /home/ubuntu/docker-compose.yml down docker compose -f /home/ubuntu/docker-compose.yml up -d --pull always From 4bf99e6b2612f74a843049d9da882380522f7c2f Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 14:43:30 +0900 Subject: [PATCH 06/32] =?UTF-8?q?=F0=9F=8E=A8=20Publish(#81):=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=99=84?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/user/styles.ts | 51 +++++++ src/features/user/ui/BadgeContainer.tsx | 57 ++++++++ src/features/user/ui/ProfileImage.tsx | 9 ++ src/pages/profile/Profile.tsx | 112 ++++++++++++++++ src/pages/profile/styles.ts | 168 ++++++++++++++++++++++++ src/route/Router.tsx | 10 +- 6 files changed, 403 insertions(+), 4 deletions(-) create mode 100644 src/features/user/styles.ts create mode 100644 src/features/user/ui/BadgeContainer.tsx create mode 100644 src/features/user/ui/ProfileImage.tsx create mode 100644 src/pages/profile/Profile.tsx create mode 100644 src/pages/profile/styles.ts diff --git a/src/features/user/styles.ts b/src/features/user/styles.ts new file mode 100644 index 0000000..6453f9c --- /dev/null +++ b/src/features/user/styles.ts @@ -0,0 +1,51 @@ +import { styled } from 'styled-components'; +export const BadgeWrapper = styled.div` + display: flex; + margin-top: 39px; + width: 100%; + gap: 12px; + justify-content: space-between; + padding: 0 17px; + > ul { + display: flex; + flex-wrap: wrap; + gap: 25px; + } +`; +export const PaginationIcon = styled.img<{ $rotate?: string }>` + width: 9px; + height: 20px; + transform: rotate(${({ $rotate }) => $rotate || 0}); +`; +export const PaginationButton = styled.button` + border: 0; + background-color: transparent; +`; +export const BadgeListItem = styled.li` + display: flex; + flex-direction: column; + gap: 11px; + > div { + display: flex; + justify-content: center; + align-items: center; + width: 133px; + height: 141px; + flex-shrink: 0; + border-radius: 8px; + border: 2px solid #ffe161; + background: #ffefaa; + } + > h5 { + border-radius: 8px; + border: 2px solid #ffefaa; + background: #ffe161; + color: #fff; + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 266.667% */ + text-transform: lowercase; + } +`; diff --git a/src/features/user/ui/BadgeContainer.tsx b/src/features/user/ui/BadgeContainer.tsx new file mode 100644 index 0000000..fa5b6c6 --- /dev/null +++ b/src/features/user/ui/BadgeContainer.tsx @@ -0,0 +1,57 @@ +import { + BadgeListItem, + BadgeWrapper, + PaginationButton, + PaginationIcon, +} from '../styles'; +import { getImageUrl } from '@/utils/getImageUrl'; + +const testBadgeList = [ + { + id: 1, + badgeImage: '테스트뱃지.webp', + badgeTitle: '왕', + }, + { + id: 2, + badgeImage: '테스트뱃지.webp', + badgeTitle: '왕왕', + }, + { + id: 3, + badgeImage: '테스트뱃지.webp', + badgeTitle: '왕대대', + }, + { + id: 4, + badgeImage: '테스트뱃지.webp', + badgeTitle: '왕대대왕', + }, +]; +export default function BadgeContainer() { + return ( + <> + + + + +
    + {testBadgeList.map(badgeItem => ( + +
    + +
    +
    {badgeItem.badgeTitle}
    +
    + ))} +
+ + + +
+ + ); +} diff --git a/src/features/user/ui/ProfileImage.tsx b/src/features/user/ui/ProfileImage.tsx new file mode 100644 index 0000000..d15a9bf --- /dev/null +++ b/src/features/user/ui/ProfileImage.tsx @@ -0,0 +1,9 @@ +import { getImageUrl } from '@/utils/getImageUrl'; + +export default function ProfileImage() { + return ( + <> + + + ); +} diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx new file mode 100644 index 0000000..5e390fd --- /dev/null +++ b/src/pages/profile/Profile.tsx @@ -0,0 +1,112 @@ +import CokoLogo from '@common/layout/CokoLogo'; +import MenuBar from '@common/layout/MenuBar'; +import { Layout, LeftSection, RightSection, Wrapper } from '@/style/style'; +import Header from '@common/layout/Header'; +import { + BadgeLabel, + JoinDateLabel, + LevelDiv, + MyProgressDiv, + MyQuizInfoDiv, + UserNameLabel, + ProfileSection, + BadgeSection, + LevelList, + LevelLabel, + MyCharacterImage, +} from './styles'; +import ProfileImage from '@features/user/ui/ProfileImage'; +import ProgressBar from '@features/progress/ui/ProgressBar'; +import { getImageUrl } from '@/utils/getImageUrl'; +import BadgeContainer from '@features/user/ui/BadgeContainer'; +import React from 'react'; +const levelList = [60, 50, 40, 30, 20, 10]; +export default function Profile() { + return ( + <> + + + + + + +
+ + +
+ + Level.1 +
+ + {levelList.map(level => ( + +
  • + Level.{level} - +
  • +
  • +
    + ))} +
    + +
    + + + + +
    + + 유저 이름 + 2024.10.01 +
    + +

    + 코코에 접속한 지 벌써 20일이 됐어요 ! +

    + +
    +

    진행도

    + +
    + +

    + 푼 문제 52개 +

    +

    + 틀린 문제 52개 +

    +

    + 안 푼 문제 52개 +

    +
    +
    +
    + + 나의 뱃지 + + +
    + + ); +} diff --git a/src/pages/profile/styles.ts b/src/pages/profile/styles.ts new file mode 100644 index 0000000..f0864c1 --- /dev/null +++ b/src/pages/profile/styles.ts @@ -0,0 +1,168 @@ +import { styled } from 'styled-components'; + +export const ProfileSection = styled.section` + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + margin-top: 50px; + width: 50vw; + height: 312px; + background-color: #ffff; + border-radius: 20px; + box-shadow: 0 3px #e5e5e5; +`; +export const BadgeSection = styled.section` + height: 306px; + width: 50vw; + margin-top: 21px; + background-color: #ffff; + border-radius: 20px; + box-shadow: 0 3px #e5e5e5; +`; +export const LevelDiv = styled.div` + position: absolute; + display: flex; + align-items: flex-end; + width: 249px; + height: 629px; + background-color: #ffff; + border-radius: 20px; + box-shadow: 0 3px #e5e5e5; + margin: 68px 28px 0 0; +`; +export const MyProgressDiv = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: center; + min-width: 350px; + width: 383px; + height: 198px; + margin-left: 45px; + padding-top: 80px; + border-radius: 16px; + justify-content: center; + border: 2px solid #bfd683; + background: #e3f3b8; + > p:nth-child(1) { + position: absolute; + top: -6px; + z-index: 10; + font-size: 11px; + color: #fff; + > span { + color: #bfd683; + font-size: 13px; + } + } + > img { + position: absolute; + top: -38px; + } + > div { + display: flex; + color: #9f9f9f; + justify-content: space-around; + gap: 20px; + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 200% */ + text-transform: lowercase; + } +`; +export const MyQuizInfoDiv = styled.div` + display: flex; + align-items: center; + margin-top: 24px; + border-radius: 8px; + background: #cbd2a1; + width: 362px; + height: 37px; + color: #9f9f9f; + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 200% */ + text-transform: lowercase; + span { + color: #85705f; + } +`; +export const UserNameLabel = styled.p` + text-align: center; + border-radius: 15px; + border: 2px solid #f09900; + background: #ffb53d; + color: #fff; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 142.857% */ + text-transform: lowercase; +`; +export const JoinDateLabel = styled.p` + color: #cbcbcb; + margin-top: 5px; + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 200% */ + text-transform: lowercase; +`; +export const BadgeLabel = styled.p` + text-align: center; + border-radius: 15px; + width: 209px; + border: 2px solid #f09900; + background: #ffb53d; + color: #fff; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 142.857% */ + text-transform: lowercase; + margin: 27px auto 0 40px; +`; +export const LevelList = styled.ul` + position: absolute; + display: flex; + flex-direction: column; + gap: 25px; + top: 20px; + left: 120px; + list-style: none; + color: #ffefaa; + :nth-child(even) { + text-align: right; + font-weight: 700; + } + span { + color: #ffb53d; + } +`; +export const LevelLabel = styled.p` + margin: 0 0 21px 70px; + text-align: center; + width: 117px; + height: 20px; + flex-shrink: 0; + border-radius: 15px; + border: 1px solid #f09900; + background: #ffb53d; + color: #fff; + font-size: 15px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 160% */ + letter-spacing: 0.2px; +`; +export const MyCharacterImage = styled.img` + width: 115px; + height: 92px; + margin: 0 0 10px 10px; +`; diff --git a/src/route/Router.tsx b/src/route/Router.tsx index a167353..fed3e98 100644 --- a/src/route/Router.tsx +++ b/src/route/Router.tsx @@ -1,8 +1,9 @@ import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'; -import Learn from '../pages/learn/Learn'; -import Quest from '../pages/quest/Quest'; -import Ranking from '../pages/ranking/Ranking'; -import Quiz from '../pages/quiz/Quiz'; +import Learn from '@/pages/learn/Learn'; +import Quest from '@/pages/quest/Quest'; +import Ranking from '@/pages/ranking/Ranking'; +import Quiz from '@/pages/quiz/Quiz'; +import Profile from '@/pages/profile/Profile'; export default function Router() { return ( @@ -14,6 +15,7 @@ export default function Router() { } /> } /> } /> + } /> From 0c21f4fdf3a7473182bc254d57c47259b6b5f944 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 14:46:59 +0900 Subject: [PATCH 07/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#81):=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/user/ui/ProfileImage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/user/ui/ProfileImage.tsx b/src/features/user/ui/ProfileImage.tsx index d15a9bf..211f33d 100644 --- a/src/features/user/ui/ProfileImage.tsx +++ b/src/features/user/ui/ProfileImage.tsx @@ -1,9 +1,10 @@ import { getImageUrl } from '@/utils/getImageUrl'; export default function ProfileImage() { + //기능 제작 시 자기 캐릭터에서 프로필이미지 추출하는 로직 추가 return ( <> - + ); } From 26d1e68dc4d8d584ab8874a31dba00e9318d26f6 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 15:23:08 +0900 Subject: [PATCH 08/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#81):=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95,=20S-dot=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/user/ui/BadgeContainer.tsx | 27 +++++----- src/pages/profile/Profile.tsx | 66 ++++++++++--------------- src/pages/profile/styles.ts | 2 +- 3 files changed, 39 insertions(+), 56 deletions(-) diff --git a/src/features/user/ui/BadgeContainer.tsx b/src/features/user/ui/BadgeContainer.tsx index fa5b6c6..ea510f4 100644 --- a/src/features/user/ui/BadgeContainer.tsx +++ b/src/features/user/ui/BadgeContainer.tsx @@ -1,9 +1,4 @@ -import { - BadgeListItem, - BadgeWrapper, - PaginationButton, - PaginationIcon, -} from '../styles'; +import * as S from '../styles'; import { getImageUrl } from '@/utils/getImageUrl'; const testBadgeList = [ @@ -31,27 +26,27 @@ const testBadgeList = [ export default function BadgeContainer() { return ( <> - - - - + + + +
      {testBadgeList.map(badgeItem => ( - +
      {badgeItem.badgeTitle}
      -
      + ))}
    - - + - -
    + + ); } diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx index 5e390fd..2767c63 100644 --- a/src/pages/profile/Profile.tsx +++ b/src/pages/profile/Profile.tsx @@ -1,20 +1,8 @@ import CokoLogo from '@common/layout/CokoLogo'; import MenuBar from '@common/layout/MenuBar'; -import { Layout, LeftSection, RightSection, Wrapper } from '@/style/style'; +import * as globalS from '@/style/style'; import Header from '@common/layout/Header'; -import { - BadgeLabel, - JoinDateLabel, - LevelDiv, - MyProgressDiv, - MyQuizInfoDiv, - UserNameLabel, - ProfileSection, - BadgeSection, - LevelList, - LevelLabel, - MyCharacterImage, -} from './styles'; +import * as S from './styles'; import ProfileImage from '@features/user/ui/ProfileImage'; import ProgressBar from '@features/progress/ui/ProgressBar'; import { getImageUrl } from '@/utils/getImageUrl'; @@ -24,20 +12,20 @@ const levelList = [60, 50, 40, 30, 20, 10]; export default function Profile() { return ( <> - - + + - - + +
    - +
    - - Level.1 + + Level.1
    - + {levelList.map(level => (
  • @@ -46,7 +34,7 @@ export default function Profile() {
  • ))} -
    + -
    - - - - + + + + +
    - 유저 이름 - 2024.10.01 + 유저 이름 + 2024.10.01
    - +

    코코에 접속한 지 벌써 20일이 됐어요 !

    @@ -89,7 +77,7 @@ export default function Profile() { }} /> - +

    푼 문제 52

    @@ -99,14 +87,14 @@ export default function Profile() {

    안 푼 문제 52

    -
    -
    -
    - - 나의 뱃지 + + + + + 나의 뱃지 - -
    + + ); } diff --git a/src/pages/profile/styles.ts b/src/pages/profile/styles.ts index f0864c1..dcef9d3 100644 --- a/src/pages/profile/styles.ts +++ b/src/pages/profile/styles.ts @@ -128,7 +128,7 @@ export const BadgeLabel = styled.p` text-transform: lowercase; margin: 27px auto 0 40px; `; -export const LevelList = styled.ul` +export const LevelList = styled.ol` position: absolute; display: flex; flex-direction: column; From 710ce4ad410a273563a24ed6651839c81c8cc993 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 16:24:38 +0900 Subject: [PATCH 09/32] =?UTF-8?q?=E2=9C=A8=20Feature(#38):=20=EC=83=81?= =?UTF-8?q?=EC=A0=90=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1,?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/store/Store.tsx | 25 +++++++++++++++++++++++++ src/pages/store/styles.ts | 18 ++++++++++++++++++ src/route/Router.tsx | 2 ++ 3 files changed, 45 insertions(+) create mode 100644 src/pages/store/Store.tsx create mode 100644 src/pages/store/styles.ts diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx new file mode 100644 index 0000000..0a66c59 --- /dev/null +++ b/src/pages/store/Store.tsx @@ -0,0 +1,25 @@ +import CokoLogo from '@common/layout/CokoLogo'; +import Header from '@common/layout/Header'; +import MenuBar from '@common/layout/MenuBar'; +import * as globalS from '@/style/style'; +import * as S from './styles'; + +export default function Store() { + return ( + <> + + + + + + +
    + + 장바구니 + + + + + + ); +} diff --git a/src/pages/store/styles.ts b/src/pages/store/styles.ts new file mode 100644 index 0000000..86015dc --- /dev/null +++ b/src/pages/store/styles.ts @@ -0,0 +1,18 @@ +import { styled } from 'styled-components'; + +export const CartListWrapper = styled.section` + position: absolute; + width: 194px; + height: 596px; + border-radius: 20px; + background: #fff; + box-shadow: 0 3px #e5e5e5; + margin: 84px 47px 0 0; +`; +export const Label = styled.label` + width: 147.003px; + height: 28.004px; + border-radius: 15px; + border: 2px solid #f09900; + background: #ffb53d; +`; diff --git a/src/route/Router.tsx b/src/route/Router.tsx index a167353..b978d4c 100644 --- a/src/route/Router.tsx +++ b/src/route/Router.tsx @@ -3,6 +3,7 @@ import Learn from '../pages/learn/Learn'; import Quest from '../pages/quest/Quest'; import Ranking from '../pages/ranking/Ranking'; import Quiz from '../pages/quiz/Quiz'; +import Store from '@/pages/store/Store'; export default function Router() { return ( @@ -14,6 +15,7 @@ export default function Router() { } /> } /> } /> + } /> From ccee0ef75049feb5ddcbb50891161fb3e26bdb77 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 16:32:06 +0900 Subject: [PATCH 10/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#81):=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/profile/styles.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/profile/styles.ts b/src/pages/profile/styles.ts index dcef9d3..9029353 100644 --- a/src/pages/profile/styles.ts +++ b/src/pages/profile/styles.ts @@ -92,7 +92,8 @@ export const MyQuizInfoDiv = styled.div` color: #85705f; } `; -export const UserNameLabel = styled.p` +export const UserNameLabel = styled.label` + display: block; text-align: center; border-radius: 15px; border: 2px solid #f09900; @@ -104,7 +105,8 @@ export const UserNameLabel = styled.p` line-height: 20px; /* 142.857% */ text-transform: lowercase; `; -export const JoinDateLabel = styled.p` +export const JoinDateLabel = styled.label` + display: block; color: #cbcbcb; margin-top: 5px; text-align: center; @@ -114,7 +116,8 @@ export const JoinDateLabel = styled.p` line-height: 24px; /* 200% */ text-transform: lowercase; `; -export const BadgeLabel = styled.p` +export const BadgeLabel = styled.label` + display: block; text-align: center; border-radius: 15px; width: 209px; @@ -145,7 +148,8 @@ export const LevelList = styled.ol` color: #ffb53d; } `; -export const LevelLabel = styled.p` +export const LevelLabel = styled.label` + display: block; margin: 0 0 21px 70px; text-align: center; width: 117px; From 5cc9c8c1778d5b8a556c1febbed737b984494057 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 18:44:59 +0900 Subject: [PATCH 11/32] =?UTF-8?q?=E2=9C=A8=20Feature(#38):=20=EC=BA=90?= =?UTF-8?q?=EB=A6=AD=ED=84=B0=20=EC=A0=9C=EC=99=B8=20=ED=8D=BC=EB=B8=94?= =?UTF-8?q?=EB=A6=AC=EC=8B=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/styles.ts | 105 ++++++++++++++++++++++++ src/features/store/ui/CartList.tsx | 50 +++++++++++ src/features/store/ui/ItemContainer.tsx | 58 +++++++++++++ src/pages/store/Store.tsx | 36 +++++++- src/pages/store/styles.ts | 90 +++++++++++++++++++- 5 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 src/features/store/styles.ts create mode 100644 src/features/store/ui/CartList.tsx create mode 100644 src/features/store/ui/ItemContainer.tsx diff --git a/src/features/store/styles.ts b/src/features/store/styles.ts new file mode 100644 index 0000000..f834454 --- /dev/null +++ b/src/features/store/styles.ts @@ -0,0 +1,105 @@ +import styled, { css } from 'styled-components'; + +export const StoreItemWrapper = styled.div` + margin: 18px 0 27px 0; + display: grid; + grid-template-columns: repeat(4, 144px); + grid-template-rows: repeat(2, 125px); + gap: 21px 19px; +`; +export const StoreItem = styled.div` + display: flex; + flex-direction: column; + align-items: center; + border-radius: 8px; + border: 2px solid #a5ecf0; + background: #f4f4f4; + padding: 10px 0; + &:hover { + border-radius: 8px; + border: 2px solid #00b6c0; + background: rgba(0, 217, 233, 0.8); + > label { + border-radius: 15px; + border: 2px solid #00d9e9; + background: #a5ecf0; + } + } +`; +export const ItemLabel = styled.label` + text-align: center; + display: block; + width: 73px; + height: 17px; + border-radius: 15px; + border: 2px solid #a5ecf0; + background: #00d9e9; + color: #fff; + font-size: 10px; + font-style: normal; + font-weight: 700; + line-height: 16px; /* 160% */ + letter-spacing: 0.2px; +`; +export const ItemImage = styled.img` + width: 125px; + height: 70px; +`; +export const PaginationDiv = styled.div` + width: 276px; + display: flex; + gap: 57px; +`; +export const PaginationButton = styled.button<{ $isSelect: boolean }>` + border: 0; + background-color: transparent; + color: #a5ecf0; + font-size: 15px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 160% */ + ${({ $isSelect }) => + $isSelect && + css` + color: #00d9e9; + `} +`; +export const StoreCartListWrapper = styled.div` + display: flex; + overflow-y: auto; + margin: 26px 0 0 0; + flex-direction: column; + gap: 28px; + padding-right: 8px; + &::-webkit-scrollbar { + padding-left: 5px; + width: 8px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: #a5ecf0; + border-radius: 4px; + } +`; + +export const PlaceLabel = styled.label` + margin-bottom: 26px; + text-align: center; + display: block; + width: 142.001px; + height: 28.004px; + border-radius: 15px; + border: 2px solid #f09900; + background: #ffb53d; + color: #fff; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 171.429% */ +`; diff --git a/src/features/store/ui/CartList.tsx b/src/features/store/ui/CartList.tsx new file mode 100644 index 0000000..bb51677 --- /dev/null +++ b/src/features/store/ui/CartList.tsx @@ -0,0 +1,50 @@ +import { getImageUrl } from '@utils/getImageUrl'; +import * as S from '../styles'; +const testItem = [ + { + id: 1, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 2, + label: '해적 의상', + img: '해적-의상.svg', + place: '1000', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, +]; +export default function CartList() { + return ( + <> + + {testItem.map(item => ( + + {item.label} + + {item.place} Point + + ))} + 총 1500포인트 + + + ); +} diff --git a/src/features/store/ui/ItemContainer.tsx b/src/features/store/ui/ItemContainer.tsx new file mode 100644 index 0000000..9bca9cd --- /dev/null +++ b/src/features/store/ui/ItemContainer.tsx @@ -0,0 +1,58 @@ +import { getImageUrl } from '@/utils/getImageUrl'; +import * as S from '../styles'; +import { useState } from 'react'; +const testItem = [ + { + id: 1, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 2, + label: '해적 의상', + img: '해적-의상.svg', + place: '1000', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 4, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, +]; +export default function ItemContainer() { + //스타일링을 위함 추후 수정 예정 + const [currentPage, setCurrentPage] = useState(); + return ( + <> + + {testItem.map(item => ( + + {item.label} + + {item.place} Point + + ))} + + {/* 추후 하드코딩 수정 */} + + {[1, 2, 3, 4, 5].map(page => ( + setCurrentPage(page)} + $isSelect={page === currentPage} + > + {page} + + ))} + + + ); +} diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx index 0a66c59..59b8f0b 100644 --- a/src/pages/store/Store.tsx +++ b/src/pages/store/Store.tsx @@ -3,8 +3,13 @@ import Header from '@common/layout/Header'; import MenuBar from '@common/layout/MenuBar'; import * as globalS from '@/style/style'; import * as S from './styles'; - +import { useState } from 'react'; +import ItemContainer from '@/features/store/ui/ItemContainer'; +import CartList from '@features/store/ui/CartList'; +const buttonLabelList = ['의상', '악세사리', '프로필', '색상'] as const; export default function Store() { + const [selectButton, setSelectButton] = + useState<(typeof buttonLabelList)[number]>(); return ( <> @@ -16,10 +21,37 @@ export default function Store() {
    장바구니 + - + + +
    + 내가 구매한 아이템 + 초기화 +
    +
    +
    대충 캐릭터
    +
    대충 포인트
    +
    +
    + + + {buttonLabelList.map(label => ( + setSelectButton(label)} + $isSelect={selectButton === label} + > + {label} + + ))} + + + + +
    ); } diff --git a/src/pages/store/styles.ts b/src/pages/store/styles.ts index 86015dc..0672a46 100644 --- a/src/pages/store/styles.ts +++ b/src/pages/store/styles.ts @@ -1,7 +1,10 @@ -import { styled } from 'styled-components'; +import { css, styled } from 'styled-components'; export const CartListWrapper = styled.section` position: absolute; + display: flex; + flex-direction: column; + align-items: center; width: 194px; height: 596px; border-radius: 20px; @@ -10,9 +13,94 @@ export const CartListWrapper = styled.section` margin: 84px 47px 0 0; `; export const Label = styled.label` + margin: 18px 0 0 0; + display: block; width: 147.003px; height: 28.004px; border-radius: 15px; border: 2px solid #f09900; background: #ffb53d; + color: #fff; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 171.429% */ +`; +export const MyCharacterSection = styled.section` + display: flex; + width: 683px; + height: 199px; + border-radius: 20px; + background: #fff; + margin: 46px 0 0 0; + box-shadow: 0 3px #e5e5e5; + + > div:nth-child(1) { + display: flex; + flex-direction: column; + gap: 9px; + margin: 12px 0 0 24px; + } +`; + +export const StoreItemListSection = styled.section` + display: flex; + flex-direction: column; + align-items: center; + width: 683px; + height: 428px; + border-radius: 20px; + background: #fff; + margin: 26px 0 0 0; + box-shadow: 0 3px #e5e5e5; +`; + +export const FilterListContainer = styled.div` + display: flex; + align-self: flex-end; + gap: 23px; + align-items: center; + justify-content: flex-end; + padding: 18px 40px 19px 0; +`; +export const FilterButton = styled.button<{ $isSelect: boolean }>` + width: 79px; + height: 23px; + border-radius: 15px; + border: 2px solid #ff4949; + background: #f4f4f4; + color: #ff4949; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 166.667% */ + text-transform: lowercase; + ${({ $isSelect }) => + $isSelect && + css` + color: #fff; + border-radius: 15px; + border: 2px solid #e8080c; + background: #ff4949; + `} +`; +export const RedLine = styled.hr` + width: 632px; + height: 2px; + border-color: #ff4949; +`; + +export const Button = styled.button` + width: 121px; + height: 23px; + border-radius: 15px; + border: 2px solid #e8080c; + background: #ff4949; + color: #fff; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 166.667% */ + text-transform: lowercase; `; From c77a805bc54d8d0f9ce4c32627421a0f9dd325e6 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 18:44:59 +0900 Subject: [PATCH 12/32] =?UTF-8?q?Publish(#83):=20=EC=BA=90=EB=A6=AD?= =?UTF-8?q?=ED=84=B0=20=EC=A0=9C=EC=99=B8=20=ED=8D=BC=EB=B8=94=EB=A6=AC?= =?UTF-8?q?=EC=8B=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/styles.ts | 105 ++++++++++++++++++++++++ src/features/store/ui/CartList.tsx | 50 +++++++++++ src/features/store/ui/ItemContainer.tsx | 58 +++++++++++++ src/pages/store/Store.tsx | 36 +++++++- src/pages/store/styles.ts | 90 +++++++++++++++++++- 5 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 src/features/store/styles.ts create mode 100644 src/features/store/ui/CartList.tsx create mode 100644 src/features/store/ui/ItemContainer.tsx diff --git a/src/features/store/styles.ts b/src/features/store/styles.ts new file mode 100644 index 0000000..f834454 --- /dev/null +++ b/src/features/store/styles.ts @@ -0,0 +1,105 @@ +import styled, { css } from 'styled-components'; + +export const StoreItemWrapper = styled.div` + margin: 18px 0 27px 0; + display: grid; + grid-template-columns: repeat(4, 144px); + grid-template-rows: repeat(2, 125px); + gap: 21px 19px; +`; +export const StoreItem = styled.div` + display: flex; + flex-direction: column; + align-items: center; + border-radius: 8px; + border: 2px solid #a5ecf0; + background: #f4f4f4; + padding: 10px 0; + &:hover { + border-radius: 8px; + border: 2px solid #00b6c0; + background: rgba(0, 217, 233, 0.8); + > label { + border-radius: 15px; + border: 2px solid #00d9e9; + background: #a5ecf0; + } + } +`; +export const ItemLabel = styled.label` + text-align: center; + display: block; + width: 73px; + height: 17px; + border-radius: 15px; + border: 2px solid #a5ecf0; + background: #00d9e9; + color: #fff; + font-size: 10px; + font-style: normal; + font-weight: 700; + line-height: 16px; /* 160% */ + letter-spacing: 0.2px; +`; +export const ItemImage = styled.img` + width: 125px; + height: 70px; +`; +export const PaginationDiv = styled.div` + width: 276px; + display: flex; + gap: 57px; +`; +export const PaginationButton = styled.button<{ $isSelect: boolean }>` + border: 0; + background-color: transparent; + color: #a5ecf0; + font-size: 15px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 160% */ + ${({ $isSelect }) => + $isSelect && + css` + color: #00d9e9; + `} +`; +export const StoreCartListWrapper = styled.div` + display: flex; + overflow-y: auto; + margin: 26px 0 0 0; + flex-direction: column; + gap: 28px; + padding-right: 8px; + &::-webkit-scrollbar { + padding-left: 5px; + width: 8px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: #a5ecf0; + border-radius: 4px; + } +`; + +export const PlaceLabel = styled.label` + margin-bottom: 26px; + text-align: center; + display: block; + width: 142.001px; + height: 28.004px; + border-radius: 15px; + border: 2px solid #f09900; + background: #ffb53d; + color: #fff; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 171.429% */ +`; diff --git a/src/features/store/ui/CartList.tsx b/src/features/store/ui/CartList.tsx new file mode 100644 index 0000000..bb51677 --- /dev/null +++ b/src/features/store/ui/CartList.tsx @@ -0,0 +1,50 @@ +import { getImageUrl } from '@utils/getImageUrl'; +import * as S from '../styles'; +const testItem = [ + { + id: 1, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 2, + label: '해적 의상', + img: '해적-의상.svg', + place: '1000', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, +]; +export default function CartList() { + return ( + <> + + {testItem.map(item => ( + + {item.label} + + {item.place} Point + + ))} + 총 1500포인트 + + + ); +} diff --git a/src/features/store/ui/ItemContainer.tsx b/src/features/store/ui/ItemContainer.tsx new file mode 100644 index 0000000..9bca9cd --- /dev/null +++ b/src/features/store/ui/ItemContainer.tsx @@ -0,0 +1,58 @@ +import { getImageUrl } from '@/utils/getImageUrl'; +import * as S from '../styles'; +import { useState } from 'react'; +const testItem = [ + { + id: 1, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 2, + label: '해적 의상', + img: '해적-의상.svg', + place: '1000', + }, + { + id: 3, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, + { + id: 4, + label: '해적 베레모', + img: '해적-베레모.svg', + place: '500', + }, +]; +export default function ItemContainer() { + //스타일링을 위함 추후 수정 예정 + const [currentPage, setCurrentPage] = useState(); + return ( + <> + + {testItem.map(item => ( + + {item.label} + + {item.place} Point + + ))} + + {/* 추후 하드코딩 수정 */} + + {[1, 2, 3, 4, 5].map(page => ( + setCurrentPage(page)} + $isSelect={page === currentPage} + > + {page} + + ))} + + + ); +} diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx index 0a66c59..59b8f0b 100644 --- a/src/pages/store/Store.tsx +++ b/src/pages/store/Store.tsx @@ -3,8 +3,13 @@ import Header from '@common/layout/Header'; import MenuBar from '@common/layout/MenuBar'; import * as globalS from '@/style/style'; import * as S from './styles'; - +import { useState } from 'react'; +import ItemContainer from '@/features/store/ui/ItemContainer'; +import CartList from '@features/store/ui/CartList'; +const buttonLabelList = ['의상', '악세사리', '프로필', '색상'] as const; export default function Store() { + const [selectButton, setSelectButton] = + useState<(typeof buttonLabelList)[number]>(); return ( <> @@ -16,10 +21,37 @@ export default function Store() {
    장바구니 + - + + +
    + 내가 구매한 아이템 + 초기화 +
    +
    +
    대충 캐릭터
    +
    대충 포인트
    +
    +
    + + + {buttonLabelList.map(label => ( + setSelectButton(label)} + $isSelect={selectButton === label} + > + {label} + + ))} + + + + +
    ); } diff --git a/src/pages/store/styles.ts b/src/pages/store/styles.ts index 86015dc..0672a46 100644 --- a/src/pages/store/styles.ts +++ b/src/pages/store/styles.ts @@ -1,7 +1,10 @@ -import { styled } from 'styled-components'; +import { css, styled } from 'styled-components'; export const CartListWrapper = styled.section` position: absolute; + display: flex; + flex-direction: column; + align-items: center; width: 194px; height: 596px; border-radius: 20px; @@ -10,9 +13,94 @@ export const CartListWrapper = styled.section` margin: 84px 47px 0 0; `; export const Label = styled.label` + margin: 18px 0 0 0; + display: block; width: 147.003px; height: 28.004px; border-radius: 15px; border: 2px solid #f09900; background: #ffb53d; + color: #fff; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 171.429% */ +`; +export const MyCharacterSection = styled.section` + display: flex; + width: 683px; + height: 199px; + border-radius: 20px; + background: #fff; + margin: 46px 0 0 0; + box-shadow: 0 3px #e5e5e5; + + > div:nth-child(1) { + display: flex; + flex-direction: column; + gap: 9px; + margin: 12px 0 0 24px; + } +`; + +export const StoreItemListSection = styled.section` + display: flex; + flex-direction: column; + align-items: center; + width: 683px; + height: 428px; + border-radius: 20px; + background: #fff; + margin: 26px 0 0 0; + box-shadow: 0 3px #e5e5e5; +`; + +export const FilterListContainer = styled.div` + display: flex; + align-self: flex-end; + gap: 23px; + align-items: center; + justify-content: flex-end; + padding: 18px 40px 19px 0; +`; +export const FilterButton = styled.button<{ $isSelect: boolean }>` + width: 79px; + height: 23px; + border-radius: 15px; + border: 2px solid #ff4949; + background: #f4f4f4; + color: #ff4949; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 166.667% */ + text-transform: lowercase; + ${({ $isSelect }) => + $isSelect && + css` + color: #fff; + border-radius: 15px; + border: 2px solid #e8080c; + background: #ff4949; + `} +`; +export const RedLine = styled.hr` + width: 632px; + height: 2px; + border-color: #ff4949; +`; + +export const Button = styled.button` + width: 121px; + height: 23px; + border-radius: 15px; + border: 2px solid #e8080c; + background: #ff4949; + color: #fff; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 166.667% */ + text-transform: lowercase; `; From 8fd7f50814927a9fbd485f15884889e7595d9479 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sat, 7 Dec 2024 23:19:24 +0900 Subject: [PATCH 13/32] =?UTF-8?q?=F0=9F=8E=A8=20Publish(#83):=20=EC=BA=90?= =?UTF-8?q?=EB=A6=AD=ED=84=B0=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/styles.ts | 9 ++++++++- src/features/store/ui/CartList.tsx | 11 +++++++---- src/features/store/ui/ItemContainer.tsx | 13 ++++++++++--- src/features/user/styles.ts | 5 +++++ src/features/user/ui/MyCharacter.tsx | 12 ++++++++++++ src/pages/store/Store.tsx | 21 ++++++++++----------- src/pages/store/styles.ts | 3 ++- 7 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 src/features/user/ui/MyCharacter.tsx diff --git a/src/features/store/styles.ts b/src/features/store/styles.ts index f834454..3d0c1ff 100644 --- a/src/features/store/styles.ts +++ b/src/features/store/styles.ts @@ -1,11 +1,17 @@ import styled, { css } from 'styled-components'; -export const StoreItemWrapper = styled.div` +export const StoreItemWrapper = styled.div<{ $query: string }>` margin: 18px 0 27px 0; display: grid; grid-template-columns: repeat(4, 144px); grid-template-rows: repeat(2, 125px); gap: 21px 19px; + ${({ $query }) => + $query === '프로필' && + css` + grid-template-columns: repeat(3, 180px); + grid-template-rows: repeat(1, 217px); + `} `; export const StoreItem = styled.div` display: flex; @@ -15,6 +21,7 @@ export const StoreItem = styled.div` border: 2px solid #a5ecf0; background: #f4f4f4; padding: 10px 0; + cursor: pointer; &:hover { border-radius: 8px; border: 2px solid #00b6c0; diff --git a/src/features/store/ui/CartList.tsx b/src/features/store/ui/CartList.tsx index bb51677..4a1386e 100644 --- a/src/features/store/ui/CartList.tsx +++ b/src/features/store/ui/CartList.tsx @@ -20,26 +20,29 @@ const testItem = [ place: '500', }, { - id: 3, + id: 4, label: '해적 베레모', img: '해적-베레모.svg', place: '500', }, { - id: 3, + id: 5, label: '해적 베레모', img: '해적-베레모.svg', place: '500', }, ]; -export default function CartList() { +interface CartListProps { + query: string; +} +export default function CartList({ query }: CartListProps) { return ( <> {testItem.map(item => ( {item.label} - + {item.place} Point ))} diff --git a/src/features/store/ui/ItemContainer.tsx b/src/features/store/ui/ItemContainer.tsx index 9bca9cd..c433131 100644 --- a/src/features/store/ui/ItemContainer.tsx +++ b/src/features/store/ui/ItemContainer.tsx @@ -7,36 +7,43 @@ const testItem = [ label: '해적 베레모', img: '해적-베레모.svg', place: '500', + category: 'hat', }, { id: 2, label: '해적 의상', img: '해적-의상.svg', place: '1000', + category: 'top', }, { id: 3, label: '해적 베레모', img: '해적-베레모.svg', place: '500', + category: 'accessories', }, { id: 4, label: '해적 베레모', img: '해적-베레모.svg', place: '500', + category: 'color', }, ]; -export default function ItemContainer() { +interface ItemContainerProps { + query: string; +} +export default function ItemContainer({ query }: ItemContainerProps) { //스타일링을 위함 추후 수정 예정 const [currentPage, setCurrentPage] = useState(); return ( <> - + {testItem.map(item => ( {item.label} - + {item.place} Point ))} diff --git a/src/features/user/styles.ts b/src/features/user/styles.ts index 6453f9c..2434665 100644 --- a/src/features/user/styles.ts +++ b/src/features/user/styles.ts @@ -49,3 +49,8 @@ export const BadgeListItem = styled.li` text-transform: lowercase; } `; +//추후에 canvas 태그 등으로 변경 +export const MyCharacterImage = styled.img` + width: 171px; + height: 138px; +`; diff --git a/src/features/user/ui/MyCharacter.tsx b/src/features/user/ui/MyCharacter.tsx new file mode 100644 index 0000000..f45bffa --- /dev/null +++ b/src/features/user/ui/MyCharacter.tsx @@ -0,0 +1,12 @@ +import { getImageUrl } from '@utils/getImageUrl'; +import { MyCharacterImage } from '../styles'; + +export default function MyCharacter() { + return ( + <> + + + ); +} diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx index 59b8f0b..2f5e412 100644 --- a/src/pages/store/Store.tsx +++ b/src/pages/store/Store.tsx @@ -4,12 +4,14 @@ import MenuBar from '@common/layout/MenuBar'; import * as globalS from '@/style/style'; import * as S from './styles'; import { useState } from 'react'; -import ItemContainer from '@/features/store/ui/ItemContainer'; +import ItemContainer from '@features/store/ui/ItemContainer'; import CartList from '@features/store/ui/CartList'; +import MyCharacter from '@features/user/ui/MyCharacter'; +import ProfileImage from '@features/user/ui/ProfileImage'; const buttonLabelList = ['의상', '악세사리', '프로필', '색상'] as const; export default function Store() { - const [selectButton, setSelectButton] = - useState<(typeof buttonLabelList)[number]>(); + const [itemQuery, setItemQuery] = + useState<(typeof buttonLabelList)[number]>('의상'); return ( <> @@ -21,7 +23,7 @@ export default function Store() {
    장바구니 - + @@ -31,25 +33,22 @@ export default function Store() { 내가 구매한 아이템 초기화 -
    -
    대충 캐릭터
    -
    대충 포인트
    -
    + {itemQuery === '프로필' ? : } {buttonLabelList.map(label => ( setSelectButton(label)} - $isSelect={selectButton === label} + onClick={() => setItemQuery(label)} + $isSelect={itemQuery === label} > {label} ))} - + diff --git a/src/pages/store/styles.ts b/src/pages/store/styles.ts index 0672a46..a77371e 100644 --- a/src/pages/store/styles.ts +++ b/src/pages/store/styles.ts @@ -34,13 +34,14 @@ export const MyCharacterSection = styled.section` border-radius: 20px; background: #fff; margin: 46px 0 0 0; + gap: 119px; box-shadow: 0 3px #e5e5e5; + padding: 12px 0 0 24px; > div:nth-child(1) { display: flex; flex-direction: column; gap: 9px; - margin: 12px 0 0 24px; } `; From 64ee2c05d83b92cf44123b2ec9481f1af5a934d5 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sun, 8 Dec 2024 15:47:38 +0900 Subject: [PATCH 14/32] =?UTF-8?q?=F0=9F=8E=A8=20Publish(#83):=20=EC=83=81?= =?UTF-8?q?=EC=A0=90=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=8D=BC=EB=B8=94?= =?UTF-8?q?=EB=A6=AC=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/styles.ts | 17 +++++--- src/features/store/ui/CartList.tsx | 54 ++++++++++++------------- src/features/store/ui/ItemContainer.tsx | 47 +++++++++++---------- src/pages/store/Store.tsx | 37 ++++++++++++----- src/types/Item.ts | 8 ++++ 5 files changed, 100 insertions(+), 63 deletions(-) create mode 100644 src/types/Item.ts diff --git a/src/features/store/styles.ts b/src/features/store/styles.ts index 3d0c1ff..4ea0a32 100644 --- a/src/features/store/styles.ts +++ b/src/features/store/styles.ts @@ -1,13 +1,14 @@ +import Item from '@type/Item'; import styled, { css } from 'styled-components'; -export const StoreItemWrapper = styled.div<{ $query: string }>` +export const StoreItemWrapper = styled.div<{ $category: Item['category'] }>` margin: 18px 0 27px 0; display: grid; grid-template-columns: repeat(4, 144px); grid-template-rows: repeat(2, 125px); gap: 21px 19px; - ${({ $query }) => - $query === '프로필' && + ${({ $category }) => + $category === 'profile' && css` grid-template-columns: repeat(3, 180px); grid-template-rows: repeat(1, 217px); @@ -48,9 +49,15 @@ export const ItemLabel = styled.label` line-height: 16px; /* 160% */ letter-spacing: 0.2px; `; -export const ItemImage = styled.img` +export const ItemImage = styled.img<{ $category: Item['category'] }>` width: 125px; height: 70px; + ${({ $category }) => + $category === 'profile' && + css` + width: 160px; + height: 160px; + `} `; export const PaginationDiv = styled.div` width: 276px; @@ -95,7 +102,7 @@ export const StoreCartListWrapper = styled.div` `; export const PlaceLabel = styled.label` - margin-bottom: 26px; + margin: 28px 0 26px 0; text-align: center; display: block; width: 142.001px; diff --git a/src/features/store/ui/CartList.tsx b/src/features/store/ui/CartList.tsx index 4a1386e..c970a1e 100644 --- a/src/features/store/ui/CartList.tsx +++ b/src/features/store/ui/CartList.tsx @@ -1,53 +1,53 @@ import { getImageUrl } from '@utils/getImageUrl'; import * as S from '../styles'; -const testItem = [ +import Item from '@type/Item'; +const testItem: Item[] = [ { id: 1, - label: '해적 베레모', - img: '해적-베레모.svg', - place: '500', + name: '해적 베레모', + image: '해적-베레모.svg', + cost: 500, + category: 'accessories', }, { id: 2, - label: '해적 의상', - img: '해적-의상.svg', - place: '1000', + name: '해적 의상', + image: '해적-의상.svg', + cost: 1000, + category: 'clothes', }, { id: 3, - label: '해적 베레모', - img: '해적-베레모.svg', - place: '500', + name: '해초의 습격', + image: '해초의-습격.svg', + cost: 500, + category: 'profile', }, { id: 4, - label: '해적 베레모', - img: '해적-베레모.svg', - place: '500', - }, - { - id: 5, - label: '해적 베레모', - img: '해적-베레모.svg', - place: '500', + name: '해적 베레모', + image: '해적-베레모.svg', + cost: 500, + category: 'color', }, ]; -interface CartListProps { - query: string; -} -export default function CartList({ query }: CartListProps) { + +export default function CartList() { return ( <> {testItem.map(item => ( - {item.label} - - {item.place} Point + {item.name} + + {item.cost} Point ))} - 총 1500포인트 + 총 1500포인트 ); } diff --git a/src/features/store/ui/ItemContainer.tsx b/src/features/store/ui/ItemContainer.tsx index c433131..df16c15 100644 --- a/src/features/store/ui/ItemContainer.tsx +++ b/src/features/store/ui/ItemContainer.tsx @@ -1,50 +1,55 @@ import { getImageUrl } from '@/utils/getImageUrl'; import * as S from '../styles'; import { useState } from 'react'; -const testItem = [ +import Item from '@type/Item'; +const testItem: Item[] = [ { id: 1, - label: '해적 베레모', - img: '해적-베레모.svg', - place: '500', - category: 'hat', + name: '해적 베레모', + image: '해적-베레모.svg', + cost: 500, + category: 'accessories', }, { id: 2, - label: '해적 의상', - img: '해적-의상.svg', - place: '1000', - category: 'top', + name: '해적 의상', + image: '해적-의상.svg', + cost: 1000, + category: 'clothes', }, { id: 3, - label: '해적 베레모', - img: '해적-베레모.svg', - place: '500', - category: 'accessories', + name: '해초의 습격', + image: '해초의-습격.svg', + cost: 500, + category: 'profile', }, { id: 4, - label: '해적 베레모', - img: '해적-베레모.svg', - place: '500', + name: '해적 베레모', + image: '해적-베레모.svg', + cost: 500, category: 'color', }, ]; + interface ItemContainerProps { - query: string; + query: Item['category']; } export default function ItemContainer({ query }: ItemContainerProps) { //스타일링을 위함 추후 수정 예정 const [currentPage, setCurrentPage] = useState(); return ( <> - + {testItem.map(item => ( - {item.label} - - {item.place} Point + {item.name} + + {item.cost} Point ))} diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx index 2f5e412..c1e467a 100644 --- a/src/pages/store/Store.tsx +++ b/src/pages/store/Store.tsx @@ -8,10 +8,27 @@ import ItemContainer from '@features/store/ui/ItemContainer'; import CartList from '@features/store/ui/CartList'; import MyCharacter from '@features/user/ui/MyCharacter'; import ProfileImage from '@features/user/ui/ProfileImage'; -const buttonLabelList = ['의상', '악세사리', '프로필', '색상'] as const; +import Item from '@type/Item'; +const buttonList: { label: string; name: Item['category'] }[] = [ + { + label: '의상', + name: 'clothes', + }, + { + label: '악세사리', + name: 'accessories', + }, + { + label: '프로필', + name: 'profile', + }, + { + label: '색상', + name: 'color', + }, +]; export default function Store() { - const [itemQuery, setItemQuery] = - useState<(typeof buttonLabelList)[number]>('의상'); + const [itemQuery, setItemQuery] = useState('clothes'); return ( <> @@ -23,7 +40,7 @@ export default function Store() {
    장바구니 - + @@ -33,17 +50,17 @@ export default function Store() { 내가 구매한 아이템 초기화 - {itemQuery === '프로필' ? : } + {itemQuery === 'profile' ? : } - {buttonLabelList.map(label => ( + {buttonList.map(item => ( setItemQuery(label)} - $isSelect={itemQuery === label} + key={item.name} + onClick={() => setItemQuery(item.name)} + $isSelect={itemQuery === item.name} > - {label} + {item.label} ))} diff --git a/src/types/Item.ts b/src/types/Item.ts new file mode 100644 index 0000000..e59ac5b --- /dev/null +++ b/src/types/Item.ts @@ -0,0 +1,8 @@ +interface Item { + id: number; + name: string; + cost: number; + image: string; + category: 'clothes' | 'accessories' | 'profile' | 'color'; +} +export default Item; From 89335fa07d029f02fd72b75027b70325b0f5b997 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sun, 8 Dec 2024 17:23:46 +0900 Subject: [PATCH 15/32] =?UTF-8?q?=F0=9F=8E=A8=20Publish(#83):=20css=20?= =?UTF-8?q?=EC=83=81=EC=86=8D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD,=20It?= =?UTF-8?q?em=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/styles.ts | 49 ++++++++++++++++++------- src/features/store/ui/CartList.tsx | 13 ++----- src/features/store/ui/ItemContainer.tsx | 17 +++------ src/features/store/ui/StoreItem.tsx | 15 ++++++++ src/pages/store/Store.tsx | 2 +- src/pages/store/styles.ts | 37 +++++++++++-------- 6 files changed, 81 insertions(+), 52 deletions(-) create mode 100644 src/features/store/ui/StoreItem.tsx diff --git a/src/features/store/styles.ts b/src/features/store/styles.ts index 4ea0a32..4f8267f 100644 --- a/src/features/store/styles.ts +++ b/src/features/store/styles.ts @@ -1,12 +1,17 @@ import Item from '@type/Item'; import styled, { css } from 'styled-components'; -export const StoreItemWrapper = styled.div<{ $category: Item['category'] }>` +export const ItemContainer = styled.ul<{ $category: Item['category'] }>` margin: 18px 0 27px 0; display: grid; grid-template-columns: repeat(4, 144px); grid-template-rows: repeat(2, 125px); gap: 21px 19px; + color: #a5ecf0; + font-size: 15px; + font-style: normal; + font-weight: 700; + line-height: 24px; ${({ $category }) => $category === 'profile' && css` @@ -14,7 +19,8 @@ export const StoreItemWrapper = styled.div<{ $category: Item['category'] }>` grid-template-rows: repeat(1, 217px); `} `; -export const StoreItem = styled.div` + +export const StoreItem = styled.li` display: flex; flex-direction: column; align-items: center; @@ -22,6 +28,12 @@ export const StoreItem = styled.div` border: 2px solid #a5ecf0; background: #f4f4f4; padding: 10px 0; + color: #fff; + font-size: 10px; + font-style: normal; + font-weight: 700; + line-height: 16px; + letter-spacing: 0.2px; cursor: pointer; &:hover { border-radius: 8px; @@ -34,6 +46,7 @@ export const StoreItem = styled.div` } } `; + export const ItemLabel = styled.label` text-align: center; display: block; @@ -42,13 +55,10 @@ export const ItemLabel = styled.label` border-radius: 15px; border: 2px solid #a5ecf0; background: #00d9e9; - color: #fff; - font-size: 10px; - font-style: normal; - font-weight: 700; - line-height: 16px; /* 160% */ - letter-spacing: 0.2px; + color: inherit; + font: inherit; `; + export const ItemImage = styled.img<{ $category: Item['category'] }>` width: 125px; height: 70px; @@ -59,32 +69,43 @@ export const ItemImage = styled.img<{ $category: Item['category'] }>` height: 160px; `} `; + export const PaginationDiv = styled.div` width: 276px; display: flex; gap: 57px; -`; -export const PaginationButton = styled.button<{ $isSelect: boolean }>` - border: 0; - background-color: transparent; color: #a5ecf0; font-size: 15px; font-style: normal; font-weight: 700; line-height: 24px; /* 160% */ +`; + +export const PaginationButton = styled.button<{ $isSelect: boolean }>` + border: 0; + background-color: transparent; + color: inherit; + font: inherit; + ${({ $isSelect }) => $isSelect && css` color: #00d9e9; `} `; -export const StoreCartListWrapper = styled.div` + +export const StoreCartListWrapper = styled.ul` display: flex; overflow-y: auto; margin: 26px 0 0 0; flex-direction: column; gap: 28px; padding-right: 8px; + color: #a5ecf0; + font-size: 15px; + font-style: normal; + font-weight: 700; + line-height: 24px; &::-webkit-scrollbar { padding-left: 5px; width: 8px; @@ -115,5 +136,5 @@ export const PlaceLabel = styled.label` font-size: 14px; font-style: normal; font-weight: 700; - line-height: 24px; /* 171.429% */ + line-height: 24px; `; diff --git a/src/features/store/ui/CartList.tsx b/src/features/store/ui/CartList.tsx index c970a1e..03dc1fa 100644 --- a/src/features/store/ui/CartList.tsx +++ b/src/features/store/ui/CartList.tsx @@ -1,6 +1,6 @@ -import { getImageUrl } from '@utils/getImageUrl'; import * as S from '../styles'; -import Item from '@type/Item'; +import type Item from '@type/Item'; +import StoreItem from './StoreItem'; const testItem: Item[] = [ { id: 1, @@ -37,14 +37,7 @@ export default function CartList() { <> {testItem.map(item => ( - - {item.name} - - {item.cost} Point - + ))} 총 1500포인트 diff --git a/src/features/store/ui/ItemContainer.tsx b/src/features/store/ui/ItemContainer.tsx index df16c15..b5cd751 100644 --- a/src/features/store/ui/ItemContainer.tsx +++ b/src/features/store/ui/ItemContainer.tsx @@ -1,7 +1,7 @@ -import { getImageUrl } from '@/utils/getImageUrl'; import * as S from '../styles'; import { useState } from 'react'; -import Item from '@type/Item'; +import type Item from '@type/Item'; +import StoreItem from './StoreItem'; const testItem: Item[] = [ { id: 1, @@ -41,18 +41,11 @@ export default function ItemContainer({ query }: ItemContainerProps) { const [currentPage, setCurrentPage] = useState(); return ( <> - + {testItem.map(item => ( - - {item.name} - - {item.cost} Point - + ))} - + {/* 추후 하드코딩 수정 */} {[1, 2, 3, 4, 5].map(page => ( diff --git a/src/features/store/ui/StoreItem.tsx b/src/features/store/ui/StoreItem.tsx new file mode 100644 index 0000000..fc645f3 --- /dev/null +++ b/src/features/store/ui/StoreItem.tsx @@ -0,0 +1,15 @@ +import type Item from '@type/Item'; +import * as S from '../styles'; +import { getImageUrl } from '@utils/getImageUrl'; + +export default function StoreItem({ id, name, image, category, cost }: Item) { + return ( + <> + + {name} + + {cost} Point + + + ); +} diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx index c1e467a..9ed01d0 100644 --- a/src/pages/store/Store.tsx +++ b/src/pages/store/Store.tsx @@ -39,7 +39,7 @@ export default function Store() {
    - 장바구니 + 장바구니 diff --git a/src/pages/store/styles.ts b/src/pages/store/styles.ts index a77371e..9fd236f 100644 --- a/src/pages/store/styles.ts +++ b/src/pages/store/styles.ts @@ -11,8 +11,14 @@ export const CartListWrapper = styled.section` background: #fff; box-shadow: 0 3px #e5e5e5; margin: 84px 47px 0 0; + color: #fff; + font-size: 10px; + font-style: normal; + font-weight: 700; + line-height: 16px; + letter-spacing: 0.2px; `; -export const Label = styled.label` +export const CartLabel = styled.label` margin: 18px 0 0 0; display: block; width: 147.003px; @@ -37,7 +43,11 @@ export const MyCharacterSection = styled.section` gap: 119px; box-shadow: 0 3px #e5e5e5; padding: 12px 0 0 24px; - + color: #fff; + font-size: 12px; + font-weight: 700; + line-height: 20px; + text-transform: lowercase; > div:nth-child(1) { display: flex; flex-direction: column; @@ -64,6 +74,11 @@ export const FilterListContainer = styled.div` align-items: center; justify-content: flex-end; padding: 18px 40px 19px 0; + font-size: 12px; + font-weight: 700; + line-height: 20px; + color: #ff4949; + text-transform: lowercase; `; export const FilterButton = styled.button<{ $isSelect: boolean }>` width: 79px; @@ -71,12 +86,8 @@ export const FilterButton = styled.button<{ $isSelect: boolean }>` border-radius: 15px; border: 2px solid #ff4949; background: #f4f4f4; - color: #ff4949; - font-size: 12px; - font-style: normal; - font-weight: 700; - line-height: 20px; /* 166.667% */ - text-transform: lowercase; + font: inherit; + color: inherit; ${({ $isSelect }) => $isSelect && css` @@ -84,7 +95,7 @@ export const FilterButton = styled.button<{ $isSelect: boolean }>` border-radius: 15px; border: 2px solid #e8080c; background: #ff4949; - `} + `}; `; export const RedLine = styled.hr` width: 632px; @@ -98,10 +109,6 @@ export const Button = styled.button` border-radius: 15px; border: 2px solid #e8080c; background: #ff4949; - color: #fff; - font-size: 12px; - font-style: normal; - font-weight: 700; - line-height: 20px; /* 166.667% */ - text-transform: lowercase; + color: inherit; + font: inherit; `; From 4c079891cc5cba38aeb65247bc8d7ec00375ab8c Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Sun, 8 Dec 2024 20:10:39 +0900 Subject: [PATCH 16/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#83):=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/ui/CartList.tsx | 2 +- src/features/store/ui/ItemContainer.tsx | 2 +- src/features/store/ui/StoreItem.tsx | 2 +- src/features/store/{ => ui}/styles.ts | 0 src/features/user/ui/BadgeContainer.tsx | 2 +- src/features/user/ui/MyCharacter.tsx | 2 +- src/features/user/{ => ui}/styles.ts | 0 7 files changed, 5 insertions(+), 5 deletions(-) rename src/features/store/{ => ui}/styles.ts (100%) rename src/features/user/{ => ui}/styles.ts (100%) diff --git a/src/features/store/ui/CartList.tsx b/src/features/store/ui/CartList.tsx index 03dc1fa..38fdbc8 100644 --- a/src/features/store/ui/CartList.tsx +++ b/src/features/store/ui/CartList.tsx @@ -1,4 +1,4 @@ -import * as S from '../styles'; +import * as S from './styles'; import type Item from '@type/Item'; import StoreItem from './StoreItem'; const testItem: Item[] = [ diff --git a/src/features/store/ui/ItemContainer.tsx b/src/features/store/ui/ItemContainer.tsx index b5cd751..4cc83ba 100644 --- a/src/features/store/ui/ItemContainer.tsx +++ b/src/features/store/ui/ItemContainer.tsx @@ -1,4 +1,4 @@ -import * as S from '../styles'; +import * as S from './styles'; import { useState } from 'react'; import type Item from '@type/Item'; import StoreItem from './StoreItem'; diff --git a/src/features/store/ui/StoreItem.tsx b/src/features/store/ui/StoreItem.tsx index fc645f3..2f0cb4d 100644 --- a/src/features/store/ui/StoreItem.tsx +++ b/src/features/store/ui/StoreItem.tsx @@ -1,5 +1,5 @@ import type Item from '@type/Item'; -import * as S from '../styles'; +import * as S from './styles'; import { getImageUrl } from '@utils/getImageUrl'; export default function StoreItem({ id, name, image, category, cost }: Item) { diff --git a/src/features/store/styles.ts b/src/features/store/ui/styles.ts similarity index 100% rename from src/features/store/styles.ts rename to src/features/store/ui/styles.ts diff --git a/src/features/user/ui/BadgeContainer.tsx b/src/features/user/ui/BadgeContainer.tsx index ea510f4..7a5b498 100644 --- a/src/features/user/ui/BadgeContainer.tsx +++ b/src/features/user/ui/BadgeContainer.tsx @@ -1,4 +1,4 @@ -import * as S from '../styles'; +import * as S from './styles'; import { getImageUrl } from '@/utils/getImageUrl'; const testBadgeList = [ diff --git a/src/features/user/ui/MyCharacter.tsx b/src/features/user/ui/MyCharacter.tsx index f45bffa..048326a 100644 --- a/src/features/user/ui/MyCharacter.tsx +++ b/src/features/user/ui/MyCharacter.tsx @@ -1,5 +1,5 @@ import { getImageUrl } from '@utils/getImageUrl'; -import { MyCharacterImage } from '../styles'; +import { MyCharacterImage } from './styles'; export default function MyCharacter() { return ( diff --git a/src/features/user/styles.ts b/src/features/user/ui/styles.ts similarity index 100% rename from src/features/user/styles.ts rename to src/features/user/ui/styles.ts From 50705cb5ad97613955c4d58f88a573ad46b98a15 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Mon, 9 Dec 2024 00:21:24 +0900 Subject: [PATCH 17/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#83):=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EC=A0=95=20Key=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/ui/CartList.tsx | 6 +++--- src/features/store/ui/ItemContainer.tsx | 8 ++++---- src/features/store/ui/StoreItem.tsx | 16 +++++++--------- src/features/store/ui/styles.ts | 2 +- src/pages/store/Store.tsx | 9 +++++---- src/types/{Item.ts => CosmeticItem .ts} | 4 ++-- 6 files changed, 22 insertions(+), 23 deletions(-) rename src/types/{Item.ts => CosmeticItem .ts} (70%) diff --git a/src/features/store/ui/CartList.tsx b/src/features/store/ui/CartList.tsx index 38fdbc8..eee5707 100644 --- a/src/features/store/ui/CartList.tsx +++ b/src/features/store/ui/CartList.tsx @@ -1,7 +1,7 @@ import * as S from './styles'; -import type Item from '@type/Item'; +import type CosmeticItem from '@/types/CosmeticItem '; import StoreItem from './StoreItem'; -const testItem: Item[] = [ +const testItem: CosmeticItem[] = [ { id: 1, name: '해적 베레모', @@ -37,7 +37,7 @@ export default function CartList() { <> {testItem.map(item => ( - + ))} 총 1500포인트 diff --git a/src/features/store/ui/ItemContainer.tsx b/src/features/store/ui/ItemContainer.tsx index 4cc83ba..481b213 100644 --- a/src/features/store/ui/ItemContainer.tsx +++ b/src/features/store/ui/ItemContainer.tsx @@ -1,8 +1,8 @@ import * as S from './styles'; import { useState } from 'react'; -import type Item from '@type/Item'; +import type CosmeticItem from '@/types/CosmeticItem '; import StoreItem from './StoreItem'; -const testItem: Item[] = [ +const testItem: CosmeticItem[] = [ { id: 1, name: '해적 베레모', @@ -34,7 +34,7 @@ const testItem: Item[] = [ ]; interface ItemContainerProps { - query: Item['category']; + query: CosmeticItem['category']; } export default function ItemContainer({ query }: ItemContainerProps) { //스타일링을 위함 추후 수정 예정 @@ -43,7 +43,7 @@ export default function ItemContainer({ query }: ItemContainerProps) { <> {testItem.map(item => ( - + ))} {/* 추후 하드코딩 수정 */} diff --git a/src/features/store/ui/StoreItem.tsx b/src/features/store/ui/StoreItem.tsx index 2f0cb4d..dde4848 100644 --- a/src/features/store/ui/StoreItem.tsx +++ b/src/features/store/ui/StoreItem.tsx @@ -1,15 +1,13 @@ -import type Item from '@type/Item'; +import type Item from '@/types/CosmeticItem '; import * as S from './styles'; import { getImageUrl } from '@utils/getImageUrl'; -export default function StoreItem({ id, name, image, category, cost }: Item) { +export default function StoreItem({ name, image, category, cost }: Item) { return ( - <> - - {name} - - {cost} Point - - + + {name} + + {cost} Point + ); } diff --git a/src/features/store/ui/styles.ts b/src/features/store/ui/styles.ts index 4f8267f..0c81632 100644 --- a/src/features/store/ui/styles.ts +++ b/src/features/store/ui/styles.ts @@ -1,4 +1,4 @@ -import Item from '@type/Item'; +import Item from '@/types/CosmeticItem '; import styled, { css } from 'styled-components'; export const ItemContainer = styled.ul<{ $category: Item['category'] }>` diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx index 9ed01d0..6fd5579 100644 --- a/src/pages/store/Store.tsx +++ b/src/pages/store/Store.tsx @@ -8,8 +8,8 @@ import ItemContainer from '@features/store/ui/ItemContainer'; import CartList from '@features/store/ui/CartList'; import MyCharacter from '@features/user/ui/MyCharacter'; import ProfileImage from '@features/user/ui/ProfileImage'; -import Item from '@type/Item'; -const buttonList: { label: string; name: Item['category'] }[] = [ +import CosmeticItem from '@/types/CosmeticItem '; +const buttonList: { label: string; name: CosmeticItem['category'] }[] = [ { label: '의상', name: 'clothes', @@ -26,9 +26,10 @@ const buttonList: { label: string; name: Item['category'] }[] = [ label: '색상', name: 'color', }, -]; +] as const; export default function Store() { - const [itemQuery, setItemQuery] = useState('clothes'); + const [itemQuery, setItemQuery] = + useState('clothes'); return ( <> diff --git a/src/types/Item.ts b/src/types/CosmeticItem .ts similarity index 70% rename from src/types/Item.ts rename to src/types/CosmeticItem .ts index e59ac5b..fe31342 100644 --- a/src/types/Item.ts +++ b/src/types/CosmeticItem .ts @@ -1,8 +1,8 @@ -interface Item { +interface CosmeticItem { id: number; name: string; cost: number; image: string; category: 'clothes' | 'accessories' | 'profile' | 'color'; } -export default Item; +export default CosmeticItem; From ce8ba1246c41992265b0da48505dd6a7d0f7d2e7 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Mon, 9 Dec 2024 00:23:32 +0900 Subject: [PATCH 18/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#83):=20CosmeticI?= =?UTF-8?q?tem=ED=83=80=EC=9E=85=20=EC=B0=B8=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/ui/StoreItem.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/features/store/ui/StoreItem.tsx b/src/features/store/ui/StoreItem.tsx index dde4848..77669bf 100644 --- a/src/features/store/ui/StoreItem.tsx +++ b/src/features/store/ui/StoreItem.tsx @@ -1,8 +1,13 @@ -import type Item from '@/types/CosmeticItem '; import * as S from './styles'; import { getImageUrl } from '@utils/getImageUrl'; +import CosmeticItem from '@type/CosmeticItem '; -export default function StoreItem({ name, image, category, cost }: Item) { +export default function StoreItem({ + name, + image, + category, + cost, +}: CosmeticItem) { return ( {name} From de4e42f64a15e82c1f96361910a34c27cf1c484b Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Mon, 9 Dec 2024 00:25:00 +0900 Subject: [PATCH 19/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#83):=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=ED=8C=8C=EC=9D=BCCosmeticItem=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=B0=B8=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/store/ui/styles.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/store/ui/styles.ts b/src/features/store/ui/styles.ts index 0c81632..ad7497c 100644 --- a/src/features/store/ui/styles.ts +++ b/src/features/store/ui/styles.ts @@ -1,7 +1,7 @@ -import Item from '@/types/CosmeticItem '; +import CosmeticItem from '@type/CosmeticItem '; import styled, { css } from 'styled-components'; -export const ItemContainer = styled.ul<{ $category: Item['category'] }>` +export const ItemContainer = styled.ul<{ $category: CosmeticItem['category'] }>` margin: 18px 0 27px 0; display: grid; grid-template-columns: repeat(4, 144px); @@ -59,7 +59,7 @@ export const ItemLabel = styled.label` font: inherit; `; -export const ItemImage = styled.img<{ $category: Item['category'] }>` +export const ItemImage = styled.img<{ $category: CosmeticItem['category'] }>` width: 125px; height: 70px; ${({ $category }) => From 09e964029d1115229ce5b0601f0f2d61aada8ce8 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 06:42:52 +0900 Subject: [PATCH 20/32] =?UTF-8?q?=E2=9C=A8=20Feature(#67):=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=9C=A0=EB=AC=B4=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?Modal=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20Interceptor=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 2 +- src/apis/axios/intercepter.ts | 18 ++++-- src/common/layout/Header.tsx | 74 +++++++++++++++++++--- src/common/layout/style.ts | 1 + src/common/ui/style.ts | 41 ++++++++++++ src/features/login/service/handleLogin.ts | 19 ++++-- src/features/login/service/handleLogout.ts | 3 +- src/features/login/styles.ts | 1 - src/hooks/useAuth.ts | 6 +- src/hooks/useOutsideClick.ts | 30 +++++++-- 10 files changed, 160 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5ede711..a9d1389 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,7 +2,7 @@ name: CI/CD Docker on: push: - branches: [main, develop, feature/#67/OAuth_2_0] + branches: [main, develop] env: DOCKER_IMAGE: ghcr.io/${{ github.actor }}/react-auto-deploy diff --git a/src/apis/axios/intercepter.ts b/src/apis/axios/intercepter.ts index 38c5c1a..b8c7413 100644 --- a/src/apis/axios/intercepter.ts +++ b/src/apis/axios/intercepter.ts @@ -1,23 +1,27 @@ import api from './instance'; +import { getCookie } from '@utils/cookies'; + +// 요청 인터셉터 api.interceptors.request.use(config => { - //요청 성공 직전 호출 - //헤더에 인가 토큰 부착 - //로컬스토리지에 저장한다고 가정한다면 - const accessToken: string | null = localStorage.getItem('Token'); - if (accessToken !== null) { + // 쿠키에서 accessToken 가져오기 + const accessToken: string | undefined = getCookie('accessToken'); + if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }); + +// 응답 인터셉터 api.interceptors.response.use( - //http status가 200번대인 경우 호출 response => { + // HTTP 상태 코드가 200번대인 경우 console.log(response); return response; }, error => { + // HTTP 상태 코드가 에러인 경우 console.log(error); - //http status가 에러 코드인경우 실행 + return Promise.reject(error); } ); diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index 204df17..7173d14 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -1,16 +1,40 @@ +import { useState, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; import { getImageUrl } from '@utils/getImageUrl'; -import HeaderItem from '../ui/HeaderItem'; import { HeaderBox } from './style'; +import * as S from '../ui/style'; +import HeaderItem from '../ui/HeaderItem'; import Login from '@features/login/ui/Login'; -import { ProfileWrapper, ProfileIcon, HeaderIcon } from '../ui/style'; import useModal from '@hooks/useModal'; import useUserStore from '@/store/useUserStore'; +import useAuth from '@hooks/useAuth'; +import useOutsideClick from '@hooks/useOutsideClick'; +import handleLogout from '@features/login/service/handleLogout'; export default function Header() { const points: number = 2999999999; const lifePoints: number = 5; + const navigate = useNavigate(); const { isShow, openModal, closeModal, Modal } = useModal(); + const [showDropdown, setShowDropdown] = useState(false); const { user } = useUserStore(); + const { isLoggedIn } = useAuth(); + + const profileRef = useRef(null); + + // DropdownMenu가 닫히지 않도록 ProfileWrapper를 제외 대상에 추가 + const dropdownRef = useOutsideClick(() => setShowDropdown(false), { + excludeRefs: [profileRef], + }); + + const handleProfileClick = () => { + if (isLoggedIn) { + setShowDropdown(prev => !prev); // 열림/닫힘 토글 + } else { + openModal(); // 로그인 모달 열기 + } + }; + return ( {user && ( @@ -27,14 +51,44 @@ export default function Header() { /> )} - - - - - {/* Modal 컴포넌트 */} - - - + + + + {isLoggedIn && showDropdown && ( + e.stopPropagation()}> + { + navigate('/profile'); + }} + > + 프로필 + + { + navigate('/setting'); + }} + > + 설정 + + + 로그아웃 + + + )} + + {!isLoggedIn && ( + + + + )} ); } diff --git a/src/common/layout/style.ts b/src/common/layout/style.ts index 9d2d49f..0f3fdc8 100644 --- a/src/common/layout/style.ts +++ b/src/common/layout/style.ts @@ -20,6 +20,7 @@ export const HeaderBox = styled.header` height: 42px; position: fixed; padding-right: 20px; + z-index: 10; `; export const LogoBoxWrapper = styled.div` diff --git a/src/common/ui/style.ts b/src/common/ui/style.ts index e46d168..d4f85ed 100644 --- a/src/common/ui/style.ts +++ b/src/common/ui/style.ts @@ -13,6 +13,11 @@ interface IconWrapperProps { $color: string; } +interface UserInfoButtonProps { + $backgroundColor: string; + $boxShadow: string; +} + export const SectionButton = styled.button` width: 100px; height: 75px; @@ -90,3 +95,39 @@ export const LogoImg = styled.img` width: 147px; height: 117px; `; + +export const DropdownMenu = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: absolute; + margin-top: 3px; + right: 0; + cursor: default; + width: 150px; + height: 180px; + background-color: #fff; + border-radius: 8px; + border: 2px solid #ffb53d; +`; + +export const UserInfoButton = styled.button` + width: 80%; + height: 30px; + margin-top: 12px; + border: none; + background-color: ${({ $backgroundColor }) => $backgroundColor}; + box-shadow: ${({ $boxShadow }) => $boxShadow}; + text-align: center; + font-size: 17px; + font-weight: 700; + color: #ffffff; + border-radius: 6px; + text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000; + &:hover { + transform: translateY(-2px); + transition: background-color 0.2s ease, transform 0.2s ease, color 0.2s ease, + box-shadow 0.2s ease; + } +`; diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts index 289f05f..881bad3 100644 --- a/src/features/login/service/handleLogin.ts +++ b/src/features/login/service/handleLogin.ts @@ -1,14 +1,23 @@ +import { setCookie } from '@utils/cookies'; + const BASE_URL = import.meta.env.VITE_BASE_URL; -// import { setCookie } from '@utils/cookies'; const handleLogin = (provider: 'google' | 'kakao' | 'github') => { const redirectUrl = `${BASE_URL}/auth/${provider}`; window.location.href = redirectUrl; - // 테스트용 JWT 생성 및 쿠키에 저장 - // const fakeJwt = 'test.jwt.token'; - // setCookie('jwt', fakeJwt, { path: '/', maxAge: 3600 }); // 1시간 유효 - // alert('JWT 생성 완료: ' + fakeJwt); + // 테스트용 accessToken 및 refreshToken 생성 및 쿠키에 저장 (실제 Service에 배포할 때는 주석 달거나 삭제) + const fakeAccessToken = 'test.access.token'; + const fakeRefreshToken = 'test.refresh.token'; + + setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 + setCookie('refreshToken', fakeRefreshToken, { + path: '/', + maxAge: 3600 * 24 * 30, + }); // 30일 유효 + + alert('AccessToken 생성 완료: ' + fakeAccessToken); + alert('RefreshToken 생성 완료: ' + fakeRefreshToken); }; export default handleLogin; diff --git a/src/features/login/service/handleLogout.ts b/src/features/login/service/handleLogout.ts index affa32f..f31be6e 100644 --- a/src/features/login/service/handleLogout.ts +++ b/src/features/login/service/handleLogout.ts @@ -1,7 +1,8 @@ import { removeCookie } from '@utils/cookies'; const handleLogout = () => { - removeCookie('jwt'); + removeCookie('accessToken'); + removeCookie('refreshToken'); window.location.reload(); // 로그아웃 후 새로고침 }; diff --git a/src/features/login/styles.ts b/src/features/login/styles.ts index 060f7f8..6a9fe32 100644 --- a/src/features/login/styles.ts +++ b/src/features/login/styles.ts @@ -1,5 +1,4 @@ import styled, { keyframes } from 'styled-components'; -import { getImageUrl } from './../../utils/getImageUrl'; interface SocialLoginLinkProps { $color: string; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 5cce3e8..e12aaf0 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -3,7 +3,7 @@ import { getCookie } from '@utils/cookies'; /** * @description * 이 커스텀 훅은 사용자의 로그인 상태를 판별합니다. - * JWT 토큰이 쿠키에 존재하는지를 확인하여 로그인 여부를 반환합니다. + * accessToken이 쿠키에 존재하는지를 확인하여 로그인 여부를 반환합니다. * * @returns * `isLoggedIn`: 사용자가 로그인 상태인지 여부 @@ -18,10 +18,10 @@ import { getCookie } from '@utils/cookies'; * } */ const useAuth = () => { - const jwt = getCookie('jwt'); + const accessToken = getCookie('accessToken'); return { - isLoggedIn: !!jwt, // JWT가 존재하면 true, 없으면 false + isLoggedIn: !!accessToken, // accessToken이 존재하면 true, 없으면 false }; }; diff --git a/src/hooks/useOutsideClick.ts b/src/hooks/useOutsideClick.ts index 501dc95..53d4266 100644 --- a/src/hooks/useOutsideClick.ts +++ b/src/hooks/useOutsideClick.ts @@ -1,4 +1,5 @@ import { useEffect, useRef } from 'react'; +import { usePreservedCallback } from '@modern-kit/react'; /** * @description @@ -6,22 +7,37 @@ import { useEffect, useRef } from 'react'; * 주로 모달 요소에서 외부 클릭을 감지하여 동작을 제어할 때 사용되며, useModal과 호환이 잘 되어 있습니다. * * @param callback 외부 클릭 시 호출할 콜백 함수 + * @param excludeRefs 외부 클릭 감지에서 제외할 요소들의 ref 배열 (optional) * * @example * const modalRef = useOutsideClick(() => { * console.log('외부 클릭'); - * }); + * }, { excludeRefs: [buttonRef, inputRef] }); * - * return
    모달 내용
    ; + * return ( + * <> + *
    모달 내용
    + * + * + * ); */ - -const useOutsideClick = (callback: () => void) => { +const useOutsideClick = ( + callback: () => void, + options?: { excludeRefs?: React.RefObject[] } +) => { const ref = useRef(null); + const preservedCallback = usePreservedCallback(callback); useEffect(() => { const onClickOutside = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) { - callback(); + const target = e.target as Node; + + const isExcluded = options?.excludeRefs?.some( + excludeRef => excludeRef.current && excludeRef.current.contains(target) + ); + + if (ref.current && !ref.current.contains(target) && !isExcluded) { + preservedCallback(); } }; @@ -30,7 +46,7 @@ const useOutsideClick = (callback: () => void) => { return () => { document.removeEventListener('mousedown', onClickOutside); }; - }, [callback]); + }, [preservedCallback, options?.excludeRefs]); return ref; }; From c14dc6af7e59a1666cbea317d00cf04c39d5d0c1 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 17:12:58 +0900 Subject: [PATCH 21/32] =?UTF-8?q?=E2=9C=A8=20Feature(#useDropdown=20hook?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- src/common/layout/Header.tsx | 22 +++++++-------- src/common/layout/style.ts | 3 +- src/common/ui/style.ts | 54 +++++++++++++++++++++++------------- src/hooks/useDropdown.ts | 27 ++++++++++++++++++ src/pages/learn/Learn.tsx | 20 ++++++------- 6 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 src/hooks/useDropdown.ts diff --git a/index.html b/index.html index cab5a2b..8ebebcd 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ href="https://cdn.jsdelivr.net/gh/fonts-archive/Maplestory/subsets/Maplestory-dynamic-subset.css" type="text/css" /> - + CokoEdu diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index 7173d14..17a05b4 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { getImageUrl } from '@utils/getImageUrl'; import { HeaderBox } from './style'; @@ -8,28 +8,30 @@ import Login from '@features/login/ui/Login'; import useModal from '@hooks/useModal'; import useUserStore from '@/store/useUserStore'; import useAuth from '@hooks/useAuth'; +import useDropdown from '@hooks/useDropdown'; import useOutsideClick from '@hooks/useOutsideClick'; import handleLogout from '@features/login/service/handleLogout'; export default function Header() { const points: number = 2999999999; const lifePoints: number = 5; + const navigate = useNavigate(); const { isShow, openModal, closeModal, Modal } = useModal(); - const [showDropdown, setShowDropdown] = useState(false); const { user } = useUserStore(); const { isLoggedIn } = useAuth(); + const { isOpen, toggleDropdown, closeDropdown } = useDropdown(); const profileRef = useRef(null); // DropdownMenu가 닫히지 않도록 ProfileWrapper를 제외 대상에 추가 - const dropdownRef = useOutsideClick(() => setShowDropdown(false), { + const dropdownRef = useOutsideClick(closeDropdown, { excludeRefs: [profileRef], }); const handleProfileClick = () => { if (isLoggedIn) { - setShowDropdown(prev => !prev); // 열림/닫힘 토글 + toggleDropdown(); // 열림/닫힘 상태 변경 } else { openModal(); // 로그인 모달 열기 } @@ -54,23 +56,21 @@ export default function Header() { - {isLoggedIn && showDropdown && ( + {isLoggedIn && isOpen && ( e.stopPropagation()}> + 유저이름 + 2024.11.19 { - navigate('/profile'); - }} + onClick={() => navigate('/profile')} > 프로필 { - navigate('/setting'); - }} + onClick={() => navigate('/setting')} > 설정 diff --git a/src/common/layout/style.ts b/src/common/layout/style.ts index 0f3fdc8..d15ae7f 100644 --- a/src/common/layout/style.ts +++ b/src/common/layout/style.ts @@ -20,7 +20,7 @@ export const HeaderBox = styled.header` height: 42px; position: fixed; padding-right: 20px; - z-index: 10; + z-index: 1; `; export const LogoBoxWrapper = styled.div` @@ -43,4 +43,5 @@ export const OverRay = styled.div` width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.2); + z-index: 100; `; diff --git a/src/common/ui/style.ts b/src/common/ui/style.ts index d4f85ed..3dbd673 100644 --- a/src/common/ui/style.ts +++ b/src/common/ui/style.ts @@ -1,24 +1,12 @@ -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import { Link } from 'react-router-dom'; -interface SectionButtonProps { - $backgroundImage: string; -} - -interface MenuButtonProps { - $activeStyle: boolean; -} - -interface IconWrapperProps { - $color: string; -} - interface UserInfoButtonProps { $backgroundColor: string; $boxShadow: string; } -export const SectionButton = styled.button` +export const SectionButton = styled.button<{ $backgroundImage: string }>` width: 100px; height: 75px; margin-top: 75px; @@ -36,7 +24,7 @@ export const MenuButtonWrapper = styled.nav` display: inline-block; `; -export const MenuButton = styled.button` +export const MenuButton = styled.button<{ $activeStyle: boolean }>` width: 193px; height: 42px; font-size: 15px; @@ -63,7 +51,7 @@ export const MenuIcon = styled.img` height: 26px; `; -export const IconWrapper = styled.div` +export const IconWrapper = styled.div<{ $color: string }>` display: flex; align-items: center; margin-right: 16px; @@ -96,6 +84,18 @@ export const LogoImg = styled.img` height: 117px; `; +// DropdownMenu 열릴 때 애니메이션 +const slideIn = keyframes` + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + export const DropdownMenu = styled.div` display: flex; flex-direction: column; @@ -105,11 +105,25 @@ export const DropdownMenu = styled.div` margin-top: 3px; right: 0; cursor: default; - width: 150px; - height: 180px; + width: 200px; + height: 200px; background-color: #fff; - border-radius: 8px; - border: 2px solid #ffb53d; + border-radius: 15px; + border: 3px solid #ffb53d; + animation: ${slideIn} 0.3s ease-out; +`; + +export const UserNameText = styled.p` + color: #000; + font-size: 18px; + text-align: center; + font-weight: 700; +`; + +export const UserJoinDate = styled.p` + font-weight: 300; + color: #cbcbcb; + font-size: 12px; `; export const UserInfoButton = styled.button` diff --git a/src/hooks/useDropdown.ts b/src/hooks/useDropdown.ts new file mode 100644 index 0000000..9973abf --- /dev/null +++ b/src/hooks/useDropdown.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +/** + * @description + * 이 훅은 드롭다운의 열림/닫힘 상태를 관리합니다. + * 드롭다운의 외부 클릭 이벤트가 필요한 경우 `useOutsideClick` 훅과 같이 사용할 수 있습니다. + * + * @example + * const { isOpen, toggleDropdown, closeDropdown } = useDropdown(); + * + * return ( + * <> + * + * {isOpen &&
    Dropdown Content
    } + * + * ); + */ +const useDropdown = () => { + const [isOpen, setIsOpen] = useState(false); + + const toggleDropdown = () => setIsOpen(prev => !prev); + const closeDropdown = () => setIsOpen(false); + + return { isOpen, toggleDropdown, closeDropdown }; +}; + +export default useDropdown; diff --git a/src/pages/learn/Learn.tsx b/src/pages/learn/Learn.tsx index 82e8064..5e5f485 100644 --- a/src/pages/learn/Learn.tsx +++ b/src/pages/learn/Learn.tsx @@ -1,4 +1,5 @@ -import { Wrapper, LeftSection, RightSection, Layout } from '../../style/style'; +import { useEffect } from 'react'; +import * as globalS from '@/style/style'; import { ScrollableContainer } from './style'; import { useScrollVisibility } from '@hooks/useScrollVisibility'; import MenuBar from '@common/layout/MenuBar'; @@ -12,7 +13,6 @@ import KeycapAdventureIntro from '@features/learn/ui/KeycapAdventureIntro'; import PartNavContainer from '@features/quiz/ui/PartNavContainer'; import usePreloadImages from '@hooks/usePreloadImages'; import useUserStore from '@store/useUserStore'; -import { useEffect } from 'react'; export default function Learn() { const { setUser } = useUserStore(); @@ -43,18 +43,18 @@ export default function Learn() { return ( <> - - + + - - + +
    - - - + + + - + ); } From 01aa72ac4f6d45964cb2384d220906ecfd55d821 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 17:12:58 +0900 Subject: [PATCH 22/32] =?UTF-8?q?=E2=9C=A8=20Feature(#67):=20useDropdown?= =?UTF-8?q?=20hook=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- src/common/layout/Header.tsx | 22 +++++++-------- src/common/layout/style.ts | 3 +- src/common/ui/style.ts | 54 +++++++++++++++++++++++------------- src/hooks/useDropdown.ts | 27 ++++++++++++++++++ src/pages/learn/Learn.tsx | 20 ++++++------- 6 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 src/hooks/useDropdown.ts diff --git a/index.html b/index.html index cab5a2b..8ebebcd 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ href="https://cdn.jsdelivr.net/gh/fonts-archive/Maplestory/subsets/Maplestory-dynamic-subset.css" type="text/css" /> - + CokoEdu diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index 7173d14..17a05b4 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { getImageUrl } from '@utils/getImageUrl'; import { HeaderBox } from './style'; @@ -8,28 +8,30 @@ import Login from '@features/login/ui/Login'; import useModal from '@hooks/useModal'; import useUserStore from '@/store/useUserStore'; import useAuth from '@hooks/useAuth'; +import useDropdown from '@hooks/useDropdown'; import useOutsideClick from '@hooks/useOutsideClick'; import handleLogout from '@features/login/service/handleLogout'; export default function Header() { const points: number = 2999999999; const lifePoints: number = 5; + const navigate = useNavigate(); const { isShow, openModal, closeModal, Modal } = useModal(); - const [showDropdown, setShowDropdown] = useState(false); const { user } = useUserStore(); const { isLoggedIn } = useAuth(); + const { isOpen, toggleDropdown, closeDropdown } = useDropdown(); const profileRef = useRef(null); // DropdownMenu가 닫히지 않도록 ProfileWrapper를 제외 대상에 추가 - const dropdownRef = useOutsideClick(() => setShowDropdown(false), { + const dropdownRef = useOutsideClick(closeDropdown, { excludeRefs: [profileRef], }); const handleProfileClick = () => { if (isLoggedIn) { - setShowDropdown(prev => !prev); // 열림/닫힘 토글 + toggleDropdown(); // 열림/닫힘 상태 변경 } else { openModal(); // 로그인 모달 열기 } @@ -54,23 +56,21 @@ export default function Header() { - {isLoggedIn && showDropdown && ( + {isLoggedIn && isOpen && ( e.stopPropagation()}> + 유저이름 + 2024.11.19 { - navigate('/profile'); - }} + onClick={() => navigate('/profile')} > 프로필 { - navigate('/setting'); - }} + onClick={() => navigate('/setting')} > 설정 diff --git a/src/common/layout/style.ts b/src/common/layout/style.ts index 0f3fdc8..d15ae7f 100644 --- a/src/common/layout/style.ts +++ b/src/common/layout/style.ts @@ -20,7 +20,7 @@ export const HeaderBox = styled.header` height: 42px; position: fixed; padding-right: 20px; - z-index: 10; + z-index: 1; `; export const LogoBoxWrapper = styled.div` @@ -43,4 +43,5 @@ export const OverRay = styled.div` width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.2); + z-index: 100; `; diff --git a/src/common/ui/style.ts b/src/common/ui/style.ts index d4f85ed..3dbd673 100644 --- a/src/common/ui/style.ts +++ b/src/common/ui/style.ts @@ -1,24 +1,12 @@ -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import { Link } from 'react-router-dom'; -interface SectionButtonProps { - $backgroundImage: string; -} - -interface MenuButtonProps { - $activeStyle: boolean; -} - -interface IconWrapperProps { - $color: string; -} - interface UserInfoButtonProps { $backgroundColor: string; $boxShadow: string; } -export const SectionButton = styled.button` +export const SectionButton = styled.button<{ $backgroundImage: string }>` width: 100px; height: 75px; margin-top: 75px; @@ -36,7 +24,7 @@ export const MenuButtonWrapper = styled.nav` display: inline-block; `; -export const MenuButton = styled.button` +export const MenuButton = styled.button<{ $activeStyle: boolean }>` width: 193px; height: 42px; font-size: 15px; @@ -63,7 +51,7 @@ export const MenuIcon = styled.img` height: 26px; `; -export const IconWrapper = styled.div` +export const IconWrapper = styled.div<{ $color: string }>` display: flex; align-items: center; margin-right: 16px; @@ -96,6 +84,18 @@ export const LogoImg = styled.img` height: 117px; `; +// DropdownMenu 열릴 때 애니메이션 +const slideIn = keyframes` + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + export const DropdownMenu = styled.div` display: flex; flex-direction: column; @@ -105,11 +105,25 @@ export const DropdownMenu = styled.div` margin-top: 3px; right: 0; cursor: default; - width: 150px; - height: 180px; + width: 200px; + height: 200px; background-color: #fff; - border-radius: 8px; - border: 2px solid #ffb53d; + border-radius: 15px; + border: 3px solid #ffb53d; + animation: ${slideIn} 0.3s ease-out; +`; + +export const UserNameText = styled.p` + color: #000; + font-size: 18px; + text-align: center; + font-weight: 700; +`; + +export const UserJoinDate = styled.p` + font-weight: 300; + color: #cbcbcb; + font-size: 12px; `; export const UserInfoButton = styled.button` diff --git a/src/hooks/useDropdown.ts b/src/hooks/useDropdown.ts new file mode 100644 index 0000000..9973abf --- /dev/null +++ b/src/hooks/useDropdown.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +/** + * @description + * 이 훅은 드롭다운의 열림/닫힘 상태를 관리합니다. + * 드롭다운의 외부 클릭 이벤트가 필요한 경우 `useOutsideClick` 훅과 같이 사용할 수 있습니다. + * + * @example + * const { isOpen, toggleDropdown, closeDropdown } = useDropdown(); + * + * return ( + * <> + * + * {isOpen &&
    Dropdown Content
    } + * + * ); + */ +const useDropdown = () => { + const [isOpen, setIsOpen] = useState(false); + + const toggleDropdown = () => setIsOpen(prev => !prev); + const closeDropdown = () => setIsOpen(false); + + return { isOpen, toggleDropdown, closeDropdown }; +}; + +export default useDropdown; diff --git a/src/pages/learn/Learn.tsx b/src/pages/learn/Learn.tsx index 82e8064..5e5f485 100644 --- a/src/pages/learn/Learn.tsx +++ b/src/pages/learn/Learn.tsx @@ -1,4 +1,5 @@ -import { Wrapper, LeftSection, RightSection, Layout } from '../../style/style'; +import { useEffect } from 'react'; +import * as globalS from '@/style/style'; import { ScrollableContainer } from './style'; import { useScrollVisibility } from '@hooks/useScrollVisibility'; import MenuBar from '@common/layout/MenuBar'; @@ -12,7 +13,6 @@ import KeycapAdventureIntro from '@features/learn/ui/KeycapAdventureIntro'; import PartNavContainer from '@features/quiz/ui/PartNavContainer'; import usePreloadImages from '@hooks/usePreloadImages'; import useUserStore from '@store/useUserStore'; -import { useEffect } from 'react'; export default function Learn() { const { setUser } = useUserStore(); @@ -43,18 +43,18 @@ export default function Learn() { return ( <> - - + + - - + +
    - - - + + + - + ); } From c591ff6de64e825e47fdb692763d8be9de5690bc Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 18:10:35 +0900 Subject: [PATCH 23/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#67):=20DropdownM?= =?UTF-8?q?enu=20->=20ProfileDropdownMenu=EB=A1=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/Header.tsx | 7 +++++-- src/common/ui/style.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index 17a05b4..c654b9a 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -57,7 +57,10 @@ export default function Header() { {isLoggedIn && isOpen && ( - e.stopPropagation()}> + e.stopPropagation()} + > 유저이름 2024.11.19 로그아웃 - + )} {!isLoggedIn && ( diff --git a/src/common/ui/style.ts b/src/common/ui/style.ts index 3dbd673..2707cf7 100644 --- a/src/common/ui/style.ts +++ b/src/common/ui/style.ts @@ -96,7 +96,7 @@ const slideIn = keyframes` } `; -export const DropdownMenu = styled.div` +export const ProfileDropdownMenu = styled.div` display: flex; flex-direction: column; justify-content: center; From 4f00d6e6c49e8ce9bdc99f0cbf63a31d00e67004 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 18:16:35 +0900 Subject: [PATCH 24/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20handleLogin?= =?UTF-8?q?=20fakeToken=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/login/service/handleLogin.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts index 881bad3..3ec829d 100644 --- a/src/features/login/service/handleLogin.ts +++ b/src/features/login/service/handleLogin.ts @@ -1,4 +1,4 @@ -import { setCookie } from '@utils/cookies'; +// import { setCookie } from '@utils/cookies'; const BASE_URL = import.meta.env.VITE_BASE_URL; @@ -7,17 +7,17 @@ const handleLogin = (provider: 'google' | 'kakao' | 'github') => { window.location.href = redirectUrl; // 테스트용 accessToken 및 refreshToken 생성 및 쿠키에 저장 (실제 Service에 배포할 때는 주석 달거나 삭제) - const fakeAccessToken = 'test.access.token'; - const fakeRefreshToken = 'test.refresh.token'; + // const fakeAccessToken = 'test.access.token'; + // const fakeRefreshToken = 'test.refresh.token'; - setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 - setCookie('refreshToken', fakeRefreshToken, { - path: '/', - maxAge: 3600 * 24 * 30, - }); // 30일 유효 + // setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 + // setCookie('refreshToken', fakeRefreshToken, { + // path: '/', + // maxAge: 3600 * 24 * 30, + // }); // 30일 유효 - alert('AccessToken 생성 완료: ' + fakeAccessToken); - alert('RefreshToken 생성 완료: ' + fakeRefreshToken); + // alert('AccessToken 생성 완료: ' + fakeAccessToken); + // alert('RefreshToken 생성 완료: ' + fakeRefreshToken); }; export default handleLogin; From 3f5865e509dccf2887ba2bdd49101bee2709b93b Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 18:57:01 +0900 Subject: [PATCH 25/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20useAuth=20-?= =?UTF-8?q?>=20isLoggedIn=EC=9C=BC=EB=A1=9C=20=ED=9B=85=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/Header.tsx | 13 ++++++------- src/{hooks/useAuth.ts => utils/isLoggedIn.ts} | 17 ++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) rename src/{hooks/useAuth.ts => utils/isLoggedIn.ts} (52%) diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index c654b9a..d89cd1a 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -5,12 +5,12 @@ import { HeaderBox } from './style'; import * as S from '../ui/style'; import HeaderItem from '../ui/HeaderItem'; import Login from '@features/login/ui/Login'; +import handleLogout from '@features/login/service/handleLogout'; +import isLoggedIn from '@utils/isLoggedIn'; import useModal from '@hooks/useModal'; -import useUserStore from '@/store/useUserStore'; -import useAuth from '@hooks/useAuth'; +import useUserStore from '@store/useUserStore'; import useDropdown from '@hooks/useDropdown'; import useOutsideClick from '@hooks/useOutsideClick'; -import handleLogout from '@features/login/service/handleLogout'; export default function Header() { const points: number = 2999999999; @@ -19,7 +19,6 @@ export default function Header() { const navigate = useNavigate(); const { isShow, openModal, closeModal, Modal } = useModal(); const { user } = useUserStore(); - const { isLoggedIn } = useAuth(); const { isOpen, toggleDropdown, closeDropdown } = useDropdown(); const profileRef = useRef(null); @@ -30,7 +29,7 @@ export default function Header() { }); const handleProfileClick = () => { - if (isLoggedIn) { + if (isLoggedIn()) { toggleDropdown(); // 열림/닫힘 상태 변경 } else { openModal(); // 로그인 모달 열기 @@ -56,7 +55,7 @@ export default function Header() { - {isLoggedIn && isOpen && ( + {isLoggedIn() && isOpen && ( e.stopPropagation()} @@ -87,7 +86,7 @@ export default function Header() { )} - {!isLoggedIn && ( + {!isLoggedIn() && ( diff --git a/src/hooks/useAuth.ts b/src/utils/isLoggedIn.ts similarity index 52% rename from src/hooks/useAuth.ts rename to src/utils/isLoggedIn.ts index e12aaf0..f98ddb3 100644 --- a/src/hooks/useAuth.ts +++ b/src/utils/isLoggedIn.ts @@ -2,27 +2,22 @@ import { getCookie } from '@utils/cookies'; /** * @description - * 이 커스텀 훅은 사용자의 로그인 상태를 판별합니다. + * 사용자의 로그인 상태를 판별합니다. * accessToken이 쿠키에 존재하는지를 확인하여 로그인 여부를 반환합니다. * * @returns - * `isLoggedIn`: 사용자가 로그인 상태인지 여부 + * `boolean`: 사용자가 로그인 상태인지 여부 * * @example - * const { isLoggedIn } = useAuth(); - * - * if (isLoggedIn) { + * if (isLoggedIn()) { * console.log("사용자가 로그인 상태입니다."); * } else { * console.log("로그인이 필요합니다."); * } */ -const useAuth = () => { +const isLoggedIn = (): boolean => { const accessToken = getCookie('accessToken'); - - return { - isLoggedIn: !!accessToken, // accessToken이 존재하면 true, 없으면 false - }; + return !!accessToken; // accessToken이 존재하면 true, 없으면 false }; -export default useAuth; +export default isLoggedIn; From 36d9fb40fb2b38b697ace1d39eeccc7f659143b0 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 19:12:53 +0900 Subject: [PATCH 26/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#67):=20intercept?= =?UTF-8?q?or=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=98=EC=86=94=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EC=98=A4=ED=83=88=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/axios/{intercepter.ts => interceptor.ts} | 1 - 1 file changed, 1 deletion(-) rename src/apis/axios/{intercepter.ts => interceptor.ts} (96%) diff --git a/src/apis/axios/intercepter.ts b/src/apis/axios/interceptor.ts similarity index 96% rename from src/apis/axios/intercepter.ts rename to src/apis/axios/interceptor.ts index b8c7413..0083df7 100644 --- a/src/apis/axios/intercepter.ts +++ b/src/apis/axios/interceptor.ts @@ -21,7 +21,6 @@ api.interceptors.response.use( }, error => { // HTTP 상태 코드가 에러인 경우 - console.log(error); return Promise.reject(error); } ); From 5bd9bbd041c67662f1736f5e436877feb813d2e2 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Wed, 11 Dec 2024 12:27:55 +0900 Subject: [PATCH 27/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20usePopover?= =?UTF-8?q?=20hook=20=EC=BD=9C=EB=B0=B1=20memoization=20=EB=B0=8F=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=ED=81=B4=EB=A6=AD=20=EA=B0=90=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/Header.tsx | 31 +++++++---------- src/common/ui/style.ts | 19 +++++------ src/features/login/service/handleLogin.ts | 20 +++++------ src/hooks/useDropdown.ts | 27 --------------- src/hooks/usePopover.ts | 41 +++++++++++++++++++++++ 5 files changed, 72 insertions(+), 66 deletions(-) delete mode 100644 src/hooks/useDropdown.ts create mode 100644 src/hooks/usePopover.ts diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index d89cd1a..ebb230e 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -9,8 +9,7 @@ import handleLogout from '@features/login/service/handleLogout'; import isLoggedIn from '@utils/isLoggedIn'; import useModal from '@hooks/useModal'; import useUserStore from '@store/useUserStore'; -import useDropdown from '@hooks/useDropdown'; -import useOutsideClick from '@hooks/useOutsideClick'; +import usePopover from '@hooks/usePopover'; export default function Header() { const points: number = 2999999999; @@ -20,17 +19,14 @@ export default function Header() { const { isShow, openModal, closeModal, Modal } = useModal(); const { user } = useUserStore(); - const { isOpen, toggleDropdown, closeDropdown } = useDropdown(); const profileRef = useRef(null); - - // DropdownMenu가 닫히지 않도록 ProfileWrapper를 제외 대상에 추가 - const dropdownRef = useOutsideClick(closeDropdown, { - excludeRefs: [profileRef], + const { isOpen, togglePopover, popoverRef } = usePopover({ + excludeRefs: [profileRef], // ProfileWrapper를 제외 목록에 추가 }); const handleProfileClick = () => { if (isLoggedIn()) { - toggleDropdown(); // 열림/닫힘 상태 변경 + togglePopover(); // 팝오버 열기/닫기 } else { openModal(); // 로그인 모달 열기 } @@ -56,34 +52,31 @@ export default function Header() { {isLoggedIn() && isOpen && ( - e.stopPropagation()} - > + e.stopPropagation()}> 유저이름 2024.11.19 navigate('/profile')} > 프로필 navigate('/setting')} > 설정 로그아웃 - + )} {!isLoggedIn() && ( diff --git a/src/common/ui/style.ts b/src/common/ui/style.ts index 2707cf7..73aa86e 100644 --- a/src/common/ui/style.ts +++ b/src/common/ui/style.ts @@ -62,11 +62,6 @@ export const IconWrapper = styled.div<{ $color: string }>` color: ${({ $color }) => $color}; `; -export const ProfileWrapper = styled.div` - position: relative; - cursor: pointer; -`; - export const ProfileIcon = styled.img` position: absolute; width: 30px; @@ -84,7 +79,12 @@ export const LogoImg = styled.img` height: 117px; `; -// DropdownMenu 열릴 때 애니메이션 +export const ProfileWrapper = styled.div` + position: relative; + cursor: pointer; +`; + +// Popover 열릴 때 애니메이션 const slideIn = keyframes` from { opacity: 0; @@ -96,7 +96,7 @@ const slideIn = keyframes` } `; -export const ProfileDropdownMenu = styled.div` +export const ProfilePopover = styled.div` display: flex; flex-direction: column; justify-content: center; @@ -140,8 +140,7 @@ export const UserInfoButton = styled.button` border-radius: 6px; text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000; &:hover { - transform: translateY(-2px); - transition: background-color 0.2s ease, transform 0.2s ease, color 0.2s ease, - box-shadow 0.2s ease; + transform: scale(1.05); + transition: transform 0.2s ease; } `; diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts index 3ec829d..881bad3 100644 --- a/src/features/login/service/handleLogin.ts +++ b/src/features/login/service/handleLogin.ts @@ -1,4 +1,4 @@ -// import { setCookie } from '@utils/cookies'; +import { setCookie } from '@utils/cookies'; const BASE_URL = import.meta.env.VITE_BASE_URL; @@ -7,17 +7,17 @@ const handleLogin = (provider: 'google' | 'kakao' | 'github') => { window.location.href = redirectUrl; // 테스트용 accessToken 및 refreshToken 생성 및 쿠키에 저장 (실제 Service에 배포할 때는 주석 달거나 삭제) - // const fakeAccessToken = 'test.access.token'; - // const fakeRefreshToken = 'test.refresh.token'; + const fakeAccessToken = 'test.access.token'; + const fakeRefreshToken = 'test.refresh.token'; - // setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 - // setCookie('refreshToken', fakeRefreshToken, { - // path: '/', - // maxAge: 3600 * 24 * 30, - // }); // 30일 유효 + setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 + setCookie('refreshToken', fakeRefreshToken, { + path: '/', + maxAge: 3600 * 24 * 30, + }); // 30일 유효 - // alert('AccessToken 생성 완료: ' + fakeAccessToken); - // alert('RefreshToken 생성 완료: ' + fakeRefreshToken); + alert('AccessToken 생성 완료: ' + fakeAccessToken); + alert('RefreshToken 생성 완료: ' + fakeRefreshToken); }; export default handleLogin; diff --git a/src/hooks/useDropdown.ts b/src/hooks/useDropdown.ts deleted file mode 100644 index 9973abf..0000000 --- a/src/hooks/useDropdown.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useState } from 'react'; - -/** - * @description - * 이 훅은 드롭다운의 열림/닫힘 상태를 관리합니다. - * 드롭다운의 외부 클릭 이벤트가 필요한 경우 `useOutsideClick` 훅과 같이 사용할 수 있습니다. - * - * @example - * const { isOpen, toggleDropdown, closeDropdown } = useDropdown(); - * - * return ( - * <> - * - * {isOpen &&
    Dropdown Content
    } - * - * ); - */ -const useDropdown = () => { - const [isOpen, setIsOpen] = useState(false); - - const toggleDropdown = () => setIsOpen(prev => !prev); - const closeDropdown = () => setIsOpen(false); - - return { isOpen, toggleDropdown, closeDropdown }; -}; - -export default useDropdown; diff --git a/src/hooks/usePopover.ts b/src/hooks/usePopover.ts new file mode 100644 index 0000000..84ec6e0 --- /dev/null +++ b/src/hooks/usePopover.ts @@ -0,0 +1,41 @@ +import { useState, useCallback } from 'react'; +import useOutsideClick from '@hooks/useOutsideClick'; + +/** + * @description + * 이 훅은 팝오버의 상태를 관리하고 외부 클릭을 감지하여 팝오버를 닫는 동작을 간단하게 처리할 수 있도록 도와줍니다. + * + * @param excludeRefs 외부 클릭 감지에서 제외할 요소들의 ref 배열 (optional) + * + * @example + * const profileRef = useRef(null); + * const { isOpen, togglePopover, popoverRef } = usePopover({ + * excludeRefs: [profileRef] + * }); + * + * return ( + *
    + * + * {isOpen && ( + *
    + *

    유저이름

    + *
    + * )} + *
    + * ); + */ +const usePopover = (options?: { + excludeRefs?: React.RefObject[]; +}) => { + const [isOpen, setIsOpen] = useState(false); + + const togglePopover = useCallback(() => setIsOpen(prev => !prev), []); + + const popoverRef = useOutsideClick(() => { + if (isOpen) setIsOpen(false); + }, options); + + return { isOpen, togglePopover, popoverRef }; +}; + +export default usePopover; From 53d65a84ca4d0fba7e5512a122695deeaa2346e3 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Thu, 12 Dec 2024 01:18:13 +0900 Subject: [PATCH 28/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20usePopover?= =?UTF-8?q?=20hook=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C?= =?UTF-8?q?=20=ED=98=B8=EC=B6=9C=EB=90=98=EB=8A=94=20callback=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=98=B5=EC=85=94=EB=84=90=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20Logout=20=EC=8B=9C=20root=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/Header.tsx | 2 +- src/features/login/service/handleLogout.ts | 2 +- src/hooks/useIsFirstMount.ts | 18 ++++++ src/hooks/usePopover.ts | 73 ++++++++++++++++------ 4 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useIsFirstMount.ts diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index ebb230e..c830b6c 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -21,7 +21,7 @@ export default function Header() { const profileRef = useRef(null); const { isOpen, togglePopover, popoverRef } = usePopover({ - excludeRefs: [profileRef], // ProfileWrapper를 제외 목록에 추가 + excludeRefs: [profileRef], }); const handleProfileClick = () => { diff --git a/src/features/login/service/handleLogout.ts b/src/features/login/service/handleLogout.ts index f31be6e..6860afb 100644 --- a/src/features/login/service/handleLogout.ts +++ b/src/features/login/service/handleLogout.ts @@ -3,7 +3,7 @@ import { removeCookie } from '@utils/cookies'; const handleLogout = () => { removeCookie('accessToken'); removeCookie('refreshToken'); - window.location.reload(); // 로그아웃 후 새로고침 + window.location.href = '/'; }; export default handleLogout; diff --git a/src/hooks/useIsFirstMount.ts b/src/hooks/useIsFirstMount.ts new file mode 100644 index 0000000..211e592 --- /dev/null +++ b/src/hooks/useIsFirstMount.ts @@ -0,0 +1,18 @@ +import { useRef, useEffect } from 'react'; + +/** + * @description 해당 컴포넌트가 초기 렌더링인지 체크해주는 훅 + * + * @returns {boolean} 초기 렌더링 여부 + */ +const useIsFirstMount = (): boolean => { + const isFirstRender = useRef(true); + + useEffect(() => { + isFirstRender.current = false; // 첫 번째 렌더링 이후 false로 설정 + }, []); + + return isFirstRender.current; +}; + +export default useIsFirstMount; diff --git a/src/hooks/usePopover.ts b/src/hooks/usePopover.ts index 84ec6e0..1810c9f 100644 --- a/src/hooks/usePopover.ts +++ b/src/hooks/usePopover.ts @@ -1,41 +1,76 @@ -import { useState, useCallback } from 'react'; +import { useState, useEffect, useCallback } from 'react'; +import { usePreservedCallback } from '@modern-kit/react'; import useOutsideClick from '@hooks/useOutsideClick'; +import useIsFirstMount from '@hooks/useIsFirstMount'; /** * @description - * 이 훅은 팝오버의 상태를 관리하고 외부 클릭을 감지하여 팝오버를 닫는 동작을 간단하게 처리할 수 있도록 도와줍니다. + * 이 훅은 팝오버 상태를 관리하고 외부 클릭을 감지하여 팝오버를 닫는 동작을 처리합니다. * - * @param excludeRefs 외부 클릭 감지에서 제외할 요소들의 ref 배열 (optional) + * @param {Object} options - 옵션 객체 (optional) + * @param {Function} options.callback - 팝오버 상태가 변경될 때 호출되는 콜백 함수 + * 콜백은 현재 상태 (isOpen: boolean)를 매개변수로 받음 + * @param {React.RefObject[]} options.excludeRefs - 외부 클릭 감지에서 제외할 요소들의 ref 배열 + * + * @returns - 팝오버 상태 관리와 관련된 메서드와 ref를 반환 + * @returns {boolean} `isOpen` - 팝오버의 현재 열림 상태 + * @returns {Function} `togglePopover` - 팝오버 열림/닫힘 상태를 토글하는 함수 + * @returns {Function} `openPopover` - 팝오버 여는 함수 + * @returns {Function} `closePopover` - 팝오버 닫는 함수 + * @returns {React.RefObject} `popoverRef` - 팝오버 요소에 연결할 ref 객체 * * @example - * const profileRef = useRef(null); - * const { isOpen, togglePopover, popoverRef } = usePopover({ - * excludeRefs: [profileRef] - * }); + * import { useRef } from 'react'; + * import usePopover from '@hooks/usePopover'; + * + * function ProfileComponent() { + * const profileRef = useRef(null); + * const { isOpen, togglePopover, openPopover, closePopover, popoverRef } = usePopover({ + * excludeRefs: [profileRef], + * callback: (isOpen) => console.log('Popover state changed:', isOpen), + * }); * - * return ( - *
    - * - * {isOpen && ( - *
    - *

    유저이름

    - *
    - * )} - *
    - * ); + * return ( + *
    + * + * {isOpen && ( + *
    + *

    유저이름

    + *
    + * )} + *
    + * ); + * }; */ const usePopover = (options?: { + callback?: (isOpen?: boolean) => void; excludeRefs?: React.RefObject[]; }) => { const [isOpen, setIsOpen] = useState(false); + const isFirst = useIsFirstMount(); + + const preservedCallback = usePreservedCallback((isOpen: boolean) => { + if (options?.callback) { + options?.callback(isOpen); + } + }); const togglePopover = useCallback(() => setIsOpen(prev => !prev), []); + const openPopover = useCallback(() => setIsOpen(true), []); + const closePopover = useCallback(() => setIsOpen(false), []); + + useEffect(() => { + if (isFirst) { + return; + } + preservedCallback(isOpen); + }, [isOpen]); const popoverRef = useOutsideClick(() => { - if (isOpen) setIsOpen(false); + if (isOpen) togglePopover(); }, options); - return { isOpen, togglePopover, popoverRef }; + return { isOpen, togglePopover, openPopover, closePopover, popoverRef }; }; export default usePopover; From d37014ca3fa3e84c64aa5f1219375dbae23148de Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Thu, 12 Dec 2024 01:30:03 +0900 Subject: [PATCH 29/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#67):=20isFirstRe?= =?UTF-8?q?nder=20->=20isFirstMount=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useIsFirstMount.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/useIsFirstMount.ts b/src/hooks/useIsFirstMount.ts index 211e592..e220d54 100644 --- a/src/hooks/useIsFirstMount.ts +++ b/src/hooks/useIsFirstMount.ts @@ -1,18 +1,18 @@ import { useRef, useEffect } from 'react'; /** - * @description 해당 컴포넌트가 초기 렌더링인지 체크해주는 훅 + * @description 해당 컴포넌트가 초기 마운트인지 체크해주는 훅 * - * @returns {boolean} 초기 렌더링 여부 + * @returns {boolean} 초기 마운트 여부 */ const useIsFirstMount = (): boolean => { - const isFirstRender = useRef(true); + const isFirstMount = useRef(true); useEffect(() => { - isFirstRender.current = false; // 첫 번째 렌더링 이후 false로 설정 + isFirstMount.current = false; // 초기 마운트 이후 false로 설정 }, []); - return isFirstRender.current; + return isFirstMount.current; }; export default useIsFirstMount; From 45d71c68b369b75f4d0e832c0f26950b52538494 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Thu, 12 Dec 2024 01:33:26 +0900 Subject: [PATCH 30/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20handleLogin?= =?UTF-8?q?=20fake=20Token=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/login/service/handleLogin.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts index 881bad3..3ec829d 100644 --- a/src/features/login/service/handleLogin.ts +++ b/src/features/login/service/handleLogin.ts @@ -1,4 +1,4 @@ -import { setCookie } from '@utils/cookies'; +// import { setCookie } from '@utils/cookies'; const BASE_URL = import.meta.env.VITE_BASE_URL; @@ -7,17 +7,17 @@ const handleLogin = (provider: 'google' | 'kakao' | 'github') => { window.location.href = redirectUrl; // 테스트용 accessToken 및 refreshToken 생성 및 쿠키에 저장 (실제 Service에 배포할 때는 주석 달거나 삭제) - const fakeAccessToken = 'test.access.token'; - const fakeRefreshToken = 'test.refresh.token'; + // const fakeAccessToken = 'test.access.token'; + // const fakeRefreshToken = 'test.refresh.token'; - setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 - setCookie('refreshToken', fakeRefreshToken, { - path: '/', - maxAge: 3600 * 24 * 30, - }); // 30일 유효 + // setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 + // setCookie('refreshToken', fakeRefreshToken, { + // path: '/', + // maxAge: 3600 * 24 * 30, + // }); // 30일 유효 - alert('AccessToken 생성 완료: ' + fakeAccessToken); - alert('RefreshToken 생성 완료: ' + fakeRefreshToken); + // alert('AccessToken 생성 완료: ' + fakeAccessToken); + // alert('RefreshToken 생성 완료: ' + fakeRefreshToken); }; export default handleLogin; From 317b7b552460029f36059319819e44eb2b8eba59 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Thu, 12 Dec 2024 20:34:36 +0900 Subject: [PATCH 31/32] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#67):=20useIsFirs?= =?UTF-8?q?tMount=20->=20useIsMounted=20hook=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/login/service/handleLogin.ts | 20 ++++++++++---------- src/hooks/useIsFirstMount.ts | 18 ------------------ src/hooks/usePopover.ts | 7 +++---- 3 files changed, 13 insertions(+), 32 deletions(-) delete mode 100644 src/hooks/useIsFirstMount.ts diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts index 3ec829d..881bad3 100644 --- a/src/features/login/service/handleLogin.ts +++ b/src/features/login/service/handleLogin.ts @@ -1,4 +1,4 @@ -// import { setCookie } from '@utils/cookies'; +import { setCookie } from '@utils/cookies'; const BASE_URL = import.meta.env.VITE_BASE_URL; @@ -7,17 +7,17 @@ const handleLogin = (provider: 'google' | 'kakao' | 'github') => { window.location.href = redirectUrl; // 테스트용 accessToken 및 refreshToken 생성 및 쿠키에 저장 (실제 Service에 배포할 때는 주석 달거나 삭제) - // const fakeAccessToken = 'test.access.token'; - // const fakeRefreshToken = 'test.refresh.token'; + const fakeAccessToken = 'test.access.token'; + const fakeRefreshToken = 'test.refresh.token'; - // setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 - // setCookie('refreshToken', fakeRefreshToken, { - // path: '/', - // maxAge: 3600 * 24 * 30, - // }); // 30일 유효 + setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 + setCookie('refreshToken', fakeRefreshToken, { + path: '/', + maxAge: 3600 * 24 * 30, + }); // 30일 유효 - // alert('AccessToken 생성 완료: ' + fakeAccessToken); - // alert('RefreshToken 생성 완료: ' + fakeRefreshToken); + alert('AccessToken 생성 완료: ' + fakeAccessToken); + alert('RefreshToken 생성 완료: ' + fakeRefreshToken); }; export default handleLogin; diff --git a/src/hooks/useIsFirstMount.ts b/src/hooks/useIsFirstMount.ts deleted file mode 100644 index e220d54..0000000 --- a/src/hooks/useIsFirstMount.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useRef, useEffect } from 'react'; - -/** - * @description 해당 컴포넌트가 초기 마운트인지 체크해주는 훅 - * - * @returns {boolean} 초기 마운트 여부 - */ -const useIsFirstMount = (): boolean => { - const isFirstMount = useRef(true); - - useEffect(() => { - isFirstMount.current = false; // 초기 마운트 이후 false로 설정 - }, []); - - return isFirstMount.current; -}; - -export default useIsFirstMount; diff --git a/src/hooks/usePopover.ts b/src/hooks/usePopover.ts index 1810c9f..bcb9a31 100644 --- a/src/hooks/usePopover.ts +++ b/src/hooks/usePopover.ts @@ -1,7 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; -import { usePreservedCallback } from '@modern-kit/react'; +import { usePreservedCallback, useIsMounted } from '@modern-kit/react'; import useOutsideClick from '@hooks/useOutsideClick'; -import useIsFirstMount from '@hooks/useIsFirstMount'; /** * @description @@ -47,7 +46,7 @@ const usePopover = (options?: { excludeRefs?: React.RefObject[]; }) => { const [isOpen, setIsOpen] = useState(false); - const isFirst = useIsFirstMount(); + const isMounted = useIsMounted(); const preservedCallback = usePreservedCallback((isOpen: boolean) => { if (options?.callback) { @@ -60,7 +59,7 @@ const usePopover = (options?: { const closePopover = useCallback(() => setIsOpen(false), []); useEffect(() => { - if (isFirst) { + if (isMounted) { return; } preservedCallback(isOpen); From ad4d3715d8edd26b2a12fcada7a715549b0af0dc Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Thu, 12 Dec 2024 20:36:31 +0900 Subject: [PATCH 32/32] =?UTF-8?q?=F0=9F=93=9D=20Modify(#67):=20handleLogin?= =?UTF-8?q?=20fake=20Token=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/login/service/handleLogin.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/features/login/service/handleLogin.ts b/src/features/login/service/handleLogin.ts index 881bad3..3ec829d 100644 --- a/src/features/login/service/handleLogin.ts +++ b/src/features/login/service/handleLogin.ts @@ -1,4 +1,4 @@ -import { setCookie } from '@utils/cookies'; +// import { setCookie } from '@utils/cookies'; const BASE_URL = import.meta.env.VITE_BASE_URL; @@ -7,17 +7,17 @@ const handleLogin = (provider: 'google' | 'kakao' | 'github') => { window.location.href = redirectUrl; // 테스트용 accessToken 및 refreshToken 생성 및 쿠키에 저장 (실제 Service에 배포할 때는 주석 달거나 삭제) - const fakeAccessToken = 'test.access.token'; - const fakeRefreshToken = 'test.refresh.token'; + // const fakeAccessToken = 'test.access.token'; + // const fakeRefreshToken = 'test.refresh.token'; - setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 - setCookie('refreshToken', fakeRefreshToken, { - path: '/', - maxAge: 3600 * 24 * 30, - }); // 30일 유효 + // setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효 + // setCookie('refreshToken', fakeRefreshToken, { + // path: '/', + // maxAge: 3600 * 24 * 30, + // }); // 30일 유효 - alert('AccessToken 생성 완료: ' + fakeAccessToken); - alert('RefreshToken 생성 완료: ' + fakeRefreshToken); + // alert('AccessToken 생성 완료: ' + fakeAccessToken); + // alert('RefreshToken 생성 완료: ' + fakeRefreshToken); }; export default handleLogin;