From 5ba1b1dd6d10e5ad17004cc72e15eca3a8f25651 Mon Sep 17 00:00:00 2001 From: sunub Date: Thu, 9 Jan 2025 21:02:29 +0900 Subject: [PATCH 01/42] =?UTF-8?q?feat=20:=20=EC=A0=84=EC=B2=B4=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=EC=9D=84=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=20layout=20=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?app=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/layout/index.tsx | 1 + frontend/src/app/layout/ui/Layout.tsx | 40 ++++++++++ frontend/src/app/layout/ui/NavItem.tsx | 46 +++++++++++ frontend/src/app/layout/ui/RootHeader.tsx | 37 +++++++++ .../src/app/layout/ui/RootSidebar.module.css | 15 ++++ frontend/src/app/layout/ui/RootSidebar.tsx | 76 +++++++++++++++++++ 6 files changed, 215 insertions(+) create mode 100644 frontend/src/app/layout/index.tsx create mode 100644 frontend/src/app/layout/ui/Layout.tsx create mode 100644 frontend/src/app/layout/ui/NavItem.tsx create mode 100644 frontend/src/app/layout/ui/RootHeader.tsx create mode 100644 frontend/src/app/layout/ui/RootSidebar.module.css create mode 100644 frontend/src/app/layout/ui/RootSidebar.tsx diff --git a/frontend/src/app/layout/index.tsx b/frontend/src/app/layout/index.tsx new file mode 100644 index 0000000..d35e90e --- /dev/null +++ b/frontend/src/app/layout/index.tsx @@ -0,0 +1 @@ +export { Layout } from "./ui/Layout"; diff --git a/frontend/src/app/layout/ui/Layout.tsx b/frontend/src/app/layout/ui/Layout.tsx new file mode 100644 index 0000000..3670d54 --- /dev/null +++ b/frontend/src/app/layout/ui/Layout.tsx @@ -0,0 +1,40 @@ +import { useLayout } from "@/shared/hooks/useLayout"; +import { cn } from "@/shared/misc"; +import { RootHeader } from "./RootHeader"; +import { RootSideBar } from "./RootSidebar"; +import { Suspense } from "react"; +import { LoadingAnimation } from "@/shared/components/Loading"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { authQueries } from "@/shared/lib/auth/authQuery"; + +const layoutStyles = { + default: "max-w-[520px]", + wide: "max-w-[1200px]", +} as const; + +function Layout({ children }: { children: React.ReactNode }) { + const { layoutType } = useLayout(); + const { data: authData } = useSuspenseQuery({ + queryKey: authQueries.queryKey, + queryFn: authQueries.queryFn, + }); + + return ( +
+ }> + + + + {children} +
+ ); +} + +export { Layout }; diff --git a/frontend/src/app/layout/ui/NavItem.tsx b/frontend/src/app/layout/ui/NavItem.tsx new file mode 100644 index 0000000..4027a3c --- /dev/null +++ b/frontend/src/app/layout/ui/NavItem.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { createLink, LinkComponent, useLocation } from "@tanstack/react-router"; + +interface LinkComponentProps + extends React.AnchorHTMLAttributes { + icon: React.ReactNode; + label: string; +} + +const Link = React.forwardRef( + ({ icon, label, className, ...props }, ref) => { + const location = useLocation(); + const isChecked = location.pathname.includes(label.split(" ")[0]); + + return ( + + + + ); + }, +); + +const CreatedLinkComponent = createLink(Link); + +const NavItem: LinkComponent = (props) => { + return ( + + ); +}; + +export { NavItem }; diff --git a/frontend/src/app/layout/ui/RootHeader.tsx b/frontend/src/app/layout/ui/RootHeader.tsx new file mode 100644 index 0000000..ea1fe44 --- /dev/null +++ b/frontend/src/app/layout/ui/RootHeader.tsx @@ -0,0 +1,37 @@ +import { LogoIcon } from "@/shared/icons"; +import { Image } from "@/shared/components/Image"; +import waitingUserImage from "@assets/images/waiting-user.png"; +import { Link } from "@tanstack/react-router"; + +function UserInfo({ nickname }: { nickname: string }) { + return ( +
+
+ 대기 중인 사용자 이미지 +
+ {nickname} +
+ ); +} + +function RootHeader({ nickname }: { nickname: string }) { + return ( +
+ + +

Betting Duck

+ + {nickname ? : null} +
+ ); +} + +export { RootHeader }; diff --git a/frontend/src/app/layout/ui/RootSidebar.module.css b/frontend/src/app/layout/ui/RootSidebar.module.css new file mode 100644 index 0000000..7b6e25e --- /dev/null +++ b/frontend/src/app/layout/ui/RootSidebar.module.css @@ -0,0 +1,15 @@ +.navigator { + content: ""; + position: absolute; + + top: 0; + left: 0; + + width: 4px; + height: 46px; + + background: #6e29da; + transition: transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + + transform: translateY(calc(48px * calc(1.5 * var(--navigator-position)))); +} diff --git a/frontend/src/app/layout/ui/RootSidebar.tsx b/frontend/src/app/layout/ui/RootSidebar.tsx new file mode 100644 index 0000000..7945efb --- /dev/null +++ b/frontend/src/app/layout/ui/RootSidebar.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { + LoginIcon, + UserIcon, + CreateVoteIcon, + WaitingRoomIcon, +} from "@/shared/icons"; +import { NavItem } from "./NavItem"; +import styles from "./RootSidebar.module.css"; +import { useLocation } from "@tanstack/react-router"; +import { LogoutButton } from "@/pages/login-page/ui/components/Logout"; + +type NavItemType = { + icon: () => JSX.Element; + label: string; + href: string; + onClick?: () => void; +}; + +const navItems = { + top: [ + { icon: UserIcon, label: "my", href: "/my-page" }, + { icon: CreateVoteIcon, label: "create vote", href: "/create-vote" }, + { icon: WaitingRoomIcon, label: "betting", href: "/betting" }, + ], + login: [{ icon: LoginIcon, label: "login", href: "/login" }], +}; + +function changeNavigatorPosition(href: string) { + let nextPosition = 0; + if (href.includes("my-page") || href.includes("guest-login")) { + nextPosition = 0; + } else if (href.includes("create-vote")) { + nextPosition = 1; + } else if (href.includes("betting") || href.includes("vote")) { + nextPosition = 2.2; + } else if (href.includes("login")) { + nextPosition = 9.9; + } + + document.documentElement.style.setProperty( + "--navigator-position", + nextPosition.toString(), + ); +} + +function NavItems({ items }: { items: NavItemType[] }) { + return ( + + ); +} + +function RootSideBar({ isAuthenticated }: { isAuthenticated: boolean }) { + const location = useLocation(); + + React.useEffect(() => { + changeNavigatorPosition(location.href); + }, [location]); + + return ( + + ); +} + +export { RootSideBar }; From 39f5138ccd2e3aeb7eb7ea6063c8ea66c044aecf Mon Sep 17 00:00:00 2001 From: sunub Date: Thu, 9 Jan 2025 21:07:26 +0900 Subject: [PATCH 02/42] =?UTF-8?q?refactor=20:=20features=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EC=9E=88=EB=8D=98=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20pages=20=EC=BD=94=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=A5=EB=9D=BD=EC=97=90=20=EB=A7=9E=EA=B2=8C=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 --- .../betting-page-admin/EndPredictButton.tsx | 0 .../betting-page-admin/index.tsx | 0 .../betting-page-admin/model/api.ts | 0 .../betting-page-admin/model/types.ts | 0 .../betting-page/api/endBetroom.ts | 0 .../betting-page/api/getBettingRoomInfo.ts | 7 ++-- .../betting-page/api/getUserInfo.ts | 0 .../betting-page/hook/useBettingContext.ts | 0 .../hook/useBettingRoomConnection.ts | 0 .../betting-page/index.tsx | 0 .../betting-page/model/schema.ts | 0 .../betting-page/model/var.ts | 0 .../betting-page/provider/BettingProvider.tsx | 0 .../betting-page/ui/BettingContainer.tsx | 0 .../betting-page/ui/BettingFooter.tsx | 0 .../betting-page/ui/BettingHeader.tsx | 0 .../betting-page/ui/BettingInput/index.tsx | 0 .../betting-page/ui/TotalBettingDisplay.tsx | 0 .../betting-page/utils/placeBetting.ts | 0 .../betting-predict-result/index.tsx | 0 .../{features => pages}/chat/hook/useChat.tsx | 0 .../src/{features => pages}/chat/index.tsx | 6 ++-- .../chat/provider/ChatProvider.tsx | 0 .../chat/ui/ChatError/index.tsx | 0 .../chat/ui/ChatHeader/index.tsx | 0 .../chat/ui/ChatHeader/ui/ChatTitle.tsx | 0 .../chat/ui/ChatHeader/ui/PredictButton.tsx | 6 ++-- .../ui/ChatHeader/ui/PredictionStatus.tsx | 0 .../chat/ui/ChatInput/index.tsx | 0 .../chat/ui/ChatInput/ui/EmoticonButton.tsx | 0 .../chat/ui/ChatInput/ui/InputBar.tsx | 2 +- .../chat/ui/ChatInput/ui/VoteButton.tsx | 0 .../chat/ui/ChatInput/ui/style.module.css | 0 .../chat/ui/ChatMessages/index.tsx | 0 .../chat/ui/ChatMessages/ui/Message.tsx | 0 .../chat/ui/ChatMessages/ui/MessageList.tsx | 0 .../{features => pages}/create-vote/index.ts | 0 .../create-vote/model/api.ts | 0 .../create-vote/model/helpers/formatData.ts | 0 .../create-vote/model/store.ts | 0 .../create-vote/model/types.ts | 0 .../create-vote/model/useCaseInput.ts | 0 .../create-vote/model/useCoinInput.ts | 0 .../create-vote/model/useTimer.ts | 0 .../create-vote/model/useTitleInput.ts | 0 .../create-vote/model/useValidation.ts | 0 .../create-vote/ui/CreateVotePage.tsx | 0 .../create-vote/ui/components/CaseInputs.tsx | 2 +- .../create-vote/ui/components/CoinInput.tsx | 2 +- .../create-vote/ui/components/Timer.tsx | 2 +- .../create-vote/ui/components/TitleInput.tsx | 2 +- .../create-vote/ui/components/index.ts | 0 .../create-vote/ui/error/CreateVoteError.tsx | 0 .../{features => pages}/login-page/index.ts | 0 .../login-page/model/api.ts | 0 .../login-page/model/store.ts | 0 .../login-page/model/types.ts | 0 .../login-page/model/validation.ts | 0 .../login-page/ui/LoginPage.tsx | 0 .../ui/components/GuestLoginForm.tsx | 0 .../login-page/ui/components/LoginForm.tsx | 0 .../login-page/ui/components/Logout.tsx | 0 .../login-page/ui/components/RegisterForm.tsx | 4 +-- .../login-page/ui/components/TabButton.tsx | 0 .../login-page/ui/components/Warning.tsx | 0 .../login-page/ui/components/index.ts | 0 .../src/pages/my-page/api/purchaseDuck.ts | 12 +++++++ .../my-page/api/updateDuckCountInCache.ts | 26 +++++++++++++++ .../my-page/error/index.tsx | 0 .../src/{features => pages}/my-page/index.tsx | 0 .../src/pages/my-page/model/useDuckState.ts | 33 +++++++++++++++++++ .../my-page/ui/AnimatedDuckCount.tsx | 11 ++----- .../my-page/ui/FallingDuck.tsx | 0 .../{features => pages}/my-page/ui/Pond.tsx | 0 .../predict-detail/index.tsx | 0 .../predict-detail/model/api.ts | 0 .../predict-detail/model/schema.ts | 0 .../predict-detail/ui/AdminBettingResult.tsx | 0 .../predict-detail/ui/Bettingstatistics.tsx | 0 .../predict-detail/ui/GuestFooter.tsx | 0 .../predict-detail/ui/UserBettingResult.tsx | 0 .../predict-detail/ui/UserFooter.tsx | 0 .../waiting-room/error/AccessError.ts | 0 .../waiting-room/error/Forbidden.tsx | 0 .../waiting-room/error/Unauthorized.tsx | 0 .../hooks/use-waiting-context.tsx | 0 .../waiting-room/index.tsx | 2 +- .../provider/WaitingRoomProvider.tsx | 0 .../waiting-room/style.module.css | 0 .../ui/AdminFooter/CancleButton.tsx | 0 .../ui/AdminFooter/ParticipateButton.tsx | 0 .../ui/AdminFooter/StartVotingButton.tsx | 2 +- .../waiting-room/ui/AdminFooter/index.tsx | 0 .../waiting-room/ui/MemberFooter/index.tsx | 0 .../waiting-room/ui/ParticipantsList.tsx | 0 .../waiting-room/ui/SharedLinkCard.tsx | 0 .../waiting-room/ui/WaitingError/index.tsx | 0 .../WaitingRoomHeader/EditFormStatusForm.tsx | 2 +- .../ui/WaitingRoomHeader/VotingStatusCard.tsx | 0 .../ui/WaitingRoomHeader/index.tsx | 0 100 files changed, 93 insertions(+), 28 deletions(-) rename frontend/src/{features => pages}/betting-page-admin/EndPredictButton.tsx (100%) rename frontend/src/{features => pages}/betting-page-admin/index.tsx (100%) rename frontend/src/{features => pages}/betting-page-admin/model/api.ts (100%) rename frontend/src/{features => pages}/betting-page-admin/model/types.ts (100%) rename frontend/src/{features => pages}/betting-page/api/endBetroom.ts (100%) rename frontend/src/{features => pages}/betting-page/api/getBettingRoomInfo.ts (73%) rename frontend/src/{features => pages}/betting-page/api/getUserInfo.ts (100%) rename frontend/src/{features => pages}/betting-page/hook/useBettingContext.ts (100%) rename frontend/src/{features => pages}/betting-page/hook/useBettingRoomConnection.ts (100%) rename frontend/src/{features => pages}/betting-page/index.tsx (100%) rename frontend/src/{features => pages}/betting-page/model/schema.ts (100%) rename frontend/src/{features => pages}/betting-page/model/var.ts (100%) rename frontend/src/{features => pages}/betting-page/provider/BettingProvider.tsx (100%) rename frontend/src/{features => pages}/betting-page/ui/BettingContainer.tsx (100%) rename frontend/src/{features => pages}/betting-page/ui/BettingFooter.tsx (100%) rename frontend/src/{features => pages}/betting-page/ui/BettingHeader.tsx (100%) rename frontend/src/{features => pages}/betting-page/ui/BettingInput/index.tsx (100%) rename frontend/src/{features => pages}/betting-page/ui/TotalBettingDisplay.tsx (100%) rename frontend/src/{features => pages}/betting-page/utils/placeBetting.ts (100%) rename frontend/src/{features => pages}/betting-predict-result/index.tsx (100%) rename frontend/src/{features => pages}/chat/hook/useChat.tsx (100%) rename frontend/src/{features => pages}/chat/index.tsx (91%) rename frontend/src/{features => pages}/chat/provider/ChatProvider.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatError/index.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatHeader/index.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatHeader/ui/ChatTitle.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatHeader/ui/PredictButton.tsx (85%) rename frontend/src/{features => pages}/chat/ui/ChatHeader/ui/PredictionStatus.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatInput/index.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatInput/ui/EmoticonButton.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatInput/ui/InputBar.tsx (98%) rename frontend/src/{features => pages}/chat/ui/ChatInput/ui/VoteButton.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatInput/ui/style.module.css (100%) rename frontend/src/{features => pages}/chat/ui/ChatMessages/index.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatMessages/ui/Message.tsx (100%) rename frontend/src/{features => pages}/chat/ui/ChatMessages/ui/MessageList.tsx (100%) rename frontend/src/{features => pages}/create-vote/index.ts (100%) rename frontend/src/{features => pages}/create-vote/model/api.ts (100%) rename frontend/src/{features => pages}/create-vote/model/helpers/formatData.ts (100%) rename frontend/src/{features => pages}/create-vote/model/store.ts (100%) rename frontend/src/{features => pages}/create-vote/model/types.ts (100%) rename frontend/src/{features => pages}/create-vote/model/useCaseInput.ts (100%) rename frontend/src/{features => pages}/create-vote/model/useCoinInput.ts (100%) rename frontend/src/{features => pages}/create-vote/model/useTimer.ts (100%) rename frontend/src/{features => pages}/create-vote/model/useTitleInput.ts (100%) rename frontend/src/{features => pages}/create-vote/model/useValidation.ts (100%) rename frontend/src/{features => pages}/create-vote/ui/CreateVotePage.tsx (100%) rename frontend/src/{features => pages}/create-vote/ui/components/CaseInputs.tsx (94%) rename frontend/src/{features => pages}/create-vote/ui/components/CoinInput.tsx (94%) rename frontend/src/{features => pages}/create-vote/ui/components/Timer.tsx (97%) rename frontend/src/{features => pages}/create-vote/ui/components/TitleInput.tsx (90%) rename frontend/src/{features => pages}/create-vote/ui/components/index.ts (100%) rename frontend/src/{features => pages}/create-vote/ui/error/CreateVoteError.tsx (100%) rename frontend/src/{features => pages}/login-page/index.ts (100%) rename frontend/src/{features => pages}/login-page/model/api.ts (100%) rename frontend/src/{features => pages}/login-page/model/store.ts (100%) rename frontend/src/{features => pages}/login-page/model/types.ts (100%) rename frontend/src/{features => pages}/login-page/model/validation.ts (100%) rename frontend/src/{features => pages}/login-page/ui/LoginPage.tsx (100%) rename frontend/src/{features => pages}/login-page/ui/components/GuestLoginForm.tsx (100%) rename frontend/src/{features => pages}/login-page/ui/components/LoginForm.tsx (100%) rename frontend/src/{features => pages}/login-page/ui/components/Logout.tsx (100%) rename frontend/src/{features => pages}/login-page/ui/components/RegisterForm.tsx (97%) rename frontend/src/{features => pages}/login-page/ui/components/TabButton.tsx (100%) rename frontend/src/{features => pages}/login-page/ui/components/Warning.tsx (100%) rename frontend/src/{features => pages}/login-page/ui/components/index.ts (100%) create mode 100644 frontend/src/pages/my-page/api/purchaseDuck.ts create mode 100644 frontend/src/pages/my-page/api/updateDuckCountInCache.ts rename frontend/src/{features => pages}/my-page/error/index.tsx (100%) rename frontend/src/{features => pages}/my-page/index.tsx (100%) create mode 100644 frontend/src/pages/my-page/model/useDuckState.ts rename frontend/src/{features => pages}/my-page/ui/AnimatedDuckCount.tsx (83%) rename frontend/src/{features => pages}/my-page/ui/FallingDuck.tsx (100%) rename frontend/src/{features => pages}/my-page/ui/Pond.tsx (100%) rename frontend/src/{features => pages}/predict-detail/index.tsx (100%) rename frontend/src/{features => pages}/predict-detail/model/api.ts (100%) rename frontend/src/{features => pages}/predict-detail/model/schema.ts (100%) rename frontend/src/{features => pages}/predict-detail/ui/AdminBettingResult.tsx (100%) rename frontend/src/{features => pages}/predict-detail/ui/Bettingstatistics.tsx (100%) rename frontend/src/{features => pages}/predict-detail/ui/GuestFooter.tsx (100%) rename frontend/src/{features => pages}/predict-detail/ui/UserBettingResult.tsx (100%) rename frontend/src/{features => pages}/predict-detail/ui/UserFooter.tsx (100%) rename frontend/src/{features => pages}/waiting-room/error/AccessError.ts (100%) rename frontend/src/{features => pages}/waiting-room/error/Forbidden.tsx (100%) rename frontend/src/{features => pages}/waiting-room/error/Unauthorized.tsx (100%) rename frontend/src/{features => pages}/waiting-room/hooks/use-waiting-context.tsx (100%) rename frontend/src/{features => pages}/waiting-room/index.tsx (97%) rename frontend/src/{features => pages}/waiting-room/provider/WaitingRoomProvider.tsx (100%) rename frontend/src/{features => pages}/waiting-room/style.module.css (100%) rename frontend/src/{features => pages}/waiting-room/ui/AdminFooter/CancleButton.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/AdminFooter/ParticipateButton.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/AdminFooter/StartVotingButton.tsx (94%) rename frontend/src/{features => pages}/waiting-room/ui/AdminFooter/index.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/MemberFooter/index.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/ParticipantsList.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/SharedLinkCard.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/WaitingError/index.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx (98%) rename frontend/src/{features => pages}/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx (100%) rename frontend/src/{features => pages}/waiting-room/ui/WaitingRoomHeader/index.tsx (100%) diff --git a/frontend/src/features/betting-page-admin/EndPredictButton.tsx b/frontend/src/pages/betting-page-admin/EndPredictButton.tsx similarity index 100% rename from frontend/src/features/betting-page-admin/EndPredictButton.tsx rename to frontend/src/pages/betting-page-admin/EndPredictButton.tsx diff --git a/frontend/src/features/betting-page-admin/index.tsx b/frontend/src/pages/betting-page-admin/index.tsx similarity index 100% rename from frontend/src/features/betting-page-admin/index.tsx rename to frontend/src/pages/betting-page-admin/index.tsx diff --git a/frontend/src/features/betting-page-admin/model/api.ts b/frontend/src/pages/betting-page-admin/model/api.ts similarity index 100% rename from frontend/src/features/betting-page-admin/model/api.ts rename to frontend/src/pages/betting-page-admin/model/api.ts diff --git a/frontend/src/features/betting-page-admin/model/types.ts b/frontend/src/pages/betting-page-admin/model/types.ts similarity index 100% rename from frontend/src/features/betting-page-admin/model/types.ts rename to frontend/src/pages/betting-page-admin/model/types.ts diff --git a/frontend/src/features/betting-page/api/endBetroom.ts b/frontend/src/pages/betting-page/api/endBetroom.ts similarity index 100% rename from frontend/src/features/betting-page/api/endBetroom.ts rename to frontend/src/pages/betting-page/api/endBetroom.ts diff --git a/frontend/src/features/betting-page/api/getBettingRoomInfo.ts b/frontend/src/pages/betting-page/api/getBettingRoomInfo.ts similarity index 73% rename from frontend/src/features/betting-page/api/getBettingRoomInfo.ts rename to frontend/src/pages/betting-page/api/getBettingRoomInfo.ts index 2181a2e..fbabf86 100644 --- a/frontend/src/features/betting-page/api/getBettingRoomInfo.ts +++ b/frontend/src/pages/betting-page/api/getBettingRoomInfo.ts @@ -1,16 +1,17 @@ import { responseBetRoomInfo } from "@betting-duck/shared"; async function getBettingRoomInfo(roomId: string) { + if (!roomId) return null; try { const response = await fetch(`/api/betrooms/${roomId}`); if (!response.ok) { - return null; // 에러 대신 null 반환 + return null; } const { data } = await response.json(); const result = responseBetRoomInfo.safeParse(data); if (!result.success) { - return null; // 파싱 실패시에도 null 반환 + return null; } return { ...result.data, @@ -18,7 +19,7 @@ async function getBettingRoomInfo(roomId: string) { placeBetAmount: 0, }; } catch { - return null; // 네트워크 에러 등의 경우에도 null 반환 + return null; } } diff --git a/frontend/src/features/betting-page/api/getUserInfo.ts b/frontend/src/pages/betting-page/api/getUserInfo.ts similarity index 100% rename from frontend/src/features/betting-page/api/getUserInfo.ts rename to frontend/src/pages/betting-page/api/getUserInfo.ts diff --git a/frontend/src/features/betting-page/hook/useBettingContext.ts b/frontend/src/pages/betting-page/hook/useBettingContext.ts similarity index 100% rename from frontend/src/features/betting-page/hook/useBettingContext.ts rename to frontend/src/pages/betting-page/hook/useBettingContext.ts diff --git a/frontend/src/features/betting-page/hook/useBettingRoomConnection.ts b/frontend/src/pages/betting-page/hook/useBettingRoomConnection.ts similarity index 100% rename from frontend/src/features/betting-page/hook/useBettingRoomConnection.ts rename to frontend/src/pages/betting-page/hook/useBettingRoomConnection.ts diff --git a/frontend/src/features/betting-page/index.tsx b/frontend/src/pages/betting-page/index.tsx similarity index 100% rename from frontend/src/features/betting-page/index.tsx rename to frontend/src/pages/betting-page/index.tsx diff --git a/frontend/src/features/betting-page/model/schema.ts b/frontend/src/pages/betting-page/model/schema.ts similarity index 100% rename from frontend/src/features/betting-page/model/schema.ts rename to frontend/src/pages/betting-page/model/schema.ts diff --git a/frontend/src/features/betting-page/model/var.ts b/frontend/src/pages/betting-page/model/var.ts similarity index 100% rename from frontend/src/features/betting-page/model/var.ts rename to frontend/src/pages/betting-page/model/var.ts diff --git a/frontend/src/features/betting-page/provider/BettingProvider.tsx b/frontend/src/pages/betting-page/provider/BettingProvider.tsx similarity index 100% rename from frontend/src/features/betting-page/provider/BettingProvider.tsx rename to frontend/src/pages/betting-page/provider/BettingProvider.tsx diff --git a/frontend/src/features/betting-page/ui/BettingContainer.tsx b/frontend/src/pages/betting-page/ui/BettingContainer.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingContainer.tsx rename to frontend/src/pages/betting-page/ui/BettingContainer.tsx diff --git a/frontend/src/features/betting-page/ui/BettingFooter.tsx b/frontend/src/pages/betting-page/ui/BettingFooter.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingFooter.tsx rename to frontend/src/pages/betting-page/ui/BettingFooter.tsx diff --git a/frontend/src/features/betting-page/ui/BettingHeader.tsx b/frontend/src/pages/betting-page/ui/BettingHeader.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingHeader.tsx rename to frontend/src/pages/betting-page/ui/BettingHeader.tsx diff --git a/frontend/src/features/betting-page/ui/BettingInput/index.tsx b/frontend/src/pages/betting-page/ui/BettingInput/index.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/BettingInput/index.tsx rename to frontend/src/pages/betting-page/ui/BettingInput/index.tsx diff --git a/frontend/src/features/betting-page/ui/TotalBettingDisplay.tsx b/frontend/src/pages/betting-page/ui/TotalBettingDisplay.tsx similarity index 100% rename from frontend/src/features/betting-page/ui/TotalBettingDisplay.tsx rename to frontend/src/pages/betting-page/ui/TotalBettingDisplay.tsx diff --git a/frontend/src/features/betting-page/utils/placeBetting.ts b/frontend/src/pages/betting-page/utils/placeBetting.ts similarity index 100% rename from frontend/src/features/betting-page/utils/placeBetting.ts rename to frontend/src/pages/betting-page/utils/placeBetting.ts diff --git a/frontend/src/features/betting-predict-result/index.tsx b/frontend/src/pages/betting-predict-result/index.tsx similarity index 100% rename from frontend/src/features/betting-predict-result/index.tsx rename to frontend/src/pages/betting-predict-result/index.tsx diff --git a/frontend/src/features/chat/hook/useChat.tsx b/frontend/src/pages/chat/hook/useChat.tsx similarity index 100% rename from frontend/src/features/chat/hook/useChat.tsx rename to frontend/src/pages/chat/hook/useChat.tsx diff --git a/frontend/src/features/chat/index.tsx b/frontend/src/pages/chat/index.tsx similarity index 91% rename from frontend/src/features/chat/index.tsx rename to frontend/src/pages/chat/index.tsx index fea9158..17c4546 100644 --- a/frontend/src/features/chat/index.tsx +++ b/frontend/src/pages/chat/index.tsx @@ -1,6 +1,6 @@ -import { ChatHeader } from "@/features/chat/ui/ChatHeader"; -import { ChatInput } from "@/features/chat/ui/ChatInput"; -import { ChatMessages } from "@/features/chat/ui/ChatMessages"; +import { ChatHeader } from "@/pages/chat/ui/ChatHeader"; +import { ChatInput } from "@/pages/chat/ui/ChatInput"; +import { ChatMessages } from "@/pages/chat/ui/ChatMessages"; import { ChatProvider } from "./provider/ChatProvider"; import { cn } from "@/shared/misc"; import { useLoaderData } from "@tanstack/react-router"; diff --git a/frontend/src/features/chat/provider/ChatProvider.tsx b/frontend/src/pages/chat/provider/ChatProvider.tsx similarity index 100% rename from frontend/src/features/chat/provider/ChatProvider.tsx rename to frontend/src/pages/chat/provider/ChatProvider.tsx diff --git a/frontend/src/features/chat/ui/ChatError/index.tsx b/frontend/src/pages/chat/ui/ChatError/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatError/index.tsx rename to frontend/src/pages/chat/ui/ChatError/index.tsx diff --git a/frontend/src/features/chat/ui/ChatHeader/index.tsx b/frontend/src/pages/chat/ui/ChatHeader/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatHeader/index.tsx rename to frontend/src/pages/chat/ui/ChatHeader/index.tsx diff --git a/frontend/src/features/chat/ui/ChatHeader/ui/ChatTitle.tsx b/frontend/src/pages/chat/ui/ChatHeader/ui/ChatTitle.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatHeader/ui/ChatTitle.tsx rename to frontend/src/pages/chat/ui/ChatHeader/ui/ChatTitle.tsx diff --git a/frontend/src/features/chat/ui/ChatHeader/ui/PredictButton.tsx b/frontend/src/pages/chat/ui/ChatHeader/ui/PredictButton.tsx similarity index 85% rename from frontend/src/features/chat/ui/ChatHeader/ui/PredictButton.tsx rename to frontend/src/pages/chat/ui/ChatHeader/ui/PredictButton.tsx index e7fb31a..4c732da 100644 --- a/frontend/src/features/chat/ui/ChatHeader/ui/PredictButton.tsx +++ b/frontend/src/pages/chat/ui/ChatHeader/ui/PredictButton.tsx @@ -1,12 +1,12 @@ -import { BettingPage } from "@/features/betting-page"; -import { useChat } from "@/features/chat/hook/useChat"; +import { BettingPage } from "@/pages/betting-page"; +import { useChat } from "@/pages/chat/hook/useChat"; import { Dialog, DialogTrigger, DialogContent, } from "@/shared/components/Dialog"; import { useNavigate, useParams } from "@tanstack/react-router"; -import { STORAGE_KEY } from "@/features/betting-page/model/var"; +import { STORAGE_KEY } from "@/pages/betting-page/model/var"; import { useUserContext } from "@/shared/hooks/useUserContext"; function PredictButton() { diff --git a/frontend/src/features/chat/ui/ChatHeader/ui/PredictionStatus.tsx b/frontend/src/pages/chat/ui/ChatHeader/ui/PredictionStatus.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatHeader/ui/PredictionStatus.tsx rename to frontend/src/pages/chat/ui/ChatHeader/ui/PredictionStatus.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/index.tsx b/frontend/src/pages/chat/ui/ChatInput/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/index.tsx rename to frontend/src/pages/chat/ui/ChatInput/index.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/ui/EmoticonButton.tsx b/frontend/src/pages/chat/ui/ChatInput/ui/EmoticonButton.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/ui/EmoticonButton.tsx rename to frontend/src/pages/chat/ui/ChatInput/ui/EmoticonButton.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/ui/InputBar.tsx b/frontend/src/pages/chat/ui/ChatInput/ui/InputBar.tsx similarity index 98% rename from frontend/src/features/chat/ui/ChatInput/ui/InputBar.tsx rename to frontend/src/pages/chat/ui/ChatInput/ui/InputBar.tsx index 0692eb9..e4de810 100644 --- a/frontend/src/features/chat/ui/ChatInput/ui/InputBar.tsx +++ b/frontend/src/pages/chat/ui/ChatInput/ui/InputBar.tsx @@ -1,5 +1,5 @@ import { UserInfo } from "@/app/provider/UserProvider"; -import { useChat } from "@/features/chat/hook/useChat"; +import { useChat } from "@/pages/chat/hook/useChat"; import { useParams } from "@tanstack/react-router"; import React from "react"; diff --git a/frontend/src/features/chat/ui/ChatInput/ui/VoteButton.tsx b/frontend/src/pages/chat/ui/ChatInput/ui/VoteButton.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/ui/VoteButton.tsx rename to frontend/src/pages/chat/ui/ChatInput/ui/VoteButton.tsx diff --git a/frontend/src/features/chat/ui/ChatInput/ui/style.module.css b/frontend/src/pages/chat/ui/ChatInput/ui/style.module.css similarity index 100% rename from frontend/src/features/chat/ui/ChatInput/ui/style.module.css rename to frontend/src/pages/chat/ui/ChatInput/ui/style.module.css diff --git a/frontend/src/features/chat/ui/ChatMessages/index.tsx b/frontend/src/pages/chat/ui/ChatMessages/index.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatMessages/index.tsx rename to frontend/src/pages/chat/ui/ChatMessages/index.tsx diff --git a/frontend/src/features/chat/ui/ChatMessages/ui/Message.tsx b/frontend/src/pages/chat/ui/ChatMessages/ui/Message.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatMessages/ui/Message.tsx rename to frontend/src/pages/chat/ui/ChatMessages/ui/Message.tsx diff --git a/frontend/src/features/chat/ui/ChatMessages/ui/MessageList.tsx b/frontend/src/pages/chat/ui/ChatMessages/ui/MessageList.tsx similarity index 100% rename from frontend/src/features/chat/ui/ChatMessages/ui/MessageList.tsx rename to frontend/src/pages/chat/ui/ChatMessages/ui/MessageList.tsx diff --git a/frontend/src/features/create-vote/index.ts b/frontend/src/pages/create-vote/index.ts similarity index 100% rename from frontend/src/features/create-vote/index.ts rename to frontend/src/pages/create-vote/index.ts diff --git a/frontend/src/features/create-vote/model/api.ts b/frontend/src/pages/create-vote/model/api.ts similarity index 100% rename from frontend/src/features/create-vote/model/api.ts rename to frontend/src/pages/create-vote/model/api.ts diff --git a/frontend/src/features/create-vote/model/helpers/formatData.ts b/frontend/src/pages/create-vote/model/helpers/formatData.ts similarity index 100% rename from frontend/src/features/create-vote/model/helpers/formatData.ts rename to frontend/src/pages/create-vote/model/helpers/formatData.ts diff --git a/frontend/src/features/create-vote/model/store.ts b/frontend/src/pages/create-vote/model/store.ts similarity index 100% rename from frontend/src/features/create-vote/model/store.ts rename to frontend/src/pages/create-vote/model/store.ts diff --git a/frontend/src/features/create-vote/model/types.ts b/frontend/src/pages/create-vote/model/types.ts similarity index 100% rename from frontend/src/features/create-vote/model/types.ts rename to frontend/src/pages/create-vote/model/types.ts diff --git a/frontend/src/features/create-vote/model/useCaseInput.ts b/frontend/src/pages/create-vote/model/useCaseInput.ts similarity index 100% rename from frontend/src/features/create-vote/model/useCaseInput.ts rename to frontend/src/pages/create-vote/model/useCaseInput.ts diff --git a/frontend/src/features/create-vote/model/useCoinInput.ts b/frontend/src/pages/create-vote/model/useCoinInput.ts similarity index 100% rename from frontend/src/features/create-vote/model/useCoinInput.ts rename to frontend/src/pages/create-vote/model/useCoinInput.ts diff --git a/frontend/src/features/create-vote/model/useTimer.ts b/frontend/src/pages/create-vote/model/useTimer.ts similarity index 100% rename from frontend/src/features/create-vote/model/useTimer.ts rename to frontend/src/pages/create-vote/model/useTimer.ts diff --git a/frontend/src/features/create-vote/model/useTitleInput.ts b/frontend/src/pages/create-vote/model/useTitleInput.ts similarity index 100% rename from frontend/src/features/create-vote/model/useTitleInput.ts rename to frontend/src/pages/create-vote/model/useTitleInput.ts diff --git a/frontend/src/features/create-vote/model/useValidation.ts b/frontend/src/pages/create-vote/model/useValidation.ts similarity index 100% rename from frontend/src/features/create-vote/model/useValidation.ts rename to frontend/src/pages/create-vote/model/useValidation.ts diff --git a/frontend/src/features/create-vote/ui/CreateVotePage.tsx b/frontend/src/pages/create-vote/ui/CreateVotePage.tsx similarity index 100% rename from frontend/src/features/create-vote/ui/CreateVotePage.tsx rename to frontend/src/pages/create-vote/ui/CreateVotePage.tsx diff --git a/frontend/src/features/create-vote/ui/components/CaseInputs.tsx b/frontend/src/pages/create-vote/ui/components/CaseInputs.tsx similarity index 94% rename from frontend/src/features/create-vote/ui/components/CaseInputs.tsx rename to frontend/src/pages/create-vote/ui/components/CaseInputs.tsx index f40ebd8..7475ff8 100644 --- a/frontend/src/features/create-vote/ui/components/CaseInputs.tsx +++ b/frontend/src/pages/create-vote/ui/components/CaseInputs.tsx @@ -1,7 +1,7 @@ import React from "react"; import { InputField } from "../../../../shared/components/input/InputField"; import { DuckIcon } from "@/shared/icons"; -import { useCaseInput } from "@/features/create-vote/model/useCaseInput"; +import { useCaseInput } from "@/pages/create-vote/model/useCaseInput"; const CaseInputs = React.memo( ({ diff --git a/frontend/src/features/create-vote/ui/components/CoinInput.tsx b/frontend/src/pages/create-vote/ui/components/CoinInput.tsx similarity index 94% rename from frontend/src/features/create-vote/ui/components/CoinInput.tsx rename to frontend/src/pages/create-vote/ui/components/CoinInput.tsx index d124887..f1d8c8c 100644 --- a/frontend/src/features/create-vote/ui/components/CoinInput.tsx +++ b/frontend/src/pages/create-vote/ui/components/CoinInput.tsx @@ -1,6 +1,6 @@ import { ArrowDownIcon, ArrowUpIcon, DuckCoinIcon } from "@/shared/icons"; import React from "react"; -import { useCoinInput } from "@/features/create-vote/model/useCoinInput"; +import { useCoinInput } from "@/pages/create-vote/model/useCoinInput"; const CoinInput = React.memo( ({ initialValue = 100 }: { initialValue?: number }) => { diff --git a/frontend/src/features/create-vote/ui/components/Timer.tsx b/frontend/src/pages/create-vote/ui/components/Timer.tsx similarity index 97% rename from frontend/src/features/create-vote/ui/components/Timer.tsx rename to frontend/src/pages/create-vote/ui/components/Timer.tsx index a9a8470..fb45cbd 100644 --- a/frontend/src/features/create-vote/ui/components/Timer.tsx +++ b/frontend/src/pages/create-vote/ui/components/Timer.tsx @@ -1,6 +1,6 @@ import { ArrowDownIcon, ArrowUpIcon, TimerIcon } from "@/shared/icons"; import React from "react"; -import { useTimer } from "@/features/create-vote/model/useTimer"; +import { useTimer } from "@/pages/create-vote/model/useTimer"; const Timer = React.memo(({ initialValue = 1 }: { initialValue?: number }) => { const { value, incrementValue, decrementValue } = useTimer(initialValue); diff --git a/frontend/src/features/create-vote/ui/components/TitleInput.tsx b/frontend/src/pages/create-vote/ui/components/TitleInput.tsx similarity index 90% rename from frontend/src/features/create-vote/ui/components/TitleInput.tsx rename to frontend/src/pages/create-vote/ui/components/TitleInput.tsx index d215c26..c2b34eb 100644 --- a/frontend/src/features/create-vote/ui/components/TitleInput.tsx +++ b/frontend/src/pages/create-vote/ui/components/TitleInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import { InputField } from "../../../../shared/components/input/InputField"; import { TextIcon } from "@/shared/icons"; -import { useTitleInput } from "@/features/create-vote/model/useTitleInput"; +import { useTitleInput } from "@/pages/create-vote/model/useTitleInput"; const TitleInput = React.memo( ({ initialValue = "" }: { initialValue?: string }) => { diff --git a/frontend/src/features/create-vote/ui/components/index.ts b/frontend/src/pages/create-vote/ui/components/index.ts similarity index 100% rename from frontend/src/features/create-vote/ui/components/index.ts rename to frontend/src/pages/create-vote/ui/components/index.ts diff --git a/frontend/src/features/create-vote/ui/error/CreateVoteError.tsx b/frontend/src/pages/create-vote/ui/error/CreateVoteError.tsx similarity index 100% rename from frontend/src/features/create-vote/ui/error/CreateVoteError.tsx rename to frontend/src/pages/create-vote/ui/error/CreateVoteError.tsx diff --git a/frontend/src/features/login-page/index.ts b/frontend/src/pages/login-page/index.ts similarity index 100% rename from frontend/src/features/login-page/index.ts rename to frontend/src/pages/login-page/index.ts diff --git a/frontend/src/features/login-page/model/api.ts b/frontend/src/pages/login-page/model/api.ts similarity index 100% rename from frontend/src/features/login-page/model/api.ts rename to frontend/src/pages/login-page/model/api.ts diff --git a/frontend/src/features/login-page/model/store.ts b/frontend/src/pages/login-page/model/store.ts similarity index 100% rename from frontend/src/features/login-page/model/store.ts rename to frontend/src/pages/login-page/model/store.ts diff --git a/frontend/src/features/login-page/model/types.ts b/frontend/src/pages/login-page/model/types.ts similarity index 100% rename from frontend/src/features/login-page/model/types.ts rename to frontend/src/pages/login-page/model/types.ts diff --git a/frontend/src/features/login-page/model/validation.ts b/frontend/src/pages/login-page/model/validation.ts similarity index 100% rename from frontend/src/features/login-page/model/validation.ts rename to frontend/src/pages/login-page/model/validation.ts diff --git a/frontend/src/features/login-page/ui/LoginPage.tsx b/frontend/src/pages/login-page/ui/LoginPage.tsx similarity index 100% rename from frontend/src/features/login-page/ui/LoginPage.tsx rename to frontend/src/pages/login-page/ui/LoginPage.tsx diff --git a/frontend/src/features/login-page/ui/components/GuestLoginForm.tsx b/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx similarity index 100% rename from frontend/src/features/login-page/ui/components/GuestLoginForm.tsx rename to frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx diff --git a/frontend/src/features/login-page/ui/components/LoginForm.tsx b/frontend/src/pages/login-page/ui/components/LoginForm.tsx similarity index 100% rename from frontend/src/features/login-page/ui/components/LoginForm.tsx rename to frontend/src/pages/login-page/ui/components/LoginForm.tsx diff --git a/frontend/src/features/login-page/ui/components/Logout.tsx b/frontend/src/pages/login-page/ui/components/Logout.tsx similarity index 100% rename from frontend/src/features/login-page/ui/components/Logout.tsx rename to frontend/src/pages/login-page/ui/components/Logout.tsx diff --git a/frontend/src/features/login-page/ui/components/RegisterForm.tsx b/frontend/src/pages/login-page/ui/components/RegisterForm.tsx similarity index 97% rename from frontend/src/features/login-page/ui/components/RegisterForm.tsx rename to frontend/src/pages/login-page/ui/components/RegisterForm.tsx index 4f0a0bf..17fcfa8 100644 --- a/frontend/src/features/login-page/ui/components/RegisterForm.tsx +++ b/frontend/src/pages/login-page/ui/components/RegisterForm.tsx @@ -1,13 +1,13 @@ import { InputField } from "@/shared/components/input/InputField"; import { EmailIcon, LoginIDIcon, LoginPasswordIcon } from "@/shared/icons"; import { useEffect, useState } from "react"; -import { useAuthStore } from "@/features/login-page/model/store"; +import { useAuthStore } from "@/pages/login-page/model/store"; import { validateConfirmPassword, validateEmail, validateNickname, validatePassword, -} from "@/features/login-page/model/validation"; +} from "@/pages/login-page/model/validation"; import { Warning } from "./Warning"; interface RegisterFormProps { diff --git a/frontend/src/features/login-page/ui/components/TabButton.tsx b/frontend/src/pages/login-page/ui/components/TabButton.tsx similarity index 100% rename from frontend/src/features/login-page/ui/components/TabButton.tsx rename to frontend/src/pages/login-page/ui/components/TabButton.tsx diff --git a/frontend/src/features/login-page/ui/components/Warning.tsx b/frontend/src/pages/login-page/ui/components/Warning.tsx similarity index 100% rename from frontend/src/features/login-page/ui/components/Warning.tsx rename to frontend/src/pages/login-page/ui/components/Warning.tsx diff --git a/frontend/src/features/login-page/ui/components/index.ts b/frontend/src/pages/login-page/ui/components/index.ts similarity index 100% rename from frontend/src/features/login-page/ui/components/index.ts rename to frontend/src/pages/login-page/ui/components/index.ts diff --git a/frontend/src/pages/my-page/api/purchaseDuck.ts b/frontend/src/pages/my-page/api/purchaseDuck.ts new file mode 100644 index 0000000..35f37af --- /dev/null +++ b/frontend/src/pages/my-page/api/purchaseDuck.ts @@ -0,0 +1,12 @@ +async function purchaseDuck() { + try { + const response = await fetch("/api/users/purchaseduck"); + if (!response.ok) { + throw new Error("오리를 구매하는데 실패했습니다."); + } + } catch (error) { + console.error(error); + } +} + +export { purchaseDuck }; diff --git a/frontend/src/pages/my-page/api/updateDuckCountInCache.ts b/frontend/src/pages/my-page/api/updateDuckCountInCache.ts new file mode 100644 index 0000000..e4f8df2 --- /dev/null +++ b/frontend/src/pages/my-page/api/updateDuckCountInCache.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { authQueries } from "@/shared/lib/auth/authQuery"; +import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useQueryClient } from "@tanstack/react-query"; + +async function updateDuckCountInCache( + queryClient: ReturnType, + { + currentDuck, + numberOfDucks, + }: { currentDuck: number; numberOfDucks: number }, +) { + await queryClient.setQueryData( + authQueries.queryKey, + (old: z.infer) => ({ + ...old, + userInfo: { + ...old.userInfo, + duck: currentDuck - 30, + realDuck: numberOfDucks + 1, + }, + }), + ); +} + +export { updateDuckCountInCache }; diff --git a/frontend/src/features/my-page/error/index.tsx b/frontend/src/pages/my-page/error/index.tsx similarity index 100% rename from frontend/src/features/my-page/error/index.tsx rename to frontend/src/pages/my-page/error/index.tsx diff --git a/frontend/src/features/my-page/index.tsx b/frontend/src/pages/my-page/index.tsx similarity index 100% rename from frontend/src/features/my-page/index.tsx rename to frontend/src/pages/my-page/index.tsx diff --git a/frontend/src/pages/my-page/model/useDuckState.ts b/frontend/src/pages/my-page/model/useDuckState.ts new file mode 100644 index 0000000..f039362 --- /dev/null +++ b/frontend/src/pages/my-page/model/useDuckState.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useCallback } from "react"; +import { authQueries } from "@/shared/lib/auth/authQuery"; + +type AuthStatusType = z.infer; + +function useDuckState(authData: AuthStatusType) { + const queryClient = useQueryClient(); + + const currentDuck = authData.userInfo.duck; + const numberOfDucks = authData.userInfo.realDuck; + + const addDuck = useCallback(() => { + queryClient.setQueryData(authQueries.queryKey, (old: AuthStatusType) => ({ + ...old, + userInfo: { + ...old.userInfo, + duck: old.userInfo.duck - 30, + realDuck: old.userInfo.realDuck + 1, + }, + })); + }, [queryClient]); + + return { + currentDuck, + numberOfDucks, + addDuck, + }; +} + +export { useDuckState }; diff --git a/frontend/src/features/my-page/ui/AnimatedDuckCount.tsx b/frontend/src/pages/my-page/ui/AnimatedDuckCount.tsx similarity index 83% rename from frontend/src/features/my-page/ui/AnimatedDuckCount.tsx rename to frontend/src/pages/my-page/ui/AnimatedDuckCount.tsx index d4c0fc7..5daddad 100644 --- a/frontend/src/features/my-page/ui/AnimatedDuckCount.tsx +++ b/frontend/src/pages/my-page/ui/AnimatedDuckCount.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { DuckCoinIcon } from "@/shared/icons"; interface DuckCoinIconProps { width: number; @@ -31,32 +32,25 @@ const AnimatedDigit: React.FC = ({ ); }; -const AnimatedDuckCount: React.FC = ({ - value, - DuckCoinIcon, -}) => { +const AnimatedDuckCount: React.FC = ({ value }) => { const [prevValue, setPrevValue] = useState(value); const [animatingDigits, setAnimatingDigits] = useState>( new Set(), ); - // 숫자를 자릿수 배열로 변환 const getDigits = (num: number): string[] => { return num.toString().padStart(4, "0").split(""); }; useEffect(() => { if (value !== prevValue) { - // 변경된 자릿수 확인 const prevDigits = getDigits(prevValue); const newDigits = getDigits(value); const changedPositions = new Set(); - // 끝에서부터 비교하여 변경된 자릿수 확인 for (let i = newDigits.length - 1; i >= 0; i--) { if (prevDigits[i] !== newDigits[i]) { changedPositions.add(i); - // 현재 자릿수가 변경되면 그 앞의 자릿수도 모두 변경된 것으로 처리 if (i < newDigits.length - 1) { changedPositions.add(i + 1); } @@ -65,7 +59,6 @@ const AnimatedDuckCount: React.FC = ({ setAnimatingDigits(changedPositions); - // 애니메이션 종료 후 상태 초기화 const timer = setTimeout(() => { setAnimatingDigits(new Set()); setPrevValue(value); diff --git a/frontend/src/features/my-page/ui/FallingDuck.tsx b/frontend/src/pages/my-page/ui/FallingDuck.tsx similarity index 100% rename from frontend/src/features/my-page/ui/FallingDuck.tsx rename to frontend/src/pages/my-page/ui/FallingDuck.tsx diff --git a/frontend/src/features/my-page/ui/Pond.tsx b/frontend/src/pages/my-page/ui/Pond.tsx similarity index 100% rename from frontend/src/features/my-page/ui/Pond.tsx rename to frontend/src/pages/my-page/ui/Pond.tsx diff --git a/frontend/src/features/predict-detail/index.tsx b/frontend/src/pages/predict-detail/index.tsx similarity index 100% rename from frontend/src/features/predict-detail/index.tsx rename to frontend/src/pages/predict-detail/index.tsx diff --git a/frontend/src/features/predict-detail/model/api.ts b/frontend/src/pages/predict-detail/model/api.ts similarity index 100% rename from frontend/src/features/predict-detail/model/api.ts rename to frontend/src/pages/predict-detail/model/api.ts diff --git a/frontend/src/features/predict-detail/model/schema.ts b/frontend/src/pages/predict-detail/model/schema.ts similarity index 100% rename from frontend/src/features/predict-detail/model/schema.ts rename to frontend/src/pages/predict-detail/model/schema.ts diff --git a/frontend/src/features/predict-detail/ui/AdminBettingResult.tsx b/frontend/src/pages/predict-detail/ui/AdminBettingResult.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/AdminBettingResult.tsx rename to frontend/src/pages/predict-detail/ui/AdminBettingResult.tsx diff --git a/frontend/src/features/predict-detail/ui/Bettingstatistics.tsx b/frontend/src/pages/predict-detail/ui/Bettingstatistics.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/Bettingstatistics.tsx rename to frontend/src/pages/predict-detail/ui/Bettingstatistics.tsx diff --git a/frontend/src/features/predict-detail/ui/GuestFooter.tsx b/frontend/src/pages/predict-detail/ui/GuestFooter.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/GuestFooter.tsx rename to frontend/src/pages/predict-detail/ui/GuestFooter.tsx diff --git a/frontend/src/features/predict-detail/ui/UserBettingResult.tsx b/frontend/src/pages/predict-detail/ui/UserBettingResult.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/UserBettingResult.tsx rename to frontend/src/pages/predict-detail/ui/UserBettingResult.tsx diff --git a/frontend/src/features/predict-detail/ui/UserFooter.tsx b/frontend/src/pages/predict-detail/ui/UserFooter.tsx similarity index 100% rename from frontend/src/features/predict-detail/ui/UserFooter.tsx rename to frontend/src/pages/predict-detail/ui/UserFooter.tsx diff --git a/frontend/src/features/waiting-room/error/AccessError.ts b/frontend/src/pages/waiting-room/error/AccessError.ts similarity index 100% rename from frontend/src/features/waiting-room/error/AccessError.ts rename to frontend/src/pages/waiting-room/error/AccessError.ts diff --git a/frontend/src/features/waiting-room/error/Forbidden.tsx b/frontend/src/pages/waiting-room/error/Forbidden.tsx similarity index 100% rename from frontend/src/features/waiting-room/error/Forbidden.tsx rename to frontend/src/pages/waiting-room/error/Forbidden.tsx diff --git a/frontend/src/features/waiting-room/error/Unauthorized.tsx b/frontend/src/pages/waiting-room/error/Unauthorized.tsx similarity index 100% rename from frontend/src/features/waiting-room/error/Unauthorized.tsx rename to frontend/src/pages/waiting-room/error/Unauthorized.tsx diff --git a/frontend/src/features/waiting-room/hooks/use-waiting-context.tsx b/frontend/src/pages/waiting-room/hooks/use-waiting-context.tsx similarity index 100% rename from frontend/src/features/waiting-room/hooks/use-waiting-context.tsx rename to frontend/src/pages/waiting-room/hooks/use-waiting-context.tsx diff --git a/frontend/src/features/waiting-room/index.tsx b/frontend/src/pages/waiting-room/index.tsx similarity index 97% rename from frontend/src/features/waiting-room/index.tsx rename to frontend/src/pages/waiting-room/index.tsx index 49a2412..33a1612 100644 --- a/frontend/src/features/waiting-room/index.tsx +++ b/frontend/src/pages/waiting-room/index.tsx @@ -1,6 +1,6 @@ import { WaitingRoomHeader } from "./ui/WaitingRoomHeader"; import { ParticipantsList } from "./ui/ParticipantsList"; -import { ShareLinkCard } from "@/features/waiting-room/ui/SharedLinkCard"; +import { ShareLinkCard } from "@/pages/waiting-room/ui/SharedLinkCard"; import { AdminFooter } from "./ui/AdminFooter"; import { MemberFooter } from "./ui/MemberFooter"; import { WaitingRoomProvider } from "./provider/WaitingRoomProvider"; diff --git a/frontend/src/features/waiting-room/provider/WaitingRoomProvider.tsx b/frontend/src/pages/waiting-room/provider/WaitingRoomProvider.tsx similarity index 100% rename from frontend/src/features/waiting-room/provider/WaitingRoomProvider.tsx rename to frontend/src/pages/waiting-room/provider/WaitingRoomProvider.tsx diff --git a/frontend/src/features/waiting-room/style.module.css b/frontend/src/pages/waiting-room/style.module.css similarity index 100% rename from frontend/src/features/waiting-room/style.module.css rename to frontend/src/pages/waiting-room/style.module.css diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/CancleButton.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/CancleButton.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/AdminFooter/CancleButton.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/CancleButton.tsx diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/ParticipateButton.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/ParticipateButton.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/AdminFooter/ParticipateButton.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/ParticipateButton.tsx diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/StartVotingButton.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/StartVotingButton.tsx similarity index 94% rename from frontend/src/features/waiting-room/ui/AdminFooter/StartVotingButton.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/StartVotingButton.tsx index 16c0f16..8c4cc9e 100644 --- a/frontend/src/features/waiting-room/ui/AdminFooter/StartVotingButton.tsx +++ b/frontend/src/pages/waiting-room/ui/AdminFooter/StartVotingButton.tsx @@ -1,7 +1,7 @@ import { useNavigate, useRouter } from "@tanstack/react-router"; import { responseBetRoomInfo } from "@betting-duck/shared"; import { z } from "zod"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import React from "react"; function StartVotingButton({ diff --git a/frontend/src/features/waiting-room/ui/AdminFooter/index.tsx b/frontend/src/pages/waiting-room/ui/AdminFooter/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/AdminFooter/index.tsx rename to frontend/src/pages/waiting-room/ui/AdminFooter/index.tsx diff --git a/frontend/src/features/waiting-room/ui/MemberFooter/index.tsx b/frontend/src/pages/waiting-room/ui/MemberFooter/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/MemberFooter/index.tsx rename to frontend/src/pages/waiting-room/ui/MemberFooter/index.tsx diff --git a/frontend/src/features/waiting-room/ui/ParticipantsList.tsx b/frontend/src/pages/waiting-room/ui/ParticipantsList.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/ParticipantsList.tsx rename to frontend/src/pages/waiting-room/ui/ParticipantsList.tsx diff --git a/frontend/src/features/waiting-room/ui/SharedLinkCard.tsx b/frontend/src/pages/waiting-room/ui/SharedLinkCard.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/SharedLinkCard.tsx rename to frontend/src/pages/waiting-room/ui/SharedLinkCard.tsx diff --git a/frontend/src/features/waiting-room/ui/WaitingError/index.tsx b/frontend/src/pages/waiting-room/ui/WaitingError/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/WaitingError/index.tsx rename to frontend/src/pages/waiting-room/ui/WaitingError/index.tsx diff --git a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx similarity index 98% rename from frontend/src/features/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx rename to frontend/src/pages/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx index b64da35..0fb6a88 100644 --- a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx +++ b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/EditFormStatusForm.tsx @@ -6,7 +6,7 @@ import { CoinInput, Timer, TitleInput, -} from "@/features/create-vote/ui/components"; +} from "@/pages/create-vote/ui/components"; import { DialogContext } from "@/shared/components/Dialog"; import { useQueryClient } from "@tanstack/react-query"; import { bettingRoomQueryKey } from "@/shared/lib/bettingRoomInfo"; diff --git a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx rename to frontend/src/pages/waiting-room/ui/WaitingRoomHeader/VotingStatusCard.tsx diff --git a/frontend/src/features/waiting-room/ui/WaitingRoomHeader/index.tsx b/frontend/src/pages/waiting-room/ui/WaitingRoomHeader/index.tsx similarity index 100% rename from frontend/src/features/waiting-room/ui/WaitingRoomHeader/index.tsx rename to frontend/src/pages/waiting-room/ui/WaitingRoomHeader/index.tsx From b068e7de0d95d015627d587e83af494cc769de39 Mon Sep 17 00:00:00 2001 From: sunub Date: Thu, 9 Jan 2025 21:07:50 +0900 Subject: [PATCH 03/42] =?UTF-8?q?refactor=20:=20features=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EC=9E=88=EB=8D=98=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20pages=20=EC=BD=94=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=A5=EB=9D=BD=EC=97=90=20=EB=A7=9E=EA=B2=8C=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 --- frontend/src/routes/__root.tsx | 43 +++---------------- frontend/src/routes/betting.index.tsx | 2 +- .../routes/betting_.$roomId.vote.admin.tsx | 4 +- .../betting_.$roomId.vote.resultDetail.tsx | 4 +- frontend/src/routes/betting_.$roomId.vote.tsx | 12 +++--- .../routes/betting_.$roomId.vote.voting.tsx | 4 +- .../src/routes/betting_.$roomId.waiting.tsx | 12 +++--- frontend/src/routes/create-vote.tsx | 6 +-- frontend/src/routes/login.tsx | 2 +- frontend/src/routes/my-page.tsx | 4 +- frontend/src/routes/require-login.tsx | 6 +-- .../shared/components/RootSideBar/index.tsx | 2 +- frontend/src/shared/lib/auth/guard.ts | 4 ++ frontend/src/shared/lib/bettingRoomInfo.ts | 2 +- frontend/src/shared/lib/validateAccess.ts | 2 +- 15 files changed, 42 insertions(+), 67 deletions(-) diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 2e16d67..33a9ae9 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,54 +1,25 @@ +import { Suspense } from "react"; import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; -import { RootHeader } from "@/shared/components/RootHeader"; -import { RootSideBar } from "@/shared/components/RootSideBar"; import { UserProvider } from "@/app/provider/UserProvider"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; -import { cn } from "@/shared/misc"; -import { useLayout } from "@/shared/hooks/useLayout"; import { LayoutProvider } from "@/app/provider/LayoutProvider"; import { RouterContext } from "@/main"; import { authQueries } from "@/shared/lib/auth/authQuery"; -import React from "react"; import { LoadingAnimation } from "@/shared/components/Loading"; +import { Layout } from "@/app/layout"; export const Route = createRootRouteWithContext()({ component: () => ( - - }> - - - - - + + }> + + + ), loader: (opts) => opts.context.queryClient.ensureQueryData(authQueries), errorComponent: ({ error }) => , }); - -const layoutStyles = { - default: "max-w-[520px]", - wide: "max-w-[1200px]", -} as const; - -function RootLayout({ children }: { children: React.ReactNode }) { - const { layoutType } = useLayout(); - - return ( -
- {children} -
- ); -} - -export { RootLayout }; diff --git a/frontend/src/routes/betting.index.tsx b/frontend/src/routes/betting.index.tsx index a416763..449a3fd 100644 --- a/frontend/src/routes/betting.index.tsx +++ b/frontend/src/routes/betting.index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { WaitingError } from "@/features/waiting-room/ui/WaitingError"; +import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; import { useUserContext } from "@/shared/hooks/useUserContext"; import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"; import { responseBetRoomInfo } from "@betting-duck/shared"; diff --git a/frontend/src/routes/betting_.$roomId.vote.admin.tsx b/frontend/src/routes/betting_.$roomId.vote.admin.tsx index f513e74..0f501a4 100644 --- a/frontend/src/routes/betting_.$roomId.vote.admin.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.admin.tsx @@ -1,6 +1,6 @@ -import { BettingPageAdmin } from "@/features/betting-page-admin"; +import { BettingPageAdmin } from "@/pages/betting-page-admin"; import { createFileRoute, redirect } from "@tanstack/react-router"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; export const Route = createFileRoute("/betting_/$roomId/vote/admin")({ diff --git a/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx b/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx index 4737bff..f469eef 100644 --- a/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.resultDetail.tsx @@ -1,5 +1,5 @@ -import { STORAGE_KEY } from "@/features/betting-page/model/var"; -import { PredictDetail } from "@/features/predict-detail"; +import { STORAGE_KEY } from "@/pages/betting-page/model/var"; +import { PredictDetail } from "@/pages/predict-detail"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; import { getSessionItem } from "@/shared/hooks/useSessionStorage"; import { BettingRoomInfoSchema } from "@/shared/types"; diff --git a/frontend/src/routes/betting_.$roomId.vote.tsx b/frontend/src/routes/betting_.$roomId.vote.tsx index f65df82..17d567e 100644 --- a/frontend/src/routes/betting_.$roomId.vote.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.tsx @@ -3,17 +3,17 @@ import { Outlet, ErrorComponent, } from "@tanstack/react-router"; -import { Chat } from "@/features/chat"; +import { Chat } from "@/pages/chat"; import { cn } from "@/shared/misc"; -import { AccessError } from "@/features/waiting-room/error/AccessError"; -import { Unauthorized } from "@/features/waiting-room/error/Unauthorized"; -import { Forbidden } from "@/features/waiting-room/error/Forbidden"; +import { AccessError } from "@/pages/waiting-room/error/AccessError"; +import { Unauthorized } from "@/pages/waiting-room/error/Unauthorized"; +import { Forbidden } from "@/pages/waiting-room/error/Forbidden"; import { Suspense } from "react"; import { LoadingAnimation } from "@/shared/components/Loading"; import { bettingRoomQueryKey } from "@/shared/lib/bettingRoomInfo"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { BettingProvider } from "@/features/betting-page/provider/BettingProvider"; +import { BettingProvider } from "@/pages/betting-page/provider/BettingProvider"; interface RouteLoaderData { roomId: string; diff --git a/frontend/src/routes/betting_.$roomId.vote.voting.tsx b/frontend/src/routes/betting_.$roomId.vote.voting.tsx index 22010ac..3e2f118 100644 --- a/frontend/src/routes/betting_.$roomId.vote.voting.tsx +++ b/frontend/src/routes/betting_.$roomId.vote.voting.tsx @@ -1,6 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; -import { BettingPage } from "@/features/betting-page"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { BettingPage } from "@/pages/betting-page"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; export const Route = createFileRoute("/betting_/$roomId/vote/voting")({ component: BettingPage, diff --git a/frontend/src/routes/betting_.$roomId.waiting.tsx b/frontend/src/routes/betting_.$roomId.waiting.tsx index 069e963..7472a5e 100644 --- a/frontend/src/routes/betting_.$roomId.waiting.tsx +++ b/frontend/src/routes/betting_.$roomId.waiting.tsx @@ -1,11 +1,11 @@ import { createFileRoute } from "@tanstack/react-router"; -import { WaitingRoom } from "@/features/waiting-room"; -import { WaitingError } from "@/features/waiting-room/ui/WaitingError"; -import { AccessError } from "@/features/waiting-room/error/AccessError"; -import { Forbidden } from "@/features/waiting-room/error/Forbidden"; +import { WaitingRoom } from "@/pages/waiting-room"; +import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; +import { AccessError } from "@/pages/waiting-room/error/AccessError"; +import { Forbidden } from "@/pages/waiting-room/error/Forbidden"; import { ErrorComponent } from "@/shared/components/Error"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; -import { GuestLoginForm } from "@/features/login-page/ui/components"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; +import { GuestLoginForm } from "@/pages/login-page/ui/components"; import { bettingRoomQueryKey } from "@/shared/lib/bettingRoomInfo"; import { getSessionItem, diff --git a/frontend/src/routes/create-vote.tsx b/frontend/src/routes/create-vote.tsx index edfd7f3..8cf5c16 100644 --- a/frontend/src/routes/create-vote.tsx +++ b/frontend/src/routes/create-vote.tsx @@ -1,9 +1,9 @@ -import { CreateVotePage } from "@/features/create-vote"; +import { CreateVotePage } from "@/pages/create-vote"; import { createFileRoute, redirect } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; -import { CreateVoteError } from "@/features/create-vote/ui/error/CreateVoteError"; +import { CreateVoteError } from "@/pages/create-vote/ui/error/CreateVoteError"; import { getSessionItem } from "@/shared/hooks/useSessionStorage"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import { ROUTES } from "@/shared/config/route"; import { authQueries } from "@/shared/lib/auth/authQuery"; import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 23a1475..b993427 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -1,4 +1,4 @@ -import { LoginPage } from "@/features/login-page"; +import { LoginPage } from "@/pages/login-page"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; import { authQueries } from "@/shared/lib/auth/authQuery"; import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; diff --git a/frontend/src/routes/my-page.tsx b/frontend/src/routes/my-page.tsx index 18c14de..887af17 100644 --- a/frontend/src/routes/my-page.tsx +++ b/frontend/src/routes/my-page.tsx @@ -1,7 +1,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; -import { MyPage } from "@/features/my-page"; -import { ErrorMyPage } from "@/features/my-page/error"; +import { MyPage } from "@/pages/my-page"; +import { ErrorMyPage } from "@/pages/my-page/error"; import { ROUTES } from "@/shared/config/route"; export const Route = createFileRoute("/my-page")({ diff --git a/frontend/src/routes/require-login.tsx b/frontend/src/routes/require-login.tsx index 942151a..88f6165 100644 --- a/frontend/src/routes/require-login.tsx +++ b/frontend/src/routes/require-login.tsx @@ -1,10 +1,10 @@ import { createFileRoute } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; -import { CreateVoteError } from "@/features/create-vote/ui/error/CreateVoteError"; +import { CreateVoteError } from "@/pages/create-vote/ui/error/CreateVoteError"; import { z } from "zod"; -import { ErrorMyPage } from "@/features/my-page/error"; +import { ErrorMyPage } from "@/pages/my-page/error"; import { ROUTE_PATH_ENUM, ROUTES } from "@/shared/config/route"; -import { WaitingError } from "@/features/waiting-room/ui/WaitingError"; +import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; import { GuestErrorComponent } from "@/shared/components/Error/GuestError"; const searchSchema = z.object({ diff --git a/frontend/src/shared/components/RootSideBar/index.tsx b/frontend/src/shared/components/RootSideBar/index.tsx index ff21c35..39bf156 100644 --- a/frontend/src/shared/components/RootSideBar/index.tsx +++ b/frontend/src/shared/components/RootSideBar/index.tsx @@ -8,7 +8,7 @@ import { import { NavItem } from "./item"; import styles from "./style.module.css"; import { useLocation } from "@tanstack/react-router"; -import { LogoutButton } from "@/features/login-page/ui/components/Logout"; +import { LogoutButton } from "@/pages/login-page/ui/components/Logout"; import { useQuery } from "@tanstack/react-query"; import { authQueries } from "@/shared/lib/auth/authQuery"; diff --git a/frontend/src/shared/lib/auth/guard.ts b/frontend/src/shared/lib/auth/guard.ts index 974e3cc..fde1168 100644 --- a/frontend/src/shared/lib/auth/guard.ts +++ b/frontend/src/shared/lib/auth/guard.ts @@ -87,6 +87,10 @@ export async function checkAuthStatus(): Promise { credentials: "include", }), fetch("/api/users/userInfo", { + headers: { + "Cache-Control": "stale-while-revalidate", + Pragma: "no-cache", + }, credentials: "include", }), ]); diff --git a/frontend/src/shared/lib/bettingRoomInfo.ts b/frontend/src/shared/lib/bettingRoomInfo.ts index f56707f..9d5ae9b 100644 --- a/frontend/src/shared/lib/bettingRoomInfo.ts +++ b/frontend/src/shared/lib/bettingRoomInfo.ts @@ -1,5 +1,5 @@ import { useSuspenseQuery } from "@tanstack/react-query"; -import { getBettingRoomInfo } from "@/features/betting-page/api/getBettingRoomInfo"; +import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; export const bettingRoomQueryKey = (roomId: string) => ["bettingRoom", roomId]; diff --git a/frontend/src/shared/lib/validateAccess.ts b/frontend/src/shared/lib/validateAccess.ts index 96f972e..a2baa04 100644 --- a/frontend/src/shared/lib/validateAccess.ts +++ b/frontend/src/shared/lib/validateAccess.ts @@ -1,4 +1,4 @@ -import { AccessError } from "@/features/waiting-room/error/AccessError"; +import { AccessError } from "@/pages/waiting-room/error/AccessError"; async function validateAccess(roomId: string, signal: AbortSignal) { try { From 5eb56f62399f0345e4365a7914dae6cc54420e99 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:04:21 +0900 Subject: [PATCH 04/42] =?UTF-8?q?feat=20:=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=A4=84=EC=9D=B4=EA=B8=B0=20=EC=9C=84=ED=95=98?= =?UTF-8?q?=EC=97=AC=20Recoil=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + pnpm-lock.yaml | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 90e5696..bb7bb9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "framer-motion": "^11.12.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "recoil": "^0.7.7", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.5.4", "three": "^0.171.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca83fc5..9440460 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,6 +256,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + recoil: + specifier: ^0.7.7 + version: 0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) socket.io-client: specifier: ^4.8.1 version: 4.8.1 @@ -3192,6 +3195,9 @@ packages: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + hamt_plus@1.0.2: + resolution: {integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==} + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -4465,6 +4471,18 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + recoil@0.7.7: + resolution: {integrity: sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==} + peerDependencies: + react: '>=16.13.1' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -8661,6 +8679,8 @@ snapshots: graphql@16.9.0: {} + hamt_plus@1.0.2: {} + has-bigints@1.0.2: {} has-flag@4.0.0: {} @@ -10026,6 +10046,13 @@ snapshots: dependencies: picomatch: 2.3.1 + recoil@0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + hamt_plus: 1.0.2 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + redis-errors@1.2.0: {} redis-parser@3.0.0: From c795ddd9ef97569e3ae1f95bae835966c293ff16 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:04:53 +0900 Subject: [PATCH 05/42] =?UTF-8?q?feat=20:=20Recoil=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20FSD=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=EB=81=94=20RouterProvider?= =?UTF-8?q?=EC=9D=84=20=EB=B6=84=EB=A6=AC=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main.tsx | 42 ++++++++++-------------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 6da5725..d6b69e3 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,12 +1,11 @@ import "./index.css"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { RouterProvider, createRouter } from "@tanstack/react-router"; -import { routeTree } from "./routeTree.gen"; -import { Auth } from "@/shared/lib/auth/auth"; import { ThemeProvider, createTheme } from "@mui/material/styles"; -import { GlobalErrorComponent } from "./shared/components/Error/GlobalError"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { GlobalRouter } from "./app/provider/RouterProvider"; +import { RecoilRoot } from "recoil"; +import { AuthProvider } from "./app/provider/AuthProvider/AuthProvider"; const queryClient = new QueryClient({ defaultOptions: { @@ -17,39 +16,18 @@ const queryClient = new QueryClient({ }, }); -type RouterContext = { - auth: Auth; - queryClient: QueryClient; -}; - -const router = createRouter({ - routeTree, - context: { - auth: {} as Auth, - queryClient, - } as RouterContext, - defaultPreload: "intent", - defaultErrorComponent: ({ error }) => ( - - ), -}); - -declare module "@tanstack/react-router" { - interface Register { - router: typeof router; - } -} - const theme = createTheme(); createRoot(document.getElementById("root")!).render( - - - + + + + + + + , ); - -export { type RouterContext }; From f10ed42d961a851c6d7b365c13bd6ce86bb7ca2c Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:07:28 +0900 Subject: [PATCH 06/42] =?UTF-8?q?feat=20:=20Recoil=EC=9D=84=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=A0=84=EC=97=AD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9D=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EB=AC=B4,=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EC=9D=98=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EA=B4=80=EB=A6=AC=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EA=B2=8C=EB=81=94=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RouterProvider/RouterProvider.tsx | 38 +++++++++++++++++++ .../src/app/provider/RouterProvider/index.ts | 2 + .../app/provider/RouterProvider/lib/auth.ts | 16 ++++++++ 3 files changed, 56 insertions(+) create mode 100644 frontend/src/app/provider/RouterProvider/RouterProvider.tsx create mode 100644 frontend/src/app/provider/RouterProvider/index.ts create mode 100644 frontend/src/app/provider/RouterProvider/lib/auth.ts diff --git a/frontend/src/app/provider/RouterProvider/RouterProvider.tsx b/frontend/src/app/provider/RouterProvider/RouterProvider.tsx new file mode 100644 index 0000000..7d05e2d --- /dev/null +++ b/frontend/src/app/provider/RouterProvider/RouterProvider.tsx @@ -0,0 +1,38 @@ +import { RouterProvider, createRouter } from "@tanstack/react-router"; +import { routeTree } from "@/routeTree.gen"; +import { Auth, AuthState } from "./lib/auth"; +import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; +import { QueryClient } from "@tanstack/react-query"; +import { RecoilState } from "recoil"; + +type RouterContext = { + auth: RecoilState; + queryClient: QueryClient; +}; + +const createGlobalRouter = (queryClient: QueryClient) => + createRouter({ + routeTree, + context: { + auth: Auth, + queryClient, + } as RouterContext, + defaultPreload: "intent", + defaultErrorComponent: ({ error }) => ( + + ), + }); + +function GlobalRouter({ queryClient }: { queryClient: QueryClient }) { + const router = createGlobalRouter(queryClient); + return ; +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} + +export { GlobalRouter }; +export type { RouterContext }; diff --git a/frontend/src/app/provider/RouterProvider/index.ts b/frontend/src/app/provider/RouterProvider/index.ts new file mode 100644 index 0000000..e3f55bb --- /dev/null +++ b/frontend/src/app/provider/RouterProvider/index.ts @@ -0,0 +1,2 @@ +export { GlobalRouter } from "./RouterProvider"; +export type { RouterContext } from "./RouterProvider"; diff --git a/frontend/src/app/provider/RouterProvider/lib/auth.ts b/frontend/src/app/provider/RouterProvider/lib/auth.ts new file mode 100644 index 0000000..e56f3b1 --- /dev/null +++ b/frontend/src/app/provider/RouterProvider/lib/auth.ts @@ -0,0 +1,16 @@ +import { atom } from "recoil"; + +export type AuthState = { + isAuthenticated: boolean; + nickname?: string; + roomId?: string; +}; + +export const Auth = atom({ + key: "auth", + default: { + isAuthenticated: false, + nickname: undefined, + roomId: undefined, + }, +}); From 1de230867515465f88318919685a87d7aede87ec Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:10:30 +0900 Subject: [PATCH 07/42] =?UTF-8?q?feat=20:=20Recoil=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20auth=20state=EB=A5=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EA=B3=A0=20hook=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=83=81=ED=83=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=9D=84=20=ED=95=98=EA=B2=8C=EB=81=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../login-page/ui/components/GuestLoginForm.tsx | 7 +++++++ .../src/pages/login-page/ui/components/LoginForm.tsx | 8 ++++++++ frontend/src/shared/hooks/useAuthState.ts | 12 ++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 frontend/src/shared/hooks/useAuthState.ts diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx b/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx index 15173ff..f65834e 100644 --- a/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx @@ -8,12 +8,14 @@ import { useQueryClient } from "@tanstack/react-query"; import { authQueries } from "@/shared/lib/auth/authQuery"; import { z } from "zod"; import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useAuthState } from "@/shared/hooks/useAuthState"; function GuestLoginForm({ to, roomId }: { to?: string; roomId?: string }) { const [nickname, setNickname] = useState(""); const { error } = useAuthStore(); const [isSignedUp, setIsSignedUp] = useState(false); const queryClient = useQueryClient(); + const { setAuthState } = useAuthState(); const { setUserInfo } = useUserContext(); const navigate = useNavigate(); @@ -80,6 +82,11 @@ function GuestLoginForm({ to, roomId }: { to?: string; roomId?: string }) { }), ); await queryClient.invalidateQueries({ queryKey: authQueries.queryKey }); + setAuthState((prev) => ({ + ...prev, + isAuthenticated: true, + nickname: data.nickname, + })); if (to && roomId) { window.location.href = `/betting/${roomId}/waiting`; diff --git a/frontend/src/pages/login-page/ui/components/LoginForm.tsx b/frontend/src/pages/login-page/ui/components/LoginForm.tsx index b5e8abc..72da555 100644 --- a/frontend/src/pages/login-page/ui/components/LoginForm.tsx +++ b/frontend/src/pages/login-page/ui/components/LoginForm.tsx @@ -9,6 +9,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { authQueries } from "@/shared/lib/auth/authQuery"; import { z } from "zod"; import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useAuthState } from "@/shared/hooks/useAuthState"; function LoginForm() { const [email, setEmail] = useState(""); @@ -18,6 +19,7 @@ function LoginForm() { const { setUserInfo } = useUserContext(); const { error, handleLogin } = useAuthStore(); const queryClient = useQueryClient(); + const { setAuthState } = useAuthState(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -44,6 +46,12 @@ function LoginForm() { }), ); await queryClient.invalidateQueries({ queryKey: authQueries.queryKey }); + setAuthState((prev) => ({ + ...prev, + isAuthenticated: true, + nickname: data.nickname, + })); + navigate({ to: "/my-page", search: { diff --git a/frontend/src/shared/hooks/useAuthState.ts b/frontend/src/shared/hooks/useAuthState.ts new file mode 100644 index 0000000..7557281 --- /dev/null +++ b/frontend/src/shared/hooks/useAuthState.ts @@ -0,0 +1,12 @@ +import { useRecoilState } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; + +function useAuthState() { + const [authState, setAuthState] = useRecoilState(Auth); + if (!authState) { + throw new Error("AuthState가 존재하지 않습니다."); + } + return { authState, setAuthState }; +} + +export { useAuthState }; From 30057eae8d9053cbc3bd44e3ae5ffeed279e9e01 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:11:00 +0900 Subject: [PATCH 08/42] =?UTF-8?q?refactor=20:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=9D=B8=EC=A6=9D=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=A0=88=EC=B0=A8=20=EC=A0=9C=EA=B1=B0=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/routes/__root.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 33a9ae9..08809f4 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -3,8 +3,7 @@ import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; import { UserProvider } from "@/app/provider/UserProvider"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; import { LayoutProvider } from "@/app/provider/LayoutProvider"; -import { RouterContext } from "@/main"; -import { authQueries } from "@/shared/lib/auth/authQuery"; +import { RouterContext } from "@/app/provider/RouterProvider"; import { LoadingAnimation } from "@/shared/components/Loading"; import { Layout } from "@/app/layout"; @@ -20,6 +19,5 @@ export const Route = createRootRouteWithContext()({ ), - loader: (opts) => opts.context.queryClient.ensureQueryData(authQueries), errorComponent: ({ error }) => , }); From d9a285c6cdc2064c116f4c306980960fa1ca6601 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:11:53 +0900 Subject: [PATCH 09/42] =?UTF-8?q?feat=20:=20Router=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20Client=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=EC=97=90=EC=84=9C=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=ED=9B=84=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=81=94=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/routes/login.tsx | 25 ++++---------- frontend/src/routes/my-page.tsx | 25 +++++--------- .../ProtectedRoute/ui/ProtectedRoute.tsx | 33 +++++++++++++++++++ 3 files changed, 48 insertions(+), 35 deletions(-) create mode 100644 frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index b993427..164ab43 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -1,24 +1,13 @@ import { LoginPage } from "@/pages/login-page"; import { GlobalErrorComponent } from "@/shared/components/Error/GlobalError"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; -import { createFileRoute, redirect } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; export const Route = createFileRoute("/login")({ - component: Component, - loader: async ({ context: { queryClient } }) => { - const queryData = await queryClient.ensureQueryData(authQueries); - const parsedData = AuthStatusTypeSchema.safeParse(queryData); - - if (parsedData.success && parsedData.data.isAuthenticated) { - return redirect({ - to: "/my-page", - }); - } - }, + component: () => ( + + + + ), errorComponent: ({ error }) => , }); - -function Component() { - return ; -} diff --git a/frontend/src/routes/my-page.tsx b/frontend/src/routes/my-page.tsx index 887af17..b8e37bc 100644 --- a/frontend/src/routes/my-page.tsx +++ b/frontend/src/routes/my-page.tsx @@ -1,26 +1,17 @@ -import { createFileRoute, redirect } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; import { MyPage } from "@/pages/my-page"; import { ErrorMyPage } from "@/pages/my-page/error"; -import { ROUTES } from "@/shared/config/route"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; export const Route = createFileRoute("/my-page")({ - beforeLoad: async () => { - const tokenResponse = await fetch("/api/users/token", { - headers: { - "Cache-Control": "stale-while-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }); - if (!tokenResponse.ok) { - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(ROUTES.MYPAGE) }, - }); - } + component: () => { + return ( + + + + ); }, - component: MyPage, shouldReload: () => true, errorComponent: ({ error }) => { return ( diff --git a/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx new file mode 100644 index 0000000..d131b30 --- /dev/null +++ b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx @@ -0,0 +1,33 @@ +import { useRecoilValue } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; +import { useNavigate, useLocation } from "@tanstack/react-router"; +import { useEffect } from "react"; +import { ROUTES } from "@/shared/config/route"; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const authState = useRecoilValue(Auth); + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + if (authState.isAuthenticated && location.pathname == ROUTES.LOGIN) { + navigate({ + to: "/my-page", + search: { from: encodeURIComponent(ROUTES.LOGIN) }, + }); + } + + if (!authState.isAuthenticated && location.pathname == ROUTES.MYPAGE) { + navigate({ + to: "/require-login", + search: { from: encodeURIComponent(ROUTES.MYPAGE) }, + }); + } + }, [authState.isAuthenticated, navigate, location.pathname]); + + return <>{children}; +} From 93c2bea1a939c491b6ce46b86743e9eebb862422 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:12:44 +0900 Subject: [PATCH 10/42] =?UTF-8?q?feat=20:=20=EC=83=88=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=20=EC=8B=9C=20=EC=84=9C=EB=B2=84=EB=A1=9C=20=EB=B6=80?= =?UTF-8?q?=ED=84=B0=20=EC=84=B8=EC=85=98=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=ED=95=98=EC=97=AC=20=EC=A0=84=EC=97=AD?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8A=94=20Aut?= =?UTF-8?q?h=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9C=A0=EC=A7=80=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EA=B2=8C=20Provider=20=EC=B6=94=EA=B0=80=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provider/AuthProvider/AuthProvider.tsx | 27 +++++++++++++++++++ .../src/app/provider/AuthProvider/index.ts | 1 + 2 files changed, 28 insertions(+) create mode 100644 frontend/src/app/provider/AuthProvider/AuthProvider.tsx create mode 100644 frontend/src/app/provider/AuthProvider/index.ts diff --git a/frontend/src/app/provider/AuthProvider/AuthProvider.tsx b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx new file mode 100644 index 0000000..c759c67 --- /dev/null +++ b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx @@ -0,0 +1,27 @@ +import { useEffect } from "react"; +import { Fragment } from "react/jsx-runtime"; +import { useSetRecoilState } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; +import { checkAuthStatus } from "@/shared/lib/auth/guard"; + +function AuthProvider({ children }: { children: React.ReactNode }) { + const setAuthState = useSetRecoilState(Auth); + + useEffect(() => { + const initializeAuth = async () => { + const { isAuthenticated, userInfo } = await checkAuthStatus(); + + setAuthState((prev) => ({ + ...prev, + isAuthenticated, + nickname: userInfo.nickname, + })); + }; + + initializeAuth(); + }, [setAuthState]); + + return {children}; +} + +export { AuthProvider }; diff --git a/frontend/src/app/provider/AuthProvider/index.ts b/frontend/src/app/provider/AuthProvider/index.ts new file mode 100644 index 0000000..842d0c1 --- /dev/null +++ b/frontend/src/app/provider/AuthProvider/index.ts @@ -0,0 +1 @@ +export { AuthProvider } from "./AuthProvider"; From b5ab0e867c7e081e10eb8329976f6cabfcf9618d Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 14 Jan 2025 17:12:59 +0900 Subject: [PATCH 11/42] =?UTF-8?q?feat=20:=20ProtectedRoute=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/shared/components/ProtectedRoute/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/src/shared/components/ProtectedRoute/index.ts diff --git a/frontend/src/shared/components/ProtectedRoute/index.ts b/frontend/src/shared/components/ProtectedRoute/index.ts new file mode 100644 index 0000000..1a219f5 --- /dev/null +++ b/frontend/src/shared/components/ProtectedRoute/index.ts @@ -0,0 +1 @@ +export { ProtectedRoute } from "./ui/ProtectedRoute"; From e830b8e3b107afcaa7a57e30f1ad9dc0e48db2db Mon Sep 17 00:00:00 2001 From: sunub Date: Thu, 16 Jan 2025 23:59:59 +0900 Subject: [PATCH 12/42] =?UTF-8?q?feat=20:=20Test=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 8 +- frontend/playwright.config.ts | 56 +++++ frontend/setupTests.ts | 2 + frontend/vitest.config.ts | 26 ++ pnpm-lock.yaml | 433 +++++++++++++++++++++++++++++++++- 5 files changed, 520 insertions(+), 5 deletions(-) create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/setupTests.ts create mode 100644 frontend/vitest.config.ts diff --git a/frontend/package.json b/frontend/package.json index bb7bb9b..c5b5f42 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "test": "vitest" + "test": "vitest --watch" }, "dependencies": { "@betting-duck/shared": "workspace:*", @@ -37,9 +37,12 @@ }, "devDependencies": { "@eslint/js": "^9.13.0", + "@playwright/test": "^1.49.1", "@tanstack/router-cli": "^1.79.0", "@tanstack/router-devtools": "^1.81.1", "@tanstack/router-plugin": "^1.79.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.2.0", "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -49,6 +52,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", + "jsdom": "^26.0.0", "msw": "^2.6.4", "postcss": "^8.4.48", "prettier": "^3.3.3", @@ -57,7 +61,7 @@ "typescript-eslint": "^8.11.0", "vite": "^5.4.10", "vite-tsconfig-paths": "^5.1.3", - "vitest": "^2.1.4" + "vitest": "^2.1.5" }, "msw": { "workerDirectory": [ diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..6d42e1b --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://127.0.0.1:3000", + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + // webServer: { + // command: "pnpm run preview", + // url: "http://localhost:4173", + // reuseExistingServer: !process.env.CI, + // } +}); diff --git a/frontend/setupTests.ts b/frontend/setupTests.ts new file mode 100644 index 0000000..26ff893 --- /dev/null +++ b/frontend/setupTests.ts @@ -0,0 +1,2 @@ +// vitest.setup.ts +import "@testing-library/react"; diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..0cae3d5 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // Since hooks are running in stack in v2, which means all hooks run serially whereas + // we need to run them in parallel + sequence: { + hooks: "parallel", + }, + setupFiles: ["./setupTests.ts"], + globals: true, + environment: "jsdom", + coverage: { + reporter: ["text", "json-summary", "json", "html", "lcovonly"], + // Since v2, it ignores empty lines by default and we need to disable it as it affects the coverage + // Additionally the thresholds also needs to be updated slightly as a result of this change + ignoreEmptyLines: false, + thresholds: { + lines: 60, + branches: 70, + functions: 63, + statements: 60, + }, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9440460..ccd7e4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,6 +272,9 @@ importers: '@eslint/js': specifier: ^9.13.0 version: 9.14.0 + '@playwright/test': + specifier: ^1.49.1 + version: 1.49.1 '@tanstack/router-cli': specifier: ^1.79.0 version: 1.79.0 @@ -281,6 +284,12 @@ importers: '@tanstack/router-plugin': specifier: ^1.79.0 version: 1.79.0(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))(webpack@5.96.1) + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.0 + '@testing-library/react': + specifier: ^16.2.0 + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/node': specifier: ^22.9.0 version: 22.9.0 @@ -308,6 +317,9 @@ importers: globals: specifier: ^15.11.0 version: 15.12.0 + jsdom: + specifier: ^26.0.0 + version: 26.0.0 msw: specifier: ^2.6.4 version: 2.6.4(@types/node@22.9.0)(typescript@5.6.3) @@ -333,8 +345,8 @@ importers: specifier: ^5.1.3 version: 5.1.3(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) vitest: - specifier: ^2.1.4 - version: 2.1.5(@types/node@22.9.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.36.0) + specifier: ^2.1.5 + version: 2.1.5(@types/node@22.9.0)(jsdom@26.0.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.36.0) shared: dependencies: @@ -370,6 +382,9 @@ packages: resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==} engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@asamuzakjp/css-color@2.8.3': + resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -564,6 +579,34 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.0.1': + resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.1': + resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.7': + resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -1424,6 +1467,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@pmndrs/cannon-worker-api@2.4.0': resolution: {integrity: sha512-oJA1Bboc+WObksRaGDKJG0Wna9Q75xi1MdXVAZ9qXzBOyPsadmAnrmiKOEF0R8v/4zsuJElvscNZmyo3msbZjA==} peerDependencies: @@ -1706,6 +1754,25 @@ packages: resolution: {integrity: sha512-soW+gE9QTmMaqXM17r7y1p8NiQVIIECjdTaYla8BKL5Flj030m3KuxEQoiG1XgjtA0O7ayznFz2YvPcXIy3qDg==} engines: {node: '>=12'} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/react@16.2.0': + resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -1721,6 +1788,9 @@ packages: '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2052,6 +2122,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -2129,6 +2203,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -2542,9 +2619,17 @@ packages: engines: {node: '>=4'} hasBin: true + cssstyle@4.2.1: + resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -2588,6 +2673,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -2630,6 +2718,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -2662,6 +2754,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -2729,6 +2824,10 @@ packages: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -3081,6 +3180,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3244,6 +3348,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3251,6 +3359,14 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3268,6 +3384,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3395,6 +3515,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} @@ -3631,6 +3754,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -3786,6 +3918,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + maath@0.10.8: resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} peerDependencies: @@ -4004,6 +4140,9 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4110,6 +4249,9 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4233,6 +4375,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4369,6 +4521,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4434,6 +4590,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -4559,6 +4718,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -4590,6 +4752,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.21.0: resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} @@ -4858,6 +5024,9 @@ packages: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.9.2: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4950,6 +5119,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.72: + resolution: {integrity: sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g==} + + tldts@6.1.72: + resolution: {integrity: sha512-QNtgIqSUb9o2CoUjX9T5TwaIvUUJFU1+12PJkgt42DFV2yf9J6549yTF2uGloQsJ/JOC8X+gIB81ind97hRiIQ==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4969,9 +5145,17 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@5.1.0: + resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5335,6 +5519,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -5354,6 +5542,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -5375,6 +5567,18 @@ packages: webpack-cli: optional: true + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.1.0: + resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -5434,6 +5638,25 @@ packages: utf-8-validate: optional: true + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@2.1.2: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} @@ -5577,6 +5800,14 @@ snapshots: transitivePeerDependencies: - chokidar + '@asamuzakjp/css-color@2.8.3': + dependencies: + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -5798,6 +6029,26 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.0.1': {} + + '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.1 + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.25.9 @@ -6630,6 +6881,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + '@pmndrs/cannon-worker-api@2.4.0(three@0.171.0)': dependencies: three: 0.171.0 @@ -6910,6 +7165,27 @@ snapshots: '@tanstack/virtual-file-routes@1.64.0': {} + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 10.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -6920,6 +7196,8 @@ snapshots: '@tweenjs/tween.js@23.1.3': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.2 @@ -7349,6 +7627,8 @@ snapshots: acorn@8.14.0: {} + agent-base@7.1.3: {} + ajv-formats@2.1.1(ajv@8.12.0): optionalDependencies: ajv: 8.12.0 @@ -7414,6 +7694,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -7886,8 +8170,18 @@ snapshots: cssesc@3.0.0: {} + cssstyle@4.2.1: + dependencies: + '@asamuzakjp/css-color': 2.8.3 + rrweb-cssom: 0.8.0 + csstype@3.1.3: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + data-view-buffer@1.0.1: dependencies: call-bind: 1.0.7 @@ -7922,6 +8216,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.4.3: {} + dedent@1.5.3(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -7954,6 +8250,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-gpu@5.0.59: @@ -7979,6 +8277,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.26.0 @@ -8052,6 +8352,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + entities@4.5.0: {} + environment@1.1.0: {} error-ex@1.3.2: @@ -8569,6 +8871,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8715,6 +9020,10 @@ snapshots: dependencies: react-is: 16.13.1 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -8725,6 +9034,20 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -8735,6 +9058,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -8880,6 +9207,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} is-regex@1.1.4: @@ -9308,6 +9637,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@26.0.0: + dependencies: + cssstyle: 4.2.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -9466,6 +9823,8 @@ snapshots: dependencies: yallist: 3.1.1 + lz-string@1.5.0: {} + maath@0.10.8(@types/three@0.170.0)(three@0.171.0): dependencies: '@types/three': 0.170.0 @@ -9649,6 +10008,8 @@ snapshots: dependencies: path-key: 4.0.0 + nwsapi@2.2.16: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -9767,6 +10128,10 @@ snapshots: parse5@6.0.1: {} + parse5@7.2.1: + dependencies: + entities: 4.5.0 + parseurl@1.3.3: {} passport-jwt@4.0.1: @@ -9862,6 +10227,14 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.49.1: {} + + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.0.0: {} @@ -9928,6 +10301,12 @@ snapshots: prettier@3.3.3: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -9999,6 +10378,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-reconciler@0.27.0(react@18.3.1): @@ -10134,6 +10515,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.26.0 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-async@2.4.1: {} run-async@3.0.0: {} @@ -10165,6 +10548,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.21.0: dependencies: loose-envify: 1.4.0 @@ -10493,6 +10880,8 @@ snapshots: symbol-observable@4.0.0: {} + symbol-tree@3.2.4: {} + synckit@0.9.2: dependencies: '@pkgr/core': 0.1.1 @@ -10593,6 +10982,12 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@6.1.72: {} + + tldts@6.1.72: + dependencies: + tldts-core: 6.1.72 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -10612,8 +11007,16 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@5.1.0: + dependencies: + tldts: 6.1.72 + tr46@0.0.3: {} + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} troika-three-text@0.52.2(three@0.171.0): @@ -10934,7 +11337,7 @@ snapshots: fsevents: 2.3.3 terser: 5.36.0 - vitest@2.1.5(@types/node@22.9.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.36.0): + vitest@2.1.5(@types/node@22.9.0)(jsdom@26.0.0)(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(terser@5.36.0): dependencies: '@vitest/expect': 2.1.5 '@vitest/mocker': 2.1.5(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) @@ -10958,6 +11361,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.9.0 + jsdom: 26.0.0 transitivePeerDependencies: - less - lightningcss @@ -10969,6 +11373,10 @@ snapshots: - supports-color - terser + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -10988,6 +11396,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-node-externals@3.0.0: {} webpack-sources@3.2.3: {} @@ -11024,6 +11434,17 @@ snapshots: - esbuild - uglify-js + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.1.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -11089,6 +11510,12 @@ snapshots: ws@8.17.1: {} + ws@8.18.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xmlhttprequest-ssl@2.1.2: {} xtend@4.0.2: {} From d0f6d14ce7b5c1d548a9f7209b3ed80cac44a04e Mon Sep 17 00:00:00 2001 From: sunub Date: Fri, 17 Jan 2025 00:00:23 +0900 Subject: [PATCH 13/42] =?UTF-8?q?test=20:=20=EC=9D=B8=EC=A6=9D=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=A6=AC=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EB=A0=89=EC=85=98=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- frontend/tests/e2e/auth-redirect.spec.ts | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 frontend/tests/e2e/auth-redirect.spec.ts diff --git a/frontend/tests/e2e/auth-redirect.spec.ts b/frontend/tests/e2e/auth-redirect.spec.ts new file mode 100644 index 0000000..c12f940 --- /dev/null +++ b/frontend/tests/e2e/auth-redirect.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from "@playwright/test"; + +test("has title", async ({ page }) => { + await page.goto("http://localhost:3000"); + await expect(page).toHaveTitle(/Betting duck/); +}); + +test("권한이 없을 경우 로그인 페이지로 리다이렉트", async ({ page }) => { + await page.goto("http://localhost:3000"); + + await page.getByRole("link", { name: /my/ }).click(); + await expect(page).toHaveURL(/require-login/); + + await page.getByRole("link", { name: /create vote/ }).click(); + await expect(page).toHaveURL(/require-login/); + + await page.getByRole("link", { name: /betting/ }).click(); + await expect(page).toHaveURL(/require-bettingRoomId/); +}); + +test("로그인 후 마이페이지로 리다이렉트", async ({ page }) => { + await page.goto("http://localhost:3000"); + + await page.getByPlaceholder("이메일을 입력해주세요.").fill("abc@naver.com"); + await page.getByPlaceholder("비밀번호를 입력해주세요.").fill("abc1234"); + + await page + .getByRole("button", { name: /로그인/ }) + .last() + .click(); + + await expect(page).toHaveURL(/my-page/); +}); + +test("쿠키 유지 상태에서 탭을 닫았다가 다시 열었을 때 인증 상태 유지 확인", async ({ + browser, +}) => { + const context = await browser.newContext({ acceptDownloads: true }); + const page = await context.newPage(); + + await page.goto("http://localhost:3000"); + + await page.getByPlaceholder("이메일을 입력해주세요.").fill("abc@naver.com"); + await page.getByPlaceholder("비밀번호를 입력해주세요.").fill("abc1234"); + + await page + .getByRole("button", { name: /로그인/ }) + .last() + .click(); + await expect(page).toHaveURL(/my-page/); + + await page.close(); + + const newPage = await context.newPage(); + await newPage.goto("http://localhost:3000"); + + await expect(newPage).not.toHaveURL(/login/); + await expect(newPage).toHaveURL(/my-page/); +}); From 2edc693e90315591e10a4023ad2a82d7f6a91f9f Mon Sep 17 00:00:00 2001 From: sunub Date: Fri, 17 Jan 2025 00:01:03 +0900 Subject: [PATCH 14/42] =?UTF-8?q?feat=20:=20playwright=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.github/workflows/playwright.yml | 27 +++++++++++++++++++++++ frontend/.gitignore | 4 ++++ 2 files changed, 31 insertions(+) create mode 100644 frontend/.github/workflows/playwright.yml diff --git a/frontend/.github/workflows/playwright.yml b/frontend/.github/workflows/playwright.yml new file mode 100644 index 0000000..8116248 --- /dev/null +++ b/frontend/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/frontend/.gitignore b/frontend/.gitignore index 9a0f74f..09f7ef7 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -28,3 +28,7 @@ dist-ssr .env.*.local .env.*.production *.local +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ From 962d0481c1d53d9b56e689fe70b3bcb0937915d1 Mon Sep 17 00:00:00 2001 From: sunub Date: Fri, 17 Jan 2025 00:02:02 +0900 Subject: [PATCH 15/42] =?UTF-8?q?feat=20:=20=EC=A0=84=EC=97=AD=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=9D=B8=20Auth=EB=A5=BC=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=9D=B8=EC=A6=9D=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20=ED=95=98=EA=B2=8C=EB=81=94=20=EB=A1=9C=EC=A7=81=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 --- frontend/src/app/layout/ui/Layout.tsx | 11 +- .../provider/AuthProvider/AuthProvider.tsx | 27 +++-- frontend/src/routes/create-vote.tsx | 107 +++++++++--------- 3 files changed, 74 insertions(+), 71 deletions(-) diff --git a/frontend/src/app/layout/ui/Layout.tsx b/frontend/src/app/layout/ui/Layout.tsx index 3670d54..cb8fd17 100644 --- a/frontend/src/app/layout/ui/Layout.tsx +++ b/frontend/src/app/layout/ui/Layout.tsx @@ -4,8 +4,8 @@ import { RootHeader } from "./RootHeader"; import { RootSideBar } from "./RootSidebar"; import { Suspense } from "react"; import { LoadingAnimation } from "@/shared/components/Loading"; -import { useSuspenseQuery } from "@tanstack/react-query"; -import { authQueries } from "@/shared/lib/auth/authQuery"; +import { useRecoilValue } from "recoil"; +import { Auth } from "@/app/provider/RouterProvider/lib/auth"; const layoutStyles = { default: "max-w-[520px]", @@ -14,10 +14,7 @@ const layoutStyles = { function Layout({ children }: { children: React.ReactNode }) { const { layoutType } = useLayout(); - const { data: authData } = useSuspenseQuery({ - queryKey: authQueries.queryKey, - queryFn: authQueries.queryFn, - }); + const authData = useRecoilValue(Auth); return (
}> - + {children} diff --git a/frontend/src/app/provider/AuthProvider/AuthProvider.tsx b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx index c759c67..da6b8bc 100644 --- a/frontend/src/app/provider/AuthProvider/AuthProvider.tsx +++ b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx @@ -1,27 +1,32 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Fragment } from "react/jsx-runtime"; import { useSetRecoilState } from "recoil"; import { Auth } from "@/app/provider/RouterProvider/lib/auth"; -import { checkAuthStatus } from "@/shared/lib/auth/guard"; +import { useQuery } from "@tanstack/react-query"; +import { authQueries } from "@/shared/lib/auth/authQuery"; +import { LoadingAnimation } from "@/shared/components/Loading"; function AuthProvider({ children }: { children: React.ReactNode }) { + const [isLoading, setIsLoading] = useState(true); const setAuthState = useSetRecoilState(Auth); + const { data } = useQuery({ + queryKey: authQueries.queryKey, + queryFn: authQueries.queryFn, + }); useEffect(() => { - const initializeAuth = async () => { - const { isAuthenticated, userInfo } = await checkAuthStatus(); - + if (data) { setAuthState((prev) => ({ ...prev, - isAuthenticated, - nickname: userInfo.nickname, + isAuthenticated: data.isAuthenticated, + nickname: data.userInfo.nickname, })); - }; + } - initializeAuth(); - }, [setAuthState]); + setIsLoading(false); + }, [data, setAuthState, setIsLoading]); - return {children}; + return {isLoading ? : children}; } export { AuthProvider }; diff --git a/frontend/src/routes/create-vote.tsx b/frontend/src/routes/create-vote.tsx index 8cf5c16..0c92578 100644 --- a/frontend/src/routes/create-vote.tsx +++ b/frontend/src/routes/create-vote.tsx @@ -5,12 +5,52 @@ import { CreateVoteError } from "@/pages/create-vote/ui/error/CreateVoteError"; import { getSessionItem } from "@/shared/hooks/useSessionStorage"; import { getBettingRoomInfo } from "@/pages/betting-page/api/getBettingRoomInfo"; import { ROUTES } from "@/shared/config/route"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; + +type RoomInfo = Awaited>; + +function redirectWaitingRoom(roomInfo: RoomInfo, roomId: string) { + if (roomInfo?.channel.status === "waiting") { + return redirect({ + to: "/betting/$roomId/waiting", + params: { roomId }, + }); + } +} + +function redirectVoting(roomInfo: RoomInfo, roomId: string) { + if (roomInfo?.channel.status === "active") { + return redirect({ + to: "/betting/$roomId/vote/voting", + params: { roomId }, + }); + } +} + +function redirectGuest(role: string) { + if (role === "guest") { + return redirect({ + to: "/require-login", + search: { from: encodeURIComponent(ROUTES.GUEST_CREATE_VOTE) }, + }); + } +} + +function redirectFinished(roomInfo: RoomInfo, roomId: string) { + if ( + roomInfo?.channel.status === "finished" || + roomInfo?.channel.status === "timeover" + ) { + return redirect({ + to: "/betting/$roomId/vote/resultDetail", + params: { roomId }, + }); + } +} export const Route = createFileRoute("/create-vote")({ component: RouteComponent, - beforeLoad: async ({ context: { queryClient } }) => { + beforeLoad: async () => { let roomId; try { const sessionUserInfo = await getSessionItem("userInfo"); @@ -18,60 +58,17 @@ export const Route = createFileRoute("/create-vote")({ roomId = parsedInfo?.roomId; const roomInfo = await getBettingRoomInfo(roomId); if (!roomId) { - return; // roomId가 없으면 조기 반환 + return; } if (!roomInfo) { - return; // roomInfo가 없으면 조기 반환 + return; } - // 상태에 따른 리다이렉트 처리 - if (roomInfo.channel.status === "waiting") { - return redirect({ - to: "/betting/$roomId/waiting", - params: { roomId }, - }); - } - - if (roomInfo.channel.status === "active") { - return redirect({ - to: "/betting/$roomId/vote/voting", - params: { roomId }, - }); - } - - if (parsedInfo.role === "guest") { - return redirect({ - to: "/require-login", - search: { from: encodeURIComponent(ROUTES.GUEST_CREATE_VOTE) }, - }); - } - - if ( - roomInfo.channel.status === "finished" || - roomInfo.channel.status === "timeover" - ) { - return redirect({ - to: "/betting/$roomId/vote/resultDetail", - params: { roomId }, - }); - } - - await queryClient.ensureQueryData(authQueries); - const queryClientData = await queryClient.getQueryData( - authQueries.queryKey, - ); - const parsedQueryClientData = - AuthStatusTypeSchema.safeParse(queryClientData); - if ( - parsedQueryClientData.success && - parsedQueryClientData.data.userInfo.role === "guest" - ) { - return redirect({ - to: "/require-login", - search: { from: encodeURIComponent(ROUTES.GUEST_CREATE_VOTE) }, - }); - } + redirectWaitingRoom(roomInfo, roomId); + redirectVoting(roomInfo, roomId); + redirectGuest(parsedInfo.role); + redirectFinished(roomInfo, roomId); } catch (error) { if (error instanceof Error && error.name === "RedirectError") { throw error; @@ -87,5 +84,9 @@ export const Route = createFileRoute("/create-vote")({ }); function RouteComponent() { - return ; + return ( + + ; + + ); } From 7d7fe65a7cefcf99f40a5b7325ccd61523ee3b44 Mon Sep 17 00:00:00 2001 From: sunub Date: Fri, 17 Jan 2025 00:02:32 +0900 Subject: [PATCH 16/42] =?UTF-8?q?refactor=20:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=EC=97=90=EC=84=9C=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=B3=91=EB=A0=AC=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/shared/lib/auth/guard.ts | 71 +++++++++++++-------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/frontend/src/shared/lib/auth/guard.ts b/frontend/src/shared/lib/auth/guard.ts index fde1168..ede16c1 100644 --- a/frontend/src/shared/lib/auth/guard.ts +++ b/frontend/src/shared/lib/auth/guard.ts @@ -77,49 +77,48 @@ export const AuthStatusTypeSchema = z.object({ }); export async function checkAuthStatus(): Promise { - try { - const [tokenResponse, userInfoResponse] = await Promise.all([ - fetch("/api/users/token", { - headers: { - "Cache-Control": "stale-while-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }), - fetch("/api/users/userInfo", { - headers: { - "Cache-Control": "stale-while-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }), - ]); - - if (!tokenResponse.ok || !userInfoResponse.ok) { - return { - isAuthenticated: false, - userInfo: defaultUserInfo, - }; - } - - const { data } = await userInfoResponse.json(); - const result = responseUserInfoSchema.safeParse(data); + const tokenResponse = await fetch("/api/users/token", { + headers: { + "Cache-Control": "stale-while-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }); + + if (!tokenResponse.ok) { + return { + isAuthenticated: false, + userInfo: defaultUserInfo, + }; + } - if (!result.success) { - return { - isAuthenticated: false, - userInfo: defaultUserInfo, - }; - } + const userInfoResponse = await fetch("/api/users/userInfo", { + headers: { + "Cache-Control": "stale-while-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }); + if (!userInfoResponse.ok) { return { - isAuthenticated: true, - userInfo: result.data, + isAuthenticated: false, + userInfo: defaultUserInfo, }; - } catch { + } + + const { data } = await userInfoResponse.json(); + const result = responseUserInfoSchema.safeParse(data); + + if (!result.success) { return { isAuthenticated: false, userInfo: defaultUserInfo, }; } + + return { + isAuthenticated: true, + userInfo: result.data, + }; } From ded56ce2f0817ac790bf79aab943666af5a31221 Mon Sep 17 00:00:00 2001 From: sunub Date: Fri, 17 Jan 2025 00:03:38 +0900 Subject: [PATCH 17/42] =?UTF-8?q?feat=20:=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=EC=8B=9C=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/routes/betting.index.tsx | 18 ++++----- frontend/src/routes/index.tsx | 40 +++++-------------- .../ProtectedRoute/ui/ProtectedRoute.tsx | 8 +++- 3 files changed, 23 insertions(+), 43 deletions(-) diff --git a/frontend/src/routes/betting.index.tsx b/frontend/src/routes/betting.index.tsx index 449a3fd..d6fc1b7 100644 --- a/frontend/src/routes/betting.index.tsx +++ b/frontend/src/routes/betting.index.tsx @@ -1,19 +1,12 @@ import React from "react"; import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; import { useUserContext } from "@/shared/hooks/useUserContext"; -import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { responseBetRoomInfo } from "@betting-duck/shared"; +import { ProtectedRoute } from "@/shared/components/ProtectedRoute"; export const Route = createFileRoute("/betting/")({ component: RouteComponent, - beforeLoad: async () => { - const response = await fetch("/api/users/token"); - if (!response.ok) { - throw redirect({ - to: "/require-bettingRoomId", - }); - } - }, errorComponent: ({ error }) => ( @@ -49,7 +42,6 @@ function RouteComponent() { const { channel } = result.data; - // 상태에 따른 리다이렉트 처리 const redirectMap = { finished: "/require-bettingRoomId", waiting: `/betting/${roomId}/waiting`, @@ -79,7 +71,11 @@ function RouteComponent() { }, [roomId, navigate]); // 로딩 중 표시 - return
; + return ( + +
+ + ); } function ErrorComponent({ diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 0d77680..f1b0271 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,40 +1,18 @@ -import { - Dialog, - DialogTrigger, - DialogContent, -} from "@/shared/components/Dialog"; +import { authQueries } from "@/shared/lib/auth/authQuery"; import { createFileRoute, redirect } from "@tanstack/react-router"; -const user = undefined; - export const Route = createFileRoute("/")({ - loader: () => { - if (!user) { + beforeLoad: ({ context }) => { + const { queryClient } = context; + const auth = queryClient.getQueryData(authQueries.queryKey); + if (!auth) { throw redirect({ to: "/login", }); } + + throw redirect({ + to: "/my-page", + }); }, - component: () => ( - - - - - -
-

기본 Dialog

-
-
-

Dialog 내용이 여기에 들어갑니다.

-
-
- -
-
-
- ), }); diff --git a/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx index d131b30..6014610 100644 --- a/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx +++ b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx @@ -17,7 +17,6 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) { if (authState.isAuthenticated && location.pathname == ROUTES.LOGIN) { navigate({ to: "/my-page", - search: { from: encodeURIComponent(ROUTES.LOGIN) }, }); } @@ -27,6 +26,13 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) { search: { from: encodeURIComponent(ROUTES.MYPAGE) }, }); } + + if (!authState.isAuthenticated && location.pathname == ROUTES.CREATE_VOTE) { + navigate({ + to: "/require-login", + search: { from: encodeURIComponent(ROUTES.CREATE_VOTE) }, + }); + } }, [authState.isAuthenticated, navigate, location.pathname]); return <>{children}; From 832f0025c31650657d5992d52cf1de68d1b42852 Mon Sep 17 00:00:00 2001 From: sunub Date: Fri, 17 Jan 2025 00:04:03 +0900 Subject: [PATCH 18/42] =?UTF-8?q?refactor=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=A7=84=ED=96=89=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20nav=20=EB=B2=84=ED=8A=BC=EB=93=A4=EC=97=90?= =?UTF-8?q?=20id=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/layout/ui/RootSidebar.tsx | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/layout/ui/RootSidebar.tsx b/frontend/src/app/layout/ui/RootSidebar.tsx index 7945efb..9cc6efd 100644 --- a/frontend/src/app/layout/ui/RootSidebar.tsx +++ b/frontend/src/app/layout/ui/RootSidebar.tsx @@ -11,6 +11,7 @@ import { useLocation } from "@tanstack/react-router"; import { LogoutButton } from "@/pages/login-page/ui/components/Logout"; type NavItemType = { + id: string; icon: () => JSX.Element; label: string; href: string; @@ -19,11 +20,28 @@ type NavItemType = { const navItems = { top: [ - { icon: UserIcon, label: "my", href: "/my-page" }, - { icon: CreateVoteIcon, label: "create vote", href: "/create-vote" }, - { icon: WaitingRoomIcon, label: "betting", href: "/betting" }, + { icon: UserIcon, id: "my-page-navigation", label: "my", href: "/my-page" }, + { + icon: CreateVoteIcon, + id: "create-vote-page-navigation", + label: "create vote", + href: "/create-vote", + }, + { + icon: WaitingRoomIcon, + id: "betting-room-navigation", + label: "betting", + href: "/betting", + }, + ], + login: [ + { + icon: LoginIcon, + id: "login-page-navigation", + label: "login", + href: "/login", + }, ], - login: [{ icon: LoginIcon, label: "login", href: "/login" }], }; function changeNavigatorPosition(href: string) { @@ -47,8 +65,8 @@ function changeNavigatorPosition(href: string) { function NavItems({ items }: { items: NavItemType[] }) { return ( ); From f2d99b960a10f0a1d7e93e7e954bab8c5b670ff0 Mon Sep 17 00:00:00 2001 From: sunub Date: Fri, 17 Jan 2025 00:04:25 +0900 Subject: [PATCH 19/42] =?UTF-8?q?refactor=20:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20Hook?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B8=B0=EB=8A=A5=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/login-page/ui/components/Logout.tsx | 71 +------------------ frontend/src/shared/hooks/useLogout.ts | 66 +++++++++++++++++ 2 files changed, 69 insertions(+), 68 deletions(-) create mode 100644 frontend/src/shared/hooks/useLogout.ts diff --git a/frontend/src/pages/login-page/ui/components/Logout.tsx b/frontend/src/pages/login-page/ui/components/Logout.tsx index 1a32659..9be9a1b 100644 --- a/frontend/src/pages/login-page/ui/components/Logout.tsx +++ b/frontend/src/pages/login-page/ui/components/Logout.tsx @@ -1,77 +1,12 @@ -import { useUserContext } from "@/shared/hooks/useUserContext"; import { LogoutIcon } from "@/shared/icons/Logout"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; -import { useQueryClient } from "@tanstack/react-query"; -import { useNavigate } from "@tanstack/react-router"; -import { z } from "zod"; - -function logout() { - // 모든 가능한 조합으로 쿠키 삭제 시도 - const cookieSettings = [ - // 기본 설정 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/", - // 모든 path 조합 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/api", - // httpOnly가 아닌 경우를 위한 설정 - "access_token=; max-age=-99999999; path=/", - // 도메인 설정 - `access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${window.location.hostname}`, - // subdomain을 포함한 도메인 설정 - `access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname}`, - // Secure 옵션 포함 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure", - // SameSite 옵션들 포함 - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=strict", - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=lax", - "access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=none", - ]; - - // 모든 설정 조합 적용 - cookieSettings.forEach((setting) => { - document.cookie = setting; - }); -} +import { useLogout } from "@/shared/hooks/useLogout"; function LogoutButton() { - const navigate = useNavigate(); - const { setUserInfo } = useUserContext(); - const queryClient = useQueryClient(); + const logout = useLogout(); return (
-
-
- ); -} - -export { MyPage }; diff --git a/frontend/src/pages/my-page/ui/AnimatedDuckCount.tsx b/frontend/src/pages/my-page/ui/AnimatedDuckCount.tsx index 5daddad..6608b68 100644 --- a/frontend/src/pages/my-page/ui/AnimatedDuckCount.tsx +++ b/frontend/src/pages/my-page/ui/AnimatedDuckCount.tsx @@ -1,21 +1,13 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, memo } from "react"; import { DuckCoinIcon } from "@/shared/icons"; - -interface DuckCoinIconProps { - width: number; - height: number; -} +import { useSuspenseQuery } from "@tanstack/react-query"; +import { userInfoQueries } from "@/shared/lib/auth/authQuery"; interface AnimatedDigitProps { digit: string; shouldAnimate: boolean; } -interface AnimatedDuckCountProps { - value: number; - DuckCoinIcon: React.ComponentType; -} - const AnimatedDigit: React.FC = ({ digit, shouldAnimate, @@ -32,8 +24,12 @@ const AnimatedDigit: React.FC = ({ ); }; -const AnimatedDuckCount: React.FC = ({ value }) => { - const [prevValue, setPrevValue] = useState(value); +const AnimatedDuckCount = memo(() => { + const { data: authData } = useSuspenseQuery({ + queryKey: userInfoQueries.queryKey, + queryFn: userInfoQueries.queryFn, + }); + const [prevValue, setPrevValue] = useState(authData.duck); const [animatingDigits, setAnimatingDigits] = useState>( new Set(), ); @@ -43,9 +39,9 @@ const AnimatedDuckCount: React.FC = ({ value }) => { }; useEffect(() => { - if (value !== prevValue) { + if (authData.duck !== prevValue) { const prevDigits = getDigits(prevValue); - const newDigits = getDigits(value); + const newDigits = getDigits(authData.duck); const changedPositions = new Set(); for (let i = newDigits.length - 1; i >= 0; i--) { @@ -61,14 +57,14 @@ const AnimatedDuckCount: React.FC = ({ value }) => { const timer = setTimeout(() => { setAnimatingDigits(new Set()); - setPrevValue(value); + setPrevValue(authData.duck); }, 500); return () => clearTimeout(timer); } - }, [value, prevValue]); + }, [authData, prevValue]); - const digits = getDigits(value); + const digits = getDigits(authData.duck); return (
@@ -84,6 +80,6 @@ const AnimatedDuckCount: React.FC = ({ value }) => {
); -}; +}); export { AnimatedDuckCount }; diff --git a/frontend/src/pages/my-page/ui/FallingDuck.tsx b/frontend/src/pages/my-page/ui/FallingDuck.tsx index a1315a7..b5f43d8 100644 --- a/frontend/src/pages/my-page/ui/FallingDuck.tsx +++ b/frontend/src/pages/my-page/ui/FallingDuck.tsx @@ -1,21 +1,17 @@ -import * as THREE from "three"; +import { Group } from "three"; import { useBox } from "@react-three/cannon"; import { Gltf } from "@react-three/drei"; import duckModel from "@/assets/models/betting-duck.glb"; +import { memo } from "react"; -function FallingDuck() { - // 약간의 랜덤 오프셋 추가 (x축과 z축에 대해) - const randomX = (Math.random() - 0.5) * 2; // -1 ~ 1 사이의 랜덤값 - const randomZ = (Math.random() - 0.5) * 2; // -1 ~ 1 사이의 랜덤값 +const FallingDuck = memo(() => { + const randomX = (Math.random() - 0.5) * 2; + const randomZ = (Math.random() - 0.5) * 2; const [ref] = useBox(() => ({ mass: 1, - position: [randomX, 5, randomZ], // 초기 위치에 랜덤값 적용 - rotation: [ - Math.random() * 0.2, // 약간의 랜덤 회전도 추가 - Math.random() * 0.2, - Math.random() * 0.2, - ], + position: [randomX, 5, randomZ], + rotation: [Math.random() * 0.2, Math.random() * 0.2, Math.random() * 0.2], linearDamping: 0.4, angularDamping: 0.4, material: { @@ -26,12 +22,12 @@ function FallingDuck() { return ( } + ref={ref as React.RefObject} castShadow receiveShadow src={duckModel} /> ); -} +}); export { FallingDuck }; diff --git a/frontend/src/pages/my-page/ui/MyPage.tsx b/frontend/src/pages/my-page/ui/MyPage.tsx new file mode 100644 index 0000000..7d37c01 --- /dev/null +++ b/frontend/src/pages/my-page/ui/MyPage.tsx @@ -0,0 +1,72 @@ +import { Suspense, lazy } from "react"; +import { DuckCoinIcon } from "@/shared/icons"; +import { useNavigate } from "@tanstack/react-router"; +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { userInfoQueries } from "@/shared/lib/auth/authQuery"; +import { AnimatedDuckCount } from "./AnimatedDuckCount"; +import { updateQueryClient } from "@/shared/lib/updateQueryClient"; + +const Pond = lazy(() => import("./Pond")); + +function MyPage() { + const { data: authData } = useSuspenseQuery({ + queryKey: userInfoQueries.queryKey, + queryFn: userInfoQueries.queryFn, + }); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + + async function purchaseDuck() { + if (authData.duck < 30) { + return; + } + + const response = await fetch("/api/users/purchaseduck", { + cache: "no-cache", + }); + if (!response.ok) { + console.error("Failed to purchase duck"); + return; + } + updateQueryClient(queryClient, userInfoQueries.queryKey, (prev) => ({ + ...prev, + duck: prev.userInfo.duck - 30, + realDuck: prev.userInfo.realDuck + 1, + })); + } + + return ( +
+
+
+

마이 페이지

+

오리를 구매해서 페이지를 꾸며보세요

+
+ + +
+ +
+
+
+ + +
+
+
+ ); +} + +export { MyPage }; diff --git a/frontend/src/pages/my-page/ui/Pond.tsx b/frontend/src/pages/my-page/ui/Pond.tsx index bd57380..7266004 100644 --- a/frontend/src/pages/my-page/ui/Pond.tsx +++ b/frontend/src/pages/my-page/ui/Pond.tsx @@ -1,4 +1,4 @@ -import * as THREE from "three"; +import { Mesh } from "three"; import { Canvas } from "@react-three/fiber"; import { OrbitControls, @@ -6,11 +6,14 @@ import { OrthographicCamera, } from "@react-three/drei"; import { Physics, usePlane } from "@react-three/cannon"; -import { Suspense } from "react"; +import { memo, Suspense, useCallback, useEffect, useState } from "react"; import envMap from "@assets/models/industrial_sunset_puresky_4k.hdr"; +import { FallingDuck } from "./FallingDuck"; +import { z } from "zod"; +import { responseUserInfoSchema } from "@betting-duck/shared"; -function Ground({ color = "#f0f4fa" }: { color?: string }) { - const [ref] = usePlane(() => ({ +const Ground = memo(() => { + const [ref] = usePlane(() => ({ rotation: [-Math.PI / 2, 0, 0], position: [0, -6.5, 0], type: "Static", @@ -23,12 +26,42 @@ function Ground({ color = "#f0f4fa" }: { color?: string }) { return ( - + ); -} +}); + +function Pond({ + authData, +}: { + authData: z.infer; +}) { + const [duckModels, setDuckModels] = useState([FallingDuck]); + + const addDuck = useCallback((count: number, remainDucks: number) => { + if (count >= remainDucks) return; + const timer = setTimeout(() => { + setDuckModels((prevDucks) => [...prevDucks, FallingDuck]); + addDuck(count + 1, remainDucks); + }, 200); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + const remainDucks = authData.realDuck - duckModels.length; + if (remainDucks <= 0) return; + + const initialTimer = setTimeout(() => { + addDuck(0, remainDucks); + }, 1000); + + return () => clearTimeout(initialTimer); + }, [authData, duckModels.length, addDuck]); -function Pond({ ducks }: { ducks: React.ElementType[] }) { return ( - {ducks.map((DuckComponent, index) => ( + {duckModels.map((DuckComponent, index) => ( ))} - + @@ -84,4 +117,4 @@ function Pond({ ducks }: { ducks: React.ElementType[] }) { ); } -export { Pond }; +export default Pond; From 31bf3c207ff145de222030e71f9c3062ad89bb8b Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:14:21 +0900 Subject: [PATCH 28/42] =?UTF-8?q?fix=20:=20=EC=95=84=EC=9D=B4=EB=94=94=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=8B=9C=20=EA=B3=84=EC=86=8D=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=9D=B4=20=EC=A0=84=EC=86=A1=EB=90=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/login-page/ui/LoginPage.tsx | 2 +- .../ui/components/GuestLoginForm.tsx | 147 ------------------ .../GuestLoginForm/api/checkSignupStatus.ts | 30 ++++ .../GuestLoginForm/api/handleGuestSignin.ts | 18 +++ .../ui/components/GuestLoginForm/index.ts | 1 + .../GuestLoginForm/ui/GuestLoginForm.tsx | 85 ++++++++++ .../ui/components/GuestLoginForm/ui/Input.tsx | 39 +++++ 7 files changed, 174 insertions(+), 148 deletions(-) delete mode 100644 frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx create mode 100644 frontend/src/pages/login-page/ui/components/GuestLoginForm/api/checkSignupStatus.ts create mode 100644 frontend/src/pages/login-page/ui/components/GuestLoginForm/api/handleGuestSignin.ts create mode 100644 frontend/src/pages/login-page/ui/components/GuestLoginForm/index.ts create mode 100644 frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/GuestLoginForm.tsx create mode 100644 frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/Input.tsx diff --git a/frontend/src/pages/login-page/ui/LoginPage.tsx b/frontend/src/pages/login-page/ui/LoginPage.tsx index 35f92ea..5807cd1 100644 --- a/frontend/src/pages/login-page/ui/LoginPage.tsx +++ b/frontend/src/pages/login-page/ui/LoginPage.tsx @@ -1,6 +1,6 @@ import { cn } from "@/shared/misc"; import React, { useState } from "react"; -import mainLogo from "@/assets/images/main-logo.png"; +import mainLogo from "@/assets/images/main-logo.avif"; import { GuestLoginForm, LoginForm, diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx b/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx deleted file mode 100644 index f65834e..0000000 --- a/frontend/src/pages/login-page/ui/components/GuestLoginForm.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { LoginIDIcon } from "@/shared/icons"; -import { useEffect, useState } from "react"; -import { useAuthStore } from "../../model/store"; -import { Warning } from "./Warning"; -import { useNavigate } from "@tanstack/react-router"; -import { useUserContext } from "@/shared/hooks/useUserContext"; -import { useQueryClient } from "@tanstack/react-query"; -import { authQueries } from "@/shared/lib/auth/authQuery"; -import { z } from "zod"; -import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; -import { useAuthState } from "@/shared/hooks/useAuthState"; - -function GuestLoginForm({ to, roomId }: { to?: string; roomId?: string }) { - const [nickname, setNickname] = useState(""); - const { error } = useAuthStore(); - const [isSignedUp, setIsSignedUp] = useState(false); - const queryClient = useQueryClient(); - const { setAuthState } = useAuthState(); - - const { setUserInfo } = useUserContext(); - const navigate = useNavigate(); - - useEffect(() => { - const checkSignupStatus = async () => { - try { - const response = await fetch("/api/users/guestloginactivity", { - method: "GET", - }); - - if (!response.ok) { - throw new Error("회원가입 상태 확인에 실패했습니다."); - } - - const result = await response.json(); - const isSigned = result.data.loggedInBefore; - setIsSignedUp(isSigned); - if (isSigned) { - setNickname(result.data.nickname.replace(/^익명의 /, "")); - } - } catch (error) { - if (error instanceof Error) { - console.error("오류 발생:", error.message); - } else { - console.error("알 수 없는 오류 발생:", error); - } - } - }; - - checkSignupStatus(); - }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - // const result = await handleGuestLogin({ nickname: `익명의 ${nickname}` }); - try { - const response = await fetch("/api/users/guestsignin", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ nickname: `익명의 ${nickname}` }), - }); - if (!response.ok) throw new Error("게스트 로그인에 실패했습니다."); - const { data } = await response.json(); - setUserInfo({ - role: "guest", - nickname: data.nickname, - isAuthenticated: true, - }); - - queryClient.setQueryData( - authQueries.queryKey, - (prev: z.infer) => ({ - ...prev, - userInfo: { - role: "guest", - nickname: data.nickname, - duck: 0, - message: "OK", - realDuck: prev.userInfo.realDuck, - }, - }), - ); - await queryClient.invalidateQueries({ queryKey: authQueries.queryKey }); - setAuthState((prev) => ({ - ...prev, - isAuthenticated: true, - nickname: data.nickname, - })); - - if (to && roomId) { - window.location.href = `/betting/${roomId}/waiting`; - } - - navigate({ - to: "/my-page", - search: { - nickname: decodeURIComponent(nickname), - }, - }); - } catch (error) { - console.error("게스트 로그인에 실패했습니다.", error); - } - }; - return ( -
-
-
- -
-
- {isSignedUp && ( - - )} - {error && } - - - ); -} - -export { GuestLoginForm }; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/checkSignupStatus.ts b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/checkSignupStatus.ts new file mode 100644 index 0000000..e531830 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/checkSignupStatus.ts @@ -0,0 +1,30 @@ +async function checkSignupStatus( + setIsSignedUp: React.Dispatch>, + setAssignednickname: React.Dispatch>, +) { + try { + const response = await fetch("/api/users/guestloginactivity", { + method: "GET", + cache: "no-cache", + }); + + if (!response.ok) { + throw new Error("회원가입 상태 확인에 실패했습니다."); + } + + const result = await response.json(); + const isSigned = result.data.loggedInBefore; + setIsSignedUp(isSigned); + if (isSigned) { + setAssignednickname(result.data.nickname.replace(/^익명의 /, "")); + } + } catch (error) { + if (error instanceof Error) { + console.error("오류 발생:", error.message); + } else { + console.error("알 수 없는 오류 발생:", error); + } + } +} + +export { checkSignupStatus }; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/handleGuestSignin.ts b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/handleGuestSignin.ts new file mode 100644 index 0000000..e687f34 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/api/handleGuestSignin.ts @@ -0,0 +1,18 @@ +async function handleGuestSignin(nickname: string) { + try { + const response = await fetch("/api/users/guestsignin", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ nickname: `익명의 ${nickname}` }), + }); + if (!response.ok) throw new Error("게스트 로그인에 실패했습니다."); + const { data } = await response.json(); + return data; + } catch (error) { + console.error("게스트 로그인에 실패했습니다.", error); + } +} + +export { handleGuestSignin }; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/index.ts b/frontend/src/pages/login-page/ui/components/GuestLoginForm/index.ts new file mode 100644 index 0000000..010e31c --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/index.ts @@ -0,0 +1 @@ +export { GuestLoginForm } from "./ui/GuestLoginForm"; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/GuestLoginForm.tsx b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/GuestLoginForm.tsx new file mode 100644 index 0000000..5f95f57 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/GuestLoginForm.tsx @@ -0,0 +1,85 @@ +import { useEffect, useRef, useState } from "react"; +import { useAuthStore } from "../../../../model/store"; +import { Warning } from "../../Warning"; +import { useNavigate } from "@tanstack/react-router"; +import { useQueryClient } from "@tanstack/react-query"; +import { authQueries } from "@/shared/lib/auth/authQuery"; +import { useAuthState } from "@/shared/hooks/useAuthState"; +import { Input } from "./Input"; +import { checkSignupStatus } from "../api/checkSignupStatus"; +import { handleGuestSignin } from "../api/handleGuestSignin"; +import { updateQueryClient } from "@/shared/lib/updateQueryClient"; + +function GuestLoginForm({ to, roomId }: { to?: string; roomId?: string }) { + const [assignednickname, setAssignednickname] = useState(""); + const { error } = useAuthStore(); + const [issignedup, setIssignedup] = useState(false); + const queryClient = useQueryClient(); + const { setAuthState } = useAuthState(); + const inputRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + checkSignupStatus(setIssignedup, setAssignednickname); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + if (!inputRef.current) return; + e.preventDefault(); + const nickname = inputRef.current?.value; + const response = await handleGuestSignin(`익명의 ${nickname}`); + if (!response.ok) { + console.error("게스트 로그인에 실패했습니다."); + return; + } + updateQueryClient(queryClient, authQueries.queryKey, (prev) => ({ + ...prev, + userInfo: { + role: "guest", + nickname: nickname, + duck: 0, + message: "OK", + realDuck: prev.userInfo.realDuck, + }, + })); + + setAuthState((prev) => ({ + ...prev, + isAuthenticated: true, + nickname: nickname, + })); + + if (to && roomId) { + window.location.href = `/betting/${roomId}/waiting`; + } + + navigate({ + to: "/my-page", + }); + }; + return ( +
+
+
+ +
+
+ {issignedup && ( + + )} + {error && } + + + ); +} + +export { GuestLoginForm }; diff --git a/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/Input.tsx b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/Input.tsx new file mode 100644 index 0000000..bc8a211 --- /dev/null +++ b/frontend/src/pages/login-page/ui/components/GuestLoginForm/ui/Input.tsx @@ -0,0 +1,39 @@ +// Input.tsx +import { forwardRef, useState } from "react"; +import { LoginIDIcon } from "@/shared/icons"; + +interface InputProps extends React.InputHTMLAttributes { + assignednickname: string; + issignedup: boolean; +} + +const Input = forwardRef((props, ref) => { + const { assignednickname, issignedup, ...rest } = props; + const [nickname, setNickname] = useState(assignednickname); + + return ( + + ); +}); + +export { Input }; From 288594eea9305a865ffed49a8ed8633af45ca1fd Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:15:39 +0900 Subject: [PATCH 29/42] =?UTF-8?q?refactor=20:=20=EB=B0=94=EB=80=90=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8F=AC=EB=A7=B7=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/waiting-room/ui/ParticipantsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/waiting-room/ui/ParticipantsList.tsx b/frontend/src/pages/waiting-room/ui/ParticipantsList.tsx index 4af9640..d060b56 100644 --- a/frontend/src/pages/waiting-room/ui/ParticipantsList.tsx +++ b/frontend/src/pages/waiting-room/ui/ParticipantsList.tsx @@ -1,4 +1,4 @@ -import waitingUserImage from "@assets/images/waiting-user.png"; +import waitingUserImage from "@assets/images/waiting-user.avif"; import React from "react"; import { useWaitingContext } from "../hooks/use-waiting-context"; import { z } from "zod"; From 25084077bf0de08836e9f68c1e9f48dbb477b714 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:16:32 +0900 Subject: [PATCH 30/42] =?UTF-8?q?feat=20:=20=EB=B3=84=EB=8F=84=EC=9D=98=20?= =?UTF-8?q?query=20Key=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EC=97=90=20=EB=AC=B6=EC=97=AC=20=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/shared/lib/auth/authQuery.ts | 10 ++- frontend/src/shared/lib/auth/guard.ts | 78 +++++++---------------- 2 files changed, 31 insertions(+), 57 deletions(-) diff --git a/frontend/src/shared/lib/auth/authQuery.ts b/frontend/src/shared/lib/auth/authQuery.ts index edef892..0af4d31 100644 --- a/frontend/src/shared/lib/auth/authQuery.ts +++ b/frontend/src/shared/lib/auth/authQuery.ts @@ -1,7 +1,8 @@ -import { checkAuthStatus } from "./guard"; +import { checkAuthStatus, getUserInfo } from "./guard"; import { QueryFunction } from "@tanstack/react-query"; import { AuthStatusTypeSchema } from "./guard"; import { z } from "zod"; +import { responseUserInfoSchema } from "@betting-duck/shared"; const authQueries = { queryKey: ["auth"], @@ -12,4 +13,9 @@ const authQueries = { staleTime: 1000 * 60 * 60, }; -export { authQueries }; +const userInfoQueries = { + queryKey: ["auth", "userInfos"], + queryFn: getUserInfo as QueryFunction>, +}; + +export { authQueries, userInfoQueries }; diff --git a/frontend/src/shared/lib/auth/guard.ts b/frontend/src/shared/lib/auth/guard.ts index ede16c1..a7a6f0f 100644 --- a/frontend/src/shared/lib/auth/guard.ts +++ b/frontend/src/shared/lib/auth/guard.ts @@ -1,61 +1,6 @@ import { responseUserInfoSchema } from "@betting-duck/shared"; -import { redirect } from "@tanstack/react-router"; import { z } from "zod"; -export async function requireAuth({ to }: { to: string }) { - try { - const [tokenResponse, userInfoResponse] = await Promise.all([ - fetch("/api/users/token", { credentials: "include" }), - fetch("/api/users/userInfo", { credentials: "include" }), - ]); - - if (!tokenResponse.ok) { - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(to) }, - }); - } - - if (!userInfoResponse.ok) { - throw new Error("사용자 정보를 불러오는데 실패했습니다."); - } - - const { data } = await userInfoResponse.json(); - const userInfo = responseUserInfoSchema.safeParse(data); - - if (!userInfo.success) { - throw new Error("사용자 정보를 파싱하는데 실패했습니다."); - } - - return userInfo.data; - } catch (error) { - console.error(error); - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(to) }, - }); - } -} - -export async function requireUesrRole({ - userInfo, - to, - role, -}: { - userInfo: z.infer; - to: string; - role: "user" | "admin" | "guest"; -}) { - if (role === "guest") { - throw redirect({ - to: "/require-login", - search: { from: encodeURIComponent(to) }, - }); - } - - return userInfo; -} - export type UserInfoType = z.infer; type AuthStatusType = { @@ -122,3 +67,26 @@ export async function checkAuthStatus(): Promise { userInfo: result.data, }; } + +export async function getUserInfo() { + const userInfoResponse = await fetch("/api/users/userInfo", { + headers: { + "Cache-Control": "stale-while-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }); + + if (!userInfoResponse.ok) { + return defaultUserInfo; + } + + const { data } = await userInfoResponse.json(); + const result = responseUserInfoSchema.safeParse(data); + + if (!result.success) { + return defaultUserInfo; + } + + return result.data; +} From d9a7100c1496bfbd32ae834c73baae4f2912d307 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:17:21 +0900 Subject: [PATCH 31/42] =?UTF-8?q?refactor=20:=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EA=B0=80=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=90=98=EC=97=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EC=A6=89?= =?UTF-8?q?=EC=8B=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EB=90=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20=EB=94=9C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EA=B0=80=20=EC=83=9D=EA=B8=B0=EA=B2=8C?= =?UTF-8?q?=EB=81=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/shared/components/Error/index.tsx | 8 +++++--- .../components/ProtectedRoute/ui/ProtectedRoute.tsx | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/shared/components/Error/index.tsx b/frontend/src/shared/components/Error/index.tsx index 2fc2c23..ab7019d 100644 --- a/frontend/src/shared/components/Error/index.tsx +++ b/frontend/src/shared/components/Error/index.tsx @@ -1,3 +1,5 @@ +import { useNavigate } from "@tanstack/react-router"; + function ErrorComponent({ error, to, @@ -9,6 +11,8 @@ function ErrorComponent({ children: React.ReactNode; feature?: string; }) { + const navigate = useNavigate(); + return (
diff --git a/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx index 6014610..f12bc10 100644 --- a/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx +++ b/frontend/src/shared/components/ProtectedRoute/ui/ProtectedRoute.tsx @@ -14,6 +14,8 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) { const location = useLocation(); useEffect(() => { + if (authState.isLoading) return; + if (authState.isAuthenticated && location.pathname == ROUTES.LOGIN) { navigate({ to: "/my-page", @@ -33,7 +35,12 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) { search: { from: encodeURIComponent(ROUTES.CREATE_VOTE) }, }); } - }, [authState.isAuthenticated, navigate, location.pathname]); + }, [ + authState.isAuthenticated, + authState.isLoading, + navigate, + location.pathname, + ]); return <>{children}; } From 947c6b86a25686f88ad8a640da7d345c57fae264 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:17:46 +0900 Subject: [PATCH 32/42] =?UTF-8?q?feat=20:=20=EC=A4=91=EB=B3=B5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EA=B2=8C=EB=81=94=20=EC=BD=94=EB=93=9C=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 --- frontend/src/shared/hooks/useLogout.ts | 39 +++++++++++--------- frontend/src/shared/lib/updateQueryClient.ts | 17 +++++++++ 2 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 frontend/src/shared/lib/updateQueryClient.ts diff --git a/frontend/src/shared/hooks/useLogout.ts b/frontend/src/shared/hooks/useLogout.ts index ee9937d..bbfb911 100644 --- a/frontend/src/shared/hooks/useLogout.ts +++ b/frontend/src/shared/hooks/useLogout.ts @@ -1,12 +1,12 @@ import { z } from "zod"; import { useCallback } from "react"; -import { useNavigate } from "@tanstack/react-router"; import { useUserContext } from "@/shared/hooks/useUserContext"; import { useSetRecoilState } from "recoil"; import { useQueryClient } from "@tanstack/react-query"; import { Auth } from "@/app/provider/RouterProvider/lib/auth"; import { authQueries } from "@/shared/lib/auth/authQuery"; import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard"; +import { useNavigate } from "@tanstack/react-router"; export const useLogout = () => { const navigate = useNavigate(); @@ -18,47 +18,52 @@ export const useLogout = () => { try { const response = await fetch("/api/users/signout", { credentials: "include", + cache: "no-cache", }); if (!response.ok) { throw new Error("로그아웃에 실패했습니다."); } - // Recoil 상태 업데이트 setAuthState((prev) => ({ ...prev, isAuthenticated: false, + isLoading: true, nickname: "", })); - // User Context 상태 업데이트 setUserInfo({ isAuthenticated: false, nickname: "", role: "guest" }); - // React Query 상태 업데이트 queryClient.setQueryData( authQueries.queryKey, - (prev: z.infer) => ({ - ...prev, - userInfo: { - role: "guest", - nickname: "", - duck: 0, - message: "OK", - realDuck: prev.userInfo.realDuck, - }, - }), + (prev: z.infer) => { + console.log("prev", prev); + return { + ...prev, + userInfo: { + role: "guest", + nickname: "", + duck: 0, + message: "OK", + realDuck: prev.userInfo.realDuck, + }, + }; + }, ); await queryClient.invalidateQueries({ queryKey: authQueries.queryKey, }); - // 로그인 페이지로 네비게이션 - navigate({ to: "/login" }); + navigate({ to: "/login", replace: true }); } catch (error) { console.error("Logout Error:", error); - // 추가적인 에러 처리 로직 (예: 사용자에게 에러 메시지 표시) alert("로그아웃 중 오류가 발생했습니다. 다시 시도해 주세요."); + } finally { + setAuthState((prev) => ({ + ...prev, + isLoading: false, + })); } }, [navigate, setUserInfo, setAuthState, queryClient]); diff --git a/frontend/src/shared/lib/updateQueryClient.ts b/frontend/src/shared/lib/updateQueryClient.ts new file mode 100644 index 0000000..d8fc233 --- /dev/null +++ b/frontend/src/shared/lib/updateQueryClient.ts @@ -0,0 +1,17 @@ +import { QueryClient } from "@tanstack/react-query"; +import { authQueries } from "../lib/auth/authQuery"; +import { z } from "zod"; +import { AuthStatusTypeSchema } from "../lib/auth/guard"; + +type QueryData = z.infer; + +async function updateQueryClient( + queryClient: QueryClient, + queryKey: string[], + callback: (prev: QueryData) => QueryData, +) { + queryClient.setQueryData(authQueries.queryKey, callback); + await queryClient.invalidateQueries({ queryKey }); +} + +export { updateQueryClient }; From 49551230efec0bb0b59ea30ad04c1a7c7e9c842d Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:18:27 +0900 Subject: [PATCH 33/42] =?UTF-8?q?refactor=20:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=A1=9C=EB=93=9C=20=EB=B0=A9=EC=8B=9D=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 --- frontend/index.html | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 9d66414..97bf58d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -25,20 +25,14 @@ crossorigin="anonymous" /> - From 83cd11852ab5ed736c2c61478131e52a4f05e4f6 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:18:45 +0900 Subject: [PATCH 34/42] =?UTF-8?q?refactor=20:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/routes/require-login.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/require-login.tsx b/frontend/src/routes/require-login.tsx index 88f6165..d2273d6 100644 --- a/frontend/src/routes/require-login.tsx +++ b/frontend/src/routes/require-login.tsx @@ -2,7 +2,6 @@ import { createFileRoute } from "@tanstack/react-router"; import { ErrorComponent } from "@/shared/components/Error"; import { CreateVoteError } from "@/pages/create-vote/ui/error/CreateVoteError"; import { z } from "zod"; -import { ErrorMyPage } from "@/pages/my-page/error"; import { ROUTE_PATH_ENUM, ROUTES } from "@/shared/config/route"; import { WaitingError } from "@/pages/waiting-room/ui/WaitingError"; import { GuestErrorComponent } from "@/shared/components/Error/GuestError"; @@ -45,7 +44,7 @@ function RouteComponent() { if (from === ROUTES.GUEST_LOGIN) { return ( - + <> ); } @@ -53,7 +52,7 @@ function RouteComponent() { if (from === ROUTES.MYPAGE) { return ( - + <> ); } From 6d9db1964b7a6579f2d64af80feb9200e732462e Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:19:26 +0900 Subject: [PATCH 35/42] =?UTF-8?q?refactor=20:=20=EB=A6=AC=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EB=A0=89=ED=8A=B8=EC=8B=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=9D=98=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20=EC=A7=80=EC=97=B0=20=EC=B2=98=EB=A6=AC=EB=90=98?= =?UTF-8?q?=EA=B2=8C=EB=81=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존의 코드는 네트워크 요청과 동시에 상태가 업데이트가 이루어졌다. - 이로 인해 클라이언트 계층에서 이루어지는 리다이렉트가 네트워크 요청을 기다리지 못하고 바로 리다이렉트 요청이 이루어지는 문제가 발생 - 이를 loading 상태를 추가하여 약간의 딜레이를 추가하여 개선 --- frontend/src/app/provider/RouterProvider/lib/auth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/app/provider/RouterProvider/lib/auth.ts b/frontend/src/app/provider/RouterProvider/lib/auth.ts index e56f3b1..17b3af6 100644 --- a/frontend/src/app/provider/RouterProvider/lib/auth.ts +++ b/frontend/src/app/provider/RouterProvider/lib/auth.ts @@ -3,6 +3,7 @@ import { atom } from "recoil"; export type AuthState = { isAuthenticated: boolean; nickname?: string; + isLoading: boolean; roomId?: string; }; @@ -10,6 +11,7 @@ export const Auth = atom({ key: "auth", default: { isAuthenticated: false, + isLoading: true, nickname: undefined, roomId: undefined, }, From 8a1bb162fd2f45b1348086a3ccb2f890e21c95a3 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:22:17 +0900 Subject: [PATCH 36/42] =?UTF-8?q?refactor=20:=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=94=EC=97=90=20=EC=95=BD=EA=B0=84=EC=9D=98=20?= =?UTF-8?q?=EB=94=9C=EB=A0=88=EC=9D=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provider/AuthProvider/AuthProvider.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/provider/AuthProvider/AuthProvider.tsx b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx index da6b8bc..0a93065 100644 --- a/frontend/src/app/provider/AuthProvider/AuthProvider.tsx +++ b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx @@ -1,32 +1,37 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { Fragment } from "react/jsx-runtime"; import { useSetRecoilState } from "recoil"; import { Auth } from "@/app/provider/RouterProvider/lib/auth"; import { useQuery } from "@tanstack/react-query"; import { authQueries } from "@/shared/lib/auth/authQuery"; -import { LoadingAnimation } from "@/shared/components/Loading"; function AuthProvider({ children }: { children: React.ReactNode }) { - const [isLoading, setIsLoading] = useState(true); const setAuthState = useSetRecoilState(Auth); - const { data } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: authQueries.queryKey, queryFn: authQueries.queryFn, }); useEffect(() => { - if (data) { - setAuthState((prev) => ({ - ...prev, - isAuthenticated: data.isAuthenticated, - nickname: data.userInfo.nickname, - })); + if (!isLoading) { + if (data?.isAuthenticated) { + setAuthState((prev) => ({ + ...prev, + isLoading: false, + isAuthenticated: data.isAuthenticated, + nickname: data.userInfo.nickname, + })); + } else { + setAuthState((prev) => ({ + ...prev, + isLoading: false, + isAuthenticated: false, + })); + } } + }, [data, isLoading, setAuthState]); - setIsLoading(false); - }, [data, setAuthState, setIsLoading]); - - return {isLoading ? : children}; + return {children}; } export { AuthProvider }; From 14da81f9baf4835e1ee4a9882a932a8aced34b04 Mon Sep 17 00:00:00 2001 From: sunub Date: Tue, 21 Jan 2025 22:22:51 +0900 Subject: [PATCH 37/42] =?UTF-8?q?refactor=20:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=A1=9C=EB=93=9C=20=EB=B0=A9=EC=8B=9D=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 --- frontend/src/app/layout/ui/RootHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/layout/ui/RootHeader.tsx b/frontend/src/app/layout/ui/RootHeader.tsx index ea1fe44..5a1d636 100644 --- a/frontend/src/app/layout/ui/RootHeader.tsx +++ b/frontend/src/app/layout/ui/RootHeader.tsx @@ -1,6 +1,6 @@ import { LogoIcon } from "@/shared/icons"; import { Image } from "@/shared/components/Image"; -import waitingUserImage from "@assets/images/waiting-user.png"; +import waitingUserImage from "@assets/images/waiting-user.avif"; import { Link } from "@tanstack/react-router"; function UserInfo({ nickname }: { nickname: string }) { From 9c0e8e08781db66181bd9cc9f1c0b6d1adc7e97a Mon Sep 17 00:00:00 2001 From: sunub Date: Wed, 22 Jan 2025 17:05:35 +0900 Subject: [PATCH 38/42] =?UTF-8?q?feat=20:=20build=EC=8B=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=8A=A4=ED=94=8C=EB=A6=BF=EC=9D=84=20=EB=8D=94=20?= =?UTF-8?q?=EC=84=B8=EB=B6=84=ED=99=94=EA=B0=80=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=A8=ED=82=A4=EC=A7=80=EC=99=80=20=EA=B7=B8?= =?UTF-8?q?=EB=A0=87=EC=A7=80=20=EC=95=8A=EC=9D=80=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=A0=81=EC=A0=88=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EC=8A=A4=ED=94=8C=EB=A6=BF=ED=95=98=EA=B2=8C=EB=81=94=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/stats.html | 2 +- frontend/vite.config.ts | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/frontend/stats.html b/frontend/stats.html index 525dba9..76bce28 100644 --- a/frontend/stats.html +++ b/frontend/stats.html @@ -4929,7 +4929,7 @@