- 페이지 폴더 재구성: 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>
2760 lines
89 KiB
JavaScript
2760 lines
89 KiB
JavaScript
// 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, '"')}", "${(cat.layout_image || '').replace(/"/g, '"')}")'>
|
||
<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, '"')}")'>
|
||
<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);
|
||
}
|