Files
TK-FB-Project/web-ui/js/work-analysis/state-manager.js
Hyungi Ahn ed40eec261 fix: 그룹 리더 대시보드 작업 저장/삭제 오류 해결 및 작업 분석 시스템 성능 최적화
🔧 그룹 리더 대시보드 수정사항:
- API 호출 방식 수정 (modern-dashboard.js)
- 서버 API 요구사항에 맞는 데이터 구조 변경
- work_entries 배열 구조로 변경
- work_type_id → task_id 필드명 매핑
- 400 Bad Request 오류 해결

 작업 분석 시스템 성능 최적화:
- 중복 함수 제거 (isWeekend, isVacationProject 통합)
- WorkAnalysisAPI 캐싱 시스템 구현 (5분 만료)
- 네임스페이스 조직화 (utils, ui, analysis, render)
- ErrorHandler 통합 에러 처리 시스템
- 성능 모니터링 및 메모리 누수 방지
- GPU 가속 CSS 애니메이션 추가
- 디바운스/스로틀 함수 적용
- 의미 없는 통계 카드 제거

📊 작업 분석 페이지 개선:
- 프로그레스 바 애니메이션
- 토스트 알림 시스템
- 부드러운 전환 효과
- 반응형 최적화
- 메모리 사용량 모니터링
2025-11-05 10:12:52 +09:00

383 lines
10 KiB
JavaScript

/**
* Work Analysis State Manager Module
* 작업 분석 페이지의 상태 관리를 담당하는 모듈
*/
class WorkAnalysisStateManager {
constructor() {
this.state = {
// 분석 설정
analysisMode: 'period', // 'period' | 'project'
confirmedPeriod: {
start: null,
end: null,
confirmed: false
},
// UI 상태
currentTab: 'work-status',
isAnalysisEnabled: false,
isLoading: false,
// 데이터 캐시
cache: {
basicStats: null,
chartData: null,
projectDistribution: null,
errorAnalysis: null
},
// 에러 상태
lastError: null
};
this.listeners = new Map();
this.init();
}
/**
* 초기화
*/
init() {
console.log('🔧 상태 관리자 초기화');
// 기본 날짜 설정 (현재 월)
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
this.updateState({
confirmedPeriod: {
start: this.formatDate(startOfMonth),
end: this.formatDate(endOfMonth),
confirmed: false
}
});
}
/**
* 상태 업데이트
* @param {Object} updates - 업데이트할 상태
*/
updateState(updates) {
const prevState = { ...this.state };
this.state = { ...this.state, ...updates };
console.log('🔄 상태 업데이트:', updates);
// 리스너들에게 상태 변경 알림
this.notifyListeners(prevState, this.state);
}
/**
* 상태 리스너 등록
* @param {string} key - 리스너 키
* @param {Function} callback - 콜백 함수
*/
subscribe(key, callback) {
this.listeners.set(key, callback);
}
/**
* 상태 리스너 제거
* @param {string} key - 리스너 키
*/
unsubscribe(key) {
this.listeners.delete(key);
}
/**
* 리스너들에게 알림
*/
notifyListeners(prevState, newState) {
this.listeners.forEach((callback, key) => {
try {
callback(newState, prevState);
} catch (error) {
console.error(`❌ 리스너 ${key} 오류:`, error);
}
});
}
// ========== 분석 설정 관리 ==========
/**
* 분석 모드 변경
* @param {string} mode - 분석 모드 ('period' | 'project')
*/
setAnalysisMode(mode) {
if (mode !== 'period' && mode !== 'project') {
throw new Error('유효하지 않은 분석 모드입니다.');
}
this.updateState({
analysisMode: mode,
currentTab: 'work-status' // 모드 변경 시 첫 번째 탭으로 리셋
});
}
/**
* 기간 확정
* @param {string} startDate - 시작일
* @param {string} endDate - 종료일
*/
confirmPeriod(startDate, endDate) {
// 날짜 유효성 검사
if (!startDate || !endDate) {
throw new Error('시작일과 종료일을 모두 입력해주세요.');
}
const start = new Date(startDate);
const end = new Date(endDate);
if (start > end) {
throw new Error('시작일이 종료일보다 늦을 수 없습니다.');
}
// 최대 1년 제한
const maxDays = 365;
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
if (daysDiff > maxDays) {
throw new Error(`분석 기간은 최대 ${maxDays}일까지 가능합니다.`);
}
this.updateState({
confirmedPeriod: {
start: startDate,
end: endDate,
confirmed: true
},
isAnalysisEnabled: true,
// 기간 변경 시 캐시 초기화
cache: {
basicStats: null,
chartData: null,
projectDistribution: null,
errorAnalysis: null
}
});
console.log('✅ 기간 확정:', startDate, '~', endDate);
}
/**
* 현재 탭 변경
* @param {string} tabId - 탭 ID
*/
setCurrentTab(tabId) {
const validTabs = ['work-status', 'project-distribution', 'worker-performance', 'error-analysis'];
if (!validTabs.includes(tabId)) {
console.warn('유효하지 않은 탭 ID:', tabId);
return;
}
this.updateState({ currentTab: tabId });
// DOM 업데이트 직접 수행
this.updateTabDOM(tabId);
console.log('🔄 탭 전환:', tabId);
}
/**
* 탭 DOM 업데이트
* @param {string} tabId - 탭 ID
*/
updateTabDOM(tabId) {
// 탭 버튼 업데이트
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
if (button.dataset.tab === tabId) {
button.classList.add('active');
}
});
// 탭 컨텐츠 업데이트
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
if (content.id === `${tabId}-tab`) {
content.classList.add('active');
}
});
}
// ========== 로딩 상태 관리 ==========
/**
* 로딩 시작
* @param {string} message - 로딩 메시지
*/
startLoading(message = '분석 중입니다...') {
this.updateState({
isLoading: true,
loadingMessage: message
});
}
/**
* 로딩 종료
*/
stopLoading() {
this.updateState({
isLoading: false,
loadingMessage: null
});
}
// ========== 데이터 캐시 관리 ==========
/**
* 캐시 데이터 저장
* @param {string} key - 캐시 키
* @param {*} data - 저장할 데이터
*/
setCache(key, data) {
this.updateState({
cache: {
...this.state.cache,
[key]: {
data,
timestamp: Date.now()
}
}
});
}
/**
* 캐시 데이터 조회
* @param {string} key - 캐시 키
* @param {number} maxAge - 최대 유효 시간 (밀리초)
* @returns {*} 캐시된 데이터 또는 null
*/
getCache(key, maxAge = 5 * 60 * 1000) { // 기본 5분
const cached = this.state.cache[key];
if (!cached) {
return null;
}
const age = Date.now() - cached.timestamp;
if (age > maxAge) {
console.log('🗑️ 캐시 만료:', key);
return null;
}
console.log('📦 캐시 히트:', key);
return cached.data;
}
/**
* 캐시 초기화
* @param {string} key - 특정 키만 초기화 (선택사항)
*/
clearCache(key = null) {
if (key) {
this.updateState({
cache: {
...this.state.cache,
[key]: null
}
});
} else {
this.updateState({
cache: {
basicStats: null,
chartData: null,
projectDistribution: null,
errorAnalysis: null
}
});
}
}
// ========== 에러 관리 ==========
/**
* 에러 설정
* @param {Error|string} error - 에러 객체 또는 메시지
*/
setError(error) {
const errorInfo = {
message: error instanceof Error ? error.message : error,
timestamp: Date.now(),
stack: error instanceof Error ? error.stack : null
};
this.updateState({
lastError: errorInfo,
isLoading: false
});
console.error('❌ 에러 발생:', errorInfo);
}
/**
* 에러 초기화
*/
clearError() {
this.updateState({ lastError: null });
}
// ========== 유틸리티 ==========
/**
* 날짜 포맷팅
* @param {Date} date - 날짜 객체
* @returns {string} YYYY-MM-DD 형식
*/
formatDate(date) {
return date.toISOString().split('T')[0];
}
/**
* 현재 상태 조회
* @returns {Object} 현재 상태
*/
getState() {
return { ...this.state };
}
/**
* 분석 가능 여부 확인
* @returns {boolean} 분석 가능 여부
*/
canAnalyze() {
return this.state.confirmedPeriod.confirmed &&
this.state.confirmedPeriod.start &&
this.state.confirmedPeriod.end &&
!this.state.isLoading;
}
/**
* 상태 디버그 정보 출력
*/
debug() {
console.log('🔍 현재 상태:', this.state);
console.log('👂 등록된 리스너:', Array.from(this.listeners.keys()));
}
}
// 전역 인스턴스 생성
window.WorkAnalysisState = new WorkAnalysisStateManager();
// 하위 호환성을 위한 전역 변수들
Object.defineProperty(window, 'currentAnalysisMode', {
get: () => window.WorkAnalysisState.state.analysisMode,
set: (value) => window.WorkAnalysisState.setAnalysisMode(value)
});
Object.defineProperty(window, 'confirmedStartDate', {
get: () => window.WorkAnalysisState.state.confirmedPeriod.start
});
Object.defineProperty(window, 'confirmedEndDate', {
get: () => window.WorkAnalysisState.state.confirmedPeriod.end
});
Object.defineProperty(window, 'isAnalysisEnabled', {
get: () => window.WorkAnalysisState.state.isAnalysisEnabled
});
// Export는 브라우저 환경에서 제거됨