From 5eb91fcf3a977ac6dde00af2c3c20d38e5f232a3 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Wed, 4 Dec 2024 10:38:32 +0900 Subject: [PATCH 01/19] =?UTF-8?q?=E2=9C=A8=20Feature(#67):=20useMemo=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B0=98=EB=B3=B5=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EB=B0=A9=EC=A7=80=20=EB=B0=8F=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/quiz/ui/PartNavContainer.tsx | 90 ++++++++++++----------- src/pages/learn/Learn.tsx | 4 + 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/features/quiz/ui/PartNavContainer.tsx b/src/features/quiz/ui/PartNavContainer.tsx index d04b25a..2d7654e 100644 --- a/src/features/quiz/ui/PartNavContainer.tsx +++ b/src/features/quiz/ui/PartNavContainer.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { UpperBackgroundImg, @@ -61,51 +61,59 @@ const dummyData: Section[] = [ ]; export default function PartNavContainer() { - const [sections, setSections] = useState(dummyData); const navigate = useNavigate(); - return ( - <> - - - {sections.map((section, sectionIndex) => { - // 각 섹션 앞의 버튼 수 합산 - const previousPartsCount = sections - .slice(0, sectionIndex) - .reduce( - (sum, currentSection) => sum + currentSection.part.length, - 0 - ); + // 모든 섹션의 이전 버튼 수 누적 계산 + const previousPartsCounts = useMemo(() => { + const counts: number[] = []; // 누적 버튼 수를 저장할 배열 + let sum = 0; // 누적합을 저장할 변수 + + dummyData.forEach(section => { + counts.push(sum); // 현재까지의 누적합을 counts에 추가 + sum += section.part.length; // 현재 섹션의 버튼 개수를 누적합에 더함 + }); + + return counts; + }, []); - return ( - - {section.name} - - {section.part.map((part, partIndex) => { - // 전역 인덱스 계산 - const globalIndex = previousPartsCount + partIndex; + // 섹션 및 파트를 캐싱하여 렌더링 + const memoItem = useMemo(() => { + return dummyData.map((section, sectionIndex) => { + const previousPartsCount = previousPartsCounts[sectionIndex]; - const { gridColumn, gridRow } = - getPartGridPosition(globalIndex); - const buttonImage = getImageUrl( - `키캡${(globalIndex % 4) + 1}.svg` - ); + return ( + + {section.name} + + {section.part.map((part, partIndex) => { + const globalIndex = previousPartsCount + partIndex; + const { gridColumn, gridRow } = getPartGridPosition(globalIndex); + const buttonImage = getImageUrl( + `키캡${(globalIndex % 4) + 1}.svg` + ); - return ( - navigate('/quiz', { state: part })} - > - {`키캡 - - ); - })} - - - ); - })} - + return ( + + navigate('/quiz', { state: { partId: part.id } }) + } + > + {`키캡 + + ); + })} + + + ); + }); + }, [previousPartsCounts]); + + return ( + <> + + {memoItem} ); } diff --git a/src/pages/learn/Learn.tsx b/src/pages/learn/Learn.tsx index 5aed2e9..4bb0561 100644 --- a/src/pages/learn/Learn.tsx +++ b/src/pages/learn/Learn.tsx @@ -22,6 +22,10 @@ export default function Learn() { '코코-멘트3.svg', '코코-멘트4.svg', '코코-멘트5.svg', + '키캡1.svg', + '키캡2.svg', + '키캡3.svg', + '키캡4.svg', ], }); From 2af81393038bbd5100e1fa70d0921081b2846520 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Fri, 6 Dec 2024 03:43:42 +0900 Subject: [PATCH 02/19] =?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 03/19] =?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 04/19] =?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 05/19] =?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 06/19] =?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 09e964029d1115229ce5b0601f0f2d61aada8ce8 Mon Sep 17 00:00:00 2001 From: bluetree7878 Date: Mon, 9 Dec 2024 06:42:52 +0900 Subject: [PATCH 07/19] =?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 08/19] =?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 09/19] =?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 10/19] =?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 11/19] =?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 12/19] =?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 13/19] =?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 14/19] =?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 15/19] =?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 16/19] =?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 17/19] =?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 18/19] =?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 19/19] =?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;