feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
489
deploy/tkfb-package/web-ui/js/tbm/api.js
Normal file
489
deploy/tkfb-package/web-ui/js/tbm/api.js
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* TBM - API Client
|
||||
* TBM 관련 모든 API 호출을 관리
|
||||
*/
|
||||
|
||||
class TbmAPI {
|
||||
constructor() {
|
||||
this.state = window.TbmState;
|
||||
this.utils = window.TbmUtils;
|
||||
console.log('[TbmAPI] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기 데이터 로드 (작업자, 프로젝트, 안전 체크리스트, 공정, 작업, 작업장)
|
||||
*/
|
||||
async loadInitialData() {
|
||||
try {
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
const userInfo = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
this.state.currentUser = userInfo;
|
||||
console.log('👤 로그인 사용자:', this.state.currentUser, 'worker_id:', this.state.currentUser?.worker_id);
|
||||
|
||||
// 병렬로 데이터 로드
|
||||
await Promise.all([
|
||||
this.loadWorkers(),
|
||||
this.loadProjects(),
|
||||
this.loadSafetyChecks(),
|
||||
this.loadWorkTypes(),
|
||||
this.loadTasks(),
|
||||
this.loadWorkplaces(),
|
||||
this.loadWorkplaceCategories()
|
||||
]);
|
||||
|
||||
console.log('✅ 초기 데이터 로드 완료');
|
||||
} catch (error) {
|
||||
console.error('❌ 초기 데이터 로드 오류:', error);
|
||||
window.showToast?.('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 목록 로드 (생산팀 소속만)
|
||||
*/
|
||||
async loadWorkers() {
|
||||
try {
|
||||
const response = await window.apiCall('/workers?limit=1000&department_id=1');
|
||||
if (response) {
|
||||
let workers = Array.isArray(response) ? response : (response.data || []);
|
||||
// 활성 상태인 작업자만 필터링
|
||||
workers = workers.filter(w => w.status === 'active' && w.employment_status === 'employed');
|
||||
this.state.allWorkers = workers;
|
||||
console.log('✅ 작업자 목록 로드:', workers.length + '명');
|
||||
return workers;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 작업자 로딩 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로젝트 목록 로드 (활성 프로젝트만)
|
||||
*/
|
||||
async loadProjects() {
|
||||
try {
|
||||
const response = await window.apiCall('/projects?is_active=1');
|
||||
if (response) {
|
||||
const projects = Array.isArray(response) ? response : (response.data || []);
|
||||
this.state.allProjects = projects.filter(p =>
|
||||
p.is_active === 1 || p.is_active === true || p.is_active === '1'
|
||||
);
|
||||
console.log('✅ 프로젝트 목록 로드:', this.state.allProjects.length + '개 (활성)');
|
||||
return this.state.allProjects;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 프로젝트 로딩 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전 체크리스트 로드
|
||||
*/
|
||||
async loadSafetyChecks() {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/safety-checks');
|
||||
if (response && response.success) {
|
||||
this.state.allSafetyChecks = response.data;
|
||||
console.log('✅ 안전 체크리스트 로드:', this.state.allSafetyChecks.length + '개');
|
||||
return this.state.allSafetyChecks;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 안전 체크리스트 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정(Work Types) 목록 로드
|
||||
*/
|
||||
async loadWorkTypes() {
|
||||
try {
|
||||
const response = await window.apiCall('/daily-work-reports/work-types');
|
||||
if (response && response.success) {
|
||||
this.state.allWorkTypes = response.data || [];
|
||||
console.log('✅ 공정 목록 로드:', this.state.allWorkTypes.length + '개');
|
||||
return this.state.allWorkTypes;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 공정 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업(Tasks) 목록 로드
|
||||
*/
|
||||
async loadTasks() {
|
||||
try {
|
||||
const response = await window.apiCall('/tasks/active/list');
|
||||
if (response && response.success) {
|
||||
this.state.allTasks = response.data || [];
|
||||
console.log('✅ 작업 목록 로드:', this.state.allTasks.length + '개');
|
||||
return this.state.allTasks;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 작업 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 목록 로드
|
||||
*/
|
||||
async loadWorkplaces() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces?is_active=true');
|
||||
if (response && response.success) {
|
||||
this.state.allWorkplaces = response.data || [];
|
||||
console.log('✅ 작업장 목록 로드:', this.state.allWorkplaces.length + '개');
|
||||
return this.state.allWorkplaces;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 작업장 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 카테고리 로드
|
||||
*/
|
||||
async loadWorkplaceCategories() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces/categories/active/list');
|
||||
if (response && response.success) {
|
||||
this.state.allWorkplaceCategories = response.data || [];
|
||||
console.log('✅ 작업장 카테고리 로드:', this.state.allWorkplaceCategories.length + '개');
|
||||
return this.state.allWorkplaceCategories;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 작업장 카테고리 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘의 TBM만 로드 (TBM 입력 탭용)
|
||||
*/
|
||||
async loadTodayOnlyTbm() {
|
||||
const today = this.utils.getTodayKST();
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/date/${today}`);
|
||||
|
||||
if (response && response.success) {
|
||||
this.state.todaySessions = response.data || [];
|
||||
} else {
|
||||
this.state.todaySessions = [];
|
||||
}
|
||||
console.log('✅ 오늘 TBM 로드:', this.state.todaySessions.length + '건');
|
||||
return this.state.todaySessions;
|
||||
} catch (error) {
|
||||
console.error('❌ 오늘 TBM 조회 오류:', error);
|
||||
window.showToast?.('오늘 TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
this.state.todaySessions = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 TBM을 날짜별로 그룹화하여 로드
|
||||
*/
|
||||
async loadRecentTbmGroupedByDate() {
|
||||
try {
|
||||
const today = new Date();
|
||||
const dates = [];
|
||||
|
||||
// 최근 N일의 날짜 생성
|
||||
for (let i = 0; i < this.state.loadedDaysCount; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
dates.push(dateStr);
|
||||
}
|
||||
|
||||
// 각 날짜의 TBM 로드
|
||||
this.state.dateGroupedSessions = {};
|
||||
this.state.allLoadedSessions = [];
|
||||
|
||||
const promises = dates.map(date => window.apiCall(`/tbm/sessions/date/${date}`));
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach((response, index) => {
|
||||
const date = dates[index];
|
||||
if (response && response.success && response.data && response.data.length > 0) {
|
||||
let sessions = response.data;
|
||||
|
||||
// admin이 아니면 본인이 작성한 TBM만 필터링
|
||||
if (!this.state.isAdminUser()) {
|
||||
const userId = this.state.currentUser?.user_id;
|
||||
const workerId = this.state.currentUser?.worker_id;
|
||||
sessions = sessions.filter(s => {
|
||||
return s.created_by === userId ||
|
||||
s.leader_id === workerId ||
|
||||
s.created_by_name === this.state.currentUser?.name;
|
||||
});
|
||||
}
|
||||
|
||||
if (sessions.length > 0) {
|
||||
this.state.dateGroupedSessions[date] = sessions;
|
||||
this.state.allLoadedSessions = this.state.allLoadedSessions.concat(sessions);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ 날짜별 TBM 로드 완료:', this.state.allLoadedSessions.length + '건');
|
||||
return this.state.dateGroupedSessions;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 날짜별 로드 오류:', error);
|
||||
window.showToast?.('TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
this.state.dateGroupedSessions = {};
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜의 TBM 세션 목록 로드
|
||||
*/
|
||||
async loadTbmSessionsByDate(date) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/date/${date}`);
|
||||
|
||||
if (response && response.success) {
|
||||
this.state.allSessions = response.data || [];
|
||||
} else {
|
||||
this.state.allSessions = [];
|
||||
}
|
||||
return this.state.allSessions;
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 세션 조회 오류:', error);
|
||||
window.showToast?.('TBM 세션을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
this.state.allSessions = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 생성
|
||||
*/
|
||||
async createTbmSession(sessionData) {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '세션 생성 실패');
|
||||
}
|
||||
console.log('✅ TBM 세션 생성 완료:', response.data?.session_id);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 세션 생성 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 정보 조회
|
||||
*/
|
||||
async getSession(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}`);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '세션 조회 실패');
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 세션 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 조회
|
||||
*/
|
||||
async getTeamMembers(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '팀원 조회 실패');
|
||||
}
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 팀원 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 일괄 추가
|
||||
*/
|
||||
async addTeamMembers(sessionId, members) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/team/batch`,
|
||||
'POST',
|
||||
{ members }
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '팀원 추가 실패');
|
||||
}
|
||||
console.log('✅ TBM 팀원 추가 완료:', members.length + '명');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 팀원 추가 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 전체 삭제
|
||||
*/
|
||||
async clearTeamMembers(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/team/clear`, 'DELETE');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 팀원 삭제 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 안전 체크 조회
|
||||
*/
|
||||
async getSafetyChecks(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety`);
|
||||
return response?.data || [];
|
||||
} catch (error) {
|
||||
console.error('❌ 안전 체크 조회 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 안전 체크 (필터링된) 조회
|
||||
*/
|
||||
async getFilteredSafetyChecks(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety-checks/filtered`);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '체크리스트를 불러올 수 없습니다.');
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('❌ 필터링된 안전 체크 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 안전 체크 저장
|
||||
*/
|
||||
async saveSafetyChecks(sessionId, records) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/safety`,
|
||||
'POST',
|
||||
{ records }
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '저장 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ 안전 체크 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 완료 처리
|
||||
*/
|
||||
async completeTbmSession(sessionId, endTime) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/complete`,
|
||||
'POST',
|
||||
{ end_time: endTime }
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '완료 처리 실패');
|
||||
}
|
||||
console.log('✅ TBM 완료 처리:', sessionId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 완료 처리 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 인계 저장
|
||||
*/
|
||||
async saveHandover(handoverData) {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/handovers', 'POST', handoverData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '인계 요청 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ 작업 인계 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리별 작업장 로드
|
||||
*/
|
||||
async loadWorkplacesByCategory(categoryId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces?category_id=${categoryId}`);
|
||||
if (!response || !response.success || !response.data) {
|
||||
return [];
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('❌ 작업장 로드 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 지도 영역 로드
|
||||
*/
|
||||
async loadMapRegions(categoryId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`);
|
||||
if (response && response.success) {
|
||||
this.state.mapRegions = response.data || [];
|
||||
return this.state.mapRegions;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('❌ 지도 영역 로드 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmAPI = new TbmAPI();
|
||||
|
||||
// 하위 호환성: 기존 함수들
|
||||
window.loadInitialData = () => window.TbmAPI.loadInitialData();
|
||||
window.loadTodayOnlyTbm = () => window.TbmAPI.loadTodayOnlyTbm();
|
||||
window.loadTodayTbm = () => window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
window.loadAllTbm = () => {
|
||||
window.TbmState.loadedDaysCount = 30;
|
||||
return window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
};
|
||||
window.loadRecentTbmGroupedByDate = () => window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
window.loadTbmSessionsByDate = (date) => window.TbmAPI.loadTbmSessionsByDate(date);
|
||||
window.loadWorkplaceCategories = () => window.TbmAPI.loadWorkplaceCategories();
|
||||
window.loadWorkplacesByCategory = (categoryId) => window.TbmAPI.loadWorkplacesByCategory(categoryId);
|
||||
|
||||
// 더 많은 날짜 로드
|
||||
window.loadMoreTbmDays = async function() {
|
||||
window.TbmState.loadedDaysCount += 7;
|
||||
await window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
window.showToast?.(`최근 ${window.TbmState.loadedDaysCount}일의 TBM을 로드했습니다.`, 'success');
|
||||
};
|
||||
|
||||
console.log('[Module] tbm/api.js 로드 완료');
|
||||
325
deploy/tkfb-package/web-ui/js/tbm/index.js
Normal file
325
deploy/tkfb-package/web-ui/js/tbm/index.js
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* TBM - Module Loader
|
||||
* TBM 모듈을 초기화하고 연결하는 메인 진입점
|
||||
*
|
||||
* 로드 순서:
|
||||
* 1. state.js - 전역 상태 관리
|
||||
* 2. utils.js - 유틸리티 함수
|
||||
* 3. api.js - API 클라이언트
|
||||
* 4. index.js - 이 파일 (메인 컨트롤러)
|
||||
*/
|
||||
|
||||
class TbmController {
|
||||
constructor() {
|
||||
this.state = window.TbmState;
|
||||
this.api = window.TbmAPI;
|
||||
this.utils = window.TbmUtils;
|
||||
this.initialized = false;
|
||||
|
||||
console.log('[TbmController] 생성');
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기화
|
||||
*/
|
||||
async init() {
|
||||
if (this.initialized) {
|
||||
console.log('[TbmController] 이미 초기화됨');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🛠️ TBM 관리 페이지 초기화');
|
||||
|
||||
// API 함수가 로드될 때까지 대기
|
||||
let retryCount = 0;
|
||||
while (!window.apiCall && retryCount < 50) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
if (!window.apiCall) {
|
||||
window.showToast?.('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 오늘 날짜 설정 (서울 시간대 기준)
|
||||
const today = this.utils.getTodayKST();
|
||||
const tbmDateEl = document.getElementById('tbmDate');
|
||||
const sessionDateEl = document.getElementById('sessionDate');
|
||||
if (tbmDateEl) tbmDateEl.value = today;
|
||||
if (sessionDateEl) sessionDateEl.value = today;
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
this.setupEventListeners();
|
||||
|
||||
// 초기 데이터 로드
|
||||
await this.api.loadInitialData();
|
||||
await this.api.loadTodayOnlyTbm();
|
||||
|
||||
// 렌더링
|
||||
this.displayTodayTbmSessions();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[TbmController] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 설정
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// 탭 버튼들
|
||||
document.querySelectorAll('.tbm-tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tabName = btn.dataset.tab;
|
||||
if (tabName) this.switchTbmTab(tabName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 전환
|
||||
*/
|
||||
async switchTbmTab(tabName) {
|
||||
this.state.setCurrentTab(tabName);
|
||||
|
||||
// 탭 버튼 활성화 상태 변경
|
||||
document.querySelectorAll('.tbm-tab-btn').forEach(btn => {
|
||||
if (btn.dataset.tab === tabName) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 탭 컨텐츠 표시 변경
|
||||
document.querySelectorAll('.tbm-tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
const tabContent = document.getElementById(`${tabName}-tab`);
|
||||
if (tabContent) tabContent.classList.add('active');
|
||||
|
||||
// 탭에 따라 데이터 로드
|
||||
if (tabName === 'tbm-input') {
|
||||
await this.api.loadTodayOnlyTbm();
|
||||
this.displayTodayTbmSessions();
|
||||
} else if (tabName === 'tbm-manage') {
|
||||
await this.api.loadRecentTbmGroupedByDate();
|
||||
this.displayTbmGroupedByDate();
|
||||
this.updateViewModeIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘의 TBM 세션 표시
|
||||
*/
|
||||
displayTodayTbmSessions() {
|
||||
const grid = document.getElementById('todayTbmGrid');
|
||||
const emptyState = document.getElementById('todayEmptyState');
|
||||
const todayTotalEl = document.getElementById('todayTotalSessions');
|
||||
const todayCompletedEl = document.getElementById('todayCompletedSessions');
|
||||
const todayActiveEl = document.getElementById('todayActiveSessions');
|
||||
|
||||
const sessions = this.state.todaySessions;
|
||||
|
||||
if (sessions.length === 0) {
|
||||
if (grid) grid.innerHTML = '';
|
||||
if (emptyState) emptyState.style.display = 'flex';
|
||||
if (todayTotalEl) todayTotalEl.textContent = '0';
|
||||
if (todayCompletedEl) todayCompletedEl.textContent = '0';
|
||||
if (todayActiveEl) todayActiveEl.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
|
||||
const completedCount = sessions.filter(s => s.status === 'completed').length;
|
||||
const activeCount = sessions.filter(s => s.status === 'draft').length;
|
||||
|
||||
if (todayTotalEl) todayTotalEl.textContent = sessions.length;
|
||||
if (todayCompletedEl) todayCompletedEl.textContent = completedCount;
|
||||
if (todayActiveEl) todayActiveEl.textContent = activeCount;
|
||||
|
||||
if (grid) {
|
||||
grid.innerHTML = sessions.map(session => this.createSessionCard(session)).join('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜별 그룹으로 TBM 표시
|
||||
*/
|
||||
displayTbmGroupedByDate() {
|
||||
const container = document.getElementById('tbmDateGroupsContainer');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const totalSessionsEl = document.getElementById('totalSessions');
|
||||
const completedSessionsEl = document.getElementById('completedSessions');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const sortedDates = Object.keys(this.state.dateGroupedSessions).sort((a, b) =>
|
||||
new Date(b) - new Date(a)
|
||||
);
|
||||
|
||||
if (sortedDates.length === 0 || this.state.allLoadedSessions.length === 0) {
|
||||
container.innerHTML = '';
|
||||
if (emptyState) emptyState.style.display = 'flex';
|
||||
if (totalSessionsEl) totalSessionsEl.textContent = '0';
|
||||
if (completedSessionsEl) completedSessionsEl.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
|
||||
// 통계 업데이트
|
||||
const completedCount = this.state.allLoadedSessions.filter(s => s.status === 'completed').length;
|
||||
if (totalSessionsEl) totalSessionsEl.textContent = this.state.allLoadedSessions.length;
|
||||
if (completedSessionsEl) completedSessionsEl.textContent = completedCount;
|
||||
|
||||
// 날짜별 그룹 HTML 생성
|
||||
const today = this.utils.getTodayKST();
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
|
||||
container.innerHTML = sortedDates.map(date => {
|
||||
const sessions = this.state.dateGroupedSessions[date];
|
||||
const dateObj = new Date(date + 'T00:00:00');
|
||||
const dayName = dayNames[dateObj.getDay()];
|
||||
const isToday = date === today;
|
||||
|
||||
const [year, month, day] = date.split('-');
|
||||
const displayDate = `${parseInt(month)}월 ${parseInt(day)}일`;
|
||||
|
||||
return `
|
||||
<div class="tbm-date-group" data-date="${date}">
|
||||
<div class="tbm-date-header ${isToday ? 'today' : ''}" onclick="toggleDateGroup('${date}')">
|
||||
<span class="tbm-date-toggle">▼</span>
|
||||
<span class="tbm-date-title">${displayDate}</span>
|
||||
<span class="tbm-date-day">${dayName}요일</span>
|
||||
${isToday ? '<span class="tbm-today-badge">오늘</span>' : ''}
|
||||
<span class="tbm-date-count">${sessions.length}건</span>
|
||||
</div>
|
||||
<div class="tbm-date-content">
|
||||
<div class="tbm-date-grid">
|
||||
${sessions.map(session => this.createSessionCard(session)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 뷰 모드 표시 업데이트
|
||||
*/
|
||||
updateViewModeIndicator() {
|
||||
const indicator = document.getElementById('viewModeIndicator');
|
||||
const text = document.getElementById('viewModeText');
|
||||
|
||||
if (indicator && text) {
|
||||
if (this.state.isAdminUser()) {
|
||||
indicator.style.display = 'none';
|
||||
} else {
|
||||
indicator.style.display = 'inline-flex';
|
||||
text.textContent = '내 TBM';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 카드 생성
|
||||
*/
|
||||
createSessionCard(session) {
|
||||
const statusBadge = this.utils.getStatusBadge(session.status);
|
||||
|
||||
const leaderName = session.leader_name || session.created_by_name || '작업 책임자';
|
||||
const leaderRole = session.leader_name
|
||||
? (session.leader_job_type || '작업자')
|
||||
: '관리자';
|
||||
|
||||
return `
|
||||
<div class="tbm-session-card" onclick="viewTbmSession(${session.session_id})">
|
||||
<div class="tbm-card-header">
|
||||
<div class="tbm-card-header-top">
|
||||
<div>
|
||||
<h3 class="tbm-card-leader">
|
||||
${leaderName}
|
||||
<span class="tbm-card-leader-role">${leaderRole}</span>
|
||||
</h3>
|
||||
</div>
|
||||
${statusBadge}
|
||||
</div>
|
||||
<div class="tbm-card-date">
|
||||
<span>📅</span>
|
||||
${this.utils.formatDate(session.session_date)} ${session.start_time ? '| ' + session.start_time : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-card-body">
|
||||
<div class="tbm-card-info-grid">
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">프로젝트</span>
|
||||
<span class="tbm-card-info-value">${session.project_name || '-'}</span>
|
||||
</div>
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">공정</span>
|
||||
<span class="tbm-card-info-value">${session.work_type_name || '-'}</span>
|
||||
</div>
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">작업장</span>
|
||||
<span class="tbm-card-info-value">${session.work_location || '-'}</span>
|
||||
</div>
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">팀원</span>
|
||||
<span class="tbm-card-info-value">${session.team_member_count || 0}명</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${session.status === 'draft' ? `
|
||||
<div class="tbm-card-footer">
|
||||
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})">
|
||||
👥 팀 구성
|
||||
</button>
|
||||
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})">
|
||||
✓ 안전 체크
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그
|
||||
*/
|
||||
debug() {
|
||||
console.log('[TbmController] 상태 디버그:');
|
||||
this.state.debug();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmController = new TbmController();
|
||||
|
||||
// 하위 호환성: 기존 전역 함수들
|
||||
window.switchTbmTab = (tabName) => window.TbmController.switchTbmTab(tabName);
|
||||
window.displayTodayTbmSessions = () => window.TbmController.displayTodayTbmSessions();
|
||||
window.displayTbmGroupedByDate = () => window.TbmController.displayTbmGroupedByDate();
|
||||
window.displayTbmSessions = () => window.TbmController.displayTbmGroupedByDate();
|
||||
window.createSessionCard = (session) => window.TbmController.createSessionCard(session);
|
||||
window.updateViewModeIndicator = () => window.TbmController.updateViewModeIndicator();
|
||||
|
||||
// 날짜 그룹 토글
|
||||
window.toggleDateGroup = function(date) {
|
||||
const group = document.querySelector(`.tbm-date-group[data-date="${date}"]`);
|
||||
if (group) {
|
||||
group.classList.toggle('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
// DOMContentLoaded 이벤트에서 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => {
|
||||
window.TbmController.init();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
console.log('[Module] tbm/index.js 로드 완료');
|
||||
392
deploy/tkfb-package/web-ui/js/tbm/state.js
Normal file
392
deploy/tkfb-package/web-ui/js/tbm/state.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* TBM - State Manager
|
||||
* TBM 페이지의 전역 상태 관리
|
||||
*/
|
||||
|
||||
class TbmState {
|
||||
constructor() {
|
||||
// 세션 데이터
|
||||
this.allSessions = [];
|
||||
this.todaySessions = [];
|
||||
this.dateGroupedSessions = {};
|
||||
this.allLoadedSessions = [];
|
||||
this.loadedDaysCount = 7;
|
||||
|
||||
// 마스터 데이터
|
||||
this.allWorkers = [];
|
||||
this.allProjects = [];
|
||||
this.allWorkTypes = [];
|
||||
this.allTasks = [];
|
||||
this.allSafetyChecks = [];
|
||||
this.allWorkplaces = [];
|
||||
this.allWorkplaceCategories = [];
|
||||
|
||||
// 현재 상태
|
||||
this.currentUser = null;
|
||||
this.currentSessionId = null;
|
||||
this.currentTab = 'tbm-input';
|
||||
|
||||
// 작업자 관련
|
||||
this.selectedWorkers = new Set();
|
||||
this.workerTaskList = [];
|
||||
this.selectedWorkersInModal = new Set();
|
||||
this.currentEditingTaskLine = null;
|
||||
|
||||
// 작업장 선택 관련
|
||||
this.selectedCategory = null;
|
||||
this.selectedWorkplace = null;
|
||||
this.selectedCategoryName = '';
|
||||
this.selectedWorkplaceName = '';
|
||||
|
||||
// 일괄 설정 관련
|
||||
this.isBulkMode = false;
|
||||
this.bulkSelectedWorkers = new Set();
|
||||
|
||||
// 지도 관련
|
||||
this.mapCanvas = null;
|
||||
this.mapCtx = null;
|
||||
this.mapImage = null;
|
||||
this.mapRegions = [];
|
||||
|
||||
// 리스너
|
||||
this.listeners = new Map();
|
||||
|
||||
console.log('[TbmState] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트
|
||||
*/
|
||||
update(key, value) {
|
||||
const prevValue = this[key];
|
||||
this[key] = value;
|
||||
this.notifyListeners(key, value, prevValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너 등록
|
||||
*/
|
||||
subscribe(key, callback) {
|
||||
if (!this.listeners.has(key)) {
|
||||
this.listeners.set(key, []);
|
||||
}
|
||||
this.listeners.get(key).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너 알림
|
||||
*/
|
||||
notifyListeners(key, newValue, prevValue) {
|
||||
const keyListeners = this.listeners.get(key) || [];
|
||||
keyListeners.forEach(callback => {
|
||||
try {
|
||||
callback(newValue, prevValue);
|
||||
} catch (error) {
|
||||
console.error(`[TbmState] 리스너 오류 (${key}):`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자 정보 가져오기
|
||||
*/
|
||||
getUser() {
|
||||
if (!this.currentUser) {
|
||||
const userInfo = localStorage.getItem('user');
|
||||
this.currentUser = userInfo ? JSON.parse(userInfo) : null;
|
||||
}
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin 여부 확인
|
||||
*/
|
||||
isAdminUser() {
|
||||
const user = this.getUser();
|
||||
if (!user) return false;
|
||||
return user.role === 'Admin' || user.role === 'System Admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 변경
|
||||
*/
|
||||
setCurrentTab(tab) {
|
||||
const prevTab = this.currentTab;
|
||||
this.currentTab = tab;
|
||||
this.notifyListeners('currentTab', tab, prevTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 목록에 추가
|
||||
*/
|
||||
addWorkerToList(worker) {
|
||||
this.workerTaskList.push({
|
||||
worker_id: worker.worker_id,
|
||||
worker_name: worker.worker_name,
|
||||
job_type: worker.job_type,
|
||||
tasks: [this.createEmptyTaskLine()]
|
||||
});
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 작업 라인 생성
|
||||
*/
|
||||
createEmptyTaskLine() {
|
||||
return {
|
||||
task_line_id: this.generateUUID(),
|
||||
project_id: null,
|
||||
work_type_id: null,
|
||||
task_id: null,
|
||||
workplace_category_id: null,
|
||||
workplace_id: null,
|
||||
workplace_category_name: '',
|
||||
workplace_name: '',
|
||||
work_detail: null,
|
||||
is_present: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자에 작업 라인 추가
|
||||
*/
|
||||
addTaskLineToWorker(workerIndex) {
|
||||
if (this.workerTaskList[workerIndex]) {
|
||||
this.workerTaskList[workerIndex].tasks.push(this.createEmptyTaskLine());
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 라인 제거
|
||||
*/
|
||||
removeTaskLine(workerIndex, taskIndex) {
|
||||
if (this.workerTaskList[workerIndex]?.tasks) {
|
||||
this.workerTaskList[workerIndex].tasks.splice(taskIndex, 1);
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 제거
|
||||
*/
|
||||
removeWorkerFromList(workerIndex) {
|
||||
const removed = this.workerTaskList.splice(workerIndex, 1);
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
return removed[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 선택 초기화
|
||||
*/
|
||||
resetWorkplaceSelection() {
|
||||
this.selectedCategory = null;
|
||||
this.selectedWorkplace = null;
|
||||
this.selectedCategoryName = '';
|
||||
this.selectedWorkplaceName = '';
|
||||
this.mapCanvas = null;
|
||||
this.mapCtx = null;
|
||||
this.mapImage = null;
|
||||
this.mapRegions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 설정 초기화
|
||||
*/
|
||||
resetBulkSettings() {
|
||||
this.isBulkMode = false;
|
||||
this.bulkSelectedWorkers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜별 세션 그룹화
|
||||
*/
|
||||
groupSessionsByDate(sessions) {
|
||||
this.dateGroupedSessions = {};
|
||||
this.allLoadedSessions = [];
|
||||
|
||||
sessions.forEach(session => {
|
||||
const date = this.formatDate(session.session_date);
|
||||
if (!this.dateGroupedSessions[date]) {
|
||||
this.dateGroupedSessions[date] = [];
|
||||
}
|
||||
this.dateGroupedSessions[date].push(session);
|
||||
this.allLoadedSessions.push(session);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 포맷팅
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||
return dateString;
|
||||
}
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID 생성
|
||||
*/
|
||||
generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화
|
||||
*/
|
||||
reset() {
|
||||
this.workerTaskList = [];
|
||||
this.selectedWorkers.clear();
|
||||
this.selectedWorkersInModal.clear();
|
||||
this.currentEditingTaskLine = null;
|
||||
this.resetWorkplaceSelection();
|
||||
this.resetBulkSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그 출력
|
||||
*/
|
||||
debug() {
|
||||
console.log('[TbmState] 현재 상태:', {
|
||||
allSessions: this.allSessions.length,
|
||||
todaySessions: this.todaySessions.length,
|
||||
allWorkers: this.allWorkers.length,
|
||||
allProjects: this.allProjects.length,
|
||||
workerTaskList: this.workerTaskList.length,
|
||||
currentTab: this.currentTab
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmState = new TbmState();
|
||||
|
||||
// 하위 호환성을 위한 전역 변수 프록시
|
||||
const tbmStateProxy = window.TbmState;
|
||||
|
||||
Object.defineProperties(window, {
|
||||
allSessions: {
|
||||
get: () => tbmStateProxy.allSessions,
|
||||
set: (v) => { tbmStateProxy.allSessions = v; }
|
||||
},
|
||||
todaySessions: {
|
||||
get: () => tbmStateProxy.todaySessions,
|
||||
set: (v) => { tbmStateProxy.todaySessions = v; }
|
||||
},
|
||||
allWorkers: {
|
||||
get: () => tbmStateProxy.allWorkers,
|
||||
set: (v) => { tbmStateProxy.allWorkers = v; }
|
||||
},
|
||||
allProjects: {
|
||||
get: () => tbmStateProxy.allProjects,
|
||||
set: (v) => { tbmStateProxy.allProjects = v; }
|
||||
},
|
||||
allWorkTypes: {
|
||||
get: () => tbmStateProxy.allWorkTypes,
|
||||
set: (v) => { tbmStateProxy.allWorkTypes = v; }
|
||||
},
|
||||
allTasks: {
|
||||
get: () => tbmStateProxy.allTasks,
|
||||
set: (v) => { tbmStateProxy.allTasks = v; }
|
||||
},
|
||||
allSafetyChecks: {
|
||||
get: () => tbmStateProxy.allSafetyChecks,
|
||||
set: (v) => { tbmStateProxy.allSafetyChecks = v; }
|
||||
},
|
||||
allWorkplaces: {
|
||||
get: () => tbmStateProxy.allWorkplaces,
|
||||
set: (v) => { tbmStateProxy.allWorkplaces = v; }
|
||||
},
|
||||
allWorkplaceCategories: {
|
||||
get: () => tbmStateProxy.allWorkplaceCategories,
|
||||
set: (v) => { tbmStateProxy.allWorkplaceCategories = v; }
|
||||
},
|
||||
currentUser: {
|
||||
get: () => tbmStateProxy.currentUser,
|
||||
set: (v) => { tbmStateProxy.currentUser = v; }
|
||||
},
|
||||
currentSessionId: {
|
||||
get: () => tbmStateProxy.currentSessionId,
|
||||
set: (v) => { tbmStateProxy.currentSessionId = v; }
|
||||
},
|
||||
selectedWorkers: {
|
||||
get: () => tbmStateProxy.selectedWorkers,
|
||||
set: (v) => { tbmStateProxy.selectedWorkers = v; }
|
||||
},
|
||||
workerTaskList: {
|
||||
get: () => tbmStateProxy.workerTaskList,
|
||||
set: (v) => { tbmStateProxy.workerTaskList = v; }
|
||||
},
|
||||
selectedWorkersInModal: {
|
||||
get: () => tbmStateProxy.selectedWorkersInModal,
|
||||
set: (v) => { tbmStateProxy.selectedWorkersInModal = v; }
|
||||
},
|
||||
currentEditingTaskLine: {
|
||||
get: () => tbmStateProxy.currentEditingTaskLine,
|
||||
set: (v) => { tbmStateProxy.currentEditingTaskLine = v; }
|
||||
},
|
||||
selectedCategory: {
|
||||
get: () => tbmStateProxy.selectedCategory,
|
||||
set: (v) => { tbmStateProxy.selectedCategory = v; }
|
||||
},
|
||||
selectedWorkplace: {
|
||||
get: () => tbmStateProxy.selectedWorkplace,
|
||||
set: (v) => { tbmStateProxy.selectedWorkplace = v; }
|
||||
},
|
||||
selectedCategoryName: {
|
||||
get: () => tbmStateProxy.selectedCategoryName,
|
||||
set: (v) => { tbmStateProxy.selectedCategoryName = v; }
|
||||
},
|
||||
selectedWorkplaceName: {
|
||||
get: () => tbmStateProxy.selectedWorkplaceName,
|
||||
set: (v) => { tbmStateProxy.selectedWorkplaceName = v; }
|
||||
},
|
||||
isBulkMode: {
|
||||
get: () => tbmStateProxy.isBulkMode,
|
||||
set: (v) => { tbmStateProxy.isBulkMode = v; }
|
||||
},
|
||||
bulkSelectedWorkers: {
|
||||
get: () => tbmStateProxy.bulkSelectedWorkers,
|
||||
set: (v) => { tbmStateProxy.bulkSelectedWorkers = v; }
|
||||
},
|
||||
dateGroupedSessions: {
|
||||
get: () => tbmStateProxy.dateGroupedSessions,
|
||||
set: (v) => { tbmStateProxy.dateGroupedSessions = v; }
|
||||
},
|
||||
allLoadedSessions: {
|
||||
get: () => tbmStateProxy.allLoadedSessions,
|
||||
set: (v) => { tbmStateProxy.allLoadedSessions = v; }
|
||||
},
|
||||
loadedDaysCount: {
|
||||
get: () => tbmStateProxy.loadedDaysCount,
|
||||
set: (v) => { tbmStateProxy.loadedDaysCount = v; }
|
||||
},
|
||||
mapRegions: {
|
||||
get: () => tbmStateProxy.mapRegions,
|
||||
set: (v) => { tbmStateProxy.mapRegions = v; }
|
||||
},
|
||||
mapCanvas: {
|
||||
get: () => tbmStateProxy.mapCanvas,
|
||||
set: (v) => { tbmStateProxy.mapCanvas = v; }
|
||||
},
|
||||
mapCtx: {
|
||||
get: () => tbmStateProxy.mapCtx,
|
||||
set: (v) => { tbmStateProxy.mapCtx = v; }
|
||||
},
|
||||
mapImage: {
|
||||
get: () => tbmStateProxy.mapImage,
|
||||
set: (v) => { tbmStateProxy.mapImage = v; }
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Module] tbm/state.js 로드 완료');
|
||||
253
deploy/tkfb-package/web-ui/js/tbm/utils.js
Normal file
253
deploy/tkfb-package/web-ui/js/tbm/utils.js
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* TBM - Utilities
|
||||
* TBM 관련 유틸리티 함수들
|
||||
*/
|
||||
|
||||
class TbmUtils {
|
||||
constructor() {
|
||||
console.log('[TbmUtils] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 서울 시간대(Asia/Seoul, UTC+9) 기준 오늘 날짜를 YYYY-MM-DD 형식으로 반환
|
||||
*/
|
||||
getTodayKST() {
|
||||
const now = new Date();
|
||||
const kstOffset = 9 * 60;
|
||||
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
|
||||
const kstTime = new Date(utc + (kstOffset * 60000));
|
||||
|
||||
const year = kstTime.getFullYear();
|
||||
const month = String(kstTime.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(kstTime.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO 날짜 문자열을 YYYY-MM-DD 형식으로 변환
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||
return dateString;
|
||||
}
|
||||
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 표시용 포맷 (MM월 DD일)
|
||||
*/
|
||||
formatDateDisplay(dateString) {
|
||||
if (!dateString) return '';
|
||||
const [year, month, day] = dateString.split('-');
|
||||
return `${parseInt(month)}월 ${parseInt(day)}일`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜를 연/월/일/요일 형식으로 포맷
|
||||
*/
|
||||
formatDateFull(dateString) {
|
||||
if (!dateString) return '';
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const [year, month, day] = dateString.split('-');
|
||||
const dateObj = new Date(dateString);
|
||||
const dayName = dayNames[dateObj.getDay()];
|
||||
return `${year}년 ${parseInt(month)}월 ${parseInt(day)}일 (${dayName})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일 반환
|
||||
*/
|
||||
getDayOfWeek(dateString) {
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const dateObj = new Date(dateString + 'T00:00:00');
|
||||
return dayNames[dateObj.getDay()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘인지 확인
|
||||
*/
|
||||
isToday(dateString) {
|
||||
const today = this.getTodayKST();
|
||||
return this.formatDate(dateString) === today;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 시간을 HH:MM 형식으로 반환
|
||||
*/
|
||||
getCurrentTime() {
|
||||
return new Date().toTimeString().slice(0, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID 생성
|
||||
*/
|
||||
generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 이스케이프
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 조건명 반환
|
||||
*/
|
||||
getWeatherConditionName(code) {
|
||||
const names = {
|
||||
clear: '맑음',
|
||||
rain: '비',
|
||||
snow: '눈',
|
||||
heat: '폭염',
|
||||
cold: '한파',
|
||||
wind: '강풍',
|
||||
fog: '안개',
|
||||
dust: '미세먼지'
|
||||
};
|
||||
return names[code] || code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 아이콘 반환
|
||||
*/
|
||||
getWeatherIcon(code) {
|
||||
const icons = {
|
||||
clear: '☀️',
|
||||
rain: '🌧️',
|
||||
snow: '❄️',
|
||||
heat: '🔥',
|
||||
cold: '🥶',
|
||||
wind: '💨',
|
||||
fog: '🌫️',
|
||||
dust: '😷'
|
||||
};
|
||||
return icons[code] || '🌤️';
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리명 반환
|
||||
*/
|
||||
getCategoryName(category) {
|
||||
const names = {
|
||||
'PPE': '개인 보호 장비',
|
||||
'EQUIPMENT': '장비 점검',
|
||||
'ENVIRONMENT': '작업 환경',
|
||||
'EMERGENCY': '비상 대응',
|
||||
'WEATHER': '날씨',
|
||||
'TASK': '작업'
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 배지 HTML 반환
|
||||
*/
|
||||
getStatusBadge(status) {
|
||||
const badges = {
|
||||
'draft': '<span class="tbm-card-status draft">진행중</span>',
|
||||
'completed': '<span class="tbm-card-status completed">완료</span>',
|
||||
'cancelled': '<span class="tbm-card-status cancelled">취소</span>'
|
||||
};
|
||||
return badges[status] || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmUtils = new TbmUtils();
|
||||
|
||||
// 하위 호환성: 기존 함수들
|
||||
window.getTodayKST = () => window.TbmUtils.getTodayKST();
|
||||
window.formatDate = (dateString) => window.TbmUtils.formatDate(dateString);
|
||||
|
||||
// 토스트 알림
|
||||
window.showToast = function(message, type = 'info', duration = 3000) {
|
||||
const container = document.getElementById('toastContainer');
|
||||
if (!container) {
|
||||
console.log(`[Toast] ${type}: ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
const iconMap = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${iconMap[type] || 'ℹ️'}</div>
|
||||
<div class="toast-message">${message}</div>
|
||||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
|
||||
toast.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
margin-bottom: 0.75rem;
|
||||
min-width: 300px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.style.animation = 'slideOut 0.3s ease-out';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// 카테고리별 그룹화
|
||||
window.groupChecksByCategory = function(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const category = check.check_category || 'OTHER';
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category].push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// 작업별 그룹화
|
||||
window.groupChecksByTask = function(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const taskId = check.task_id || 0;
|
||||
const taskName = check.task_name || '기타 작업';
|
||||
if (!acc[taskId]) acc[taskId] = { name: taskName, items: [] };
|
||||
acc[taskId].items.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// Admin 사용자 확인
|
||||
window.isAdminUser = function() {
|
||||
return window.TbmState?.isAdminUser() || false;
|
||||
};
|
||||
|
||||
console.log('[Module] tbm/utils.js 로드 완료');
|
||||
Reference in New Issue
Block a user