diff --git a/A06_Login/A06_Login_UI_Auth_Page.ts b/A06_Login/A06_Login_UI_Auth_Page.ts index 2b5f9e9a..45e92a9a 100644 --- a/A06_Login/A06_Login_UI_Auth_Page.ts +++ b/A06_Login/A06_Login_UI_Auth_Page.ts @@ -14,6 +14,56 @@ import "./A06_Login_UI_Style.css"; const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex]; +/* 인증 코드 입력 단계는 **새로고침을 견뎌야 한다**(2026-09-04 사용자 보고). + * 휴대폰에서 코드를 보러 메일 앱으로 갔다 오면 브라우저가 탭을 버려 페이지가 다시 뜨는데, + * 그때 단계가 메모리에만 있어 이메일/비밀번호 화면으로 되돌아갔다. 서버는 코드를 DB에 + * 5분간 들고 있고 검증에 이메일+코드만 쓰므로(`/api/auth/login/verify`), 그 사이 단계만 + * 세션에 남겨 두면 받은 코드를 그대로 넣을 수 있다. 비밀번호는 저장하지 않는다. */ +const OTP_PENDING_KEY = "a06:otp-pending"; +/** 서버 OTP 유효시간(분) — `config_system.OTP_VALID_MINUTES` 기본값과 맞춘다. */ +const OTP_VALID_MS = 5 * 60 * 1000; +/** 재발송 대기(초) — 아래 쿨다운과 같은 값. */ +const RESEND_COOLDOWN_S = 60; + +interface OtpPending { + email: string; + /** 코드를 보낸 시각(ms). 유효시간·재발송 대기 계산의 기준. */ + sentAt: number; +} + +function readOtpPending(): OtpPending | null { + try { + const raw = sessionStorage.getItem(OTP_PENDING_KEY); + if (!raw) return null; + const value = JSON.parse(raw) as OtpPending; + if (!value?.email || typeof value.sentAt !== "number") return null; + // 유효시간이 지났으면 되살리지 않는다 — 어차피 서버가 거절한다. + if (Date.now() - value.sentAt > OTP_VALID_MS) { + sessionStorage.removeItem(OTP_PENDING_KEY); + return null; + } + return value; + } catch { + return null; + } +} + +function writeOtpPending(email: string): void { + try { + sessionStorage.setItem(OTP_PENDING_KEY, JSON.stringify({ email, sentAt: Date.now() })); + } catch { + // 세션 저장이 막힌 브라우저(시크릿 등)에서는 예전처럼 메모리로만 동작한다. + } +} + +function clearOtpPending(): void { + try { + sessionStorage.removeItem(OTP_PENDING_KEY); + } catch { + // 지우기 실패는 무시 — 유효시간이 지나면 어차피 되살리지 않는다. + } +} + export function renderA06Login(root: HTMLElement): void { const page = document.createElement("div"); page.className = "a06-login"; @@ -59,9 +109,10 @@ export function renderA06Login(root: HTMLElement): void { setButtonLabel(resend, L("A06_Login_OtpResend")); } - function startResendCooldown(): void { + function startResendCooldown(remainSeconds = RESEND_COOLDOWN_S): void { stopResendCooldown(); - resendSeconds = 60; + if (remainSeconds <= 0) return; + resendSeconds = remainSeconds; resend.disabled = true; const updateLabel = (): void => { setButtonLabel( @@ -80,17 +131,18 @@ export function renderA06Login(root: HTMLElement): void { }, 1000); } - function showOtpStep(): void { + function showOtpStep(remainSeconds = RESEND_COOLDOWN_S): void { otpRequired = true; email.input.disabled = true; otp.root.hidden = false; otpActions.hidden = false; password.root.hidden = true; setButtonLabel(submit, L("A06_Login_Verify")); - startResendCooldown(); + startResendCooldown(remainSeconds); } function onA06_Login_Back_Click(): void { + clearOtpPending(); otpRequired = false; email.input.disabled = false; password.root.hidden = false; @@ -107,6 +159,7 @@ export function renderA06Login(root: HTMLElement): void { try { const result = await requestLogin(email.input.value.trim(), password.input.value); if (result.status === "otp_required") { + writeOtpPending(email.input.value.trim()); showToast(L("A06_Login_OtpSent"), "info"); startResendCooldown(); } else { @@ -140,9 +193,11 @@ export function renderA06Login(root: HTMLElement): void { ? await verifyLogin(emailValue, otp.input.value) : await requestLogin(emailValue, password.input.value); if (result.status === "otp_required") { + writeOtpPending(emailValue); showOtpStep(); showToast(L("A06_Login_OtpSent"), "info"); } else { + clearOtpPending(); showToast(L("A06_Login_Success"), "success"); navigateTo(ROUTES.B01_ACCOUNT); } @@ -163,4 +218,14 @@ export function renderA06Login(root: HTMLElement): void { card.append(title, form, register); page.append(card); root.append(page); + + // 코드를 기다리던 중 화면이 다시 뜬 경우 — 그 단계로 되돌려 준다(이메일은 채워 두고, + // 남은 재발송 대기도 보낸 시각 기준으로 이어 센다). + const pending = readOtpPending(); + if (pending) { + email.input.value = pending.email; + const elapsed = Math.floor((Date.now() - pending.sentAt) / 1000); + showOtpStep(Math.max(RESEND_COOLDOWN_S - elapsed, 0)); + otp.input.focus(); + } }