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/
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"
/>
-
diff --git a/frontend/package.json b/frontend/package.json
index 90e5696..e20c4c3 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:*",
@@ -21,7 +21,7 @@
"@radix-ui/react-slot": "^1.1.0",
"@react-three/cannon": "^6.6.0",
"@react-three/drei": "^9.119.0",
- "@react-three/fiber": "^8.17.10",
+ "@react-three/fiber": "^8.17.12",
"@tanstack/react-query": "^5.62.0",
"@tanstack/react-router": "^1.81.1",
"@types/three": "^0.170.0",
@@ -30,33 +30,44 @@
"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"
+ "terser": "^5.37.0",
+ "three": "^0.172.0"
},
"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-devtools": "^1.81.5",
"@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",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
+ "cssnano": "^7.0.6",
"eslint": "^9.13.0",
"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",
+ "rollup-plugin-visualizer": "^5.14.0",
+ "sharp": "^0.33.5",
"tailwindcss": "^3.4.14",
"typescript": "~5.6.3",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10",
+ "vite-plugin-compression2": "^1.3.3",
+ "vite-plugin-image-optimizer": "^1.1.8",
"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/postcss.config.js b/frontend/postcss.config.js
index 2e7af2b..3fc77ed 100644
--- a/frontend/postcss.config.js
+++ b/frontend/postcss.config.js
@@ -2,5 +2,6 @@ export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
+ ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
},
-}
+};
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/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..cb8fd17
--- /dev/null
+++ b/frontend/src/app/layout/ui/Layout.tsx
@@ -0,0 +1,37 @@
+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 { useRecoilValue } from "recoil";
+import { Auth } from "@/app/provider/RouterProvider/lib/auth";
+
+const layoutStyles = {
+ default: "max-w-[520px]",
+ wide: "max-w-[1200px]",
+} as const;
+
+function Layout({ children }: { children: React.ReactNode }) {
+ const { layoutType } = useLayout();
+ const authData = useRecoilValue(Auth);
+
+ 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..5a1d636
--- /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.avif";
+import { Link } from "@tanstack/react-router";
+
+function UserInfo({ nickname }: { nickname: string }) {
+ return (
+
+ );
+}
+
+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..9cc6efd
--- /dev/null
+++ b/frontend/src/app/layout/ui/RootSidebar.tsx
@@ -0,0 +1,94 @@
+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 = {
+ id: string;
+ icon: () => JSX.Element;
+ label: string;
+ href: string;
+ onClick?: () => void;
+};
+
+const navItems = {
+ top: [
+ { 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",
+ },
+ ],
+};
+
+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 };
diff --git a/frontend/src/app/provider/AuthProvider/AuthProvider.tsx b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx
new file mode 100644
index 0000000..0a93065
--- /dev/null
+++ b/frontend/src/app/provider/AuthProvider/AuthProvider.tsx
@@ -0,0 +1,37 @@
+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";
+
+function AuthProvider({ children }: { children: React.ReactNode }) {
+ const setAuthState = useSetRecoilState(Auth);
+ const { data, isLoading } = useQuery({
+ queryKey: authQueries.queryKey,
+ queryFn: authQueries.queryFn,
+ });
+
+ useEffect(() => {
+ 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]);
+
+ 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";
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..17b3af6
--- /dev/null
+++ b/frontend/src/app/provider/RouterProvider/lib/auth.ts
@@ -0,0 +1,18 @@
+import { atom } from "recoil";
+
+export type AuthState = {
+ isAuthenticated: boolean;
+ nickname?: string;
+ isLoading: boolean;
+ roomId?: string;
+};
+
+export const Auth = atom({
+ key: "auth",
+ default: {
+ isAuthenticated: false,
+ isLoading: true,
+ nickname: undefined,
+ roomId: undefined,
+ },
+});
diff --git a/frontend/src/assets/images/main-logo.avif b/frontend/src/assets/images/main-logo.avif
new file mode 100644
index 0000000..5a27e0f
Binary files /dev/null and b/frontend/src/assets/images/main-logo.avif differ
diff --git a/frontend/src/assets/images/main-logo.avif:Zone.Identifier b/frontend/src/assets/images/main-logo.avif:Zone.Identifier
new file mode 100644
index 0000000..45a0c90
--- /dev/null
+++ b/frontend/src/assets/images/main-logo.avif:Zone.Identifier
@@ -0,0 +1,3 @@
+[ZoneTransfer]
+ZoneId=3
+HostUrl=https://squoosh.app/
diff --git a/frontend/src/assets/images/main-logo.png b/frontend/src/assets/images/main-logo.png
deleted file mode 100644
index 07586a1..0000000
Binary files a/frontend/src/assets/images/main-logo.png and /dev/null differ
diff --git a/frontend/src/assets/images/pond.png b/frontend/src/assets/images/pond.png
deleted file mode 100644
index 9e39c6d..0000000
Binary files a/frontend/src/assets/images/pond.png and /dev/null differ
diff --git a/frontend/src/assets/images/waiting-user.avif b/frontend/src/assets/images/waiting-user.avif
new file mode 100644
index 0000000..803a529
Binary files /dev/null and b/frontend/src/assets/images/waiting-user.avif differ
diff --git a/frontend/src/assets/images/waiting-user.avif:Zone.Identifier b/frontend/src/assets/images/waiting-user.avif:Zone.Identifier
new file mode 100644
index 0000000..45a0c90
--- /dev/null
+++ b/frontend/src/assets/images/waiting-user.avif:Zone.Identifier
@@ -0,0 +1,3 @@
+[ZoneTransfer]
+ZoneId=3
+HostUrl=https://squoosh.app/
diff --git a/frontend/src/assets/images/waiting-user.png b/frontend/src/assets/images/waiting-user.png
deleted file mode 100644
index 1250fa9..0000000
Binary files a/frontend/src/assets/images/waiting-user.png and /dev/null differ
diff --git a/frontend/src/features/login-page/ui/components/GuestLoginForm.tsx b/frontend/src/features/login-page/ui/components/GuestLoginForm.tsx
deleted file mode 100644
index 15173ff..0000000
--- a/frontend/src/features/login-page/ui/components/GuestLoginForm.tsx
+++ /dev/null
@@ -1,140 +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";
-
-function GuestLoginForm({ to, roomId }: { to?: string; roomId?: string }) {
- const [nickname, setNickname] = useState("");
- const { error } = useAuthStore();
- const [isSignedUp, setIsSignedUp] = useState(false);
- const queryClient = useQueryClient();
-
- 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 });
-
- if (to && roomId) {
- window.location.href = `/betting/${roomId}/waiting`;
- }
-
- navigate({
- to: "/my-page",
- search: {
- nickname: decodeURIComponent(nickname),
- },
- });
- } catch (error) {
- console.error("게스트 로그인에 실패했습니다.", error);
- }
- };
- return (
-
- );
-}
-
-export { GuestLoginForm };
diff --git a/frontend/src/features/login-page/ui/components/Logout.tsx b/frontend/src/features/login-page/ui/components/Logout.tsx
deleted file mode 100644
index 1a32659..0000000
--- a/frontend/src/features/login-page/ui/components/Logout.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-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;
- });
-}
-
-function LogoutButton() {
- const navigate = useNavigate();
- const { setUserInfo } = useUserContext();
- const queryClient = useQueryClient();
-
- return (
-
- );
-}
-
-export { LogoutButton };
diff --git a/frontend/src/features/my-page/index.tsx b/frontend/src/features/my-page/index.tsx
deleted file mode 100644
index 39a05f2..0000000
--- a/frontend/src/features/my-page/index.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import { DuckCoinIcon } from "@/shared/icons";
-import { useNavigate } from "@tanstack/react-router";
-import { Pond } from "./ui/Pond";
-import { FallingDuck } from "./ui/FallingDuck";
-import React from "react";
-import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
-import { authQueries } from "@/shared/lib/auth/authQuery";
-import { AnimatedDuckCount } from "./ui/AnimatedDuckCount";
-import { z } from "zod";
-import { AuthStatusTypeSchema } from "@/shared/lib/auth/guard";
-
-function MyPage() {
- const { data: authData } = useSuspenseQuery({
- queryKey: authQueries.queryKey,
- queryFn: authQueries.queryFn,
- });
- const queryClient = useQueryClient();
-
- const navigate = useNavigate();
- const [currentDuck, setCurrentDuck] = React.useState(authData.userInfo.duck);
- const [numberOfDucks, setNumberOfDucks] = React.useState(
- authData.userInfo.realDuck,
- );
- const [ducks, setDucks] = React.useState([FallingDuck]);
-
- React.useEffect(() => {
- const remainingDucks = numberOfDucks - ducks.length;
- if (remainingDucks <= 0) return;
-
- const addDuck = (count: number) => {
- if (count >= remainingDucks) return;
- const timer = setTimeout(() => {
- setDucks((prevDucks) => [...prevDucks, FallingDuck]);
- addDuck(count + 1); // 다음 오리 추가
- }, 200);
- return () => clearTimeout(timer);
- };
-
- const initialTimer = setTimeout(() => {
- addDuck(0);
- }, 2000);
- return () => clearTimeout(initialTimer);
- }, [numberOfDucks, ducks.length]);
-
- async function purchaseDuck() {
- if (currentDuck < 30) {
- return;
- }
-
- const response = await fetch("/api/users/purchaseduck");
- if (!response.ok) {
- console.error("Failed to purchase duck");
- return;
- }
- await queryClient.setQueryData(
- authQueries.queryKey,
- (old: z.infer) => ({
- ...old,
- userInfo: {
- ...old.userInfo,
- duck: currentDuck - 30,
- realDuck: numberOfDucks + 1,
- },
- }),
- );
- setDucks([...ducks, FallingDuck]);
- setCurrentDuck(currentDuck - 30);
- setNumberOfDucks(numberOfDucks + 1);
- }
-
- return (
-
-
-
-
마이 페이지
-
오리를 구매해서 페이지를 꾸며보세요
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export { MyPage };
diff --git a/frontend/src/features/my-page/ui/FallingDuck.tsx b/frontend/src/features/my-page/ui/FallingDuck.tsx
deleted file mode 100644
index a1315a7..0000000
--- a/frontend/src/features/my-page/ui/FallingDuck.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import * as THREE from "three";
-import { useBox } from "@react-three/cannon";
-import { Gltf } from "@react-three/drei";
-import duckModel from "@/assets/models/betting-duck.glb";
-
-function FallingDuck() {
- // 약간의 랜덤 오프셋 추가 (x축과 z축에 대해)
- const randomX = (Math.random() - 0.5) * 2; // -1 ~ 1 사이의 랜덤값
- const randomZ = (Math.random() - 0.5) * 2; // -1 ~ 1 사이의 랜덤값
-
- const [ref] = useBox(() => ({
- mass: 1,
- position: [randomX, 5, randomZ], // 초기 위치에 랜덤값 적용
- rotation: [
- Math.random() * 0.2, // 약간의 랜덤 회전도 추가
- Math.random() * 0.2,
- Math.random() * 0.2,
- ],
- linearDamping: 0.4,
- angularDamping: 0.4,
- material: {
- friction: 0.3,
- restitution: 0.3,
- },
- }));
-
- return (
- }
- castShadow
- receiveShadow
- src={duckModel}
- />
- );
-}
-
-export { FallingDuck };
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 };
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-admin/ui/BettingDetails.tsx b/frontend/src/pages/betting-page-admin/ui/BettingDetails.tsx
similarity index 100%
rename from frontend/src/features/betting-page-admin/ui/BettingDetails.tsx
rename to frontend/src/pages/betting-page-admin/ui/BettingDetails.tsx
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 98%
rename from frontend/src/features/login-page/ui/LoginPage.tsx
rename to frontend/src/pages/login-page/ui/LoginPage.tsx
index 35f92ea..5807cd1 100644
--- a/frontend/src/features/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/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 (
+
+ );
+}
+
+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 };
diff --git a/frontend/src/features/login-page/ui/components/LoginForm.tsx b/frontend/src/pages/login-page/ui/components/LoginForm.tsx
similarity index 93%
rename from frontend/src/features/login-page/ui/components/LoginForm.tsx
rename to frontend/src/pages/login-page/ui/components/LoginForm.tsx
index b5e8abc..72da555 100644
--- a/frontend/src/features/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/pages/login-page/ui/components/Logout.tsx b/frontend/src/pages/login-page/ui/components/Logout.tsx
new file mode 100644
index 0000000..9be9a1b
--- /dev/null
+++ b/frontend/src/pages/login-page/ui/components/Logout.tsx
@@ -0,0 +1,23 @@
+import { LogoutIcon } from "@/shared/icons/Logout";
+import { useLogout } from "@/shared/hooks/useLogout";
+
+function LogoutButton() {
+ const logout = useLogout();
+
+ return (
+
+ );
+}
+
+export { LogoutButton };
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..bf55800
--- /dev/null
+++ b/frontend/src/pages/my-page/api/purchaseDuck.ts
@@ -0,0 +1,24 @@
+import { QueryClient } from "@tanstack/react-query";
+import { userInfoQueries } from "@/shared/lib/auth/authQuery";
+import { updateQueryClient } from "@/shared/lib/updateQueryClient";
+
+async function purchaseDuck(currentDuck: number, queryClient: QueryClient) {
+ if (currentDuck < 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,
+ }));
+}
+
+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 84%
rename from frontend/src/features/my-page/error/index.tsx
rename to frontend/src/pages/my-page/error/index.tsx
index 8e87253..a771375 100644
--- a/frontend/src/features/my-page/error/index.tsx
+++ b/frontend/src/pages/my-page/error/index.tsx
@@ -1,5 +1,3 @@
-import { Image } from "@/shared/components/Image";
-import pondImage from "@/assets/images/pond.png";
import { DuckCoinIcon } from "@/shared/icons";
function ErrorMyPage() {
@@ -13,9 +11,7 @@ function ErrorMyPage() {
{0}
-
-
-
+
);
-};
+});
export { AnimatedDuckCount };
diff --git a/frontend/src/pages/my-page/ui/FallingDuck.tsx b/frontend/src/pages/my-page/ui/FallingDuck.tsx
new file mode 100644
index 0000000..bd3de41
--- /dev/null
+++ b/frontend/src/pages/my-page/ui/FallingDuck.tsx
@@ -0,0 +1,33 @@
+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";
+
+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],
+ linearDamping: 0.4,
+ angularDamping: 0.4,
+ material: {
+ friction: 0.3,
+ restitution: 0.3,
+ },
+ }));
+
+ return (
+ }
+ castShadow
+ receiveShadow
+ src={duckModel}
+ />
+ );
+});
+
+export default 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..7744e58
--- /dev/null
+++ b/frontend/src/pages/my-page/ui/MyPage.tsx
@@ -0,0 +1,44 @@
+import { Suspense, lazy } from "react";
+import { useNavigate } from "@tanstack/react-router";
+import { useSuspenseQuery } from "@tanstack/react-query";
+import { userInfoQueries } from "@/shared/lib/auth/authQuery";
+import { AnimatedDuckCount } from "./AnimatedDuckCount";
+import { PurchaseButton } from "./PurchaseButton";
+
+const Pond = lazy(() => import("./Pond"));
+
+function MyPage() {
+ const { data: authData } = useSuspenseQuery({
+ queryKey: userInfoQueries.queryKey,
+ queryFn: userInfoQueries.queryFn,
+ });
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
마이 페이지
+
오리를 구매해서 페이지를 꾸며보세요
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export { MyPage };
diff --git a/frontend/src/features/my-page/ui/Pond.tsx b/frontend/src/pages/my-page/ui/Pond.tsx
similarity index 53%
rename from frontend/src/features/my-page/ui/Pond.tsx
rename to frontend/src/pages/my-page/ui/Pond.tsx
index bd57380..e854f1d 100644
--- a/frontend/src/features/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,12 @@ import {
OrthographicCamera,
} from "@react-three/drei";
import { Physics, usePlane } from "@react-three/cannon";
-import { Suspense } from "react";
-import envMap from "@assets/models/industrial_sunset_puresky_4k.hdr";
+import { lazy, memo, Suspense, useCallback, useEffect, useState } from "react";
-function Ground({ color = "#f0f4fa" }: { color?: string }) {
- const [ref] = usePlane(() => ({
+const FallingDuck = lazy(() => import("./FallingDuck"));
+
+const Ground = memo(() => {
+ const [ref] = usePlane(() => ({
rotation: [-Math.PI / 2, 0, 0],
position: [0, -6.5, 0],
type: "Static",
@@ -23,12 +24,48 @@ function Ground({ color = "#f0f4fa" }: { color?: string }) {
return (
-
+
);
-}
+});
+
+function Pond({ realDuck }: { realDuck: number }) {
+ const [envMap, setEnvMap] = useState(null);
+ 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 = realDuck - duckModels.length;
+ if (remainDucks <= 0) return;
+
+ const initialTimer = setTimeout(() => {
+ addDuck(0, remainDucks);
+ }, 1000);
+
+ return () => clearTimeout(initialTimer);
+ }, [realDuck, duckModels.length, addDuck]);
+
+ useEffect(() => {
+ (async () => {
+ const env = await import(
+ "@assets/models/industrial_sunset_puresky_4k.hdr"
+ );
+ setEnvMap(env.default);
+ })();
+ }, []);
-function Pond({ ducks }: { ducks: React.ElementType[] }) {
return (