feat: implement session timeout countdown and UI optimization

This commit is contained in:
leeyj
2026-04-20 16:47:47 +09:00
parent ac58e14c8c
commit b376eedc48
15 changed files with 321 additions and 18 deletions
+21
View File
@@ -146,5 +146,26 @@ export const AppService = {
if (changed) {
await this.refreshData(onUpdateSidebar);
}
},
/**
* 세션 유지 확인을 위한 Heartbeat 시작 (1~2분 간격)
*/
startSessionHeartbeat() {
if (this.heartbeatInterval) return; // 이미 실행 중이면 무시
console.log('[AppService] Session heartbeat started.');
// 초기 실행 후 인터벌 설정
API.checkAuthStatus().catch(() => {});
this.heartbeatInterval = setInterval(async () => {
try {
await API.checkAuthStatus();
} catch (err) {
console.warn('[AppService] Session expired or server error during heartbeat.');
// API.request에서 401 발생 시 이미 리다이렉트 처리함
}
}, 120000); // 120,000ms = 2분
}
};
+13 -6
View File
@@ -80,16 +80,23 @@ export const API = {
return await this.request(`/api/attachments/${filename}`, { method: 'DELETE' });
},
// 설정 관련
fetchSettings: async () => {
const res = await fetch('/api/settings');
return await res.json();
fetchSettings: async function() {
return await this.request('/api/settings');
},
saveSettings: async (data) => {
const res = await fetch('/api/settings', {
saveSettings: async function(data) {
return await this.request('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return await res.json();
},
// 인증 상태 확인 (Heartbeat용)
checkAuthStatus: async function() {
try {
return await this.request('/api/auth/status');
} catch (err) {
// API.request 내부에서 401 발생 시 이미 리다이렉트 처리함
throw err;
}
}
};
+127
View File
@@ -0,0 +1,127 @@
/**
* SessionManager.js
* - 세션 라이프타임 관리 및 로그아웃 버튼 카운트다운 인터페이스
* - 사용자 활동(클릭, 키 입력) 시 타이머 초기화
*/
import { API } from '../api.js';
import { I18nManager } from '../utils/I18nManager.js';
export const SessionManager = {
state: {
timeoutMinutes: 60, // 기본값
remainingSeconds: 3600,
timerId: null,
logoutBtn: null,
baseText: ''
},
/**
* 초기화: 설정 로드 및 이벤트 바인딩
*/
async init() {
try {
const settings = await API.fetchSettings();
this.state.timeoutMinutes = settings.session_timeout || 60;
this.resetTimer();
this.state.logoutBtn = document.getElementById('logoutBtn');
if (this.state.logoutBtn) {
const textSpan = this.state.logoutBtn.querySelector('.text');
if (textSpan) {
// i18n 로딩이 늦을 경우를 위해 초기 텍스트 보관
this.state.baseText = textSpan.textContent.trim().split(' ')[0] || I18nManager.t('nav_logout');
console.log(`⏱️ SessionManager: Timer target found. Base text: ${this.state.baseText}`);
this.startCountdown();
} else {
console.error('❌ SessionManager: .text span not found in logoutBtn');
}
} else {
console.error('❌ SessionManager: logoutBtn not found');
}
// 사용자 활동 감지 이벤트 바인딩
this.bindActivityListeners();
console.log(`⏱️ SessionManager: Initialized with ${this.state.timeoutMinutes} min timeout.`);
} catch (err) {
console.error('❌ SessionManager Init Error:', err);
}
},
/**
* 타이머 시작 (1초마다 업데이트)
*/
startCountdown() {
if (this.state.timerId) clearInterval(this.state.timerId);
this.state.timerId = setInterval(() => {
this.state.remainingSeconds--;
if (this.state.remainingSeconds <= 0) {
this.handleTimeout();
} else {
this.updateUI();
}
}, 1000);
},
/**
* 타이머를 원복함 (활동 감지 시 호출)
*/
resetTimer() {
this.state.remainingSeconds = this.state.timeoutMinutes * 60;
this.updateUI();
},
/**
* UI 업데이트 (로그아웃 버튼 텍스트 변경)
*/
updateUI() {
if (!this.state.logoutBtn) return;
const minutes = Math.floor(this.state.remainingSeconds / 60);
const seconds = this.state.remainingSeconds % 60;
const timeStr = `(${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')})`;
const textSpan = this.state.logoutBtn.querySelector('.text');
if (textSpan) {
// i18n이 중간에 바뀔 수 있으므로 매번 새로 가져오는 것이 안전할 수 있음
const currentBase = I18nManager.t('nav_logout');
textSpan.textContent = `${currentBase} ${timeStr}`;
}
},
/**
* 활동 감지 리스너 등록
*/
bindActivityListeners() {
const resetAction = () => this.resetTimer();
// 클릭 및 키보드 입력 감지
window.addEventListener('mousedown', resetAction);
window.addEventListener('keydown', resetAction);
window.addEventListener('touchstart', resetAction);
// 추가적으로 API 요청이 발생할 때도 리셋되면 좋음 (api.js에서 처리 권장)
},
/**
* 타임아웃 설정을 갱신함 (설정 변경 시 호출 가능)
*/
updateTimeout(minutes) {
if (!minutes || isNaN(minutes)) return;
this.state.timeoutMinutes = parseInt(minutes);
this.resetTimer();
console.log(`⏱️ SessionManager: Timeout updated to ${this.state.timeoutMinutes} min.`);
},
/**
* 타임아웃 발생 시 처리
*/
handleTimeout() {
clearInterval(this.state.timerId);
console.warn('⌛ Session expired. Logging out...');
alert(I18nManager.t('msg_session_expired') || '세션이 만료되었습니다. 다시 로그인해주세요.');
window.location.href = '/logout';
}
};
+32 -2
View File
@@ -65,10 +65,22 @@ export const ThemeManager = {
data['enable_ai'] = document.getElementById('set-enable-ai').checked;
data['enable_categories'] = document.getElementById('set-enable-categories').checked;
// 언어 설정이 UI에 있다면 추가 (현재는 config.json 수동 명시 권장이나 대비책 마련)
// 언어 설정 처리
const langSelect = document.getElementById('set-lang');
const newLang = langSelect ? langSelect.value : (this.initialLang || 'ko');
if (langSelect) data['lang'] = newLang;
// 세션 타임아웃 검증
const timeoutInput = document.getElementById('set-session-timeout');
let sessionTimeout = timeoutInput ? parseInt(timeoutInput.value) : 60;
if (sessionTimeout < 10) {
alert(I18nManager.t('msg_session_timeout_min') || '세션 타임아웃은 최소 10분 이상이어야 합니다.');
sessionTimeout = 10;
timeoutInput.value = 10;
return;
}
data['session_timeout'] = sessionTimeout;
try {
await API.saveSettings(data);
@@ -97,7 +109,8 @@ export const ThemeManager = {
encrypted_border: "#00f3ff",
ai_accent: "#8b5cf6",
lang: "ko",
enable_categories: false
enable_categories: false,
session_timeout: 60
};
this.applyTheme(defaults);
}
@@ -152,6 +165,23 @@ export const ThemeManager = {
await I18nManager.init(lang);
const langSelect = document.getElementById('set-lang');
if (langSelect) langSelect.value = lang;
// 5. 세션 타임아웃 UI 반영 및 Heartbeat 시작
const sessionTimeout = settings.session_timeout || 60;
const timeoutInput = document.getElementById('set-session-timeout');
if (timeoutInput) timeoutInput.value = sessionTimeout;
// 세션 타이머(종료/EXIT) 즉시 반영
import('./SessionManager.js').then(({ SessionManager }) => {
if (SessionManager && typeof SessionManager.updateTimeout === 'function') {
SessionManager.updateTimeout(sessionTimeout);
}
});
// 세션 체크 시작 (Heartbeat) - AppService에 위임하거나 여기서 직접 실행
if (window.AppService && typeof window.AppService.startSessionHeartbeat === 'function') {
window.AppService.startSessionHeartbeat();
}
},
rgbaToHex(rgba) {