feat: 대시보드 작업장 현황 지도 구현
- 실시간 작업장 현황을 지도로 시각화 - 작업장 관리 페이지에서 정의한 구역 정보 활용 - TBM 작업자 및 방문자 현황 표시 주요 변경사항: - dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거) - workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현 - modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가 시각화 방식: - 인원 없음: 회색 테두리 + 작업장 이름 - 내부 작업자: 파란색 영역 + 인원 수 - 외부 방문자: 보라색 영역 + 인원 수 - 둘 다: 초록색 영역 + 총 인원 수 기술 구현: - Canvas API 기반 사각형 영역 렌더링 - map-regions API를 통한 데이터 일관성 보장 - 클릭 이벤트로 상세 정보 모달 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,18 @@ async function loadIncompleteTbms() {
|
||||
throw new Error(response.message || '미완료 TBM 조회 실패');
|
||||
}
|
||||
|
||||
incompleteTbms = response.data || [];
|
||||
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);
|
||||
@@ -88,6 +99,14 @@ async function loadIncompleteTbms() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 정보 가져오기 (auth-check.js와 동일한 로직)
|
||||
*/
|
||||
function getUser() {
|
||||
const user = localStorage.getItem('user');
|
||||
return user ? JSON.parse(user) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 작업 목록 렌더링 (세션별 그룹화)
|
||||
*/
|
||||
@@ -120,11 +139,42 @@ function renderTbmWorkList() {
|
||||
</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>작업시간<br>(시간)</th>
|
||||
<th>부적합<br>(시간)</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">
|
||||
<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>
|
||||
@@ -150,7 +200,7 @@ function renderTbmWorkList() {
|
||||
${group.items.map(tbm => {
|
||||
const index = tbm.originalIndex;
|
||||
return `
|
||||
<tr data-index="${index}" data-type="tbm">
|
||||
<tr data-index="${index}" data-type="tbm" data-session-key="${key}">
|
||||
<td>
|
||||
<div class="worker-cell">
|
||||
<strong>${tbm.worker_name || '작업자'}</strong>
|
||||
@@ -202,41 +252,17 @@ function renderTbmWorkList() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="batch-submit-container">
|
||||
<button type="button"
|
||||
class="btn-batch-submit"
|
||||
onclick="batchSubmitTbmSession('${key}')">
|
||||
📤 이 세션 일괄제출 (${group.items.length}건)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// 수동 입력 섹션 추가
|
||||
html += `
|
||||
<div class="tbm-session-group" style="margin-top: 2rem;">
|
||||
<div class="tbm-session-header" style="background-color: #fef3c7;">
|
||||
<span class="tbm-session-badge" style="background-color: #f59e0b;">수동 입력</span>
|
||||
<span class="tbm-session-info">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>작업시간<br>(시간)</th>
|
||||
<th>부적합<br>(시간)</th>
|
||||
<th>부적합 원인</th>
|
||||
<th>제출</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manualWorkTableBody">
|
||||
<!-- 수동 입력 행들이 여기에 추가됩니다 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
@@ -286,13 +312,20 @@ window.submitTbmWorkReport = async function(index) {
|
||||
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: tbm.session_date,
|
||||
report_date: reportDate,
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
total_hours: totalHours,
|
||||
@@ -301,6 +334,9 @@ window.submitTbmWorkReport = async function(index) {
|
||||
work_status_id: errorHours > 0 ? 2 : 1
|
||||
};
|
||||
|
||||
console.log('🔍 TBM 제출 데이터:', JSON.stringify(reportData, null, 2));
|
||||
console.log('🔍 tbm 객체:', tbm);
|
||||
|
||||
try {
|
||||
const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', reportData);
|
||||
|
||||
@@ -325,6 +361,155 @@ window.submitTbmWorkReport = async function(index) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}건)`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 수동 작업 추가
|
||||
*/
|
||||
@@ -1075,15 +1260,23 @@ function showSaveResultModal(type, title, message, details = null) {
|
||||
`;
|
||||
|
||||
// 상세 정보가 있으면 추가
|
||||
if (details && details.length > 0) {
|
||||
content += `
|
||||
<div class="result-details">
|
||||
<h4>상세 정보:</h4>
|
||||
<ul>
|
||||
${details.map(detail => `<li>${detail}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
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 = '저장 결과';
|
||||
@@ -1114,6 +1307,9 @@ function closeSaveResultModal() {
|
||||
document.removeEventListener('keydown', closeSaveResultModal);
|
||||
}
|
||||
|
||||
// 전역에서 접근 가능하도록 window에 할당
|
||||
window.closeSaveResultModal = closeSaveResultModal;
|
||||
|
||||
// 단계 이동
|
||||
function goToStep(stepNumber) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
|
||||
Reference in New Issue
Block a user