자동로그인, 아이디 패스워드 찾기 적용1
This commit is contained in:
@@ -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) {
|
||||
if (autoLogin) {
|
||||
// 자동 로그인이 체크되어 있으면 localStorage와 쿠키에 장기 저장 (30일)
|
||||
localStorage.setItem('token', token);
|
||||
// 쿠키에도 토큰 저장 (middleware에서 사용)
|
||||
document.cookie = `token=${token}; path=/; max-age=${30 * 24 * 60 * 60}; SameSite=Lax`;
|
||||
console.log("토큰 저장 완료");
|
||||
console.log("자동 로그인 토큰 저장 완료 (30일 유지)");
|
||||
} else {
|
||||
// 자동 로그인이 체크되어 있지 않으면 쿠키에만 세션 쿠키로 저장 (브라우저 종료 시 삭제)
|
||||
// localStorage에는 저장하지 않음
|
||||
document.cookie = `token=${token}; path=/; SameSite=Lax`;
|
||||
console.log("세션 토큰 저장 완료 (브라우저 종료 시 삭제)");
|
||||
}
|
||||
} else {
|
||||
console.warn("토큰이 응답에 없습니다. 응답 데이터:", data);
|
||||
// 토큰이 없어도 로그인은 성공했으므로 진행
|
||||
|
||||
@@ -14,13 +14,18 @@ export default function ResetPasswordPage() {
|
||||
const [focused, setFocused] = useState<Record<string, boolean>>({});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// 인증번호 관련 상태
|
||||
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<string, string> = {};
|
||||
@@ -31,11 +36,157 @@ export default function ResetPasswordPage() {
|
||||
return Object.keys(nextErrors).length === 0;
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
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<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
if (!validateAll()) return;
|
||||
|
||||
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 (
|
||||
@@ -73,9 +224,10 @@ export default function ResetPasswordPage() {
|
||||
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'}`}
|
||||
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 && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => { e.preventDefault(); setEmail(""); }}
|
||||
@@ -88,17 +240,70 @@ export default function ResetPasswordPage() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isEmailValid}
|
||||
className={`h-[40px] px-[12px] rounded-[8px] text-[16px] font-semibold ${isEmailValid ? "bg-inactive-button text-white" : "bg-gray-50 text-input-placeholder-text"}`}
|
||||
onClick={() => {
|
||||
if (!isEmailValid) return;
|
||||
alert("인증번호 전송 (가상 동작)");
|
||||
}}
|
||||
disabled={!isEmailValid || emailCodeVerified}
|
||||
className={`h-[40px] w-[130px] px-[12px] rounded-[8px] text-[16px] font-semibold ${isEmailValid && !emailCodeVerified ? "bg-[#F1F3F5] text-neutral-700" : "bg-gray-50 text-input-placeholder-text"}`}
|
||||
onClick={handleSendVerificationCode}
|
||||
>
|
||||
인증번호 전송
|
||||
{emailCodeSent && !emailCodeVerified ? "인증번호 재전송" : "인증번호 전송"}
|
||||
</button>
|
||||
</div>
|
||||
{errors.email && <p className="text-error text-[13px] leading-tight">{errors.email}</p>}
|
||||
{emailCodeSent && (
|
||||
<div className="space-y-2" aria-expanded={emailCodeSent}>
|
||||
<label htmlFor="emailCode" className="sr-only">인증번호</label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
id="emailCode"
|
||||
name="emailCode"
|
||||
inputMode="numeric"
|
||||
value={emailCode}
|
||||
onChange={(e) => {
|
||||
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 && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => { e.preventDefault(); setEmailCode(""); }}
|
||||
aria-label="입력 지우기"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer"
|
||||
>
|
||||
<LoginInputSvg />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={emailCodeVerified || emailCode.trim().length === 0}
|
||||
className={`h-[40px] w-[130px] px-[12px] rounded-[8px] text-[16px] font-semibold ${!emailCodeVerified && emailCode.trim().length > 0 ? "bg-[#F1F3F5] text-neutral-700" : "bg-gray-50 text-input-placeholder-text"}`}
|
||||
onClick={verifyEmailCode}
|
||||
>
|
||||
{emailCodeVerified ? "인증완료" : "인증하기"}
|
||||
</button>
|
||||
</div>
|
||||
{!errors.emailCode && (
|
||||
<p className="text-[13px] leading-[normal] text-[#384fbf]">
|
||||
{emailCodeVerified ? (
|
||||
"인증이 완료됐습니다"
|
||||
) : (
|
||||
<>
|
||||
인증 확인을 위해 작성한 이메일로 인증번호를 발송했습니다.
|
||||
<br />
|
||||
이메일을 확인해 주세요.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{errors.emailCode && <p className="text-error text-[13px] leading-tight">{errors.emailCode}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 새 비밀번호 */}
|
||||
|
||||
Reference in New Issue
Block a user