feat: localStorage 문제 해결 및 시스템 개선

- localStorage와 DB ID 불일치 문제 해결
- 프로젝트별 보고서 시간 필터링 수정
- 일반 사용자에게 일일공수 메뉴 숨김
- 공통 헤더 및 인증 시스템 구현
- 프로젝트별 일일공수 분리 기능 추가 (ProjectDailyWork 모델)
- IssuesAPI에서 project_id 누락 문제 수정
- 사용자 인증 통합 (TokenManager 기반)
This commit is contained in:
hyungi
2025-10-24 12:24:24 +09:00
parent b024a178d0
commit 5fe51ab1d5
9 changed files with 358 additions and 87 deletions

View File

@@ -196,7 +196,7 @@
<nav class="bg-white border-b">
<div class="container mx-auto px-4">
<div id="navContainer" class="flex gap-2 py-2 overflow-x-auto">
<a href="daily-work.html" class="nav-link">
<a href="daily-work.html" class="nav-link" id="dailyWorkBtn" style="display: none;">
<i class="fas fa-calendar-check mr-2"></i>일일 공수
</a>
<button class="nav-link active" onclick="showSection('report')">
@@ -361,10 +361,10 @@
<option value="pending">검토 필요</option>
<option value="completed">검토 완료</option>
</select>
</div>
</div>
<!-- 날짜 필터 -->
<div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">날짜</label>
<select id="dateFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="displayIssueList()">
<option value="">전체</option>
@@ -372,8 +372,8 @@
<option value="week">이번 주</option>
<option value="month">이번 달</option>
</select>
</div>
</div>
<!-- 사용자 정의 날짜 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">특정 날짜</label>
@@ -403,9 +403,9 @@
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
</select>
</div>
<button onclick="printReport()" class="btn-primary px-4 py-2 rounded-lg text-sm">
<i class="fas fa-print mr-2"></i>인쇄
</button>
<button onclick="printReport()" class="btn-primary px-4 py-2 rounded-lg text-sm">
<i class="fas fa-print mr-2"></i>인쇄
</button>
</div>
</div>
<div id="reportContent">
@@ -415,7 +415,7 @@
</section>
</div>
<script src="/static/js/api.js?v=20250917"></script>
<script src="/static/js/api.js?v=20251024m"></script>
<script src="/static/js/image-utils.js?v=20250917"></script>
<script src="/static/js/date-utils.js?v=20250917"></script>
<script>
@@ -480,14 +480,15 @@
const listBtn = document.getElementById('listBtn');
const summaryBtn = document.getElementById('summaryBtn');
const adminBtn = document.getElementById('adminBtn');
const projectBtn = document.getElementById('projectBtn');
const dailyWorkBtn = document.getElementById('dailyWorkBtn');
if (currentUser.role === 'admin') {
// 관리자는 모든 메뉴 표시
listBtn.style.display = '';
summaryBtn.style.display = '';
projectBtn.style.display = '';
dailyWorkBtn.style.display = '';
adminBtn.style.display = '';
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
} else {
@@ -495,6 +496,7 @@
listBtn.style.display = 'none';
summaryBtn.style.display = 'none';
projectBtn.style.display = 'none';
dailyWorkBtn.style.display = 'none';
adminBtn.style.display = '';
adminBtn.innerHTML = '<i class="fas fa-key mr-2"></i>비밀번호 변경';
}
@@ -811,6 +813,10 @@
project_id: parseInt(projectId)
};
console.log('DEBUG: 전송할 issueData:', issueData);
console.log('DEBUG: projectId 원본:', projectId);
console.log('DEBUG: parseInt(projectId):', parseInt(projectId));
const startTime = Date.now();
await IssuesAPI.create(issueData);
const uploadTime = Date.now() - startTime;
@@ -873,59 +879,92 @@
}
// 프로젝트 로드
function loadProjects() {
async function loadProjects() {
// 1. 즉시 localStorage에서 로드 (빠른 응답)
const saved = localStorage.getItem('work-report-projects');
let projects = [];
if (saved) {
const projects = JSON.parse(saved);
const activeProjects = projects.filter(p => p.isActive);
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
const projectSelect = document.getElementById('projectSelect');
if (projectSelect) {
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
activeProjects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.jobNo} - ${project.projectName}`;
projectSelect.appendChild(option);
});
}
// 목록 관리의 프로젝트 필터 (모든 프로젝트)
const listProjectFilter = document.getElementById('listProjectFilter');
if (listProjectFilter) {
listProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
listProjectFilter.appendChild(option);
});
}
// 보고서의 프로젝트 필터 (모든 프로젝트)
const reportProjectFilter = document.getElementById('reportProjectFilter');
if (reportProjectFilter) {
reportProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
reportProjectFilter.appendChild(option);
});
}
projects = JSON.parse(saved);
displayProjectsInUI(projects);
}
// 2. 백그라운드에서 API 동기화
try {
const apiProjects = await ProjectsAPI.getAll(false);
// API 데이터를 localStorage 형식으로 변환
const syncedProjects = apiProjects.map(p => ({
id: p.id,
jobNo: p.job_no,
projectName: p.project_name,
isActive: p.is_active,
createdAt: p.created_at || new Date().toISOString(),
createdByName: '관리자'
}));
// localStorage 업데이트
localStorage.setItem('work-report-projects', JSON.stringify(syncedProjects));
// UI 다시 업데이트 (동기화된 데이터로)
displayProjectsInUI(syncedProjects);
} catch (error) {
// API 실패해도 localStorage 데이터로 계속 동작
console.log('API 동기화 실패, localStorage 데이터 사용:', error.message);
}
}
function displayProjectsInUI(projects) {
const activeProjects = projects.filter(p => p.isActive);
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
const projectSelect = document.getElementById('projectSelect');
if (projectSelect) {
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
activeProjects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.jobNo} - ${project.projectName}`;
projectSelect.appendChild(option);
});
}
// 목록 관리의 프로젝트 필터 (모든 프로젝트)
const listProjectFilter = document.getElementById('listProjectFilter');
if (listProjectFilter) {
listProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
listProjectFilter.appendChild(option);
});
}
// 보고서의 프로젝트 필터 (모든 프로젝트)
const reportProjectFilter = document.getElementById('reportProjectFilter');
if (reportProjectFilter) {
reportProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
reportProjectFilter.appendChild(option);
});
}
// 전역 캐시에 저장
window.projectsCache = projects;
}
// 선택된 프로젝트 정보 가져오기
function getSelectedProject(projectId) {
const saved = localStorage.getItem('work-report-projects');
if (saved) {
const projects = JSON.parse(saved);
return projects.find(p => p.id == projectId);
if (window.projectsCache) {
return window.projectsCache.find(p => p.id == projectId);
}
return null;
}
@@ -1090,7 +1129,7 @@
div.innerHTML = `
<div class="space-y-4">
<!-- 프로젝트 정보 및 상태 (오른쪽 상단) -->
<div class="flex justify-between items-start mb-2">
<div class="flex justify-between items-start mb-2">
<div class="flex items-center gap-2">
${isCompleted ?
'<div class="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium"><i class="fas fa-check-circle mr-1"></i>검토완료</div>' :
@@ -1475,35 +1514,38 @@
// 프로젝트별 일일 공수 데이터 계산
let dailyWorkTotal = 0;
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
console.log('일일공수 데이터:', dailyWorkData);
console.log('선택된 프로젝트 ID:', selectedProjectId);
// localStorage의 프로젝트별 데이터 우선 사용 (프로젝트별 분리 지원)
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
if (selectedProjectId) {
// 선택된 프로젝트의 일일 공수만 합계
dailyWorkData.forEach(dayWork => {
console.log('일일공수 항목:', dayWork);
if (dayWork.projects) {
dayWork.projects.forEach(project => {
console.log('프로젝트:', project, '매칭 확인:', project.projectId == selectedProjectId);
if (project.projectId == selectedProjectId || project.projectId.toString() === selectedProjectId.toString()) {
dailyWorkTotal += project.hours || 0;
console.log('시간 추가:', project.hours, '누적:', dailyWorkTotal);
}
});
}
});
console.log(`프로젝트 ID ${selectedProjectId}의 총 일일공수:`, dailyWorkTotal);
} else {
// 전체 프로젝트의 일일 공수 합계
dailyWorkData.forEach(dayWork => {
console.log('전체 일일공수 항목:', dayWork);
dailyWorkTotal += dayWork.totalHours || 0;
});
try {
// 백엔드 API에서 전체 일일공수 데이터 가져오기
const apiDailyWork = await DailyWorkAPI.getAll();
dailyWorkTotal = apiDailyWork.reduce((sum, work) => sum + (work.total_hours || 0), 0);
console.log('API에서 가져온 전체 총 일일공수:', dailyWorkTotal);
} catch (error) {
// API 실패 시 localStorage 사용
dailyWorkData.forEach(dayWork => {
dailyWorkTotal += dayWork.totalHours || 0;
});
console.log('localStorage에서 가져온 전체 총 일일공수:', dailyWorkTotal);
}
}
console.log('최종 일일공수 합계:', dailyWorkTotal);
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
const issueHours = filteredIssues.reduce((sum, issue) => sum + (issue.work_hours || 0), 0);
const categoryCount = {};