자동로그인, 아이디 패스워드 찾기 적용1
This commit is contained in:
@@ -22,14 +22,49 @@ export default function LoginPage() {
|
|||||||
const [idError, setIdError] = useState("");
|
const [idError, setIdError] = useState("");
|
||||||
const [passwordError, setPasswordError] = useState("");
|
const [passwordError, setPasswordError] = useState("");
|
||||||
|
|
||||||
// 컴포넌트 마운트 시 저장된 아이디 불러오기
|
// 컴포넌트 마운트 시 저장된 아이디 불러오기 및 자동 로그인 확인
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedId = localStorage.getItem('savedUserId');
|
const savedId = localStorage.getItem('savedUserId');
|
||||||
if (savedId) {
|
if (savedId) {
|
||||||
setUserId(savedId);
|
setUserId(savedId);
|
||||||
setRememberId(true);
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -99,10 +134,17 @@ export default function LoginPage() {
|
|||||||
// 로그인 성공 시 토큰 저장 (다양한 필드명 지원)
|
// 로그인 성공 시 토큰 저장 (다양한 필드명 지원)
|
||||||
const token = data.token || data.accessToken || data.access_token;
|
const token = data.token || data.accessToken || data.access_token;
|
||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem('token', token);
|
if (autoLogin) {
|
||||||
// 쿠키에도 토큰 저장 (middleware에서 사용)
|
// 자동 로그인이 체크되어 있으면 localStorage와 쿠키에 장기 저장 (30일)
|
||||||
document.cookie = `token=${token}; path=/; max-age=${30 * 24 * 60 * 60}; SameSite=Lax`;
|
localStorage.setItem('token', token);
|
||||||
console.log("토큰 저장 완료");
|
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 {
|
} else {
|
||||||
console.warn("토큰이 응답에 없습니다. 응답 데이터:", data);
|
console.warn("토큰이 응답에 없습니다. 응답 데이터:", data);
|
||||||
// 토큰이 없어도 로그인은 성공했으므로 진행
|
// 토큰이 없어도 로그인은 성공했으므로 진행
|
||||||
|
|||||||
@@ -13,14 +13,19 @@ export default function ResetPasswordPage() {
|
|||||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||||
const [focused, setFocused] = useState<Record<string, boolean>>({});
|
const [focused, setFocused] = useState<Record<string, boolean>>({});
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
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 isEmailValid = useMemo(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), [email]);
|
||||||
const isPasswordValid = useMemo(() => password.length >= 8 && password.length <= 16, [password]);
|
const isPasswordValid = useMemo(() => password.length >= 8 && password.length <= 16, [password]);
|
||||||
const isPasswordConfirmValid = useMemo(() => passwordConfirm === password && passwordConfirm.length > 0, [password, passwordConfirm]);
|
const isPasswordConfirmValid = useMemo(() => passwordConfirm === password && passwordConfirm.length > 0, [password, passwordConfirm]);
|
||||||
|
|
||||||
const canSubmit = useMemo(() => {
|
const canSubmit = useMemo(() => {
|
||||||
return isEmailValid && isPasswordValid && isPasswordConfirmValid;
|
return isEmailValid && isPasswordValid && isPasswordConfirmValid && emailCodeVerified;
|
||||||
}, [isEmailValid, isPasswordValid, isPasswordConfirmValid]);
|
}, [isEmailValid, isPasswordValid, isPasswordConfirmValid, emailCodeVerified]);
|
||||||
|
|
||||||
function validateAll() {
|
function validateAll() {
|
||||||
const nextErrors: Record<string, string> = {};
|
const nextErrors: Record<string, string> = {};
|
||||||
@@ -31,11 +36,157 @@ export default function ResetPasswordPage() {
|
|||||||
return Object.keys(nextErrors).length === 0;
|
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();
|
e.preventDefault();
|
||||||
if (!validateAll()) return;
|
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 (
|
return (
|
||||||
@@ -68,14 +219,15 @@ export default function ResetPasswordPage() {
|
|||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
onFocus={() => setFocused((p) => ({ ...p, email: true }))}
|
onFocus={() => setFocused((p) => ({ ...p, email: true }))}
|
||||||
onBlur={() => setFocused((p) => ({ ...p, email: false }))}
|
onBlur={() => setFocused((p) => ({ ...p, email: false }))}
|
||||||
placeholder="이메일을 입력해 주세요."
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onMouseDown={(e) => { e.preventDefault(); setEmail(""); }}
|
onMouseDown={(e) => { e.preventDefault(); setEmail(""); }}
|
||||||
@@ -88,17 +240,70 @@ export default function ResetPasswordPage() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!isEmailValid}
|
disabled={!isEmailValid || emailCodeVerified}
|
||||||
className={`h-[40px] px-[12px] rounded-[8px] text-[16px] font-semibold ${isEmailValid ? "bg-inactive-button text-white" : "bg-gray-50 text-input-placeholder-text"}`}
|
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={() => {
|
onClick={handleSendVerificationCode}
|
||||||
if (!isEmailValid) return;
|
|
||||||
alert("인증번호 전송 (가상 동작)");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
인증번호 전송
|
{emailCodeSent && !emailCodeVerified ? "인증번호 재전송" : "인증번호 전송"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{errors.email && <p className="text-error text-[13px] leading-tight">{errors.email}</p>}
|
{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>
|
</div>
|
||||||
|
|
||||||
{/* 새 비밀번호 */}
|
{/* 새 비밀번호 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user