From ef85a880e5bc9a5cea9304dd17f0225970a72efb Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 28 Jul 2025 12:28:06 +0900 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EC=83=9D=EC=84=B1=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API, UI, Controller 로직을 work-report-api.js, work-report-ui.js, work-report-create.js로 분리 - 관심사 분리를 통해 코드의 재사용성 및 유지보수성 향상 --- web-ui/js/work-report-api.js | 46 +++++++ web-ui/js/work-report-create.js | 224 +++++++++----------------------- web-ui/js/work-report-ui.js | 141 ++++++++++++++++++++ 3 files changed, 246 insertions(+), 165 deletions(-) create mode 100644 web-ui/js/work-report-api.js create mode 100644 web-ui/js/work-report-ui.js diff --git a/web-ui/js/work-report-api.js b/web-ui/js/work-report-api.js new file mode 100644 index 0000000..bb206ae --- /dev/null +++ b/web-ui/js/work-report-api.js @@ -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} reportData - 전송할 작업 보고서 데이터 배열 + * @returns {Promise} - 서버의 응답 결과 + */ +export async function createWorkReport(reportData) { + try { + const result = await apiPost('/workreports', reportData); + return result; + } catch (error) { + console.error('작업 보고서 생성 요청 실패:', error); + throw error; + } +} \ No newline at end of file diff --git a/web-ui/js/work-report-create.js b/web-ui/js/work-report-create.js index 2a386a9..d61ed79 100644 --- a/web-ui/js/work-report-create.js +++ b/web-ui/js/work-report-create.js @@ -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 = '데이터를 불러오는 중...'; 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 = ` - ${i + 1} - - - ${w.worker_name} - - - - - - - - - - - - - - - - - - - - `; - 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 = '오류 발생! 데이터를 불러올 수 없습니다.'; } } -// ✅ 행 번호 다시 매기기 -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 = '전체 등록'; } -}); \ No newline at end of file +} + +/** + * 페이지 초기화 함수 + */ +function initializePage() { + renderCalendar('calendar', onDateSelect); + + const submitBtn = document.getElementById('submitBtn'); + submitBtn.addEventListener('click', handleSubmit); +} + +// DOM이 로드되면 페이지 초기화를 시작합니다. +document.addEventListener('DOMContentLoaded', initializePage); \ No newline at end of file diff --git a/web-ui/js/work-report-ui.js b/web-ui/js/work-report-ui.js new file mode 100644 index 0000000..bdc6042 --- /dev/null +++ b/web-ui/js/work-report-ui.js @@ -0,0 +1,141 @@ +// /js/work-report-ui.js + +const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음 +const DEFAULT_TASK_ID = '15'; + +/** + * 주어진 데이터를 바탕으로 + ${worker.worker_name} + + + + + + + + + + + + `; + + // 이벤트 리스너 설정 + 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 = '등록할 작업자 정보가 없습니다.'; + return; + } + + workers.forEach((worker, index) => { + const row = createReportRow(worker, projects, tasks, index); + tableBody.appendChild(row); + }); +} + +/** + * 테이블에서 폼 데이터를 추출하여 배열로 반환합니다. + * @returns {Array|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; +} \ No newline at end of file