fix(auth): resume Kakao OTP during auto login

This commit is contained in:
2026-08-08 04:41:16 +00:00
parent 504b4cda69
commit bf3725e040
7 changed files with 404 additions and 31 deletions
+44
View File
@@ -0,0 +1,44 @@
import { assert } from 'chai';
import { classifyAutoLoginFailure, OTP_REQUIRED_MESSAGE } from '../ts/gateway/loginFlow';
describe('automatic login failure flow', () => {
it('prompts for OTP without treating the challenge as a terminal login failure', () => {
assert.equal(
classifyAutoLoginFailure({
result: false,
silent: false,
reqOTP: true,
reason: '인증 코드를 입력해주세요',
}, 0),
'prompt_otp'
);
assert.equal(OTP_REQUIRED_MESSAGE, '인증 코드 입력이 필요합니다.');
});
it('keeps the existing retry and alert behavior for non-OTP failures', () => {
assert.equal(
classifyAutoLoginFailure({
result: false,
silent: false,
reason: '자동 로그인: 절차 오류',
}, 0),
'retry'
);
assert.equal(
classifyAutoLoginFailure({
result: false,
silent: false,
reason: '로그인할 수 없습니다.',
}, 1),
'alert'
);
assert.equal(
classifyAutoLoginFailure({
result: false,
silent: true,
reason: 'failed',
}, 1),
'silent'
);
});
});
+2 -1
View File
@@ -36,5 +36,6 @@ export type AutoLoginResponse = {
export type AutoLoginFailed = {
result: false,
silent: boolean,
reqOTP?: boolean,
reason: string,
}
}
+28 -25
View File
@@ -12,8 +12,9 @@ import { delay } from '@util/delay';
import { Modal } from 'bootstrap';
import '@/gateway/common';
import { isString } from 'lodash-es';
import { SammoRootAPI, type InvalidResponse } from '@/SammoRootAPI';
import type { LoginFailed, LoginResponse, LoginResponseWithKakao, OTPResponse } from '@/defs/API/Login';
import { SammoRootAPI, type InvalidResponse } from '@/SammoRootAPI';
import type { LoginFailed, LoginResponse, LoginResponseWithKakao, OTPResponse } from '@/defs/API/Login';
import { classifyAutoLoginFailure, OTP_REQUIRED_MESSAGE } from '@/gateway/loginFlow';
declare global {
interface Window {
getOAuthToken: (mode: string, scope_list: string[]) => void;
@@ -51,9 +52,20 @@ function getToken(): [number, string] | undefined {
return token;
}
function resetToken() {
localStorage.removeItem(LOGIN_TOKEN_KEY);
}
function resetToken() {
localStorage.removeItem(LOGIN_TOKEN_KEY);
}
function showOTPModal() {
const modalEl = unwrap(document.querySelector('#modalOTP'));
if (!modalOTP) {
modalOTP = new Modal(modalEl);
modalEl.addEventListener('shown.bs.modal', function () {
unwrap(document.querySelector<HTMLElement>('#otp_code')).focus();
});
}
modalOTP.show();
}
async function tryAutoLogin() {
try {
@@ -93,13 +105,18 @@ async function tryAutoLogin() {
}, true);
if (!loginResult.result) {
if (loginResult.reason === '자동 로그인: 절차 오류' && attempt === 0) {
const failureAction = classifyAutoLoginFailure(loginResult, attempt);
if (failureAction === 'retry') {
console.warn('auto login failed by procedure error. retrying once.');
await delay(150);
continue;
}
if (!loginResult.silent) {
if (failureAction === 'prompt_otp') {
alert(OTP_REQUIRED_MESSAGE);
showOTPModal();
return;
}
if (failureAction === 'alert') {
alert(loginResult.reason);
}
console.error(loginResult.reason);
@@ -208,14 +225,7 @@ async function doLoginUsingOAuth() {
return;
}
const modalEl = unwrap(document.querySelector('#modalOTP'))
if (!modalOTP) {
modalOTP = new Modal(modalEl);
modalEl.addEventListener('shown.bs.modal', function () {
unwrap(document.querySelector<HTMLElement>('#otp_code')).focus();
});
}
modalOTP.show();
showOTPModal();
}
function postOAuthResult(mode: string) {
@@ -314,14 +324,7 @@ $(async function ($) {
return;
}
const modalEl = unwrap(document.querySelector('#modalOTP'))
if (!modalOTP) {
modalOTP = new Modal(modalEl);
modalEl.addEventListener('shown.bs.modal', function () {
unwrap(document.querySelector<HTMLElement>('#otp_code')).focus();
});
}
modalOTP.show();
showOTPModal();
});
$('#otp_form').on('submit', async function (e) {
@@ -393,4 +396,4 @@ window.fitIframe = function () {
const iframe = unwrap(document.querySelector<HTMLIFrameElement>('#running_map'));//TODO: 근황 여러개 볼 수 있도록?
const scrollHeight = unwrap(iframe.contentWindow).document.body.scrollHeight;
iframe.style.height = `${scrollHeight}px`;
}
}
+18
View File
@@ -0,0 +1,18 @@
import type { AutoLoginFailed } from '@/defs/API/Login';
export const OTP_REQUIRED_MESSAGE = '인증 코드 입력이 필요합니다.';
export type AutoLoginFailureAction = 'retry' | 'prompt_otp' | 'alert' | 'silent';
export function classifyAutoLoginFailure(result: AutoLoginFailed, attempt: number): AutoLoginFailureAction {
if (result.reason === '자동 로그인: 절차 오류' && attempt === 0) {
return 'retry';
}
if (result.reqOTP) {
return 'prompt_otp';
}
if (result.silent) {
return 'silent';
}
return 'alert';
}