diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 77975c4..dc56b7a 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -6,7 +6,7 @@ import ClientMyPage from '@/features/mypage/ClientMyPage'; export default function MyPage() { return (
-
+

마이페이지

{/* TODO: 계정 정보 뭘 수정할지 확인 */} diff --git a/src/app/page.tsx b/src/app/page.tsx index 6fe1269..f9d28c4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,26 +1,13 @@ +import CTASection from '@/features/home/CTASection'; import { HeroSection } from '@/features/home/HeroSection'; +import PrimaryContentSection from '@/features/home/PrimaryContentSection'; export default function Home() { return (
- {/*

- 도전할 HSK 급수를 선택하세요 -

-
- {levels.map((item, index) => ( - - ))} -
*/} + +
); } diff --git a/src/app/word/[level]/[id]/page.tsx b/src/app/word/[level]/[id]/page.tsx index 1e53a2a..12496dd 100644 --- a/src/app/word/[level]/[id]/page.tsx +++ b/src/app/word/[level]/[id]/page.tsx @@ -58,7 +58,7 @@ export default async function WordDetailPage({ params }: Props) { }; return ( -
+
); diff --git a/src/app/word/[level]/page.tsx b/src/app/word/[level]/page.tsx index 5e3f70e..5e8834a 100644 --- a/src/app/word/[level]/page.tsx +++ b/src/app/word/[level]/page.tsx @@ -43,7 +43,7 @@ export default async function WordPage({ params }: Props) { } return ( -
+

{level}급 단어

{/* 학습중 으로 상태 변경하려면... usestate 필요. */} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index f0fefcc..c6f5c50 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -55,9 +55,10 @@ export default function Header() { ]; return ( -
+ //
+
-

+

HSKPass

{/* desktop layout */} diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 862fada..61f5e36 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -1,7 +1,7 @@ 'use client'; import { Button } from '@/components/ui/button'; -import { supabase } from '@/lib/supabase/client'; + import { toast } from 'sonner'; import { Dialog, @@ -10,7 +10,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { logout } from '@/lib/supabase/userApi'; +import { logout, loginWithGoogle } from '@/lib/supabase/userApi'; import { LogInIcon } from 'lucide-react'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useRouter } from 'next/navigation'; @@ -18,33 +18,7 @@ import { useUser } from '@/hooks/useUser'; import { useQueryClient } from '@tanstack/react-query'; import { useModal } from '@/hooks/useMoal'; -// Google Icon -function GoogleIcon() { - return ( - - - - - - - ); -} +import { GoogleIcon } from '@/components/icons/GoogleIcon'; export default function Login() { const { data: user } = useUser(); @@ -52,15 +26,10 @@ export default function Login() { const router = useRouter(); const { loginOpen, openLoginModal, closeLoginModal } = useModal(); - const handleLogin = () => { + const handleLogin = async () => { try { // 브라우저에서 팝업 또는 리다이렉트를 통해 동작하므로 client component - supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - redirectTo: process.env.NEXT_PUBLIC_BASE_URL, - }, - }); + await loginWithGoogle(); } catch (error) { console.error(`[ERROR] Failed login: ${error}`); toast.error('로그인 실패. 다시 시도해주세요.'); diff --git a/src/components/icons/GoogleIcon.tsx b/src/components/icons/GoogleIcon.tsx new file mode 100644 index 0000000..89469c2 --- /dev/null +++ b/src/components/icons/GoogleIcon.tsx @@ -0,0 +1,26 @@ +export function GoogleIcon() { + return ( + + + + + + + ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 0205413..fd3a406 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", { variants: { variant: { diff --git a/src/features/home/CTASection.tsx b/src/features/home/CTASection.tsx new file mode 100644 index 0000000..a0d11ca --- /dev/null +++ b/src/features/home/CTASection.tsx @@ -0,0 +1,3 @@ +export default function CTASection() { + return
; +} diff --git a/src/features/home/HeroSection.tsx b/src/features/home/HeroSection.tsx index a512c29..ac2237e 100644 --- a/src/features/home/HeroSection.tsx +++ b/src/features/home/HeroSection.tsx @@ -1,9 +1,23 @@ 'use client'; -import { ArrowRight, BookOpen, Sparkles, Trophy } from 'lucide-react'; import Link from 'next/link'; +import { GoogleIcon } from '@/components/icons/GoogleIcon'; +import { loginWithGoogle } from '@/lib/supabase/userApi'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { useModal } from '@/hooks/useMoal'; +import { toast } from 'sonner'; +import { ArrowRight, BookOpen, Sparkles, Trophy } from 'lucide-react'; export function HeroSection() { + const { loginOpen, openLoginModal, closeLoginModal } = useModal(); + const hskLevels = [ { level: 1, words: 150, color: 'from-green-400 to-emerald-500' }, { level: 2, words: 300, color: 'from-blue-400 to-cyan-500' }, @@ -28,8 +42,18 @@ export function HeroSection() { { char: '文', left: '65%', top: '80%' }, ]; + const handleLogin = async () => { + try { + // 브라우저에서 팝업 또는 리다이렉트를 통해 동작하므로 client component + await loginWithGoogle(); + } catch (error) { + console.error(`[ERROR] Failed login: ${error}`); + toast.error('로그인 실패. 다시 시도해주세요.'); + } + }; + return ( -
+
{/* 백그라운드에 한자 플로팅 */}
{floatingChars.map((item, i) => ( @@ -53,7 +77,7 @@ export function HeroSection() {
-
+
@@ -65,9 +89,9 @@ export function HeroSection() { 똑똑한 HSK 학습

- 10,000개 이상의 단어, AI 생성 예문, 실시간 퀴즈로 + 10,000개 이상의 HSK 단어를
- 획순 애니메이션과 함께 정확하게 배우세요 + AI가 생성한 예문과 퀴즈로 학습하세요

@@ -88,7 +112,10 @@ export function HeroSection() { {/* CTA Buttons */}
- @@ -144,7 +171,33 @@ export function HeroSection() {
+ + {/* 로그인 모달 */} + { + if (!open) closeLoginModal(); + }} + > + + + 로그인 + + + + + + +
- //
); } diff --git a/src/features/home/PrimaryContentSection.tsx b/src/features/home/PrimaryContentSection.tsx new file mode 100644 index 0000000..7d4cfcf --- /dev/null +++ b/src/features/home/PrimaryContentSection.tsx @@ -0,0 +1,63 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +import { Dot } from 'lucide-react'; + +export default function PrimaryContentSection() { + const contents = [ + { + title: 'AI가 만든 예문·동의어로 맥락 학습', + features: [ + '자연스러운 예문으로 단어 이해', + '유의어·반의어·동의어 한눈에 파악', + '맥락 속에서 실제 사용법 학습', + ], + }, + { + title: 'AI 퀴즈로 바로 복습', + features: [ + '다양한 유형의 퀴즈 자동 생성', + '실제 시험처럼 반복 학습', + '즉시 피드백으로 완벽 이해', + ], + }, + { + title: '학습 결과를 한눈에', + features: [ + '잘 외워지지 않는 단어 자동 체크', + '퀴즈 결과와 오답 내역 확인', + '마이페이지에서 한눈에 확인', + ], + }, + ]; + + return ( +
+ {contents.map((content, index) => ( + + + + + Step {index + 1} + +

{content.title}

+
+
+ +
    + {content.features.map((item, index) => ( +
  • + + {item} +
  • + ))} +
+
+
+ ))} +
+ ); +} diff --git a/src/lib/supabase/userApi.ts b/src/lib/supabase/userApi.ts index 7eb11bc..a3ddcea 100644 --- a/src/lib/supabase/userApi.ts +++ b/src/lib/supabase/userApi.ts @@ -24,3 +24,17 @@ export const logout = async () => { throw error; } }; + +export const loginWithGoogle = async () => { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: process.env.NEXT_PUBLIC_BASE_URL, + }, + }); + + if (error) { + console.error(`[ERROR] Failed login: ${error}`); + throw error; + } +};