diff --git a/web-ui/js/tbm.js b/web-ui/js/tbm.js
new file mode 100644
index 0000000..9c018ad
--- /dev/null
+++ b/web-ui/js/tbm.js
@@ -0,0 +1,638 @@
+// tbm.js - TBM 관리 페이지 JavaScript
+
+// 전역 변수
+let allSessions = [];
+let allWorkers = [];
+let allProjects = [];
+let allSafetyChecks = [];
+let currentSessionId = null;
+let selectedWorkers = new Set();
+
+// 페이지 초기화
+document.addEventListener('DOMContentLoaded', async () => {
+ console.log('🛠️ TBM 관리 페이지 초기화');
+
+ // API 함수가 로드될 때까지 대기
+ let retryCount = 0;
+ while (!window.apiCall && retryCount < 50) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ retryCount++;
+ }
+
+ if (!window.apiCall) {
+ showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
+ return;
+ }
+
+ // 오늘 날짜 설정
+ const today = new Date().toISOString().split('T')[0];
+ document.getElementById('tbmDate').value = today;
+ document.getElementById('sessionDate').value = today;
+
+ // 이벤트 리스너 설정
+ setupEventListeners();
+
+ // 초기 데이터 로드
+ await loadInitialData();
+ await loadTodayTbm();
+});
+
+// 이벤트 리스너 설정
+function setupEventListeners() {
+ const tbmDateInput = document.getElementById('tbmDate');
+ if (tbmDateInput) {
+ tbmDateInput.addEventListener('change', () => {
+ const date = tbmDateInput.value;
+ loadTbmSessionsByDate(date);
+ });
+ }
+}
+
+// 초기 데이터 로드
+async function loadInitialData() {
+ try {
+ // 작업자 목록 로드
+ const workersResponse = await window.apiCall('/workers?limit=1000');
+ 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) {
+ allProjects = Array.isArray(projectsResponse) ? projectsResponse : (projectsResponse.data || []);
+ console.log('✅ 프로젝트 목록 로드:', allProjects.length + '개');
+ populateProjectSelect();
+ }
+
+ // 안전 체크리스트 로드
+ const safetyResponse = await window.apiCall('/tbm/safety-checks');
+ if (safetyResponse && safetyResponse.success) {
+ allSafetyChecks = safetyResponse.data;
+ console.log('✅ 안전 체크리스트 로드:', allSafetyChecks.length + '개');
+ }
+
+ } catch (error) {
+ console.error('❌ 초기 데이터 로드 오류:', error);
+ showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
+ }
+}
+
+// 오늘 TBM 로드
+async function loadTodayTbm() {
+ const today = new Date().toISOString().split('T')[0];
+ document.getElementById('tbmDate').value = today;
+ await loadTbmSessionsByDate(today);
+}
+window.loadTodayTbm = loadTodayTbm;
+
+// 특정 날짜의 TBM 세션 목록 로드
+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();
+ }
+}
+
+// TBM 세션 목록 표시
+function displayTbmSessions() {
+ const grid = document.getElementById('tbmSessionsGrid');
+ const emptyState = document.getElementById('emptyState');
+ const totalSessionsEl = document.getElementById('totalSessions');
+ const completedSessionsEl = document.getElementById('completedSessions');
+
+ if (allSessions.length === 0) {
+ grid.innerHTML = '';
+ emptyState.style.display = 'flex';
+ totalSessionsEl.textContent = '0';
+ completedSessionsEl.textContent = '0';
+ return;
+ }
+
+ emptyState.style.display = 'none';
+
+ const completedCount = allSessions.filter(s => s.status === 'completed').length;
+ totalSessionsEl.textContent = allSessions.length;
+ completedSessionsEl.textContent = completedCount;
+
+ grid.innerHTML = allSessions.map(session => {
+ const statusBadge = {
+ 'draft': '진행중',
+ 'completed': '완료',
+ 'cancelled': '취소'
+ }[session.status] || '';
+
+ return `
+
+
+
+
+
+ 프로젝트
+ ${session.project_name || '-'}
+
+
+ 작업 장소
+ ${session.work_location || '-'}
+
+
+ 팀원 수
+ ${session.team_member_count || 0}명
+
+
+ 시작 시간
+ ${session.start_time || '-'}
+
+
+
+ ${session.work_description ? `
+
+ ${session.work_description}
+
+ ` : ''}
+
+
+ ${session.status === 'draft' ? `
+
+
+
+ ` : ''}
+
+
+ `;
+ }).join('');
+}
+
+// 새 TBM 모달 열기
+function openNewTbmModal() {
+ currentSessionId = null;
+ document.getElementById('modalTitle').textContent = '새 TBM 시작';
+ document.getElementById('sessionId').value = '';
+ document.getElementById('tbmForm').reset();
+
+ const today = new Date().toISOString().split('T')[0];
+ document.getElementById('sessionDate').value = today;
+
+ // 팀장 목록 로드
+ populateLeaderSelect();
+ populateProjectSelect();
+
+ document.getElementById('tbmModal').style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+}
+window.openNewTbmModal = openNewTbmModal;
+
+// 팀장 선택 드롭다운 채우기
+function populateLeaderSelect() {
+ const leaderSelect = document.getElementById('leaderId');
+ if (!leaderSelect) return;
+
+ const leaders = allWorkers.filter(w =>
+ w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin'
+ );
+
+ leaderSelect.innerHTML = '' +
+ leaders.map(w => `
+
+ `).join('');
+}
+
+// 프로젝트 선택 드롭다운 채우기
+function populateProjectSelect() {
+ const projectSelect = document.getElementById('projectId');
+ if (!projectSelect) return;
+
+ projectSelect.innerHTML = '' +
+ allProjects.map(p => `
+
+ `).join('');
+}
+
+// TBM 모달 닫기
+function closeTbmModal() {
+ document.getElementById('tbmModal').style.display = 'none';
+ document.body.style.overflow = 'auto';
+}
+window.closeTbmModal = closeTbmModal;
+
+// TBM 세션 저장
+async function saveTbmSession() {
+ const sessionData = {
+ session_date: document.getElementById('sessionDate').value,
+ leader_id: parseInt(document.getElementById('leaderId').value),
+ project_id: document.getElementById('projectId').value || null,
+ work_location: document.getElementById('workLocation').value || null,
+ work_description: document.getElementById('workDescription').value || null,
+ safety_notes: document.getElementById('safetyNotes').value || null,
+ start_time: document.getElementById('startTime').value || null
+ };
+
+ if (!sessionData.session_date || !sessionData.leader_id) {
+ showToast('TBM 날짜와 팀장을 선택해주세요.', 'error');
+ return;
+ }
+
+ try {
+ const response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
+
+ if (response && response.success) {
+ showToast('TBM 세션이 생성되었습니다.', 'success');
+ closeTbmModal();
+
+ const createdSessionId = response.data.session_id;
+
+ // 목록 새로고침
+ await loadTbmSessionsByDate(sessionData.session_date);
+
+ // 팀 구성 모달 열기
+ setTimeout(() => {
+ openTeamCompositionModal(createdSessionId);
+ }, 500);
+ } else {
+ throw new Error(response.message || '저장에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('❌ TBM 세션 저장 오류:', error);
+ showToast('TBM 세션 저장 중 오류가 발생했습니다.', 'error');
+ }
+}
+window.saveTbmSession = saveTbmSession;
+
+// 팀 구성 모달 열기
+async function openTeamCompositionModal(sessionId) {
+ currentSessionId = sessionId;
+ selectedWorkers.clear();
+
+ // 기존 팀 구성 로드
+ try {
+ const response = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
+ if (response && response.success) {
+ response.data.forEach(member => {
+ selectedWorkers.add(member.worker_id);
+ });
+ }
+ } catch (error) {
+ console.error('팀 구성 조회 오류:', error);
+ }
+
+ // 작업자 선택 그리드 생성
+ const grid = document.getElementById('workerSelectionGrid');
+ grid.innerHTML = allWorkers.map(worker => `
+
+ `).join('');
+
+ updateSelectedWorkers();
+
+ document.getElementById('teamModal').style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+}
+window.openTeamCompositionModal = openTeamCompositionModal;
+
+// 선택된 작업자 업데이트
+function updateSelectedWorkers() {
+ selectedWorkers.clear();
+
+ document.querySelectorAll('.worker-checkbox:checked').forEach(cb => {
+ selectedWorkers.add(parseInt(cb.dataset.workerId));
+ });
+
+ const selectedCount = document.getElementById('selectedCount');
+ const selectedList = document.getElementById('selectedWorkersList');
+
+ selectedCount.textContent = selectedWorkers.size;
+
+ if (selectedWorkers.size === 0) {
+ selectedList.innerHTML = '작업자를 선택해주세요
';
+ } else {
+ const selectedWorkersArray = Array.from(selectedWorkers).map(id => {
+ const worker = allWorkers.find(w => w.worker_id === id);
+ return worker ? `
+
+ ${worker.worker_name}
+
+
+ ` : '';
+ });
+ selectedList.innerHTML = selectedWorkersArray.join('');
+ }
+}
+window.updateSelectedWorkers = updateSelectedWorkers;
+
+// 작업자 제거
+function removeWorker(workerId) {
+ const checkbox = document.querySelector(`.worker-checkbox[data-worker-id="${workerId}"]`);
+ if (checkbox) {
+ checkbox.checked = false;
+ updateSelectedWorkers();
+ }
+}
+window.removeWorker = removeWorker;
+
+// 전체 선택
+function selectAllWorkers() {
+ document.querySelectorAll('.worker-checkbox').forEach(cb => {
+ cb.checked = true;
+ });
+ updateSelectedWorkers();
+}
+window.selectAllWorkers = selectAllWorkers;
+
+// 전체 해제
+function deselectAllWorkers() {
+ document.querySelectorAll('.worker-checkbox').forEach(cb => {
+ cb.checked = false;
+ });
+ updateSelectedWorkers();
+}
+window.deselectAllWorkers = deselectAllWorkers;
+
+// 팀 구성 모달 닫기
+function closeTeamModal() {
+ document.getElementById('teamModal').style.display = 'none';
+ document.body.style.overflow = 'auto';
+}
+window.closeTeamModal = closeTeamModal;
+
+// 팀 구성 저장
+async function saveTeamComposition() {
+ if (selectedWorkers.size === 0) {
+ showToast('최소 1명 이상의 작업자를 선택해주세요.', 'error');
+ return;
+ }
+
+ const members = Array.from(selectedWorkers).map(workerId => ({
+ worker_id: workerId
+ }));
+
+ try {
+ const response = await window.apiCall(
+ `/tbm/sessions/${currentSessionId}/team/batch`,
+ 'POST',
+ { members }
+ );
+
+ if (response && response.success) {
+ showToast(`${selectedWorkers.size}명의 팀원이 추가되었습니다.`, 'success');
+ closeTeamModal();
+
+ // 목록 새로고침
+ const date = document.getElementById('tbmDate').value;
+ await loadTbmSessionsByDate(date);
+ } else {
+ throw new Error(response.message || '저장에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('❌ 팀 구성 저장 오류:', error);
+ showToast('팀 구성 저장 중 오류가 발생했습니다.', 'error');
+ }
+}
+window.saveTeamComposition = saveTeamComposition;
+
+// 안전 체크 모달 열기
+async function openSafetyCheckModal(sessionId) {
+ currentSessionId = sessionId;
+
+ // 기존 안전 체크 기록 로드
+ try {
+ const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety`);
+ const existingRecords = response && response.success ? response.data : [];
+
+ // 카테고리별로 그룹화
+ const grouped = {};
+ allSafetyChecks.forEach(check => {
+ if (!grouped[check.check_category]) {
+ grouped[check.check_category] = [];
+ }
+
+ const existingRecord = existingRecords.find(r => r.check_id === check.check_id);
+ grouped[check.check_category].push({
+ ...check,
+ is_checked: existingRecord ? existingRecord.is_checked : false,
+ notes: existingRecord ? existingRecord.notes : ''
+ });
+ });
+
+ const categoryNames = {
+ 'PPE': '개인 보호 장비',
+ 'EQUIPMENT': '장비 점검',
+ 'ENVIRONMENT': '작업 환경',
+ 'EMERGENCY': '비상 대응'
+ };
+
+ const container = document.getElementById('safetyChecklistContainer');
+ container.innerHTML = Object.keys(grouped).map(category => `
+
+
+ ${categoryNames[category] || category}
+
+ ${grouped[category].map(check => `
+
+
+
+ `).join('')}
+
+ `).join('');
+
+ document.getElementById('safetyModal').style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+
+ } catch (error) {
+ console.error('❌ 안전 체크 조회 오류:', error);
+ showToast('안전 체크 정보를 불러오는 중 오류가 발생했습니다.', 'error');
+ }
+}
+window.openSafetyCheckModal = openSafetyCheckModal;
+
+// 안전 체크 모달 닫기
+function closeSafetyModal() {
+ document.getElementById('safetyModal').style.display = 'none';
+ document.body.style.overflow = 'auto';
+}
+window.closeSafetyModal = closeSafetyModal;
+
+// 안전 체크리스트 저장
+async function saveSafetyChecklist() {
+ const records = [];
+
+ document.querySelectorAll('.safety-check').forEach(cb => {
+ records.push({
+ check_id: parseInt(cb.dataset.checkId),
+ is_checked: cb.checked
+ });
+ });
+
+ 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 || '저장에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('❌ 안전 체크 저장 오류:', error);
+ showToast('안전 체크 저장 중 오류가 발생했습니다.', 'error');
+ }
+}
+window.saveSafetyChecklist = saveSafetyChecklist;
+
+// TBM 완료 모달 열기
+function openCompleteTbmModal(sessionId) {
+ currentSessionId = sessionId;
+ const now = new Date();
+ const timeString = now.toTimeString().slice(0, 5);
+ document.getElementById('endTime').value = timeString;
+
+ document.getElementById('completeModal').style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+}
+window.openCompleteTbmModal = openCompleteTbmModal;
+
+// 완료 모달 닫기
+function closeCompleteModal() {
+ document.getElementById('completeModal').style.display = 'none';
+ document.body.style.overflow = 'auto';
+}
+window.closeCompleteModal = closeCompleteModal;
+
+// TBM 세션 완료
+async function completeTbmSession() {
+ const endTime = document.getElementById('endTime').value;
+
+ try {
+ const response = await window.apiCall(
+ `/tbm/sessions/${currentSessionId}/complete`,
+ 'POST',
+ { end_time: endTime }
+ );
+
+ if (response && response.success) {
+ showToast('TBM이 완료되었습니다.', 'success');
+ closeCompleteModal();
+
+ // 목록 새로고침
+ const date = document.getElementById('tbmDate').value;
+ await loadTbmSessionsByDate(date);
+ } else {
+ throw new Error(response.message || '완료 처리에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('❌ TBM 완료 처리 오류:', error);
+ showToast('TBM 완료 처리 중 오류가 발생했습니다.', 'error');
+ }
+}
+window.completeTbmSession = completeTbmSession;
+
+// TBM 세션 상세 보기
+function viewTbmSession(sessionId) {
+ // TODO: 상세 보기 페이지 또는 모달 구현
+ console.log('TBM 세션 상세 보기:', sessionId);
+}
+window.viewTbmSession = viewTbmSession;
+
+// 토스트 알림
+function showToast(message, type = 'info', duration = 3000) {
+ const container = document.getElementById('toastContainer');
+ if (!container) return;
+
+ const toast = document.createElement('div');
+ toast.className = `toast ${type}`;
+
+ const iconMap = {
+ success: '✅',
+ error: '❌',
+ warning: '⚠️',
+ info: 'ℹ️'
+ };
+
+ toast.innerHTML = `
+ ${iconMap[type] || 'ℹ️'}
+ ${message}
+
+ `;
+
+ toast.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem 1.25rem;
+ background: white;
+ border-radius: 0.5rem;
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
+ margin-bottom: 0.75rem;
+ min-width: 300px;
+ animation: slideIn 0.3s ease-out;
+ `;
+
+ container.appendChild(toast);
+
+ setTimeout(() => {
+ if (toast.parentElement) {
+ toast.style.animation = 'slideOut 0.3s ease-out';
+ setTimeout(() => toast.remove(), 300);
+ }
+ }, duration);
+}