- 실시간 작업장 현황을 지도로 시각화 - 작업장 관리 페이지에서 정의한 구역 정보 활용 - TBM 작업자 및 방문자 현황 표시 주요 변경사항: - dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거) - workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현 - modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가 시각화 방식: - 인원 없음: 회색 테두리 + 작업장 이름 - 내부 작업자: 파란색 영역 + 인원 수 - 외부 방문자: 보라색 영역 + 인원 수 - 둘 다: 초록색 영역 + 총 인원 수 기술 구현: - Canvas API 기반 사각형 영역 렌더링 - map-regions API를 통한 데이터 일관성 보장 - 클릭 이벤트로 상세 정보 모달 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2316 lines
82 KiB
JavaScript
2316 lines
82 KiB
JavaScript
// tbm.js - TBM 관리 페이지 JavaScript
|
||
|
||
// 전역 변수
|
||
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';
|
||
|
||
// 새로운 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(); // 일괄 설정에서 선택된 작업자 인덱스
|
||
|
||
// ==================== 유틸리티 함수 ====================
|
||
|
||
/**
|
||
* 서울 시간대(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}`;
|
||
}
|
||
|
||
// ==================== 페이지 초기화 ====================
|
||
|
||
// 페이지 초기화
|
||
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 = getTodayKST();
|
||
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 userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||
currentUser = userInfo;
|
||
console.log('👤 로그인 사용자:', currentUser);
|
||
|
||
// 작업자 목록 로드
|
||
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) {
|
||
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');
|
||
}
|
||
}
|
||
|
||
// ==================== 탭 전환 ====================
|
||
|
||
// 탭 전환
|
||
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 = 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();
|
||
}
|
||
}
|
||
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 = getTodayKST();
|
||
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] || '';
|
||
|
||
// 작업 책임자 표시 (leader_name이 있으면 표시, 없으면 created_by_name 표시)
|
||
const leaderDisplay = session.leader_name
|
||
? `${session.leader_name} (${session.leader_job_type || '작업자'})`
|
||
: `${session.created_by_name || '작업 책임자'} (관리자)`;
|
||
|
||
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;">
|
||
${leaderDisplay}
|
||
</h3>
|
||
<p style="font-size: 0.75rem; color: #6b7280; margin: 0;">
|
||
${formatDate(session.session_date)}
|
||
</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>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 새 TBM 모달 열기
|
||
function openNewTbmModal() {
|
||
currentSessionId = null;
|
||
workerTaskList = []; // 작업자 목록 초기화
|
||
|
||
document.getElementById('modalTitle').textContent = '새 TBM 시작';
|
||
document.getElementById('sessionId').value = '';
|
||
document.getElementById('tbmForm').reset();
|
||
|
||
const today = getTodayKST();
|
||
document.getElementById('sessionDate').value = today;
|
||
|
||
// 입력자 자동 설정 (readonly)
|
||
if (currentUser && currentUser.worker_id) {
|
||
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
|
||
if (worker) {
|
||
document.getElementById('leaderName').value = `${worker.worker_name} (${worker.job_type || ''})`;
|
||
document.getElementById('leaderId').value = worker.worker_id;
|
||
}
|
||
} else if (currentUser && currentUser.name) {
|
||
// 관리자: 관리자로 표시
|
||
document.getElementById('leaderName').value = `${currentUser.name} (관리자)`;
|
||
document.getElementById('leaderId').value = '';
|
||
}
|
||
|
||
// 작업자 목록 UI 초기화
|
||
renderWorkerTaskList();
|
||
|
||
document.getElementById('tbmModal').style.display = 'flex';
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
window.openNewTbmModal = openNewTbmModal;
|
||
|
||
// 입력자 선택 드롭다운 채우기
|
||
function populateLeaderSelect() {
|
||
const leaderSelect = document.getElementById('leaderId');
|
||
if (!leaderSelect) return;
|
||
|
||
// 로그인한 사용자가 작업자와 연결되어 있는지 확인
|
||
if (currentUser && currentUser.worker_id) {
|
||
// 작업자와 연결된 경우: 자동으로 선택하고 비활성화
|
||
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
|
||
if (worker) {
|
||
leaderSelect.innerHTML = `<option value="${worker.worker_id}" selected>${worker.worker_name} (${worker.job_type || ''})</option>`;
|
||
leaderSelect.disabled = true;
|
||
console.log('✅ 입력자 자동 설정:', worker.worker_name);
|
||
} else {
|
||
// 작업자를 찾을 수 없는 경우
|
||
leaderSelect.innerHTML = '<option value="">입력자를 찾을 수 없습니다</option>';
|
||
leaderSelect.disabled = true;
|
||
}
|
||
} else {
|
||
// 관리자 계정 (worker_id가 없음): 드롭다운으로 선택 가능
|
||
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('');
|
||
leaderSelect.disabled = false;
|
||
console.log('✅ 관리자: 입력자 선택 가능');
|
||
}
|
||
}
|
||
|
||
// 프로젝트 선택 드롭다운 채우기
|
||
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('');
|
||
}
|
||
|
||
// 작업장 선택 드롭다운 채우기
|
||
function populateWorkplaceSelect() {
|
||
const workLocationSelect = document.getElementById('workLocation');
|
||
if (!workLocationSelect) return;
|
||
|
||
workLocationSelect.innerHTML = '<option value="">작업장 선택...</option>' +
|
||
allWorkplaces.map(wp => `
|
||
<option value="${wp.workplace_name}">${wp.workplace_name}${wp.location ? ' - ' + wp.location : ''}</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() {
|
||
console.log('💾 TBM 저장 시작...');
|
||
|
||
let leaderId = parseInt(document.getElementById('leaderId').value);
|
||
|
||
// 관리자 계정인 경우 leader_id를 null로 설정
|
||
if (!leaderId || isNaN(leaderId)) {
|
||
if (!currentUser.worker_id) {
|
||
console.log('📝 관리자 계정: leader_id를 NULL로 설정');
|
||
leaderId = null;
|
||
} else {
|
||
console.error('❌ 입력자 설정 오류');
|
||
showToast('입력자 정보가 올바르지 않습니다.', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
const sessionData = {
|
||
session_date: document.getElementById('sessionDate').value,
|
||
leader_id: leaderId
|
||
};
|
||
|
||
console.log('📅 세션 데이터:', sessionData);
|
||
console.log('👥 작업자 리스트:', workerTaskList);
|
||
console.log('👤 현재 사용자:', currentUser);
|
||
|
||
if (!sessionData.session_date) {
|
||
console.error('❌ 날짜 누락');
|
||
showToast('TBM 날짜를 확인해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
if (workerTaskList.length === 0) {
|
||
console.error('❌ 작업자 리스트가 비어있음');
|
||
showToast('최소 1명 이상의 작업자를 추가해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
// 필수 항목 검증 (공정, 작업, 작업장)
|
||
let hasError = false;
|
||
for (const workerData of workerTaskList) {
|
||
for (const taskLine of workerData.tasks) {
|
||
if (!taskLine.work_type_id || !taskLine.task_id || !taskLine.workplace_id) {
|
||
showToast(`${workerData.worker_name}의 공정, 작업, 작업장을 모두 선택해주세요.`, 'error');
|
||
hasError = true;
|
||
break;
|
||
}
|
||
}
|
||
if (hasError) break;
|
||
}
|
||
if (hasError) return;
|
||
|
||
// 작업자-작업 데이터를 평평하게 변환
|
||
const members = [];
|
||
for (const workerData of workerTaskList) {
|
||
for (const taskLine of workerData.tasks) {
|
||
members.push({
|
||
worker_id: workerData.worker_id,
|
||
project_id: taskLine.project_id || null,
|
||
work_type_id: taskLine.work_type_id,
|
||
task_id: taskLine.task_id,
|
||
workplace_category_id: taskLine.workplace_category_id || null,
|
||
workplace_id: taskLine.workplace_id,
|
||
work_detail: taskLine.work_detail || null,
|
||
is_present: taskLine.is_present !== undefined ? taskLine.is_present : true
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log('📤 전송할 팀 데이터:', members);
|
||
|
||
try {
|
||
const editingSessionId = document.getElementById('sessionId').value;
|
||
|
||
if (editingSessionId) {
|
||
// 수정 모드: 기존 팀원 삭제 후 재등록
|
||
console.log('📝 TBM 수정 모드:', editingSessionId);
|
||
|
||
// 기존 팀원 삭제
|
||
await window.apiCall(`/tbm/sessions/${editingSessionId}/team/clear`, 'DELETE');
|
||
|
||
// 새 팀원 일괄 추가
|
||
const teamResponse = await window.apiCall(
|
||
`/tbm/sessions/${editingSessionId}/team/batch`,
|
||
'POST',
|
||
{ members }
|
||
);
|
||
|
||
if (teamResponse && teamResponse.success) {
|
||
showToast(`TBM이 수정되었습니다 (작업자 ${workerTaskList.length}명, 작업 ${members.length}건)`, 'success');
|
||
closeTbmModal();
|
||
|
||
// 목록 새로고침
|
||
if (currentTab === 'tbm-input') {
|
||
await loadTodayOnlyTbm();
|
||
} else {
|
||
await loadTbmSessionsByDate(sessionData.session_date);
|
||
}
|
||
} else {
|
||
throw new Error(teamResponse.message || '팀원 수정에 실패했습니다.');
|
||
}
|
||
} else {
|
||
// 생성 모드: 새 TBM 세션 생성
|
||
console.log('✨ TBM 생성 모드');
|
||
|
||
const response = await window.apiCall('/tbm/sessions', 'POST', 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 }
|
||
);
|
||
|
||
if (teamResponse && teamResponse.success) {
|
||
showToast(`TBM이 생성되었습니다 (작업자 ${workerTaskList.length}명, 작업 ${members.length}건)`, 'success');
|
||
closeTbmModal();
|
||
|
||
// 목록 새로고침
|
||
if (currentTab === 'tbm-input') {
|
||
await loadTodayOnlyTbm();
|
||
} else {
|
||
await loadTbmSessionsByDate(sessionData.session_date);
|
||
}
|
||
} else {
|
||
throw new Error(teamResponse.message || '팀원 추가에 실패했습니다.');
|
||
}
|
||
} else {
|
||
throw new Error(response.message || '저장에 실패했습니다.');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ TBM 세션 저장 오류:', error);
|
||
showToast('TBM 세션 저장 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
}
|
||
window.saveTbmSession = saveTbmSession;
|
||
|
||
// ==================== 작업자 관리 ====================
|
||
|
||
// UUID 생성 함수
|
||
function generateUUID() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
// 작업자 카드 리스트 렌더링
|
||
function renderWorkerTaskList() {
|
||
const listContainer = document.getElementById('workerTaskList');
|
||
const emptyState = document.getElementById('workerListEmpty');
|
||
|
||
if (workerTaskList.length === 0) {
|
||
if (emptyState) emptyState.style.display = 'flex';
|
||
listContainer.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
if (emptyState) emptyState.style.display = 'none';
|
||
|
||
listContainer.innerHTML = workerTaskList.map((workerData, workerIndex) => {
|
||
return `
|
||
<div style="border: 2px solid #e5e7eb; border-radius: 0.5rem; padding: 1rem; background: white;">
|
||
<!-- 작업자 헤더 -->
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid #e5e7eb;">
|
||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||
<span style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">👤 ${workerData.worker_name}</span>
|
||
<span style="padding: 0.125rem 0.5rem; background: #dbeafe; color: #1e40af; border-radius: 0.25rem; font-size: 0.75rem;">
|
||
${workerData.job_type || '작업자'}
|
||
</span>
|
||
</div>
|
||
<button type="button" onclick="removeWorkerFromList(${workerIndex})" class="btn btn-sm btn-danger">
|
||
<span style="font-size: 1rem;">✕ 작업자 제거</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 작업 라인들 -->
|
||
${workerData.tasks.map((taskLine, taskIndex) => renderTaskLine(workerData, workerIndex, taskLine, taskIndex)).join('')}
|
||
|
||
<!-- 작업 추가 버튼 -->
|
||
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px dashed #d1d5db;">
|
||
<button type="button" onclick="addTaskLineToWorker(${workerIndex})" class="btn btn-sm btn-secondary" style="width: 100%;">
|
||
<span style="font-size: 1.2rem; margin-right: 0.25rem;">+</span>
|
||
이 작업자의 추가 작업 등록
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// 작업 라인 렌더링
|
||
function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||
const project = allProjects.find(p => p.project_id === taskLine.project_id);
|
||
const workType = allWorkTypes.find(wt => wt.id === taskLine.work_type_id);
|
||
const task = allTasks.find(t => t.task_id === taskLine.task_id);
|
||
|
||
const projectText = project ? project.project_name : '프로젝트 선택';
|
||
const workTypeText = workType ? workType.name : '공정 선택 *';
|
||
const taskText = task ? task.task_name : '작업 선택 *';
|
||
const workplaceText = taskLine.workplace_name
|
||
? `${taskLine.workplace_category_name || ''} → ${taskLine.workplace_name}`
|
||
: '작업장 선택 *';
|
||
|
||
return `
|
||
<div style="padding: 0.75rem; margin-bottom: 0.5rem; background: #f9fafb; border-radius: 0.5rem; border: 1px solid #e5e7eb;">
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-bottom: 0.5rem;">
|
||
<!-- 프로젝트 선택 -->
|
||
<button type="button"
|
||
onclick="openItemSelect('project', ${workerIndex}, ${taskIndex})"
|
||
class="btn btn-sm ${project ? 'btn-primary' : 'btn-secondary'}"
|
||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
|
||
📁 ${projectText}
|
||
</button>
|
||
|
||
<!-- 작업장 선택 -->
|
||
<button type="button"
|
||
onclick="openWorkplaceSelect(${workerIndex}, ${taskIndex})"
|
||
class="btn btn-sm ${taskLine.workplace_id ? 'btn-primary' : 'btn-secondary'}"
|
||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
|
||
📍 ${workplaceText}
|
||
</button>
|
||
|
||
<!-- 공정 선택 -->
|
||
<button type="button"
|
||
onclick="openItemSelect('workType', ${workerIndex}, ${taskIndex})"
|
||
class="btn btn-sm ${workType ? 'btn-primary' : 'btn-secondary'}"
|
||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
|
||
⚙️ ${workTypeText}
|
||
</button>
|
||
|
||
<!-- 작업 선택 -->
|
||
<button type="button"
|
||
onclick="openItemSelect('task', ${workerIndex}, ${taskIndex})"
|
||
class="btn btn-sm ${task ? 'btn-primary' : 'btn-secondary'}"
|
||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;"
|
||
${!taskLine.work_type_id ? 'disabled' : ''}>
|
||
🔧 ${taskText}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 작업 라인 제거 버튼 -->
|
||
${workerData.tasks.length > 1 ? `
|
||
<button type="button" onclick="removeTaskLine(${workerIndex}, ${taskIndex})"
|
||
class="btn btn-sm btn-danger" style="width: 100%; font-size: 0.8rem;">
|
||
<span style="margin-right: 0.25rem;">−</span> 이 작업 라인 제거
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
window.renderWorkerTaskList = renderWorkerTaskList;
|
||
|
||
// 작업자 선택 모달 열기
|
||
function openWorkerSelectionModal() {
|
||
selectedWorkersInModal.clear();
|
||
|
||
const workerCardGrid = document.getElementById('workerCardGrid');
|
||
if (!workerCardGrid) return;
|
||
|
||
// 이미 추가된 작업자 ID 세트
|
||
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id));
|
||
|
||
workerCardGrid.innerHTML = allWorkers.map(worker => {
|
||
const isAdded = addedWorkerIds.has(worker.worker_id);
|
||
return `
|
||
<div id="worker-card-${worker.worker_id}"
|
||
onclick="toggleWorkerSelection(${worker.worker_id})"
|
||
style="padding: 1rem; border: 2px solid ${isAdded ? '#d1d5db' : '#e5e7eb'}; border-radius: 0.5rem; cursor: ${isAdded ? 'not-allowed' : 'pointer'}; background: ${isAdded ? '#f3f4f6' : 'white'}; opacity: ${isAdded ? '0.5' : '1'}; transition: all 0.2s;">
|
||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||
${isAdded ? '✓' : '☐'}
|
||
<span style="font-weight: 600; font-size: 0.95rem;">${worker.worker_name}</span>
|
||
</div>
|
||
<div style="font-size: 0.8rem; color: #6b7280;">
|
||
${worker.job_type || '작업자'}${worker.department ? ' · ' + worker.department : ''}
|
||
</div>
|
||
${isAdded ? '<div style="font-size: 0.75rem; color: #9ca3af; margin-top: 0.25rem;">이미 추가됨</div>' : ''}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
document.getElementById('workerSelectionModal').style.display = 'flex';
|
||
}
|
||
window.openWorkerSelectionModal = openWorkerSelectionModal;
|
||
|
||
// 작업자 선택 토글
|
||
function toggleWorkerSelection(workerId) {
|
||
// 이미 추가된 작업자는 선택 불가
|
||
const alreadyAdded = workerTaskList.some(w => w.worker_id === workerId);
|
||
if (alreadyAdded) return;
|
||
|
||
const card = document.getElementById(`worker-card-${workerId}`);
|
||
if (!card) return;
|
||
|
||
if (selectedWorkersInModal.has(workerId)) {
|
||
selectedWorkersInModal.delete(workerId);
|
||
card.style.borderColor = '#e5e7eb';
|
||
card.style.background = 'white';
|
||
card.innerHTML = card.innerHTML.replace('☑', '☐');
|
||
} else {
|
||
selectedWorkersInModal.add(workerId);
|
||
card.style.borderColor = '#3b82f6';
|
||
card.style.background = '#eff6ff';
|
||
card.innerHTML = card.innerHTML.replace('☐', '☑');
|
||
}
|
||
}
|
||
window.toggleWorkerSelection = toggleWorkerSelection;
|
||
|
||
// 전체 선택
|
||
function selectAllWorkersInModal() {
|
||
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id));
|
||
allWorkers.forEach(worker => {
|
||
if (!addedWorkerIds.has(worker.worker_id)) {
|
||
selectedWorkersInModal.add(worker.worker_id);
|
||
const card = document.getElementById(`worker-card-${worker.worker_id}`);
|
||
if (card) {
|
||
card.style.borderColor = '#3b82f6';
|
||
card.style.background = '#eff6ff';
|
||
card.innerHTML = card.innerHTML.replace('☐', '☑');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
window.selectAllWorkersInModal = selectAllWorkersInModal;
|
||
|
||
// 전체 해제
|
||
function deselectAllWorkersInModal() {
|
||
selectedWorkersInModal.forEach(workerId => {
|
||
const card = document.getElementById(`worker-card-${workerId}`);
|
||
if (card) {
|
||
card.style.borderColor = '#e5e7eb';
|
||
card.style.background = 'white';
|
||
card.innerHTML = card.innerHTML.replace('☑', '☐');
|
||
}
|
||
});
|
||
selectedWorkersInModal.clear();
|
||
}
|
||
window.deselectAllWorkersInModal = deselectAllWorkersInModal;
|
||
|
||
// 작업자 선택 확정
|
||
function confirmWorkerSelection() {
|
||
if (selectedWorkersInModal.size === 0) {
|
||
showToast('작업자를 선택해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
selectedWorkersInModal.forEach(workerId => {
|
||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||
if (worker) {
|
||
workerTaskList.push({
|
||
worker_id: worker.worker_id,
|
||
worker_name: worker.worker_name,
|
||
job_type: worker.job_type,
|
||
tasks: [
|
||
{
|
||
task_line_id: generateUUID(),
|
||
project_id: null,
|
||
work_type_id: null,
|
||
task_id: null,
|
||
workplace_category_id: null,
|
||
workplace_id: null,
|
||
workplace_category_name: '',
|
||
workplace_name: '',
|
||
work_detail: null,
|
||
is_present: true
|
||
}
|
||
]
|
||
});
|
||
}
|
||
});
|
||
|
||
renderWorkerTaskList();
|
||
closeWorkerSelectionModal();
|
||
showToast(`${selectedWorkersInModal.size}명의 작업자가 추가되었습니다.`, 'success');
|
||
}
|
||
window.confirmWorkerSelection = confirmWorkerSelection;
|
||
|
||
// 작업자 선택 모달 닫기
|
||
function closeWorkerSelectionModal() {
|
||
document.getElementById('workerSelectionModal').style.display = 'none';
|
||
selectedWorkersInModal.clear();
|
||
}
|
||
window.closeWorkerSelectionModal = closeWorkerSelectionModal;
|
||
|
||
// 작업자에 작업 라인 추가
|
||
function addTaskLineToWorker(workerIndex) {
|
||
workerTaskList[workerIndex].tasks.push({
|
||
task_line_id: generateUUID(),
|
||
project_id: null,
|
||
work_type_id: null,
|
||
task_id: null,
|
||
workplace_category_id: null,
|
||
workplace_id: null,
|
||
workplace_category_name: '',
|
||
workplace_name: '',
|
||
work_detail: null,
|
||
is_present: true
|
||
});
|
||
renderWorkerTaskList();
|
||
showToast('작업 라인이 추가되었습니다.', 'success');
|
||
}
|
||
window.addTaskLineToWorker = addTaskLineToWorker;
|
||
|
||
// 작업 라인 제거
|
||
function removeTaskLine(workerIndex, taskIndex) {
|
||
workerTaskList[workerIndex].tasks.splice(taskIndex, 1);
|
||
renderWorkerTaskList();
|
||
showToast('작업 라인이 제거되었습니다.', 'info');
|
||
}
|
||
window.removeTaskLine = removeTaskLine;
|
||
|
||
// 작업자 제거
|
||
function removeWorkerFromList(workerIndex) {
|
||
const workerName = workerTaskList[workerIndex].worker_name;
|
||
workerTaskList.splice(workerIndex, 1);
|
||
renderWorkerTaskList();
|
||
showToast(`${workerName}이(가) 제거되었습니다.`, 'info');
|
||
}
|
||
window.removeWorkerFromList = removeWorkerFromList;
|
||
|
||
// ==================== 일괄 설정 ====================
|
||
|
||
// 일괄 설정 모달 열기
|
||
function openBulkSettingModal() {
|
||
if (workerTaskList.length === 0) {
|
||
showToast('먼저 작업자를 선택해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
// 작업자 선택 영역 초기화
|
||
bulkSelectedWorkers.clear();
|
||
renderBulkWorkerSelection();
|
||
|
||
// 작업 정보 초기화
|
||
document.getElementById('bulkProjectId').value = '';
|
||
document.getElementById('bulkWorkTypeId').value = '';
|
||
document.getElementById('bulkTaskId').value = '';
|
||
document.getElementById('bulkWorkplaceCategoryId').value = '';
|
||
document.getElementById('bulkWorkplaceId').value = '';
|
||
|
||
document.getElementById('bulkProjectBtn').textContent = '📁 프로젝트 선택';
|
||
document.getElementById('bulkProjectBtn').classList.remove('btn-primary');
|
||
document.getElementById('bulkProjectBtn').classList.add('btn-secondary');
|
||
|
||
document.getElementById('bulkWorkTypeBtn').textContent = '⚙️ 공정 선택';
|
||
document.getElementById('bulkWorkTypeBtn').classList.remove('btn-primary');
|
||
document.getElementById('bulkWorkTypeBtn').classList.add('btn-secondary');
|
||
|
||
document.getElementById('bulkTaskBtn').textContent = '🔧 작업 선택';
|
||
document.getElementById('bulkTaskBtn').classList.remove('btn-primary');
|
||
document.getElementById('bulkTaskBtn').classList.add('btn-secondary');
|
||
document.getElementById('bulkTaskBtn').disabled = true;
|
||
|
||
document.getElementById('bulkWorkplaceBtn').textContent = '📍 작업장 선택';
|
||
document.getElementById('bulkWorkplaceBtn').classList.remove('btn-primary');
|
||
document.getElementById('bulkWorkplaceBtn').classList.add('btn-secondary');
|
||
|
||
document.getElementById('bulkSettingModal').style.display = 'flex';
|
||
}
|
||
window.openBulkSettingModal = openBulkSettingModal;
|
||
|
||
// 일괄 설정용 작업자 선택 영역 렌더링
|
||
function renderBulkWorkerSelection() {
|
||
const container = document.getElementById('bulkWorkerSelection');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = workerTaskList.map((workerData, index) => {
|
||
const isSelected = bulkSelectedWorkers.has(index);
|
||
return `
|
||
<div onclick="toggleBulkWorkerSelection(${index})"
|
||
style="padding: 0.5rem; border: 2px solid ${isSelected ? '#3b82f6' : '#e5e7eb'}; border-radius: 0.5rem; cursor: pointer; background: ${isSelected ? '#eff6ff' : 'white'}; transition: all 0.2s; display: flex; align-items: center; gap: 0.5rem;">
|
||
<input type="checkbox" ${isSelected ? 'checked' : ''} style="pointer-events: none;">
|
||
<span style="font-size: 0.875rem; font-weight: ${isSelected ? '600' : '400'}; color: ${isSelected ? '#1e40af' : '#374151'};">
|
||
${workerData.worker_name}
|
||
</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
window.renderBulkWorkerSelection = renderBulkWorkerSelection;
|
||
|
||
// 일괄 설정용 작업자 선택 토글
|
||
function toggleBulkWorkerSelection(workerIndex) {
|
||
if (bulkSelectedWorkers.has(workerIndex)) {
|
||
bulkSelectedWorkers.delete(workerIndex);
|
||
} else {
|
||
bulkSelectedWorkers.add(workerIndex);
|
||
}
|
||
renderBulkWorkerSelection();
|
||
}
|
||
window.toggleBulkWorkerSelection = toggleBulkWorkerSelection;
|
||
|
||
// 일괄 설정용 전체 선택
|
||
function selectAllForBulk() {
|
||
workerTaskList.forEach((_, index) => {
|
||
bulkSelectedWorkers.add(index);
|
||
});
|
||
renderBulkWorkerSelection();
|
||
}
|
||
window.selectAllForBulk = selectAllForBulk;
|
||
|
||
// 일괄 설정용 전체 해제
|
||
function deselectAllForBulk() {
|
||
bulkSelectedWorkers.clear();
|
||
renderBulkWorkerSelection();
|
||
}
|
||
window.deselectAllForBulk = deselectAllForBulk;
|
||
|
||
// 일괄 설정 모달 닫기
|
||
function closeBulkSettingModal() {
|
||
document.getElementById('bulkSettingModal').style.display = 'none';
|
||
isBulkMode = false;
|
||
}
|
||
window.closeBulkSettingModal = closeBulkSettingModal;
|
||
|
||
// 일괄 설정용 항목 선택
|
||
function openBulkItemSelect(type) {
|
||
isBulkMode = true;
|
||
const modal = document.getElementById('itemSelectModal');
|
||
const titleEl = document.getElementById('itemSelectModalTitle');
|
||
const listEl = document.getElementById('itemSelectList');
|
||
|
||
let title = '';
|
||
let items = [];
|
||
|
||
if (type === 'project') {
|
||
title = '프로젝트 선택';
|
||
// 활성 프로젝트만 표시
|
||
const activeProjects = allProjects.filter(p =>
|
||
p.is_active === 1 || p.is_active === true || p.is_active === '1'
|
||
);
|
||
items = activeProjects.map(p => ({
|
||
id: p.project_id,
|
||
name: p.project_name,
|
||
icon: '📁'
|
||
}));
|
||
} else if (type === 'workType') {
|
||
title = '공정 선택';
|
||
items = allWorkTypes.map(wt => ({
|
||
id: wt.id,
|
||
name: wt.name,
|
||
icon: '⚙️'
|
||
}));
|
||
} else if (type === 'task') {
|
||
title = '작업 선택';
|
||
const currentWorkTypeId = parseInt(document.getElementById('bulkWorkTypeId').value);
|
||
if (!currentWorkTypeId) {
|
||
showToast('공정을 먼저 선택해주세요.', 'error');
|
||
return;
|
||
}
|
||
const filteredTasks = allTasks.filter(t => t.work_type_id === currentWorkTypeId);
|
||
items = filteredTasks.map(t => ({
|
||
id: t.task_id,
|
||
name: t.task_name,
|
||
icon: '🔧'
|
||
}));
|
||
}
|
||
|
||
titleEl.textContent = title;
|
||
listEl.innerHTML = items.length > 0 ? items.map(item => `
|
||
<button type="button"
|
||
onclick="selectBulkItem('${type}', ${item.id}, '${item.name.replace(/'/g, "\\'")}')"
|
||
class="btn btn-secondary"
|
||
style="text-align: left; justify-content: flex-start; padding: 0.75rem;">
|
||
${item.icon} ${item.name}
|
||
</button>
|
||
`).join('') : '<div style="text-align: center; padding: 2rem; color: #9ca3af;">선택 가능한 항목이 없습니다</div>';
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
window.openBulkItemSelect = openBulkItemSelect;
|
||
|
||
// 일괄 설정용 항목 선택 처리
|
||
function selectBulkItem(type, itemId, itemName) {
|
||
if (type === 'project') {
|
||
document.getElementById('bulkProjectId').value = itemId;
|
||
document.getElementById('bulkProjectBtn').textContent = `📁 ${itemName}`;
|
||
document.getElementById('bulkProjectBtn').classList.remove('btn-secondary');
|
||
document.getElementById('bulkProjectBtn').classList.add('btn-primary');
|
||
} else if (type === 'workType') {
|
||
document.getElementById('bulkWorkTypeId').value = itemId;
|
||
document.getElementById('bulkWorkTypeBtn').textContent = `⚙️ ${itemName}`;
|
||
document.getElementById('bulkWorkTypeBtn').classList.remove('btn-secondary');
|
||
document.getElementById('bulkWorkTypeBtn').classList.add('btn-primary');
|
||
|
||
// 공정 변경 시 작업 초기화
|
||
document.getElementById('bulkTaskId').value = '';
|
||
document.getElementById('bulkTaskBtn').textContent = '🔧 작업 선택';
|
||
document.getElementById('bulkTaskBtn').classList.remove('btn-primary');
|
||
document.getElementById('bulkTaskBtn').classList.add('btn-secondary');
|
||
document.getElementById('bulkTaskBtn').disabled = false;
|
||
} else if (type === 'task') {
|
||
document.getElementById('bulkTaskId').value = itemId;
|
||
document.getElementById('bulkTaskBtn').textContent = `🔧 ${itemName}`;
|
||
document.getElementById('bulkTaskBtn').classList.remove('btn-secondary');
|
||
document.getElementById('bulkTaskBtn').classList.add('btn-primary');
|
||
}
|
||
|
||
closeItemSelectModal();
|
||
isBulkMode = false;
|
||
}
|
||
window.selectBulkItem = selectBulkItem;
|
||
|
||
// 일괄 설정용 작업장 선택
|
||
function openBulkWorkplaceSelect() {
|
||
isBulkMode = true;
|
||
loadWorkplaceCategories();
|
||
document.getElementById('workplaceSelectModal').style.display = 'flex';
|
||
}
|
||
window.openBulkWorkplaceSelect = openBulkWorkplaceSelect;
|
||
|
||
// 일괄 설정 적용
|
||
function applyBulkSettings() {
|
||
if (bulkSelectedWorkers.size === 0) {
|
||
showToast('작업자를 선택해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
const projectId = document.getElementById('bulkProjectId').value;
|
||
const workTypeId = document.getElementById('bulkWorkTypeId').value;
|
||
const taskId = document.getElementById('bulkTaskId').value;
|
||
const workplaceCategoryId = document.getElementById('bulkWorkplaceCategoryId').value;
|
||
const workplaceId = document.getElementById('bulkWorkplaceId').value;
|
||
|
||
if (!workTypeId || !taskId || !workplaceId) {
|
||
showToast('공정, 작업, 작업장은 필수 항목입니다.', 'error');
|
||
return;
|
||
}
|
||
|
||
// 선택된 작업자들의 첫 번째 작업 라인에 적용
|
||
let appliedCount = 0;
|
||
bulkSelectedWorkers.forEach(workerIndex => {
|
||
const workerData = workerTaskList[workerIndex];
|
||
if (workerData && workerData.tasks.length > 0) {
|
||
workerData.tasks[0].project_id = projectId ? parseInt(projectId) : null;
|
||
workerData.tasks[0].work_type_id = parseInt(workTypeId);
|
||
workerData.tasks[0].task_id = parseInt(taskId);
|
||
workerData.tasks[0].workplace_category_id = workplaceCategoryId ? parseInt(workplaceCategoryId) : null;
|
||
workerData.tasks[0].workplace_id = parseInt(workplaceId);
|
||
workerData.tasks[0].workplace_category_name = selectedCategoryName;
|
||
workerData.tasks[0].workplace_name = selectedWorkplaceName;
|
||
appliedCount++;
|
||
}
|
||
});
|
||
|
||
renderWorkerTaskList();
|
||
closeBulkSettingModal();
|
||
showToast(`${appliedCount}명의 작업자에게 일괄 설정이 적용되었습니다.`, 'success');
|
||
}
|
||
window.applyBulkSettings = applyBulkSettings;
|
||
|
||
// ==================== 항목 선택 (프로젝트/공정/작업) ====================
|
||
|
||
// 항목 선택 모달 열기
|
||
function openItemSelect(type, workerIndex, taskIndex) {
|
||
currentEditingTaskLine = { workerIndex, taskIndex };
|
||
|
||
const modal = document.getElementById('itemSelectModal');
|
||
const titleEl = document.getElementById('itemSelectModalTitle');
|
||
const listEl = document.getElementById('itemSelectList');
|
||
|
||
let title = '';
|
||
let items = [];
|
||
|
||
if (type === 'project') {
|
||
title = '프로젝트 선택';
|
||
// 활성 프로젝트만 표시
|
||
const activeProjects = allProjects.filter(p =>
|
||
p.is_active === 1 || p.is_active === true || p.is_active === '1'
|
||
);
|
||
items = activeProjects.map(p => ({
|
||
id: p.project_id,
|
||
name: p.project_name,
|
||
icon: '📁'
|
||
}));
|
||
} else if (type === 'workType') {
|
||
title = '공정 선택';
|
||
items = allWorkTypes.map(wt => ({
|
||
id: wt.id,
|
||
name: wt.name,
|
||
icon: '⚙️'
|
||
}));
|
||
} else if (type === 'task') {
|
||
title = '작업 선택';
|
||
const currentWorkTypeId = workerTaskList[workerIndex].tasks[taskIndex].work_type_id;
|
||
if (!currentWorkTypeId) {
|
||
showToast('공정을 먼저 선택해주세요.', 'error');
|
||
return;
|
||
}
|
||
const filteredTasks = allTasks.filter(t => t.work_type_id === currentWorkTypeId);
|
||
items = filteredTasks.map(t => ({
|
||
id: t.task_id,
|
||
name: t.task_name,
|
||
icon: '🔧'
|
||
}));
|
||
}
|
||
|
||
titleEl.textContent = title;
|
||
listEl.innerHTML = items.length > 0 ? items.map(item => `
|
||
<button type="button"
|
||
onclick="selectItem('${type}', ${item.id})"
|
||
class="btn btn-secondary"
|
||
style="text-align: left; justify-content: flex-start; padding: 0.75rem;">
|
||
${item.icon} ${item.name}
|
||
</button>
|
||
`).join('') : '<div style="text-align: center; padding: 2rem; color: #9ca3af;">선택 가능한 항목이 없습니다</div>';
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
window.openItemSelect = openItemSelect;
|
||
|
||
// 항목 선택
|
||
function selectItem(type, itemId) {
|
||
// 일괄 모드면 여기서 처리하지 않음
|
||
if (isBulkMode) return;
|
||
|
||
if (!currentEditingTaskLine) return;
|
||
|
||
const { workerIndex, taskIndex } = currentEditingTaskLine;
|
||
const taskLine = workerTaskList[workerIndex].tasks[taskIndex];
|
||
|
||
if (type === 'project') {
|
||
taskLine.project_id = itemId;
|
||
} else if (type === 'workType') {
|
||
taskLine.work_type_id = itemId;
|
||
// 공정 변경 시 작업 초기화
|
||
taskLine.task_id = null;
|
||
} else if (type === 'task') {
|
||
taskLine.task_id = itemId;
|
||
}
|
||
|
||
renderWorkerTaskList();
|
||
closeItemSelectModal();
|
||
}
|
||
window.selectItem = selectItem;
|
||
|
||
// 항목 선택 모달 닫기
|
||
function closeItemSelectModal() {
|
||
document.getElementById('itemSelectModal').style.display = 'none';
|
||
currentEditingTaskLine = null;
|
||
}
|
||
window.closeItemSelectModal = closeItemSelectModal;
|
||
|
||
// ==================== 작업장 2단계 선택 ====================
|
||
|
||
// 작업장 선택 모달 열기 (작업 라인용)
|
||
async function openWorkplaceSelect(workerIndex, taskIndex) {
|
||
currentEditingTaskLine = { workerIndex, taskIndex };
|
||
await loadWorkplaceCategories();
|
||
document.getElementById('workplaceSelectModal').style.display = 'flex';
|
||
}
|
||
window.openWorkplaceSelect = openWorkplaceSelect;
|
||
|
||
// 작업장 선택 모달 닫기
|
||
function closeWorkplaceSelectModal() {
|
||
document.getElementById('workplaceSelectModal').style.display = 'none';
|
||
document.getElementById('workplaceSelectionArea').style.display = 'none';
|
||
document.getElementById('layoutMapArea').style.display = 'none';
|
||
document.getElementById('workplaceList').style.display = 'none';
|
||
currentEditingTaskLine = null;
|
||
selectedCategory = null;
|
||
selectedWorkplace = null;
|
||
selectedCategoryName = '';
|
||
selectedWorkplaceName = '';
|
||
mapCanvas = null;
|
||
mapCtx = null;
|
||
mapImage = null;
|
||
mapRegions = [];
|
||
}
|
||
window.closeWorkplaceSelectModal = closeWorkplaceSelectModal;
|
||
|
||
// 공장 카테고리 로드
|
||
async function loadWorkplaceCategories() {
|
||
const categoryList = document.getElementById('categoryList');
|
||
if (!categoryList) return;
|
||
|
||
if (allWorkplaceCategories.length === 0) {
|
||
categoryList.innerHTML = '<div style="color: #9ca3af; text-align: center; padding: 2rem;">등록된 공장이 없습니다</div>';
|
||
return;
|
||
}
|
||
|
||
categoryList.innerHTML = allWorkplaceCategories.map(category => `
|
||
<button type="button"
|
||
onclick="selectCategory(${category.category_id}, '${category.category_name}')"
|
||
class="btn btn-secondary"
|
||
id="category-${category.category_id}"
|
||
style="text-align: left; justify-content: flex-start;">
|
||
🏭 ${category.category_name}
|
||
</button>
|
||
`).join('');
|
||
}
|
||
window.loadWorkplaceCategories = loadWorkplaceCategories;
|
||
|
||
// 공장 카테고리 선택
|
||
async function selectCategory(categoryId, categoryName) {
|
||
selectedCategory = categoryId;
|
||
selectedCategoryName = categoryName;
|
||
selectedWorkplace = null;
|
||
selectedWorkplaceName = '';
|
||
|
||
// 카테고리 버튼 스타일 업데이트
|
||
document.querySelectorAll('[id^="category-"]').forEach(btn => {
|
||
btn.classList.remove('btn-primary');
|
||
btn.classList.add('btn-secondary');
|
||
});
|
||
const selectedBtn = document.getElementById(`category-${categoryId}`);
|
||
if (selectedBtn) {
|
||
selectedBtn.classList.remove('btn-secondary');
|
||
selectedBtn.classList.add('btn-primary');
|
||
}
|
||
|
||
// 작업장 선택 영역 표시
|
||
document.getElementById('workplaceSelectionArea').style.display = 'block';
|
||
|
||
// 해당 카테고리 정보 가져오기
|
||
const category = allWorkplaceCategories.find(c => c.category_id === categoryId);
|
||
|
||
// 지도 또는 리스트 로드
|
||
if (category && category.layout_image) {
|
||
// 지도가 있는 경우 - 지도 영역 표시
|
||
await loadWorkplaceMap(categoryId, category.layout_image);
|
||
document.getElementById('layoutMapArea').style.display = 'block';
|
||
} else {
|
||
// 지도가 없는 경우 - 리스트만 표시
|
||
document.getElementById('layoutMapArea').style.display = 'none';
|
||
document.getElementById('workplaceList').style.display = 'flex';
|
||
document.getElementById('toggleListBtn').style.display = 'none';
|
||
}
|
||
|
||
// 해당 카테고리의 작업장 리스트 로드 (오류 대비용)
|
||
await loadWorkplacesByCategory(categoryId);
|
||
|
||
// 선택 완료 버튼 비활성화 (작업장 선택 필요)
|
||
document.getElementById('confirmWorkplaceBtn').disabled = true;
|
||
}
|
||
window.selectCategory = selectCategory;
|
||
|
||
// 카테고리별 작업장 로드
|
||
async function loadWorkplacesByCategory(categoryId) {
|
||
const workplaceList = document.getElementById('workplaceList');
|
||
if (!workplaceList) return;
|
||
|
||
try {
|
||
const response = await window.apiCall(`/workplaces?category_id=${categoryId}`);
|
||
if (!response || !response.success || !response.data || response.data.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}')"
|
||
class="btn btn-secondary"
|
||
id="workplace-${workplace.workplace_id}"
|
||
style="text-align: left; justify-content: flex-start;">
|
||
📍 ${workplace.workplace_name}
|
||
</button>
|
||
`).join('');
|
||
} catch (error) {
|
||
console.error('❌ 작업장 로드 오류:', error);
|
||
workplaceList.innerHTML = '<div style="color: #ef4444; text-align: center; padding: 2rem;">작업장을 불러오는 중 오류가 발생했습니다</div>';
|
||
}
|
||
}
|
||
window.loadWorkplacesByCategory = loadWorkplacesByCategory;
|
||
|
||
// 작업장 선택
|
||
function selectWorkplace(workplaceId, workplaceName) {
|
||
selectedWorkplace = workplaceId;
|
||
selectedWorkplaceName = workplaceName;
|
||
|
||
// 작업장 버튼 스타일 업데이트 (리스트)
|
||
document.querySelectorAll('[id^="workplace-"]').forEach(btn => {
|
||
btn.classList.remove('btn-primary');
|
||
btn.classList.add('btn-secondary');
|
||
});
|
||
const selectedBtn = document.getElementById(`workplace-${workplaceId}`);
|
||
if (selectedBtn) {
|
||
selectedBtn.classList.remove('btn-secondary');
|
||
selectedBtn.classList.add('btn-primary');
|
||
}
|
||
|
||
// 지도 업데이트 (지도가 로드되어 있는 경우)
|
||
if (mapCanvas && mapCtx && mapImage) {
|
||
drawWorkplaceMap();
|
||
}
|
||
|
||
// 선택 완료 버튼 활성화
|
||
document.getElementById('confirmWorkplaceBtn').disabled = false;
|
||
}
|
||
window.selectWorkplace = selectWorkplace;
|
||
|
||
// 작업장 선택 확정
|
||
function confirmWorkplaceSelection() {
|
||
if (!selectedCategory || !selectedWorkplace) {
|
||
showToast('공장과 작업장을 모두 선택해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
// 일괄 모드인 경우
|
||
if (isBulkMode) {
|
||
document.getElementById('bulkWorkplaceCategoryId').value = selectedCategory;
|
||
document.getElementById('bulkWorkplaceId').value = selectedWorkplace;
|
||
document.getElementById('bulkWorkplaceBtn').textContent = `📍 ${selectedCategoryName} → ${selectedWorkplaceName}`;
|
||
document.getElementById('bulkWorkplaceBtn').classList.remove('btn-secondary');
|
||
document.getElementById('bulkWorkplaceBtn').classList.add('btn-primary');
|
||
closeWorkplaceSelectModal();
|
||
isBulkMode = false;
|
||
showToast('작업장이 선택되었습니다.', 'success');
|
||
return;
|
||
}
|
||
|
||
// 현재 편집 중인 작업 라인에 저장
|
||
if (currentEditingTaskLine) {
|
||
const { workerIndex, taskIndex } = currentEditingTaskLine;
|
||
const taskLine = workerTaskList[workerIndex].tasks[taskIndex];
|
||
taskLine.workplace_category_id = selectedCategory;
|
||
taskLine.workplace_id = selectedWorkplace;
|
||
taskLine.workplace_category_name = selectedCategoryName;
|
||
taskLine.workplace_name = selectedWorkplaceName;
|
||
renderWorkerTaskList();
|
||
}
|
||
|
||
closeWorkplaceSelectModal();
|
||
showToast('작업장이 선택되었습니다.', 'success');
|
||
}
|
||
window.confirmWorkplaceSelection = confirmWorkplaceSelection;
|
||
|
||
// 리스트 토글 함수
|
||
function toggleWorkplaceList() {
|
||
const list = document.getElementById('workplaceList');
|
||
const icon = document.getElementById('toggleListIcon');
|
||
const btn = document.getElementById('toggleListBtn');
|
||
|
||
if (list.style.display === 'none' || list.style.display === '') {
|
||
list.style.display = 'flex';
|
||
icon.textContent = '▲';
|
||
btn.textContent = ' 리스트 닫기';
|
||
btn.insertBefore(icon, btn.firstChild);
|
||
} else {
|
||
list.style.display = 'none';
|
||
icon.textContent = '▼';
|
||
btn.textContent = ' 리스트 보기';
|
||
btn.insertBefore(icon, btn.firstChild);
|
||
}
|
||
}
|
||
window.toggleWorkplaceList = toggleWorkplaceList;
|
||
|
||
// 작업장 지도 로드 및 렌더링
|
||
let mapRegions = []; // 현재 로드된 지도 영역들
|
||
let mapCanvas = null;
|
||
let mapCtx = null;
|
||
let mapImage = null;
|
||
|
||
async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
||
try {
|
||
mapCanvas = document.getElementById('workplaceMapCanvas');
|
||
if (!mapCanvas) return;
|
||
|
||
mapCtx = mapCanvas.getContext('2d');
|
||
|
||
// 이미지 URL 생성
|
||
const baseUrl = window.API_BASE_URL || 'http://localhost:20005';
|
||
const apiBaseUrl = baseUrl.replace('/api', ''); // /api 제거
|
||
const fullImageUrl = layoutImagePath.startsWith('http')
|
||
? layoutImagePath
|
||
: `${apiBaseUrl}${layoutImagePath}`;
|
||
|
||
console.log('🖼️ 이미지 로드 시도:', fullImageUrl);
|
||
|
||
// 지도 영역 데이터 로드
|
||
const regionsResponse = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`);
|
||
if (regionsResponse && regionsResponse.success) {
|
||
mapRegions = regionsResponse.data || [];
|
||
} else {
|
||
mapRegions = [];
|
||
}
|
||
|
||
// 이미지 로드
|
||
mapImage = new Image();
|
||
mapImage.crossOrigin = 'anonymous';
|
||
|
||
mapImage.onload = function() {
|
||
// 캔버스 크기 설정 (최대 너비 800px)
|
||
const maxWidth = 800;
|
||
const scale = mapImage.width > maxWidth ? maxWidth / mapImage.width : 1;
|
||
|
||
mapCanvas.width = mapImage.width * scale;
|
||
mapCanvas.height = mapImage.height * scale;
|
||
|
||
// 이미지와 영역 그리기
|
||
drawWorkplaceMap();
|
||
|
||
// 클릭 이벤트 리스너 추가
|
||
mapCanvas.onclick = handleMapClick;
|
||
|
||
console.log(`✅ 작업장 지도 로드 완료: ${mapRegions.length}개 영역`);
|
||
};
|
||
|
||
mapImage.onerror = function() {
|
||
console.error('❌ 지도 이미지 로드 실패');
|
||
document.getElementById('layoutMapArea').style.display = 'none';
|
||
document.getElementById('workplaceList').style.display = 'flex';
|
||
document.getElementById('toggleListBtn').style.display = 'none';
|
||
showToast('지도를 불러올 수 없어 리스트로 표시합니다.', 'warning');
|
||
};
|
||
|
||
mapImage.src = fullImageUrl;
|
||
|
||
} catch (error) {
|
||
console.error('❌ 작업장 지도 로드 오류:', error);
|
||
document.getElementById('layoutMapArea').style.display = 'none';
|
||
document.getElementById('workplaceList').style.display = 'flex';
|
||
}
|
||
}
|
||
window.loadWorkplaceMap = loadWorkplaceMap;
|
||
|
||
// 지도 그리기 (이미지 + 영역 + 라벨)
|
||
function drawWorkplaceMap() {
|
||
if (!mapCanvas || !mapCtx || !mapImage) return;
|
||
|
||
// 이미지 그리기
|
||
mapCtx.drawImage(mapImage, 0, 0, mapCanvas.width, mapCanvas.height);
|
||
|
||
// 각 영역 그리기
|
||
mapRegions.forEach((region, index) => {
|
||
// 퍼센트를 픽셀로 변환
|
||
const x1 = (region.x_start / 100) * mapCanvas.width;
|
||
const y1 = (region.y_start / 100) * mapCanvas.height;
|
||
const x2 = (region.x_end / 100) * mapCanvas.width;
|
||
const y2 = (region.y_end / 100) * mapCanvas.height;
|
||
|
||
const width = x2 - x1;
|
||
const height = y2 - y1;
|
||
|
||
// 선택된 영역인지 확인
|
||
const isSelected = region.workplace_id === selectedWorkplace;
|
||
|
||
// 영역 테두리
|
||
mapCtx.strokeStyle = isSelected ? '#3b82f6' : '#10b981';
|
||
mapCtx.lineWidth = isSelected ? 4 : 2;
|
||
mapCtx.strokeRect(x1, y1, width, height);
|
||
|
||
// 영역 배경 (반투명)
|
||
mapCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.25)' : 'rgba(16, 185, 129, 0.15)';
|
||
mapCtx.fillRect(x1, y1, width, height);
|
||
|
||
// 작업장 이름 표시
|
||
if (region.workplace_name) {
|
||
mapCtx.font = 'bold 14px sans-serif';
|
||
|
||
// 텍스트 배경
|
||
const textMetrics = mapCtx.measureText(region.workplace_name);
|
||
const textPadding = 6;
|
||
mapCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.95)' : 'rgba(16, 185, 129, 0.95)';
|
||
mapCtx.fillRect(x1 + 5, y1 + 5, textMetrics.width + textPadding * 2, 24);
|
||
|
||
// 텍스트
|
||
mapCtx.fillStyle = '#ffffff';
|
||
mapCtx.fillText(region.workplace_name, x1 + 5 + textPadding, y1 + 22);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 지도 클릭 이벤트 처리
|
||
function handleMapClick(event) {
|
||
if (!mapCanvas || mapRegions.length === 0) return;
|
||
|
||
const rect = mapCanvas.getBoundingClientRect();
|
||
const x = event.clientX - rect.left;
|
||
const y = event.clientY - rect.top;
|
||
|
||
// 클릭한 위치에 있는 영역 찾기
|
||
for (let i = mapRegions.length - 1; i >= 0; i--) {
|
||
const region = mapRegions[i];
|
||
const x1 = (region.x_start / 100) * mapCanvas.width;
|
||
const y1 = (region.y_start / 100) * mapCanvas.height;
|
||
const x2 = (region.x_end / 100) * mapCanvas.width;
|
||
const y2 = (region.y_end / 100) * mapCanvas.height;
|
||
|
||
if (x >= x1 && x <= x2 && y >= y1 && y <= y2) {
|
||
// 영역 클릭됨
|
||
selectWorkplace(region.workplace_id, region.workplace_name);
|
||
|
||
// 지도 다시 그리기 (선택 효과 표시)
|
||
drawWorkplaceMap();
|
||
|
||
// 리스트에서도 동기화
|
||
syncWorkplaceListSelection(region.workplace_id);
|
||
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 리스트 선택 동기화
|
||
function syncWorkplaceListSelection(workplaceId) {
|
||
// 리스트의 버튼들도 업데이트
|
||
document.querySelectorAll('[id^="workplace-"]').forEach(btn => {
|
||
if (btn.id === `workplace-${workplaceId}`) {
|
||
btn.classList.remove('btn-secondary');
|
||
btn.classList.add('btn-primary');
|
||
} else {
|
||
btn.classList.remove('btn-primary');
|
||
btn.classList.add('btn-secondary');
|
||
}
|
||
});
|
||
}
|
||
window.syncWorkplaceListSelection = syncWorkplaceListSelection;
|
||
|
||
// ==================== 기존 팀 구성 모달 (백업) ====================
|
||
|
||
// 팀 구성 모달 열기
|
||
// 팀 구성 수정 (TBM 수정 모달 열기)
|
||
async function openTeamCompositionModal(sessionId) {
|
||
currentSessionId = sessionId;
|
||
|
||
try {
|
||
// 세션 정보 로드
|
||
const sessionResponse = await window.apiCall(`/tbm/sessions/${sessionId}`);
|
||
if (!sessionResponse || !sessionResponse.success) {
|
||
showToast('TBM 정보를 불러올 수 없습니다.', 'error');
|
||
return;
|
||
}
|
||
|
||
const session = sessionResponse.data; // data는 이미 객체
|
||
|
||
// 팀원 정보 로드
|
||
const teamResponse = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
|
||
if (!teamResponse || !teamResponse.success) {
|
||
showToast('팀원 정보를 불러올 수 없습니다.', 'error');
|
||
return;
|
||
}
|
||
|
||
const teamMembers = teamResponse.data;
|
||
|
||
// workerTaskList 구성
|
||
workerTaskList = [];
|
||
const workerMap = new Map();
|
||
|
||
// 팀원별로 작업 그룹화
|
||
teamMembers.forEach(member => {
|
||
if (!workerMap.has(member.worker_id)) {
|
||
workerMap.set(member.worker_id, {
|
||
worker_id: member.worker_id,
|
||
worker_name: member.worker_name,
|
||
job_type: member.job_type,
|
||
tasks: []
|
||
});
|
||
}
|
||
|
||
workerMap.get(member.worker_id).tasks.push({
|
||
task_line_id: generateUUID(),
|
||
project_id: member.project_id,
|
||
work_type_id: member.work_type_id,
|
||
task_id: member.task_id,
|
||
workplace_category_id: member.workplace_category_id,
|
||
workplace_id: member.workplace_id,
|
||
workplace_category_name: member.workplace_category_name,
|
||
workplace_name: member.workplace_name,
|
||
work_detail: member.work_detail,
|
||
is_present: member.is_present !== undefined ? member.is_present : true
|
||
});
|
||
});
|
||
|
||
workerTaskList = Array.from(workerMap.values());
|
||
|
||
// 모달 열기
|
||
document.getElementById('modalTitle').textContent = '팀 구성 수정';
|
||
document.getElementById('sessionId').value = sessionId;
|
||
document.getElementById('sessionDate').value = session.session_date;
|
||
|
||
// 입력자 표시
|
||
if (session.leader_name) {
|
||
document.getElementById('leaderName').value = `${session.leader_name} (${session.leader_job_type || ''})`;
|
||
document.getElementById('leaderId').value = session.leader_id;
|
||
} else if (session.created_by_name) {
|
||
document.getElementById('leaderName').value = `${session.created_by_name} (관리자)`;
|
||
document.getElementById('leaderId').value = '';
|
||
}
|
||
|
||
renderWorkerTaskList();
|
||
|
||
document.getElementById('tbmModal').style.display = 'flex';
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
} catch (error) {
|
||
console.error('❌ 팀 구성 로드 오류:', error);
|
||
showToast('팀 구성을 불러오는 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
}
|
||
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 = getTodayKST();
|
||
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);
|
||
}
|