Files
TK-FB-Project/web-ui/js/daily-work-report.js
Hyungi Ahn 480206912b feat: TBM 시스템 완성 - 작업 인계, 상세보기, 작업보고서 연동
## 주요 기능 추가

### 1. 작업 인계 시스템 (반차/조퇴 시)
- **인계 모달** (`handoverModal`)
  - 인계 사유 선택 (반차/조퇴/긴급/기타)
  - 인수자 (다른 팀장) 선택
  - 인계 날짜/시간 입력
  - 인계할 팀원 선택 (체크박스)
  - 인계 내용 메모

- **API 연동**
  - POST /api/tbm/handovers (인계 요청 생성)
  - 세션 정보와 팀 구성 자동 조회
  - from_leader_id 자동 설정

- **UI 개선**
  - TBM 카드에 "📤 인계" 버튼 추가
  - 인계할 팀원 목록 자동 로드
  - 현재 팀장 제외한 리더만 표시

### 2. TBM 상세보기 모달
- **상세 정보 표시** (`detailModal`)
  - 기본 정보 (팀장, 날짜, 프로젝트, 작업 장소, 작업 내용)
  - 안전 특이사항 (노란색 강조)
  - 팀 구성 (그리드 레이아웃)
  - 안전 체크리스트 (카테고리별 그룹화)

- **안전 체크 시각화**
  - / 아이콘으로 체크 상태 표시
  - 체크됨: 초록색 배경
  - 미체크: 빨간색 배경
  - 카테고리별 구분 (PPE/EQUIPMENT/ENVIRONMENT/EMERGENCY)

- **병렬 API 호출**
  - Promise.all로 세션/팀/안전체크 동시 조회
  - 로딩 성능 최적화

### 3. 작업 보고서와 TBM 연동
- **TBM 팀 구성 자동 불러오기**
  - `loadTbmTeamForDate()` 함수 추가
  - 선택한 날짜의 TBM 세션 자동 조회
  - 진행중(draft) 세션 우선 선택
  - 팀 구성 정보 자동 로드

- **작업자 자동 선택**
  - TBM에서 구성한 팀원 자동 선택
  - 선택된 작업자 시각적 표시 (.selected 클래스)
  - 다음 단계 버튼 자동 활성화

- **안내 메시지**
  - "🛠️ TBM 팀 구성 자동 적용" 알림
  - 자동 선택된 팀원 수 표시
  - 파란색 강조 스타일

### 4. UI/UX 개선
- TBM 카드 버튼 레이아웃 개선 (flex-wrap)
- 인계 버튼 오렌지색 (#f59e0b)
- 모달 스크롤 가능 (max-height: 70vh)
- 반응형 그리드 (auto-fill, minmax)

## 기술 구현

### 함수 추가
- `viewTbmSession()`: 상세보기 (병렬 API 호출)
- `openHandoverModal()`: 인계 모달 (팀 구성 자동 로드)
- `saveHandover()`: 인계 저장 (worker_ids JSON array)
- `loadTbmTeamForDate()`: TBM 팀 구성 조회
- `closeDetailModal()`, `closeHandoverModal()`: 모달 닫기

### 수정 함수
- `populateWorkerGrid()`: TBM 연동 추가 (async/await)
- `displayTbmSessions()`: 인계 버튼 추가

## 파일 변경사항
- web-ui/pages/work/tbm.html (모달 2개 추가, 약 110줄)
- web-ui/js/tbm.js (함수 추가, 약 250줄 증가)
- web-ui/js/daily-work-report.js (TBM 연동, 약 60줄 추가)

## 사용 시나리오

### 시나리오 1: TBM → 작업보고서
1. 아침 TBM에서 팀 구성 (예: 5명 선택)
2. 작업 보고서 작성 시 날짜 선택
3. **자동으로 5명 선택됨** 
4. 바로 작업 내역 입력 가능

### 시나리오 2: 조퇴 시 인계
1. TBM 카드에서 "📤 인계" 클릭
2. 사유 선택 (조퇴), 인수자 선택
3. 인계할 팀원 선택 (기본 전체 선택)
4. 인계 요청 → DB 저장

### 시나리오 3: TBM 상세 확인
1. TBM 카드 클릭
2. 기본 정보, 팀 구성, 안전 체크 한눈에 확인
3. 안전 체크 완료 여부 시각적 확인

## 데이터 흐름

```
TBM 시작
  ↓
팀 구성 저장 (tbm_team_assignments)
  ↓
작업 보고서 작성 시
  ↓
GET /api/tbm/sessions/date/:date
  ↓
GET /api/tbm/sessions/:id/team
  ↓
팀원 자동 선택
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 15:46:02 +09:00

1211 lines
38 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// daily-work-report.js - 브라우저 호환 버전
// =================================================================
// 🌐 API 설정 (window 객체에서 가져오기)
// =================================================================
// API 설정은 api-config.js에서 window 객체에 설정됨
// 전역 변수
let workTypes = [];
let workStatusTypes = [];
let errorTypes = [];
let workers = [];
let projects = [];
let selectedWorkers = new Set();
let workEntryCounter = 0;
let currentStep = 1;
let editingWorkId = null; // 수정 중인 작업 ID
// 한국 시간 기준 오늘 날짜 가져오기
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 && details.length > 0) {
content += `
<div class="result-details">
<h4>상세 정보:</h4>
<ul>
${details.map(detail => `<li>${detail}</li>`).join('')}
</ul>
</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);
}
// 단계 이동
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);
populateWorkerGrid();
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();
}
// 이벤트 리스너 설정
function setupEventListeners() {
document.getElementById('nextStep1').addEventListener('click', () => {
const dateInput = document.getElementById('reportDate');
if (dateInput && dateInput.value) {
goToStep(2);
} else {
showMessage('날짜를 선택해주세요.', 'error');
}
});
document.getElementById('nextStep2').addEventListener('click', () => {
if (selectedWorkers.size > 0) {
goToStep(3);
addWorkEntry();
} else {
showMessage('작업자를 선택해주세요.', 'error');
}
});
document.getElementById('addWorkBtn').addEventListener('click', addWorkEntry);
document.getElementById('submitBtn').addEventListener('click', saveWorkReport);
}
// 초기화
async function init() {
try {
const token = localStorage.getItem('token');
if (!token || token === 'undefined') {
showMessage('로그인이 필요합니다.', 'error');
localStorage.removeItem('token');
setTimeout(() => {
window.location.href = '/';
}, 2000);
return;
}
document.getElementById('reportDate').value = getKoreaToday();
await loadData();
setupEventListeners();
loadTodayWorkers();
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;