자동로그인, 아이디 패스워드 찾기 적용1

This commit is contained in:
2025-11-26 20:38:03 +09:00
parent 6434c580fe
commit 47eedf6837
2 changed files with 272 additions and 25 deletions

View File

@@ -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);
// 토큰이 없어도 로그인은 성공했으므로 진행

View File

@@ -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>
{/* 새 비밀번호 */}