Files
TK-FB-Project/web-ui/js/tbm.js
Hyungi Ahn 566a38562c fix: work-types API 경로 수정
- /api/tools/work-types → /api/daily-work-reports/work-types
- task-management.js와 tbm.js에서 올바른 엔드포인트 사용
- API 서버 재시작으로 /api/tasks 라우트 활성화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 15:09:16 +09:00

1105 lines
38 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// tbm.js - TBM 관리 페이지 JavaScript
// 전역 변수
let allSessions = [];
let todaySessions = [];
let allWorkers = [];
let allProjects = [];
let allWorkTypes = [];
let allTasks = [];
let allSafetyChecks = [];
let currentSessionId = null;
let selectedWorkers = new Set();
let currentTab = 'tbm-input';
// 페이지 초기화
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 loadTodayOnlyTbm();
});
// 이벤트 리스너 설정
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 + '개');
}
// 공정(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 + '개');
}
} catch (error) {
console.error('❌ 초기 데이터 로드 오류:', error);
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
// ==================== 탭 전환 ====================
// 탭 전환
function switchTbmTab(tabName) {
currentTab = tabName;
// 탭 버튼 활성화 상태 변경
document.querySelectorAll('.tab-btn').forEach(btn => {
if (btn.dataset.tab === tabName) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// 탭 컨텐츠 표시 변경
document.querySelectorAll('.code-tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
// 탭에 따라 데이터 로드
if (tabName === 'tbm-input') {
loadTodayOnlyTbm();
} else if (tabName === 'tbm-manage') {
const tbmDate = document.getElementById('tbmDate');
if (tbmDate && tbmDate.value) {
loadTbmSessionsByDate(tbmDate.value);
} else {
loadTodayTbm();
}
}
}
window.switchTbmTab = switchTbmTab;
// ==================== TBM 입력 탭 ====================
// 오늘의 TBM만 로드 (TBM 입력 탭용)
async function loadTodayOnlyTbm() {
const today = new Date().toISOString().split('T')[0];
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;
// 오늘의 TBM 세션 표시
function displayTodayTbmSessions() {
const grid = document.getElementById('todayTbmGrid');
const emptyState = document.getElementById('todayEmptyState');
const todayTotalEl = document.getElementById('todayTotalSessions');
const todayCompletedEl = document.getElementById('todayCompletedSessions');
const todayActiveEl = document.getElementById('todayActiveSessions');
if (todaySessions.length === 0) {
grid.innerHTML = '';
emptyState.style.display = 'flex';
todayTotalEl.textContent = '0';
todayCompletedEl.textContent = '0';
todayActiveEl.textContent = '0';
return;
}
emptyState.style.display = 'none';
const completedCount = todaySessions.filter(s => s.status === 'completed').length;
const activeCount = todaySessions.filter(s => s.status === 'draft').length;
todayTotalEl.textContent = todaySessions.length;
todayCompletedEl.textContent = completedCount;
todayActiveEl.textContent = activeCount;
grid.innerHTML = todaySessions.map(session => createSessionCard(session)).join('');
}
// ==================== TBM 관리 탭 ====================
// 오늘 TBM 로드 (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 loadAllTbm() {
try {
const response = await window.apiCall('/tbm/sessions');
if (response && response.success) {
allSessions = response.data || [];
document.getElementById('tbmDate').value = '';
displayTbmSessions();
} else {
allSessions = [];
displayTbmSessions();
}
} catch (error) {
console.error('❌ 전체 TBM 조회 오류:', error);
showToast('전체 TBM을 불러오는 중 오류가 발생했습니다.', 'error');
allSessions = [];
displayTbmSessions();
}
}
window.loadAllTbm = loadAllTbm;
// 특정 날짜의 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 => createSessionCard(session)).join('');
}
// TBM 세션 카드 생성 (공통)
function createSessionCard(session) {
const statusBadge = {
'draft': '<span class="badge" style="background: #fef3c7; color: #92400e;">진행중</span>',
'completed': '<span class="badge" style="background: #dcfce7; color: #166534;">완료</span>',
'cancelled': '<span class="badge" style="background: #fee2e2; color: #991b1b;">취소</span>'
}[session.status] || '';
return `
<div class="project-card" style="cursor: pointer;" onclick="viewTbmSession(${session.session_id})">
<div class="project-header">
<div>
<h3 class="project-name" style="font-size: 1rem; margin-bottom: 0.25rem;">
${session.leader_name || '팀장 미지정'}
</h3>
<p style="font-size: 0.75rem; color: #6b7280; margin: 0;">
${session.session_date} | ${session.leader_job_type || ''}
</p>
</div>
${statusBadge}
</div>
<div class="project-info" style="margin-top: 1rem;">
<div class="info-item">
<span class="info-label">프로젝트</span>
<span class="info-value">${session.project_name || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">공정</span>
<span class="info-value">${session.work_type_name || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">작업</span>
<span class="info-value">${session.task_name || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">작업 장소</span>
<span class="info-value">${session.work_location || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">팀원 수</span>
<span class="info-value">${session.team_member_count || 0}명</span>
</div>
<div class="info-item">
<span class="info-label">시작 시간</span>
<span class="info-value">${session.start_time || '-'}</span>
</div>
</div>
${session.work_description ? `
<div style="margin-top: 0.75rem; padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; font-size: 0.875rem; color: #374151;">
${session.work_description}
</div>
` : ''}
<div style="margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
${session.status === 'draft' ? `
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})" style="flex: 1; min-width: 100px;">
👥 팀 구성
</button>
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})" style="flex: 1; min-width: 100px;">
✅ 안전 체크
</button>
<button class="btn btn-sm" style="background: #f59e0b; color: white; border: none;" onclick="event.stopPropagation(); openHandoverModal(${session.session_id})">
📤 인계
</button>
<button class="btn btn-sm btn-success" onclick="event.stopPropagation(); openCompleteTbmModal(${session.session_id})">
완료
</button>
` : ''}
</div>
</div>
`;
}
// 새 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();
populateWorkTypeSelect();
// 작업 드롭다운 초기화
const taskSelect = document.getElementById('taskId');
if (taskSelect) {
taskSelect.innerHTML = '<option value="">작업 선택...</option>';
taskSelect.disabled = true;
}
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 = '<option value="">팀장 선택...</option>' +
leaders.map(w => `
<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>
`).join('');
}
// 프로젝트 선택 드롭다운 채우기
function populateProjectSelect() {
const projectSelect = document.getElementById('projectId');
if (!projectSelect) return;
projectSelect.innerHTML = '<option value="">프로젝트 선택...</option>' +
allProjects.map(p => `
<option value="${p.project_id}">${p.project_name} (${p.job_no})</option>
`).join('');
}
// 공정(Work Type) 선택 드롭다운 채우기
function populateWorkTypeSelect() {
const workTypeSelect = document.getElementById('workTypeId');
if (!workTypeSelect) return;
workTypeSelect.innerHTML = '<option value="">공정 선택...</option>' +
allWorkTypes.map(wt => `
<option value="${wt.id}">${wt.name}${wt.category ? ' (' + wt.category + ')' : ''}</option>
`).join('');
}
// 작업(Task) 선택 드롭다운 채우기 (공정 선택 시 호출)
function loadTasksByWorkType() {
const workTypeId = document.getElementById('workTypeId').value;
const taskSelect = document.getElementById('taskId');
if (!taskSelect) return;
if (!workTypeId) {
taskSelect.innerHTML = '<option value="">작업 선택...</option>';
taskSelect.disabled = true;
return;
}
// 선택한 공정에 해당하는 작업만 필터링
const filteredTasks = allTasks.filter(task =>
task.work_type_id === parseInt(workTypeId)
);
taskSelect.disabled = false;
taskSelect.innerHTML = '<option value="">작업 선택...</option>' +
filteredTasks.map(task => `
<option value="${task.task_id}">${task.task_name}</option>
`).join('');
if (filteredTasks.length === 0) {
taskSelect.innerHTML = '<option value="">등록된 작업이 없습니다</option>';
taskSelect.disabled = true;
}
}
window.loadTasksByWorkType = loadTasksByWorkType;
// TBM 모달 닫기
function closeTbmModal() {
document.getElementById('tbmModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
window.closeTbmModal = closeTbmModal;
// TBM 세션 저장
async function saveTbmSession() {
const workTypeId = document.getElementById('workTypeId').value;
const taskId = document.getElementById('taskId').value;
const sessionData = {
session_date: document.getElementById('sessionDate').value,
leader_id: parseInt(document.getElementById('leaderId').value),
project_id: document.getElementById('projectId').value || null,
work_type_id: workTypeId ? parseInt(workTypeId) : null,
task_id: taskId ? parseInt(taskId) : 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;
}
if (!sessionData.work_type_id || !sessionData.task_id) {
showToast('공정과 작업을 선택해주세요.', '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;
// 목록 새로고침
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
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 => `
<label style="display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border: 1px solid #e5e7eb; border-radius: 0.375rem; cursor: pointer; transition: all 0.2s;"
onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background='white'">
<input type="checkbox"
class="worker-checkbox"
data-worker-id="${worker.worker_id}"
data-worker-name="${worker.worker_name}"
${selectedWorkers.has(worker.worker_id) ? 'checked' : ''}
onchange="updateSelectedWorkers()"
style="width: 16px; height: 16px; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 500; font-size: 0.875rem;">${worker.worker_name}</div>
<div style="font-size: 0.75rem; color: #6b7280;">${worker.job_type || ''}</div>
</div>
</label>
`).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 = '<p style="margin: 0; color: #9ca3af; font-size: 0.875rem;">작업자를 선택해주세요</p>';
} else {
const selectedWorkersArray = Array.from(selectedWorkers).map(id => {
const worker = allWorkers.find(w => w.worker_id === id);
return worker ? `
<span style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.75rem; background: #3b82f6; color: white; border-radius: 9999px; font-size: 0.875rem;">
${worker.worker_name}
<button onclick="removeWorker(${id})" style="background: none; border: none; color: white; cursor: pointer; padding: 0; margin-left: 0.25rem; font-size: 1rem; line-height: 1;">×</button>
</span>
` : '';
});
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();
// 목록 새로고침
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
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 => `
<div style="margin-bottom: 1.5rem;">
<div style="font-weight: 600; font-size: 0.9375rem; color: #374151; padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; margin-bottom: 0.5rem;">
${categoryNames[category] || category}
</div>
${grouped[category].map(check => `
<div style="padding: 0.75rem; border-bottom: 1px solid #f3f4f6;">
<label style="display: flex; align-items: start; gap: 0.75rem; cursor: pointer;">
<input type="checkbox"
class="safety-check"
data-check-id="${check.check_id}"
${check.is_checked ? 'checked' : ''}
${check.is_required ? 'required' : ''}
style="width: 18px; height: 18px; margin-top: 0.125rem; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 500; color: #111827;">
${check.check_item}
${check.is_required ? '<span style="color: #ef4444;">*</span>' : ''}
</div>
${check.description ? `<div style="font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem;">${check.description}</div>` : ''}
</div>
</label>
</div>
`).join('')}
</div>
`).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();
// 목록 새로고침
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
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 세션 상세 보기
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 = sessionRes?.data;
const team = teamRes?.data || [];
const safety = safetyRes?.data || [];
if (!session) {
showToast('세션 정보를 불러올 수 없습니다.', 'error');
return;
}
// 기본 정보 표시
const basicInfo = document.getElementById('detailBasicInfo');
basicInfo.innerHTML = `
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">팀장</div>
<div style="font-weight: 600; color: #111827;">${session.leader_name}</div>
</div>
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">날짜</div>
<div style="font-weight: 600; color: #111827;">${session.session_date}</div>
</div>
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">프로젝트</div>
<div style="font-weight: 600; color: #111827;">${session.project_name || '-'}</div>
</div>
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">작업 장소</div>
<div style="font-weight: 600; color: #111827;">${session.work_location || '-'}</div>
</div>
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; grid-column: span 2;">
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">작업 내용</div>
<div style="color: #111827;">${session.work_description || '-'}</div>
</div>
${session.safety_notes ? `
<div style="padding: 0.75rem; background: #fef3c7; border-radius: 0.375rem; grid-column: span 2;">
<div style="font-size: 0.75rem; color: #92400e; margin-bottom: 0.25rem;">⚠️ 안전 특이사항</div>
<div style="color: #78350f;">${session.safety_notes}</div>
</div>
` : ''}
`;
// 팀 구성 표시
const teamMembers = document.getElementById('detailTeamMembers');
if (team.length === 0) {
teamMembers.innerHTML = '<p style="color: #6b7280; font-size: 0.875rem;">등록된 팀원이 없습니다.</p>';
} else {
teamMembers.innerHTML = team.map(member => `
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; border: 1px solid #e5e7eb;">
<div style="font-weight: 600; color: #111827; margin-bottom: 0.25rem;">${member.worker_name}</div>
<div style="font-size: 0.75rem; color: #6b7280;">${member.job_type || ''}</div>
${member.is_present ? '' : '<div style="font-size: 0.75rem; color: #ef4444; margin-top: 0.25rem;">결석</div>'}
</div>
`).join('');
}
// 안전 체크 표시
const safetyChecks = document.getElementById('detailSafetyChecks');
if (safety.length === 0) {
safetyChecks.innerHTML = '<p style="color: #6b7280; font-size: 0.875rem;">안전 체크 기록이 없습니다.</p>';
} else {
// 카테고리별 그룹화
const grouped = {};
safety.forEach(check => {
if (!grouped[check.check_category]) {
grouped[check.check_category] = [];
}
grouped[check.check_category].push(check);
});
const categoryNames = {
'PPE': '개인 보호 장비',
'EQUIPMENT': '장비 점검',
'ENVIRONMENT': '작업 환경',
'EMERGENCY': '비상 대응'
};
safetyChecks.innerHTML = Object.keys(grouped).map(category => `
<div style="margin-bottom: 1rem;">
<div style="font-weight: 600; font-size: 0.875rem; color: #374151; margin-bottom: 0.5rem; padding: 0.5rem; background: #f3f4f6; border-radius: 0.25rem;">
${categoryNames[category] || category}
</div>
<div style="display: grid; gap: 0.5rem;">
${grouped[category].map(check => `
<div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-radius: 0.25rem; ${check.is_checked ? 'background: #f0fdf4;' : 'background: #fef2f2;'}">
<span style="font-size: 1.25rem;">${check.is_checked ? '✅' : '❌'}</span>
<span style="flex: 1; font-size: 0.875rem; color: #374151;">${check.check_item}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
}
document.getElementById('detailModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
} catch (error) {
console.error('❌ TBM 상세 조회 오류:', error);
showToast('상세 정보를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
window.viewTbmSession = viewTbmSession;
// 상세보기 모달 닫기
function closeDetailModal() {
document.getElementById('detailModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
window.closeDetailModal = closeDetailModal;
// 작업 인계 모달 열기
async function openHandoverModal(sessionId) {
currentSessionId = sessionId;
// 세션 정보와 팀 구성 조회
try {
const [sessionRes, teamRes] = await Promise.all([
window.apiCall(`/tbm/sessions/${sessionId}`),
window.apiCall(`/tbm/sessions/${sessionId}/team`)
]);
const session = sessionRes?.data;
const team = teamRes?.data || [];
if (!session) {
showToast('세션 정보를 불러올 수 없습니다.', 'error');
return;
}
// 현재 세션의 팀장을 제외한 리더 목록
const toLeaderSelect = document.getElementById('toLeaderId');
const otherLeaders = allWorkers.filter(w =>
(w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
w.worker_id !== session.leader_id
);
toLeaderSelect.innerHTML = '<option value="">인수자 선택...</option>' +
otherLeaders.map(w => `
<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>
`).join('');
// 인계할 팀원 목록
const handoverTeamList = document.getElementById('handoverTeamList');
if (team.length === 0) {
handoverTeamList.innerHTML = '<p style="padding: 1rem; color: #6b7280; text-align: center;">팀 구성이 없습니다.</p>';
} else {
handoverTeamList.innerHTML = team.map(member => `
<label style="display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; cursor: pointer; border-radius: 0.25rem; transition: background 0.2s;"
onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background='white'">
<input type="checkbox"
class="handover-worker-checkbox"
value="${member.worker_id}"
checked
style="width: 16px; height: 16px; cursor: pointer;">
<span style="font-weight: 500; font-size: 0.875rem;">${member.worker_name}</span>
<span style="font-size: 0.75rem; color: #6b7280; margin-left: auto;">${member.job_type || ''}</span>
</label>
`).join('');
}
// 기본값 설정
document.getElementById('handoverSessionId').value = sessionId;
const today = new Date().toISOString().split('T')[0];
const now = new Date().toTimeString().slice(0, 5);
document.getElementById('handoverDate').value = today;
document.getElementById('handoverTime').value = now;
document.getElementById('handoverReason').value = '';
document.getElementById('handoverNotes').value = '';
document.getElementById('handoverModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
} catch (error) {
console.error('❌ 인계 모달 열기 오류:', error);
showToast('인계 정보를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
window.openHandoverModal = openHandoverModal;
// 인계 모달 닫기
function closeHandoverModal() {
document.getElementById('handoverModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
window.closeHandoverModal = closeHandoverModal;
// 작업 인계 저장
async function saveHandover() {
const sessionId = currentSessionId;
const toLeaderId = parseInt(document.getElementById('toLeaderId').value);
const reason = document.getElementById('handoverReason').value;
const handoverDate = document.getElementById('handoverDate').value;
const handoverTime = document.getElementById('handoverTime').value;
const handoverNotes = document.getElementById('handoverNotes').value;
if (!toLeaderId || !reason || !handoverDate) {
showToast('필수 항목을 입력해주세요.', 'error');
return;
}
// 인계할 작업자 목록
const workerIds = [];
document.querySelectorAll('.handover-worker-checkbox:checked').forEach(cb => {
workerIds.push(parseInt(cb.value));
});
if (workerIds.length === 0) {
showToast('인계할 팀원을 최소 1명 이상 선택해주세요.', 'error');
return;
}
try {
// 세션 정보 조회 (from_leader_id 가져오기)
const sessionRes = await window.apiCall(`/tbm/sessions/${sessionId}`);
const fromLeaderId = sessionRes?.data?.leader_id;
if (!fromLeaderId) {
showToast('세션 정보를 찾을 수 없습니다.', 'error');
return;
}
const handoverData = {
session_id: sessionId,
from_leader_id: fromLeaderId,
to_leader_id: toLeaderId,
handover_date: handoverDate,
handover_time: handoverTime,
reason: reason,
handover_notes: handoverNotes,
worker_ids: workerIds
};
const response = await window.apiCall('/tbm/handovers', 'POST', handoverData);
if (response && response.success) {
showToast('작업 인계가 요청되었습니다.', 'success');
closeHandoverModal();
} else {
throw new Error(response.message || '인계 요청에 실패했습니다.');
}
} catch (error) {
console.error('❌ 작업 인계 저장 오류:', error);
showToast('작업 인계 중 오류가 발생했습니다.', 'error');
}
}
window.saveHandover = saveHandover;
// 토스트 알림
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 = `
<div class="toast-icon">${iconMap[type] || ''}</div>
<div class="toast-message">${message}</div>
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
`;
toast.style.cssText = `
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
background: white;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
margin-bottom: 0.75rem;
min-width: 300px;
animation: slideIn 0.3s ease-out;
`;
container.appendChild(toast);
setTimeout(() => {
if (toast.parentElement) {
toast.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => toast.remove(), 300);
}
}, duration);
}