From f665479c3d8c1e5272738883620588ba4ab9c147 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Thu, 3 Apr 2025 18:34:31 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat=20:=20sonner=20+=20zustand=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=A0=9C=EC=96=B4=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?#gpt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/api/auth/callback/route.ts | 2 +- apps/web/app/api/auth/confirm.ts | 2 +- apps/web/app/api/search/route.ts | 2 +- apps/web/app/auth.tsx | 17 + apps/web/app/components/ui/dropdown-menu.tsx | 228 +++++++ apps/web/app/components/ui/sonner.tsx | 25 + apps/web/app/home/SongList.tsx | 3 +- apps/web/app/layout.tsx | 34 +- apps/web/app/lib/store/useAuthStore.ts | 153 +++++ apps/web/app/{ => lib}/supabase/api.ts | 0 apps/web/app/{ => lib}/supabase/client.ts | 0 apps/web/app/{ => lib}/supabase/middleware.ts | 0 apps/web/app/{ => lib}/supabase/server.ts | 0 apps/web/app/login/KakaoLogin.tsx | 12 +- apps/web/app/login/actions.ts | 5 +- apps/web/app/login/page.tsx | 35 +- apps/web/app/signup/actions.ts | 2 +- apps/web/app/signup/page.tsx | 18 +- apps/web/app/testing-table/button.tsx | 2 +- apps/web/app/testing-table/page.tsx | 2 +- apps/web/package.json | 6 +- pnpm-lock.yaml | 642 ++++++++++++++++++ 22 files changed, 1135 insertions(+), 55 deletions(-) create mode 100644 apps/web/app/auth.tsx create mode 100644 apps/web/app/components/ui/dropdown-menu.tsx create mode 100644 apps/web/app/components/ui/sonner.tsx create mode 100644 apps/web/app/lib/store/useAuthStore.ts rename apps/web/app/{ => lib}/supabase/api.ts (100%) rename apps/web/app/{ => lib}/supabase/client.ts (100%) rename apps/web/app/{ => lib}/supabase/middleware.ts (100%) rename apps/web/app/{ => lib}/supabase/server.ts (100%) diff --git a/apps/web/app/api/auth/callback/route.ts b/apps/web/app/api/auth/callback/route.ts index 4361aea..3b9d4d2 100644 --- a/apps/web/app/api/auth/callback/route.ts +++ b/apps/web/app/api/auth/callback/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; // The client you created from the Server-Side Auth instructions -import { createClient } from '@/supabase/server'; +import { createClient } from '@/lib/supabase/server'; export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); diff --git a/apps/web/app/api/auth/confirm.ts b/apps/web/app/api/auth/confirm.ts index 41dcdda..559a382 100644 --- a/apps/web/app/api/auth/confirm.ts +++ b/apps/web/app/api/auth/confirm.ts @@ -1,7 +1,7 @@ import { type EmailOtpType } from '@supabase/supabase-js'; import type { NextApiRequest, NextApiResponse } from 'next'; -import createClient from '@/supabase/api'; +import createClient from '@/lib/supabase/api'; function stringOrFirstString(item: string | string[] | undefined) { return Array.isArray(item) ? item[0] : item; diff --git a/apps/web/app/api/search/route.ts b/apps/web/app/api/search/route.ts index ecc6490..599c2ac 100644 --- a/apps/web/app/api/search/route.ts +++ b/apps/web/app/api/search/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/supabase/server'; +import { createClient } from '@/lib/supabase/server'; export async function GET(request: Request) { // API KEY 노출을 막기 위해 미들웨어 역할을 할 API ROUTE 활용 diff --git a/apps/web/app/auth.tsx b/apps/web/app/auth.tsx new file mode 100644 index 0000000..657c273 --- /dev/null +++ b/apps/web/app/auth.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useEffect } from 'react'; + +import { useAuthStore } from '@/lib/store/useAuthStore'; + +const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const { checkAuth } = useAuthStore(); + + useEffect(() => { + checkAuth(); + }, [checkAuth]); + + return <>{children}; +}; + +export default AuthProvider; diff --git a/apps/web/app/components/ui/dropdown-menu.tsx b/apps/web/app/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..6619f91 --- /dev/null +++ b/apps/web/app/components/ui/dropdown-menu.tsx @@ -0,0 +1,228 @@ +'use client'; + +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function DropdownMenu({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ); +} + +function DropdownMenuSub({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/apps/web/app/components/ui/sonner.tsx b/apps/web/app/components/ui/sonner.tsx new file mode 100644 index 0000000..957524e --- /dev/null +++ b/apps/web/app/components/ui/sonner.tsx @@ -0,0 +1,25 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner, ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/apps/web/app/home/SongList.tsx b/apps/web/app/home/SongList.tsx index cc606d3..f016c00 100644 --- a/apps/web/app/home/SongList.tsx +++ b/apps/web/app/home/SongList.tsx @@ -16,9 +16,10 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; +import dynamic from 'next/dynamic'; import { useState } from 'react'; -import SongCard from './SongCard'; +const SongCard = dynamic(() => import('./SongCard'), { ssr: false }); // 초기 노래 데이터 const initialSongs = [ diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 1229766..835d6ad 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,8 +1,10 @@ import type { Metadata } from 'next'; +import { Toaster } from 'sonner'; import ErrorWrapper from './ErrorWrapper'; import Footer from './Footer'; import Header from './Header'; +import AuthProvider from './auth'; import './globals.css'; import QueryProvider from './query'; @@ -21,11 +23,33 @@ export default function RootLayout({ -
-
-
{children}
-
-
+ +
+
+
{children}
+
+
+ + +
diff --git a/apps/web/app/lib/store/useAuthStore.ts b/apps/web/app/lib/store/useAuthStore.ts new file mode 100644 index 0000000..fddf04b --- /dev/null +++ b/apps/web/app/lib/store/useAuthStore.ts @@ -0,0 +1,153 @@ +import { toast } from 'sonner'; +import { create } from 'zustand'; + +import { createClient } from '@/lib/supabase/client'; + +export const supabase = createClient(); + +// 사용자 타입 정의 +export interface User { + id: string; + nickname: string; + profile_image: string | null; +} + +interface AuthState { + user: User | null; + isLoading: boolean; + isAuthenticated: boolean; + + // 액션 + register: (email: string, password: string) => Promise; // 반환 타입 변경 + login: (email: string, password: string) => Promise; // 반환 타입 변경 + authKaKaoLogin: () => Promise; + + logout: () => Promise; + checkAuth: () => Promise; + insertUser: (id: string) => Promise; +} + +export const useAuthStore = create((set, get) => ({ + user: null, + isLoading: false, + isAuthenticated: false, + + register: async (email: string, password: string) => { + try { + set({ isLoading: true }); + const supabase = createClient(); + const { data, error } = await supabase.auth.signUp({ email, password }); + if (error) throw error; + + console.log('data : ', data); + + toast.success('회원가입 성공', { + description: '만나서 반가워요!', + }); + + return true; + } catch (error) { + console.error('회원가입 오류:', error); + toast.error('회원가입 실패', { + description: '회원 가입이 실패했어요...', + }); + return false; + } finally { + set({ isLoading: false }); + } + }, + // 로그인 액션 + login: async (email: string, password: string) => { + try { + set({ isLoading: true }); + const supabase = createClient(); + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + if (error) throw error; + + const { session } = data; + const uid = session.user.id; + console.log('uid : ', uid); + + toast.success('로그인 성공', { + description: '다시 만나서 반가워요!', + }); + + return true; // 성공 시 true 반환 + } catch (error) { + console.error('로그인 오류:', error); + toast.error('로그인 실패', { + description: '이메일 또는 비밀번호가 일치하지 않아요...', + }); + return false; // 실패 시 false 반환 + } finally { + set({ isLoading: false }); + } + }, + authKaKaoLogin: async () => { + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'kakao', + }); + if (error) throw error; + + toast.success('카카오 로그인 성공', { + description: '다시 만나서 반가워요!', + }); + return true; + } catch (error) { + console.error('카카오 로그인 오류:', error); + toast.error('카카오 로그인 실패', { + description: '카카오 로그인에 문제가 있어요...', + }); + + return false; + } + }, + // 로그아웃 액션 + logout: async () => { + await supabase.auth.signOut(); + set({ user: null, isAuthenticated: false }); + }, + + // 인증 상태 확인 + checkAuth: async () => { + set({ isLoading: true }); + + try { + const { data, error } = await supabase.auth.getUser(); + + if (error) throw error; + console.log('checkAuth data : ', data); + if (!get().user) { + const id = data.user.id; + const { data: existingUser } = await supabase + .from('users') + .select('*') + .eq('id', id) + .single(); + console.log('existingUser : ', existingUser); + if (!existingUser) get().insertUser(id); + else { + set({ user: existingUser, isAuthenticated: true }); + } + } + } catch (error) { + console.error('checkAuth 오류:', error); + } finally { + set({ isLoading: false }); + } + }, + insertUser: async (id: string) => { + try { + const { data: user, error } = await supabase.from('users').insert({ id }).select().single(); + if (error) throw error; + set({ user: user, isAuthenticated: true }); + } catch (error) { + console.error('insertUser 오류:', error); + } + }, +})); diff --git a/apps/web/app/supabase/api.ts b/apps/web/app/lib/supabase/api.ts similarity index 100% rename from apps/web/app/supabase/api.ts rename to apps/web/app/lib/supabase/api.ts diff --git a/apps/web/app/supabase/client.ts b/apps/web/app/lib/supabase/client.ts similarity index 100% rename from apps/web/app/supabase/client.ts rename to apps/web/app/lib/supabase/client.ts diff --git a/apps/web/app/supabase/middleware.ts b/apps/web/app/lib/supabase/middleware.ts similarity index 100% rename from apps/web/app/supabase/middleware.ts rename to apps/web/app/lib/supabase/middleware.ts diff --git a/apps/web/app/supabase/server.ts b/apps/web/app/lib/supabase/server.ts similarity index 100% rename from apps/web/app/supabase/server.ts rename to apps/web/app/lib/supabase/server.ts diff --git a/apps/web/app/login/KakaoLogin.tsx b/apps/web/app/login/KakaoLogin.tsx index 5d9d6ee..373ed8b 100644 --- a/apps/web/app/login/KakaoLogin.tsx +++ b/apps/web/app/login/KakaoLogin.tsx @@ -2,19 +2,15 @@ import Image from 'next/image'; -import { createClient } from '@/supabase/client'; +import { useAuthStore } from '@/lib/store/useAuthStore'; // 클라이언트용 Supabase 클라이언트 export default function KakaoLogin() { - const handleKakaoLogin = async () => { - const supabase = createClient(); - const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'kakao', - }); + const { authKaKaoLogin } = useAuthStore(); - console.log('data : ', data); - console.log('error : ', error); + const handleKakaoLogin = async () => { + await authKaKaoLogin(); }; return ( diff --git a/apps/web/app/login/actions.ts b/apps/web/app/login/actions.ts index 7207684..26a49ac 100644 --- a/apps/web/app/login/actions.ts +++ b/apps/web/app/login/actions.ts @@ -1,9 +1,8 @@ 'use server'; import { revalidatePath } from 'next/cache'; -import { redirect } from 'next/navigation'; -import { createClient } from '@/supabase/server'; +import { createClient } from '@/lib/supabase/server'; export async function login(email: string, password: string) { const supabase = await createClient(); @@ -24,5 +23,5 @@ export async function login(email: string, password: string) { } revalidatePath('/', 'layout'); - redirect('/'); + return response; } diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 7f94763..1315177 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -2,46 +2,43 @@ import { Eye, EyeOff } from 'lucide-react'; import Link from 'next/link'; -// import { redirect } from 'next/navigation'; +import { redirect } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; -import { createClient } from '@/supabase/client'; +import { useAuthStore } from '@/lib/store/useAuthStore'; +import { createClient } from '@/lib/supabase/client'; import KakaoLogin from './KakaoLogin'; -import { login } from './actions'; - -// 새로운 클라이언트 컴포넌트 export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); + + const { isLoading, isAuthenticated, login, checkAuth } = useAuthStore(); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - setIsLoading(true); - await login(email, password); - setIsLoading(false); - }; - - const getUser = async () => { - const supabase = createClient(); - const { data } = await supabase.auth.getUser(); - - if (data) { - console.log('data : ', data); + const success = await login(email, password); + checkAuth(); + if (success) { // redirect('/'); } }; useEffect(() => { - getUser(); - }, []); + if (isAuthenticated) { + toast.success('로그인 확인', { + description: '이미 로그인 하셨어요!', + }); + // redirect('/'); + } + }, [isAuthenticated]); return (
{ e.preventDefault(); @@ -24,15 +25,8 @@ export default function SignupPage() { return; } - setIsLoading(true); - - try { - await register(email, password); - } catch (error) { - console.error('회원가입 실패:', error); - } finally { - setIsLoading(false); - } + await register(email, password); + redirect('/login'); }; return ( diff --git a/apps/web/app/testing-table/button.tsx b/apps/web/app/testing-table/button.tsx index 6e62a34..136fac8 100644 --- a/apps/web/app/testing-table/button.tsx +++ b/apps/web/app/testing-table/button.tsx @@ -1,6 +1,6 @@ 'use client'; -import { createClient } from '@/supabase/client'; +import { createClient } from '@/lib/supabase/client'; const Button = () => { const supabase = createClient(); diff --git a/apps/web/app/testing-table/page.tsx b/apps/web/app/testing-table/page.tsx index 1715267..3dae2e9 100644 --- a/apps/web/app/testing-table/page.tsx +++ b/apps/web/app/testing-table/page.tsx @@ -1,4 +1,4 @@ -import { createClient } from '@/supabase/server'; +import { createClient } from '@/lib/supabase/server'; import Button from './button'; diff --git a/apps/web/package.json b/apps/web/package.json index 1b8c4c6..58fc03a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", @@ -29,10 +30,13 @@ "clsx": "^2.1.1", "lucide-react": "^0.483.0", "next": "15.2.2", + "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "sonner": "^2.0.3", "tailwind-merge": "^3.0.2", - "tw-animate-css": "^1.2.4" + "tw-animate-css": "^1.2.4", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d44b01..5736405 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,6 +139,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.0.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.6 + version: 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -181,18 +184,27 @@ importers: next: specifier: 15.2.2 version: 15.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + sonner: + specifier: ^2.0.3 + version: 2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tailwind-merge: specifier: ^3.0.2 version: 3.0.2 tw-animate-css: specifier: ^1.2.4 version: 1.2.4 + zustand: + specifier: ^5.0.3 + version: 5.0.3(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -1406,6 +1418,21 @@ packages: resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==} hasBin: true + '@floating-ui/core@1.6.9': + resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} + + '@floating-ui/dom@1.6.13': + resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1722,6 +1749,35 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/react-arrow@1.1.2': + resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.2': + resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.0.0': resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} peerDependencies: @@ -1736,6 +1792,81 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.5': + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.6': + resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.2': + resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-label@2.1.2': resolution: {integrity: sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==} peerDependencies: @@ -1749,6 +1880,58 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menu@2.1.6': + resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.2': + resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.4': + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.2': resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} peerDependencies: @@ -1762,6 +1945,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.2': + resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-separator@1.1.2': resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==} peerDependencies: @@ -1789,6 +1985,63 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@react-native/assets-registry@0.76.7': resolution: {integrity: sha512-o79whsqL5fbPTUQO9w1FptRd4cw1TaeOrXtQSLQeDrMVAenw/wmsjyPK10VKtvqxa1KNMtWEyfgxcM8CVZVFmg==} engines: {node: '>=18'} @@ -2530,6 +2783,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -3211,6 +3468,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3879,6 +4139,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -5066,6 +5330,12 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@15.2.2: resolution: {integrity: sha512-dgp8Kcx5XZRjMw2KNwBtUzhngRaURPioxoNIVl5BOyJbhi9CUgEtKDO7fx5wh8Z8vOVX1nYZ9meawJoRrlASYA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -5675,6 +5945,26 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.6.3: + resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-server-dom-webpack@19.0.0-rc-6230622a1a-20240610: resolution: {integrity: sha512-nr+IsOVD07QdeCr4BLvR5TALfLaZLi9AIaoa6vXymBc051iDPWedJujYYrjRJy5+9jp9oCx3G8Tt/Bs//TckJw==} engines: {node: '>=0.10.0'} @@ -5688,6 +5978,16 @@ packages: peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-test-renderer@18.3.1: resolution: {integrity: sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==} peerDependencies: @@ -6050,6 +6350,12 @@ packages: resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner@2.0.3: + resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -6618,11 +6924,31 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-latest-callback@0.2.3: resolution: {integrity: sha512-7vI3fBuyRcP91pazVboc4qu+6ZqM8izPWX9k7cRnT8hbD5svslcknsh3S9BUhaK11OmgTV4oWZZVSeQAiV53SQ==} peerDependencies: react: '>=16.8' + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.4.0: resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} peerDependencies: @@ -6887,6 +7213,24 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@0no-co/graphql.web@1.1.2': {} @@ -8262,6 +8606,23 @@ snapshots: find-up: 5.0.0 js-yaml: 4.1.0 + '@floating-ui/core@1.6.9': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.6.13': + dependencies: + '@floating-ui/core': 1.6.9 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/dom': 1.6.13 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@floating-ui/utils@0.2.9': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -8619,6 +8980,29 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + + '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-compose-refs@1.0.0(react@18.3.1)': dependencies: '@babel/runtime': 7.26.10 @@ -8630,6 +9014,70 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-context@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-direction@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + + '@radix-ui/react-dropdown-menu@2.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-menu': 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + + '@radix-ui/react-id@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-label@2.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -8639,6 +9087,70 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-menu@2.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + aria-hidden: 1.2.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.3(@types/react@19.0.10)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + + '@radix-ui/react-popper@1.2.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-arrow': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/rect': 1.1.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + + '@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) @@ -8648,6 +9160,23 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-separator@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -8670,6 +9199,48 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-use-size@1.1.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/rect@1.1.0': {} + '@react-native/assets-registry@0.76.7': {} '@react-native/babel-plugin-codegen@0.76.7(@babel/preset-env@7.26.9(@babel/core@7.26.10))': @@ -9551,6 +10122,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.4: + dependencies: + tslib: 2.8.1 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -10341,6 +10916,8 @@ snapshots: detect-newline@3.1.0: {} + detect-node-es@1.1.0: {} + diff-sequences@29.6.3: {} diff@4.0.2: {} @@ -11239,6 +11816,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-package-type@0.1.0: {} get-port@3.2.0: {} @@ -12765,6 +13344,11 @@ snapshots: netmask@2.0.2: {} + next-themes@0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + next@15.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.2.2 @@ -13427,6 +14011,25 @@ snapshots: react-refresh@0.14.2: {} + react-remove-scroll-bar@2.3.8(@types/react@19.0.10)(react@19.0.0): + dependencies: + react: 19.0.0 + react-style-singleton: 2.2.3(@types/react@19.0.10)(react@19.0.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.0.10 + + react-remove-scroll@2.6.3(@types/react@19.0.10)(react@19.0.0): + dependencies: + react: 19.0.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.0.10)(react@19.0.0) + react-style-singleton: 2.2.3(@types/react@19.0.10)(react@19.0.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.0.10)(react@19.0.0) + use-sidecar: 1.1.3(@types/react@19.0.10)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + react-server-dom-webpack@19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.98.0): dependencies: acorn-loose: 8.4.0 @@ -13441,6 +14044,14 @@ snapshots: react: 18.3.1 react-is: 18.3.1 + react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0): + dependencies: + get-nonce: 1.0.1 + react: 19.0.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.0.10 + react-test-renderer@18.3.1(react@18.3.1): dependencies: react: 18.3.1 @@ -13897,6 +14508,11 @@ snapshots: ip-address: 9.0.5 smart-buffer: 4.2.0 + sonner@2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -14482,14 +15098,34 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0): + dependencies: + react: 19.0.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.0.10 + use-latest-callback@0.2.3(react@18.3.1): dependencies: react: 18.3.1 + use-sidecar@1.1.3(@types/react@19.0.10)(react@19.0.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.0.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.0.10 + use-sync-external-store@1.4.0(react@18.3.1): dependencies: react: 18.3.1 + use-sync-external-store@1.4.0(react@19.0.0): + dependencies: + react: 19.0.0 + optional: true + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -14743,3 +15379,9 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zustand@5.0.3(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)): + optionalDependencies: + '@types/react': 19.0.10 + react: 19.0.0 + use-sync-external-store: 1.4.0(react@19.0.0) From 485bff9dbb076e48af3e5e67778da125fea4c545 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Thu, 3 Apr 2025 20:59:17 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=A4=91=EB=B3=B5=20=EC=8B=9C=20=EC=A0=9C=EC=96=B4?= =?UTF-8?q?=20=EB=BC=88=EB=8C=80.=20Dialog=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/Header.tsx | 9 ++ apps/web/app/components/messageDialog.tsx | 94 +++++++++++++++ apps/web/app/components/ui/dialog.tsx | 135 ++++++++++++++++++++++ apps/web/app/lib/store/useAuthStore.ts | 15 ++- apps/web/app/lib/store/useModalStore.ts | 46 ++++++++ apps/web/app/login/page.tsx | 1 - apps/web/app/signup/page.tsx | 5 +- apps/web/package.json | 1 + pnpm-lock.yaml | 38 ++++++ 9 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 apps/web/app/components/messageDialog.tsx create mode 100644 apps/web/app/components/ui/dialog.tsx create mode 100644 apps/web/app/lib/store/useModalStore.ts diff --git a/apps/web/app/Header.tsx b/apps/web/app/Header.tsx index fc96049..0e3cdf7 100644 --- a/apps/web/app/Header.tsx +++ b/apps/web/app/Header.tsx @@ -4,13 +4,22 @@ import { User } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; +import { useAuthStore } from '@/lib/store/useAuthStore'; + export default function Header() { // login const router = useRouter(); + const { logout } = useAuthStore(); + + const handleLogout = async () => { + await logout(); + }; return (
logo router.push('/')} /> + +
handleLogout()}>로그아웃
router.push('/login')} />
); diff --git a/apps/web/app/components/messageDialog.tsx b/apps/web/app/components/messageDialog.tsx new file mode 100644 index 0000000..1c9307f --- /dev/null +++ b/apps/web/app/components/messageDialog.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { AlertCircle, CheckCircle2, Info, XCircle } from 'lucide-react'; +import { useEffect } from 'react'; + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { useMessageStore } from '@/lib/store/useModalStore'; +import { cn } from '@/lib/utils'; + +export function MessageDialog() { + const { isOpen, title, message, variant, buttonText, onButtonClick, closeMessage } = + useMessageStore(); + + // 버튼 클릭 핸들러 + const handleButtonClick = () => { + if (onButtonClick) { + onButtonClick(); + } + closeMessage(); + }; + + // ESC 키로 닫기 방지 (필요한 경우) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + e.preventDefault(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen]); + + // 아이콘 선택 + const IconComponent = (() => { + switch (variant) { + case 'success': + return CheckCircle2; + case 'error': + return XCircle; + case 'warning': + case 'info': + return AlertCircle; + default: + return Info; + } + })(); + + return ( + !open && closeMessage()}> + + +
+ + {title && {title}} +
+
+ +
+

{message}

+
+ + + + +
+
+ ); +} diff --git a/apps/web/app/components/ui/dialog.tsx b/apps/web/app/components/ui/dialog.tsx new file mode 100644 index 0000000..7d7a9d3 --- /dev/null +++ b/apps/web/app/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/apps/web/app/lib/store/useAuthStore.ts b/apps/web/app/lib/store/useAuthStore.ts index fddf04b..7998a87 100644 --- a/apps/web/app/lib/store/useAuthStore.ts +++ b/apps/web/app/lib/store/useAuthStore.ts @@ -23,7 +23,7 @@ interface AuthState { authKaKaoLogin: () => Promise; logout: () => Promise; - checkAuth: () => Promise; + checkAuth: () => Promise; insertUser: (id: string) => Promise; } @@ -39,7 +39,12 @@ export const useAuthStore = create((set, get) => ({ const { data, error } = await supabase.auth.signUp({ email, password }); if (error) throw error; - console.log('data : ', data); + if (data.user?.identities?.length === 0) { + toast.error('회원가입 실패', { + description: '이미 가입된 이메일입니다.', + }); + return false; + } toast.success('회원가입 성공', { description: '만나서 반가워요!', @@ -115,8 +120,6 @@ export const useAuthStore = create((set, get) => ({ // 인증 상태 확인 checkAuth: async () => { - set({ isLoading: true }); - try { const { data, error } = await supabase.auth.getUser(); @@ -135,10 +138,10 @@ export const useAuthStore = create((set, get) => ({ set({ user: existingUser, isAuthenticated: true }); } } + return true; } catch (error) { console.error('checkAuth 오류:', error); - } finally { - set({ isLoading: false }); + return false; } }, insertUser: async (id: string) => { diff --git a/apps/web/app/lib/store/useModalStore.ts b/apps/web/app/lib/store/useModalStore.ts new file mode 100644 index 0000000..09b6902 --- /dev/null +++ b/apps/web/app/lib/store/useModalStore.ts @@ -0,0 +1,46 @@ +import { create } from 'zustand'; + +export type MessageVariant = 'default' | 'success' | 'error' | 'warning' | 'info'; + +export interface MessageState { + isOpen: boolean; + title?: string; + message: string; + variant: MessageVariant; + buttonText?: string; + onButtonClick?: () => void; + + // 액션 + openMessage: (props: { + title?: string; + message: string; + variant?: MessageVariant; + buttonText?: string; + onButtonClick?: () => void; + }) => void; + closeMessage: () => void; +} + +export const useMessageStore = create(set => ({ + isOpen: false, + title: undefined, + message: '', + variant: 'default', + buttonText: undefined, + onButtonClick: undefined, + + openMessage: ({ title, message, variant = 'default', buttonText, onButtonClick }) => { + set({ + isOpen: true, + title, + message, + variant, + buttonText, + onButtonClick, + }); + }, + + closeMessage: () => { + set({ isOpen: false }); + }, +})); diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 1315177..5df9dd3 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -11,7 +11,6 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { useAuthStore } from '@/lib/store/useAuthStore'; -import { createClient } from '@/lib/supabase/client'; import KakaoLogin from './KakaoLogin'; diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx index ff48d60..52b9222 100644 --- a/apps/web/app/signup/page.tsx +++ b/apps/web/app/signup/page.tsx @@ -25,8 +25,9 @@ export default function SignupPage() { return; } - await register(email, password); - redirect('/login'); + const result = await register(email, password); + console.log('result : ', result); + if (result) redirect('/login'); }; return ( diff --git a/apps/web/package.json b/apps/web/package.json index 58fc03a..a4eb0f2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-separator": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5736405..7cad86c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,6 +139,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.0.0) + '@radix-ui/react-dialog': + specifier: ^1.1.6 + version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dropdown-menu': specifier: ^2.1.6 version: 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1801,6 +1804,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.6': + resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-direction@1.1.0': resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} peerDependencies: @@ -9020,6 +9036,28 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) + aria-hidden: 1.2.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.3(@types/react@19.0.10)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-direction@1.1.0(@types/react@19.0.10)(react@19.0.0)': dependencies: react: 19.0.0 From 51dea343c7132bb4d6c0315943280f5414abddb0 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 4 Apr 2025 18:09:12 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat=20:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EB=AA=A8=EB=8B=AC=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4,=20=ED=83=80=EC=9E=85=EC=97=90=20=EB=94=B0=EB=A5=B8?= =?UTF-8?q?=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/components/messageDialog.tsx | 4 +- apps/web/app/layout.tsx | 4 ++ apps/web/app/lib/store/useAuthStore.ts | 59 ++++++++++++++--------- apps/web/app/lib/store/useModalStore.ts | 5 +- apps/web/app/lib/utils.ts | 46 +++++++++++++++++- apps/web/app/login/page.tsx | 23 ++++++--- apps/web/app/signup/page.tsx | 30 ++++++++++-- 7 files changed, 129 insertions(+), 42 deletions(-) diff --git a/apps/web/app/components/messageDialog.tsx b/apps/web/app/components/messageDialog.tsx index 1c9307f..0952c50 100644 --- a/apps/web/app/components/messageDialog.tsx +++ b/apps/web/app/components/messageDialog.tsx @@ -11,12 +11,12 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { useMessageStore } from '@/lib/store/useModalStore'; +import { useModalStore } from '@/lib/store/useModalStore'; import { cn } from '@/lib/utils'; export function MessageDialog() { const { isOpen, title, message, variant, buttonText, onButtonClick, closeMessage } = - useMessageStore(); + useModalStore(); // 버튼 클릭 핸들러 const handleButtonClick = () => { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 835d6ad..bbdb0b3 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from 'next'; import { Toaster } from 'sonner'; +import { MessageDialog } from '@/components/messageDialog'; + import ErrorWrapper from './ErrorWrapper'; import Footer from './Footer'; import Header from './Header'; @@ -49,6 +51,8 @@ export default function RootLayout({ // }, }} /> + + diff --git a/apps/web/app/lib/store/useAuthStore.ts b/apps/web/app/lib/store/useAuthStore.ts index 7998a87..3cc0a33 100644 --- a/apps/web/app/lib/store/useAuthStore.ts +++ b/apps/web/app/lib/store/useAuthStore.ts @@ -1,8 +1,11 @@ +import { AuthError } from '@supabase/supabase-js'; import { toast } from 'sonner'; import { create } from 'zustand'; import { createClient } from '@/lib/supabase/client'; +import { getErrorMessage } from '../utils'; + export const supabase = createClient(); // 사용자 타입 정의 @@ -18,8 +21,8 @@ interface AuthState { isAuthenticated: boolean; // 액션 - register: (email: string, password: string) => Promise; // 반환 타입 변경 - login: (email: string, password: string) => Promise; // 반환 타입 변경 + register: (email: string, password: string) => Promise; // 반환 타입 변경 + login: (email: string, password: string) => Promise; // 반환 타입 변경 authKaKaoLogin: () => Promise; logout: () => Promise; @@ -27,6 +30,12 @@ interface AuthState { insertUser: (id: string) => Promise; } +interface ResponseState { + isSuccess: boolean; + errorTitle?: string; + errorMessage?: string; +} + export const useAuthStore = create((set, get) => ({ user: null, isLoading: false, @@ -36,27 +45,35 @@ export const useAuthStore = create((set, get) => ({ try { set({ isLoading: true }); const supabase = createClient(); + const { data, error } = await supabase.auth.signUp({ email, password }); if (error) throw error; if (data.user?.identities?.length === 0) { - toast.error('회원가입 실패', { - description: '이미 가입된 이메일입니다.', - }); - return false; + return { + isSuccess: false, + errorTitle: '이메일 중복', + errorMessage: '이미 가입된 이메일입니다.', + }; } toast.success('회원가입 성공', { description: '만나서 반가워요!', }); - return true; + return { + isSuccess: true, + }; } catch (error) { - console.error('회원가입 오류:', error); - toast.error('회원가입 실패', { - description: '회원 가입이 실패했어요...', - }); - return false; + if (error instanceof AuthError) { + return getErrorMessage(error.code as string); + } else { + return { + isSuccess: false, + errorTitle: '회원가입 실패', + errorMessage: '회원 가입이 실패했어요.', + }; + } } finally { set({ isLoading: false }); } @@ -67,27 +84,22 @@ export const useAuthStore = create((set, get) => ({ set({ isLoading: true }); const supabase = createClient(); - const { data, error } = await supabase.auth.signInWithPassword({ + const { error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) throw error; - const { session } = data; - const uid = session.user.id; - console.log('uid : ', uid); - toast.success('로그인 성공', { description: '다시 만나서 반가워요!', }); - return true; // 성공 시 true 반환 + return { + isSuccess: true, + }; // 성공 시 true 반환 } catch (error) { - console.error('로그인 오류:', error); - toast.error('로그인 실패', { - description: '이메일 또는 비밀번호가 일치하지 않아요...', - }); - return false; // 실패 시 false 반환 + const { code } = error as AuthError; + return getErrorMessage(code as string); } finally { set({ isLoading: false }); } @@ -124,7 +136,6 @@ export const useAuthStore = create((set, get) => ({ const { data, error } = await supabase.auth.getUser(); if (error) throw error; - console.log('checkAuth data : ', data); if (!get().user) { const id = data.user.id; const { data: existingUser } = await supabase diff --git a/apps/web/app/lib/store/useModalStore.ts b/apps/web/app/lib/store/useModalStore.ts index 09b6902..d992084 100644 --- a/apps/web/app/lib/store/useModalStore.ts +++ b/apps/web/app/lib/store/useModalStore.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; export type MessageVariant = 'default' | 'success' | 'error' | 'warning' | 'info'; -export interface MessageState { +export interface ModalState { isOpen: boolean; title?: string; message: string; @@ -21,13 +21,14 @@ export interface MessageState { closeMessage: () => void; } -export const useMessageStore = create(set => ({ +export const useModalStore = create(set => ({ isOpen: false, title: undefined, message: '', variant: 'default', buttonText: undefined, onButtonClick: undefined, + // onButtonClick 없어도 closeMessage는 기본적으로 호출 된다 openMessage: ({ title, message, variant = 'default', buttonText, onButtonClick }) => { set({ diff --git a/apps/web/app/lib/utils.ts b/apps/web/app/lib/utils.ts index 9ad0df4..ed73155 100644 --- a/apps/web/app/lib/utils.ts +++ b/apps/web/app/lib/utils.ts @@ -1,6 +1,48 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; -export function cn(...inputs: ClassValue[]) { +export const cn = (...inputs: ClassValue[]) => { return twMerge(clsx(inputs)); -} +}; + +export const getErrorMessage = (errorCode: string | null) => { + if (!errorCode) { + return { + isSuccess: false, + errorTitle: '알 수 없는 오류', + errorMessage: '회원가입이 실패했어요.', + }; + } + switch (errorCode) { + case 'email_address_invalid': + return { + isSuccess: false, + errorTitle: '유효하지 않은 이메일', + errorMessage: '이메일 주소가 올바르지 않아요.', + }; + case 'weak_password': + return { + isSuccess: false, + errorTitle: '약한 비밀번호', + errorMessage: '비밀번호가 최소 6자 이상이어야 해요.', + }; + case 'email_not_confirmed': + return { + isSuccess: false, + errorTitle: '인증되지 않은 계정', + errorMessage: '이메일 인증이 필요해요.', + }; + case 'invalid_credentials': + return { + isSuccess: false, + errorTitle: '잘못된 정보', + errorMessage: '이메일 또는 비밀번호가 일치하지 않아요.', + }; + default: + return { + isSuccess: false, + errorTitle: '알 수 없는 오류', + errorMessage: '회원가입이 실패했어요.', + }; + } +}; diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 5df9dd3..9f44d5a 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -2,7 +2,7 @@ import { Eye, EyeOff } from 'lucide-react'; import Link from 'next/link'; -import { redirect } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; @@ -11,6 +11,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { useAuthStore } from '@/lib/store/useAuthStore'; +import { useModalStore } from '@/lib/store/useModalStore'; import KakaoLogin from './KakaoLogin'; @@ -20,13 +21,21 @@ export default function LoginPage() { const [showPassword, setShowPassword] = useState(false); const { isLoading, isAuthenticated, login, checkAuth } = useAuthStore(); + const { openMessage } = useModalStore(); + const router = useRouter(); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - const success = await login(email, password); - checkAuth(); - if (success) { - // redirect('/'); + const { isSuccess, errorTitle, errorMessage } = await login(email, password); + if (isSuccess) { + checkAuth(); + router.push('/'); + } else { + openMessage({ + title: errorTitle, + message: errorMessage || '로그인 실패', + variant: 'error', + }); } }; @@ -35,9 +44,9 @@ export default function LoginPage() { toast.success('로그인 확인', { description: '이미 로그인 하셨어요!', }); - // redirect('/'); } - }, [isAuthenticated]); + router.push('/'); + }, [isAuthenticated, router]); return (
{ e.preventDefault(); if (password !== confirmPassword) { - alert('비밀번호가 일치하지 않습니다.'); + openMessage({ + title: '일치하지 않는 비밀번호', + message: '비밀번호가 일치하지 않습니다.', + variant: 'error', + }); return; } - const result = await register(email, password); - console.log('result : ', result); - if (result) redirect('/login'); + const { isSuccess, errorTitle, errorMessage } = await register(email, password); + + if (isSuccess) { + openMessage({ + title: '회원가입 성공', + message: '입력한 이메일로 인증 메일을 보냈어요.', + variant: 'success', + onButtonClick: () => router.push('/login'), + }); + } else { + openMessage({ + title: errorTitle, + message: errorMessage || '회원가입 실패', + variant: 'error', + }); + } }; return ( From 3ca1ad24236c52eeb0cb0a5577c819592fa652e6 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 4 Apr 2025 18:29:19 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat=20:=20SideBar=20=EC=B6=94=EA=B0=80.=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83,=20=EB=AC=B8=EC=9D=98=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/Header.tsx | 14 +-- apps/web/app/SideBar.tsx | 151 +++++++++++++++++++++++++++ apps/web/app/components/ui/sheet.tsx | 130 +++++++++++++++++++++++ apps/web/app/login/page.tsx | 2 +- 4 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 apps/web/app/SideBar.tsx create mode 100644 apps/web/app/components/ui/sheet.tsx diff --git a/apps/web/app/Header.tsx b/apps/web/app/Header.tsx index 0e3cdf7..8b8ccc5 100644 --- a/apps/web/app/Header.tsx +++ b/apps/web/app/Header.tsx @@ -1,26 +1,20 @@ 'use client'; -import { User } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { useAuthStore } from '@/lib/store/useAuthStore'; +import SideBar from './SideBar'; export default function Header() { // login const router = useRouter(); - const { logout } = useAuthStore(); - - const handleLogout = async () => { - await logout(); - }; return ( -
+
logo router.push('/')} /> -
handleLogout()}>로그아웃
- router.push('/login')} /> + + {/* router.push('/login')} /> */}
); } diff --git a/apps/web/app/SideBar.tsx b/apps/web/app/SideBar.tsx new file mode 100644 index 0000000..df7853e --- /dev/null +++ b/apps/web/app/SideBar.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { LogOut, Mail, Menu, Pencil, User } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@/components/ui/sheet'; +import { useAuthStore } from '@/lib/store/useAuthStore'; + +import { Input } from './components/ui/input'; + +const SideBar = () => { + // 목업 인증 상태 + const { user, isAuthenticated, logout } = useAuthStore(); + const [isOpen, setIsOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [newNickname, setNewNickname] = useState(user?.nickname || ''); + + const router = useRouter(); + + const handleEditStart = () => { + setIsEditing(true); + setNewNickname(user?.nickname || ''); + }; + + // 닉네임 수정 취소 + const handleEditCancel = () => { + setIsEditing(false); + setNewNickname(user?.nickname || ''); + }; + + const handleEditSave = async () => { + // try { + // // TODO: API 호출하여 닉네임 업데이트 + // // await updateNickname(newNickname); + // setIsEditing(false); + // toast.success('닉네임이 변경되었습니다.'); + // } catch (error) { + // toast.error('닉네임 변경에 실패했습니다.'); + // } + }; + + const handleLogin = () => { + console.log('login'); + console.log('isAuthenticated', isAuthenticated); + router.push('/login'); + setIsOpen(false); + }; + + const handleLogout = () => { + logout(); + window.location.reload(); + }; + + const handleClickContact = () => { + const contactUrl = 'https://walla.my/survey/K79c5bC6alDqc1qiaaES'; + window.open(contactUrl, '_blank'); + }; + + return ( + + + + + + + 메뉴 + +
+
+
+ ? +
+
+
+ {isEditing ? ( + // 수정 모드 +
+ setNewNickname(e.target.value)} + className="h-8 w-32" + maxLength={10} + /> + + +
+ ) : ( + // 표시 모드 +
+ {user ? user.nickname : '손님'} + {user && ( +
+ +
+ )} +
+ )} +
+
+
+ + + +
+ +
+
+ + {isAuthenticated ? ( + + ) : ( + + )} + +
+
+ ); +}; + +export default SideBar; diff --git a/apps/web/app/components/ui/sheet.tsx b/apps/web/app/components/ui/sheet.tsx new file mode 100644 index 0000000..64e9453 --- /dev/null +++ b/apps/web/app/components/ui/sheet.tsx @@ -0,0 +1,130 @@ +'use client'; + +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Sheet({ ...props }: React.ComponentProps) { + return ; +} + +function SheetTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function SheetClose({ ...props }: React.ComponentProps) { + return ; +} + +function SheetPortal({ ...props }: React.ComponentProps) { + return ; +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SheetContent({ + className, + children, + side = 'right', + ...props +}: React.ComponentProps & { + side?: 'top' | 'right' | 'bottom' | 'left'; +}) { + return ( + + + + {children} + + + Close + + + + ); +} + +function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function SheetTitle({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 9f44d5a..a694c44 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -44,8 +44,8 @@ export default function LoginPage() { toast.success('로그인 확인', { description: '이미 로그인 하셨어요!', }); + router.push('/'); } - router.push('/'); }, [isAuthenticated, router]); return ( From b06f5d2c2da73be6d2c4ab520bacf631512e31b4 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 4 Apr 2025 19:15:12 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat=20:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=97=85.?= =?UTF-8?q?=20update=20=EC=95=88=EB=90=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EA=B0=90=EC=A7=80.=20#gpt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/Header.tsx | 6 +-- apps/web/app/SideBar.tsx | 55 +++++++++++++++----------- apps/web/app/lib/store/useAuthStore.ts | 14 ++++++- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/apps/web/app/Header.tsx b/apps/web/app/Header.tsx index 8b8ccc5..dc22b11 100644 --- a/apps/web/app/Header.tsx +++ b/apps/web/app/Header.tsx @@ -3,18 +3,16 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import SideBar from './SideBar'; +import Sidebar from './Sidebar'; export default function Header() { - // login const router = useRouter(); return (
logo router.push('/')} /> - - {/* router.push('/login')} /> */} +
); } diff --git a/apps/web/app/SideBar.tsx b/apps/web/app/SideBar.tsx index df7853e..db31264 100644 --- a/apps/web/app/SideBar.tsx +++ b/apps/web/app/SideBar.tsx @@ -3,6 +3,7 @@ import { LogOut, Mail, Menu, Pencil, User } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; @@ -20,7 +21,7 @@ import { Input } from './components/ui/input'; const SideBar = () => { // 목업 인증 상태 - const { user, isAuthenticated, logout } = useAuthStore(); + const { user, isAuthenticated, logout, changeNickname } = useAuthStore(); const [isOpen, setIsOpen] = useState(false); const [isEditing, setIsEditing] = useState(false); const [newNickname, setNewNickname] = useState(user?.nickname || ''); @@ -39,14 +40,22 @@ const SideBar = () => { }; const handleEditSave = async () => { - // try { - // // TODO: API 호출하여 닉네임 업데이트 - // // await updateNickname(newNickname); - // setIsEditing(false); - // toast.success('닉네임이 변경되었습니다.'); - // } catch (error) { - // toast.error('닉네임 변경에 실패했습니다.'); - // } + if (newNickname.length < 2) { + toast.error('닉네임 수정 실패', { + description: '닉네임은 2자 이상이어야 합니다.', + }); + return; + } + + if (newNickname === user?.nickname) { + toast.error('닉네임 수정 실패', { + description: '이전과 동일한 닉네임입니다다.', + }); + return; + } + + const result = await changeNickname(newNickname); + console.log('result', result); }; const handleLogin = () => { @@ -82,27 +91,29 @@ const SideBar = () => {
?
-
-
+
+
{isEditing ? ( // 수정 모드 -
+ <> setNewNickname(e.target.value)} - className="h-8 w-32" + className="h-8 w-32 text-center" maxLength={10} /> - - -
+
+ + +
+ ) : ( // 표시 모드 -
+ <> {user ? user.nickname : '손님'} {user && (
@@ -111,7 +122,7 @@ const SideBar = () => {
)} -
+ )}
diff --git a/apps/web/app/lib/store/useAuthStore.ts b/apps/web/app/lib/store/useAuthStore.ts index 3cc0a33..f856140 100644 --- a/apps/web/app/lib/store/useAuthStore.ts +++ b/apps/web/app/lib/store/useAuthStore.ts @@ -24,10 +24,11 @@ interface AuthState { register: (email: string, password: string) => Promise; // 반환 타입 변경 login: (email: string, password: string) => Promise; // 반환 타입 변경 authKaKaoLogin: () => Promise; - logout: () => Promise; checkAuth: () => Promise; insertUser: (id: string) => Promise; + + changeNickname: (nickname: string) => Promise; } interface ResponseState { @@ -164,4 +165,15 @@ export const useAuthStore = create((set, get) => ({ console.error('insertUser 오류:', error); } }, + changeNickname: async (nickname: string) => { + try { + const { user } = get(); + if (!user) throw new Error('User not found'); + + const result = await supabase.from('users').update({ nickname: nickname }).eq('id', user.id); + console.log('result : ', result); + } catch (error) { + console.error('changeNickname 오류:', error); + } + }, })); From 6863868d609238f5da8d5e98b0b7232405b6e5e3 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Mon, 7 Apr 2025 18:36:10 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat=20:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/SideBar.tsx | 17 +- apps/web/app/lib/store/middleware.ts | 27 +++ apps/web/app/lib/store/useAuthStore.ts | 300 +++++++++++++------------ apps/web/package.json | 1 + 4 files changed, 189 insertions(+), 156 deletions(-) create mode 100644 apps/web/app/lib/store/middleware.ts diff --git a/apps/web/app/SideBar.tsx b/apps/web/app/SideBar.tsx index db31264..91a767a 100644 --- a/apps/web/app/SideBar.tsx +++ b/apps/web/app/SideBar.tsx @@ -3,7 +3,6 @@ import { LogOut, Mail, Menu, Pencil, User } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; -import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; @@ -40,22 +39,8 @@ const SideBar = () => { }; const handleEditSave = async () => { - if (newNickname.length < 2) { - toast.error('닉네임 수정 실패', { - description: '닉네임은 2자 이상이어야 합니다.', - }); - return; - } - - if (newNickname === user?.nickname) { - toast.error('닉네임 수정 실패', { - description: '이전과 동일한 닉네임입니다다.', - }); - return; - } - const result = await changeNickname(newNickname); - console.log('result', result); + if (result) setIsEditing(false); }; const handleLogin = () => { diff --git a/apps/web/app/lib/store/middleware.ts b/apps/web/app/lib/store/middleware.ts new file mode 100644 index 0000000..4228ef9 --- /dev/null +++ b/apps/web/app/lib/store/middleware.ts @@ -0,0 +1,27 @@ +// const withLoading = async (set: any, fn: () => Promise): Promise => { +import { toast } from 'sonner'; + +export const withLoading = async ( + set: (fn: (state: S) => void) => void, + get: () => S, + fn: () => Promise, +): Promise => { + const state = get(); + + if (state.isLoading) { + toast.warning('기다려주세요', { description: '요청을 처리 중입니다.' }); + return Promise.resolve(false as T); + } + + set(state => { + state.isLoading = true; + }); + + try { + return await fn(); + } finally { + set(state => { + state.isLoading = false; + }); + } +}; diff --git a/apps/web/app/lib/store/useAuthStore.ts b/apps/web/app/lib/store/useAuthStore.ts index f856140..cee093d 100644 --- a/apps/web/app/lib/store/useAuthStore.ts +++ b/apps/web/app/lib/store/useAuthStore.ts @@ -1,11 +1,14 @@ import { AuthError } from '@supabase/supabase-js'; import { toast } from 'sonner'; import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; import { createClient } from '@/lib/supabase/client'; import { getErrorMessage } from '../utils'; +import { withLoading } from './middleware'; + export const supabase = createClient(); // 사용자 타입 정의 @@ -21,159 +24,176 @@ interface AuthState { isAuthenticated: boolean; // 액션 - register: (email: string, password: string) => Promise; // 반환 타입 변경 - login: (email: string, password: string) => Promise; // 반환 타입 변경 + register: (email: string, password: string) => Promise; // 반환 타입 변경 + login: (email: string, password: string) => Promise; // 반환 타입 변경 authKaKaoLogin: () => Promise; logout: () => Promise; checkAuth: () => Promise; insertUser: (id: string) => Promise; - changeNickname: (nickname: string) => Promise; + changeNickname: (nickname: string) => Promise; } -interface ResponseState { +// useModalStore에서 사용할 데이터를 전달해줘야 할 때의 타입 +// 기본적인 toast 제어는 store 단에서 처리할 계획 +interface ModalResponseState { isSuccess: boolean; errorTitle?: string; errorMessage?: string; } -export const useAuthStore = create((set, get) => ({ - user: null, - isLoading: false, - isAuthenticated: false, - - register: async (email: string, password: string) => { - try { - set({ isLoading: true }); - const supabase = createClient(); - - const { data, error } = await supabase.auth.signUp({ email, password }); - if (error) throw error; - - if (data.user?.identities?.length === 0) { - return { - isSuccess: false, - errorTitle: '이메일 중복', - errorMessage: '이미 가입된 이메일입니다.', - }; - } - - toast.success('회원가입 성공', { - description: '만나서 반가워요!', - }); - - return { - isSuccess: true, - }; - } catch (error) { - if (error instanceof AuthError) { - return getErrorMessage(error.code as string); - } else { - return { - isSuccess: false, - errorTitle: '회원가입 실패', - errorMessage: '회원 가입이 실패했어요.', - }; - } - } finally { - set({ isLoading: false }); - } - }, - // 로그인 액션 - login: async (email: string, password: string) => { - try { - set({ isLoading: true }); - const supabase = createClient(); - - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - if (error) throw error; - - toast.success('로그인 성공', { - description: '다시 만나서 반가워요!', - }); - - return { - isSuccess: true, - }; // 성공 시 true 반환 - } catch (error) { - const { code } = error as AuthError; - return getErrorMessage(code as string); - } finally { - set({ isLoading: false }); - } - }, - authKaKaoLogin: async () => { - try { - const { error } = await supabase.auth.signInWithOAuth({ - provider: 'kakao', - }); - if (error) throw error; - - toast.success('카카오 로그인 성공', { - description: '다시 만나서 반가워요!', +export const useAuthStore = create( + immer((set, get) => ({ + user: null, + isLoading: false, + isAuthenticated: false, + + register: async (email, password) => { + return await withLoading(set, get, async () => { + try { + const { data, error } = await supabase.auth.signUp({ email, password }); + if (error) throw error; + + if (data.user?.identities?.length === 0) { + return { + isSuccess: false, + errorTitle: '이메일 중복', + errorMessage: '이미 가입된 이메일입니다.', + }; + } + + toast.success('회원가입 성공', { description: '만나서 반가워요!' }); + return { isSuccess: true }; + } catch (error) { + if (error instanceof AuthError) { + return getErrorMessage(error.code as string); + } + return { + isSuccess: false, + errorTitle: '회원가입 실패', + errorMessage: '회원 가입이 실패했어요.', + }; + } }); - return true; - } catch (error) { - console.error('카카오 로그인 오류:', error); - toast.error('카카오 로그인 실패', { - description: '카카오 로그인에 문제가 있어요...', + }, + // 로그인 액션 + login: async (email, password) => { + return await withLoading(set, get, async () => { + try { + const { error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) throw error; + toast.success('로그인 성공', { description: '다시 만나서 반가워요!' }); + return { isSuccess: true }; + } catch (error) { + const { code } = error as AuthError; + return getErrorMessage(code as string); + } }); - - return false; - } - }, - // 로그아웃 액션 - logout: async () => { - await supabase.auth.signOut(); - set({ user: null, isAuthenticated: false }); - }, - - // 인증 상태 확인 - checkAuth: async () => { - try { - const { data, error } = await supabase.auth.getUser(); - - if (error) throw error; - if (!get().user) { - const id = data.user.id; - const { data: existingUser } = await supabase - .from('users') - .select('*') - .eq('id', id) - .single(); - console.log('existingUser : ', existingUser); - if (!existingUser) get().insertUser(id); - else { - set({ user: existingUser, isAuthenticated: true }); + }, + authKaKaoLogin: async () => { + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'kakao', + }); + if (error) throw error; + + return true; + } catch (error) { + console.error('카카오 로그인 오류:', error); + toast.error('카카오 로그인 실패', { + description: '카카오 로그인에 문제가 있어요...', + }); + + return false; + } + }, + // 로그아웃 액션 + logout: async () => { + await supabase.auth.signOut(); + set({ user: null, isAuthenticated: false }); + }, + + // 인증 상태 확인 + checkAuth: async () => { + try { + const { data, error } = await supabase.auth.getUser(); + + if (error) throw error; + if (!get().user) { + const id = data.user.id; + const { data: existingUser } = await supabase + .from('users') + .select('*') + .eq('id', id) + .single(); + + if (!existingUser) get().insertUser(id); + else { + set(state => { + state.user = existingUser; + state.isAuthenticated = true; + }); + } } + return true; + } catch (error) { + console.error('checkAuth 오류:', error); + return false; + } + }, + insertUser: async (id: string) => { + try { + const { data: user, error } = await supabase.from('users').insert({ id }).select().single(); + if (error) throw error; + set(state => { + state.user = user; + state.isAuthenticated = true; + }); + } catch (error) { + console.error('insertUser 오류:', error); } - return true; - } catch (error) { - console.error('checkAuth 오류:', error); - return false; - } - }, - insertUser: async (id: string) => { - try { - const { data: user, error } = await supabase.from('users').insert({ id }).select().single(); - if (error) throw error; - set({ user: user, isAuthenticated: true }); - } catch (error) { - console.error('insertUser 오류:', error); - } - }, - changeNickname: async (nickname: string) => { - try { - const { user } = get(); - if (!user) throw new Error('User not found'); - - const result = await supabase.from('users').update({ nickname: nickname }).eq('id', user.id); - console.log('result : ', result); - } catch (error) { - console.error('changeNickname 오류:', error); - } - }, -})); + }, + changeNickname: async (nickname: string) => { + return await withLoading(set, get, async () => { + try { + const { user } = get(); + if (!user) throw new Error('No user found in store'); + + if (nickname.length < 2) { + toast.error('닉네임 수정 실패', { + description: '닉네임은 2자 이상이어야 합니다.', + }); + return false; + } + + if (nickname === user.nickname) { + toast.error('닉네임 수정 실패', { + description: '이전과 동일한 닉네임입니다.', + }); + return false; + } + + const result = await supabase + .from('users') + .update({ nickname: nickname }) + .eq('id', user.id) + .select() + .single(); + set({ user: result.data, isAuthenticated: true }); + + toast.success('닉네임 수정 성공', { + description: '닉네임이 성공적으로 수정되었어요!', + }); + return true; + } catch (error) { + toast.error('닉네임 수정 실패', { + description: '닉네임 수정에 실패했어요.', + }); + + console.error('changeNickname 오류:', error); + return false; + } + }); + }, + })), +); diff --git a/apps/web/package.json b/apps/web/package.json index a4eb0f2..8962af5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,6 +29,7 @@ "axios": "^1.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "immer": "^10.1.1", "lucide-react": "^0.483.0", "next": "15.2.2", "next-themes": "^0.4.6", From 420d6483faa5addb9829b0ab2e4ae71c9646fbee Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Mon, 7 Apr 2025 18:37:10 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat=20:=20=EB=B9=84=EB=B0=80=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9E=AC=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/app/api/songs/[type]/[param]/route.ts | 78 ------- apps/web/app/find/page.tsx | 22 -- apps/web/app/lib/store/useAuthStore.ts | 46 +++- apps/web/app/login/page.tsx | 6 + apps/web/app/update-password/page.tsx | 202 ++++++++++++++++++ 5 files changed, 252 insertions(+), 102 deletions(-) delete mode 100644 apps/web/app/api/songs/[type]/[param]/route.ts delete mode 100644 apps/web/app/find/page.tsx create mode 100644 apps/web/app/update-password/page.tsx diff --git a/apps/web/app/api/songs/[type]/[param]/route.ts b/apps/web/app/api/songs/[type]/[param]/route.ts deleted file mode 100644 index 7ed8d01..0000000 --- a/apps/web/app/api/songs/[type]/[param]/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -// app/api/songs/[type]/[param]/route.ts -import { NextRequest, NextResponse } from 'next/server'; - -import { - Brand, - Period, - getComposer, - getLyricist, - getNo, - getPopular, - getRelease, - getSinger, - getSong, -} from '@repo/api'; - -export async function GET( - request: NextRequest, - { params }: { params: { type: string; param: string } }, -) { - try { - const { type, param } = params; - const searchParams = request.nextUrl.searchParams; - const brand = searchParams.get('brand') as Brand | undefined; - - let result = null; - - switch (type) { - case 'title': - result = await getSong({ title: param, brand }); - break; - - case 'singer': - result = await getSinger({ singer: param, brand }); - break; - - case 'composer': - result = await getComposer({ composer: param, brand }); - break; - - case 'lyricist': - result = await getLyricist({ lyricist: param, brand }); - break; - - case 'no': - result = await getNo({ no: param, brand }); - break; - - case 'release': - result = await getRelease({ release: param, brand }); - break; - - case 'popular': - // popular의 경우는 좀 특별하게 처리 - // param은 brand 값이 되고, period는 쿼리 파라미터로 받음 - const period = searchParams.get('period') as Period; - if (!period) { - return NextResponse.json( - { error: '기간(period)은 필수 파라미터입니다.' }, - { status: 400 }, - ); - } - result = await getPopular({ brand: param as Brand, period }); - break; - - default: - return NextResponse.json({ error: '지원하지 않는 검색 유형입니다' }, { status: 400 }); - } - - if (!result) { - return NextResponse.json({ error: '검색 결과가 없습니다' }, { status: 404 }); - } - - return NextResponse.json({ data: result }); - } catch (error) { - console.error('API 요청 오류:', error); - return NextResponse.json({ error: '서버 오류가 발생했습니다' }, { status: 500 }); - } -} diff --git a/apps/web/app/find/page.tsx b/apps/web/app/find/page.tsx deleted file mode 100644 index e7b8eea..0000000 --- a/apps/web/app/find/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -import { getComposer } from '@repo/api'; - -export default function Home() { - const [search, setSearch] = useState(''); - - const handleSearch = (e: React.ChangeEvent) => { - setSearch(e.target.value); - }; - - return ( -
- -
-

fotter

-
-
- ); -} diff --git a/apps/web/app/lib/store/useAuthStore.ts b/apps/web/app/lib/store/useAuthStore.ts index cee093d..93ffe7f 100644 --- a/apps/web/app/lib/store/useAuthStore.ts +++ b/apps/web/app/lib/store/useAuthStore.ts @@ -32,6 +32,8 @@ interface AuthState { insertUser: (id: string) => Promise; changeNickname: (nickname: string) => Promise; + sendPasswordResetLink: (email: string) => Promise; + changePassword: (password: string) => Promise; } // useModalStore에서 사용할 데이터를 전달해줘야 할 때의 타입 @@ -161,14 +163,14 @@ export const useAuthStore = create( if (nickname.length < 2) { toast.error('닉네임 수정 실패', { - description: '닉네임은 2자 이상이어야 합니다.', + description: '닉네임은 2자 이상이어야 해요.', }); return false; } if (nickname === user.nickname) { toast.error('닉네임 수정 실패', { - description: '이전과 동일한 닉네임입니다.', + description: '이전과 동일한 닉네임이에요.', }); return false; } @@ -195,5 +197,45 @@ export const useAuthStore = create( } }); }, + sendPasswordResetLink: async (email: string) => { + return await withLoading(set, get, async () => { + try { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `http://localhost:3000/update-password`, + }); + if (error) { + throw error; + } + toast.success('재설정 링크 발송 완료', { + description: `${email}로 비밀번호 재설정 링크를 발송했어요. 이메일을 확인해주세요.`, + }); + } catch (error) { + console.error('비밀번호 재설정 링크 발송 실패:', error); + toast.error('링크 발송 실패', { + description: '비밀번호 재설정 링크 발송 중 오류가 발생했어요.', + }); + } + }); + }, + + changePassword: async (password: string) => { + return await withLoading(set, get, async () => { + try { + const { error } = await supabase.auth.updateUser({ password }); + if (error) throw error; + + toast.success('비밀번호 변경 완료', { + description: '비밀번호가 성공적으로 변경되었어요.', + }); + return true; + } catch (error) { + console.error('비밀번호 변경 실패:', error); + toast.error('비밀번호 변경 실패', { + description: '비밀번호 변경 중 오류가 발생했어요.', + }); + return false; + } + }); + }, })), ); diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index a694c44..a99a1f0 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -102,6 +102,12 @@ export default function LoginPage() { 계정이 없으신가요? 회원가입
+ +
+ + 비밀번호를 잊으셨나요? 비밀번호 재설정 + +
diff --git a/apps/web/app/update-password/page.tsx b/apps/web/app/update-password/page.tsx new file mode 100644 index 0000000..97eaf21 --- /dev/null +++ b/apps/web/app/update-password/page.tsx @@ -0,0 +1,202 @@ +'use client'; + +import { AlertCircle, ArrowLeft, CheckCircle2, Eye, EyeOff } from 'lucide-react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import type React from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useAuthStore } from '@/lib/store/useAuthStore'; +import { useModalStore } from '@/lib/store/useModalStore'; +import { createClient } from '@/lib/supabase/client'; + +export default function UpdatePasswordPage() { + // 상태 관리 + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [step, setStep] = useState<'email' | 'reset'>('reset'); + + const { isLoading, sendPasswordResetLink, changePassword } = useAuthStore(); + const { openMessage } = useModalStore(); + + const router = useRouter(); + + // 이메일 제출 처리 (비밀번호 재설정 링크 요청) + const handleSendResetLink = async (e: React.FormEvent) => { + e.preventDefault(); + await sendPasswordResetLink(email); + }; + + // 비밀번호 재설정 처리 + const handleUpdatePassword = async (e: React.FormEvent) => { + e.preventDefault(); + + // 비밀번호 일치 여부 확인 + if (password !== confirmPassword) { + toast.error('비밀번호 불일치', { + description: '비밀번호를 다시 확인해주세요.', + }); + return; + } + + const result = await changePassword(password); + + if (result) { + openMessage({ + title: '비밀번호 변경 성공', + message: '비밀번호가 성공적으로 변경되었어요.', + variant: 'success', + onButtonClick: () => router.push('/login'), + }); + } + }; + + useEffect(() => { + const supabase = createClient(); + supabase.auth.onAuthStateChange(async (event, session) => { + if (event == 'PASSWORD_RECOVERY') { + setStep('reset'); // 비밀번호 재설정 단계로 이동 + } + }); + }, []); + + return ( +
+
+ {step === 'email' ? ( + // 이메일 입력 단계 + <> +
+

비밀번호 찾기

+

+ 가입한 이메일 주소를 입력하시면 비밀번호 재설정 링크를 보내드립니다. +

+
+ +
+
+ + setEmail(e.target.value)} + required + /> +
+ + + +
+ + + 로그인 페이지로 돌아가기 + +
+
+ + ) : ( + // 비밀번호 재설정 단계 + <> +
+

비밀번호 재설정

+

새로운 비밀번호를 입력해주세요

+
+ +
+
+ +
+ setPassword(e.target.value)} + required + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + required + className={ + confirmPassword && + (password === confirmPassword ? 'border-green-500' : 'border-red-500') + } + /> + +
+ + {/* 비밀번호 일치 여부 */} + {confirmPassword && ( +
+ {password === confirmPassword ? ( + <> + + 비밀번호가 일치합니다 + + ) : ( + <> + + 비밀번호가 일치하지 않습니다 + + )} +
+ )} +
+ + + +
+ + + 로그인 페이지로 돌아가기 + +
+
+ + )} +
+
+ ); +}