From 3f2002206218d0cdbb774c0b2a72ee5b0082cb5f Mon Sep 17 00:00:00 2001 From: hyeonggil <> Date: Sun, 8 Mar 2026 21:25:34 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC/=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 매직 링크 방식 → 이메일/비밀번호 직접 인증으로 전환 - 로그인/회원가입 탭 UI 추가 - 비밀번호 재설정(forgot-password) 뷰 추가 - Supabase 에러 메시지 한국어 변환 처리 --- app/pages/login.vue | 442 ++++++++++++++++++++++++++++++++------------ 1 file changed, 322 insertions(+), 120 deletions(-) diff --git a/app/pages/login.vue b/app/pages/login.vue index cb9d3f8..2ca4a93 100644 --- a/app/pages/login.vue +++ b/app/pages/login.vue @@ -2,40 +2,110 @@ definePageMeta({ layout: false }); const supabase = useSupabaseClient(); -const email = ref(""); + +type ViewMode = 'auth' | 'forgot-password'; + +const email = ref(''); +const password = ref(''); +const passwordConfirm = ref(''); const loading = ref(false); -const message = ref(""); -const error = ref(""); +const message = ref(''); +const error = ref(''); +const activeTab = ref<'login' | 'signup'>('login'); +const viewMode = ref('auth'); -async function sendMagicLink() { - if (!email.value) return; +function resetForm() { + email.value = ''; + password.value = ''; + passwordConfirm.value = ''; + error.value = ''; + message.value = ''; +} + +function translateAuthError(msg: string): string { + if (msg.includes('Invalid login credentials')) return '이메일 또는 비밀번호가 올바르지 않습니다.'; + if (msg.includes('Email not confirmed')) return '이메일 확인이 완료되지 않았습니다. 받은 편지함을 확인해주세요.'; + if (msg.includes('User already registered')) return '이미 가입된 이메일입니다. 로그인을 시도해주세요.'; + if (msg.includes('Password should be at least 6 characters')) return '비밀번호는 최소 6자 이상이어야 합니다.'; + return msg; +} + +async function signIn() { + if (!email.value || !password.value) return; loading.value = true; - error.value = ""; - message.value = ""; + error.value = ''; + message.value = ''; - const { error: err } = await supabase.auth.signInWithOtp({ + const { error: err } = await supabase.auth.signInWithPassword({ email: email.value, - options: { - emailRedirectTo: `${window.location.origin}/confirm`, - }, + password: password.value, }); loading.value = false; if (err) { - error.value = err.message; + error.value = translateAuthError(err.message); } else { - message.value = `${email.value}로 로그인 링크를 전송했습니다. 이메일을 확인해주세요.`; + await navigateTo('/'); + } +} + +async function signUp() { + if (!email.value || !password.value) return; + if (password.value !== passwordConfirm.value) { + error.value = '비밀번호가 일치하지 않습니다.'; + return; + } + + loading.value = true; + error.value = ''; + message.value = ''; + + const { data, error: err } = await supabase.auth.signUp({ + email: email.value, + password: password.value, + }); + + loading.value = false; + if (err) { + error.value = translateAuthError(err.message); + } else if (data.session) { + await navigateTo('/'); + } else { + message.value = `${email.value}로 확인 이메일을 전송했습니다. 이메일을 확인한 후 로그인해주세요.`; + } +} + +async function sendResetEmail() { + if (!email.value) return; + loading.value = true; + error.value = ''; + message.value = ''; + + const { error: err } = await supabase.auth.resetPasswordForEmail(email.value, { + redirectTo: `${window.location.origin}/confirm`, + }); + + loading.value = false; + if (err) { + error.value = translateAuthError(err.message); + } else { + message.value = `${email.value}로 비밀번호 재설정 링크를 전송했습니다.`; } } async function signInWithGoogle() { await supabase.auth.signInWithOAuth({ - provider: "google", + provider: 'google', options: { redirectTo: `${window.location.origin}/confirm`, }, }); } + +const tabs = [ + { label: '로그인', value: 'login' }, + { label: '회원가입', value: 'signup' }, +];