refactor(frontend): 작업 보고서 생성 기능 모듈화

- API, UI, Controller 로직을 work-report-api.js, work-report-ui.js, work-report-create.js로 분리
- 관심사 분리를 통해 코드의 재사용성 및 유지보수성 향상
This commit is contained in:
2025-07-28 12:28:06 +09:00
parent 892215a15d
commit ef85a880e5
3 changed files with 246 additions and 165 deletions

View File

@@ -1,185 +1,79 @@
import { renderCalendar } from '/js/calendar.js'; // 날짜 캘린더 모듈
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// /js/work-report-create.js
import { renderCalendar } from './calendar.js';
import { getInitialData, createWorkReport } from './work-report-api.js';
import { initializeReportTable, getReportData } from './work-report-ui.js';
// 인증 확인
ensureAuthenticated();
// 전역 상태 변수
let selectedDate = '';
// ✅ DOM 요소
const reportBody = document.getElementById('reportBody');
const submitBtn = document.getElementById('submitBtn');
const defaultProjectId = '13';
const defaultTaskId = '15';
let selectedDateStr = '';
// ✅ 페이지 로드시 초기 렌더링
document.addEventListener('DOMContentLoaded', () => {
fetch('/components/navbar.html')
.then(r => r.text())
.then(html => {
document.getElementById('navbar-container').innerHTML = html;
})
.catch(err => console.error('🔴 네비게이션 바 로딩 실패:', err));
renderCalendar('calendar', date => {
selectedDateStr = date;
loadWorkers();
});
});
// ✅ 작업자, 프로젝트, 작업 불러오기
async function loadWorkers() {
if (!selectedDateStr) return;
/**
* 날짜가 선택되었을 때 실행되는 콜백 함수.
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
* @param {string} date - 선택된 날짜 (YYYY-MM-DD 형식)
*/
async function onDateSelect(date) {
selectedDate = date;
const tableBody = document.getElementById('reportBody');
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">데이터를 불러오는 중...</td></tr>';
try {
const [wrRes, prRes, tkRes] = await Promise.all([
fetch(`${API}/workers`, { headers: getAuthHeaders() }),
fetch(`${API}/projects`, { headers: getAuthHeaders() }),
fetch(`${API}/tasks`, { headers: getAuthHeaders() })
]);
if (!wrRes.ok || !prRes.ok || !tkRes.ok) {
throw new Error('데이터 불러오기 실패');
}
const workers = await wrRes.json();
const projects = await prRes.json();
const tasks = await tkRes.json();
// 배열 체크
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
throw new Error('잘못된 데이터 형식');
}
workers.sort((a, b) => a.worker_id - b.worker_id);
reportBody.innerHTML = '';
workers.forEach((w, i) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${i + 1}</td>
<td>
<input type="hidden" name="worker_id" value="${w.worker_id}">
${w.worker_name}
</td>
<td>
<select name="project_id">
${projects.map(p =>
`<option value="${p.project_id}">${p.project_name}</option>`
).join('')}
</select>
</td>
<td>
<select name="task_id">
${tasks.map(t =>
`<option value="${t.task_id}">${t.category}:${t.subcategory}</option>`
).join('')}
</select>
</td>
<td>
<select name="overtime">
<option value="">없음</option>
<option>1</option><option>2</option>
<option>3</option><option>4</option>
</select>
</td>
<td>
<select name="work_type">
<option>근무</option><option>연차</option><option>유급</option>
<option>반차</option><option>반반차</option><option>조퇴</option>
<option>휴무</option>
</select>
</td>
<td>
<input type="text" name="memo" placeholder="메모">
</td>
<td>
<button class="remove-btn">x</button>
</td>
`;
reportBody.appendChild(tr);
// 근무형태 변경시 프로젝트/작업 필드 비활성화
const workSel = tr.querySelector('[name="work_type"]');
const projSel = tr.querySelector('[name="project_id"]');
const taskSel = tr.querySelector('[name="task_id"]');
workSel.addEventListener('change', () => {
const disabled = ['연차','휴무','유급'].includes(workSel.value);
projSel.value = disabled ? defaultProjectId : projSel.value;
taskSel.value = disabled ? defaultTaskId : taskSel.value;
projSel.disabled = taskSel.disabled = disabled;
});
tr.querySelector('.remove-btn').addEventListener('click', () => {
tr.remove();
updateRowNumbers();
});
});
} catch (err) {
console.error(err);
alert(err.message || '작업자 불러오기 중 오류 발생');
const initialData = await getInitialData();
initializeReportTable(initialData);
} catch (error) {
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
}
}
// ✅ 행 번호 다시 매기기
function updateRowNumbers() {
reportBody.querySelectorAll('tr').forEach((tr, i) => {
tr.children[0].textContent = i + 1;
});
}
// ✅ 전체 등록 처리
submitBtn.addEventListener('click', async () => {
if (!selectedDateStr) {
alert('날짜를 먼저 선택하세요.');
/**
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
* 폼 데이터를 서버에 전송합니다.
*/
async function handleSubmit() {
if (!selectedDate) {
alert('먼저 달력에서 날짜를 선택해주세요.');
return;
}
const rows = Array.from(reportBody.querySelectorAll('tr'));
if (rows.length === 0) {
alert('등록할 작업자가 없습니다.');
const reportData = getReportData();
if (!reportData) {
// getReportData 내부에서 이미 alert으로 사용자에게 알림
return;
}
const seen = new Set();
const payload = [];
// 각 항목에 선택된 날짜 추가
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
for (let tr of rows) {
const wid = tr.querySelector('[name="worker_id"]').value;
if (seen.has(wid)) {
alert('중복된 작업자가 있습니다.');
return;
}
seen.add(wid);
payload.push({
date: selectedDateStr,
worker_id: wid,
project_id: tr.querySelector('[name="project_id"]').value,
task_id: tr.querySelector('[name="task_id"]').value,
overtime_hours: tr.querySelector('[name="overtime"]').value,
work_details: tr.querySelector('[name="work_type"]').value,
memo: tr.querySelector('[name="memo"]').value
});
}
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '등록 중...';
try {
const res = await fetch(`${API}/workreports`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(payload)
});
const result = await res.json();
const result = await createWorkReport(payload);
if (result.success) {
alert('✅ 등록 완료!');
// 선택적: 페이지 새로고침 또는 다른 날짜로 이동
// loadWorkers();
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
} else {
alert('❌ 등록 실패: ' + (result.error || '알 수 없는 오류'));
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
}
} catch (err) {
console.error(err);
alert('서버 오류가 발생했습니다: ' + err.message);
} catch (error) {
alert('❌ 등록 실패: ' + error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '전체 등록';
}
});
}
/**
* 페이지 초기화 함수
*/
function initializePage() {
renderCalendar('calendar', onDateSelect);
const submitBtn = document.getElementById('submitBtn');
submitBtn.addEventListener('click', handleSubmit);
}
// DOM이 로드되면 페이지 초기화를 시작합니다.
document.addEventListener('DOMContentLoaded', initializePage);