diff --git a/system1-factory/web/js/common/base-state.js b/system1-factory/web/js/common/base-state.js new file mode 100644 index 0000000..994f8be --- /dev/null +++ b/system1-factory/web/js/common/base-state.js @@ -0,0 +1,56 @@ +/** + * BaseState - 상태 관리 베이스 클래스 + * TbmState / DailyWorkReportState 공통 패턴 + */ + +class BaseState { + constructor() { + this.listeners = new Map(); + } + + /** + * 상태 업데이트 + 리스너 알림 + */ + 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(`[${this.constructor.name}] 리스너 오류 (${key}):`, error); + } + }); + } + + /** + * 현재 사용자 정보 (localStorage) + */ + getUser() { + const userInfo = localStorage.getItem('sso_user'); + return userInfo ? JSON.parse(userInfo) : null; + } +} + +// 전역 노출 +window.BaseState = BaseState; + +console.log('[Module] common/base-state.js 로드 완료'); diff --git a/system1-factory/web/js/common/utils.js b/system1-factory/web/js/common/utils.js new file mode 100644 index 0000000..66250be --- /dev/null +++ b/system1-factory/web/js/common/utils.js @@ -0,0 +1,144 @@ +/** + * Common Utilities + * TBM/작업보고 공통 유틸리티 함수 + */ + +class CommonUtils { + /** + * 서울 시간대(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}`; + } + + /** + * 날짜를 YYYY-MM-DD 형식으로 변환 (문자열 또는 Date 객체) + */ + formatDate(date) { + if (!date) return ''; + + // 이미 YYYY-MM-DD 형식이면 그대로 반환 + if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) { + return date; + } + + const dateObj = date instanceof Date ? date : new Date(date); + const year = dateObj.getFullYear(); + const month = String(dateObj.getMonth() + 1).padStart(2, '0'); + const day = String(dateObj.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; + } + + /** + * 요일 반환 (일/월/화/수/목/금/토) + */ + getDayOfWeek(date) { + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + const dateObj = date instanceof Date ? date : new Date(date instanceof String || typeof date === 'string' ? date + 'T00:00:00' : date); + return dayNames[dateObj.getDay()]; + } + + /** + * 오늘인지 확인 + */ + isToday(date) { + return this.formatDate(date) === this.getTodayKST(); + } + + /** + * UUID v4 생성 + */ + 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; + } + + /** + * 디바운스 + */ + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + /** + * 쓰로틀 + */ + throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + + /** + * 객체 깊은 복사 + */ + deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + + /** + * 빈 값 확인 + */ + isEmpty(value) { + if (value === null || value === undefined) return true; + if (typeof value === 'string') return value.trim() === ''; + if (Array.isArray(value)) return value.length === 0; + if (typeof value === 'object') return Object.keys(value).length === 0; + return false; + } + + /** + * 배열 그룹화 + */ + groupBy(array, key) { + return array.reduce((result, item) => { + const groupKey = typeof key === 'function' ? key(item) : item[key]; + if (!result[groupKey]) { + result[groupKey] = []; + } + result[groupKey].push(item); + return result; + }, {}); + } +} + +// 전역 인스턴스 생성 +window.CommonUtils = new CommonUtils(); + +console.log('[Module] common/utils.js 로드 완료'); diff --git a/system1-factory/web/js/tbm.js b/system1-factory/web/js/tbm.js index c67a70e..e6399ab 100644 --- a/system1-factory/web/js/tbm.js +++ b/system1-factory/web/js/tbm.js @@ -1,36 +1,15 @@ // tbm.js - TBM 관리 페이지 JavaScript +// 전역 변수: TbmState 프록시 사용 (state.js에서 정의) +// allSessions, todaySessions, allWorkers, allProjects, allWorkTypes, allTasks, +// allSafetyChecks, allWorkplaces, allWorkplaceCategories, currentUser, +// currentSessionId, selectedWorkers, workerTaskList, selectedWorkersInModal, +// currentEditingTaskLine, selectedCategory, selectedWorkplace, selectedCategoryName, +// selectedWorkplaceName, isBulkMode, bulkSelectedWorkers, loadedDaysCount, +// dateGroupedSessions, allLoadedSessions → window 프록시로 접근 -// 전역 변수 -let allSessions = []; -let todaySessions = []; -let allWorkers = []; -let allProjects = []; -let allWorkTypes = []; -let allTasks = []; -let allSafetyChecks = []; -let allWorkplaces = []; -let allWorkplaceCategories = []; -let currentUser = null; -let currentSessionId = null; -let selectedWorkers = new Set(); +// UI 전용 변수 (프록시 없음) let currentTab = 'tbm-input'; -// 새로운 TBM 입력 방식 관련 변수 -let workerTaskList = []; // [{worker_id, worker_name, job_type, tasks: [{task_line_id, project_id, ...}]}] -let selectedWorkersInModal = new Set(); // 모달에서 선택된 작업자 ID 세트 -let currentEditingTaskLine = null; // 현재 편집 중인 작업 라인 정보 {workerIndex, taskIndex} -let selectedCategory = null; -let selectedWorkplace = null; -let selectedCategoryName = ''; -let selectedWorkplaceName = ''; -let isBulkMode = false; // 일괄 설정 모드인지 여부 -let bulkSelectedWorkers = new Set(); // 일괄 설정에서 선택된 작업자 인덱스 - -// TBM 관리 탭용 변수 -let loadedDaysCount = 7; // 처음에 로드할 일수 -let dateGroupedSessions = {}; // 날짜별로 그룹화된 세션 -let allLoadedSessions = []; // 전체 로드된 세션 - // 모달 스크롤 잠금 let scrollLockY = 0; let scrollLockCount = 0; @@ -56,46 +35,10 @@ function unlockBodyScroll() { document.body.classList.remove('tbm-modal-open'); } -// ==================== 유틸리티 함수 ==================== - -/** - * 서울 시간대(Asia/Seoul, UTC+9) 기준 오늘 날짜를 YYYY-MM-DD 형식으로 반환 - */ -function getTodayKST() { - const now = new Date(); - // 한국 시간대로 변환 (UTC+9) - const kstOffset = 9 * 60; // 9시간을 분 단위로 - const utc = now.getTime() + (now.getTimezoneOffset() * 60000); // UTC 시간 - const kstTime = new Date(utc + (kstOffset * 60000)); // KST 시간 - - 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 형식으로 변환 - * @param {string} dateString - ISO 형식 날짜 문자열 또는 YYYY-MM-DD 형식 - * @returns {string} YYYY-MM-DD 형식 날짜 - */ -function formatDate(dateString) { - if (!dateString) return ''; - - // 이미 YYYY-MM-DD 형식이면 그대로 반환 - if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { - return dateString; - } - - // ISO 형식 또는 다른 형식이면 변환 - 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}`; -} +// ==================== 유틸리티 함수 (CommonUtils 위임) ==================== +// getTodayKST, formatDate → window.CommonUtils 사용 (common/utils.js) +function getTodayKST() { return window.CommonUtils.getTodayKST(); } +function formatDate(d) { return window.CommonUtils.formatDate(d); } // ==================== 페이지 초기화 ==================== @@ -135,72 +78,12 @@ function setupEventListeners() { // 날짜 선택기 제거됨 - 날짜별 그룹 뷰 사용 } -// 초기 데이터 로드 +// 초기 데이터 로드 → TbmAPI 위임 async function loadInitialData() { - try { - // 현재 로그인한 사용자 정보 가져오기 - const userInfo = JSON.parse(localStorage.getItem('sso_user') || '{}'); - currentUser = userInfo; - console.log('👤 로그인 사용자:', currentUser, 'worker_id:', currentUser?.worker_id); - - // 작업자 목록 로드 (생산팀 소속만) - const workersResponse = await window.apiCall('/workers?limit=1000&department_id=1'); - if (workersResponse) { - allWorkers = Array.isArray(workersResponse) ? workersResponse : (workersResponse.data || []); - // 활성 상태인 작업자만 필터링 - allWorkers = allWorkers.filter(w => w.status === 'active' && w.employment_status === 'employed'); - console.log('✅ 작업자 목록 로드:', allWorkers.length + '명'); - } - - // 프로젝트 목록 로드 (활성 프로젝트만) - const projectsResponse = await window.apiCall('/projects?is_active=1'); - if (projectsResponse) { - const projects = Array.isArray(projectsResponse) ? projectsResponse : (projectsResponse.data || []); - // 활성 프로젝트만 필터링 (is_active가 1 또는 true인 경우) - allProjects = projects.filter(p => p.is_active === 1 || p.is_active === true || p.is_active === '1'); - console.log('✅ 프로젝트 목록 로드:', allProjects.length + '개 (활성)'); - populateProjectSelect(); - } - - // 안전 체크리스트 로드 - const safetyResponse = await window.apiCall('/tbm/safety-checks'); - if (safetyResponse && safetyResponse.success) { - allSafetyChecks = safetyResponse.data; - console.log('✅ 안전 체크리스트 로드:', allSafetyChecks.length + '개'); - } - - // 공정(Work Types) 목록 로드 - const workTypesResponse = await window.apiCall('/daily-work-reports/work-types'); - if (workTypesResponse && workTypesResponse.success) { - allWorkTypes = workTypesResponse.data || []; - console.log('✅ 공정 목록 로드:', allWorkTypes.length + '개'); - } - - // 작업(Tasks) 목록 로드 - const tasksResponse = await window.apiCall('/tasks/active/list'); - if (tasksResponse && tasksResponse.success) { - allTasks = tasksResponse.data || []; - console.log('✅ 작업 목록 로드:', allTasks.length + '개'); - } - - // 작업장 목록 로드 - const workplacesResponse = await window.apiCall('/workplaces?is_active=true'); - if (workplacesResponse && workplacesResponse.success) { - allWorkplaces = workplacesResponse.data || []; - console.log('✅ 작업장 목록 로드:', allWorkplaces.length + '개'); - } - - // 작업장 카테고리 로드 - const categoriesResponse = await window.apiCall('/workplaces/categories/active/list'); - if (categoriesResponse && categoriesResponse.success) { - allWorkplaceCategories = categoriesResponse.data || []; - console.log('✅ 작업장 카테고리 로드:', allWorkplaceCategories.length + '개'); - } - - } catch (error) { - console.error('❌ 초기 데이터 로드 오류:', error); - showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error'); - } + await window.TbmAPI.loadInitialData(); + // TbmAPI가 TbmState에 데이터를 설정 → 프록시를 통해 전역 변수로 접근 가능 + // UI 드롭다운 채우기 + populateProjectSelect(); } // ==================== 탭 전환 ==================== @@ -235,26 +118,10 @@ window.switchTbmTab = switchTbmTab; // ==================== TBM 입력 탭 ==================== -// 오늘의 TBM만 로드 (TBM 입력 탭용) +// 오늘의 TBM만 로드 → TbmAPI 위임 async function loadTodayOnlyTbm() { - const today = getTodayKST(); - - try { - const response = await window.apiCall(`/tbm/sessions/date/${today}`); - - if (response && response.success) { - todaySessions = response.data || []; - displayTodayTbmSessions(); - } else { - todaySessions = []; - displayTodayTbmSessions(); - } - } catch (error) { - console.error('❌ 오늘 TBM 조회 오류:', error); - showToast('오늘 TBM을 불러오는 중 오류가 발생했습니다.', 'error'); - todaySessions = []; - displayTodayTbmSessions(); - } + await window.TbmAPI.loadTodayOnlyTbm(); + displayTodayTbmSessions(); } window.loadTodayOnlyTbm = loadTodayOnlyTbm; @@ -289,87 +156,20 @@ function displayTodayTbmSessions() { // ==================== TBM 관리 탭 ==================== -// 오늘 TBM 로드 (TBM 관리 탭용) - 레거시 호환 -async function loadTodayTbm() { - await loadRecentTbmGroupedByDate(); -} -window.loadTodayTbm = loadTodayTbm; - -// 전체 TBM 로드 - 레거시 호환 -async function loadAllTbm() { - loadedDaysCount = 30; // 30일치 로드 - await loadRecentTbmGroupedByDate(); -} -window.loadAllTbm = loadAllTbm; +// 레거시 호환 → api.js의 window alias 사용 // ==================== 날짜별 그룹 TBM 로드 (새 기능) ==================== -/** - * 사용자가 Admin인지 확인 - */ -function isAdminUser() { - if (!currentUser) return false; - return currentUser.role === 'Admin' || currentUser.role === 'System Admin'; -} +function isAdminUser() { return window.TbmState.isAdminUser(); } /** - * 최근 TBM을 날짜별로 그룹화하여 로드 + * 최근 TBM을 날짜별로 그룹화하여 로드 → TbmAPI 위임 */ async function loadRecentTbmGroupedByDate() { - try { - const today = new Date(); - const dates = []; - - // 최근 N일의 날짜 생성 - for (let i = 0; i < loadedDaysCount; i++) { - const date = new Date(today); - date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; - dates.push(dateStr); - } - - // 각 날짜의 TBM 로드 - dateGroupedSessions = {}; - 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 (!isAdminUser()) { - const userId = currentUser?.user_id; - const workerId = currentUser?.worker_id; - sessions = sessions.filter(s => { - return s.created_by === userId || - s.leader_id === workerId || - s.created_by_name === currentUser?.name; - }); - } - - if (sessions.length > 0) { - dateGroupedSessions[date] = sessions; - allLoadedSessions = allLoadedSessions.concat(sessions); - } - } - }); - - // 날짜별 그룹 표시 - displayTbmGroupedByDate(); - - // 뷰 모드 표시 - updateViewModeIndicator(); - - } catch (error) { - console.error('❌ TBM 날짜별 로드 오류:', error); - showToast('TBM을 불러오는 중 오류가 발생했습니다.', 'error'); - dateGroupedSessions = {}; - displayTbmGroupedByDate(); - } + await window.TbmAPI.loadRecentTbmGroupedByDate(); + // TbmState에 dateGroupedSessions, allLoadedSessions가 설정됨 + displayTbmGroupedByDate(); + updateViewModeIndicator(); } window.loadRecentTbmGroupedByDate = loadRecentTbmGroupedByDate; @@ -464,31 +264,12 @@ window.toggleDateGroup = toggleDateGroup; /** * 더 많은 날짜 로드 */ -async function loadMoreTbmDays() { - loadedDaysCount += 7; // 7일씩 추가 - await loadRecentTbmGroupedByDate(); - showToast(`최근 ${loadedDaysCount}일의 TBM을 로드했습니다.`, 'success'); -} -window.loadMoreTbmDays = loadMoreTbmDays; +// loadMoreTbmDays → api.js의 window alias 사용 -// 특정 날짜의 TBM 세션 목록 로드 +// 특정 날짜의 TBM 세션 목록 로드 → TbmAPI 위임 async function loadTbmSessionsByDate(date) { - try { - const response = await window.apiCall(`/tbm/sessions/date/${date}`); - - if (response && response.success) { - allSessions = response.data || []; - displayTbmSessions(); - } else { - allSessions = []; - displayTbmSessions(); - } - } catch (error) { - console.error('❌ TBM 세션 조회 오류:', error); - showToast('TBM 세션을 불러오는 중 오류가 발생했습니다.', 'error'); - allSessions = []; - displayTbmSessions(); - } + await window.TbmAPI.loadTbmSessionsByDate(date); + displayTbmSessions(); } // TBM 세션 목록 표시 (관리 탭용) - 레거시 호환 (날짜별 그룹 뷰 사용) @@ -659,15 +440,13 @@ async function renderNewTbmWorkerGrid() { if (!todayAssignmentsMap) { try { const today = getTodayKST(); - const res = await apiCall(`/tbm/sessions/date/${today}/assignments`); + const assignments = await window.TbmAPI.loadTodayAssignments(today); todayAssignmentsMap = {}; - if (res && res.success) { - res.data.forEach(a => { - if (a.sessions && a.sessions.length > 0) { - todayAssignmentsMap[a.worker_id] = a; - } - }); - } + assignments.forEach(a => { + if (a.sessions && a.sessions.length > 0) { + todayAssignmentsMap[a.worker_id] = a; + } + }); } catch(e) { console.error('배정 현황 로드 오류:', e); todayAssignmentsMap = {}; @@ -914,12 +693,8 @@ async function saveTbmSession() { } try { - await window.apiCall(`/tbm/sessions/${editingSessionId}/team/clear`, 'DELETE'); - const teamResponse = await window.apiCall( - `/tbm/sessions/${editingSessionId}/team/batch`, - 'POST', - { members } - ); + await window.TbmAPI.clearTeamMembers(editingSessionId); + const teamResponse = await window.TbmAPI.addTeamMembers(editingSessionId, members); if (teamResponse && teamResponse.success) { showToast(`TBM이 수정되었습니다 (작업자 ${workerTaskList.length}명)`, 'success'); @@ -969,17 +744,13 @@ async function saveTbmSession() { }); try { - const response = await window.apiCall('/tbm/sessions', 'POST', sessionData); + const response = await window.TbmAPI.createTbmSession(sessionData); if (response && response.success) { const createdSessionId = response.data.session_id; console.log('✅ TBM 세션 생성 완료:', createdSessionId); - const teamResponse = await window.apiCall( - `/tbm/sessions/${createdSessionId}/team/batch`, - 'POST', - { members } - ); + const teamResponse = await window.TbmAPI.addTeamMembers(createdSessionId, members); if (teamResponse && teamResponse.success) { showToast(`TBM이 생성되었습니다 (작업자 ${members.length}명)`, 'success'); @@ -1739,13 +1510,11 @@ async function loadWorkplacesByCategory(categoryId) { if (!workplaceList) return; try { - const response = await window.apiCall(`/workplaces?category_id=${categoryId}`); - if (!response || !response.success || !response.data || response.data.length === 0) { + const workplaces = await window.TbmAPI.loadWorkplacesByCategory(categoryId); + if (!workplaces || workplaces.length === 0) { workplaceList.innerHTML = '
등록된 작업장이 없습니다
'; return; } - - const workplaces = response.data; workplaceList.innerHTML = workplaces.map(workplace => ` + + `; } else { @@ -2906,27 +2647,20 @@ function confirmDeleteTbm(sessionId) { } window.confirmDeleteTbm = confirmDeleteTbm; -// TBM 세션 삭제 +// TBM 세션 삭제 → TbmAPI 위임 async function deleteTbmSession(sessionId) { try { - const response = await window.apiCall(`/tbm/sessions/${sessionId}`, 'DELETE'); - - if (response && response.success) { - showToast('TBM이 삭제되었습니다.', 'success'); - closeDetailModal(); - - // 목록 새로고침 - if (currentTab === 'tbm-input') { - await loadTodayOnlyTbm(); - } else { - await loadRecentTbmGroupedByDate(); - } + await window.TbmAPI.deleteSession(sessionId); + showToast('TBM이 삭제되었습니다.', 'success'); + closeDetailModal(); + if (currentTab === 'tbm-input') { + await loadTodayOnlyTbm(); } else { - showToast(response?.message || 'TBM 삭제에 실패했습니다.', 'error'); + await loadRecentTbmGroupedByDate(); } } catch (error) { console.error('❌ TBM 삭제 오류:', error); - showToast('TBM 삭제 중 오류가 발생했습니다.', 'error'); + showToast(error?.message || 'TBM 삭제 중 오류가 발생했습니다.', 'error'); } } window.deleteTbmSession = deleteTbmSession; @@ -2944,14 +2678,11 @@ async function openHandoverModal(sessionId) { // 세션 정보와 팀 구성 조회 try { - const [sessionRes, teamRes] = await Promise.all([ - window.apiCall(`/tbm/sessions/${sessionId}`), - window.apiCall(`/tbm/sessions/${sessionId}/team`) + const [session, team] = await Promise.all([ + window.TbmAPI.getSession(sessionId), + window.TbmAPI.getTeamMembers(sessionId) ]); - const session = sessionRes?.data; - const team = teamRes?.data || []; - if (!session) { showToast('세션 정보를 불러올 수 없습니다.', 'error'); return; @@ -3041,8 +2772,8 @@ async function saveHandover() { try { // 세션 정보 조회 (from_leader_id 가져오기) - const sessionRes = await window.apiCall(`/tbm/sessions/${sessionId}`); - const fromLeaderId = sessionRes?.data?.leader_id; + const sessionData = await window.TbmAPI.getSession(sessionId); + const fromLeaderId = sessionData?.leader_id; if (!fromLeaderId) { showToast('세션 정보를 찾을 수 없습니다.', 'error'); @@ -3060,7 +2791,7 @@ async function saveHandover() { worker_ids: workerIds }; - const response = await window.apiCall('/tbm/handovers', 'POST', handoverData); + const response = await window.TbmAPI.saveHandover(handoverData); if (response && response.success) { showToast('작업 인계가 요청되었습니다.', 'success'); @@ -3075,4 +2806,174 @@ async function saveHandover() { } window.saveHandover = saveHandover; +// ==================== 데스크탑 분할 기능 ==================== + +let splitModalSessionId = null; +let splitModalTeam = []; + +async function openDesktopSplitModal(sessionId) { + splitModalSessionId = sessionId; + try { + splitModalTeam = await window.TbmAPI.getTeamMembers(sessionId); + if (splitModalTeam.length === 0) { + showToast('팀원이 없습니다.', 'error'); return; + } + const modal = document.getElementById('splitModal'); + if (!modal) { showToast('분할 모달을 찾을 수 없습니다.', 'error'); return; } + + const list = document.getElementById('splitMemberList'); + list.innerHTML = splitModalTeam.map((m, i) => { + const hours = m.work_hours != null ? parseFloat(m.work_hours) : 8; + return ` +
+
+ ${escapeHtml(m.worker_name)} + (${hours}h) +
+
+ + +
+
`; + }).join(''); + + modal.style.display = 'flex'; + lockBodyScroll(); + } catch(e) { + console.error('분할 모달 오류:', e); + showToast('팀원 조회 오류', 'error'); + } +} +window.openDesktopSplitModal = openDesktopSplitModal; + +async function executeSplit(memberIdx) { + const m = splitModalTeam[memberIdx]; + const currentHours = m.work_hours != null ? parseFloat(m.work_hours) : 8; + const splitHours = parseFloat(document.getElementById(`split_hours_${memberIdx}`).value); + if (!splitHours || splitHours <= 0 || splitHours >= currentHours) { + showToast(`올바른 시간 입력 (0 < 시간 < ${currentHours})`, 'error'); return; + } + try { + await window.TbmAPI.updateTeamMember(splitModalSessionId, { + worker_id: m.worker_id, project_id: m.project_id, work_type_id: m.work_type_id, + task_id: m.task_id, workplace_category_id: m.workplace_category_id, workplace_id: m.workplace_id, + work_detail: m.work_detail, is_present: true, work_hours: splitHours + }); + await window.TbmAPI.splitAssignment(splitModalSessionId, { + worker_id: m.worker_id, work_hours: currentHours - splitHours, + project_id: m.project_id, work_type_id: m.work_type_id + }); + showToast(`${escapeHtml(m.worker_name)} 분할 완료: ${splitHours}h + ${currentHours - splitHours}h`, 'success'); + closeSplitModal(); + if (currentTab === 'tbm-input') await loadTodayOnlyTbm(); else await loadRecentTbmGroupedByDate(); + } catch(e) { + console.error('분할 오류:', e); + showToast('분할 처리 중 오류', 'error'); + } +} +window.executeSplit = executeSplit; + +function closeSplitModal() { + const modal = document.getElementById('splitModal'); + if (modal) modal.style.display = 'none'; + unlockBodyScroll(); +} +window.closeSplitModal = closeSplitModal; + +// ==================== 데스크탑 빼오기 기능 ==================== + +let pullModalSessionId = null; + +async function openDesktopPullModal(targetSessionId) { + pullModalSessionId = targetSessionId; + try { + const todayStr = getTodayKST(); + const sessions = await window.TbmAPI.fetchSessionsByDate(todayStr); + const otherSessions = sessions.filter(s => s.session_id !== targetSessionId && s.status === 'draft'); + + const modal = document.getElementById('pullModal'); + if (!modal) { showToast('빼오기 모달을 찾을 수 없습니다.', 'error'); return; } + + const list = document.getElementById('pullSessionList'); + if (otherSessions.length === 0) { + list.innerHTML = '
빼올 수 있는 다른 TBM이 없습니다.
'; + } else { + list.innerHTML = otherSessions.map(s => { + const leader = escapeHtml(s.leader_name || s.created_by_name || '미지정'); + const count = parseInt(s.team_member_count) || 0; + return ` +
+
+
${leader} (${count}명)
+ +
+ +
`; + }).join(''); + } + + modal.style.display = 'flex'; + lockBodyScroll(); + } catch(e) { + console.error('빼오기 모달 오류:', e); + showToast('빼오기 데이터 로드 오류', 'error'); + } +} +window.openDesktopPullModal = openDesktopPullModal; + +async function togglePullSessionMembers(sessionId, el) { + const container = document.getElementById(`pullMembers_${sessionId}`); + if (container.style.display !== 'none') { + container.style.display = 'none'; return; + } + try { + const members = await window.TbmAPI.getTeamMembers(sessionId); + container.innerHTML = members.map(m => { + const hours = m.work_hours != null ? parseFloat(m.work_hours) : 8; + return ` +
+ ${escapeHtml(m.worker_name)} (${hours}h) +
+ + +
+
`; + }).join('') || '
팀원 없음
'; + container.style.display = 'block'; + } catch(e) { + container.innerHTML = '
로드 오류
'; + container.style.display = 'block'; + } +} +window.togglePullSessionMembers = togglePullSessionMembers; + +async function executePull(sourceSessionId, workerId, workerName) { + const hoursInput = document.getElementById(`pull_h_${sourceSessionId}_${workerId}`); + const hours = parseFloat(hoursInput?.value); + if (!hours || hours <= 0) { showToast('시간을 입력하세요', 'error'); return; } + try { + const res = await window.TbmAPI.transfer({ + transfer_type: 'pull', + worker_id: workerId, + source_session_id: sourceSessionId, + dest_session_id: pullModalSessionId, + hours: hours + }); + showToast(`${workerName} ${hours}h 빼오기 완료` + (res.data?.warning ? ` (${res.data.warning})` : ''), 'success'); + closePullModal(); + if (currentTab === 'tbm-input') await loadTodayOnlyTbm(); else await loadRecentTbmGroupedByDate(); + } catch(e) { + console.error('빼오기 오류:', e); + showToast(e.message || '빼오기 처리 오류', 'error'); + } +} +window.executePull = executePull; + +function closePullModal() { + const modal = document.getElementById('pullModal'); + if (modal) modal.style.display = 'none'; + unlockBodyScroll(); +} +window.closePullModal = closePullModal; + // showToast → api-base.js 전역 사용 diff --git a/system1-factory/web/pages/work/tbm-mobile.html b/system1-factory/web/pages/work/tbm-mobile.html index 50f7049..56cb6ba 100644 --- a/system1-factory/web/pages/work/tbm-mobile.html +++ b/system1-factory/web/pages/work/tbm-mobile.html @@ -5,860 +5,10 @@ TBM | (주)테크니컬코리아 - - - @@ -976,6 +126,7 @@
+
@@ -1088,1211 +239,44 @@ - - - - + +
+
+
+
+

작업 인계

+ +
+

인계할 반장을 선택하세요

+
+
+
+ + +
+
+ +
+
+
+ + +
+
+ +
+ + + + + + + + + diff --git a/system1-factory/web/pages/work/tbm.html b/system1-factory/web/pages/work/tbm.html index 3c4afc1..4eda6a1 100644 --- a/system1-factory/web/pages/work/tbm.html +++ b/system1-factory/web/pages/work/tbm.html @@ -10,8 +10,8 @@ - - + + @@ -692,17 +692,55 @@ + + + + + +
+ + + + - - - + + + - +