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
|
||||
// 전역 변수: 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 = '<div style="color: #9ca3af; text-align: center; padding: 2rem;">등록된 작업장이 없습니다</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const workplaces = response.data;
|
||||
workplaceList.innerHTML = workplaces.map(workplace => `
|
||||
<button type="button"
|
||||
onclick="selectWorkplace(${workplace.workplace_id}, '${workplace.workplace_name}')"
|
||||
@@ -1841,10 +1610,7 @@ function toggleWorkplaceList() {
|
||||
window.toggleWorkplaceList = toggleWorkplaceList;
|
||||
|
||||
// 작업장 지도 로드 및 렌더링
|
||||
let mapRegions = []; // 현재 로드된 지도 영역들
|
||||
let mapCanvas = null;
|
||||
let mapCtx = null;
|
||||
let mapImage = null;
|
||||
// mapRegions, mapCanvas, mapCtx, mapImage → TbmState 프록시 사용 (state.js)
|
||||
|
||||
async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
||||
try {
|
||||
@@ -1863,12 +1629,7 @@ async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
||||
console.log('🖼️ 이미지 로드 시도:', fullImageUrl);
|
||||
|
||||
// 지도 영역 데이터 로드
|
||||
const regionsResponse = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`);
|
||||
if (regionsResponse && regionsResponse.success) {
|
||||
mapRegions = regionsResponse.data || [];
|
||||
} else {
|
||||
mapRegions = [];
|
||||
}
|
||||
mapRegions = await window.TbmAPI.loadMapRegions(categoryId);
|
||||
|
||||
// 이미지 로드
|
||||
mapImage = new Image();
|
||||
@@ -2190,23 +1951,19 @@ async function openTeamCompositionModal(sessionId) {
|
||||
|
||||
try {
|
||||
// 세션 정보 로드
|
||||
const sessionResponse = await window.apiCall(`/tbm/sessions/${sessionId}`);
|
||||
if (!sessionResponse || !sessionResponse.success) {
|
||||
const session = await window.TbmAPI.getSession(sessionId);
|
||||
if (!session) {
|
||||
showToast('TBM 정보를 불러올 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const session = sessionResponse.data; // data는 이미 객체
|
||||
|
||||
// 팀원 정보 로드
|
||||
const teamResponse = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
|
||||
if (!teamResponse || !teamResponse.success) {
|
||||
const teamMembers = await window.TbmAPI.getTeamMembers(sessionId);
|
||||
if (!teamMembers) {
|
||||
showToast('팀원 정보를 불러올 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const teamMembers = teamResponse.data;
|
||||
|
||||
// workerTaskList 구성
|
||||
workerTaskList = [];
|
||||
const workerMap = new Map();
|
||||
@@ -2341,11 +2098,7 @@ async function saveTeamComposition() {
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${currentSessionId}/team/batch`,
|
||||
'POST',
|
||||
{ members }
|
||||
);
|
||||
const response = await window.TbmAPI.addTeamMembers(currentSessionId, members);
|
||||
|
||||
if (response && response.success) {
|
||||
showToast(`${selectedWorkers.size}명의 팀원이 추가되었습니다.`, 'success');
|
||||
@@ -2374,13 +2127,9 @@ async function openSafetyCheckModal(sessionId) {
|
||||
|
||||
try {
|
||||
// 필터링된 체크리스트 조회 (기본 + 날씨 + 작업별)
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety-checks/filtered`);
|
||||
const filteredData = await window.TbmAPI.getFilteredSafetyChecks(sessionId);
|
||||
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '체크리스트를 불러올 수 없습니다.');
|
||||
}
|
||||
|
||||
const { basic, weather, task, weatherInfo } = response.data;
|
||||
const { basic, weather, task, weatherInfo } = filteredData;
|
||||
|
||||
const categoryNames = {
|
||||
'PPE': '개인 보호 장비',
|
||||
@@ -2560,18 +2309,9 @@ async function saveSafetyChecklist() {
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${currentSessionId}/safety`,
|
||||
'POST',
|
||||
{ records }
|
||||
);
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('안전 체크가 완료되었습니다.', 'success');
|
||||
closeSafetyModal();
|
||||
} else {
|
||||
throw new Error(response.message || '저장에 실패했습니다.');
|
||||
}
|
||||
await window.TbmAPI.saveSafetyChecks(currentSessionId, records);
|
||||
showToast('안전 체크가 완료되었습니다.', 'success');
|
||||
closeSafetyModal();
|
||||
} catch (error) {
|
||||
console.error('❌ 안전 체크 저장 오류:', error);
|
||||
showToast('안전 체크 저장 중 오류가 발생했습니다.', 'error');
|
||||
@@ -2594,8 +2334,7 @@ async function openCompleteTbmModal(sessionId) {
|
||||
|
||||
// 팀원 조회 → 근태 선택 렌더링
|
||||
try {
|
||||
const teamRes = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
|
||||
completeModalTeam = (teamRes && teamRes.data) ? teamRes.data : [];
|
||||
completeModalTeam = await window.TbmAPI.getTeamMembers(sessionId);
|
||||
renderCompleteAttendanceList();
|
||||
} catch (e) {
|
||||
console.error('팀원 조회 오류:', e);
|
||||
@@ -2731,16 +2470,12 @@ window.completeTbmSession = completeTbmSession;
|
||||
async function viewTbmSession(sessionId) {
|
||||
try {
|
||||
// 세션 정보, 팀 구성, 안전 체크 동시 조회
|
||||
const [sessionRes, teamRes, safetyRes] = await Promise.all([
|
||||
window.apiCall(`/tbm/sessions/${sessionId}`),
|
||||
window.apiCall(`/tbm/sessions/${sessionId}/team`),
|
||||
window.apiCall(`/tbm/sessions/${sessionId}/safety`)
|
||||
const [session, team, safety] = await Promise.all([
|
||||
window.TbmAPI.getSession(sessionId),
|
||||
window.TbmAPI.getTeamMembers(sessionId),
|
||||
window.TbmAPI.getSafetyChecks(sessionId)
|
||||
]);
|
||||
|
||||
const session = sessionRes?.data;
|
||||
const team = teamRes?.data || [];
|
||||
const safety = safetyRes?.data || [];
|
||||
|
||||
if (!session) {
|
||||
showToast('세션 정보를 불러올 수 없습니다.', 'error');
|
||||
return;
|
||||
@@ -2881,6 +2616,12 @@ async function viewTbmSession(sessionId) {
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="closeDetailModal(); openTeamCompositionModal(${safeId})">
|
||||
수정
|
||||
</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>
|
||||
`;
|
||||
} 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 `
|
||||
<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 전역 사용
|
||||
|
||||
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="icon" type="image/png" href="/img/favicon.png">
|
||||
<!-- 최적화된 로딩: API 설정 → 앱 초기화 (병렬 컴포넌트 로딩) -->
|
||||
<script src="/js/api-base.js"></script>
|
||||
<script src="/js/app-init.js?v=5" defer></script>
|
||||
<script src="/js/api-base.js?v=2"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<!-- instant.page: 링크 호버 시 페이지 프리로딩 -->
|
||||
<script src="https://instant.page/5.2.0" type="module"></script>
|
||||
</head>
|
||||
@@ -692,17 +692,55 @@
|
||||
</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>
|
||||
|
||||
<!-- 공통 모듈 -->
|
||||
<script src="/js/common/utils.js?v=1"></script>
|
||||
<script src="/js/common/base-state.js?v=1"></script>
|
||||
|
||||
<!-- TBM 모듈 (리팩토링된 구조) -->
|
||||
<script src="/js/tbm/state.js?v=1"></script>
|
||||
<script src="/js/tbm/utils.js?v=1"></script>
|
||||
<script src="/js/tbm/api.js?v=1"></script>
|
||||
<script src="/js/tbm/state.js?v=2"></script>
|
||||
<script src="/js/tbm/utils.js?v=2"></script>
|
||||
<script src="/js/tbm/api.js?v=4"></script>
|
||||
|
||||
<!-- 기존 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>
|
||||
|
||||
Reference in New Issue
Block a user