fix: TBM 관리 탭 데스크탑-모바일 데이터 불일치 해결
- TBM 관리 탭의 비관리자 클라이언트 필터링 제거 (모바일과 동일하게 전체 표시) - tbm.js의 state.js 프록시 변수 중복 선언 제거 (mapRegions, mapCanvas 등) - 누락된 common/utils.js, common/base-state.js 추가 - 캐시 버스팅을 위한 버전 쿼리 스트링 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
56
system1-factory/web/js/common/base-state.js
Normal file
56
system1-factory/web/js/common/base-state.js
Normal file
@@ -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 로드 완료');
|
||||||
144
system1-factory/web/js/common/utils.js
Normal file
144
system1-factory/web/js/common/utils.js
Normal file
@@ -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 로드 완료');
|
||||||
@@ -1,36 +1,15 @@
|
|||||||
// tbm.js - TBM 관리 페이지 JavaScript
|
// 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 프록시로 접근
|
||||||
|
|
||||||
// 전역 변수
|
// UI 전용 변수 (프록시 없음)
|
||||||
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();
|
|
||||||
let currentTab = 'tbm-input';
|
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 scrollLockY = 0;
|
||||||
let scrollLockCount = 0;
|
let scrollLockCount = 0;
|
||||||
@@ -56,46 +35,10 @@ function unlockBodyScroll() {
|
|||||||
document.body.classList.remove('tbm-modal-open');
|
document.body.classList.remove('tbm-modal-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 유틸리티 함수 ====================
|
// ==================== 유틸리티 함수 (CommonUtils 위임) ====================
|
||||||
|
// getTodayKST, formatDate → window.CommonUtils 사용 (common/utils.js)
|
||||||
/**
|
function getTodayKST() { return window.CommonUtils.getTodayKST(); }
|
||||||
* 서울 시간대(Asia/Seoul, UTC+9) 기준 오늘 날짜를 YYYY-MM-DD 형식으로 반환
|
function formatDate(d) { return window.CommonUtils.formatDate(d); }
|
||||||
*/
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 페이지 초기화 ====================
|
// ==================== 페이지 초기화 ====================
|
||||||
|
|
||||||
@@ -135,72 +78,12 @@ function setupEventListeners() {
|
|||||||
// 날짜 선택기 제거됨 - 날짜별 그룹 뷰 사용
|
// 날짜 선택기 제거됨 - 날짜별 그룹 뷰 사용
|
||||||
}
|
}
|
||||||
|
|
||||||
// 초기 데이터 로드
|
// 초기 데이터 로드 → TbmAPI 위임
|
||||||
async function loadInitialData() {
|
async function loadInitialData() {
|
||||||
try {
|
await window.TbmAPI.loadInitialData();
|
||||||
// 현재 로그인한 사용자 정보 가져오기
|
// TbmAPI가 TbmState에 데이터를 설정 → 프록시를 통해 전역 변수로 접근 가능
|
||||||
const userInfo = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
// UI 드롭다운 채우기
|
||||||
currentUser = userInfo;
|
populateProjectSelect();
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 탭 전환 ====================
|
// ==================== 탭 전환 ====================
|
||||||
@@ -235,26 +118,10 @@ window.switchTbmTab = switchTbmTab;
|
|||||||
|
|
||||||
// ==================== TBM 입력 탭 ====================
|
// ==================== TBM 입력 탭 ====================
|
||||||
|
|
||||||
// 오늘의 TBM만 로드 (TBM 입력 탭용)
|
// 오늘의 TBM만 로드 → TbmAPI 위임
|
||||||
async function loadTodayOnlyTbm() {
|
async function loadTodayOnlyTbm() {
|
||||||
const today = getTodayKST();
|
await window.TbmAPI.loadTodayOnlyTbm();
|
||||||
|
displayTodayTbmSessions();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.loadTodayOnlyTbm = loadTodayOnlyTbm;
|
window.loadTodayOnlyTbm = loadTodayOnlyTbm;
|
||||||
|
|
||||||
@@ -289,87 +156,20 @@ function displayTodayTbmSessions() {
|
|||||||
|
|
||||||
// ==================== TBM 관리 탭 ====================
|
// ==================== TBM 관리 탭 ====================
|
||||||
|
|
||||||
// 오늘 TBM 로드 (TBM 관리 탭용) - 레거시 호환
|
// 레거시 호환 → api.js의 window alias 사용
|
||||||
async function loadTodayTbm() {
|
|
||||||
await loadRecentTbmGroupedByDate();
|
|
||||||
}
|
|
||||||
window.loadTodayTbm = loadTodayTbm;
|
|
||||||
|
|
||||||
// 전체 TBM 로드 - 레거시 호환
|
|
||||||
async function loadAllTbm() {
|
|
||||||
loadedDaysCount = 30; // 30일치 로드
|
|
||||||
await loadRecentTbmGroupedByDate();
|
|
||||||
}
|
|
||||||
window.loadAllTbm = loadAllTbm;
|
|
||||||
|
|
||||||
// ==================== 날짜별 그룹 TBM 로드 (새 기능) ====================
|
// ==================== 날짜별 그룹 TBM 로드 (새 기능) ====================
|
||||||
|
|
||||||
/**
|
function isAdminUser() { return window.TbmState.isAdminUser(); }
|
||||||
* 사용자가 Admin인지 확인
|
|
||||||
*/
|
|
||||||
function isAdminUser() {
|
|
||||||
if (!currentUser) return false;
|
|
||||||
return currentUser.role === 'Admin' || currentUser.role === 'System Admin';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 최근 TBM을 날짜별로 그룹화하여 로드
|
* 최근 TBM을 날짜별로 그룹화하여 로드 → TbmAPI 위임
|
||||||
*/
|
*/
|
||||||
async function loadRecentTbmGroupedByDate() {
|
async function loadRecentTbmGroupedByDate() {
|
||||||
try {
|
await window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||||
const today = new Date();
|
// TbmState에 dateGroupedSessions, allLoadedSessions가 설정됨
|
||||||
const dates = [];
|
displayTbmGroupedByDate();
|
||||||
|
updateViewModeIndicator();
|
||||||
// 최근 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.loadRecentTbmGroupedByDate = loadRecentTbmGroupedByDate;
|
window.loadRecentTbmGroupedByDate = loadRecentTbmGroupedByDate;
|
||||||
|
|
||||||
@@ -464,31 +264,12 @@ window.toggleDateGroup = toggleDateGroup;
|
|||||||
/**
|
/**
|
||||||
* 더 많은 날짜 로드
|
* 더 많은 날짜 로드
|
||||||
*/
|
*/
|
||||||
async function loadMoreTbmDays() {
|
// loadMoreTbmDays → api.js의 window alias 사용
|
||||||
loadedDaysCount += 7; // 7일씩 추가
|
|
||||||
await loadRecentTbmGroupedByDate();
|
|
||||||
showToast(`최근 ${loadedDaysCount}일의 TBM을 로드했습니다.`, 'success');
|
|
||||||
}
|
|
||||||
window.loadMoreTbmDays = loadMoreTbmDays;
|
|
||||||
|
|
||||||
// 특정 날짜의 TBM 세션 목록 로드
|
// 특정 날짜의 TBM 세션 목록 로드 → TbmAPI 위임
|
||||||
async function loadTbmSessionsByDate(date) {
|
async function loadTbmSessionsByDate(date) {
|
||||||
try {
|
await window.TbmAPI.loadTbmSessionsByDate(date);
|
||||||
const response = await window.apiCall(`/tbm/sessions/date/${date}`);
|
displayTbmSessions();
|
||||||
|
|
||||||
if (response && response.success) {
|
|
||||||
allSessions = response.data || [];
|
|
||||||
displayTbmSessions();
|
|
||||||
} else {
|
|
||||||
allSessions = [];
|
|
||||||
displayTbmSessions();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ TBM 세션 조회 오류:', error);
|
|
||||||
showToast('TBM 세션을 불러오는 중 오류가 발생했습니다.', 'error');
|
|
||||||
allSessions = [];
|
|
||||||
displayTbmSessions();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TBM 세션 목록 표시 (관리 탭용) - 레거시 호환 (날짜별 그룹 뷰 사용)
|
// TBM 세션 목록 표시 (관리 탭용) - 레거시 호환 (날짜별 그룹 뷰 사용)
|
||||||
@@ -659,15 +440,13 @@ async function renderNewTbmWorkerGrid() {
|
|||||||
if (!todayAssignmentsMap) {
|
if (!todayAssignmentsMap) {
|
||||||
try {
|
try {
|
||||||
const today = getTodayKST();
|
const today = getTodayKST();
|
||||||
const res = await apiCall(`/tbm/sessions/date/${today}/assignments`);
|
const assignments = await window.TbmAPI.loadTodayAssignments(today);
|
||||||
todayAssignmentsMap = {};
|
todayAssignmentsMap = {};
|
||||||
if (res && res.success) {
|
assignments.forEach(a => {
|
||||||
res.data.forEach(a => {
|
if (a.sessions && a.sessions.length > 0) {
|
||||||
if (a.sessions && a.sessions.length > 0) {
|
todayAssignmentsMap[a.worker_id] = a;
|
||||||
todayAssignmentsMap[a.worker_id] = a;
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('배정 현황 로드 오류:', e);
|
console.error('배정 현황 로드 오류:', e);
|
||||||
todayAssignmentsMap = {};
|
todayAssignmentsMap = {};
|
||||||
@@ -914,12 +693,8 @@ async function saveTbmSession() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.apiCall(`/tbm/sessions/${editingSessionId}/team/clear`, 'DELETE');
|
await window.TbmAPI.clearTeamMembers(editingSessionId);
|
||||||
const teamResponse = await window.apiCall(
|
const teamResponse = await window.TbmAPI.addTeamMembers(editingSessionId, members);
|
||||||
`/tbm/sessions/${editingSessionId}/team/batch`,
|
|
||||||
'POST',
|
|
||||||
{ members }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (teamResponse && teamResponse.success) {
|
if (teamResponse && teamResponse.success) {
|
||||||
showToast(`TBM이 수정되었습니다 (작업자 ${workerTaskList.length}명)`, 'success');
|
showToast(`TBM이 수정되었습니다 (작업자 ${workerTaskList.length}명)`, 'success');
|
||||||
@@ -969,17 +744,13 @@ async function saveTbmSession() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
|
const response = await window.TbmAPI.createTbmSession(sessionData);
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
const createdSessionId = response.data.session_id;
|
const createdSessionId = response.data.session_id;
|
||||||
console.log('✅ TBM 세션 생성 완료:', createdSessionId);
|
console.log('✅ TBM 세션 생성 완료:', createdSessionId);
|
||||||
|
|
||||||
const teamResponse = await window.apiCall(
|
const teamResponse = await window.TbmAPI.addTeamMembers(createdSessionId, members);
|
||||||
`/tbm/sessions/${createdSessionId}/team/batch`,
|
|
||||||
'POST',
|
|
||||||
{ members }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (teamResponse && teamResponse.success) {
|
if (teamResponse && teamResponse.success) {
|
||||||
showToast(`TBM이 생성되었습니다 (작업자 ${members.length}명)`, 'success');
|
showToast(`TBM이 생성되었습니다 (작업자 ${members.length}명)`, 'success');
|
||||||
@@ -1739,13 +1510,11 @@ async function loadWorkplacesByCategory(categoryId) {
|
|||||||
if (!workplaceList) return;
|
if (!workplaceList) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.apiCall(`/workplaces?category_id=${categoryId}`);
|
const workplaces = await window.TbmAPI.loadWorkplacesByCategory(categoryId);
|
||||||
if (!response || !response.success || !response.data || response.data.length === 0) {
|
if (!workplaces || workplaces.length === 0) {
|
||||||
workplaceList.innerHTML = '<div style="color: #9ca3af; text-align: center; padding: 2rem;">등록된 작업장이 없습니다</div>';
|
workplaceList.innerHTML = '<div style="color: #9ca3af; text-align: center; padding: 2rem;">등록된 작업장이 없습니다</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workplaces = response.data;
|
|
||||||
workplaceList.innerHTML = workplaces.map(workplace => `
|
workplaceList.innerHTML = workplaces.map(workplace => `
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onclick="selectWorkplace(${workplace.workplace_id}, '${workplace.workplace_name}')"
|
onclick="selectWorkplace(${workplace.workplace_id}, '${workplace.workplace_name}')"
|
||||||
@@ -1841,10 +1610,7 @@ function toggleWorkplaceList() {
|
|||||||
window.toggleWorkplaceList = toggleWorkplaceList;
|
window.toggleWorkplaceList = toggleWorkplaceList;
|
||||||
|
|
||||||
// 작업장 지도 로드 및 렌더링
|
// 작업장 지도 로드 및 렌더링
|
||||||
let mapRegions = []; // 현재 로드된 지도 영역들
|
// mapRegions, mapCanvas, mapCtx, mapImage → TbmState 프록시 사용 (state.js)
|
||||||
let mapCanvas = null;
|
|
||||||
let mapCtx = null;
|
|
||||||
let mapImage = null;
|
|
||||||
|
|
||||||
async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
||||||
try {
|
try {
|
||||||
@@ -1863,12 +1629,7 @@ async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
|||||||
console.log('🖼️ 이미지 로드 시도:', fullImageUrl);
|
console.log('🖼️ 이미지 로드 시도:', fullImageUrl);
|
||||||
|
|
||||||
// 지도 영역 데이터 로드
|
// 지도 영역 데이터 로드
|
||||||
const regionsResponse = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`);
|
mapRegions = await window.TbmAPI.loadMapRegions(categoryId);
|
||||||
if (regionsResponse && regionsResponse.success) {
|
|
||||||
mapRegions = regionsResponse.data || [];
|
|
||||||
} else {
|
|
||||||
mapRegions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이미지 로드
|
// 이미지 로드
|
||||||
mapImage = new Image();
|
mapImage = new Image();
|
||||||
@@ -2190,23 +1951,19 @@ async function openTeamCompositionModal(sessionId) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 세션 정보 로드
|
// 세션 정보 로드
|
||||||
const sessionResponse = await window.apiCall(`/tbm/sessions/${sessionId}`);
|
const session = await window.TbmAPI.getSession(sessionId);
|
||||||
if (!sessionResponse || !sessionResponse.success) {
|
if (!session) {
|
||||||
showToast('TBM 정보를 불러올 수 없습니다.', 'error');
|
showToast('TBM 정보를 불러올 수 없습니다.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = sessionResponse.data; // data는 이미 객체
|
|
||||||
|
|
||||||
// 팀원 정보 로드
|
// 팀원 정보 로드
|
||||||
const teamResponse = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
|
const teamMembers = await window.TbmAPI.getTeamMembers(sessionId);
|
||||||
if (!teamResponse || !teamResponse.success) {
|
if (!teamMembers) {
|
||||||
showToast('팀원 정보를 불러올 수 없습니다.', 'error');
|
showToast('팀원 정보를 불러올 수 없습니다.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamMembers = teamResponse.data;
|
|
||||||
|
|
||||||
// workerTaskList 구성
|
// workerTaskList 구성
|
||||||
workerTaskList = [];
|
workerTaskList = [];
|
||||||
const workerMap = new Map();
|
const workerMap = new Map();
|
||||||
@@ -2341,11 +2098,7 @@ async function saveTeamComposition() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.apiCall(
|
const response = await window.TbmAPI.addTeamMembers(currentSessionId, members);
|
||||||
`/tbm/sessions/${currentSessionId}/team/batch`,
|
|
||||||
'POST',
|
|
||||||
{ members }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
showToast(`${selectedWorkers.size}명의 팀원이 추가되었습니다.`, 'success');
|
showToast(`${selectedWorkers.size}명의 팀원이 추가되었습니다.`, 'success');
|
||||||
@@ -2374,13 +2127,9 @@ async function openSafetyCheckModal(sessionId) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 필터링된 체크리스트 조회 (기본 + 날씨 + 작업별)
|
// 필터링된 체크리스트 조회 (기본 + 날씨 + 작업별)
|
||||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety-checks/filtered`);
|
const filteredData = await window.TbmAPI.getFilteredSafetyChecks(sessionId);
|
||||||
|
|
||||||
if (!response || !response.success) {
|
const { basic, weather, task, weatherInfo } = filteredData;
|
||||||
throw new Error(response?.message || '체크리스트를 불러올 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { basic, weather, task, weatherInfo } = response.data;
|
|
||||||
|
|
||||||
const categoryNames = {
|
const categoryNames = {
|
||||||
'PPE': '개인 보호 장비',
|
'PPE': '개인 보호 장비',
|
||||||
@@ -2560,18 +2309,9 @@ async function saveSafetyChecklist() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.apiCall(
|
await window.TbmAPI.saveSafetyChecks(currentSessionId, records);
|
||||||
`/tbm/sessions/${currentSessionId}/safety`,
|
showToast('안전 체크가 완료되었습니다.', 'success');
|
||||||
'POST',
|
closeSafetyModal();
|
||||||
{ records }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response && response.success) {
|
|
||||||
showToast('안전 체크가 완료되었습니다.', 'success');
|
|
||||||
closeSafetyModal();
|
|
||||||
} else {
|
|
||||||
throw new Error(response.message || '저장에 실패했습니다.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 안전 체크 저장 오류:', error);
|
console.error('❌ 안전 체크 저장 오류:', error);
|
||||||
showToast('안전 체크 저장 중 오류가 발생했습니다.', 'error');
|
showToast('안전 체크 저장 중 오류가 발생했습니다.', 'error');
|
||||||
@@ -2594,8 +2334,7 @@ async function openCompleteTbmModal(sessionId) {
|
|||||||
|
|
||||||
// 팀원 조회 → 근태 선택 렌더링
|
// 팀원 조회 → 근태 선택 렌더링
|
||||||
try {
|
try {
|
||||||
const teamRes = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
|
completeModalTeam = await window.TbmAPI.getTeamMembers(sessionId);
|
||||||
completeModalTeam = (teamRes && teamRes.data) ? teamRes.data : [];
|
|
||||||
renderCompleteAttendanceList();
|
renderCompleteAttendanceList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('팀원 조회 오류:', e);
|
console.error('팀원 조회 오류:', e);
|
||||||
@@ -2731,16 +2470,12 @@ window.completeTbmSession = completeTbmSession;
|
|||||||
async function viewTbmSession(sessionId) {
|
async function viewTbmSession(sessionId) {
|
||||||
try {
|
try {
|
||||||
// 세션 정보, 팀 구성, 안전 체크 동시 조회
|
// 세션 정보, 팀 구성, 안전 체크 동시 조회
|
||||||
const [sessionRes, teamRes, safetyRes] = await Promise.all([
|
const [session, team, safety] = await Promise.all([
|
||||||
window.apiCall(`/tbm/sessions/${sessionId}`),
|
window.TbmAPI.getSession(sessionId),
|
||||||
window.apiCall(`/tbm/sessions/${sessionId}/team`),
|
window.TbmAPI.getTeamMembers(sessionId),
|
||||||
window.apiCall(`/tbm/sessions/${sessionId}/safety`)
|
window.TbmAPI.getSafetyChecks(sessionId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const session = sessionRes?.data;
|
|
||||||
const team = teamRes?.data || [];
|
|
||||||
const safety = safetyRes?.data || [];
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
showToast('세션 정보를 불러올 수 없습니다.', 'error');
|
showToast('세션 정보를 불러올 수 없습니다.', 'error');
|
||||||
return;
|
return;
|
||||||
@@ -2881,6 +2616,12 @@ async function viewTbmSession(sessionId) {
|
|||||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="closeDetailModal(); openTeamCompositionModal(${safeId})">
|
<button type="button" class="tbm-btn tbm-btn-primary" onclick="closeDetailModal(); openTeamCompositionModal(${safeId})">
|
||||||
수정
|
수정
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="tbm-btn" style="background:#8b5cf6; color:white;" onclick="closeDetailModal(); openDesktopSplitModal(${safeId})">
|
||||||
|
분할
|
||||||
|
</button>
|
||||||
|
<button type="button" class="tbm-btn" style="background:#f59e0b; color:white;" onclick="closeDetailModal(); openDesktopPullModal(${safeId})">
|
||||||
|
빼오기
|
||||||
|
</button>
|
||||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeDetailModal()">닫기</button>
|
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeDetailModal()">닫기</button>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@@ -2906,27 +2647,20 @@ function confirmDeleteTbm(sessionId) {
|
|||||||
}
|
}
|
||||||
window.confirmDeleteTbm = confirmDeleteTbm;
|
window.confirmDeleteTbm = confirmDeleteTbm;
|
||||||
|
|
||||||
// TBM 세션 삭제
|
// TBM 세션 삭제 → TbmAPI 위임
|
||||||
async function deleteTbmSession(sessionId) {
|
async function deleteTbmSession(sessionId) {
|
||||||
try {
|
try {
|
||||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}`, 'DELETE');
|
await window.TbmAPI.deleteSession(sessionId);
|
||||||
|
showToast('TBM이 삭제되었습니다.', 'success');
|
||||||
if (response && response.success) {
|
closeDetailModal();
|
||||||
showToast('TBM이 삭제되었습니다.', 'success');
|
if (currentTab === 'tbm-input') {
|
||||||
closeDetailModal();
|
await loadTodayOnlyTbm();
|
||||||
|
|
||||||
// 목록 새로고침
|
|
||||||
if (currentTab === 'tbm-input') {
|
|
||||||
await loadTodayOnlyTbm();
|
|
||||||
} else {
|
|
||||||
await loadRecentTbmGroupedByDate();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showToast(response?.message || 'TBM 삭제에 실패했습니다.', 'error');
|
await loadRecentTbmGroupedByDate();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ TBM 삭제 오류:', error);
|
console.error('❌ TBM 삭제 오류:', error);
|
||||||
showToast('TBM 삭제 중 오류가 발생했습니다.', 'error');
|
showToast(error?.message || 'TBM 삭제 중 오류가 발생했습니다.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.deleteTbmSession = deleteTbmSession;
|
window.deleteTbmSession = deleteTbmSession;
|
||||||
@@ -2944,14 +2678,11 @@ async function openHandoverModal(sessionId) {
|
|||||||
|
|
||||||
// 세션 정보와 팀 구성 조회
|
// 세션 정보와 팀 구성 조회
|
||||||
try {
|
try {
|
||||||
const [sessionRes, teamRes] = await Promise.all([
|
const [session, team] = await Promise.all([
|
||||||
window.apiCall(`/tbm/sessions/${sessionId}`),
|
window.TbmAPI.getSession(sessionId),
|
||||||
window.apiCall(`/tbm/sessions/${sessionId}/team`)
|
window.TbmAPI.getTeamMembers(sessionId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const session = sessionRes?.data;
|
|
||||||
const team = teamRes?.data || [];
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
showToast('세션 정보를 불러올 수 없습니다.', 'error');
|
showToast('세션 정보를 불러올 수 없습니다.', 'error');
|
||||||
return;
|
return;
|
||||||
@@ -3041,8 +2772,8 @@ async function saveHandover() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 세션 정보 조회 (from_leader_id 가져오기)
|
// 세션 정보 조회 (from_leader_id 가져오기)
|
||||||
const sessionRes = await window.apiCall(`/tbm/sessions/${sessionId}`);
|
const sessionData = await window.TbmAPI.getSession(sessionId);
|
||||||
const fromLeaderId = sessionRes?.data?.leader_id;
|
const fromLeaderId = sessionData?.leader_id;
|
||||||
|
|
||||||
if (!fromLeaderId) {
|
if (!fromLeaderId) {
|
||||||
showToast('세션 정보를 찾을 수 없습니다.', 'error');
|
showToast('세션 정보를 찾을 수 없습니다.', 'error');
|
||||||
@@ -3060,7 +2791,7 @@ async function saveHandover() {
|
|||||||
worker_ids: workerIds
|
worker_ids: workerIds
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await window.apiCall('/tbm/handovers', 'POST', handoverData);
|
const response = await window.TbmAPI.saveHandover(handoverData);
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
showToast('작업 인계가 요청되었습니다.', 'success');
|
showToast('작업 인계가 요청되었습니다.', 'success');
|
||||||
@@ -3075,4 +2806,174 @@ async function saveHandover() {
|
|||||||
}
|
}
|
||||||
window.saveHandover = 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 `
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem; border:1px solid #e5e7eb; border-radius:0.375rem;">
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(m.worker_name)}</strong>
|
||||||
|
<span style="font-size:0.75rem; color:#6b7280;">(${hours}h)</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:0.25rem; align-items:center;">
|
||||||
|
<input type="number" id="split_hours_${i}" step="0.5" min="0.5" max="${hours - 0.5}" placeholder="분할시간" style="width:80px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.8125rem;">
|
||||||
|
<button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executeSplit(${i})">분할</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).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 = '<div style="padding:1rem; text-align:center; color:#9ca3af;">빼올 수 있는 다른 TBM이 없습니다.</div>';
|
||||||
|
} 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 `
|
||||||
|
<div style="border:1px solid #e5e7eb; border-radius:0.375rem; margin-bottom:0.5rem;">
|
||||||
|
<div style="padding:0.5rem 0.75rem; cursor:pointer; display:flex; justify-content:space-between; align-items:center;" onclick="togglePullSessionMembers(${s.session_id}, this)">
|
||||||
|
<div><strong>${leader}</strong> <span style="font-size:0.75rem; color:#6b7280;">(${count}명)</span></div>
|
||||||
|
<span style="font-size:0.75rem; color:#6b7280;">▼</span>
|
||||||
|
</div>
|
||||||
|
<div id="pullMembers_${s.session_id}" style="display:none; padding:0.5rem; border-top:1px solid #f3f4f6;"></div>
|
||||||
|
</div>`;
|
||||||
|
}).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 `
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.375rem 0; border-bottom:1px solid #f9fafb;">
|
||||||
|
<span>${escapeHtml(m.worker_name)} <span style="font-size:0.75rem; color:#6b7280;">(${hours}h)</span></span>
|
||||||
|
<div style="display:flex; gap:0.25rem; align-items:center;">
|
||||||
|
<input type="number" id="pull_h_${sessionId}_${m.worker_id}" step="0.5" min="0.5" max="${hours}" value="${hours}" style="width:60px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.75rem;">
|
||||||
|
<button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executePull(${sessionId}, ${m.worker_id}, '${escapeHtml(m.worker_name)}')">빼오기</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('') || '<div style="color:#9ca3af; padding:0.25rem;">팀원 없음</div>';
|
||||||
|
container.style.display = 'block';
|
||||||
|
} catch(e) {
|
||||||
|
container.innerHTML = '<div style="color:#ef4444; padding:0.25rem;">로드 오류</div>';
|
||||||
|
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 전역 사용
|
// showToast → api-base.js 전역 사용
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@
|
|||||||
<link rel="stylesheet" href="/css/mobile.css?v=1">
|
<link rel="stylesheet" href="/css/mobile.css?v=1">
|
||||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||||
<!-- 최적화된 로딩: API 설정 → 앱 초기화 (병렬 컴포넌트 로딩) -->
|
<!-- 최적화된 로딩: API 설정 → 앱 초기화 (병렬 컴포넌트 로딩) -->
|
||||||
<script src="/js/api-base.js"></script>
|
<script src="/js/api-base.js?v=2"></script>
|
||||||
<script src="/js/app-init.js?v=5" defer></script>
|
<script src="/js/app-init.js?v=9" defer></script>
|
||||||
<!-- instant.page: 링크 호버 시 페이지 프리로딩 -->
|
<!-- instant.page: 링크 호버 시 페이지 프리로딩 -->
|
||||||
<script src="https://instant.page/5.2.0" type="module"></script>
|
<script src="https://instant.page/5.2.0" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -692,17 +692,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 분할 모달 -->
|
||||||
|
<div id="splitModal" class="tbm-modal-overlay" style="display:none;">
|
||||||
|
<div class="tbm-modal-container" style="max-width:500px;">
|
||||||
|
<div class="tbm-modal-header">
|
||||||
|
<h2>작업 분할</h2>
|
||||||
|
<button type="button" class="tbm-modal-close" onclick="closeSplitModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="tbm-modal-body">
|
||||||
|
<p style="font-size:0.8125rem; color:#6b7280; margin-bottom:0.75rem;">작업자의 배정 시간을 분할합니다.</p>
|
||||||
|
<div id="splitMemberList" style="display:flex; flex-direction:column; gap:0.5rem;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tbm-modal-footer">
|
||||||
|
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeSplitModal()">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 빼오기 모달 -->
|
||||||
|
<div id="pullModal" class="tbm-modal-overlay" style="display:none;">
|
||||||
|
<div class="tbm-modal-container" style="max-width:500px;">
|
||||||
|
<div class="tbm-modal-header">
|
||||||
|
<h2>빼오기</h2>
|
||||||
|
<button type="button" class="tbm-modal-close" onclick="closePullModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="tbm-modal-body">
|
||||||
|
<p style="font-size:0.8125rem; color:#6b7280; margin-bottom:0.75rem;">다른 반장의 TBM에서 작업자를 빼옵니다.</p>
|
||||||
|
<div id="pullSessionList"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tbm-modal-footer">
|
||||||
|
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closePullModal()">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 토스트 알림 -->
|
<!-- 토스트 알림 -->
|
||||||
<div class="toast-container" id="toastContainer"></div>
|
<div class="toast-container" id="toastContainer"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 공통 모듈 -->
|
||||||
|
<script src="/js/common/utils.js?v=1"></script>
|
||||||
|
<script src="/js/common/base-state.js?v=1"></script>
|
||||||
|
|
||||||
<!-- TBM 모듈 (리팩토링된 구조) -->
|
<!-- TBM 모듈 (리팩토링된 구조) -->
|
||||||
<script src="/js/tbm/state.js?v=1"></script>
|
<script src="/js/tbm/state.js?v=2"></script>
|
||||||
<script src="/js/tbm/utils.js?v=1"></script>
|
<script src="/js/tbm/utils.js?v=2"></script>
|
||||||
<script src="/js/tbm/api.js?v=1"></script>
|
<script src="/js/tbm/api.js?v=4"></script>
|
||||||
|
|
||||||
<!-- 기존 UI 로직 (점진적 마이그레이션) -->
|
<!-- 기존 UI 로직 (점진적 마이그레이션) -->
|
||||||
<script type="module" src="/js/tbm.js?v=10"></script>
|
<script defer src="/js/tbm.js?v=13"></script>
|
||||||
|
|
||||||
<!-- 모바일 하단 네비게이션 -->
|
<!-- 모바일 하단 네비게이션 -->
|
||||||
<div id="mobile-nav-container"></div>
|
<div id="mobile-nav-container"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user