// daily-work-report.js - 브라우저 호환 버전
// =================================================================
// 🌐 API 설정 (window 객체에서 가져오기)
// =================================================================
// API 설정은 api-config.js에서 window 객체에 설정됨
// 전역 변수 → DailyWorkReportState 프록시 사용 (state.js에서 window 프록시 정의)
// workTypes, workStatusTypes, errorTypes, issueCategories, issueItems,
// workers, projects, selectedWorkers, incompleteTbms, tempDefects,
// dailyIssuesCache, currentTab, currentStep, editingWorkId, workEntryCounter,
// currentDefectIndex, currentEditingField, currentTimeValue,
// selectedWorkplace, selectedWorkplaceName, selectedWorkplaceCategory, selectedWorkplaceCategoryName
// 지도 관련 변수 (프록시 아님)
var mapCanvas = null;
var mapCtx = null;
var mapImage = null;
var mapRegions = [];
// 시간 선택 관련 변수
// currentEditingField, currentTimeValue → DailyWorkReportState 프록시 사용
// 당일 신고 리마인더 관련 변수
// dailyIssuesCache → DailyWorkReportState 프록시 사용
// =================================================================
// 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;
// 각 세션 날짜에 대해 관련 신고 조회
await loadDailyIssuesForTbms();
renderTbmWorkList();
} catch (error) {
console.error('미완료 TBM 로드 오류:', error);
showMessage('TBM 작업 목록을 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* TBM 세션들에 대한 당일 신고 조회
* - 각 세션 날짜별로 관련 신고를 가져와서 캐시에 저장
*/
async function loadDailyIssuesForTbms() {
if (!incompleteTbms || incompleteTbms.length === 0) {
console.log('[작업보고서] 미완료 TBM 없음, 신고 조회 건너뜀');
return;
}
console.log('[작업보고서] TBM 세션 수:', incompleteTbms.length);
// 세션별로 고유한 날짜 + 작업장 조합 수집
const sessionKeys = new Set();
incompleteTbms.forEach(tbm => {
const dateStr = formatDateForApi(tbm.session_date);
console.log('[작업보고서] TBM 세션 날짜:', tbm.session_date, '→ 변환:', dateStr);
if (dateStr) {
// 날짜_작업장ID_프로젝트ID 형태로 키 생성
const key = `${dateStr}_${tbm.workplace_id || 0}_${tbm.project_id || 0}`;
sessionKeys.add(key);
}
});
// 각 날짜에 대해 신고 조회
const uniqueDates = [...new Set([...sessionKeys].map(k => k.split('_')[0]))];
console.log('[작업보고서] 조회할 날짜들:', uniqueDates);
for (const dateStr of uniqueDates) {
if (dailyIssuesCache[dateStr]) {
console.log(`[작업보고서] 캐시 사용 (${dateStr}):`, dailyIssuesCache[dateStr].length, '건');
continue;
}
try {
console.log(`[작업보고서] 신고 API 호출: /work-issues?start_date=${dateStr}&end_date=${dateStr}`);
const response = await window.apiCall(`/work-issues?start_date=${dateStr}&end_date=${dateStr}`);
if (response.success) {
dailyIssuesCache[dateStr] = response.data || [];
console.log(`[작업보고서] 신고 로드 완료 (${dateStr}):`, dailyIssuesCache[dateStr].length, '건');
} else {
console.warn(`[작업보고서] 신고 API 실패:`, response);
dailyIssuesCache[dateStr] = [];
}
} catch (error) {
console.error(`[작업보고서] 신고 조회 오류 (${dateStr}):`, error);
dailyIssuesCache[dateStr] = [];
}
}
console.log('[작업보고서] 전체 신고 캐시:', dailyIssuesCache);
}
/**
* 특정 날짜의 모든 신고 반환 (작업장소 관계없이)
* - 참고용으로 해당 날짜에 발생한 모든 신고를 표시
* @param {string} dateStr - 날짜 (YYYY-MM-DD)
* @param {number} workplaceId - 작업장소 ID (현재 미사용, 향후 하이라이트 용도)
* @param {number} projectId - 프로젝트 ID (현재 미사용)
* @returns {Array} 해당 날짜의 모든 신고 목록
*/
function getRelatedIssues(dateStr, workplaceId, projectId) {
const issues = dailyIssuesCache[dateStr] || [];
// 해당 날짜의 모든 신고를 반환 (작업장소 필터 제거)
// 사용자가 참고하여 관련 여부를 직접 판단하도록 함
return issues;
}
/**
* 날짜를 API 형식(YYYY-MM-DD)으로 변환
*/
function formatDateForApi(date) {
if (window.CommonUtils) return window.CommonUtils.formatDate(date) || null;
if (!date) return null;
const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) return null;
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
}
/**
* 사용자 정보 가져오기 (auth-check.js와 동일한 로직)
*/
function getUser() {
if (window.getSSOUser) return window.getSSOUser();
const raw = localStorage.getItem('sso_user') || localStorage.getItem('user');
try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; }
}
/**
* TBM 작업 목록 렌더링 (날짜별 > 세션별 그룹화)
* - 날짜별로 접기/펼치기 가능
* - 날짜 헤더에 이슈 요약 표시
*/
function renderTbmWorkList() {
const container = document.getElementById('tbmWorkList');
// 1단계: 날짜별로 그룹화
const byDate = {};
if (incompleteTbms && incompleteTbms.length > 0) {
incompleteTbms.forEach((tbm, index) => {
const dateStr = formatDateForApi(tbm.session_date);
if (!byDate[dateStr]) {
byDate[dateStr] = {
date: tbm.session_date,
sessions: {}
};
}
// 2단계: 날짜 내에서 세션별로 그룹화
const sessionKey = `${tbm.session_id}_${dateStr}`;
if (!byDate[dateStr].sessions[sessionKey]) {
byDate[dateStr].sessions[sessionKey] = {
session_id: tbm.session_id,
session_date: tbm.session_date,
created_by_name: tbm.leader_name || tbm.created_by_name || '-',
items: []
};
}
byDate[dateStr].sessions[sessionKey].items.push({ ...tbm, originalIndex: index });
});
}
// 날짜 정렬 (최신순)
const sortedDates = Object.keys(byDate).sort((a, b) => new Date(b) - new Date(a));
// 레거시 호환: 기존 groupedTbms 구조도 유지
const groupedTbms = {};
sortedDates.forEach(dateStr => {
Object.assign(groupedTbms, byDate[dateStr].sessions);
});
let html = `
작업보고서 목록
➕ 작업 추가
`;
// 수동 입력 섹션 먼저 추가 (맨 위)
html += `
`;
// 날짜별로 테이블 생성 (접기/펼치기 가능)
sortedDates.forEach((dateStr, dateIndex) => {
const dateData = byDate[dateStr];
const sessions = Object.values(dateData.sessions);
const totalWorkers = sessions.reduce((sum, s) => sum + s.items.length, 0);
// 해당 날짜의 모든 신고 조회
const relatedIssues = getRelatedIssues(dateStr);
const nonconformityCount = relatedIssues.filter(i => i.category_type === 'nonconformity').length;
const safetyCount = relatedIssues.filter(i => i.category_type === 'safety').length;
const hasIssues = relatedIssues.length > 0;
// 요일 계산
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
const dayOfWeek = dayNames[new Date(dateStr).getDay()];
// 오늘 날짜인지 확인
const today = formatDateForApi(new Date());
const isToday = dateStr === today;
// 날짜 그룹 시작 (기본적으로 오늘만 펼침)
const isExpanded = isToday || dateIndex === 0;
html += `
`;
// 신고 리마인더 HTML 생성 (날짜 그룹 내부)
if (hasIssues) {
html += `
${relatedIssues.slice(0, 5).map(issue => {
// 아이템명과 추가설명 조합
let itemText = issue.issue_item_name || '';
if (issue.additional_description) {
itemText = itemText ? `${itemText} - ${issue.additional_description}` : issue.additional_description;
}
return `
${issue.category_type === 'safety' ? '안전' : '부적합'}
${issue.issue_category_name || ''}
${itemText || '-'}
${issue.workplace_name || issue.custom_location || ''}
${getStatusLabel(issue.status)}
`}).join('')}
${relatedIssues.length > 5 ? `
외 ${relatedIssues.length - 5}건 더 있음
` : ''}
💡 위 문제로 인해 작업이 지연되었다면, 아래에서 부적합 시간을 추가해주세요.
`;
}
// 해당 날짜의 각 세션별로 테이블 생성
sessions.forEach(group => {
const key = `${group.session_id}_${dateStr}`;
html += `
작업자
프로젝트
공정
작업
작업장소
작업시간
부적합
제출
${group.items.map(tbm => {
const index = tbm.originalIndex;
// 이 작업자의 작업장소와 관련된 이슈가 있는지 확인 (부적합 버튼 강조용)
const hasRelatedIssue = relatedIssues.some(issue => {
if (issue.category_type !== 'nonconformity') return false;
// 작업장소 매칭
if (tbm.workplace_id && issue.workplace_id) {
return tbm.workplace_id === issue.workplace_id;
}
if (tbm.workplace_name && (issue.workplace_name || issue.custom_location)) {
const issueLocation = issue.workplace_name || issue.custom_location || '';
return issueLocation.includes(tbm.workplace_name) || tbm.workplace_name.includes(issueLocation);
}
return false;
});
return `
${tbm.worker_name || '작업자'}
${tbm.job_type || '-'}
${tbm.project_name || '-'}
${tbm.work_type_name || '-'}
${tbm.task_name || '-'}
${tbm.category_name || ''}
${tbm.workplace_name || '-'}
시간 선택
없음
제출
`;
}).join('')}
📤 이 세션 일괄제출 (${group.items.length}건)
`;
});
// 날짜 그룹 닫기
html += `
`;
});
container.innerHTML = html;
}
/**
* 날짜 그룹 접기/펼치기 토글
*/
window.toggleDateGroup = function(dateStr) {
const group = document.querySelector(`.date-group[data-date="${dateStr}"]`);
if (!group) return;
const content = group.querySelector('.date-group-content');
const icon = group.querySelector('.date-toggle-icon');
const isExpanded = group.classList.contains('expanded');
if (isExpanded) {
group.classList.remove('expanded');
group.classList.add('collapsed');
content.style.display = 'none';
icon.textContent = '▶';
} else {
group.classList.remove('collapsed');
group.classList.add('expanded');
content.style.display = 'block';
icon.textContent = '▼';
}
};
/**
* 부적합 시간 입력 처리
*/
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) {
// busy guard - 중복 제출 방지
const submitBtn = document.querySelector(`tr[data-index="${index}"][data-type="tbm"] .btn-submit-compact`);
if (submitBtn && submitBtn.classList.contains('is-loading')) return;
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);
// item_id를 error_type_id로 사용 (issue_report_items.item_id)
const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null;
// 필수 필드 검증
if (!totalHours || totalHours <= 0) {
showMessage('작업시간을 입력해주세요.', 'error');
return;
}
if (errorHours > totalHours) {
showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error');
return;
}
// 로딩 상태 시작
if (submitBtn) {
submitBtn.classList.add('is-loading');
submitBtn.disabled = true;
submitBtn.textContent = '제출 중';
}
// 부적합 원인 유효성 검사 (issue_report_id 또는 category_id 또는 error_type_id 필요)
console.log('🔍 부적합 검증 시작:', defects.map(d => ({
defect_hours: d.defect_hours,
category_id: d.category_id,
item_id: d.item_id,
error_type_id: d.error_type_id,
issue_report_id: d.issue_report_id,
_saved: d._saved
})));
const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id && !d.issue_report_id && !d.category_id && !d.item_id);
if (invalidDefects.length > 0) {
console.error('❌ 유효하지 않은 부적합:', invalidDefects);
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.task_id, // task_id를 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.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0);
console.log('📋 부적합 원인 필터링:', {
전체: defects.length,
유효: validDefects.length,
validDefects: validDefects.map(d => ({
category_id: d.category_id,
item_id: d.item_id,
defect_hours: d.defect_hours,
_saved: d._saved
}))
});
if (validDefects.length > 0) {
// 내부 플래그 제거 (백엔드 전송용)
const defectsToSend = validDefects.map(d => ({
issue_report_id: d.issue_report_id || null,
category_id: d.category_id || null,
item_id: d.item_id || null,
error_type_id: d.error_type_id || null,
defect_hours: d.defect_hours,
note: d.note || ''
}));
console.log('📤 부적합 저장 요청:', defectsToSend);
const defectResponse = await window.apiCall(`/daily-work-reports/${response.data.report_id}/defects`, 'PUT', {
defects: defectsToSend
});
if (!defectResponse.success) {
console.error('❌ 부적합 저장 실패:', defectResponse);
showMessage('작업보고서는 저장되었으나 부적합 원인 저장에 실패했습니다.', 'warning');
} else {
console.log('✅ 부적합 저장 성공:', defectResponse);
}
} else {
console.log('⚠️ 유효한 부적합 항목이 없어 저장 건너뜀');
}
}
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);
} finally {
// 로딩 상태 해제
if (submitBtn) {
submitBtn.classList.remove('is-loading');
submitBtn.disabled = false;
submitBtn.textContent = '제출';
}
}
};
/**
* TBM 세션 일괄제출
*/
window.batchSubmitTbmSession = async function(sessionKey) {
// busy guard - 일괄제출 버튼
const batchBtn = document.querySelector(`[data-session-key="${sessionKey}"] ~ .batch-submit-container .btn-batch-submit, .tbm-session-group[data-session-key="${sessionKey}"] .btn-batch-submit`);
if (batchBtn && batchBtn.classList.contains('is-loading')) return;
// 해당 세션의 모든 항목 가져오기
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.task_id, // task_id를 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 = batchBtn || event.target;
submitBtn.classList.add('is-loading');
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.classList.remove('is-loading');
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 = `
작업자 선택
${workers.map(w => `${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')}) `).join('')}
프로젝트 선택
${projects.map(p => `${escapeHtml(p.project_name)} `).join('')}
공정 선택
${workTypes.map(wt => `${escapeHtml(wt.name)} `).join('')}
공정을 먼저 선택하세요
시간 선택
없음
제출
✕
`;
tbody.appendChild(newRow);
// 부적합 인라인 영역 행 추가
const defectRow = document.createElement('tr');
defectRow.className = 'defect-row';
defectRow.id = `defectRow_${manualIndex}`;
defectRow.style.display = 'none';
defectRow.innerHTML = `
`;
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 = '공정을 먼저 선택하세요 ';
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 = `
작업 선택
${tasks.map(task => `${escapeHtml(task.task_name)} `).join('')}
`;
} else {
taskSelect.disabled = true;
taskSelect.innerHTML = '등록된 작업이 없습니다 ';
}
} catch (error) {
console.error('작업 목록 로드 오류:', error);
taskSelect.disabled = true;
taskSelect.innerHTML = '작업 로드 실패 ';
}
};
/**
* 수동 입력 부적합 시간 토글
*/
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 => {
const safeId = parseInt(cat.category_id) || 0;
const safeName = escapeHtml(cat.category_name);
const safeImage = escapeHtml(cat.layout_image || '');
return `
🏭
${safeName}
`;
}).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 => {
const safeId = parseInt(wp.workplace_id) || 0;
const safeName = escapeHtml(wp.workplace_name);
return `
📍
${safeName}
`;
}).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 = `
✓
작업장소 선택됨
🏭 ${escapeHtml(selectedWorkplaceCategoryName)}
📍 ${escapeHtml(selectedWorkplaceName)}
`;
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.selectExternalWorkplace = function() {
const manualIndex = window.currentManualIndex;
// 외부 작업장소 ID는 0 또는 특별한 값으로 설정 (DB에 저장시 처리 필요)
const externalCategoryId = 0;
const externalCategoryName = '외부';
const externalWorkplaceId = 0;
const externalWorkplaceName = '외부 (외근/연차/휴무)';
// hidden input에 값 설정
document.getElementById(`workplaceCategory_${manualIndex}`).value = externalCategoryId;
document.getElementById(`workplace_${manualIndex}`).value = externalWorkplaceId;
// 선택 결과 표시
const displayDiv = document.getElementById(`workplaceDisplay_${manualIndex}`);
if (displayDiv) {
displayDiv.innerHTML = `
✓
외부 선택됨
🌐 ${escapeHtml(externalWorkplaceName)}
`;
displayDiv.style.background = '#f0f9ff';
displayDiv.style.borderColor = '#0ea5e9';
}
// 모달 닫기
document.getElementById('workplaceModal').style.display = 'none';
showMessage('외부 작업장소가 선택되었습니다.', 'success');
};
/**
* 수동 작업보고서 제출
*/
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);
// item_id를 error_type_id로 사용 (issue_report_items.item_id)
const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : 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 === '' || workplaceId === null || workplaceId === undefined) {
showMessage('작업장소를 선택해주세요.', 'error');
return;
}
if (!totalHours || totalHours <= 0) {
showMessage('작업시간을 입력해주세요.', 'error');
return;
}
if (errorHours > totalHours) {
showMessage('부적합 처리 시간은 총 작업시간을 초과할 수 없습니다.', 'error');
return;
}
// 부적합 원인 유효성 검사 (issue_report_id 또는 error_type_id 필요)
const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id && !d.issue_report_id && !d.category_id && !d.item_id);
if (invalidDefects.length > 0) {
showMessage('부적합 시간이 있는 항목은 원인을 선택해주세요.', 'error');
return;
}
// 서비스 레이어가 기대하는 형식으로 변환
// 주의: 서비스에서 task_id를 work_type_id 컬럼에 매핑함
const reportData = {
report_date: reportDate,
worker_id: parseInt(workerId),
work_entries: [{
project_id: parseInt(projectId),
task_id: parseInt(taskId), // 서비스에서 work_type_id로 매핑됨
work_hours: totalHours,
work_status_id: errorHours > 0 ? 2 : 1,
error_type_id: errorTypeId ? parseInt(errorTypeId) : null
}]
};
try {
// 429 오류 재시도 로직 포함
let response;
let retries = 3;
for (let i = 0; i < retries; i++) {
try {
response = await window.apiCall('/daily-work-reports', 'POST', reportData);
break;
} catch (err) {
if ((err.message?.includes('429') || err.message?.includes('너무 많은 요청')) && i < retries - 1) {
const waitTime = (i + 1) * 2000;
showMessage(`서버가 바쁩니다. ${waitTime/1000}초 후 재시도...`, 'loading');
await new Promise(r => setTimeout(r, waitTime));
continue;
}
throw err;
}
}
if (!response.success) {
throw new Error(response.message || '작업보고서 제출 실패');
}
// 부적합 원인이 있으면 저장 (이슈 기반 또는 레거시)
const reportId = response.data?.inserted_ids?.[0] || response.data?.workReport_ids?.[0];
if (defects.length > 0 && reportId) {
const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0);
if (validDefects.length > 0) {
await window.apiCall(`/daily-work-reports/${reportId}/defects`, 'PUT', {
defects: validDefects
});
}
}
// 행 제거 (부적합 임시 데이터도 함께 삭제됨)
removeManualWorkRow(manualIndex);
showMessage('작업보고서가 제출되었습니다.', 'success');
// 남은 행이 없으면 완료 메시지
const remainingRows = document.querySelectorAll('#manualWorkTableBody tr[data-index]');
if (remainingRows.length === 0) {
showSaveResultModal(
'success',
'작업보고서 제출 완료',
'모든 작업보고서가 성공적으로 제출되었습니다.'
);
}
} catch (error) {
console.error('수동 작업보고서 제출 오류:', error);
showMessage('제출 실패: ' + error.message, 'error');
}
};
/**
* 딜레이 함수
*/
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* API 호출 (429 재시도 포함)
*/
async function apiCallWithRetry(url, method, data, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await window.apiCall(url, method, data);
return response;
} catch (error) {
// 429 Rate Limit 오류인 경우 재시도
if (error.message && error.message.includes('429') || error.message.includes('너무 많은 요청')) {
if (attempt < maxRetries) {
const waitTime = attempt * 2000; // 2초, 4초, 6초 대기
console.log(`Rate limit 도달. ${waitTime/1000}초 후 재시도... (${attempt}/${maxRetries})`);
await delay(waitTime);
continue;
}
}
throw error;
}
}
}
/**
* 수동 작업보고서 일괄 제출
*/
window.submitAllManualWorkReports = async function() {
const rows = document.querySelectorAll('#manualWorkTableBody tr[data-index]');
if (rows.length === 0) {
showMessage('제출할 작업보고서가 없습니다.', 'error');
return;
}
// 확인 다이얼로그
if (!confirm(`${rows.length}개의 작업보고서를 일괄 제출하시겠습니까?`)) {
return;
}
let successCount = 0;
let failCount = 0;
const errors = [];
let currentIndex = 0;
showMessage(`작업보고서 제출 중... (0/${rows.length})`, 'loading');
// 각 행을 순차적으로 제출 (딜레이 포함)
for (const row of rows) {
currentIndex++;
const manualIndex = row.dataset.index;
// Rate Limit 방지를 위한 딜레이 (1초)
if (currentIndex > 1) {
await delay(1000);
}
showMessage(`작업보고서 제출 중... (${currentIndex}/${rows.length})`, 'loading');
try {
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);
// item_id를 error_type_id로 사용 (issue_report_items.item_id)
const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null;
// 필수 필드 검증
if (!workerId || !reportDate || !projectId || !workTypeId || !taskId || !totalHours || totalHours <= 0) {
errors.push(`행 ${manualIndex}: 필수 항목 누락`);
failCount++;
continue;
}
if (workplaceId === '' || workplaceId === null || workplaceId === undefined) {
errors.push(`행 ${manualIndex}: 작업장소 미선택`);
failCount++;
continue;
}
// 서비스 레이어가 기대하는 형식으로 변환
const reportData = {
report_date: reportDate,
worker_id: parseInt(workerId),
work_entries: [{
project_id: parseInt(projectId),
task_id: parseInt(taskId),
work_hours: totalHours,
work_status_id: errorHours > 0 ? 2 : 1,
error_type_id: errorTypeId ? parseInt(errorTypeId) : null
}]
};
const response = await apiCallWithRetry('/daily-work-reports', 'POST', reportData);
if (!response.success) {
throw new Error(response.message || '작업보고서 제출 실패');
}
// 부적합 원인이 있으면 저장
const reportId = response.data?.inserted_ids?.[0] || response.data?.workReport_ids?.[0];
if (defects.length > 0 && reportId) {
const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0);
if (validDefects.length > 0) {
await apiCallWithRetry(`/daily-work-reports/${reportId}/defects`, 'PUT', {
defects: validDefects
});
}
}
// 성공 - 행 제거
removeManualWorkRow(manualIndex);
successCount++;
} catch (error) {
console.error(`행 ${manualIndex} 제출 오류:`, error);
errors.push(`행 ${manualIndex}: ${error.message}`);
failCount++;
}
}
// 로딩 메시지 숨기기
hideMessage();
// 결과 표시
let resultMessage = `성공: ${successCount}건`;
if (failCount > 0) {
resultMessage += `, 실패: ${failCount}건`;
}
if (failCount > 0 && errors.length > 0) {
showSaveResultModal(
'warning',
'일괄 제출 완료 (일부 실패)',
`${resultMessage}\n\n실패 원인:\n${errors.slice(0, 5).join('\n')}${errors.length > 5 ? `\n... 외 ${errors.length - 5}건` : ''}`
);
} else {
showSaveResultModal(
'success',
'일괄 제출 완료',
resultMessage
);
}
};
/**
* 날짜 포맷 함수
*/
function formatDate(dateString) {
if (window.CommonUtils) return window.CommonUtils.formatDate(dateString);
return formatDateForApi(dateString);
}
/**
* 신고 상태 라벨 반환
*/
function getStatusLabel(status) {
const labels = {
'reported': '신고됨',
'received': '접수됨',
'in_progress': '처리중',
'completed': '완료',
'closed': '종료'
};
return labels[status] || status || '-';
}
/**
* 작성 완료된 작업보고서 로드
*/
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 = '작성된 작업보고서가 없습니다.
';
return;
}
const html = reports.map(report => `
프로젝트:
${escapeHtml(report.project_name || '-')}
공정:
${escapeHtml(report.work_type_name || '-')}
작업:
${escapeHtml(report.task_name || '-')}
작업시간:
${parseFloat(report.total_hours || report.work_hours || 0)}시간
${report.regular_hours !== undefined && report.regular_hours !== null ? `
정규 시간:
${parseFloat(report.regular_hours)}시간
` : ''}
${report.error_hours && report.error_hours > 0 ? `
부적합 처리:
${parseFloat(report.error_hours)}시간
부적합 원인:
${escapeHtml(report.error_type_name || '-')}
` : ''}
작성자:
${escapeHtml(report.created_by_name || '-')}
${report.start_time && report.end_time ? `
작업 시간:
${escapeHtml(report.start_time)} ~ ${escapeHtml(report.end_time)}
` : ''}
✏️ 수정
🗑️ 삭제
`).join('');
container.innerHTML = html;
}
/**
* 작업보고서 수정 모달 열기
*/
window.openEditReportModal = function(report) {
// 수정 모달이 없으면 동적 생성
let modal = document.getElementById('editReportModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'editReportModal';
modal.className = 'modal-overlay';
modal.style.cssText = 'display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1003; align-items: center; justify-content: center;';
modal.innerHTML = `
`;
document.body.appendChild(modal);
}
// 폼에 데이터 채우기
document.getElementById('editReportId').value = report.id;
document.getElementById('editWorkerName').value = report.worker_name || '작업자';
document.getElementById('editProjectId').value = report.project_id || '';
document.getElementById('editWorkHours').value = report.work_hours || report.total_hours || 0;
document.getElementById('editWorkStatusId').value = report.work_status_id || 1;
// 공정 선택 후 작업 목록 로드
const workTypeSelect = document.getElementById('editWorkTypeId');
// work_type_id가 실제로는 task_id를 저장하고 있으므로, task에서 work_type을 찾아야 함
// 일단 task 기반으로 찾기 시도
loadTasksForEdit().then(() => {
const taskSelect = document.getElementById('editTaskId');
// work_type_id 컬럼에 저장된 값이 실제로는 task_id
if (report.work_type_id) {
taskSelect.value = report.work_type_id;
}
});
modal.style.display = 'flex';
};
/**
* 수정 모달용 작업 목록 로드
*/
window.loadTasksForEdit = async function() {
const workTypeId = document.getElementById('editWorkTypeId').value;
const taskSelect = document.getElementById('editTaskId');
if (!workTypeId) {
taskSelect.innerHTML = '공정을 먼저 선택하세요 ';
return;
}
try {
const response = await window.apiCall(`/tasks?work_type_id=${workTypeId}`);
const tasks = response.data || response || [];
taskSelect.innerHTML = '작업 선택 ' +
tasks.map(t => `${escapeHtml(t.task_name)} `).join('');
} catch (error) {
console.error('작업 목록 로드 오류:', error);
taskSelect.innerHTML = '로드 실패 ';
}
};
/**
* 수정 모달 닫기
*/
window.closeEditReportModal = function() {
const modal = document.getElementById('editReportModal');
if (modal) {
modal.style.display = 'none';
}
};
/**
* 수정된 보고서 저장
*/
window.saveEditedReport = async function() {
const reportId = document.getElementById('editReportId').value;
const projectId = document.getElementById('editProjectId').value;
const taskId = document.getElementById('editTaskId').value;
const workHours = parseFloat(document.getElementById('editWorkHours').value);
const workStatusId = document.getElementById('editWorkStatusId').value;
if (!projectId || !taskId || !workHours) {
showMessage('필수 항목을 모두 입력해주세요.', 'error');
return;
}
try {
const updateData = {
project_id: parseInt(projectId),
work_type_id: parseInt(taskId), // task_id가 work_type_id 컬럼에 저장됨
work_hours: workHours,
work_status_id: parseInt(workStatusId)
};
const response = await window.apiCall(`/daily-work-reports/${reportId}`, 'PUT', updateData);
if (response.success) {
showMessage('작업보고서가 수정되었습니다.', 'success');
closeEditReportModal();
loadCompletedReports(); // 목록 새로고침
} else {
throw new Error(response.message || '수정 실패');
}
} catch (error) {
console.error('작업보고서 수정 오류:', error);
showMessage('수정 실패: ' + error.message, 'error');
}
};
/**
* 작업보고서 삭제
*/
window.deleteWorkReport = async function(reportId) {
if (!confirm('이 작업보고서를 삭제하시겠습니까?')) {
return;
}
try {
const response = await window.apiCall(`/daily-work-reports/${reportId}`, 'DELETE');
if (response.success) {
showMessage('작업보고서가 삭제되었습니다.', 'success');
loadCompletedReports(); // 목록 새로고침
} else {
throw new Error(response.message || '삭제 실패');
}
} catch (error) {
console.error('작업보고서 삭제 오류:', error);
showMessage('삭제 실패: ' + error.message, 'error');
}
};
// =================================================================
// 기존 함수들
// =================================================================
// 한국 시간 기준 오늘 날짜
function getKoreaToday() {
if (window.CommonUtils) return window.CommonUtils.getTodayKST();
const now = new Date();
return now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
}
// 현재 로그인한 사용자 정보 가져오기
function getCurrentUser() {
// SSO 사용자 정보 우선
if (window.getSSOUser) {
const ssoUser = window.getSSOUser();
if (ssoUser) return ssoUser;
}
try {
const token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
if (token) {
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
return JSON.parse(atob(payloadBase64));
}
}
} catch (error) {
console.log('토큰에서 사용자 정보 추출 실패:', error);
}
try {
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('user') || localStorage.getItem('userInfo');
if (userInfo) return JSON.parse(userInfo);
} catch (error) {
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
}
return null;
}
// 메시지 표시
function showMessage(message, type = 'info') {
const container = document.getElementById('message-container');
container.innerHTML = `${message}
`;
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 = `
${icon}
${title}
${message}
`;
// 상세 정보가 있으면 추가
if (details) {
if (Array.isArray(details) && details.length > 0) {
content += `
상세 정보:
${details.map(detail => `${detail} `).join('')}
`;
} else if (typeof details === 'string') {
content += `
`;
}
}
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 사용)');
// 생산팀 소속 작업자만 조회
const data = await window.apiCall(`/workers?limit=1000&department_id=1`);
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(`/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 response = await window.apiCall(`/daily-work-reports/work-types`);
const data = response.data || response;
if (Array.isArray(data) && data.length > 0) {
workTypes = data;
console.log('✅ 작업 유형 API 사용 (통합 설정):', workTypes.length + '개');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용:', error.message);
workTypes = [
{ id: 1, name: 'Base' },
{ id: 2, name: 'Vessel' },
{ id: 3, name: 'Piping' }
];
}
}
async function loadWorkStatusTypes() {
try {
const response = await window.apiCall(`/daily-work-reports/work-status-types`);
const data = response.data || response;
if (Array.isArray(data) && data.length > 0) {
workStatusTypes = data;
console.log('✅ 업무 상태 유형 API 사용 (통합 설정):', workStatusTypes.length + '개');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용');
workStatusTypes = [
{ id: 1, name: '정규' },
{ id: 2, name: '에러' }
];
}
}
async function loadErrorTypes() {
// 레거시 에러 유형 로드 (호환성)
try {
const response = await window.apiCall(`/daily-work-reports/error-types`);
const data = response.data || response;
if (Array.isArray(data) && data.length > 0) {
errorTypes = data;
}
} catch (error) {
errorTypes = [];
}
// 신고 카테고리 로드 (부적합 유형만)
try {
const catResponse = await window.apiCall('/work-issues/categories/type/nonconformity');
if (catResponse && catResponse.success && Array.isArray(catResponse.data)) {
issueCategories = catResponse.data;
console.log(`✅ 부적합 카테고리 ${issueCategories.length}개 로드`);
// 모든 아이템 로드
const itemResponse = await window.apiCall('/work-issues/items');
if (itemResponse && itemResponse.success && Array.isArray(itemResponse.data)) {
// 부적합 카테고리의 아이템만 필터링
const categoryIds = issueCategories.map(c => c.category_id);
issueItems = itemResponse.data.filter(item => categoryIds.includes(item.category_id));
console.log(`✅ 부적합 아이템 ${issueItems.length}개 로드`);
}
}
} catch (error) {
console.log('⚠️ 신고 카테고리 로드 실패:', error);
issueCategories = [];
issueItems = [];
}
}
// 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 = `
🛠️ TBM 팀 구성 자동 적용
오늘 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 = `
⚠️
에러 유형
에러 유형을 선택하세요
${errorTypes.map(et => `${et.name} `).join('')}
`;
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(`/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 = '📊 내가 입력한 오늘의 작업 현황을 불러오는 중... (통합 API)
';
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(`/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 = `
❌ 오늘의 작업 현황을 불러올 수 없습니다.
${error.message}
`;
}
}
// 본인 입력 작업자 현황 표시 (수정/삭제 기능 포함)
function displayMyDailyWorkers(data, date) {
const content = document.getElementById('dailyWorkersContent');
if (!Array.isArray(data) || data.length === 0) {
content.innerHTML = `
📝 내가 오늘(${date}) 입력한 작업이 없습니다.
새로운 작업을 추가해보세요!
`;
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 = `
`;
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 = escapeHtml(work.project_name || '미지정');
const workTypeName = escapeHtml(work.work_type_name || '미지정');
const workStatusName = escapeHtml(work.work_status_name || '미지정');
const workHours = parseFloat(work.work_hours || 0);
const errorTypeName = work.error_type_name ? escapeHtml(work.error_type_name) : null;
const workId = parseInt(work.id) || 0;
return `
${errorTypeName ? `
` : ''}
✏️ 수정
🗑️ 삭제
`;
}).join('');
return `
`;
}).join('');
content.innerHTML = headerHtml + '' + workersHtml + '
';
}
// 작업 항목 수정 함수 (통합 API 사용)
async function editWorkItem(workId) {
try {
console.log('수정할 작업 ID:', workId);
// 1. 기존 데이터 조회 (통합 API 사용)
showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading');
const workData = await window.apiCall(`/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 = `
🏗️ 프로젝트
프로젝트 선택
${projects.map(p => `
${p.project_name}
`).join('')}
⚙️ 작업 유형
작업 유형 선택
${workTypes.map(wt => `
${wt.name}
`).join('')}
📊 업무 상태
업무 상태 선택
${workStatusTypes.map(ws => `
${ws.name}
`).join('')}
❌ 에러 유형
에러 유형 선택
${errorTypes.map(et => `
${et.name}
`).join('')}
⏰ 작업 시간
`;
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(`/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(`/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 {
// app-init.js(defer)가 토큰/apiCall 설정 완료할 때까지 대기
if (window.waitForApi) {
await window.waitForApi(8000);
} else if (!window.apiCall) {
// waitForApi 없으면 간단 폴링
await new Promise((resolve, reject) => {
let elapsed = 0;
const iv = setInterval(() => { elapsed += 50; if (window.apiCall) { clearInterval(iv); resolve(); } else if (elapsed >= 8000) { clearInterval(iv); reject(new Error('apiCall timeout')); } }, 50);
});
}
await loadData();
setupEventListeners();
// TBM 작업 목록 로드 (기본 탭)
await loadIncompleteTbms();
console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)');
} catch (error) {
console.error('초기화 오류:', error);
showMessage('초기화 중 오류가 발생했습니다.', 'error');
}
}
// 페이지 로드 시 초기화 (module 스크립트는 DOMContentLoaded 이후 실행될 수 있음)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
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, issueReportId } = currentEditingField;
// 이슈 기반 부적합 시간 선택인 경우
if (type === 'issueDefect') {
if (tempDefects[index] && tempDefects[index][defectIndex] !== undefined) {
tempDefects[index][defectIndex].defect_hours = currentTimeValue;
// 시간 표시 업데이트
const timeDisplay = document.getElementById(`issueDefectTime_${index}_${issueReportId}`);
if (timeDisplay) {
timeDisplay.textContent = currentTimeValue;
}
// 요약 및 hidden 필드 업데이트
updateDefectSummary(index);
updateHiddenDefectFields(index);
}
closeTimePicker();
return;
}
// 레거시 부적합 시간 선택인 경우
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);
updateHiddenDefectFields(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] = [{
issue_report_id: null,
category_id: null,
item_id: null,
error_type_id: '', // 레거시 호환
defect_hours: 0,
note: ''
}];
}
renderInlineDefectList(index);
defectRow.style.display = '';
}
};
/**
* 인라인 부적합 목록 렌더링
* - 해당 날짜에 신고된 이슈 목록을 표시
* - 이슈 선택 → 시간 입력 방식
*/
function renderInlineDefectList(index) {
const listContainer = document.getElementById(`defectList_${index}`);
if (!listContainer) return;
// 해당 TBM의 날짜 가져오기
const tbm = incompleteTbms[index];
const dateStr = tbm ? formatDateForApi(tbm.session_date) : null;
const issues = dateStr ? (dailyIssuesCache[dateStr] || []) : [];
// 작업장소 정보 가져오기
const workerWorkplaceId = tbm?.workplace_id;
const workerWorkplaceName = tbm?.workplace_name;
// 부적합 유형 + 작업장소 일치하는 것만 필터링
const nonconformityIssues = issues.filter(i => {
// 부적합 유형만
if (i.category_type !== 'nonconformity') return false;
// 작업장소 매칭 (workplace_id 우선, 없으면 이름 비교)
if (workerWorkplaceId && i.workplace_id) {
return i.workplace_id === workerWorkplaceId;
}
if (workerWorkplaceName && (i.workplace_name || i.custom_location)) {
const issueLocation = i.workplace_name || i.custom_location || '';
return issueLocation.includes(workerWorkplaceName) || workerWorkplaceName.includes(issueLocation);
}
// 작업장소 정보가 없으면 포함하지 않음
return false;
});
const defects = tempDefects[index] || [];
console.log(`📝 [renderInlineDefectList] index=${index}, 부적합 수=${defects.length}`, defects);
// 이슈가 있으면 이슈 선택 UI, 없으면 레거시 UI
if (nonconformityIssues.length > 0) {
// 이슈 선택 방식 UI
let html = `
`;
nonconformityIssues.forEach(issue => {
// 이 이슈가 이미 선택되었는지 확인
const existingDefect = defects.find(d => d.issue_report_id == issue.report_id);
const isSelected = !!existingDefect;
const defectHours = existingDefect?.defect_hours || 0;
// 아이템명과 추가설명 조합
let itemText = issue.issue_item_name || '';
if (issue.additional_description) {
itemText = itemText ? `${itemText} - ${issue.additional_description}` : issue.additional_description;
}
const safeReportId = parseInt(issue.report_id) || 0;
html += `
${escapeHtml(issue.issue_category_name || '부적합')}
${escapeHtml(itemText || '-')}
${escapeHtml(issue.workplace_name || issue.custom_location || '')}
${parseFloat(defectHours) || 0}
시간
`;
});
html += `
`;
// 레거시 방식도 추가 (기타 부적합 추가 버튼)
html += `
${renderLegacyDefects(index, defects)}
+ 부적합 추가
저장
부적합 내용이 저장되었습니다.
`;
listContainer.innerHTML = html;
} else {
// 이슈가 없으면 레거시 UI (error_types 선택)
const noIssueMsg = workerWorkplaceName
? `${escapeHtml(workerWorkplaceName)}에 신고된 부적합이 없습니다.`
: '신고된 부적합이 없습니다.';
listContainer.innerHTML = `
${noIssueMsg}
${renderLegacyDefects(index, defects)}
+ 부적합 추가
저장
부적합 내용이 저장되었습니다.
`;
}
updateDefectSummary(index);
}
/**
* 레거시 부적합 렌더링 (저장된 항목 + 입력 중인 항목 분리)
*/
function renderLegacyDefects(index, defects) {
// issue_report_id가 없는 직접 입력 부적합
const legacyDefects = defects.filter(d => !d.issue_report_id);
if (legacyDefects.length === 0) return '';
// 저장된 항목과 입력 중인 항목 분리
const savedDefects = legacyDefects.filter(d => d._saved);
const editingDefects = legacyDefects.filter(d => !d._saved);
let html = '';
// 저장된 부적합 표시 (위쪽)
if (savedDefects.length > 0) {
html += `
`;
savedDefects.forEach(defect => {
const defectIndex = defects.indexOf(defect);
const categoryName = defect.category_id
? (issueCategories.find(c => c.category_id == defect.category_id)?.category_name || '미분류')
: '미분류';
const itemName = defect.item_id
? (issueItems.find(i => i.item_id == defect.item_id)?.item_name || '')
: '';
const displayText = itemName
? `${categoryName} → ${itemName}`
: categoryName;
html += `
${categoryName}
${itemName}${defect.note ? ' - ' + defect.note : ''}
${defect.defect_hours || 0}시간
수정
삭제
`;
});
html += `
`;
}
// 입력 중인 부적합 표시 (아래쪽)
if (editingDefects.length > 0) {
html += ``;
editingDefects.forEach(defect => {
const defectIndex = defects.indexOf(defect);
html += renderDefectInputForm(index, defect, defectIndex);
});
html += `
`;
}
return html;
}
/**
* 부적합 입력 폼 렌더링 (단일 항목)
*/
function renderDefectInputForm(index, defect, defectIndex) {
// 대분류 (카테고리) 옵션
const categoryOptions = issueCategories.map(cat =>
`${cat.category_name} `
).join('');
// 소분류 (아이템) 옵션 - 선택된 카테고리의 아이템만
const filteredItems = defect.category_id
? issueItems.filter(item => item.category_id == defect.category_id)
: [];
const itemOptions = filteredItems.map(item =>
`${item.item_name} `
).join('');
// 카테고리명/아이템명 표시 (미리보기용)
const categoryName = defect.category_id
? (issueCategories.find(c => c.category_id == defect.category_id)?.category_name || '')
: '';
const itemName = defect.item_id
? (issueItems.find(i => i.item_id == defect.item_id)?.item_name || '')
: '';
return `
대분류
선택
${categoryOptions}
+ 새 대분류 추가
소분류
선택
${itemOptions}
${defect.category_id ? '+ 새 소분류 추가 ' : ''}
시간
${defect.defect_hours || 0}
시간
×
${(categoryName || itemName) ? `
${categoryName}${itemName ? ' → ' + itemName : ''}${defect.note ? ' → ' + defect.note : ''}
` : ''}
`;
}
/**
* 대분류 선택 변경
*/
window.onDefectCategoryChange = async function(index, defectIndex, value) {
if (!tempDefects[index] || !tempDefects[index][defectIndex]) return;
const defect = tempDefects[index][defectIndex];
if (value === '__new__') {
// 새 카테고리 추가 모달
const newName = prompt('새 대분류(카테고리) 이름을 입력하세요:');
if (newName && newName.trim()) {
try {
const response = await window.apiCall('/work-issues/categories', 'POST', {
category_name: newName.trim(),
category_type: 'nonconformity',
severity: 'medium'
});
if (response.success) {
// 카테고리 목록 새로고침
await loadErrorTypes();
// 새로 생성된 카테고리 선택
const newCat = issueCategories.find(c => c.category_name === newName.trim());
if (newCat) {
defect.category_id = newCat.category_id;
defect.item_id = null;
}
} else {
alert('카테고리 추가 실패: ' + (response.error || '알 수 없는 오류'));
defect.category_id = null;
}
} catch (e) {
alert('카테고리 추가 중 오류: ' + e.message);
defect.category_id = null;
}
} else {
// 취소 시 이전 값 유지
defect.category_id = defect.category_id || null;
}
} else if (value) {
defect.category_id = parseInt(value);
defect.item_id = null; // 카테고리 변경 시 소분류 초기화
} else {
defect.category_id = null;
defect.item_id = null;
}
renderInlineDefectList(index);
updateHiddenDefectFields(index);
};
/**
* 소분류 선택 변경
*/
window.onDefectItemChange = async function(index, defectIndex, value) {
if (!tempDefects[index] || !tempDefects[index][defectIndex]) return;
const defect = tempDefects[index][defectIndex];
if (value === '__new__') {
// 새 아이템 추가 모달
const newName = prompt('새 소분류(항목) 이름을 입력하세요:');
if (newName && newName.trim() && defect.category_id) {
try {
const response = await window.apiCall('/work-issues/items', 'POST', {
category_id: defect.category_id,
item_name: newName.trim(),
severity: 'medium'
});
if (response.success) {
// 아이템 목록 새로고침
await loadErrorTypes();
// 새로 생성된 아이템 선택
const newItem = issueItems.find(i => i.item_name === newName.trim() && i.category_id == defect.category_id);
if (newItem) {
defect.item_id = newItem.item_id;
}
} else {
alert('항목 추가 실패: ' + (response.error || '알 수 없는 오류'));
}
} catch (e) {
alert('항목 추가 중 오류: ' + e.message);
}
}
} else if (value) {
defect.item_id = parseInt(value);
} else {
defect.item_id = null;
}
renderInlineDefectList(index);
updateHiddenDefectFields(index);
};
/**
* 이슈 부적합 토글 (체크박스)
*/
window.toggleIssueDefect = function(index, issueReportId, isChecked) {
if (!tempDefects[index]) {
tempDefects[index] = [];
}
if (isChecked) {
// 이슈 부적합 추가
tempDefects[index].push({
issue_report_id: issueReportId,
error_type_id: null, // 이슈 기반이므로 null
defect_hours: 0,
note: ''
});
} else {
// 이슈 부적합 제거
const idx = tempDefects[index].findIndex(d => d.issue_report_id == issueReportId);
if (idx !== -1) {
tempDefects[index].splice(idx, 1);
}
}
renderInlineDefectList(index);
updateHiddenDefectFields(index);
};
/**
* 이슈 부적합 시간 선택기 열기
*/
window.openIssueDefectTimePicker = function(index, issueReportId) {
// 해당 이슈의 defect 찾기
const defects = tempDefects[index] || [];
const defectIndex = defects.findIndex(d => d.issue_report_id == issueReportId);
if (defectIndex === -1) return;
currentEditingField = { index, type: 'issueDefect', issueReportId, defectIndex };
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);
};
/**
* 레거시 부적합 추가 (error_types 기반)
*/
window.addLegacyDefect = function(index) {
if (!tempDefects[index]) {
tempDefects[index] = [];
}
tempDefects[index].push({
issue_report_id: null,
category_id: null,
item_id: null,
error_type_id: '', // 레거시 호환
defect_hours: 0,
note: ''
});
renderInlineDefectList(index);
};
/**
* 부적합 카테고리/아이템 선택 업데이트 (레거시 호환)
* @deprecated onDefectCategoryChange, onDefectItemChange 사용 권장
*/
window.updateDefectCategory = function(index, defectIndex, value) {
// 레거시 호환 - 새 함수로 리다이렉트
if (value && value.startsWith('cat_')) {
onDefectCategoryChange(index, defectIndex, value.replace('cat_', ''));
} else if (value && value.startsWith('item_')) {
const itemId = parseInt(value.replace('item_', ''));
const item = issueItems.find(i => i.item_id === itemId);
if (item) {
if (!tempDefects[index]?.[defectIndex]) return;
tempDefects[index][defectIndex].category_id = item.category_id;
tempDefects[index][defectIndex].item_id = itemId;
renderInlineDefectList(index);
updateHiddenDefectFields(index);
}
}
};
/**
* 인라인 부적합 추가 (레거시 호환)
*/
window.addInlineDefect = function(index) {
addLegacyDefect(index);
};
/**
* 부적합 저장 확인 (유효성 검사 후 저장 상태로 변경)
*/
window.saveDefectsConfirm = function(index) {
const defects = tempDefects[index] || [];
// 입력 중인 항목만 (저장되지 않은 항목)
const editingDefects = defects.filter(d => !d.issue_report_id && !d._saved);
if (editingDefects.length === 0) {
alert('저장할 부적합 항목이 없습니다.\n"+ 부적합 추가" 버튼을 눌러 항목을 추가하세요.');
return;
}
// 유효성 검사
const invalidDefects = [];
editingDefects.forEach((defect, i) => {
const errors = [];
if (!defect.category_id) {
errors.push('대분류');
}
if (!defect.defect_hours || defect.defect_hours <= 0) {
errors.push('시간');
}
if (errors.length > 0) {
invalidDefects.push({ index: i + 1, errors });
}
});
if (invalidDefects.length > 0) {
const errorMsg = invalidDefects.map(d =>
`${d.index}번째 항목: ${d.errors.join(', ')} 미입력`
).join('\n');
alert(`입력이 완료되지 않은 항목이 있습니다.\n\n${errorMsg}`);
return;
}
// 모든 입력 중인 항목을 저장 상태로 변경
editingDefects.forEach(defect => {
defect._saved = true;
});
// UI 다시 렌더링
renderInlineDefectList(index);
updateHiddenDefectFields(index);
updateDefectSummary(index);
console.log(`[부적합 저장] index=${index}, 저장된 항목 수=${editingDefects.length}`);
};
/**
* 저장된 부적합 수정 (저장 상태 해제하여 입력 폼으로 이동)
*/
window.editSavedDefect = function(index, defectIndex) {
if (!tempDefects[index] || !tempDefects[index][defectIndex]) return;
// 원래 저장 상태였음을 기록 (취소 시 복원용)
tempDefects[index][defectIndex]._originalSaved = true;
tempDefects[index][defectIndex]._saved = false;
// UI 다시 렌더링
renderInlineDefectList(index);
};
/**
* 저장된 부적합 삭제
*/
window.deleteSavedDefect = function(index, defectIndex) {
if (!tempDefects[index] || !tempDefects[index][defectIndex]) return;
if (!confirm('이 부적합 항목을 삭제하시겠습니까?')) return;
tempDefects[index].splice(defectIndex, 1);
// UI 다시 렌더링
renderInlineDefectList(index);
updateHiddenDefectFields(index);
updateDefectSummary(index);
};
/**
* 부적합 입력 취소 (저장되지 않은 항목 삭제)
*/
window.cancelDefectEdit = function(index, defectIndex) {
if (!tempDefects[index] || !tempDefects[index][defectIndex]) return;
const defect = tempDefects[index][defectIndex];
// 원래 저장된 항목이었으면 저장 상태로 복원, 아니면 삭제
if (defect._originalSaved) {
defect._saved = true;
delete defect._originalSaved;
} else {
tempDefects[index].splice(defectIndex, 1);
}
// UI 다시 렌더링
renderInlineDefectList(index);
updateHiddenDefectFields(index);
updateDefectSummary(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);
// UI 다시 렌더링 (항상 - 빈 상태도 표시)
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 저장 (첫 번째 값, item_id fallback)
const errorTypeInput = document.getElementById(`errorType_${index}`);
const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null;
if (errorTypeInput && errorTypeId) {
errorTypeInput.value = errorTypeId;
} 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] || [];
// 이슈 기반 또는 레거시 부적합 중 시간이 입력된 것만 유효 (item_id도 체크)
const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || 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) {
let typeName = '부적합';
if (validDefects[0].issue_report_id) {
// 이슈 기반 - 이슈 캐시에서 이름 찾기
const tbm = incompleteTbms[index];
if (tbm) {
const dateStr = formatDateForApi(tbm.session_date);
const issues = dailyIssuesCache[dateStr] || [];
const issue = issues.find(i => i.report_id == validDefects[0].issue_report_id);
typeName = issue?.issue_item_name || issue?.issue_category_name || '부적합';
}
} else if (validDefects[0].item_id) {
// 신규 방식 - issue_report_items에서 이름 찾기
typeName = issueItems.find(i => i.item_id == validDefects[0].item_id)?.item_name || '부적합';
} else if (validDefects[0].category_id) {
// 카테고리만 선택된 경우
typeName = issueCategories.find(c => c.category_id == validDefects[0].category_id)?.category_name || '부적합';
} else if (validDefects[0].error_type_id) {
// 레거시 - error_types에서 이름 찾기 또는 issue_report_items에서 찾기
typeName = issueItems.find(i => i.item_id == validDefects[0].error_type_id)?.item_name ||
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);
}