diff --git a/src/app/api/v2/word/[hanzi]/route.ts b/src/app/api/v2/word/[hanzi]/route.ts index ff0d802..4d319d5 100644 --- a/src/app/api/v2/word/[hanzi]/route.ts +++ b/src/app/api/v2/word/[hanzi]/route.ts @@ -1,10 +1,34 @@ import { NextRequest, NextResponse } from 'next/server'; +import { createClient as createServerClient } from '@/lib/supabase/server'; +import { SupabaseClient, User } from '@supabase/supabase-js'; export async function GET( req: NextRequest, { params }: { params: Promise<{ hanzi: string }> } ) { const { hanzi } = await params; + const supabase = await createServerClient(); + const { data, error } = await supabase + .from('words') + .select( + ` + id, + hanzi, + pinyin, + meaning, + examples:examples ( + id, + cn, + pinyin, + kr + ) + ` + ) + .eq('hanzi', hanzi) + .single(); + + if (error) return Response.json({ error }, { status: 500 }); + const systemPrompt = '너는 중국어 교육 플랫폼의 콘텐츠 생성 도우미야. 입력된 한자 단어를 바탕으로 예문과 한국어로 해석된 문장을 만들어줘.'; const userPrompt = ` diff --git a/src/components/Bookmark.tsx b/src/components/Bookmark.tsx index 28f135d..b28a9df 100644 --- a/src/components/Bookmark.tsx +++ b/src/components/Bookmark.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import { Star } from 'lucide-react'; +import { useUser } from '@/hooks/useUser'; +import { useModal } from '@/hooks/useMoal'; type BookmarkProps = { id: string; @@ -10,35 +12,32 @@ type BookmarkProps = { const Bookmark = ({ id, isBookmarked = false }: BookmarkProps) => { const [marked, setMarked] = useState(isBookmarked); + const { data: user } = useUser(); + const { openLoginModal } = useModal(); const handleClickBookmark = async () => { - setMarked(!marked); - - if (marked) { - await fetch(`/api/v1/bookmark`, { - method: 'DELETE', - body: JSON.stringify({ - wordId: id, - }), - }); - } else { - await fetch(`/api/v1/bookmark`, { - method: 'POST', - body: JSON.stringify({ - wordId: id, - }), - }); + if (!user) { + openLoginModal(); + return; } + + const nextState = !marked; + setMarked(nextState); + + await fetch(`/api/v1/bookmark`, { + method: nextState ? 'POST' : 'DELETE', + body: JSON.stringify({ wordId: id }), + }); }; return ( ); diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 3786679..38a9028 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -9,7 +9,6 @@ import { DialogDescription, DialogHeader, DialogTitle, - DialogTrigger, } from '@/components/ui/dialog'; import { logout } from '@/lib/supabase/userApi'; import { LogInIcon } from 'lucide-react'; @@ -17,10 +16,11 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useRouter } from 'next/navigation'; import { useUser } from '@/hooks/useUser'; import { useQueryClient } from '@tanstack/react-query'; +import { useModal } from '@/hooks/useMoal'; +// Google Icon const GoogleIcon = () => ( ( fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" /> - ); @@ -49,6 +48,7 @@ const Login = () => { const { data: user } = useUser(); const queryClient = useQueryClient(); const router = useRouter(); + const { loginOpen, openLoginModal, closeLoginModal } = useModal(); const handleLogin = () => { try { @@ -90,48 +90,60 @@ const Login = () => { className="mr-3 rounded-full" src={user?.user_metadata?.avatar_url} alt="profile image" - width="50" - height="50" /> {user?.user_metadata.name.charAt(0)} - ) : ( - + <> + {/* 기존 DialogTrigger에서 사용하던 주석 */} {/* asChild: 중첩된 DOM 태그를 없애고 자식 컴포넌트를 그대로 트리거로 사용 */} - - - - - - 로그인 - - - - - - + {/* 지금은 trigger를 상태 기반으로 제어하므로 asChild는 사용되지 않음 */} + + {/* 트리거 버튼 */} + + + {/* 로그인 모달 */} + { + if (!open) closeLoginModal(); + }} + > + + + 로그인 + + + + + + + + )} ); diff --git a/src/components/RequireLogin.tsx b/src/components/RequireLogin.tsx index 6827acf..035e215 100644 --- a/src/components/RequireLogin.tsx +++ b/src/components/RequireLogin.tsx @@ -1,5 +1,4 @@ import { supabase } from '@/lib/supabase/client'; -import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; @@ -12,8 +11,6 @@ const RequireLogin = () => { redirectTo: process.env.NEXT_PUBLIC_BASE_URL, }, }); - - // 로그인 완료 후 home으로 이동함 } catch (error) { console.error(`[ERROR] Failed login: ${error}`); toast.error('로그인 실패. 다시 시도해주세요.'); @@ -22,23 +19,16 @@ const RequireLogin = () => { return (
- -
-
🚨
-

- 로그인해주세요. -

-

- 계정이 없다면 회원가입 후 이용 가능합니다. -

- -
-
+
🔒
+

+ 로그인 후 이용할 수 있어요 +

+

+ 북마크, 학습 기록 등 모든 기능을 사용하려면 로그인해주세요. +

+
); }; diff --git a/src/components/mypage/QuizHistory.tsx b/src/components/mypage/QuizHistory.tsx index 617cec7..45e9ca5 100644 --- a/src/components/mypage/QuizHistory.tsx +++ b/src/components/mypage/QuizHistory.tsx @@ -28,8 +28,6 @@ async function fetchQuizHistorys(limit: number) { const { quizHistory, totalCount } = await response.json(); - console.log('totalCount-', totalCount); - return { quizHistory, totalCount } as { quizHistory: QuizType[]; totalCount: number; diff --git a/src/components/quiz/ClientQuizPage.tsx b/src/components/quiz/ClientQuizPage.tsx index 50e90ce..ee19c24 100644 --- a/src/components/quiz/ClientQuizPage.tsx +++ b/src/components/quiz/ClientQuizPage.tsx @@ -56,6 +56,7 @@ const ClientQuizPage = ({ level }: Props) => { setStartTime(Date.now()); } catch (error) { console.error('[ERROR] Quiz fetch:', error); + toast.error( error instanceof Error ? error.message @@ -66,11 +67,13 @@ const ClientQuizPage = ({ level }: Props) => { } }; - if (!user) return; - fetchQuizData(); }, [level, user]); + if (!user) { + return ; + } + if (getUserError) { toast.error(getUserError.message); } @@ -651,6 +654,7 @@ const ClientQuizPage = ({ level }: Props) => { )} + 여기오냐 diff --git a/src/components/word/ClientWordDetail.tsx b/src/components/word/ClientWordDetail.tsx index 3425040..19e0fd9 100644 --- a/src/components/word/ClientWordDetail.tsx +++ b/src/components/word/ClientWordDetail.tsx @@ -287,22 +287,19 @@ const ClientWordDetail = ({ wordId }: WordDetailProps) => { toast.error('복사에 실패했어요. 다시 시도해 주세요.'); } }; + return ( <> -
- -
[{wordData.pinyin}]
-
- {wordData.meaning} ({wordData.part_of_speech}) - - - - - - {partOfSpeechMap[wordData.part_of_speech]} - - -
+
+ + + + + + {partOfSpeechMap[wordData.part_of_speech]} + + +
{audioUrl && } @@ -409,7 +406,7 @@ const ClientWordDetail = ({ wordId }: WordDetailProps) => { 동의어/유의어 {synonyms.length > 0 ? ( -
+
{synonyms.map((word, index) => (
*/} -
+
{/* {(showNewOnly ? newWords : allWords).map((word, index) => ( */} {allWords.map((word, index) => ( - - {word.word} + + + {word.word} + [{word.pinyin}] - + {word.meaning} ({word.part_of_speech}) diff --git a/src/hooks/useMoal.ts b/src/hooks/useMoal.ts new file mode 100644 index 0000000..46ae1d8 --- /dev/null +++ b/src/hooks/useMoal.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; + +type ModalStore = { + loginOpen: boolean; + openLoginModal: () => void; + closeLoginModal: () => void; +}; + +export const useModal = create((set) => ({ + loginOpen: false, + openLoginModal: () => set({ loginOpen: true }), + closeLoginModal: () => set({ loginOpen: false }), +})); diff --git a/src/lib/supabase/userApi.ts b/src/lib/supabase/userApi.ts index debcc85..a3eaacf 100644 --- a/src/lib/supabase/userApi.ts +++ b/src/lib/supabase/userApi.ts @@ -6,6 +6,10 @@ export const fetchCurrentUser = async (): Promise => { const { data, error } = await supabase.auth.getUser(); if (error) { + if (error.message?.includes('Auth session missing')) { + // 로그인 안 된 상태: 정상, null 반환 + return null; + } console.error(`[ERROR] SELECT User data: ${error}`); throw error; } diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..8827f67 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,12 @@ +module.exports = { + theme: { + screens: { + xs: '480px', + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + }, +};