해당 서비스 도커화 성공, 룰 추가, 로그인 오류 수정, 소문자 룰 어느정도 해결
This commit is contained in:
210
fastapi-bridge/static/js/work-report-manage.js
Normal file
210
fastapi-bridge/static/js/work-report-manage.js
Normal file
@@ -0,0 +1,210 @@
|
||||
import { renderCalendar } from '/js/calendar.js';
|
||||
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
|
||||
|
||||
// 인증 확인
|
||||
ensureAuthenticated();
|
||||
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const reportBody = document.getElementById('reportBody');
|
||||
let selectedDate = '';
|
||||
|
||||
// 캘린더 렌더링
|
||||
renderCalendar('calendar', (dateStr) => {
|
||||
selectedDate = dateStr;
|
||||
loadReports();
|
||||
});
|
||||
|
||||
// 보고서 로딩
|
||||
async function loadReports() {
|
||||
if (!selectedDate) return;
|
||||
reportBody.innerHTML = '<tr><td colspan="8">불러오는 중...</td></tr>';
|
||||
|
||||
try {
|
||||
const [wRes, pRes, tRes, rRes] = await Promise.all([
|
||||
fetch(`${API}/workers`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/projects`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/tasks`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/workreports?start=${selectedDate}&end=${selectedDate}`, { headers: getAuthHeaders() })
|
||||
]);
|
||||
|
||||
if (![wRes, pRes, tRes, rRes].every(res => res.ok)) throw new Error('불러오기 실패');
|
||||
|
||||
const [workers, projects, tasks, reports] = await Promise.all([
|
||||
wRes.json(), pRes.json(), tRes.json(), rRes.json()
|
||||
]);
|
||||
|
||||
// 배열 체크
|
||||
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks) || !Array.isArray(reports)) {
|
||||
throw new Error('잘못된 데이터 형식');
|
||||
}
|
||||
|
||||
if (!reports.length) {
|
||||
reportBody.innerHTML = '<tr><td colspan="8">등록된 보고서가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const nameMap = Object.fromEntries(workers.map(w => [w.worker_id, w.worker_name]));
|
||||
const projMap = Object.fromEntries(projects.map(p => [p.project_id, p.project_name]));
|
||||
const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`]));
|
||||
|
||||
reportBody.innerHTML = '';
|
||||
reports.forEach((r, i) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${i + 1}</td>
|
||||
<td>${nameMap[r.worker_id] || r.worker_id}</td>
|
||||
<td><select data-id="project">
|
||||
${projects.map(p =>
|
||||
`<option value="${p.project_id}" ${p.project_id === r.project_id ? 'selected' : ''}>${p.project_name}</option>`
|
||||
).join('')}</select></td>
|
||||
<td><select data-id="task">
|
||||
${tasks.map(t =>
|
||||
`<option value="${t.task_id}" ${t.task_id === r.task_id ? 'selected' : ''}>${t.category}:${t.subcategory}</option>`
|
||||
).join('')}</select></td>
|
||||
<td><input type="number" min="0" step="0.5" value="${r.overtime_hours || ''}" data-id="overtime"></td>
|
||||
<td><select data-id="work_details">
|
||||
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(opt =>
|
||||
`<option value="${opt}" ${r.work_details === opt ? 'selected' : ''}>${opt}</option>`
|
||||
).join('')}</select></td>
|
||||
<td><input type="text" value="${r.memo || ''}" data-id="memo"></td>
|
||||
<td>
|
||||
<button class="action-btn save-btn">저장</button>
|
||||
<button class="action-btn delete-btn">삭제</button>
|
||||
</td>`;
|
||||
|
||||
// 저장 버튼
|
||||
tr.querySelector('.save-btn').onclick = async () => {
|
||||
// 입력값 검증
|
||||
const projectId = tr.querySelector('[data-id="project"]').value;
|
||||
const taskId = tr.querySelector('[data-id="task"]').value;
|
||||
const overtimeHours = tr.querySelector('[data-id="overtime"]').value;
|
||||
|
||||
if (!projectId || !taskId) {
|
||||
alert('❌ 프로젝트와 작업을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 날짜 형식 처리 - MySQL DATE 형식으로 변환
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return selectedDate;
|
||||
|
||||
// 이미 YYYY-MM-DD 형식인지 확인
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
// ISO 형식이나 다른 형식을 YYYY-MM-DD로 변환
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) {
|
||||
return selectedDate; // 잘못된 날짜면 선택된 날짜 사용
|
||||
}
|
||||
|
||||
return date.toISOString().split('T')[0]; // YYYY-MM-DD 형식으로 변환
|
||||
};
|
||||
|
||||
const payload = {
|
||||
date: formatDate(r.date), // 날짜 형식 변환
|
||||
worker_id: r.worker_id, // 기존 작업자 ID 유지
|
||||
project_id: Number(projectId),
|
||||
task_id: Number(taskId),
|
||||
overtime_hours: overtimeHours ? Number(overtimeHours) : null,
|
||||
work_details: tr.querySelector('[data-id="work_details"]').value,
|
||||
memo: tr.querySelector('[data-id="memo"]').value.trim() || null
|
||||
};
|
||||
|
||||
// 저장 버튼 상태 변경 (로딩 중)
|
||||
const saveBtn = tr.querySelector('.save-btn');
|
||||
const originalText = saveBtn.textContent;
|
||||
const originalColor = saveBtn.style.backgroundColor;
|
||||
|
||||
saveBtn.textContent = '저장 중...';
|
||||
saveBtn.style.backgroundColor = '#ffc107';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/workreports/${r.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (res.ok && result.success) {
|
||||
// 성공 상태 표시
|
||||
saveBtn.textContent = '✅ 완료';
|
||||
saveBtn.style.backgroundColor = '#28a745';
|
||||
saveBtn.style.color = 'white';
|
||||
|
||||
setTimeout(() => {
|
||||
saveBtn.textContent = originalText;
|
||||
saveBtn.style.backgroundColor = originalColor;
|
||||
saveBtn.style.color = '';
|
||||
saveBtn.disabled = false;
|
||||
}, 2000);
|
||||
|
||||
// alert 대신 조용한 알림
|
||||
console.log('저장 완료:', result);
|
||||
} else {
|
||||
console.error('저장 실패:', result);
|
||||
alert(`❌ 저장 실패: ${result.error || result.message || '알 수 없는 오류'}`);
|
||||
|
||||
// 실패 시 버튼 복원
|
||||
saveBtn.textContent = originalText;
|
||||
saveBtn.style.backgroundColor = originalColor;
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('저장 요청 에러:', err);
|
||||
alert('❌ 저장 요청 실패: ' + err.message);
|
||||
|
||||
// 에러 시 버튼 복원
|
||||
saveBtn.textContent = originalText;
|
||||
saveBtn.style.backgroundColor = originalColor;
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제 버튼
|
||||
tr.querySelector('.delete-btn').onclick = async () => {
|
||||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/workreports/${r.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
tr.remove();
|
||||
// 행 번호 다시 매기기
|
||||
updateRowNumbers();
|
||||
alert('✅ 삭제 완료');
|
||||
} else {
|
||||
const result = await res.json();
|
||||
alert(`❌ 삭제 실패: ${result.error || result.message || '알 수 없는 오류'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('삭제 요청 에러:', err);
|
||||
alert('❌ 삭제 요청 실패: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
reportBody.appendChild(tr);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('데이터 로딩 에러:', err);
|
||||
reportBody.innerHTML = '<tr><td colspan="8">❌ 불러오기 실패: ' + err.message + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// 행 번호 다시 매기기
|
||||
function updateRowNumbers() {
|
||||
reportBody.querySelectorAll('tr').forEach((tr, i) => {
|
||||
const firstTd = tr.querySelector('td:first-child');
|
||||
if (firstTd) firstTd.textContent = i + 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user