feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
382
deploy/tkfb-package/web-ui/js/work-analysis/state-manager.js
Normal file
382
deploy/tkfb-package/web-ui/js/work-analysis/state-manager.js
Normal file
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* 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는 브라우저 환경에서 제거됨
|
||||
Reference in New Issue
Block a user