mirror of
https://github.com/sotam0316/brain_dogfood.git
synced 2026-04-24 19:48:35 +09:00
feat: implement session timeout countdown and UI optimization
This commit is contained in:
@@ -16,6 +16,8 @@ import { CategoryManager } from './js/components/CategoryManager.js';
|
||||
import { ModalManager } from './js/components/ModalManager.js';
|
||||
import { I18nManager } from './js/utils/I18nManager.js';
|
||||
import { Constants } from './js/utils/Constants.js';
|
||||
import { SessionManager } from './js/components/SessionManager.js';
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
UI.initSidebarToggle();
|
||||
@@ -244,6 +246,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// --- 🔹 App Start ---
|
||||
AppService.refreshData(updateSidebarCallback);
|
||||
VisualLinker.init(); // 💡 연결 도구 초기화
|
||||
SessionManager.init(); // ⏱️ 세션 타이머 초기화 (종료/EXIT)
|
||||
|
||||
|
||||
// 💡 전역 취소 리스너 (시각적 연결용)
|
||||
window.addEventListener('keydown', (e) => {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"nav_explorer": "Knowledge Explorer",
|
||||
"nav_calendar": "Calendar",
|
||||
"nav_nebula": "Knowledge Nebula",
|
||||
"nav_logout": "Logout",
|
||||
"nav_logout": "EXIT",
|
||||
"nav_settings": "Settings",
|
||||
"nav_toggle": "Toggle Sidebar",
|
||||
"nav_categories": "Categories",
|
||||
@@ -35,6 +35,7 @@
|
||||
"settings_ai_enable": "Enable AI Features",
|
||||
"settings_category_enable": "Enable Category Feature (Advanced)",
|
||||
"settings_lang": "Language",
|
||||
"settings_session_timeout": "Session Timeout (min, min 10m)",
|
||||
"settings_save": "Save Settings",
|
||||
"settings_reset": "Reset",
|
||||
"settings_close": "Close",
|
||||
@@ -43,12 +44,14 @@
|
||||
"msg_delete_confirm": "Are you sure you want to delete this memo? This cannot be undone.",
|
||||
"msg_save_success": "Saved successfully!",
|
||||
"msg_settings_saved": "🎨 Settings have been saved to the server!",
|
||||
"msg_session_timeout_min": "Session timeout must be at least 10 minutes.",
|
||||
"msg_ai_loading": "Gemini AI is analyzing the memo...",
|
||||
"msg_encrypted_locked": "🚫 Encryption detected. Decrypt first to modify.",
|
||||
"msg_auth_failed": "Invalid credentials. Please try again.",
|
||||
"msg_network_error": "Network instability or server error occurred.",
|
||||
"msg_confirm_discard": "Discard all current content and delete uploaded files from the server?",
|
||||
"msg_alert_password_required": "A password is required to encrypt this knowledge.",
|
||||
"msg_session_expired": "⌛ Session expired. Please log in again for security.",
|
||||
"msg_draft_restore_confirm": "📝 There is an auto-saved draft.\nTitle: \"{title}\"\nWould you like to restore it?",
|
||||
|
||||
"title_pin": "Pin to Top",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"nav_explorer": "지식 탐색기",
|
||||
"nav_calendar": "달력 탐색",
|
||||
"nav_nebula": "지식 맵 보기",
|
||||
"nav_logout": "로그아웃",
|
||||
"nav_logout": "종료",
|
||||
"nav_settings": "환경 설정",
|
||||
"nav_toggle": "사이드바 토글",
|
||||
"nav_categories": "카테고리",
|
||||
@@ -37,6 +37,7 @@
|
||||
"settings_ai_enable": "AI 기능 활성화",
|
||||
"settings_category_enable": "카테고리 기능 활성화 (고급)",
|
||||
"settings_lang": "언어 설정",
|
||||
"settings_session_timeout": "세션 유지 시간 (분, 최소 10분)",
|
||||
"settings_save": "저장",
|
||||
"settings_reset": "초기화",
|
||||
"settings_close": "닫기",
|
||||
@@ -45,12 +46,14 @@
|
||||
"msg_delete_confirm": "이 메모를 정말 삭제할까요? 되돌릴 수 없습니다.",
|
||||
"msg_save_success": "저장되었습니다!",
|
||||
"msg_settings_saved": "🎨 테마 설정이 서버에 저장되었습니다!",
|
||||
"msg_session_timeout_min": "세션 타임아웃은 최소 10분 이상이어야 합니다.",
|
||||
"msg_ai_loading": "Gemini AI가 메모를 분석 중입니다...",
|
||||
"msg_encrypted_locked": "🚫 암호화된 메모입니다. 먼저 해독하세요.",
|
||||
"msg_auth_failed": "올바른 자격 증명이 아닙니다. 다시 시도해 주세요.",
|
||||
"msg_network_error": "네트워크 불안정 또는 서버 오류가 발생했습니다.",
|
||||
"msg_confirm_discard": "작성 중인 내용을 지우고 창을 닫을까요?",
|
||||
"msg_alert_password_required": "암호화하려면 비밀번호를 입력해야 합니다.",
|
||||
"msg_session_expired": "⌛ 세션이 만료되었습니다. 보안을 위해 다시 로그인해 주세요.",
|
||||
"msg_draft_restore_confirm": "📝 임시 저장된 메모가 있습니다.\n제목: \"{title}\"\n복원하시겠습니까?",
|
||||
|
||||
"title_pin": "중요 (상단 고정)",
|
||||
|
||||
Reference in New Issue
Block a user