refactor(frontend): 일일 이슈 보고 기능 모듈화

- daily-issue.js를 API, UI, Controller 로직으로 분리
- 프로젝트 전반에 일관된 아키텍처 패턴을 적용하여 유지보수성 향상
This commit is contained in:
2025-07-28 12:39:29 +09:00
parent 71c06f38b1
commit 5268fec1ef
3 changed files with 239 additions and 135 deletions

View File

@@ -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);
}
});
});
// DOM이 로드되면 페이지 초기화를 시작합니다.
document.addEventListener('DOMContentLoaded', initializePage);