Files
TK-FB-Project/web-ui/js/daily-work-report.js
Hyungi Ahn 74d3a78aa3 feat: 페이지 구조 재구성 및 사이드바 네비게이션 구현
- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성
  - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동
  - common/ → attendance/: 근태/휴가 관련 페이지 이동
  - admin/ 정리: safety-* 파일들을 safety/로 이동

- 사이드바 네비게이션 메뉴 구현
  - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리
  - 접기/펼치기 기능 및 상태 저장
  - 관리자 전용 메뉴 자동 표시/숨김

- 날씨 API 연동 (기상청 단기예보)
  - TBM 및 navbar에 현재 날씨 표시
  - weatherService.js 추가

- 안전 체크리스트 확장
  - 기본/날씨별/작업별 체크 유형 추가
  - checklist-manage.html 페이지 추가

- 이슈 신고 시스템 구현
  - workIssueController, workIssueModel, workIssueRoutes 추가

- DB 마이그레이션 파일 추가 (실행 대기)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:27:22 +09:00

2760 lines
89 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.
// daily-work-report.js - 브라우저 호환 버전
// =================================================================
// 🌐 API 설정 (window 객체에서 가져오기)
// =================================================================
// API 설정은 api-config.js에서 window 객체에 설정됨
// 전역 변수
let workTypes = [];
let workStatusTypes = [];
let errorTypes = [];
let workers = [];
let projects = [];
let selectedWorkers = new Set();
let workEntryCounter = 0;
let currentStep = 1;
let editingWorkId = null; // 수정 중인 작업 ID
let incompleteTbms = []; // 미완료 TBM 작업 목록
let currentTab = 'tbm'; // 현재 활성 탭
// 부적합 원인 관리
let currentDefectIndex = null; // 현재 편집 중인 행 인덱스
let tempDefects = {}; // 임시 부적합 원인 저장 { index: [{ error_type_id, defect_hours, note }] }
// 작업장소 지도 관련 변수
let mapCanvas = null;
let mapCtx = null;
let mapImage = null;
let mapRegions = [];
let selectedWorkplace = null;
let selectedWorkplaceName = null;
let selectedWorkplaceCategory = null;
let selectedWorkplaceCategoryName = null;
// 시간 선택 관련 변수
let currentEditingField = null; // { index, type: 'total' | 'error' }
let currentTimeValue = 0;
// =================================================================
// TBM 작업보고 관련 함수
// =================================================================
/**
* 탭 전환 함수
*/
window.switchTab = function(tab) {
currentTab = tab;
const tbmBtn = document.getElementById('tbmReportTab');
const completedBtn = document.getElementById('completedReportTab');
const tbmSection = document.getElementById('tbmReportSection');
const completedSection = document.getElementById('completedReportSection');
// 모든 탭 버튼 비활성화
tbmBtn.classList.remove('active');
completedBtn.classList.remove('active');
// 모든 섹션 숨기기
tbmSection.style.display = 'none';
completedSection.style.display = 'none';
// 선택된 탭 활성화
if (tab === 'tbm') {
tbmBtn.classList.add('active');
tbmSection.style.display = 'block';
loadIncompleteTbms(); // TBM 목록 로드
} else if (tab === 'completed') {
completedBtn.classList.add('active');
completedSection.style.display = 'block';
// 오늘 날짜로 초기화
document.getElementById('completedReportDate').value = getKoreaToday();
loadCompletedReports();
}
};
/**
* 미완료 TBM 작업 로드
*/
async function loadIncompleteTbms() {
try {
const response = await window.apiCall('/tbm/sessions/incomplete-reports');
if (!response.success) {
throw new Error(response.message || '미완료 TBM 조회 실패');
}
let data = response.data || [];
// 사용자 권한 확인 및 필터링
const user = getUser();
if (user && user.role !== 'Admin' && user.access_level !== 'system') {
// 일반 사용자: 자신이 생성한 세션만 표시
const userId = user.user_id;
data = data.filter(tbm => tbm.created_by === userId);
}
// 관리자는 모든 데이터 표시
incompleteTbms = data;
renderTbmWorkList();
} catch (error) {
console.error('미완료 TBM 로드 오류:', error);
showMessage('TBM 작업 목록을 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* 사용자 정보 가져오기 (auth-check.js와 동일한 로직)
*/
function getUser() {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
}
/**
* TBM 작업 목록 렌더링 (세션별 그룹화)
*/
function renderTbmWorkList() {
const container = document.getElementById('tbmWorkList');
// TBM을 세션별로 그룹화
const groupedTbms = {};
if (incompleteTbms && incompleteTbms.length > 0) {
incompleteTbms.forEach((tbm, index) => {
const key = `${tbm.session_id}_${tbm.session_date}`;
if (!groupedTbms[key]) {
groupedTbms[key] = {
session_id: tbm.session_id,
session_date: tbm.session_date,
created_by_name: tbm.created_by_name,
items: []
};
}
groupedTbms[key].items.push({ ...tbm, originalIndex: index });
});
}
let html = `
<div style="margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0;">작업보고서 목록</h3>
<button type="button" class="btn-add-work" onclick="addManualWorkRow()">
작업 추가
</button>
</div>
`;
// 수동 입력 섹션 먼저 추가 (맨 위)
html += `
<div class="tbm-session-group manual-input-section">
<div class="tbm-session-header" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);">
<span class="tbm-session-badge" style="background-color: #92400e; color: white;">수동 입력</span>
<span class="tbm-session-info" style="color: white; font-weight: 500;">TBM에 없는 작업을 추가로 입력할 수 있습니다</span>
</div>
<div class="tbm-table-container">
<table class="tbm-work-table">
<thead>
<tr>
<th>작업자</th>
<th>날짜</th>
<th>프로젝트</th>
<th>공정</th>
<th>작업</th>
<th>작업장소</th>
<th>작업시간</th>
<th>부적합</th>
<th>제출</th>
</tr>
</thead>
<tbody id="manualWorkTableBody">
<!-- 수동 입력 행들이 여기에 추가됩니다 -->
</tbody>
</table>
</div>
</div>
`;
// 각 TBM 세션별로 테이블 생성
Object.keys(groupedTbms).forEach(key => {
const group = groupedTbms[key];
html += `
<div class="tbm-session-group" data-session-key="${key}">
<div class="tbm-session-header">
<span class="tbm-session-badge">TBM 세션</span>
<span class="tbm-session-date">${formatDate(group.session_date)}</span>
<span class="tbm-session-creator">작성자: ${group.created_by_name}</span>
<span class="tbm-session-count">${group.items.length}명</span>
</div>
<div class="tbm-table-container">
<table class="tbm-work-table">
<thead>
<tr>
<th>작업자</th>
<th>프로젝트</th>
<th>공정</th>
<th>작업</th>
<th>작업장소</th>
<th>작업시간</th>
<th>부적합</th>
<th>제출</th>
</tr>
</thead>
<tbody>
${group.items.map(tbm => {
const index = tbm.originalIndex;
return `
<tr data-index="${index}" data-type="tbm" data-session-key="${key}">
<td>
<div class="worker-cell">
<strong>${tbm.worker_name || '작업자'}</strong>
<div class="worker-job-type">${tbm.job_type || '-'}</div>
</div>
</td>
<td>${tbm.project_name || '-'}</td>
<td>${tbm.work_type_name || '-'}</td>
<td>${tbm.task_name || '-'}</td>
<td>
<div class="workplace-cell">
<div>${tbm.category_name || ''}</div>
<div>${tbm.workplace_name || '-'}</div>
</div>
</td>
<td>
<input type="hidden" id="totalHours_${index}" value="" required>
<div class="time-input-trigger placeholder"
id="totalHoursDisplay_${index}"
onclick="openTimePicker(${index}, 'total')">
시간 선택
</div>
</td>
<td>
<input type="hidden" id="errorHours_${index}" value="0">
<input type="hidden" id="errorType_${index}" value="">
<button type="button"
class="btn-defect-toggle"
id="defectToggle_${index}"
onclick="toggleDefectArea(${index})">
<span id="defectSummary_${index}">없음</span>
</button>
</td>
<td>
<button type="button"
class="btn-submit-compact"
onclick="submitTbmWorkReport(${index})">
제출
</button>
</td>
</tr>
<tr class="defect-row" id="defectRow_${index}" style="display: none;">
<td colspan="8" style="padding: 0; background: #fef3c7;">
<div class="defect-inline-area" id="defectArea_${index}">
<div class="defect-list" id="defectList_${index}">
<!-- 부적합 원인 목록 -->
</div>
<button type="button" class="btn-add-defect-inline" onclick="addInlineDefect(${index})">
+ 부적합 추가
</button>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
<div class="batch-submit-container">
<button type="button"
class="btn-batch-submit"
onclick="batchSubmitTbmSession('${key}')">
📤 이 세션 일괄제출 (${group.items.length}건)
</button>
</div>
</div>
`;
});
container.innerHTML = html;
}
/**
* 부적합 시간 입력 처리
*/
window.calculateRegularHours = function(index) {
const errorInput = document.getElementById(`errorHours_${index}`);
const errorTypeSelect = document.getElementById(`errorType_${index}`);
const errorTypeNone = document.getElementById(`errorTypeNone_${index}`);
const errorHours = parseFloat(errorInput.value) || 0;
// 부적합 시간이 있으면 원인 선택 표시
if (errorHours > 0) {
errorTypeSelect.style.display = 'inline-block';
if (errorTypeNone) errorTypeNone.style.display = 'none';
} else {
errorTypeSelect.style.display = 'none';
if (errorTypeNone) errorTypeNone.style.display = 'inline';
}
};
/**
* TBM 작업보고서 제출
*/
window.submitTbmWorkReport = async function(index) {
const tbm = incompleteTbms[index];
const totalHours = parseFloat(document.getElementById(`totalHours_${index}`).value);
const defects = tempDefects[index] || [];
// 총 부적합 시간 계산
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
const errorTypeId = defects.length > 0 && defects[0].error_type_id ? defects[0].error_type_id : null;
// 필수 필드 검증
if (!totalHours || totalHours <= 0) {
showMessage('작업시간을 입력해주세요.', 'error');
return;
}
if (errorHours > totalHours) {
showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error');
return;
}
// 부적합 원인 유효성 검사
const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id);
if (invalidDefects.length > 0) {
showMessage('부적합 시간이 있는 항목은 원인을 선택해주세요.', 'error');
return;
}
// 날짜를 YYYY-MM-DD 형식으로 변환
const reportDate = tbm.session_date instanceof Date
? tbm.session_date.toISOString().split('T')[0]
: (typeof tbm.session_date === 'string' && tbm.session_date.includes('T')
? tbm.session_date.split('T')[0]
: tbm.session_date);
const reportData = {
tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id,
project_id: tbm.project_id,
work_type_id: tbm.work_type_id,
report_date: reportDate,
start_time: null,
end_time: null,
total_hours: totalHours,
error_hours: errorHours,
error_type_id: errorTypeId,
work_status_id: errorHours > 0 ? 2 : 1
};
console.log('🔍 TBM 제출 데이터:', JSON.stringify(reportData, null, 2));
console.log('🔍 부적합 원인:', defects);
try {
const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', reportData);
if (!response.success) {
throw new Error(response.message || '작업보고서 제출 실패');
}
// 부적합 원인이 있으면 저장
if (defects.length > 0 && response.data?.report_id) {
const validDefects = defects.filter(d => d.error_type_id && d.defect_hours > 0);
if (validDefects.length > 0) {
await window.apiCall(`/daily-work-reports/${response.data.report_id}/defects`, 'PUT', {
defects: validDefects
});
}
}
showSaveResultModal(
'success',
'작업보고서 제출 완료',
`${tbm.worker_name}의 작업보고서가 성공적으로 제출되었습니다.`,
response.data.tbm_completed ?
'모든 팀원의 작업보고서가 제출되어 TBM이 완료되었습니다.' :
response.data.completion_status
);
// 임시 부적합 데이터 삭제
delete tempDefects[index];
// 목록 새로고침
await loadIncompleteTbms();
} catch (error) {
console.error('TBM 작업보고서 제출 오류:', error);
showSaveResultModal('error', '제출 실패', error.message);
}
};
/**
* TBM 세션 일괄제출
*/
window.batchSubmitTbmSession = async function(sessionKey) {
// 해당 세션의 모든 항목 가져오기
const sessionRows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"]`);
if (sessionRows.length === 0) {
showMessage('제출할 항목이 없습니다.', 'error');
return;
}
// 1단계: 모든 항목 검증
const validationErrors = [];
const itemsToSubmit = [];
sessionRows.forEach((row, rowIndex) => {
const index = parseInt(row.getAttribute('data-index'));
const tbm = incompleteTbms[index];
const totalHours = parseFloat(document.getElementById(`totalHours_${index}`)?.value);
const errorHours = parseFloat(document.getElementById(`errorHours_${index}`)?.value) || 0;
const errorTypeId = document.getElementById(`errorType_${index}`)?.value;
// 검증
if (!totalHours || totalHours <= 0) {
validationErrors.push(`${tbm.worker_name}: 작업시간 미입력`);
return;
}
if (errorHours > totalHours) {
validationErrors.push(`${tbm.worker_name}: 부적합 시간이 총 작업시간 초과`);
return;
}
if (errorHours > 0 && !errorTypeId) {
validationErrors.push(`${tbm.worker_name}: 부적합 원인 미선택`);
return;
}
// 검증 통과한 항목 저장
const reportDate = tbm.session_date instanceof Date
? tbm.session_date.toISOString().split('T')[0]
: (typeof tbm.session_date === 'string' && tbm.session_date.includes('T')
? tbm.session_date.split('T')[0]
: tbm.session_date);
itemsToSubmit.push({
index,
tbm,
data: {
tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id,
project_id: tbm.project_id,
work_type_id: tbm.work_type_id,
report_date: reportDate,
start_time: null,
end_time: null,
total_hours: totalHours,
error_hours: errorHours,
error_type_id: errorTypeId || null,
work_status_id: errorHours > 0 ? 2 : 1
}
});
});
// 검증 실패가 하나라도 있으면 전체 중단
if (validationErrors.length > 0) {
showSaveResultModal(
'error',
'일괄제출 검증 실패',
'모든 항목이 유효해야 제출할 수 있습니다.',
validationErrors
);
return;
}
// 2단계: 모든 항목 제출
const submitBtn = event.target;
submitBtn.disabled = true;
submitBtn.textContent = '제출 중...';
const results = {
success: [],
failed: []
};
try {
for (const item of itemsToSubmit) {
try {
const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', item.data);
if (response.success) {
results.success.push(item.tbm.worker_name);
} else {
results.failed.push(`${item.tbm.worker_name}: ${response.message}`);
}
} catch (error) {
results.failed.push(`${item.tbm.worker_name}: ${error.message}`);
}
}
// 결과 표시
const totalCount = itemsToSubmit.length;
const successCount = results.success.length;
const failedCount = results.failed.length;
if (failedCount === 0) {
// 모두 성공
showSaveResultModal(
'success',
'일괄제출 완료',
`${totalCount}건의 작업보고서가 모두 성공적으로 제출되었습니다.`,
results.success.map(name => `${name}`)
);
} else if (successCount === 0) {
// 모두 실패
showSaveResultModal(
'error',
'일괄제출 실패',
`${totalCount}건의 작업보고서가 모두 실패했습니다.`,
results.failed.map(msg => `${msg}`)
);
} else {
// 일부 성공, 일부 실패
const details = [
...results.success.map(name => `${name} - 성공`),
...results.failed.map(msg => `${msg}`)
];
showSaveResultModal(
'warning',
'일괄제출 부분 완료',
`성공: ${successCount}건 / 실패: ${failedCount}`,
details
);
}
// 목록 새로고침
await loadIncompleteTbms();
} catch (error) {
console.error('일괄제출 오류:', error);
showSaveResultModal('error', '일괄제출 오류', error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = `📤 이 세션 일괄제출 (${sessionRows.length}건)`;
}
};
/**
* 수동 작업 추가
*/
window.addManualWorkRow = function() {
const tbody = document.getElementById('manualWorkTableBody');
if (!tbody) {
showMessage('수동 입력 테이블을 찾을 수 없습니다.', 'error');
return;
}
const manualIndex = `manual_${workEntryCounter++}`;
const newRow = document.createElement('tr');
newRow.setAttribute('data-index', manualIndex);
newRow.setAttribute('data-type', 'manual');
newRow.innerHTML = `
<td>
<select class="form-input-compact" id="worker_${manualIndex}" style="width: 120px;" required>
<option value="">작업자 선택</option>
${workers.map(w => `<option value="${w.worker_id}">${w.worker_name} (${w.job_type || '-'})</option>`).join('')}
</select>
</td>
<td>
<input type="date" class="form-input-compact" id="date_${manualIndex}" value="${getKoreaToday()}" required style="width: 130px;">
</td>
<td>
<select class="form-input-compact" id="project_${manualIndex}" style="width: 120px;" required>
<option value="">프로젝트 선택</option>
${projects.map(p => `<option value="${p.project_id}">${p.project_name}</option>`).join('')}
</select>
</td>
<td>
<select class="form-input-compact" id="workType_${manualIndex}" style="width: 120px;" required onchange="loadTasksForWorkType('${manualIndex}')">
<option value="">공정 선택</option>
${workTypes.map(wt => `<option value="${wt.id}">${wt.name}</option>`).join('')}
</select>
</td>
<td>
<select class="form-input-compact" id="task_${manualIndex}" style="width: 120px;" required disabled>
<option value="">공정을 먼저 선택하세요</option>
</select>
</td>
<td style="min-width: 180px;">
<input type="hidden" id="workplaceCategory_${manualIndex}">
<input type="hidden" id="workplace_${manualIndex}">
<div id="workplaceDisplay_${manualIndex}" class="workplace-select-box" style="display: flex; flex-direction: column; gap: 0.25rem; padding: 0.5rem; background: #f9fafb; border: 2px solid #e5e7eb; border-radius: 6px; min-height: 60px; cursor: pointer;" onclick="openWorkplaceMapForManual('${manualIndex}')">
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: #6b7280; font-weight: 500;">
<span>🗺️</span>
<span>작업장소</span>
</div>
<div id="workplaceText_${manualIndex}" style="font-size: 0.8rem; color: #9ca3af; font-style: italic;">
클릭하여 선택
</div>
</div>
</td>
<td>
<input type="hidden" id="totalHours_${manualIndex}" value="" required>
<div class="time-input-trigger placeholder"
id="totalHoursDisplay_${manualIndex}"
onclick="openTimePicker('${manualIndex}', 'total')">
시간 선택
</div>
</td>
<td>
<input type="hidden" id="errorHours_${manualIndex}" value="0">
<input type="hidden" id="errorType_${manualIndex}" value="">
<button type="button"
class="btn-defect-toggle"
id="defectToggle_${manualIndex}"
onclick="toggleDefectArea('${manualIndex}')">
<span id="defectSummary_${manualIndex}">없음</span>
</button>
</td>
<td>
<button type="button" class="btn-submit-compact" onclick="submitManualWorkReport('${manualIndex}')">
제출
</button>
<button type="button" class="btn-delete-compact" onclick="removeManualWorkRow('${manualIndex}')" style="margin-left: 4px;">
</button>
</td>
`;
tbody.appendChild(newRow);
// 부적합 인라인 영역 행 추가
const defectRow = document.createElement('tr');
defectRow.className = 'defect-row';
defectRow.id = `defectRow_${manualIndex}`;
defectRow.style.display = 'none';
defectRow.innerHTML = `
<td colspan="9" style="padding: 0; background: #fef3c7;">
<div class="defect-inline-area" id="defectArea_${manualIndex}">
<div class="defect-list" id="defectList_${manualIndex}">
<!-- 부적합 원인 목록 -->
</div>
<button type="button" class="btn-add-defect-inline" onclick="addInlineDefect('${manualIndex}')">
+ 부적합 추가
</button>
</div>
</td>
`;
tbody.appendChild(defectRow);
showMessage('새 작업 행이 추가되었습니다. 정보를 입력하고 제출하세요.', 'info');
};
/**
* 수동 작업 행 제거
*/
window.removeManualWorkRow = function(manualIndex) {
const row = document.querySelector(`tr[data-index="${manualIndex}"]`);
const defectRow = document.getElementById(`defectRow_${manualIndex}`);
if (row) {
row.remove();
}
if (defectRow) {
defectRow.remove();
}
// 임시 부적합 데이터도 삭제
delete tempDefects[manualIndex];
};
/**
* 공정 선택 시 작업 목록 로드
*/
window.loadTasksForWorkType = async function(manualIndex) {
const workTypeId = document.getElementById(`workType_${manualIndex}`).value;
const taskSelect = document.getElementById(`task_${manualIndex}`);
if (!workTypeId) {
taskSelect.disabled = true;
taskSelect.innerHTML = '<option value="">공정을 먼저 선택하세요</option>';
return;
}
try {
// 해당 공정의 작업 목록 조회
const response = await window.apiCall(`/tasks?work_type_id=${workTypeId}`);
const tasks = response.success ? response.data : (Array.isArray(response) ? response : []);
if (tasks && tasks.length > 0) {
taskSelect.disabled = false;
taskSelect.innerHTML = `
<option value="">작업 선택</option>
${tasks.map(task => `<option value="${task.task_id}">${task.task_name}</option>`).join('')}
`;
} else {
taskSelect.disabled = true;
taskSelect.innerHTML = '<option value="">등록된 작업이 없습니다</option>';
}
} catch (error) {
console.error('작업 목록 로드 오류:', error);
taskSelect.disabled = true;
taskSelect.innerHTML = '<option value="">작업 로드 실패</option>';
}
};
/**
* 수동 입력 부적합 시간 토글
*/
window.toggleManualErrorType = function(manualIndex) {
const errorInput = document.getElementById(`errorHours_${manualIndex}`);
const errorTypeSelect = document.getElementById(`errorType_${manualIndex}`);
const errorTypeNone = document.getElementById(`errorTypeNone_${manualIndex}`);
const errorHours = parseFloat(errorInput.value) || 0;
if (errorHours > 0) {
errorTypeSelect.style.display = 'inline-block';
if (errorTypeNone) errorTypeNone.style.display = 'none';
} else {
errorTypeSelect.style.display = 'none';
if (errorTypeNone) errorTypeNone.style.display = 'inline';
}
};
/**
* 수동 입력용 작업장소 선택 모달 열기
*/
window.openWorkplaceMapForManual = async function(manualIndex) {
window.currentManualIndex = manualIndex;
// 변수 초기화
selectedWorkplace = null;
selectedWorkplaceName = null;
selectedWorkplaceCategory = null;
selectedWorkplaceCategoryName = null;
try {
// 작업장소 카테고리 로드
const categoriesResponse = await window.apiCall('/workplaces/categories');
const categories = categoriesResponse.success ? categoriesResponse.data : categoriesResponse;
// 작업장소 모달 표시
const modal = document.getElementById('workplaceModal');
const categoryList = document.getElementById('workplaceCategoryList');
categoryList.innerHTML = categories.map(cat => `
<button type="button" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceCategory(${cat.category_id}, "${cat.category_name.replace(/"/g, '&quot;')}", "${(cat.layout_image || '').replace(/"/g, '&quot;')}")'>
<span style="margin-right: 0.5rem;">🏭</span>
${cat.category_name}
</button>
`).join('');
// 카테고리 선택 화면 표시
document.getElementById('categorySelectionArea').style.display = 'block';
document.getElementById('workplaceSelectionArea').style.display = 'none';
modal.style.display = 'flex';
} catch (error) {
console.error('작업장소 카테고리 로드 오류:', error);
showMessage('작업장소 목록을 불러오는 중 오류가 발생했습니다.', 'error');
}
};
/**
* 작업장소 카테고리 선택
*/
window.selectWorkplaceCategory = async function(categoryId, categoryName, layoutImage) {
selectedWorkplaceCategory = categoryId;
selectedWorkplaceCategoryName = categoryName;
try {
// 타이틀 업데이트
document.getElementById('selectedCategoryTitle').textContent = `${categoryName} - 작업장 선택`;
// 카테고리 화면 숨기고 작업장 선택 화면 표시
document.getElementById('categorySelectionArea').style.display = 'none';
document.getElementById('workplaceSelectionArea').style.display = 'block';
// 해당 카테고리의 작업장소 로드
const workplacesResponse = await window.apiCall(`/workplaces?category_id=${categoryId}`);
const workplaces = workplacesResponse.success ? workplacesResponse.data : workplacesResponse;
// 지도 또는 리스트 로드
if (layoutImage && layoutImage !== '') {
// 지도가 있는 경우 - 지도 영역 표시
await loadWorkplaceMap(categoryId, layoutImage, workplaces);
document.getElementById('layoutMapArea').style.display = 'block';
} else {
// 지도가 없는 경우 - 리스트만 표시
document.getElementById('layoutMapArea').style.display = 'none';
}
// 리스트 항상 표시
const workplaceListArea = document.getElementById('workplaceListArea');
workplaceListArea.innerHTML = workplaces.map(wp => `
<button type="button" id="workplace-${wp.workplace_id}" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceFromList(${wp.workplace_id}, "${wp.workplace_name.replace(/"/g, '&quot;')}")'>
<span style="margin-right: 0.5rem;">📍</span>
${wp.workplace_name}
</button>
`).join('');
} catch (error) {
console.error('작업장소 로드 오류:', error);
showMessage('작업장소를 불러오는 중 오류가 발생했습니다.', 'error');
}
};
/**
* 작업장소 지도 로드
*/
async function loadWorkplaceMap(categoryId, layoutImagePath, workplaces) {
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';
showMessage('지도를 불러올 수 없어 리스트로 표시합니다.', 'warning');
};
mapImage.src = fullImageUrl;
} catch (error) {
console.error('❌ 작업장 지도 로드 오류:', error);
document.getElementById('layoutMapArea').style.display = 'none';
}
}
/**
* 지도 그리기
*/
function drawWorkplaceMap() {
if (!mapCanvas || !mapCtx || !mapImage) return;
// 이미지 그리기
mapCtx.drawImage(mapImage, 0, 0, mapCanvas.width, mapCanvas.height);
// 각 영역 그리기
mapRegions.forEach((region) => {
// 퍼센트를 픽셀로 변환
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) {
// 영역 클릭됨
selectWorkplaceFromList(region.workplace_id, region.workplace_name);
return;
}
}
}
/**
* 리스트에서 작업장소 선택
*/
window.selectWorkplaceFromList = function(workplaceId, workplaceName) {
selectedWorkplace = workplaceId;
selectedWorkplaceName = workplaceName;
// 지도 다시 그리기 (선택 효과 표시)
if (mapCanvas && mapCtx && mapImage) {
drawWorkplaceMap();
}
// 리스트 버튼 업데이트
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');
}
});
// 선택 완료 버튼 활성화
document.getElementById('confirmWorkplaceBtn').disabled = false;
};
/**
* 작업장소 선택 완료
*/
window.confirmWorkplaceSelection = function() {
const manualIndex = window.currentManualIndex;
if (!selectedWorkplace || !selectedWorkplaceCategory) {
showMessage('작업장소를 선택해주세요.', 'error');
return;
}
document.getElementById(`workplaceCategory_${manualIndex}`).value = selectedWorkplaceCategory;
document.getElementById(`workplace_${manualIndex}`).value = selectedWorkplace;
const displayDiv = document.getElementById(`workplaceDisplay_${manualIndex}`);
if (displayDiv) {
displayDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: #059669; font-weight: 600;">
<span>✓</span>
<span>작업장소 선택됨</span>
</div>
<div style="font-size: 0.8rem; color: #111827; font-weight: 500;">
<div style="color: #6b7280; font-size: 0.7rem; margin-bottom: 2px;">🏭 ${selectedWorkplaceCategoryName}</div>
<div>📍 ${selectedWorkplaceName}</div>
</div>
`;
displayDiv.style.background = '#ecfdf5';
displayDiv.style.borderColor = '#10b981';
}
// 모달 닫기
closeWorkplaceModal();
showMessage('작업장소가 선택되었습니다.', 'success');
};
/**
* 작업장소 모달 닫기
*/
window.closeWorkplaceModal = function() {
document.getElementById('workplaceModal').style.display = 'none';
// 초기화
selectedWorkplace = null;
selectedWorkplaceName = null;
mapCanvas = null;
mapCtx = null;
mapImage = null;
mapRegions = [];
};
/**
* 수동 작업보고서 제출
*/
window.submitManualWorkReport = async function(manualIndex) {
const workerId = document.getElementById(`worker_${manualIndex}`).value;
const reportDate = document.getElementById(`date_${manualIndex}`).value;
const projectId = document.getElementById(`project_${manualIndex}`).value;
const workTypeId = document.getElementById(`workType_${manualIndex}`).value;
const taskId = document.getElementById(`task_${manualIndex}`).value;
const workplaceCategoryId = document.getElementById(`workplaceCategory_${manualIndex}`).value;
const workplaceId = document.getElementById(`workplace_${manualIndex}`).value;
const totalHours = parseFloat(document.getElementById(`totalHours_${manualIndex}`).value);
// 부적합 원인 가져오기
const defects = tempDefects[manualIndex] || [];
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
const errorTypeId = defects.length > 0 && defects[0].error_type_id ? defects[0].error_type_id : null;
// 필수 필드 검증
if (!workerId) {
showMessage('작업자를 선택해주세요.', 'error');
return;
}
if (!reportDate) {
showMessage('작업 날짜를 입력해주세요.', 'error');
return;
}
if (!projectId) {
showMessage('프로젝트를 선택해주세요.', 'error');
return;
}
if (!workTypeId) {
showMessage('공정을 선택해주세요.', 'error');
return;
}
if (!taskId) {
showMessage('작업을 선택해주세요.', 'error');
return;
}
if (!workplaceId) {
showMessage('작업장소를 선택해주세요.', 'error');
return;
}
if (!totalHours || totalHours <= 0) {
showMessage('작업시간을 입력해주세요.', 'error');
return;
}
if (errorHours > totalHours) {
showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error');
return;
}
// 부적합 원인 유효성 검사
const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id);
if (invalidDefects.length > 0) {
showMessage('부적합 시간이 있는 항목은 원인을 선택해주세요.', 'error');
return;
}
const reportData = {
worker_id: parseInt(workerId),
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
task_id: parseInt(taskId),
report_date: reportDate,
workplace_category_id: parseInt(workplaceCategoryId),
workplace_id: parseInt(workplaceId),
start_time: null,
end_time: null,
total_hours: totalHours,
error_hours: errorHours,
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
work_status_id: errorHours > 0 ? 2 : 1
};
try {
const response = await window.apiCall('/daily-work-reports', 'POST', reportData);
if (!response.success) {
throw new Error(response.message || '작업보고서 제출 실패');
}
// 부적합 원인이 있으면 저장
if (defects.length > 0 && response.data?.workReport_ids?.[0]) {
const validDefects = defects.filter(d => d.error_type_id && d.defect_hours > 0);
if (validDefects.length > 0) {
await window.apiCall(`/daily-work-reports/${response.data.workReport_ids[0]}/defects`, 'PUT', {
defects: validDefects
});
}
}
showSaveResultModal(
'success',
'작업보고서 제출 완료',
'작업보고서가 성공적으로 제출되었습니다.'
);
// 행 제거 (부적합 임시 데이터도 함께 삭제됨)
removeManualWorkRow(manualIndex);
// 목록 새로고침
await loadIncompleteTbms();
} catch (error) {
console.error('수동 작업보고서 제출 오류:', error);
showSaveResultModal('error', '제출 실패', error.message);
}
};
/**
* 날짜 포맷 함수
*/
function formatDate(dateString) {
if (!dateString) return '';
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}`;
}
/**
* 작성 완료된 작업보고서 로드
*/
window.loadCompletedReports = async function() {
try {
const selectedDate = document.getElementById('completedReportDate').value;
if (!selectedDate) {
showMessage('날짜를 선택해주세요.', 'error');
return;
}
// 해당 날짜의 작업보고서 조회
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}`);
console.log('완료된 보고서 API 응답:', response);
// API 응답이 배열인지 객체인지 확인
let reports = [];
if (Array.isArray(response)) {
reports = response;
} else if (response.success && response.data) {
reports = Array.isArray(response.data) ? response.data : [];
} else if (response.data) {
reports = Array.isArray(response.data) ? response.data : [];
}
renderCompletedReports(reports);
} catch (error) {
console.error('완료된 보고서 로드 오류:', error);
showMessage('작업보고서를 불러오는 중 오류가 발생했습니다.', 'error');
}
};
/**
* 완료된 보고서 목록 렌더링
*/
function renderCompletedReports(reports) {
const container = document.getElementById('completedReportsList');
if (!reports || reports.length === 0) {
container.innerHTML = '<p style="text-align: center; padding: 2rem; color: #9ca3af;">작성된 작업보고서가 없습니다.</p>';
return;
}
const html = reports.map(report => `
<div class="completed-report-card">
<div class="report-header">
<div>
<h4>${report.worker_name || '작업자'}</h4>
${report.tbm_session_id ? '<span class="tbm-badge">TBM 연동</span>' : '<span class="manual-badge">수동 입력</span>'}
</div>
<span class="report-date">${formatDate(report.report_date)}</span>
</div>
<div class="report-info">
<div class="info-row">
<span class="label">프로젝트:</span>
<span class="value">${report.project_name || '-'}</span>
</div>
<div class="info-row">
<span class="label">공정:</span>
<span class="value">${report.work_type_name || '-'}</span>
</div>
<div class="info-row">
<span class="label">작업시간:</span>
<span class="value">${report.total_hours || report.work_hours || 0}시간</span>
</div>
${report.regular_hours !== undefined && report.regular_hours !== null ? `
<div class="info-row">
<span class="label">정규 시간:</span>
<span class="value">${report.regular_hours}시간</span>
</div>
` : ''}
${report.error_hours && report.error_hours > 0 ? `
<div class="info-row">
<span class="label">부적합 처리:</span>
<span class="value" style="color: #dc2626;">${report.error_hours}시간</span>
</div>
<div class="info-row">
<span class="label">부적합 원인:</span>
<span class="value">${report.error_type_name || '-'}</span>
</div>
` : ''}
<div class="info-row">
<span class="label">작성자:</span>
<span class="value">${report.created_by_name || '-'}</span>
</div>
${report.start_time && report.end_time ? `
<div class="info-row">
<span class="label">작업 시간:</span>
<span class="value">${report.start_time} ~ ${report.end_time}</span>
</div>
` : ''}
</div>
</div>
`).join('');
container.innerHTML = html;
}
// =================================================================
// 기존 함수들
// =================================================================
// 한국 시간 기준 오늘 날짜 가져오기
function getKoreaToday() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 현재 로그인한 사용자 정보 가져오기
function getCurrentUser() {
try {
const token = localStorage.getItem('token');
if (!token) return null;
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
const payload = JSON.parse(atob(payloadBase64));
console.log('토큰에서 추출한 사용자 정보:', payload);
return payload;
}
} catch (error) {
console.log('토큰에서 사용자 정보 추출 실패:', error);
}
try {
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
if (userInfo) {
const parsed = JSON.parse(userInfo);
console.log('localStorage에서 가져온 사용자 정보:', parsed);
return parsed;
}
} catch (error) {
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
}
return null;
}
// 메시지 표시
function showMessage(message, type = 'info') {
const container = document.getElementById('message-container');
container.innerHTML = `<div class="message ${type}">${message}</div>`;
if (type === 'success') {
setTimeout(() => {
hideMessage();
}, 5000);
}
}
function hideMessage() {
document.getElementById('message-container').innerHTML = '';
}
// 저장 결과 모달 표시
function showSaveResultModal(type, title, message, details = null) {
const modal = document.getElementById('saveResultModal');
const titleElement = document.getElementById('resultModalTitle');
const contentElement = document.getElementById('resultModalContent');
// 아이콘 설정
let icon = '';
switch (type) {
case 'success':
icon = '✅';
break;
case 'error':
icon = '❌';
break;
case 'warning':
icon = '⚠️';
break;
default:
icon = '';
}
// 모달 내용 구성
let content = `
<div class="result-icon ${type}">${icon}</div>
<h3 class="result-title ${type}">${title}</h3>
<p class="result-message">${message}</p>
`;
// 상세 정보가 있으면 추가
if (details) {
if (Array.isArray(details) && details.length > 0) {
content += `
<div class="result-details">
<h4>상세 정보:</h4>
<ul>
${details.map(detail => `<li>${detail}</li>`).join('')}
</ul>
</div>
`;
} else if (typeof details === 'string') {
content += `
<div class="result-details">
<p>${details}</p>
</div>
`;
}
}
titleElement.textContent = '저장 결과';
contentElement.innerHTML = content;
modal.style.display = 'flex';
// ESC 키로 닫기
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
closeSaveResultModal();
}
});
// 배경 클릭으로 닫기
modal.addEventListener('click', function (e) {
if (e.target === modal) {
closeSaveResultModal();
}
});
}
// 저장 결과 모달 닫기
function closeSaveResultModal() {
const modal = document.getElementById('saveResultModal');
modal.style.display = 'none';
// 이벤트 리스너 제거
document.removeEventListener('keydown', closeSaveResultModal);
}
// 전역에서 접근 가능하도록 window에 할당
window.closeSaveResultModal = closeSaveResultModal;
// 단계 이동
function goToStep(stepNumber) {
for (let i = 1; i <= 3; i++) {
const step = document.getElementById(`step${i}`);
if (step) {
step.classList.remove('active', 'completed');
if (i < stepNumber) {
step.classList.add('completed');
const stepNum = step.querySelector('.step-number');
if (stepNum) stepNum.classList.add('completed');
} else if (i === stepNumber) {
step.classList.add('active');
}
}
}
// 진행 단계 표시 업데이트
updateProgressSteps(stepNumber);
currentStep = stepNumber;
}
// 진행 단계 표시 업데이트
function updateProgressSteps(currentStepNumber) {
for (let i = 1; i <= 3; i++) {
const progressStep = document.getElementById(`progressStep${i}`);
if (progressStep) {
progressStep.classList.remove('active', 'completed');
if (i < currentStepNumber) {
progressStep.classList.add('completed');
} else if (i === currentStepNumber) {
progressStep.classList.add('active');
}
}
}
}
// 초기 데이터 로드 (통합 API 사용)
async function loadData() {
try {
showMessage('데이터를 불러오는 중...', 'loading');
console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩 시작...');
await loadWorkers();
await loadProjects();
await loadWorkTypes();
await loadWorkStatusTypes();
await loadErrorTypes();
console.log('로드된 작업자 수:', workers.length);
console.log('로드된 프로젝트 수:', projects.length);
console.log('작업 유형 수:', workTypes.length);
hideMessage();
} catch (error) {
console.error('데이터 로드 실패:', error);
showMessage('데이터 로드 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
async function loadWorkers() {
try {
console.log('Workers API 호출 중... (통합 API 사용)');
// 모든 작업자 1000명까지 조회
const data = await window.apiCall(`${window.API}/workers?limit=1000`);
const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []);
// 작업 보고서에 표시할 작업자만 필터링
// 퇴사자만 제외 (계정 여부와 무관하게 재직자는 모두 표시)
workers = allWorkers.filter(worker => {
const notResigned = worker.employment_status !== 'resigned';
return notResigned;
});
console.log(`✅ Workers 로드 성공: ${workers.length}명 (전체: ${allWorkers.length}명)`);
console.log(`📊 필터링 조건: employment_status≠resigned (퇴사자만 제외)`);
} catch (error) {
console.error('작업자 로딩 오류:', error);
throw error;
}
}
async function loadProjects() {
try {
console.log('Projects API 호출 중... (활성 프로젝트만)');
const data = await window.apiCall(`${window.API}/projects/active/list`);
projects = Array.isArray(data) ? data : (data.data || data.projects || []);
console.log('✅ 활성 프로젝트 로드 성공:', projects.length);
} catch (error) {
console.error('프로젝트 로딩 오류:', error);
throw error;
}
}
async function loadWorkTypes() {
try {
const data = await window.apiCall(`${window.API}/daily-work-reports/work-types`);
if (Array.isArray(data) && data.length > 0) {
workTypes = data;
console.log('✅ 작업 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용');
workTypes = [
{ id: 1, name: 'Base' },
{ id: 2, name: 'Vessel' },
{ id: 3, name: 'Piping' }
];
}
}
async function loadWorkStatusTypes() {
try {
const data = await window.apiCall(`${window.API}/daily-work-reports/work-status-types`);
if (Array.isArray(data) && data.length > 0) {
workStatusTypes = data;
console.log('✅ 업무 상태 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용');
workStatusTypes = [
{ id: 1, name: '정규' },
{ id: 2, name: '에러' }
];
}
}
async function loadErrorTypes() {
try {
const data = await window.apiCall(`${window.API}/daily-work-reports/error-types`);
if (Array.isArray(data) && data.length > 0) {
errorTypes = data;
console.log('✅ 에러 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 에러 유형 API 사용 불가, 기본값 사용');
errorTypes = [
{ id: 1, name: '설계미스' },
{ id: 2, name: '외주작업 불량' },
{ id: 3, name: '입고지연' },
{ id: 4, name: '작업 불량' }
];
}
}
// TBM 팀 구성 자동 불러오기
async function loadTbmTeamForDate(date) {
try {
console.log('🛠️ TBM 팀 구성 조회 중:', date);
const response = await window.apiCall(`/tbm/sessions/date/${date}`);
if (response && response.success && response.data && response.data.length > 0) {
// 가장 최근 세션 선택 (진행중인 세션 우선)
const draftSessions = response.data.filter(s => s.status === 'draft');
const targetSession = draftSessions.length > 0 ? draftSessions[0] : response.data[0];
if (targetSession) {
// 팀 구성 조회
const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`);
if (teamRes && teamRes.success && teamRes.data) {
const teamWorkerIds = teamRes.data.map(m => m.worker_id);
console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}`);
return teamWorkerIds;
}
}
}
console.log(' 해당 날짜의 TBM 팀 구성이 없습니다.');
return [];
} catch (error) {
console.error('❌ TBM 팀 구성 조회 오류:', error);
return [];
}
}
// 작업자 그리드 생성
async function populateWorkerGrid() {
const grid = document.getElementById('workerGrid');
grid.innerHTML = '';
// 선택된 날짜의 TBM 팀 구성 불러오기
const reportDate = document.getElementById('reportDate').value;
let tbmWorkerIds = [];
if (reportDate) {
tbmWorkerIds = await loadTbmTeamForDate(reportDate);
}
// TBM 팀 구성이 있으면 안내 메시지 표시
if (tbmWorkerIds.length > 0) {
const infoDiv = document.createElement('div');
infoDiv.style.cssText = `
padding: 1rem;
background: #eff6ff;
border: 1px solid #3b82f6;
border-radius: 0.5rem;
margin-bottom: 1rem;
color: #1e40af;
font-size: 0.875rem;
`;
infoDiv.innerHTML = `
<strong>🛠️ TBM 팀 구성 자동 적용</strong><br>
오늘 TBM에서 구성된 팀원 ${tbmWorkerIds.length}명이 자동으로 선택되었습니다.
`;
grid.appendChild(infoDiv);
}
workers.forEach(worker => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'worker-card';
btn.textContent = worker.worker_name;
btn.dataset.id = worker.worker_id;
// TBM 팀 구성에 포함된 작업자는 자동 선택
if (tbmWorkerIds.includes(worker.worker_id)) {
btn.classList.add('selected');
selectedWorkers.add(worker.worker_id);
}
btn.addEventListener('click', () => {
toggleWorkerSelection(worker.worker_id, btn);
});
grid.appendChild(btn);
});
// 자동 선택된 작업자가 있으면 다음 단계 버튼 활성화
const nextBtn = document.getElementById('nextStep2');
if (nextBtn) {
nextBtn.disabled = selectedWorkers.size === 0;
}
}
// 작업자 선택 토글
function toggleWorkerSelection(workerId, btnElement) {
if (selectedWorkers.has(workerId)) {
selectedWorkers.delete(workerId);
btnElement.classList.remove('selected');
} else {
selectedWorkers.add(workerId);
btnElement.classList.add('selected');
}
const nextBtn = document.getElementById('nextStep2');
nextBtn.disabled = selectedWorkers.size === 0;
}
// 작업 항목 추가
function addWorkEntry() {
console.log('🔧 addWorkEntry 함수 호출됨');
const container = document.getElementById('workEntriesList');
console.log('🔧 컨테이너:', container);
workEntryCounter++;
console.log('🔧 작업 항목 카운터:', workEntryCounter);
const entryDiv = document.createElement('div');
entryDiv.className = 'work-entry';
entryDiv.dataset.id = workEntryCounter;
console.log('🔧 생성된 작업 항목 div:', entryDiv);
entryDiv.innerHTML = `
<div class="work-entry-header">
<div class="work-entry-title">작업 항목 #${workEntryCounter}</div>
<button type="button" class="remove-work-btn" onclick="event.stopPropagation(); removeWorkEntry(${workEntryCounter})" title="이 작업 삭제">
🗑️ 삭제
</button>
</div>
<div class="work-entry-grid">
<div class="form-field-group">
<div class="form-field-label">
<span class="form-field-icon">🏗️</span>
프로젝트
</div>
<select class="form-select project-select" required>
<option value="">프로젝트를 선택하세요</option>
${projects.map(p => `<option value="${p.project_id}">${p.project_name}</option>`).join('')}
</select>
</div>
<div class="form-field-group">
<div class="form-field-label">
<span class="form-field-icon">⚙️</span>
작업 유형
</div>
<select class="form-select work-type-select" required>
<option value="">작업 유형을 선택하세요</option>
${workTypes.map(wt => `<option value="${wt.id}">${wt.name}</option>`).join('')}
</select>
</div>
</div>
<div class="work-entry-full">
<div class="form-field-group">
<div class="form-field-label">
<span class="form-field-icon">📊</span>
업무 상태
</div>
<select class="form-select work-status-select" required>
<option value="">업무 상태를 선택하세요</option>
${workStatusTypes.map(ws => `<option value="${ws.id}">${ws.name}</option>`).join('')}
</select>
</div>
</div>
<div class="error-type-section work-entry-full">
<div class="form-field-label">
<span class="form-field-icon">⚠️</span>
에러 유형
</div>
<select class="form-select error-type-select">
<option value="">에러 유형을 선택하세요</option>
${errorTypes.map(et => `<option value="${et.id}">${et.name}</option>`).join('')}
</select>
</div>
<div class="time-input-section work-entry-full">
<div class="form-field-label">
<span class="form-field-icon">⏰</span>
작업 시간 (시간)
</div>
<input type="number" class="form-select time-input"
placeholder="작업 시간을 입력하세요"
min="0.25"
max="24"
step="0.25"
value="1.00"
required>
<div class="quick-time-buttons">
<button type="button" class="quick-time-btn" data-hours="0.5">30분</button>
<button type="button" class="quick-time-btn" data-hours="1">1시간</button>
<button type="button" class="quick-time-btn" data-hours="2">2시간</button>
<button type="button" class="quick-time-btn" data-hours="4">4시간</button>
<button type="button" class="quick-time-btn" data-hours="8">8시간</button>
</div>
</div>
`;
container.appendChild(entryDiv);
console.log('🔧 작업 항목이 컨테이너에 추가됨');
console.log('🔧 현재 컨테이너 내용:', container.innerHTML.length, '문자');
console.log('🔧 현재 .work-entry 개수:', container.querySelectorAll('.work-entry').length);
setupWorkEntryEvents(entryDiv);
console.log('🔧 이벤트 설정 완료');
}
// 작업 항목 이벤트 설정
function setupWorkEntryEvents(entryDiv) {
const timeInput = entryDiv.querySelector('.time-input');
const workStatusSelect = entryDiv.querySelector('.work-status-select');
const errorTypeSection = entryDiv.querySelector('.error-type-section');
const errorTypeSelect = entryDiv.querySelector('.error-type-select');
// 시간 입력 이벤트
timeInput.addEventListener('input', updateTotalHours);
// 빠른 시간 버튼 이벤트
entryDiv.querySelectorAll('.quick-time-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
timeInput.value = btn.dataset.hours;
updateTotalHours();
// 버튼 클릭 효과
btn.style.transform = 'scale(0.95)';
setTimeout(() => {
btn.style.transform = '';
}, 150);
});
});
// 업무 상태 변경 시 에러 유형 섹션 토글
workStatusSelect.addEventListener('change', (e) => {
const isError = e.target.value === '2'; // 에러 상태 ID가 2라고 가정
if (isError) {
errorTypeSection.classList.add('visible');
errorTypeSelect.required = true;
// 에러 상태일 때 시각적 피드백
errorTypeSection.style.animation = 'slideDown 0.4s ease-out';
} else {
errorTypeSection.classList.remove('visible');
errorTypeSelect.required = false;
errorTypeSelect.value = '';
}
});
// 폼 필드 포커스 효과
entryDiv.querySelectorAll('.form-field-group').forEach(group => {
const input = group.querySelector('select, input');
if (input) {
input.addEventListener('focus', () => {
group.classList.add('focused');
});
input.addEventListener('blur', () => {
group.classList.remove('focused');
});
}
});
}
// 작업 항목 제거
function removeWorkEntry(id) {
console.log('🗑️ removeWorkEntry 호출됨, id:', id);
const entry = document.querySelector(`.work-entry[data-id="${id}"]`);
console.log('🗑️ 찾은 entry:', entry);
if (entry) {
entry.remove();
updateTotalHours();
console.log('✅ 작업 항목 삭제 완료');
} else {
console.log('❌ 작업 항목을 찾을 수 없음');
}
}
// 총 시간 업데이트
function updateTotalHours() {
const timeInputs = document.querySelectorAll('.time-input');
let total = 0;
timeInputs.forEach(input => {
const value = parseFloat(input.value) || 0;
total += value;
});
const display = document.getElementById('totalHoursDisplay');
display.textContent = `총 작업시간: ${total}시간`;
if (total > 24) {
display.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
display.textContent += ' ⚠️ 24시간 초과';
} else {
display.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
}
}
// 저장 함수 (통합 API 사용)
async function saveWorkReport() {
const reportDate = document.getElementById('reportDate').value;
if (!reportDate || selectedWorkers.size === 0) {
showSaveResultModal(
'error',
'입력 오류',
'날짜와 작업자를 선택해주세요.'
);
return;
}
const entries = document.querySelectorAll('.work-entry');
console.log('🔍 찾은 작업 항목들:', entries);
console.log('🔍 작업 항목 개수:', entries.length);
if (entries.length === 0) {
showSaveResultModal(
'error',
'작업 항목 없음',
'최소 하나의 작업을 추가해주세요.'
);
return;
}
const newWorkEntries = [];
console.log('🔍 작업 항목 수집 시작...');
for (const entry of entries) {
console.log('🔍 작업 항목 처리 중:', entry);
const projectSelect = entry.querySelector('.project-select');
const workTypeSelect = entry.querySelector('.work-type-select');
const workStatusSelect = entry.querySelector('.work-status-select');
const errorTypeSelect = entry.querySelector('.error-type-select');
const timeInput = entry.querySelector('.time-input');
console.log('🔍 선택된 요소들:', {
projectSelect,
workTypeSelect,
workStatusSelect,
errorTypeSelect,
timeInput
});
const projectId = projectSelect?.value;
const workTypeId = workTypeSelect?.value;
const workStatusId = workStatusSelect?.value;
const errorTypeId = errorTypeSelect?.value;
const workHours = timeInput?.value;
console.log('🔍 수집된 값들:', {
projectId,
workTypeId,
workStatusId,
errorTypeId,
workHours
});
if (!projectId || !workTypeId || !workStatusId || !workHours) {
showSaveResultModal(
'error',
'입력 오류',
'모든 작업 항목을 완성해주세요.'
);
return;
}
if (workStatusId === '2' && !errorTypeId) {
showSaveResultModal(
'error',
'입력 오류',
'에러 상태인 경우 에러 유형을 선택해주세요.'
);
return;
}
const workEntry = {
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
work_hours: parseFloat(workHours)
};
console.log('🔍 생성된 작업 항목:', workEntry);
console.log('🔍 작업 항목 상세:', {
project_id: workEntry.project_id,
work_type_id: workEntry.work_type_id,
work_status_id: workEntry.work_status_id,
error_type_id: workEntry.error_type_id,
work_hours: workEntry.work_hours
});
newWorkEntries.push(workEntry);
}
console.log('🔍 최종 수집된 작업 항목들:', newWorkEntries);
console.log('🔍 총 작업 항목 개수:', newWorkEntries.length);
try {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '💾 저장 중...';
const currentUser = getCurrentUser();
let totalSaved = 0;
let totalFailed = 0;
const failureDetails = [];
for (const workerId of selectedWorkers) {
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음';
// 서버가 기대하는 work_entries 배열 형태로 전송
const requestData = {
report_date: reportDate,
worker_id: parseInt(workerId),
work_entries: newWorkEntries.map(entry => ({
project_id: entry.project_id,
task_id: entry.work_type_id, // 서버에서 task_id로 기대
work_hours: entry.work_hours,
work_status_id: entry.work_status_id,
error_type_id: entry.error_type_id
})),
created_by: currentUser?.user_id || currentUser?.id
};
console.log('🔄 배열 형태로 전송:', requestData);
console.log('🔄 work_entries:', requestData.work_entries);
console.log('🔄 work_entries[0] 상세:', requestData.work_entries[0]);
console.log('🔄 전송 데이터 JSON:', JSON.stringify(requestData, null, 2));
try {
const result = await window.apiCall(`${window.API}/daily-work-reports`, 'POST', requestData);
console.log('✅ 저장 성공:', result);
totalSaved++;
} catch (error) {
console.error('❌ 저장 실패:', error);
totalFailed++;
failureDetails.push(`${workerName}: ${error.message}`);
}
}
// 결과 모달 표시
if (totalSaved > 0 && totalFailed === 0) {
showSaveResultModal(
'success',
'저장 완료!',
`${totalSaved}명의 작업보고서가 성공적으로 저장되었습니다.`
);
} else if (totalSaved > 0 && totalFailed > 0) {
showSaveResultModal(
'warning',
'부분 저장 완료',
`${totalSaved}명은 성공했지만 ${totalFailed}명은 실패했습니다.`,
failureDetails
);
} else {
showSaveResultModal(
'error',
'저장 실패',
'모든 작업보고서 저장이 실패했습니다.',
failureDetails
);
}
if (totalSaved > 0) {
setTimeout(() => {
refreshTodayWorkers();
resetForm();
}, 2000);
}
} catch (error) {
console.error('저장 오류:', error);
showSaveResultModal(
'error',
'저장 오류',
'저장 중 예기치 못한 오류가 발생했습니다.',
[error.message]
);
} finally {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = false;
submitBtn.textContent = '💾 작업보고서 저장';
}
}
// 폼 초기화
function resetForm() {
goToStep(1);
selectedWorkers.clear();
document.querySelectorAll('.worker-card.selected').forEach(btn => {
btn.classList.remove('selected');
});
const container = document.getElementById('workEntriesList');
container.innerHTML = '';
workEntryCounter = 0;
updateTotalHours();
document.getElementById('nextStep2').disabled = true;
}
// 당일 작업자 현황 로드 (본인 입력분만) - 통합 API 사용
async function loadTodayWorkers() {
const section = document.getElementById('dailyWorkersSection');
const content = document.getElementById('dailyWorkersContent');
if (!section || !content) {
console.log('당일 현황 섹션이 HTML에 없습니다.');
return;
}
try {
const today = getKoreaToday();
const currentUser = getCurrentUser();
content.innerHTML = '<div class="loading-spinner">📊 내가 입력한 오늘의 작업 현황을 불러오는 중... (통합 API)</div>';
section.style.display = 'block';
// 본인이 입력한 데이터만 조회 (통합 API 사용)
let queryParams = `date=${today}`;
if (currentUser?.user_id) {
queryParams += `&created_by=${currentUser.user_id}`;
} else if (currentUser?.id) {
queryParams += `&created_by=${currentUser.id}`;
}
console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`);
const rawData = await window.apiCall(`${window.API}/daily-work-reports?${queryParams}`);
console.log('📊 당일 작업 데이터 (통합 API):', rawData);
let data = [];
if (Array.isArray(rawData)) {
data = rawData;
} else if (rawData?.data) {
data = rawData.data;
}
displayMyDailyWorkers(data, today);
} catch (error) {
console.error('당일 작업자 로드 오류:', error);
content.innerHTML = `
<div class="no-data-message">
❌ 오늘의 작업 현황을 불러올 수 없습니다.<br>
<small>${error.message}</small>
</div>
`;
}
}
// 본인 입력 작업자 현황 표시 (수정/삭제 기능 포함)
function displayMyDailyWorkers(data, date) {
const content = document.getElementById('dailyWorkersContent');
if (!Array.isArray(data) || data.length === 0) {
content.innerHTML = `
<div class="no-data-message">
📝 내가 오늘(${date}) 입력한 작업이 없습니다.<br>
<small>새로운 작업을 추가해보세요!</small>
</div>
`;
return;
}
// 작업자별로 데이터 그룹화
const workerGroups = {};
data.forEach(work => {
const workerName = work.worker_name || '미지정';
if (!workerGroups[workerName]) {
workerGroups[workerName] = [];
}
workerGroups[workerName].push(work);
});
const totalWorkers = Object.keys(workerGroups).length;
const totalWorks = data.length;
const headerHtml = `
<div class="daily-workers-header">
<h4>📊 내가 입력한 오늘(${date}) 작업 현황 - 총 ${totalWorkers}명, ${totalWorks}개 작업</h4>
<button class="refresh-btn" onclick="refreshTodayWorkers()">
🔄 새로고침
</button>
</div>
`;
const workersHtml = Object.entries(workerGroups).map(([workerName, works]) => {
const totalHours = works.reduce((sum, work) => {
return sum + parseFloat(work.work_hours || 0);
}, 0);
// 개별 작업 항목들 (수정/삭제 버튼 포함)
const individualWorksHtml = works.map((work) => {
const projectName = work.project_name || '미지정';
const workTypeName = work.work_type_name || '미지정';
const workStatusName = work.work_status_name || '미지정';
const workHours = work.work_hours || 0;
const errorTypeName = work.error_type_name || null;
const workId = work.id;
return `
<div class="individual-work-item">
<div class="work-details-grid">
<div class="detail-item">
<div class="detail-label">🏗️ 프로젝트</div>
<div class="detail-value">${projectName}</div>
</div>
<div class="detail-item">
<div class="detail-label">⚙️ 작업종류</div>
<div class="detail-value">${workTypeName}</div>
</div>
<div class="detail-item">
<div class="detail-label">📊 작업상태</div>
<div class="detail-value">${workStatusName}</div>
</div>
<div class="detail-item">
<div class="detail-label">⏰ 작업시간</div>
<div class="detail-value">${workHours}시간</div>
</div>
${errorTypeName ? `
<div class="detail-item">
<div class="detail-label">❌ 에러유형</div>
<div class="detail-value">${errorTypeName}</div>
</div>
` : ''}
</div>
<div class="action-buttons">
<button class="edit-btn" onclick="editWorkItem('${workId}')">
✏️ 수정
</button>
<button class="delete-btn" onclick="deleteWorkItem('${workId}')">
🗑️ 삭제
</button>
</div>
</div>
`;
}).join('');
return `
<div class="worker-status-item">
<div class="worker-header">
<div class="worker-name">👤 ${workerName}</div>
<div class="worker-total-hours">총 ${totalHours}시간</div>
</div>
<div class="individual-works-container">
${individualWorksHtml}
</div>
</div>
`;
}).join('');
content.innerHTML = headerHtml + '<div class="worker-status-grid">' + workersHtml + '</div>';
}
// 작업 항목 수정 함수 (통합 API 사용)
async function editWorkItem(workId) {
try {
console.log('수정할 작업 ID:', workId);
// 1. 기존 데이터 조회 (통합 API 사용)
showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading');
const workData = await window.apiCall(`${window.API}/daily-work-reports/${workId}`);
console.log('수정할 작업 데이터 (통합 API):', workData);
// 2. 수정 모달 표시
showEditModal(workData);
hideMessage();
} catch (error) {
console.error('작업 정보 조회 오류:', error);
showMessage('작업 정보를 불러올 수 없습니다: ' + error.message, 'error');
}
}
// 수정 모달 표시
function showEditModal(workData) {
editingWorkId = workData.id;
const modalHtml = `
<div class="edit-modal" id="editModal">
<div class="edit-modal-content">
<div class="edit-modal-header">
<h3>✏️ 작업 수정</h3>
<button class="close-modal-btn" onclick="closeEditModal()">×</button>
</div>
<div class="edit-modal-body">
<div class="edit-form-group">
<label>🏗️ 프로젝트</label>
<select class="edit-select" id="editProject">
<option value="">프로젝트 선택</option>
${projects.map(p => `
<option value="${p.project_id}" ${p.project_id == workData.project_id ? 'selected' : ''}>
${p.project_name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⚙️ 작업 유형</label>
<select class="edit-select" id="editWorkType">
<option value="">작업 유형 선택</option>
${workTypes.map(wt => `
<option value="${wt.id}" ${wt.id == workData.work_type_id ? 'selected' : ''}>
${wt.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>📊 업무 상태</label>
<select class="edit-select" id="editWorkStatus">
<option value="">업무 상태 선택</option>
${workStatusTypes.map(ws => `
<option value="${ws.id}" ${ws.id == workData.work_status_id ? 'selected' : ''}>
${ws.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group" id="editErrorTypeGroup" style="${workData.work_status_id == 2 ? '' : 'display: none;'}">
<label>❌ 에러 유형</label>
<select class="edit-select" id="editErrorType">
<option value="">에러 유형 선택</option>
${errorTypes.map(et => `
<option value="${et.id}" ${et.id == workData.error_type_id ? 'selected' : ''}>
${et.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⏰ 작업 시간</label>
<input type="number" class="edit-input" id="editWorkHours"
value="${workData.work_hours}"
min="0" max="24" step="0.5">
</div>
</div>
<div class="edit-modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">취소</button>
<button class="btn btn-success" onclick="saveEditedWork()">💾 저장</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 업무 상태 변경 이벤트
document.getElementById('editWorkStatus').addEventListener('change', (e) => {
const errorTypeGroup = document.getElementById('editErrorTypeGroup');
if (e.target.value === '2') {
errorTypeGroup.style.display = 'block';
} else {
errorTypeGroup.style.display = 'none';
}
});
}
// 수정 모달 닫기
function closeEditModal() {
const modal = document.getElementById('editModal');
if (modal) {
modal.remove();
}
editingWorkId = null;
}
// 수정된 작업 저장 (통합 API 사용)
async function saveEditedWork() {
try {
const projectId = document.getElementById('editProject').value;
const workTypeId = document.getElementById('editWorkType').value;
const workStatusId = document.getElementById('editWorkStatus').value;
const errorTypeId = document.getElementById('editErrorType').value;
const workHours = document.getElementById('editWorkHours').value;
if (!projectId || !workTypeId || !workStatusId || !workHours) {
showMessage('모든 필수 항목을 입력해주세요.', 'error');
return;
}
if (workStatusId === '2' && !errorTypeId) {
showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error');
return;
}
const updateData = {
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
work_hours: parseFloat(workHours)
};
showMessage('작업을 수정하는 중... (통합 API)', 'loading');
const result = await window.apiCall(`${window.API}/daily-work-reports/${editingWorkId}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
refreshTodayWorkers();
} catch (error) {
console.error('❌ 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 작업 항목 삭제 함수 (통합 API 사용)
async function deleteWorkItem(workId) {
if (!confirm('정말로 이 작업을 삭제하시겠습니까?\n삭제된 작업은 복구할 수 없습니다.')) {
return;
}
try {
console.log('삭제할 작업 ID:', workId);
showMessage('작업을 삭제하는 중... (통합 API)', 'loading');
// 개별 항목 삭제 API 호출 (본인 작성분만 삭제 가능) - 통합 API 사용
const result = await window.apiCall(`${window.API}/daily-work-reports/my-entry/${workId}`, {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
// 화면 새로고침
refreshTodayWorkers();
} catch (error) {
console.error('❌ 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 오늘 현황 새로고침
function refreshTodayWorkers() {
loadTodayWorkers();
}
// 이벤트 리스너 설정 (이제 테이블 기반 UI를 사용하므로 별도 리스너 불필요)
function setupEventListeners() {
// 기존 단계별 입력 UI 제거됨
// 모든 이벤트는 onclick 핸들러로 직접 처리
}
// 초기화
async function init() {
try {
const token = localStorage.getItem('token');
if (!token || token === 'undefined') {
showMessage('로그인이 필요합니다.', 'error');
localStorage.removeItem('token');
setTimeout(() => {
window.location.href = '/';
}, 2000);
return;
}
await loadData();
setupEventListeners();
// TBM 작업 목록 로드 (기본 탭)
await loadIncompleteTbms();
console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)');
} catch (error) {
console.error('초기화 오류:', error);
showMessage('초기화 중 오류가 발생했습니다.', 'error');
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', init);
// 전역 함수로 노출
window.removeWorkEntry = removeWorkEntry;
window.refreshTodayWorkers = refreshTodayWorkers;
window.editWorkItem = editWorkItem;
window.deleteWorkItem = deleteWorkItem;
window.closeEditModal = closeEditModal;
window.saveEditedWork = saveEditedWork;
// =================================================================
// 시간 선택 팝오버 관련 함수
// =================================================================
/**
* 시간 포맷팅 함수
*/
function formatHours(hours) {
const h = Math.floor(hours);
const m = (hours % 1) * 60;
if (m === 0) return `${h}시간`;
return `${h}시간 ${m}`;
}
/**
* 시간 선택 팝오버 열기
*/
window.openTimePicker = function(index, type) {
currentEditingField = { index, type };
// 현재 값 가져오기
const inputId = type === 'total' ? `totalHours_${index}` : `errorHours_${index}`;
const hiddenInput = document.getElementById(inputId);
currentTimeValue = parseFloat(hiddenInput?.value) || 0;
// 팝오버 표시
const overlay = document.getElementById('timePickerOverlay');
const title = document.getElementById('timePickerTitle');
title.textContent = type === 'total' ? '작업시간 선택' : '부적합 시간 선택';
updateTimeDisplay();
overlay.style.display = 'flex';
// ESC 키로 닫기
document.addEventListener('keydown', handleEscapeKey);
};
/**
* ESC 키 핸들러
*/
function handleEscapeKey(e) {
if (e.key === 'Escape') {
closeTimePicker();
}
}
/**
* 시간 값 설정
*/
window.setTimeValue = function(hours) {
currentTimeValue = hours;
updateTimeDisplay();
};
/**
* 시간 조정 (±30분)
*/
window.adjustTime = function(delta) {
currentTimeValue = Math.max(0, Math.min(24, currentTimeValue + delta));
updateTimeDisplay();
};
/**
* 시간 표시 업데이트
*/
function updateTimeDisplay() {
const display = document.getElementById('currentTimeDisplay');
if (display) {
display.textContent = formatHours(currentTimeValue);
}
}
/**
* 시간 선택 확인
*/
window.confirmTimeSelection = function() {
if (!currentEditingField) return;
const { index, type, defectIndex } = currentEditingField;
// 부적합 시간 선택인 경우
if (type === 'defect') {
if (tempDefects[index] && tempDefects[index][defectIndex] !== undefined) {
tempDefects[index][defectIndex].defect_hours = currentTimeValue;
// 시간 표시 업데이트
const timeDisplay = document.getElementById(`defectTime_${index}_${defectIndex}`);
if (timeDisplay) {
timeDisplay.textContent = currentTimeValue;
}
// 요약 및 hidden 필드 업데이트
updateDefectSummary(index);
}
closeTimePicker();
return;
}
// 기존 total/error 시간 선택
const inputId = type === 'total' ? `totalHours_${index}` : `errorHours_${index}`;
const displayId = type === 'total' ? `totalHoursDisplay_${index}` : `errorHoursDisplay_${index}`;
// hidden input 값 설정
const hiddenInput = document.getElementById(inputId);
if (hiddenInput) {
hiddenInput.value = currentTimeValue;
}
// 표시 영역 업데이트
const displayDiv = document.getElementById(displayId);
if (displayDiv) {
displayDiv.textContent = formatHours(currentTimeValue);
displayDiv.classList.remove('placeholder');
displayDiv.classList.add('has-value');
}
// 부적합 시간 입력 시 에러 타입 토글 (기존 방식 - 이제 사용안함)
if (type === 'error') {
if (index.toString().startsWith('manual_')) {
toggleManualErrorType(index);
} else {
calculateRegularHours(index);
}
}
closeTimePicker();
};
/**
* 시간 선택 팝오버 닫기
*/
window.closeTimePicker = function() {
const overlay = document.getElementById('timePickerOverlay');
if (overlay) {
overlay.style.display = 'none';
}
currentEditingField = null;
currentTimeValue = 0;
// ESC 키 리스너 제거
document.removeEventListener('keydown', handleEscapeKey);
};
// =================================================================
// 부적합 원인 관리 (인라인 방식)
// =================================================================
/**
* 부적합 영역 토글
*/
window.toggleDefectArea = function(index) {
const defectRow = document.getElementById(`defectRow_${index}`);
if (!defectRow) return;
const isVisible = defectRow.style.display !== 'none';
if (isVisible) {
// 숨기기
defectRow.style.display = 'none';
} else {
// 보이기 - 부적합 원인이 없으면 자동으로 하나 추가
if (!tempDefects[index] || tempDefects[index].length === 0) {
tempDefects[index] = [{
error_type_id: '',
defect_hours: 0,
note: ''
}];
}
renderInlineDefectList(index);
defectRow.style.display = '';
}
};
/**
* 인라인 부적합 목록 렌더링
*/
function renderInlineDefectList(index) {
const listContainer = document.getElementById(`defectList_${index}`);
if (!listContainer) return;
const defects = tempDefects[index] || [];
listContainer.innerHTML = defects.map((defect, i) => `
<div class="defect-inline-item" data-defect-index="${i}">
<select class="defect-select"
onchange="updateInlineDefect('${index}', ${i}, 'error_type_id', this.value)">
<option value="">원인 선택</option>
${errorTypes.map(et => `<option value="${et.id}" ${defect.error_type_id == et.id ? 'selected' : ''}>${et.name}</option>`).join('')}
</select>
<div class="defect-time-input"
onclick="openDefectTimePicker('${index}', ${i})">
<span class="defect-time-value" id="defectTime_${index}_${i}">${defect.defect_hours || 0}</span>
<span class="defect-time-unit">시간</span>
</div>
<button type="button" class="btn-remove-defect" onclick="removeInlineDefect('${index}', ${i})"></button>
</div>
`).join('');
updateDefectSummary(index);
}
/**
* 인라인 부적합 추가
*/
window.addInlineDefect = function(index) {
if (!tempDefects[index]) {
tempDefects[index] = [];
}
tempDefects[index].push({
error_type_id: '',
defect_hours: 0,
note: ''
});
renderInlineDefectList(index);
};
/**
* 인라인 부적합 수정
*/
window.updateInlineDefect = function(index, defectIndex, field, value) {
if (tempDefects[index] && tempDefects[index][defectIndex]) {
if (field === 'defect_hours') {
tempDefects[index][defectIndex][field] = parseFloat(value) || 0;
} else {
tempDefects[index][defectIndex][field] = value;
}
updateDefectSummary(index);
updateHiddenDefectFields(index);
}
};
/**
* 인라인 부적합 삭제
*/
window.removeInlineDefect = function(index, defectIndex) {
if (tempDefects[index]) {
tempDefects[index].splice(defectIndex, 1);
// 모든 부적합이 삭제되면 영역 숨기기
if (tempDefects[index].length === 0) {
const defectRow = document.getElementById(`defectRow_${index}`);
if (defectRow) {
defectRow.style.display = 'none';
}
} else {
renderInlineDefectList(index);
}
updateDefectSummary(index);
updateHiddenDefectFields(index);
}
};
/**
* 부적합 시간 선택기 열기 (시간 선택 팝오버 재사용)
*/
window.openDefectTimePicker = function(index, defectIndex) {
currentEditingField = { index, type: 'defect', defectIndex };
// 현재 값 가져오기
const defects = tempDefects[index] || [];
currentTimeValue = defects[defectIndex]?.defect_hours || 0;
// 팝오버 표시
const overlay = document.getElementById('timePickerOverlay');
const title = document.getElementById('timePickerTitle');
title.textContent = '부적합 시간 선택';
updateTimeDisplay();
overlay.style.display = 'flex';
// ESC 키로 닫기
document.addEventListener('keydown', handleEscapeKey);
};
/**
* hidden input 필드 업데이트
*/
function updateHiddenDefectFields(index) {
const defects = tempDefects[index] || [];
// 총 부적합 시간 계산
const totalErrorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
// hidden input에 대표 error_type_id 저장 (첫 번째 값)
const errorTypeInput = document.getElementById(`errorType_${index}`);
if (errorTypeInput && defects.length > 0 && defects[0].error_type_id) {
errorTypeInput.value = defects[0].error_type_id;
} else if (errorTypeInput) {
errorTypeInput.value = '';
}
// 부적합 시간 input 업데이트
const errorHoursInput = document.getElementById(`errorHours_${index}`);
if (errorHoursInput) {
errorHoursInput.value = totalErrorHours;
}
}
/**
* 부적합 요약 텍스트 업데이트
*/
function updateDefectSummary(index) {
const summaryEl = document.getElementById(`defectSummary_${index}`);
const toggleBtn = document.getElementById(`defectToggle_${index}`);
if (!summaryEl) return;
const defects = tempDefects[index] || [];
const validDefects = defects.filter(d => d.error_type_id && d.defect_hours > 0);
if (validDefects.length === 0) {
summaryEl.textContent = '없음';
summaryEl.style.color = '#6b7280';
if (toggleBtn) toggleBtn.classList.remove('has-defect');
} else {
const totalHours = validDefects.reduce((sum, d) => sum + d.defect_hours, 0);
if (validDefects.length === 1) {
const typeName = errorTypes.find(et => et.id == validDefects[0].error_type_id)?.name || '부적합';
summaryEl.textContent = `${typeName} ${totalHours}h`;
} else {
summaryEl.textContent = `${validDefects.length}${totalHours}h`;
}
summaryEl.style.color = '#dc2626';
if (toggleBtn) toggleBtn.classList.add('has-defect');
}
// hidden 필드도 업데이트
updateHiddenDefectFields(index);
}