diff --git a/web-ui/js/daily-issue-api.js b/web-ui/js/daily-issue-api.js new file mode 100644 index 0000000..ef9bcec --- /dev/null +++ b/web-ui/js/daily-issue-api.js @@ -0,0 +1,66 @@ +// /js/daily-issue-api.js +import { apiGet, apiPost } from './api-helper.js'; + +/** + * 이슈 보고서 작성을 위해 필요한 초기 데이터(프로젝트, 이슈 유형)를 가져옵니다. + * @returns {Promise<{projects: Array, issueTypes: Array}>} + */ +export async function getInitialData() { + try { + const [projects, issueTypes] = await Promise.all([ + apiGet('/projects'), + apiGet('/issue-types') + ]); + return { projects, issueTypes }; + } catch (error) { + console.error('이슈 보고서 초기 데이터 로딩 실패:', error); + throw error; + } +} + +/** + * 특정 날짜에 근무한 작업자 목록을 가져옵니다. + * @param {string} date - 조회할 날짜 (YYYY-MM-DD) + * @returns {Promise} - 작업자 목록 + */ +export async function getWorkersByDate(date) { + try { + // 백엔드에 해당 날짜의 작업자 목록을 요청하는 API가 있다고 가정합니다. + // (예: /api/workers?work_date=YYYY-MM-DD) + // 현재는 기존 로직을 최대한 활용하여 구현합니다. + let workers = []; + const reports = await apiGet(`/daily-work-reports?date=${date}`); + + if (reports && reports.length > 0) { + const workerMap = new Map(); + reports.forEach(r => { + if (!workerMap.has(r.worker_id)) { + workerMap.set(r.worker_id, { worker_id: r.worker_id, worker_name: r.worker_name }); + } + }); + workers = Array.from(workerMap.values()); + } else { + // 보고서가 없으면 전체 작업자 목록을 가져옵니다. + workers = await apiGet('/workers'); + } + return workers.sort((a, b) => a.worker_name.localeCompare(b.worker_name)); + } catch (error) { + console.error(`${date}의 작업자 목록 로딩 실패:`, error); + throw error; + } +} + +/** + * 작성된 이슈 보고서 데이터를 서버에 전송합니다. + * @param {object} issueData - 전송할 이슈 데이터 + * @returns {Promise} - 서버 응답 결과 + */ +export async function createIssueReport(issueData) { + try { + const result = await apiPost('/issue-reports', issueData); + return result; + } catch (error) { + console.error('이슈 보고서 생성 요청 실패:', error); + throw error; + } +} \ No newline at end of file diff --git a/web-ui/js/daily-issue-ui.js b/web-ui/js/daily-issue-ui.js new file mode 100644 index 0000000..c125ea8 --- /dev/null +++ b/web-ui/js/daily-issue-ui.js @@ -0,0 +1,103 @@ +// /js/daily-issue-ui.js + +const DOM = { + dateSelect: document.getElementById('dateSelect'), + projectSelect: document.getElementById('projectSelect'), + issueTypeSelect: document.getElementById('issueTypeSelect'), + timeStart: document.getElementById('timeStart'), + timeEnd: document.getElementById('timeEnd'), + workerList: document.getElementById('workerList'), + form: document.getElementById('issueForm'), + submitBtn: document.getElementById('submitBtn'), +}; + +function createOption(value, text) { + const option = document.createElement('option'); + option.value = value; + option.textContent = text; + return option; +} + +export function populateProjects(projects) { + DOM.projectSelect.innerHTML = ''; + if (Array.isArray(projects)) { + projects.forEach(p => DOM.projectSelect.appendChild(createOption(p.project_id, p.project_name))); + } +} + +export function populateIssueTypes(issueTypes) { + DOM.issueTypeSelect.innerHTML = ''; + if (Array.isArray(issueTypes)) { + issueTypes.forEach(t => DOM.issueTypeSelect.appendChild(createOption(t.issue_type_id, `${t.category}:${t.subcategory}`))); + } +} + +export function populateTimeOptions() { + for (let h = 0; h < 24; h++) { + for (let m of [0, 30]) { + const time = `${String(h).padStart(2, '0')}:${m === 0 ? '00' : '30'}`; + DOM.timeStart.appendChild(createOption(time, time)); + DOM.timeEnd.appendChild(createOption(time, time.replace('00:00', '24:00'))); + } + } + DOM.timeEnd.value = "24:00"; // 기본값 설정 +} + +export function renderWorkerList(workers) { + DOM.workerList.innerHTML = ''; + if (!Array.isArray(workers) || workers.length === 0) { + DOM.workerList.textContent = '선택 가능한 작업자가 없습니다.'; + return; + } + workers.forEach(worker => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'btn'; + btn.textContent = worker.worker_name; + btn.dataset.id = worker.worker_id; + btn.addEventListener('click', () => btn.classList.toggle('selected')); + DOM.workerList.appendChild(btn); + }); +} + +export function getFormData() { + const selectedWorkers = [...DOM.workerList.querySelectorAll('.btn.selected')].map(b => b.dataset.id); + + if (selectedWorkers.length === 0) { + alert('작업자를 한 명 이상 선택해주세요.'); + return null; + } + if (DOM.timeEnd.value <= DOM.timeStart.value) { + alert('종료 시간은 시작 시간보다 이후여야 합니다.'); + return null; + } + + const formData = new FormData(DOM.form); + const data = { + date: formData.get('dateSelect'), // input name 속성이 없어 직접 가져옴 + project_id: DOM.projectSelect.value, + issue_type_id: DOM.issueTypeSelect.value, + start_time: DOM.timeStart.value, + end_time: DOM.timeEnd.value, + worker_ids: selectedWorkers, // worker_id -> worker_ids 로 명확하게 변경 + }; + + for (const key in data) { + if (!data[key] || (Array.isArray(data[key]) && data[key].length === 0)) { + alert('모든 필수 항목을 입력해주세요.'); + return null; + } + } + + return data; +} + +export function setSubmitButtonState(isLoading) { + if (isLoading) { + DOM.submitBtn.disabled = true; + DOM.submitBtn.textContent = '등록 중...'; + } else { + DOM.submitBtn.disabled = false; + DOM.submitBtn.textContent = '등록'; + } +} \ No newline at end of file diff --git a/web-ui/js/daily-issue.js b/web-ui/js/daily-issue.js index 2975e87..f7f024b 100644 --- a/web-ui/js/daily-issue.js +++ b/web-ui/js/daily-issue.js @@ -1,154 +1,89 @@ // /js/daily-issue.js -import { API, getAuthHeaders } from '/js/api-config.js'; +import { getInitialData, getWorkersByDate, createIssueReport } from './daily-issue-api.js'; +import { + populateProjects, + populateIssueTypes, + populateTimeOptions, + renderWorkerList, + getFormData, + setSubmitButtonState +} from './daily-issue-ui.js'; -const dateInput = document.getElementById('dateSelect'); -const projectSel = document.getElementById('projectSelect'); -const issueTypeSel = document.getElementById('issueTypeSelect'); -const timeStartSel = document.getElementById('timeStart'); -const timeEndSel = document.getElementById('timeEnd'); -const workerList = document.getElementById('workerList'); +const dateSelect = document.getElementById('dateSelect'); const form = document.getElementById('issueForm'); -// 오늘 날짜 기본 설정 -const today = new Date().toISOString().split('T')[0]; -dateInput.value = today; - -// 시간 옵션 생성 -function populateTimeOptions(startEl, endEl) { - for (let h = 0; h < 24; h++) { - for (let m of [0, 30]) { - const time = `${String(h).padStart(2, '0')}:${m === 0 ? '00' : '30'}`; - const option = new Option(time, time); - startEl.appendChild(option); - endEl.appendChild(option.cloneNode(true)); - } +/** + * 날짜가 변경될 때마다 해당 날짜의 작업자 목록을 다시 불러옵니다. + */ +async function handleDateChange() { + const selectedDate = dateSelect.value; + if (!selectedDate) { + document.getElementById('workerList').textContent = '날짜를 먼저 선택하세요.'; + return; } -} -populateTimeOptions(timeStartSel, timeEndSel); - -// 📌 프로젝트 목록 -async function loadProjects() { + + document.getElementById('workerList').textContent = '작업자 목록을 불러오는 중...'; try { - const res = await fetch(`${API}/projects`, { - headers: getAuthHeaders() - }); - const data = await res.json(); - if (Array.isArray(data)) { - data.forEach(p => { - projectSel.appendChild(new Option(p.project_name, p.project_id)); - }); - } - } catch (err) { - console.error('프로젝트 로딩 오류:', err); + const workers = await getWorkersByDate(selectedDate); + renderWorkerList(workers); + } catch (error) { + document.getElementById('workerList').textContent = '작업자 목록 로딩에 실패했습니다.'; } } -// 📌 이슈 유형 목록 -async function loadIssueTypes() { +/** + * 폼 제출 이벤트를 처리합니다. + */ +async function handleSubmit(event) { + event.preventDefault(); + const issueData = getFormData(); + + if (!issueData) return; // 유효성 검사 실패 + + setSubmitButtonState(true); try { - const res = await fetch(`${API}/issue-types`, { - headers: getAuthHeaders() - }); - const data = await res.json(); - if (Array.isArray(data)) { - data.forEach(t => { - issueTypeSel.appendChild(new Option(`${t.category}:${t.subcategory}`, t.issue_type_id)); - }); + const result = await createIssueReport(issueData); + if (result.success) { + alert('✅ 이슈가 성공적으로 등록되었습니다.'); + form.reset(); // 폼 초기화 + dateSelect.value = new Date().toISOString().split('T')[0]; // 날짜 오늘로 리셋 + handleDateChange(); // 작업자 목록 새로고침 + } else { + throw new Error(result.error || '알 수 없는 오류가 발생했습니다.'); } - } catch (err) { - console.error('이슈 타입 로딩 오류:', err); + } catch (error) { + alert(`🚨 등록 실패: ${error.message}`); + } finally { + setSubmitButtonState(false); } } -// 📌 작업자 목록 -async function loadWorkers() { - const d = dateInput.value; - workerList.textContent = '로딩 중...'; +/** + * 페이지 초기화 함수 + */ +async function initializePage() { + // 오늘 날짜 기본 설정 + dateSelect.value = new Date().toISOString().split('T')[0]; + + populateTimeOptions(); + + // 프로젝트, 이슈유형, 작업자 목록을 병렬로 로드 try { - let res = await fetch(`${API}/workreports/date/${d}`, { - headers: getAuthHeaders() - }); - let reports = await res.json(); - - if (!reports.length) { - const wRes = await fetch(`${API}/workers`, { - headers: getAuthHeaders() - }); - const allWorkers = await wRes.json(); - if (Array.isArray(allWorkers)) { - reports = allWorkers.map(w => ({ - worker_id: w.worker_id, - worker_name: w.worker_name - })); - } - } - - const seen = new Set(); - workerList.innerHTML = ''; - reports.forEach(r => { - if (!seen.has(r.worker_id)) { - seen.add(r.worker_id); - const btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'btn'; - btn.textContent = r.worker_name; - btn.dataset.id = r.worker_id; - btn.addEventListener('click', () => btn.classList.toggle('selected')); - workerList.appendChild(btn); - } - }); - } catch (err) { - console.error('👿 작업자 로딩 오류:', err); - workerList.textContent = '작업자 로딩 실패'; + const [initialData] = await Promise.all([ + getInitialData(), + handleDateChange() // 초기 작업자 목록 로드 + ]); + populateProjects(initialData.projects); + populateIssueTypes(initialData.issueTypes); + } catch (error) { + alert('페이지 초기화 중 오류가 발생했습니다. 새로고침 해주세요.'); } + + // 이벤트 리스너 설정 + dateSelect.addEventListener('change', handleDateChange); + form.addEventListener('submit', handleSubmit); } -// 📌 초기 실행 -document.addEventListener('DOMContentLoaded', () => { - loadProjects(); - loadIssueTypes(); - loadWorkers(); - - dateInput.addEventListener('change', loadWorkers); - - form.addEventListener('submit', async e => { - e.preventDefault(); - const workerIds = [...workerList.querySelectorAll('.btn.selected')].map(b => b.dataset.id); - if (!workerIds.length) return alert('작업자를 선택하세요.'); - - const projectId = projectSel.value; - const issueTypeId = issueTypeSel.value; - const start = timeStartSel.value; - const end = timeEndSel.value; - - if (!projectId || !issueTypeId || !start || !end) return alert('모든 값을 입력하세요.'); - if (end <= start) return alert('종료 시간은 시작 시간 이후여야 합니다.'); - - const payload = { - date: dateInput.value, - worker_id: workerIds, - project_id: projectId, - start_time: timeStartSel.value, - end_time: timeEndSel.value, - issue_type_id: issueTypeId - }; - - try { - const res = await fetch(`${API}/issue-reports`, { - method: 'POST', - headers: getAuthHeaders(), - body: JSON.stringify(payload) - }); - const json = await res.json(); - if (res.ok && json.success) { - alert('✅ 등록 완료!'); - loadWorkers(); - } else { - alert(json.error || '등록 실패'); - } - } catch (err) { - alert('🚨 서버 오류: ' + err.message); - } - }); -}); \ No newline at end of file +// DOM이 로드되면 페이지 초기화를 시작합니다. +document.addEventListener('DOMContentLoaded', initializePage); \ No newline at end of file