From 47eedf68379be43c14d45bc9f13ff3563728ea2b Mon Sep 17 00:00:00 2001 From: wallace Date: Wed, 26 Nov 2025 20:38:03 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=90=EB=8F=99=EB=A1=9C=EA=B7=B8=EC=9D=B8,?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EB=94=94=20=ED=8C=A8=EC=8A=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B0=BE=EA=B8=B0=20=EC=A0=81=EC=9A=A91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/page.tsx | 54 ++++++- src/app/reset-password/page.tsx | 243 +++++++++++++++++++++++++++++--- 2 files changed, 272 insertions(+), 25 deletions(-) diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 5101ccc..ea9036b 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -22,14 +22,49 @@ export default function LoginPage() { const [idError, setIdError] = useState(""); const [passwordError, setPasswordError] = useState(""); - // 컴포넌트 마운트 시 저장된 아이디 불러오기 + // 컴포넌트 마운트 시 저장된 아이디 불러오기 및 자동 로그인 확인 useEffect(() => { const savedId = localStorage.getItem('savedUserId'); if (savedId) { setUserId(savedId); setRememberId(true); } - }, []); + + // 자동 로그인 확인: localStorage에 토큰이 있고 쿠키에도 토큰이 있으면 자동 로그인 + const savedToken = localStorage.getItem('token'); + const cookieToken = document.cookie + .split('; ') + .find(row => row.startsWith('token=')) + ?.split('=')[1]; + + if (savedToken && cookieToken && savedToken === cookieToken) { + // 토큰이 유효한지 확인 + fetch('https://hrdi.coconutmeet.net/auth/me', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${savedToken}`, + }, + }) + .then(response => { + if (response.ok) { + // 토큰이 유효하면 메인 페이지로 리다이렉트 + const searchParams = new URLSearchParams(window.location.search); + const redirectPath = searchParams.get('redirect') || '/'; + router.push(redirectPath); + } else { + // 토큰이 유효하지 않으면 삭제 + localStorage.removeItem('token'); + document.cookie = 'token=; path=/; max-age=0'; + } + }) + .catch(() => { + // 에러 발생 시 토큰 삭제 + localStorage.removeItem('token'); + document.cookie = 'token=; path=/; max-age=0'; + }); + } + }, [router]); // 아이디 기억하기 상태나 아이디가 변경될 때마다 저장 처리 useEffect(() => { @@ -99,10 +134,17 @@ export default function LoginPage() { // 로그인 성공 시 토큰 저장 (다양한 필드명 지원) const token = data.token || data.accessToken || data.access_token; if (token) { - localStorage.setItem('token', token); - // 쿠키에도 토큰 저장 (middleware에서 사용) - document.cookie = `token=${token}; path=/; max-age=${30 * 24 * 60 * 60}; SameSite=Lax`; - console.log("토큰 저장 완료"); + if (autoLogin) { + // 자동 로그인이 체크되어 있으면 localStorage와 쿠키에 장기 저장 (30일) + localStorage.setItem('token', token); + document.cookie = `token=${token}; path=/; max-age=${30 * 24 * 60 * 60}; SameSite=Lax`; + console.log("자동 로그인 토큰 저장 완료 (30일 유지)"); + } else { + // 자동 로그인이 체크되어 있지 않으면 쿠키에만 세션 쿠키로 저장 (브라우저 종료 시 삭제) + // localStorage에는 저장하지 않음 + document.cookie = `token=${token}; path=/; SameSite=Lax`; + console.log("세션 토큰 저장 완료 (브라우저 종료 시 삭제)"); + } } else { console.warn("토큰이 응답에 없습니다. 응답 데이터:", data); // 토큰이 없어도 로그인은 성공했으므로 진행 diff --git a/src/app/reset-password/page.tsx b/src/app/reset-password/page.tsx index a666fcf..e16783b 100644 --- a/src/app/reset-password/page.tsx +++ b/src/app/reset-password/page.tsx @@ -13,14 +13,19 @@ export default function ResetPasswordPage() { const [passwordConfirm, setPasswordConfirm] = useState(""); const [focused, setFocused] = useState>({}); const [errors, setErrors] = useState>({}); + + // 인증번호 관련 상태 + const [emailCodeSent, setEmailCodeSent] = useState(false); + const [emailCode, setEmailCode] = useState(""); + const [emailCodeVerified, setEmailCodeVerified] = useState(false); const isEmailValid = useMemo(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), [email]); const isPasswordValid = useMemo(() => password.length >= 8 && password.length <= 16, [password]); const isPasswordConfirmValid = useMemo(() => passwordConfirm === password && passwordConfirm.length > 0, [password, passwordConfirm]); const canSubmit = useMemo(() => { - return isEmailValid && isPasswordValid && isPasswordConfirmValid; - }, [isEmailValid, isPasswordValid, isPasswordConfirmValid]); + return isEmailValid && isPasswordValid && isPasswordConfirmValid && emailCodeVerified; + }, [isEmailValid, isPasswordValid, isPasswordConfirmValid, emailCodeVerified]); function validateAll() { const nextErrors: Record = {}; @@ -31,11 +36,157 @@ export default function ResetPasswordPage() { return Object.keys(nextErrors).length === 0; } - function handleSubmit(e: React.FormEvent) { + async function handleSendVerificationCode() { + if (!isEmailValid) return; + + try { + const response = await fetch("https://hrdi.coconutmeet.net/auth/password/forgot", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: email + }) + }); + + if (!response.ok) { + let errorMessage = `인증번호 전송 실패 (${response.status})`; + try { + const errorData = await response.json(); + if (errorData.error) { + errorMessage = errorData.error; + } else if (errorData.message) { + errorMessage = errorData.message; + } else if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } catch (parseError) { + if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } + console.error("인증번호 전송 실패:", errorMessage); + setErrors((prev) => ({ ...prev, email: errorMessage })); + return; + } + + const data = await response.json(); + console.log("인증번호 전송 성공:", data); + // 성공 시 상태 업데이트 + setEmailCodeSent(true); + setEmailCode(""); + setEmailCodeVerified(false); + // 성공 시 에러 메시지 제거 + setErrors((prev) => { + const next = { ...prev }; + delete next.email; + return next; + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "네트워크 오류가 발생했습니다."; + console.error("인증번호 전송 오류:", errorMessage); + setErrors((prev) => ({ ...prev, email: errorMessage })); + } + } + + async function verifyEmailCode() { + try { + const response = await fetch("https://hrdi.coconutmeet.net/auth/password/confirm", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: email, + emailCode: emailCode + }) + }); + + if (!response.ok) { + let errorMessage = `인증번호 확인 실패 (${response.status})`; + try { + const errorData = await response.json(); + if (errorData.error) { + errorMessage = errorData.error; + } else if (errorData.message) { + errorMessage = errorData.message; + } else if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } catch (parseError) { + if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } + // 특정 에러 메시지를 사용자 친화적인 메시지로 변경 + if (errorMessage.includes("잘못되었거나 만료된 코드") || errorMessage.includes("잘못되었거나") || errorMessage.includes("만료된")) { + errorMessage = "올바르지 않은 인증번호입니다. 인증번호를 확인해주세요."; + } + console.error("인증번호 확인 실패:", errorMessage); + setErrors((prev) => ({ ...prev, emailCode: errorMessage })); + return; + } + + const data = await response.json(); + console.log("인증번호 확인 성공:", data); + // 인증 성공 시 상태 업데이트 + setEmailCodeVerified(true); + // 성공 시 에러 메시지 제거 + setErrors((prev) => { + const next = { ...prev }; + delete next.emailCode; + return next; + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "네트워크 오류가 발생했습니다."; + console.error("인증번호 확인 오류:", errorMessage); + setErrors((prev) => ({ ...prev, emailCode: errorMessage })); + } + } + + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!validateAll()) return; - // 성공 시 완료 오버레이 표시 - setIsDoneOpen(true); + + try { + const response = await fetch("https://hrdi.coconutmeet.net/auth/password/reset", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: email, + emailCode: emailCode, + newPassword: password, + newPasswordConfirm: passwordConfirm + }) + }); + + if (!response.ok) { + let errorMessage = `비밀번호 재설정 실패 (${response.status})`; + try { + const errorData = await response.json(); + if (errorData.error) { + errorMessage = errorData.error; + } else if (errorData.message) { + errorMessage = errorData.message; + } else if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } catch (parseError) { + if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } + console.error("비밀번호 재설정 실패:", errorMessage); + setErrors((prev) => ({ ...prev, submit: errorMessage })); + return; + } + + const data = await response.json(); + console.log("비밀번호 재설정 성공:", data); + // 성공 시 완료 오버레이 표시 + setIsDoneOpen(true); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "네트워크 오류가 발생했습니다."; + console.error("비밀번호 재설정 오류:", errorMessage); + setErrors((prev) => ({ ...prev, submit: errorMessage })); + } } return ( @@ -68,14 +219,15 @@ export default function ResetPasswordPage() { id="email" name="email" type="email" - value={email} - onChange={(e) => setEmail(e.target.value)} - onFocus={() => setFocused((p) => ({ ...p, email: true }))} - onBlur={() => setFocused((p) => ({ ...p, email: false }))} - placeholder="이메일을 입력해 주세요." - className={`h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border focus:outline-none text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px] ${errors.email ? 'border-error' : 'border-neutral-40 focus:border-neutral-700'}`} + value={email} + onChange={(e) => setEmail(e.target.value)} + onFocus={() => setFocused((p) => ({ ...p, email: true }))} + onBlur={() => setFocused((p) => ({ ...p, email: false }))} + placeholder="이메일을 입력해 주세요." + disabled={emailCodeVerified} + className={`h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border focus:outline-none text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px] disabled:bg-gray-100 disabled:cursor-not-allowed ${emailCodeVerified ? '' : (errors.email ? 'border-error' : 'border-neutral-40 focus:border-neutral-700')}`} /> - {email.trim().length > 0 && focused.email && ( + {email.trim().length > 0 && focused.email && !emailCodeVerified && ( {errors.email &&

{errors.email}

} + {emailCodeSent && ( +
+ +
+
+ { + const onlyDigits = e.target.value.replace(/[^0-9]/g, ""); + setEmailCode(onlyDigits.slice(0, 6)); + }} + onFocus={() => setFocused((p) => ({ ...p, emailCode: true }))} + onBlur={() => setFocused((p) => ({ ...p, emailCode: false }))} + placeholder="인증번호 6자리" + disabled={emailCodeVerified} + className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px] disabled:bg-gray-100 disabled:cursor-not-allowed" + /> + {emailCode.trim().length > 0 && focused.emailCode && !emailCodeVerified && ( + + )} +
+ +
+ {!errors.emailCode && ( +

+ {emailCodeVerified ? ( + "인증이 완료됐습니다" + ) : ( + <> + 인증 확인을 위해 작성한 이메일로 인증번호를 발송했습니다. +
+ 이메일을 확인해 주세요. + + )} +

+ )} + {errors.emailCode &&

{errors.emailCode}

} +
+ )} {/* 새 비밀번호 */}