중복 로그인 확인 및 처리 로직 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- 동시 접속 확인 API(DuplicateLoginController) 및 서비스 구현
- 중복 세션 확인 후 로그인 확정/취소 처리 로직 추가
- 2FA 흐름과 연계된 프론트엔드 수정 및 UI 개선
This commit is contained in:
Rinjae
2026-07-28 17:00:10 +09:00
parent d4d5d66727
commit 929980c089
17 changed files with 446 additions and 103 deletions
@@ -149,34 +149,21 @@
});
}
// 로그인 전 중복 접속 확인 → 중복 시 기존 세션 강제 로그아웃 여부 질의
function checkDuplicateAndLogin(form) {
var loginId = form.id.value;
$.ajax({
url: /*[[@{/api/session/check-duplicate}]]*/ '/api/session/check-duplicate',
type: 'POST',
data: { loginId: loginId },
success: function (data) {
if (data && data.duplicateSession) {
$('#loginLoading').removeClass('active');
var msg = '해당 계정은 이미 다른 곳(' + data.ipAddress + ')에서 접속 중입니다.<br>'
+ '접속 시각: ' + data.loginTime + '<br><br>'
+ '기존 접속을 해제하고 로그인하시겠습니까?';
customPopups.showConfirm(msg, function (confirmed) {
if (confirmed) {
$('#loginLoading').addClass('active');
form.submit();
}
});
} else {
form.submit();
}
},
error: function () {
// 중복 체크 실패 시 로그인은 그대로 진행
form.submit();
}
});
// 동시 접속 확인은 비밀번호 검증(1차 인증) 통과 후 서버가 안내한다
// (2FA pending 또는 /login?duplicate=1 대기 상태에서 duplicateInfo 모델로 수신).
function duplicateConfirmMessage(info) {
return '해당 계정은 이미 다른 곳(' + info.ipAddress + ')에서 접속 중입니다.<br>'
+ '접속 시각: ' + info.loginTime + '<br><br>'
+ '기존 접속을 해제하고 로그인하시겠습니까?';
}
// 세션 CSRF 헤더 (meta[name=_csrf]) — 인증 후 확인/취소 POST 용
function csrfHeaders() {
var t = document.querySelector('meta[name="_csrf"]');
var h = document.querySelector('meta[name="_csrf_header"]');
var headers = {};
headers[h ? h.getAttribute('content') : 'X-XSRF-TOKEN'] = t ? t.getAttribute('content') : '';
return headers;
}
function fnInit() {
@@ -242,7 +229,7 @@
customPopups.showAlert('[[#{login.passLengthShort}]]');
} else {
refreshCsrfAndThen(form, function () {
checkDuplicateAndLogin(form);
form.submit();
});
}
}
@@ -260,9 +247,12 @@
}
});
// 로그인 2FA: 1차 인증 통과 후 pending 상태면 추가 인증 팝업 자동 오픈
// 1차 인증(비밀번호) 통과 후 흐름: [동시 접속 확인] → [2FA] 순서.
var twoFactorPending = [[${twoFactorPending}]];
if (twoFactorPending && typeof TwoFactorAuth !== 'undefined') {
var duplicateConfirmPending = [[${duplicateConfirmPending}]];
var duplicateInfo = [[${duplicateInfo}]];
function openLoginTwoFactor() {
TwoFactorAuth.open({
mode: 'login',
onSuccess: function (res) {
@@ -279,6 +269,53 @@
});
}
if (twoFactorPending && typeof TwoFactorAuth !== 'undefined') {
// 로그인 2FA pending: 동시 접속이 있으면 확인 후 2FA 팝업, 취소 시 로그인 포기
if (duplicateInfo) {
customPopups.showConfirm(duplicateConfirmMessage(duplicateInfo), function (confirmed) {
if (confirmed) {
openLoginTwoFactor();
} else {
$.ajax({
url: /*[[@{/auth/2fa/cancel}]]*/ '/auth/2fa/cancel',
type: 'POST',
headers: csrfHeaders(),
data: { reason: 'CANCELLED' }
});
customPopups.showAlert('로그인이 취소되었습니다.');
}
});
} else {
openLoginTwoFactor();
}
} else if (duplicateConfirmPending && duplicateInfo) {
// 2FA off + 동시 접속: 확인 시 서버가 로그인 확정(기존 접속 해제), 취소 시 포기
customPopups.showConfirm(duplicateConfirmMessage(duplicateInfo), function (confirmed) {
var url = confirmed
? /*[[@{/login/duplicate/confirm}]]*/ '/login/duplicate/confirm'
: /*[[@{/login/duplicate/cancel}]]*/ '/login/duplicate/cancel';
$.ajax({
url: url,
type: 'POST',
headers: csrfHeaders(),
success: function (res) {
if (confirmed) {
if (res && res.valid && res.redirect) {
window.location.href = res.redirect;
} else {
customPopups.showAlert((res && res.message) || '로그인 확인이 만료되었습니다. 다시 로그인해주세요.');
}
}
},
error: function () {
if (confirmed) {
customPopups.showAlert('로그인 처리 중 오류가 발생했습니다. 다시 로그인해주세요.');
}
}
});
});
}
fnInit();
});
</script>