From a9e26edbbdaa1a77fab6c4c66d083b91ec40cc81 Mon Sep 17 00:00:00 2001 From: mingo Date: Fri, 22 May 2026 04:12:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Feat:=20=EA=B8=B0=EB=B3=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20api=20=EC=97=B0=EA=B2=B0=20[JDDEV-26]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/login/EmailLoginScreen.tsx | 209 +++++++++++++++--- jobdri/src/lib/auth.ts | 174 +++++++++++++++ 2 files changed, 350 insertions(+), 33 deletions(-) create mode 100644 jobdri/src/lib/auth.ts diff --git a/jobdri/src/components/login/EmailLoginScreen.tsx b/jobdri/src/components/login/EmailLoginScreen.tsx index aa22af3..a4acde8 100644 --- a/jobdri/src/components/login/EmailLoginScreen.tsx +++ b/jobdri/src/components/login/EmailLoginScreen.tsx @@ -9,6 +9,7 @@ import type { SetStateAction, } from "react"; import { useEffect, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; import clsx from "clsx"; import { Button, @@ -17,6 +18,16 @@ import { } from "@/components/common/buttons"; import { InputMain, InputSingleLine } from "@/components/common/input"; import { Tooltip } from "@/components/common/tooltip"; +import { ROUTES } from "@/constants/routes"; +import { + AuthApiError, + confirmEmailVerification, + getGoogleAuthorizationUrl, + loginWithEmail, + saveAuthTokens, + sendEmailVerification, + signupWithEmail, +} from "@/lib/auth"; const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const passwordPattern = /^(?=.*[A-Za-z])(?=.*\d).{8,20}$/; @@ -26,14 +37,16 @@ const passwordMaxLengthMessage = "비밀번호는 최대 20자까지만 가능 const passwordMismatchMessage = "비밀번호가 일치하지 않습니다"; const verificationCodeLength = 6; const initialVerificationCode = Array(verificationCodeLength).fill(""); -const verificationErrorMessage = "인증번호를 다시 확인해주세요."; -const mockVerificationSuccessCode = "123456"; +const defaultVerificationErrorMessage = "인증번호를 다시 확인해주세요."; +const loginValidationErrorMessage = "이메일과 비밀번호를 확인해주세요"; export default function EmailLoginScreen() { + const router = useRouter(); const [authMode, setAuthMode] = useState< "login" | "signup" | "verify" | "success" >("login"); const [showCreditTooltip, setShowCreditTooltip] = useState(true); + const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); @@ -41,7 +54,20 @@ export default function EmailLoginScreen() { initialVerificationCode, ); const [hasVerificationError, setHasVerificationError] = useState(false); + const [verificationErrorMessage, setVerificationErrorMessage] = useState( + defaultVerificationErrorMessage, + ); const [loginError, setLoginError] = useState(false); + const [loginErrorMessage, setLoginErrorMessage] = useState( + loginValidationErrorMessage, + ); + const [isLoginSubmitting, setIsLoginSubmitting] = useState(false); + const [signupErrorMessage, setSignupErrorMessage] = useState(""); + const [isSignupSubmitting, setIsSignupSubmitting] = useState(false); + const [isVerificationSubmitting, setIsVerificationSubmitting] = + useState(false); + const [isResendingVerificationCode, setIsResendingVerificationCode] = + useState(false); const verificationInputRefs = useRef>([]); const isLoginReady = email.length > 0 && password.length > 0; @@ -60,6 +86,7 @@ export default function EmailLoginScreen() { const hasPasswordMismatchError = passwordConfirm.length > 0 && passwordConfirm !== password; const isSignupReady = + name.trim().length > 0 && emailPattern.test(email) && passwordPattern.test(password) && passwordConfirm === password; @@ -91,6 +118,9 @@ export default function EmailLoginScreen() { ) => { setter(value); setLoginError(false); + setLoginErrorMessage(loginValidationErrorMessage); + setSignupErrorMessage(""); + setVerificationErrorMessage(defaultVerificationErrorMessage); hideCreditTooltip(); }; @@ -200,57 +230,117 @@ export default function EmailLoginScreen() { fillVerificationCode(index, event.clipboardData.getData("text")); }; - const handleLoginSubmit = (event: FormEvent) => { + const handleLoginSubmit = async (event: FormEvent) => { event.preventDefault(); if ( + isLoginSubmitting || !isLoginReady || !emailPattern.test(email) || !passwordPattern.test(password) ) { setLoginError(true); + setLoginErrorMessage(loginValidationErrorMessage); return; } - setLoginError(true); + setLoginError(false); + setIsLoginSubmitting(true); + hideCreditTooltip(); + + try { + const tokens = await loginWithEmail({ email, password }); + saveAuthTokens(tokens); + router.push(ROUTES.APPLY); + } catch (error) { + setLoginError(true); + setLoginErrorMessage( + error instanceof AuthApiError + ? error.errorDetail || error.message + : "로그인 중 문제가 발생했습니다.", + ); + } finally { + setIsLoginSubmitting(false); + } + }; + + const handleGoogleLogin = () => { + window.location.assign(getGoogleAuthorizationUrl()); }; - const handleSignupSubmit = (event: FormEvent) => { + const handleSignupSubmit = async (event: FormEvent) => { event.preventDefault(); - if (!isSignupReady || !emailPattern.test(email)) { + if (isSignupSubmitting || !isSignupReady || !emailPattern.test(email)) { return; } - setVerificationCode([...initialVerificationCode]); - setHasVerificationError(false); - setAuthMode("verify"); + setSignupErrorMessage(""); + setIsSignupSubmitting(true); + + try { + await sendEmailVerification({ email }); + setVerificationCode([...initialVerificationCode]); + setHasVerificationError(false); + setVerificationErrorMessage(defaultVerificationErrorMessage); + setAuthMode("verify"); + } catch (error) { + setSignupErrorMessage( + error instanceof AuthApiError + ? error.errorDetail || error.message + : "인증번호 발송 중 문제가 발생했습니다.", + ); + } finally { + setIsSignupSubmitting(false); + } }; - const handleVerificationSubmit = (event: FormEvent) => { + const handleVerificationSubmit = async (event: FormEvent) => { event.preventDefault(); - if (!isVerificationReady) { + if (isVerificationSubmitting || !isVerificationReady) { return; } - if (verificationCode.join("") === mockVerificationSuccessCode) { + setIsVerificationSubmitting(true); + + try { + await confirmEmailVerification({ + email, + code: verificationCode.join(""), + }); + await signupWithEmail({ + name: name.trim(), + email, + password, + }); setAuthMode("success"); setVerificationCode([...initialVerificationCode]); setHasVerificationError(false); - return; - } + setVerificationErrorMessage(defaultVerificationErrorMessage); + } catch (error) { + setVerificationCode([...initialVerificationCode]); + setHasVerificationError(true); + setVerificationErrorMessage( + error instanceof AuthApiError + ? error.errorDetail || error.message + : defaultVerificationErrorMessage, + ); - setVerificationCode([...initialVerificationCode]); - setHasVerificationError(true); - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + } finally { + setIsVerificationSubmitting(false); } }; const handleModeChange = (mode: "login" | "signup") => { setAuthMode(mode); setLoginError(false); + setSignupErrorMessage(""); + setVerificationErrorMessage(defaultVerificationErrorMessage); + setName(""); setEmail(""); setPassword(""); setPasswordConfirm(""); @@ -267,11 +357,31 @@ export default function EmailLoginScreen() { setAuthMode("signup"); setVerificationCode([...initialVerificationCode]); setHasVerificationError(false); + setVerificationErrorMessage(defaultVerificationErrorMessage); hideCreditTooltip(); }; - const handleResendVerificationCode = () => { + const handleResendVerificationCode = async () => { + if (isResendingVerificationCode) { + return; + } + resetVerificationToInitial(); + setVerificationErrorMessage(defaultVerificationErrorMessage); + setIsResendingVerificationCode(true); + + try { + await sendEmailVerification({ email }); + } catch (error) { + setHasVerificationError(true); + setVerificationErrorMessage( + error instanceof AuthApiError + ? error.errorDetail || error.message + : "인증번호 재발송 중 문제가 발생했습니다.", + ); + } finally { + setIsResendingVerificationCode(false); + } }; return ( @@ -304,7 +414,10 @@ export default function EmailLoginScreen() { verificationCode={verificationCode} verificationInputRefs={verificationInputRefs} hasVerificationError={hasVerificationError} + verificationErrorMessage={verificationErrorMessage} isVerificationReady={isVerificationReady} + isSubmitting={isVerificationSubmitting} + isResending={isResendingVerificationCode} onBack={handleBackToSignup} onCodeChange={handleVerificationCodeChange} onCodeFocus={handleVerificationCodeFocus} @@ -345,6 +458,7 @@ export default function EmailLoginScreen() { autoComplete="email" placeholder="내용을 입력해주세요." value={email} + disabled={isLoginSubmitting} hasError={loginError} className="self-stretch" onChange={(value) => @@ -358,23 +472,20 @@ export default function EmailLoginScreen() { autoComplete="current-password" placeholder="내용을 입력해주세요." value={password} - error={ - loginError - ? "이메일과 비밀번호를 확인해주세요" - : undefined - } + disabled={isLoginSubmitting} + error={loginError ? loginErrorMessage : undefined} className="self-stretch" onChange={handlePasswordChange} />