Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/app/api/v2/word/[hanzi]/route.ts
Original file line number Diff line number Diff line change
@@ -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 = `
Expand Down
37 changes: 18 additions & 19 deletions src/components/Bookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
<button
className=" rounded-full p-2 transition duration-300 hover:bg-gray-100 hover:opacity-100 opacity-90"
className="rounded-full p-2 transition duration-300 hover:bg-gray-100 hover:opacity-100 opacity-90"
onClick={handleClickBookmark}
>
<Star
fill={`${marked ? '#facc15' : 'none'}`}
stroke={`${marked ? '#facc15' : 'currentColor'}`}
fill={marked ? '#facc15' : 'none'}
stroke={marked ? '#facc15' : 'currentColor'}
/>
</button>
);
Expand Down
80 changes: 46 additions & 34 deletions src/components/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { logout } from '@/lib/supabase/userApi';
import { LogInIcon } from 'lucide-react';
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 = () => (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
className="w-5 h-5"
Expand All @@ -41,14 +41,14 @@ 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"
/>
<path fill="none" d="M0 0h48v48H0z" />
</svg>
);

const Login = () => {
const { data: user } = useUser();
const queryClient = useQueryClient();
const router = useRouter();
const { loginOpen, openLoginModal, closeLoginModal } = useModal();

const handleLogin = () => {
try {
Expand Down Expand Up @@ -90,48 +90,60 @@ const Login = () => {
className="mr-3 rounded-full"
src={user?.user_metadata?.avatar_url}
alt="profile image"
width="50"
height="50"
/>
<AvatarFallback className="rounded-full bg-zinc-600 text-white text-sm">
{user?.user_metadata.name.charAt(0)}
</AvatarFallback>
</Avatar>
</Button>
<Button variant={'outline'} onClick={handleLogout}>

<Button variant="outline" onClick={handleLogout}>
로그아웃
</Button>
</div>
) : (
<Dialog>
<>
{/* 기존 DialogTrigger에서 사용하던 주석 */}
{/* asChild: 중첩된 DOM 태그를 없애고 자식 컴포넌트를 그대로 트리거로 사용 */}
<DialogTrigger asChild>
<Button
className="flex justify-center items-center gap-2"
variant="outline"
>
<LogInIcon />
<span>로그인</span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle className="">로그인</DialogTitle>
<DialogDescription className="py-10 flex justify-center">
<Button
onClick={handleLogin}
variant="outline"
className="w-full p-6 gap-3 flex items-center justify-center"
>
<GoogleIcon />
<span className="text-sm text-gray-700">
구글 계정으로 로그인
</span>
</Button>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
{/* 지금은 trigger를 상태 기반으로 제어하므로 asChild는 사용되지 않음 */}

{/* 트리거 버튼 */}
<Button
className="flex justify-center items-center gap-2"
variant="outline"
onClick={openLoginModal}
>
<LogInIcon />
<span>로그인</span>
</Button>

{/* 로그인 모달 */}
<Dialog
open={loginOpen}
onOpenChange={(open) => {
if (!open) closeLoginModal();
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>로그인</DialogTitle>

<DialogDescription className="py-10 flex justify-center">
<Button
onClick={handleLogin}
variant="outline"
className="w-full p-6 gap-3 flex items-center justify-center"
>
<GoogleIcon />
<span className="text-sm text-gray-700">
구글 계정으로 로그인
</span>
</Button>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
)}
</>
);
Expand Down
30 changes: 10 additions & 20 deletions src/components/RequireLogin.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -12,8 +11,6 @@ const RequireLogin = () => {
redirectTo: process.env.NEXT_PUBLIC_BASE_URL,
},
});

// 로그인 완료 후 home으로 이동함
} catch (error) {
console.error(`[ERROR] Failed login: ${error}`);
toast.error('로그인 실패. 다시 시도해주세요.');
Expand All @@ -22,23 +19,16 @@ const RequireLogin = () => {

return (
<div className="flex flex-col items-center justify-center min-h-[70vh] text-center space-y-6">
<Card className="p-10 max-w-sm border-none">
<div className="flex flex-col items-center space-y-3">
<div className="text-3xl">🚨</div>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">
로그인해주세요.
</h2>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-10">
계정이 없다면 회원가입 후 이용 가능합니다.
</p>
<Button
onClick={handleLogin}
className="w-full font-semibold py-5 mt-2"
>
로그인하기
</Button>
</div>
</Card>
<div className="text-4xl">🔒</div>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">
로그인 후 이용할 수 있어요
</h2>
<p className="text-gray-600 dark:text-gray-300 text-sm">
북마크, 학습 기록 등 모든 기능을 사용하려면 로그인해주세요.
</p>
<Button onClick={handleLogin} className="px-8 py-5 font-semibold">
로그인하기
</Button>
</div>
);
};
Expand Down
2 changes: 0 additions & 2 deletions src/components/mypage/QuizHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/components/quiz/ClientQuizPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -66,11 +67,13 @@ const ClientQuizPage = ({ level }: Props) => {
}
};

if (!user) return;

fetchQuizData();
}, [level, user]);

if (!user) {
return <RequireLogin />;
}

if (getUserError) {
toast.error(getUserError.message);
}
Expand Down Expand Up @@ -651,6 +654,7 @@ const ClientQuizPage = ({ level }: Props) => {
</div>
</Card>
)}
여기오냐
</div>
</div>
</div>
Expand Down
27 changes: 12 additions & 15 deletions src/components/word/ClientWordDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,22 +287,19 @@ const ClientWordDetail = ({ wordId }: WordDetailProps) => {
toast.error('복사에 실패했어요. 다시 시도해 주세요.');
}
};

return (
<>
<div className="text-center mb-8">
<HanziWriter characters={wordData.word.split('')} />
<div className="text-2xl text-gray-600 mb-2">[{wordData.pinyin}]</div>
<div className="text-xl text-gray-700 mb-4 flex justify-center items-center gap-2">
{wordData.meaning} ({wordData.part_of_speech})
<Tooltip>
<TooltipTrigger className="text-sm font-medium text-gray-600">
<Info width={18} />
</TooltipTrigger>
<TooltipContent>
{partOfSpeechMap[wordData.part_of_speech]}
</TooltipContent>
</Tooltip>
</div>
<div>
<Tooltip>
<TooltipTrigger className="text-sm font-medium text-gray-600">
<Info width={18} className="hidden md:block" />
</TooltipTrigger>
<TooltipContent>
{partOfSpeechMap[wordData.part_of_speech]}
</TooltipContent>
</Tooltip>

<div className="flex justify-center gap-4 mb-6">
<Bookmark id={wordId} isBookmarked={wordData.is_bookmarked} />
{audioUrl && <PlayAudioButton audioUrl={audioUrl} />}
Expand Down Expand Up @@ -409,7 +406,7 @@ const ClientWordDetail = ({ wordId }: WordDetailProps) => {
동의어/유의어
</h3>
{synonyms.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-3 gap-4">
{synonyms.map((word, index) => (
<div
key={index}
Expand Down
10 changes: 6 additions & 4 deletions src/components/word/ClientWordList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,20 @@ const ClientWordList = ({
{showNewOnly && `(${newWords.length}개)`}
</Label>
</div> */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
{/* {(showNewOnly ? newWords : allWords).map((word, index) => ( */}
{allWords.map((word, index) => (
<Link key={`${word.id}${index}`} href={`/word/${level}/${word.id}`}>
<Card className="hover:bg-sky-100">
<CardHeader className="text-center">
<CardTitle className="text-4xl">{word.word}</CardTitle>
<CardHeader className="text-center px-0">
<CardTitle className="text-3xl md:text-4xl">
{word.word}
</CardTitle>
<CardDescription className="text-xl">
[{word.pinyin}]
</CardDescription>
</CardHeader>
<CardContent className="text-center">
<CardContent className="text-center text-sm font-semibold">
{word.meaning}
<span className="text-blue-400 ml-1">
({word.part_of_speech})
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/useMoal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { create } from 'zustand';

type ModalStore = {
loginOpen: boolean;
openLoginModal: () => void;
closeLoginModal: () => void;
};

export const useModal = create<ModalStore>((set) => ({
loginOpen: false,
openLoginModal: () => set({ loginOpen: true }),
closeLoginModal: () => set({ loginOpen: false }),
}));
Loading
Loading