refactor(frontend): 작업 보고서 생성 기능 모듈화
- API, UI, Controller 로직을 work-report-api.js, work-report-ui.js, work-report-create.js로 분리 - 관심사 분리를 통해 코드의 재사용성 및 유지보수성 향상
This commit is contained in:
46
web-ui/js/work-report-api.js
Normal file
46
web-ui/js/work-report-api.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// /js/work-report-api.js
|
||||||
|
import { apiGet, apiPost } from './api-helper.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업 보고서 작성을 위해 필요한 초기 데이터(작업자, 프로젝트, 태스크)를 가져옵니다.
|
||||||
|
* Promise.all을 사용하여 병렬로 API를 호출합니다.
|
||||||
|
* @returns {Promise<{workers: Array, projects: Array, tasks: Array}>}
|
||||||
|
*/
|
||||||
|
export async function getInitialData() {
|
||||||
|
try {
|
||||||
|
const [workers, projects, tasks] = await Promise.all([
|
||||||
|
apiGet('/workers'),
|
||||||
|
apiGet('/projects'),
|
||||||
|
apiGet('/tasks')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 데이터 형식 검증
|
||||||
|
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
|
||||||
|
throw new Error('서버에서 받은 데이터 형식이 올바르지 않습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작업자 목록은 ID 기준으로 정렬
|
||||||
|
workers.sort((a, b) => a.worker_id - b.worker_id);
|
||||||
|
|
||||||
|
return { workers, projects, tasks };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('초기 데이터 로딩 중 오류 발생:', error);
|
||||||
|
// 에러를 다시 던져서 호출한 쪽에서 처리할 수 있도록 함
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작성된 작업 보고서 데이터를 서버에 전송합니다.
|
||||||
|
* @param {Array<object>} reportData - 전송할 작업 보고서 데이터 배열
|
||||||
|
* @returns {Promise<object>} - 서버의 응답 결과
|
||||||
|
*/
|
||||||
|
export async function createWorkReport(reportData) {
|
||||||
|
try {
|
||||||
|
const result = await apiPost('/workreports', reportData);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('작업 보고서 생성 요청 실패:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,185 +1,79 @@
|
|||||||
import { renderCalendar } from '/js/calendar.js'; // 날짜 캘린더 모듈
|
// /js/work-report-create.js
|
||||||
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.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 = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜가 선택되었을 때 실행되는 콜백 함수.
|
||||||
|
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
|
||||||
|
* @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 initialData = await getInitialData();
|
||||||
|
initializeReportTable(initialData);
|
||||||
|
} catch (error) {
|
||||||
|
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
|
||||||
|
* 폼 데이터를 서버에 전송합니다.
|
||||||
|
*/
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!selectedDate) {
|
||||||
|
alert('먼저 달력에서 날짜를 선택해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportData = getReportData();
|
||||||
|
if (!reportData) {
|
||||||
|
// getReportData 내부에서 이미 alert으로 사용자에게 알림
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 각 항목에 선택된 날짜 추가
|
||||||
|
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
|
||||||
|
|
||||||
// ✅ DOM 요소
|
|
||||||
const reportBody = document.getElementById('reportBody');
|
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
const defaultProjectId = '13';
|
submitBtn.disabled = true;
|
||||||
const defaultTaskId = '15';
|
submitBtn.textContent = '등록 중...';
|
||||||
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;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [wrRes, prRes, tkRes] = await Promise.all([
|
const result = await createWorkReport(payload);
|
||||||
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 || '작업자 불러오기 중 오류 발생');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 행 번호 다시 매기기
|
|
||||||
function updateRowNumbers() {
|
|
||||||
reportBody.querySelectorAll('tr').forEach((tr, i) => {
|
|
||||||
tr.children[0].textContent = i + 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 전체 등록 처리
|
|
||||||
submitBtn.addEventListener('click', async () => {
|
|
||||||
if (!selectedDateStr) {
|
|
||||||
alert('날짜를 먼저 선택하세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = Array.from(reportBody.querySelectorAll('tr'));
|
|
||||||
if (rows.length === 0) {
|
|
||||||
alert('등록할 작업자가 없습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set();
|
|
||||||
const payload = [];
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API}/workreports`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
const result = await res.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert('✅ 등록 완료!');
|
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
|
||||||
// 선택적: 페이지 새로고침 또는 다른 날짜로 이동
|
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
|
||||||
// loadWorkers();
|
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
|
||||||
} else {
|
} else {
|
||||||
alert('❌ 등록 실패: ' + (result.error || '알 수 없는 오류'));
|
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
console.error(err);
|
alert('❌ 등록 실패: ' + error.message);
|
||||||
alert('서버 오류가 발생했습니다: ' + err.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);
|
||||||
141
web-ui/js/work-report-ui.js
Normal file
141
web-ui/js/work-report-ui.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// /js/work-report-ui.js
|
||||||
|
|
||||||
|
const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음
|
||||||
|
const DEFAULT_TASK_ID = '15';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주어진 데이터를 바탕으로 <select> 요소의 <option>들을 생성합니다.
|
||||||
|
* @param {Array<object>} items - 옵션으로 만들 데이터 배열
|
||||||
|
* @param {string} valueField - <option>의 value 속성에 사용할 필드 이름
|
||||||
|
* @param {string} textField - <option>의 텍스트에 사용할 필드 이름
|
||||||
|
* @returns {string} - 생성된 HTML 옵션 문자열
|
||||||
|
*/
|
||||||
|
function createOptions(items, valueField, textField) {
|
||||||
|
return items.map(item => `<option value="${item[valueField]}">${textField(item)}</option>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블의 모든 행 번호를 다시 매깁니다.
|
||||||
|
* @param {HTMLTableSectionElement} tableBody - tbody 요소
|
||||||
|
*/
|
||||||
|
function updateRowNumbers(tableBody) {
|
||||||
|
tableBody.querySelectorAll('tr').forEach((tr, index) => {
|
||||||
|
tr.cells[0].textContent = index + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 하나의 작업 보고서 행(tr)을 생성합니다.
|
||||||
|
* @param {object} worker - 작업자 정보
|
||||||
|
* @param {Array} projects - 전체 프로젝트 목록
|
||||||
|
* @param {Array} tasks - 전체 태스크 목록
|
||||||
|
* @param {number} index - 행 번호
|
||||||
|
* @returns {HTMLTableRowElement} - 생성된 tr 요소
|
||||||
|
*/
|
||||||
|
function createReportRow(worker, projects, tasks, index) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="worker_id" value="${worker.worker_id}">
|
||||||
|
${worker.worker_name}
|
||||||
|
</td>
|
||||||
|
<td><select name="project_id">${createOptions(projects, 'project_id', p => p.project_name)}</select></td>
|
||||||
|
<td><select name="task_id">${createOptions(tasks, 'task_id', t => `${t.category}:${t.subcategory}`)}</select></td>
|
||||||
|
<td>
|
||||||
|
<select name="overtime">
|
||||||
|
<option value="">없음</option>
|
||||||
|
${[1, 2, 3, 4].map(n => `<option>${n}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="work_type">
|
||||||
|
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(t => `<option>${t}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td><input type="text" name="memo" placeholder="메모"></td>
|
||||||
|
<td><button type="button" class="remove-btn">x</button></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 이벤트 리스너 설정
|
||||||
|
const workTypeSelect = tr.querySelector('[name="work_type"]');
|
||||||
|
const projectSelect = tr.querySelector('[name="project_id"]');
|
||||||
|
const taskSelect = tr.querySelector('[name="task_id"]');
|
||||||
|
|
||||||
|
workTypeSelect.addEventListener('change', () => {
|
||||||
|
const isDisabled = ['연차', '휴무', '유급'].includes(workTypeSelect.value);
|
||||||
|
projectSelect.disabled = isDisabled;
|
||||||
|
taskSelect.disabled = isDisabled;
|
||||||
|
if (isDisabled) {
|
||||||
|
projectSelect.value = DEFAULT_PROJECT_ID;
|
||||||
|
taskSelect.value = DEFAULT_TASK_ID;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||||
|
tr.remove();
|
||||||
|
updateRowNumbers(tr.parentElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업 보고서 테이블을 초기화하고 데이터를 채웁니다.
|
||||||
|
* @param {{workers: Array, projects: Array, tasks: Array}} initialData - 초기 데이터
|
||||||
|
*/
|
||||||
|
export function initializeReportTable(initialData) {
|
||||||
|
const tableBody = document.getElementById('reportBody');
|
||||||
|
if (!tableBody) return;
|
||||||
|
|
||||||
|
tableBody.innerHTML = ''; // 기존 내용 초기화
|
||||||
|
const { workers, projects, tasks } = initialData;
|
||||||
|
|
||||||
|
if (!workers || workers.length === 0) {
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">등록할 작업자 정보가 없습니다.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
workers.forEach((worker, index) => {
|
||||||
|
const row = createReportRow(worker, projects, tasks, index);
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블에서 폼 데이터를 추출하여 배열로 반환합니다.
|
||||||
|
* @returns {Array<object>|null} - 추출된 데이터 배열 또는 유효성 검사 실패 시 null
|
||||||
|
*/
|
||||||
|
export function getReportData() {
|
||||||
|
const tableBody = document.getElementById('reportBody');
|
||||||
|
const rows = tableBody.querySelectorAll('tr');
|
||||||
|
|
||||||
|
if (rows.length === 0 || (rows.length === 1 && rows[0].cells.length < 2)) {
|
||||||
|
alert('등록할 내용이 없습니다.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportData = [];
|
||||||
|
const workerIds = new Set();
|
||||||
|
|
||||||
|
for (const tr of rows) {
|
||||||
|
const workerId = tr.querySelector('[name="worker_id"]').value;
|
||||||
|
if (workerIds.has(workerId)) {
|
||||||
|
alert(`오류: 작업자 '${tr.cells[1].textContent.trim()}'가 중복 등록되었습니다.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
workerIds.add(workerId);
|
||||||
|
|
||||||
|
reportData.push({
|
||||||
|
worker_id: workerId,
|
||||||
|
project_id: tr.querySelector('[name="project_id"]').value,
|
||||||
|
task_id: tr.querySelector('[name="task_id"]').value,
|
||||||
|
overtime_hours: tr.querySelector('[name="overtime"]').value || 0,
|
||||||
|
work_details: tr.querySelector('[name="work_type"]').value,
|
||||||
|
memo: tr.querySelector('[name="memo"]').value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return reportData;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user