feat: 목록 관리 및 보고서 페이지 개선
- 목록 관리 페이지에 고급 필터링 시스템 추가 - 프로젝트별, 검토상태별, 날짜별 필터링 - 검토 완료/필요 항목 시각적 구분 및 정렬 - 해결 시간 입력 + 확인 버튼으로 검토 완료 처리 - 부적합 조회 페이지에 동일한 필터링 기능 적용 - 검토 상태에 따른 카드 스타일링 (음영 처리) - JavaScript 템플릿 리터럴 오류 수정 - 보고서 페이지 프로젝트별 분석 기능 추가 - 프로젝트 선택 드롭다운 추가 - 총 작업 공수를 프로젝트별 일일공수 데이터로 계산 - 부적합 처리 시간, 카테고리 분석, 상세 목록 모두 프로젝트별 필터링 - localStorage 키 이름 통일 (daily-work-data)
This commit is contained in:
193
frontend/create-project-api.html
Normal file
193
frontend/create-project-api.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API 프로젝트 생성 - M Project</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="/static/js/api.js?v=20250917"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-plus text-green-500 mr-2"></i>API 프로젝트 생성
|
||||
</h1>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<h3 class="font-semibold text-green-800 mb-2">🎯 작업 내용</h3>
|
||||
<p class="text-green-700 text-sm">
|
||||
백엔드 API를 통해 TKR-25009R M Project를 생성합니다.<br>
|
||||
데이터베이스에 실제 프로젝트 레코드가 생성됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="status" class="space-y-2">
|
||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="createBtn"
|
||||
onclick="createProject()"
|
||||
class="w-full bg-green-500 text-white py-3 px-4 rounded-lg hover:bg-green-600 transition-colors font-medium"
|
||||
>
|
||||
<i class="fas fa-plus mr-2"></i>TKR-25009R 프로젝트 생성
|
||||
</button>
|
||||
|
||||
<a href="fix-api-data.html" class="block text-center text-orange-600 hover:text-orange-800 mt-4">
|
||||
<i class="fas fa-wrench mr-1"></i>API 데이터 수정 도구로 이동
|
||||
</a>
|
||||
|
||||
<a href="debug-data.html" class="block text-center text-blue-600 hover:text-blue-800 mt-2">
|
||||
<i class="fas fa-bug mr-1"></i>디버그 도구로 이동
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUser = null;
|
||||
|
||||
// 페이지 로드 시 사용자 확인
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const user = TokenManager.getUser();
|
||||
if (!user) {
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
currentUser = user;
|
||||
|
||||
if (currentUser.role !== 'admin') {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
addStatus('✅ 관리자 권한 확인됨', 'text-green-600');
|
||||
});
|
||||
|
||||
function addStatus(message, className = 'text-gray-600') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
const p = document.createElement('p');
|
||||
p.className = `text-sm ${className}`;
|
||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
||||
statusDiv.appendChild(p);
|
||||
}
|
||||
|
||||
async function createProject() {
|
||||
const btn = document.getElementById('createBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>생성 중...';
|
||||
|
||||
try {
|
||||
// 1. 기존 프로젝트 확인
|
||||
addStatus('기존 프로젝트 확인 중...');
|
||||
|
||||
let existingProjects = [];
|
||||
try {
|
||||
existingProjects = await ProjectsAPI.getAll();
|
||||
addStatus(`기존 프로젝트 ${existingProjects.length}개 발견`, 'text-blue-600');
|
||||
} catch (error) {
|
||||
addStatus('기존 프로젝트 조회 실패, 새로 생성합니다.', 'text-yellow-600');
|
||||
}
|
||||
|
||||
// TKR-25009R 프로젝트가 이미 있는지 확인
|
||||
const existingMProject = existingProjects.find(p => p.job_no === 'TKR-25009R');
|
||||
if (existingMProject) {
|
||||
addStatus(`✅ TKR-25009R 프로젝트가 이미 존재합니다 (ID: ${existingMProject.id})`, 'text-green-600');
|
||||
|
||||
// localStorage 프로젝트 ID 업데이트
|
||||
updateLocalStorageProjectId(existingMProject.id);
|
||||
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>이미 존재함';
|
||||
btn.className = 'w-full bg-blue-500 text-white py-3 px-4 rounded-lg font-medium';
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 새 프로젝트 생성
|
||||
addStatus('TKR-25009R 프로젝트 생성 중...');
|
||||
|
||||
const projectData = {
|
||||
job_no: 'TKR-25009R',
|
||||
project_name: 'M Project'
|
||||
};
|
||||
|
||||
const newProject = await ProjectsAPI.create(projectData);
|
||||
addStatus(`✅ 프로젝트 생성 완료! ID: ${newProject.id}`, 'text-green-600');
|
||||
|
||||
// 3. localStorage 프로젝트 ID 업데이트
|
||||
updateLocalStorageProjectId(newProject.id);
|
||||
|
||||
// 완료
|
||||
addStatus('🎉 프로젝트 생성 및 동기화 완료!', 'text-green-600 font-bold');
|
||||
addStatus('이제 API 데이터 수정 도구를 실행하세요.', 'text-blue-600');
|
||||
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>생성 완료';
|
||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
||||
|
||||
} catch (error) {
|
||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
||||
}
|
||||
}
|
||||
|
||||
function updateLocalStorageProjectId(apiProjectId) {
|
||||
try {
|
||||
// localStorage 프로젝트 데이터 업데이트
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
||||
|
||||
if (mProject) {
|
||||
const oldId = mProject.id;
|
||||
mProject.id = apiProjectId;
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
addStatus(`localStorage 프로젝트 ID 업데이트: ${oldId} → ${apiProjectId}`, 'text-blue-600');
|
||||
|
||||
// 일일 공수 데이터도 업데이트
|
||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
||||
let updatedCount = 0;
|
||||
|
||||
dailyWorkData.forEach(dayData => {
|
||||
if (dayData.projects) {
|
||||
dayData.projects.forEach(project => {
|
||||
if (project.projectId == oldId || project.projectId == 1) {
|
||||
project.projectId = apiProjectId;
|
||||
project.projectName = 'TKR-25009R - M Project';
|
||||
updatedCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
|
||||
addStatus(`일일 공수 데이터 ${updatedCount}개 업데이트`, 'text-blue-600');
|
||||
|
||||
// localStorage 부적합 사항도 업데이트
|
||||
const localIssues = JSON.parse(localStorage.getItem('work-report-issues') || '[]');
|
||||
let issueUpdatedCount = 0;
|
||||
|
||||
localIssues.forEach(issue => {
|
||||
if (issue.projectId == oldId || issue.projectId == 1 || issue.project_id == oldId || issue.project_id == 1) {
|
||||
issue.projectId = apiProjectId;
|
||||
issue.project_id = apiProjectId;
|
||||
issue.projectName = 'TKR-25009R - M Project';
|
||||
issue.project_name = 'TKR-25009R - M Project';
|
||||
issueUpdatedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('work-report-issues', JSON.stringify(localIssues));
|
||||
addStatus(`localStorage 부적합 사항 ${issueUpdatedCount}개 업데이트`, 'text-blue-600');
|
||||
}
|
||||
} catch (error) {
|
||||
addStatus(`localStorage 업데이트 실패: ${error.message}`, 'text-red-600');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -98,11 +98,14 @@
|
||||
<div class="container mx-auto px-4 py-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-xl font-bold text-gray-800">
|
||||
<i class="fas fa-clipboard-list mr-2"></i>작업보고서 시스템
|
||||
<i class="fas fa-clipboard-check text-blue-500 mr-2"></i>작업보고서
|
||||
</h1>
|
||||
<button onclick="AuthAPI.logout()" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>로그아웃
|
||||
</button>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-gray-600" id="userDisplay"></span>
|
||||
<button onclick="logout()" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -126,6 +129,9 @@
|
||||
<a href="index.html#summary" class="nav-link" style="display:none;" id="summaryBtn">
|
||||
<i class="fas fa-chart-bar mr-2"></i>보고서
|
||||
</a>
|
||||
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
||||
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
||||
</a>
|
||||
<a href="admin.html" class="nav-link" style="display:none;" id="adminBtn">
|
||||
<i class="fas fa-users-cog mr-2"></i>관리
|
||||
</a>
|
||||
@@ -152,65 +158,34 @@
|
||||
id="workDate"
|
||||
class="input-field w-full px-4 py-3 rounded-lg text-lg"
|
||||
required
|
||||
onchange="loadExistingData()"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 인원 입력 -->
|
||||
<!-- 프로젝트별 시간 입력 섹션 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-users mr-1"></i>작업 인원
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="workerCount"
|
||||
min="1"
|
||||
class="input-field w-full px-4 py-3 rounded-lg text-lg"
|
||||
placeholder="예: 5"
|
||||
required
|
||||
>
|
||||
<p class="text-sm text-gray-500 mt-1">기본 근무시간: 8시간/인</p>
|
||||
</div>
|
||||
|
||||
<!-- 잔업 섹션 -->
|
||||
<div class="border-t pt-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
<i class="fas fa-clock mr-1"></i>잔업 여부
|
||||
<i class="fas fa-folder-open mr-1"></i>프로젝트별 작업 시간
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
id="overtimeToggle"
|
||||
class="px-4 py-2 rounded-lg border border-gray-300 hover:bg-gray-50 transition-colors"
|
||||
onclick="toggleOvertime()"
|
||||
id="addProjectBtn"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
onclick="addProjectEntry()"
|
||||
>
|
||||
<i class="fas fa-plus mr-2"></i>잔업 추가
|
||||
<i class="fas fa-plus mr-2"></i>프로젝트 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 잔업 입력 영역 -->
|
||||
<div id="overtimeSection" class="hidden space-y-3 bg-gray-50 p-4 rounded-lg">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">잔업 인원</label>
|
||||
<input
|
||||
type="number"
|
||||
id="overtimeWorkers"
|
||||
min="0"
|
||||
class="input-field w-full px-3 py-2 rounded"
|
||||
placeholder="명"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">잔업 시간</label>
|
||||
<input
|
||||
type="number"
|
||||
id="overtimeHours"
|
||||
min="0"
|
||||
step="0.5"
|
||||
class="input-field w-full px-3 py-2 rounded"
|
||||
placeholder="시간"
|
||||
>
|
||||
</div>
|
||||
<div id="projectEntries" class="space-y-3">
|
||||
<!-- 프로젝트 입력 항목들이 여기에 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium text-blue-900">총 작업 시간:</span>
|
||||
<span id="totalHours" class="text-lg font-bold text-blue-600">0시간</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,7 +221,9 @@
|
||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||
<script>
|
||||
let currentUser = null;
|
||||
let dailyWorks = [];
|
||||
let projects = [];
|
||||
let dailyWorkData = [];
|
||||
let projectEntryCounter = 0;
|
||||
|
||||
// 페이지 로드 시 인증 체크
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
@@ -257,12 +234,22 @@
|
||||
}
|
||||
currentUser = user;
|
||||
|
||||
// 사용자 표시
|
||||
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
||||
|
||||
// 네비게이션 권한 체크
|
||||
updateNavigation();
|
||||
|
||||
// 프로젝트 및 일일 공수 데이터 로드
|
||||
loadProjects();
|
||||
loadDailyWorkData();
|
||||
|
||||
// 오늘 날짜로 초기화
|
||||
document.getElementById('workDate').valueAsDate = new Date();
|
||||
|
||||
// 첫 번째 프로젝트 입력 항목 추가
|
||||
addProjectEntry();
|
||||
|
||||
// 최근 내역 로드
|
||||
await loadRecentEntries();
|
||||
});
|
||||
@@ -272,95 +259,207 @@
|
||||
const listBtn = document.getElementById('listBtn');
|
||||
const summaryBtn = document.getElementById('summaryBtn');
|
||||
const adminBtn = document.getElementById('adminBtn');
|
||||
const projectBtn = document.getElementById('projectBtn');
|
||||
|
||||
if (currentUser.role === 'admin') {
|
||||
// 관리자는 모든 메뉴 표시
|
||||
listBtn.style.display = '';
|
||||
summaryBtn.style.display = '';
|
||||
projectBtn.style.display = '';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
||||
} else {
|
||||
// 일반 사용자는 제한된 메뉴만 표시
|
||||
listBtn.style.display = 'none';
|
||||
summaryBtn.style.display = 'none';
|
||||
projectBtn.style.display = 'none';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-key mr-2"></i>비밀번호 변경';
|
||||
}
|
||||
}
|
||||
|
||||
// 잔업 토글
|
||||
function toggleOvertime() {
|
||||
const section = document.getElementById('overtimeSection');
|
||||
const button = document.getElementById('overtimeToggle');
|
||||
// 프로젝트 데이터 로드
|
||||
function loadProjects() {
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
}
|
||||
}
|
||||
|
||||
// 일일 공수 데이터 로드
|
||||
function loadDailyWorkData() {
|
||||
const saved = localStorage.getItem('daily-work-data');
|
||||
if (saved) {
|
||||
dailyWorkData = JSON.parse(saved);
|
||||
}
|
||||
}
|
||||
|
||||
// 일일 공수 데이터 저장
|
||||
function saveDailyWorkData() {
|
||||
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
|
||||
}
|
||||
|
||||
// 활성 프로젝트만 필터링
|
||||
function getActiveProjects() {
|
||||
return projects.filter(p => p.isActive);
|
||||
}
|
||||
|
||||
// 프로젝트 입력 항목 추가
|
||||
function addProjectEntry() {
|
||||
const activeProjects = getActiveProjects();
|
||||
if (activeProjects.length === 0) {
|
||||
alert('활성 프로젝트가 없습니다. 먼저 프로젝트를 생성해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.classList.contains('hidden')) {
|
||||
section.classList.remove('hidden');
|
||||
button.innerHTML = '<i class="fas fa-minus mr-2"></i>잔업 취소';
|
||||
button.classList.add('bg-gray-100');
|
||||
projectEntryCounter++;
|
||||
const entryId = `project-entry-${projectEntryCounter}`;
|
||||
|
||||
const entryHtml = `
|
||||
<div id="${entryId}" class="flex gap-3 items-center p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex-1">
|
||||
<select class="input-field w-full px-3 py-2 rounded-lg" onchange="updateTotalHours()">
|
||||
<option value="">프로젝트 선택</option>
|
||||
${activeProjects.map(p => `<option value="${p.id}">${p.jobNo} - ${p.projectName}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-32">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="시간"
|
||||
min="0"
|
||||
step="0.5"
|
||||
class="input-field w-full px-3 py-2 rounded-lg text-center"
|
||||
onchange="updateTotalHours()"
|
||||
>
|
||||
</div>
|
||||
<div class="w-16 text-center text-gray-600">시간</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick="removeProjectEntry('${entryId}')"
|
||||
class="text-red-500 hover:text-red-700 p-2"
|
||||
title="제거"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('projectEntries').insertAdjacentHTML('beforeend', entryHtml);
|
||||
updateTotalHours();
|
||||
}
|
||||
|
||||
// 프로젝트 입력 항목 제거
|
||||
function removeProjectEntry(entryId) {
|
||||
const entry = document.getElementById(entryId);
|
||||
if (entry) {
|
||||
entry.remove();
|
||||
updateTotalHours();
|
||||
}
|
||||
}
|
||||
|
||||
// 총 시간 계산 및 업데이트
|
||||
function updateTotalHours() {
|
||||
const entries = document.querySelectorAll('#projectEntries > div');
|
||||
let totalHours = 0;
|
||||
|
||||
entries.forEach(entry => {
|
||||
const hoursInput = entry.querySelector('input[type="number"]');
|
||||
const hours = parseFloat(hoursInput.value) || 0;
|
||||
totalHours += hours;
|
||||
});
|
||||
|
||||
document.getElementById('totalHours').textContent = `${totalHours}시간`;
|
||||
}
|
||||
|
||||
// 기존 데이터 로드 (날짜 선택 시)
|
||||
function loadExistingData() {
|
||||
const selectedDate = document.getElementById('workDate').value;
|
||||
if (!selectedDate) return;
|
||||
|
||||
const existingData = dailyWorkData.find(d => d.date === selectedDate);
|
||||
if (existingData) {
|
||||
// 기존 프로젝트 입력 항목들 제거
|
||||
document.getElementById('projectEntries').innerHTML = '';
|
||||
projectEntryCounter = 0;
|
||||
|
||||
// 기존 데이터로 프로젝트 입력 항목들 생성
|
||||
existingData.projects.forEach(projectData => {
|
||||
addProjectEntry();
|
||||
const lastEntry = document.querySelector('#projectEntries > div:last-child');
|
||||
const select = lastEntry.querySelector('select');
|
||||
const input = lastEntry.querySelector('input[type="number"]');
|
||||
|
||||
select.value = projectData.projectId;
|
||||
input.value = projectData.hours;
|
||||
});
|
||||
|
||||
updateTotalHours();
|
||||
} else {
|
||||
section.classList.add('hidden');
|
||||
button.innerHTML = '<i class="fas fa-plus mr-2"></i>잔업 추가';
|
||||
button.classList.remove('bg-gray-100');
|
||||
// 잔업 입력값 초기화
|
||||
document.getElementById('overtimeWorkers').value = '';
|
||||
document.getElementById('overtimeHours').value = '';
|
||||
// 새로운 날짜인 경우 초기화
|
||||
document.getElementById('projectEntries').innerHTML = '';
|
||||
projectEntryCounter = 0;
|
||||
addProjectEntry();
|
||||
}
|
||||
calculateTotal();
|
||||
}
|
||||
|
||||
// 총 공수 계산
|
||||
function calculateTotal() {
|
||||
const workerCount = parseInt(document.getElementById('workerCount').value) || 0;
|
||||
const regularHours = workerCount * 8; // 기본 8시간
|
||||
|
||||
let overtimeTotal = 0;
|
||||
if (!document.getElementById('overtimeSection').classList.contains('hidden')) {
|
||||
const overtimeWorkers = parseInt(document.getElementById('overtimeWorkers').value) || 0;
|
||||
const overtimeHours = parseFloat(document.getElementById('overtimeHours').value) || 0;
|
||||
overtimeTotal = overtimeWorkers * overtimeHours;
|
||||
}
|
||||
|
||||
const total = regularHours + overtimeTotal;
|
||||
document.getElementById('totalHours').textContent = `${total}시간`;
|
||||
}
|
||||
|
||||
// 입력값 변경 시 총 공수 재계산
|
||||
document.getElementById('workerCount').addEventListener('input', calculateTotal);
|
||||
document.getElementById('overtimeWorkers').addEventListener('input', calculateTotal);
|
||||
document.getElementById('overtimeHours').addEventListener('input', calculateTotal);
|
||||
|
||||
// 폼 제출
|
||||
document.getElementById('dailyWorkForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const workDate = document.getElementById('workDate').value;
|
||||
const workerCount = parseInt(document.getElementById('workerCount').value);
|
||||
const selectedDate = document.getElementById('workDate').value;
|
||||
const entries = document.querySelectorAll('#projectEntries > div');
|
||||
|
||||
let overtimeWorkers = 0;
|
||||
let overtimeHours = 0;
|
||||
const projectData = [];
|
||||
let hasValidEntry = false;
|
||||
|
||||
if (!document.getElementById('overtimeSection').classList.contains('hidden')) {
|
||||
overtimeWorkers = parseInt(document.getElementById('overtimeWorkers').value) || 0;
|
||||
overtimeHours = parseFloat(document.getElementById('overtimeHours').value) || 0;
|
||||
entries.forEach(entry => {
|
||||
const select = entry.querySelector('select');
|
||||
const input = entry.querySelector('input[type="number"]');
|
||||
const projectId = select.value;
|
||||
const hours = parseFloat(input.value) || 0;
|
||||
|
||||
if (projectId && hours > 0) {
|
||||
const project = projects.find(p => p.id == projectId);
|
||||
projectData.push({
|
||||
projectId: projectId,
|
||||
projectName: project ? `${project.jobNo} - ${project.projectName}` : '알 수 없음',
|
||||
hours: hours
|
||||
});
|
||||
hasValidEntry = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasValidEntry) {
|
||||
alert('최소 하나의 프로젝트에 시간을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// API 호출
|
||||
await DailyWorkAPI.create({
|
||||
date: new Date(workDate).toISOString(),
|
||||
worker_count: workerCount,
|
||||
overtime_workers: overtimeWorkers,
|
||||
overtime_hours: overtimeHours
|
||||
});
|
||||
// 기존 데이터 업데이트 또는 새로 추가
|
||||
const existingIndex = dailyWorkData.findIndex(d => d.date === selectedDate);
|
||||
const newData = {
|
||||
date: selectedDate,
|
||||
projects: projectData,
|
||||
totalHours: projectData.reduce((sum, p) => sum + p.hours, 0),
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: currentUser.username || currentUser
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
dailyWorkData[existingIndex] = newData;
|
||||
} else {
|
||||
dailyWorkData.push(newData);
|
||||
}
|
||||
|
||||
saveDailyWorkData();
|
||||
|
||||
// 성공 메시지
|
||||
showSuccessMessage();
|
||||
|
||||
// 폼 초기화
|
||||
resetForm();
|
||||
|
||||
// 최근 내역 갱신
|
||||
await loadRecentEntries();
|
||||
|
||||
} catch (error) {
|
||||
alert(error.message || '저장에 실패했습니다.');
|
||||
}
|
||||
@@ -369,22 +468,16 @@
|
||||
// 최근 데이터 로드
|
||||
async function loadRecentEntries() {
|
||||
try {
|
||||
// 최근 7일간 데이터 가져오기
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 7);
|
||||
// 최근 7일 데이터 표시
|
||||
const recentData = dailyWorkData
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
.slice(0, 7);
|
||||
|
||||
dailyWorks = await DailyWorkAPI.getAll({
|
||||
start_date: startDate.toISOString().split('T')[0],
|
||||
end_date: endDate.toISOString().split('T')[0],
|
||||
limit: 7
|
||||
});
|
||||
|
||||
displayRecentEntries();
|
||||
displayRecentEntries(recentData);
|
||||
} catch (error) {
|
||||
console.error('데이터 로드 실패:', error);
|
||||
dailyWorks = [];
|
||||
displayRecentEntries();
|
||||
document.getElementById('recentEntries').innerHTML =
|
||||
'<p class="text-gray-500 text-center py-4">데이터를 불러올 수 없습니다.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,52 +496,42 @@
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 폼 초기화
|
||||
function resetForm() {
|
||||
document.getElementById('workerCount').value = '';
|
||||
document.getElementById('overtimeWorkers').value = '';
|
||||
document.getElementById('overtimeHours').value = '';
|
||||
document.getElementById('overtimeSection').classList.add('hidden');
|
||||
document.getElementById('overtimeToggle').innerHTML = '<i class="fas fa-plus mr-2"></i>잔업 추가';
|
||||
document.getElementById('overtimeToggle').classList.remove('bg-gray-100');
|
||||
document.getElementById('totalHours').textContent = '0시간';
|
||||
|
||||
// 날짜는 오늘로 유지
|
||||
document.getElementById('workDate').valueAsDate = new Date();
|
||||
}
|
||||
|
||||
// 최근 입력 내역 표시
|
||||
function displayRecentEntries() {
|
||||
function displayRecentEntries(entries) {
|
||||
const container = document.getElementById('recentEntries');
|
||||
|
||||
if (dailyWorks.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 text-center py-4">입력된 내역이 없습니다.</p>';
|
||||
if (!entries || entries.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 text-center py-4">최근 입력 내역이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = dailyWorks.map(item => {
|
||||
const dateStr = DateUtils.formatKST(item.date);
|
||||
const relativeTime = DateUtils.getRelativeTime(item.created_at || item.date);
|
||||
container.innerHTML = entries.map(item => {
|
||||
const date = new Date(item.date);
|
||||
const dateStr = `${date.getMonth() + 1}/${date.getDate()} (${['일','월','화','수','목','금','토'][date.getDay()]})`;
|
||||
|
||||
return `
|
||||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div>
|
||||
<div class="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="font-medium text-gray-800">${dateStr}</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
인원: ${item.worker_count}명
|
||||
${item.overtime_total > 0 ? `• 잔업: ${item.overtime_workers}명 × ${item.overtime_hours}시간` : ''}
|
||||
</p>
|
||||
<div class="text-right">
|
||||
<p class="text-lg font-bold text-blue-600">${item.totalHours}시간</p>
|
||||
${currentUser && (currentUser.role === 'admin' || currentUser.username === 'hyungi') ? `
|
||||
<button
|
||||
onclick="deleteDailyWork('${item.date}')"
|
||||
class="mt-1 px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-xs"
|
||||
>
|
||||
<i class="fas fa-trash mr-1"></i>삭제
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-lg font-bold text-blue-600">${item.total_hours}시간</p>
|
||||
${currentUser && currentUser.role === 'admin' ? `
|
||||
<button
|
||||
onclick="deleteDailyWork(${item.id})"
|
||||
class="mt-1 px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-xs"
|
||||
>
|
||||
<i class="fas fa-trash mr-1"></i>삭제
|
||||
</button>
|
||||
` : ''}
|
||||
<div class="space-y-1">
|
||||
${item.projects.map(p => `
|
||||
<div class="flex justify-between text-sm text-gray-600">
|
||||
<span>${p.projectName}</span>
|
||||
<span>${p.hours}시간</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -456,8 +539,8 @@
|
||||
}
|
||||
|
||||
// 일일 공수 삭제 (관리자만)
|
||||
async function deleteDailyWork(workId) {
|
||||
if (!currentUser || currentUser.role !== 'admin') {
|
||||
async function deleteDailyWork(date) {
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.username !== 'hyungi')) {
|
||||
alert('관리자만 삭제할 수 있습니다.');
|
||||
return;
|
||||
}
|
||||
@@ -467,16 +550,23 @@
|
||||
}
|
||||
|
||||
try {
|
||||
await DailyWorkAPI.delete(workId);
|
||||
|
||||
// 성공 시 목록 다시 로드
|
||||
await loadRecentEntries();
|
||||
|
||||
alert('삭제되었습니다.');
|
||||
const index = dailyWorkData.findIndex(d => d.date === date);
|
||||
if (index >= 0) {
|
||||
dailyWorkData.splice(index, 1);
|
||||
saveDailyWorkData();
|
||||
await loadRecentEntries();
|
||||
alert('삭제되었습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message || '삭제에 실패했습니다.');
|
||||
alert('삭제에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 로그아웃
|
||||
function logout() {
|
||||
TokenManager.clearToken();
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
158
frontend/debug-data.html
Normal file
158
frontend/debug-data.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>데이터 디버그 - M Project</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="/static/js/api.js?v=20250917"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-bug text-red-500 mr-2"></i>데이터 디버그
|
||||
</h1>
|
||||
|
||||
<div class="space-y-6">
|
||||
<button
|
||||
onclick="debugData()"
|
||||
class="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
|
||||
>
|
||||
<i class="fas fa-search mr-2"></i>데이터 상태 확인
|
||||
</button>
|
||||
|
||||
<div id="results" class="space-y-4">
|
||||
<!-- 결과가 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function debugData() {
|
||||
const resultsDiv = document.getElementById('results');
|
||||
resultsDiv.innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin text-2xl"></i></div>';
|
||||
|
||||
try {
|
||||
// 1. 프로젝트 데이터 확인
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
|
||||
// 2. 부적합 사항 데이터 확인 (API)
|
||||
let apiIssues = [];
|
||||
try {
|
||||
apiIssues = await IssuesAPI.getAll();
|
||||
} catch (error) {
|
||||
console.error('API 조회 실패:', error);
|
||||
}
|
||||
|
||||
// 3. 부적합 사항 데이터 확인 (localStorage)
|
||||
const localIssues = JSON.parse(localStorage.getItem('work-report-issues') || '[]');
|
||||
|
||||
// 4. 일일 공수 데이터 확인
|
||||
const dailyWork = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
||||
|
||||
// 결과 표시
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<!-- 프로젝트 데이터 -->
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<h3 class="font-bold text-blue-800 mb-3">📁 프로젝트 데이터 (${projects.length}개)</h3>
|
||||
<div class="space-y-2">
|
||||
${projects.map(p => `
|
||||
<div class="bg-white p-3 rounded border">
|
||||
<div class="font-medium">ID: ${p.id}</div>
|
||||
<div>Job No: ${p.jobNo}</div>
|
||||
<div>이름: ${p.projectName}</div>
|
||||
<div>활성: ${p.isActive ? '✅' : '❌'}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API 부적합 사항 -->
|
||||
<div class="bg-green-50 p-4 rounded-lg">
|
||||
<h3 class="font-bold text-green-800 mb-3">🔗 API 부적합 사항 (${apiIssues.length}개)</h3>
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||
${apiIssues.map(issue => `
|
||||
<div class="bg-white p-3 rounded border text-sm">
|
||||
<div><strong>ID:</strong> ${issue.id}</div>
|
||||
<div><strong>project_id:</strong> ${issue.project_id || 'null'}</div>
|
||||
<div><strong>project_name:</strong> ${issue.project_name || 'null'}</div>
|
||||
<div><strong>설명:</strong> ${issue.description?.substring(0, 50)}...</div>
|
||||
<div><strong>카테고리:</strong> ${issue.category}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- localStorage 부적합 사항 -->
|
||||
<div class="bg-yellow-50 p-4 rounded-lg">
|
||||
<h3 class="font-bold text-yellow-800 mb-3">💾 localStorage 부적합 사항 (${localIssues.length}개)</h3>
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||
${localIssues.map(issue => `
|
||||
<div class="bg-white p-3 rounded border text-sm">
|
||||
<div><strong>ID:</strong> ${issue.id}</div>
|
||||
<div><strong>project_id:</strong> ${issue.project_id || 'null'}</div>
|
||||
<div><strong>projectId:</strong> ${issue.projectId || 'null'}</div>
|
||||
<div><strong>project_name:</strong> ${issue.project_name || 'null'}</div>
|
||||
<div><strong>projectName:</strong> ${issue.projectName || 'null'}</div>
|
||||
<div><strong>설명:</strong> ${issue.description?.substring(0, 50)}...</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 일일 공수 데이터 -->
|
||||
<div class="bg-purple-50 p-4 rounded-lg">
|
||||
<h3 class="font-bold text-purple-800 mb-3">⏰ 일일 공수 데이터 (${dailyWork.length}개)</h3>
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||
${dailyWork.map(day => `
|
||||
<div class="bg-white p-3 rounded border text-sm">
|
||||
<div><strong>날짜:</strong> ${day.date}</div>
|
||||
<div><strong>총 시간:</strong> ${day.totalHours}시간</div>
|
||||
<div><strong>프로젝트들:</strong></div>
|
||||
${day.projects?.map(p => `
|
||||
<div class="ml-4 text-xs">
|
||||
- ID: ${p.projectId}, 이름: ${p.projectName}, 시간: ${p.hours}
|
||||
</div>
|
||||
`).join('') || '<div class="ml-4 text-xs text-red-500">프로젝트 데이터 없음</div>'}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터링 테스트 -->
|
||||
<div class="bg-red-50 p-4 rounded-lg">
|
||||
<h3 class="font-bold text-red-800 mb-3">🔍 필터링 테스트</h3>
|
||||
<div class="space-y-2">
|
||||
${projects.map(project => {
|
||||
const matchingApiIssues = apiIssues.filter(issue => issue.project_id == project.id);
|
||||
const matchingLocalIssues = localIssues.filter(issue => issue.project_id == project.id || issue.projectId == project.id);
|
||||
|
||||
return `
|
||||
<div class="bg-white p-3 rounded border">
|
||||
<div class="font-medium">${project.jobNo} - ${project.projectName} (ID: ${project.id})</div>
|
||||
<div class="text-sm">API 매칭: ${matchingApiIssues.length}개</div>
|
||||
<div class="text-sm">localStorage 매칭: ${matchingLocalIssues.length}개</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="bg-red-50 p-4 rounded-lg">
|
||||
<h3 class="font-bold text-red-800 mb-3">❌ 오류 발생</h3>
|
||||
<pre class="text-sm">${error.message}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
171
frontend/fix-api-data.html
Normal file
171
frontend/fix-api-data.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API 데이터 수정 - M Project</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="/static/js/api.js?v=20250917"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-wrench text-orange-500 mr-2"></i>API 데이터 수정
|
||||
</h1>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<h3 class="font-semibold text-yellow-800 mb-2">⚠️ 주의사항</h3>
|
||||
<p class="text-yellow-700 text-sm">
|
||||
이 작업은 API 데이터베이스의 모든 부적합 사항에 TKR-25009R 프로젝트 정보를 추가합니다.<br>
|
||||
관리자(hyungi) 계정으로 로그인한 상태에서만 실행하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="status" class="space-y-2">
|
||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="fixBtn"
|
||||
onclick="fixApiData()"
|
||||
class="w-full bg-orange-500 text-white py-3 px-4 rounded-lg hover:bg-orange-600 transition-colors font-medium"
|
||||
>
|
||||
<i class="fas fa-tools mr-2"></i>API 데이터 수정 시작
|
||||
</button>
|
||||
|
||||
<a href="debug-data.html" class="block text-center text-blue-600 hover:text-blue-800 mt-4">
|
||||
<i class="fas fa-bug mr-1"></i>디버그 도구로 이동
|
||||
</a>
|
||||
|
||||
<a href="index.html" class="block text-center text-gray-600 hover:text-gray-800 mt-2">
|
||||
<i class="fas fa-arrow-left mr-1"></i>메인으로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUser = null;
|
||||
|
||||
// 페이지 로드 시 사용자 확인
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const user = TokenManager.getUser();
|
||||
if (!user) {
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
currentUser = user;
|
||||
|
||||
if (currentUser.role !== 'admin') {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
addStatus('✅ 관리자 권한 확인됨', 'text-green-600');
|
||||
});
|
||||
|
||||
function addStatus(message, className = 'text-gray-600') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
const p = document.createElement('p');
|
||||
p.className = `text-sm ${className}`;
|
||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
||||
statusDiv.appendChild(p);
|
||||
}
|
||||
|
||||
async function fixApiData() {
|
||||
const btn = document.getElementById('fixBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>수정 중...';
|
||||
|
||||
try {
|
||||
// 1. TKR-25009R 프로젝트 찾기
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
||||
|
||||
if (!mProject) {
|
||||
throw new Error('TKR-25009R 프로젝트를 찾을 수 없습니다. 먼저 마이그레이션을 실행하세요.');
|
||||
}
|
||||
|
||||
addStatus(`TKR-25009R 프로젝트 발견 (ID: ${mProject.id})`, 'text-blue-600');
|
||||
|
||||
// 2. API에서 모든 부적합 사항 가져오기
|
||||
addStatus('API에서 부적합 사항 조회 중...');
|
||||
const issues = await IssuesAPI.getAll();
|
||||
addStatus(`총 ${issues.length}개 부적합 사항 발견`, 'text-blue-600');
|
||||
|
||||
// 3. project_id가 null인 부적합 사항들 찾기
|
||||
const issuesWithoutProject = issues.filter(issue => !issue.project_id);
|
||||
addStatus(`프로젝트 정보가 없는 부적합 사항: ${issuesWithoutProject.length}개`, 'text-yellow-600');
|
||||
|
||||
if (issuesWithoutProject.length === 0) {
|
||||
addStatus('✅ 모든 부적합 사항에 이미 프로젝트 정보가 있습니다.', 'text-green-600');
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>완료';
|
||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 각 부적합 사항 업데이트
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (let i = 0; i < issuesWithoutProject.length; i++) {
|
||||
const issue = issuesWithoutProject[i];
|
||||
|
||||
try {
|
||||
addStatus(`${i + 1}/${issuesWithoutProject.length}: 부적합 사항 ID ${issue.id} 업데이트 중...`);
|
||||
|
||||
await IssuesAPI.update(issue.id, {
|
||||
project_id: mProject.id
|
||||
});
|
||||
|
||||
successCount++;
|
||||
|
||||
// UI 업데이트를 위한 짧은 대기
|
||||
if (i % 5 === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`부적합 사항 ID ${issue.id} 업데이트 실패:`, error);
|
||||
addStatus(`❌ 부적합 사항 ID ${issue.id} 업데이트 실패: ${error.message}`, 'text-red-600');
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 결과 표시
|
||||
if (successCount > 0) {
|
||||
addStatus(`✅ ${successCount}개 부적합 사항 업데이트 완료!`, 'text-green-600 font-bold');
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
addStatus(`❌ ${errorCount}개 부적합 사항 업데이트 실패`, 'text-red-600');
|
||||
}
|
||||
|
||||
// 6. 완료 처리
|
||||
if (errorCount === 0) {
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>모든 업데이트 완료';
|
||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
||||
|
||||
addStatus('🎉 모든 API 데이터 수정이 완료되었습니다!', 'text-green-600 font-bold');
|
||||
addStatus('이제 부적합 조회에서 프로젝트 필터가 정상 작동합니다.', 'text-blue-600');
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
||||
btn.className = 'w-full bg-orange-500 text-white py-3 px-4 rounded-lg hover:bg-orange-600 transition-colors font-medium';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
134
frontend/fix-project-id.html
Normal file
134
frontend/fix-project-id.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>프로젝트 ID 수정 - M Project</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-edit text-blue-500 mr-2"></i>프로젝트 ID 수정
|
||||
</h1>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<h3 class="font-semibold text-blue-800 mb-2">📋 작업 내용</h3>
|
||||
<p class="text-blue-700 text-sm">
|
||||
큰 타임스탬프 ID를 작은 정수 ID로 변경합니다.<br>
|
||||
TKR-25009R 프로젝트 ID: <code>1761264279704</code> → <code>1</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="status" class="space-y-2">
|
||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="fixBtn"
|
||||
onclick="fixProjectId()"
|
||||
class="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
|
||||
>
|
||||
<i class="fas fa-tools mr-2"></i>프로젝트 ID 수정 시작
|
||||
</button>
|
||||
|
||||
<a href="fix-api-data.html" class="block text-center text-orange-600 hover:text-orange-800 mt-4">
|
||||
<i class="fas fa-wrench mr-1"></i>API 데이터 수정 도구로 이동
|
||||
</a>
|
||||
|
||||
<a href="debug-data.html" class="block text-center text-blue-600 hover:text-blue-800 mt-2">
|
||||
<i class="fas fa-bug mr-1"></i>디버그 도구로 이동
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addStatus(message, className = 'text-gray-600') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
const p = document.createElement('p');
|
||||
p.className = `text-sm ${className}`;
|
||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
||||
statusDiv.appendChild(p);
|
||||
}
|
||||
|
||||
function fixProjectId() {
|
||||
const btn = document.getElementById('fixBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>수정 중...';
|
||||
|
||||
try {
|
||||
// 1. 프로젝트 데이터 수정
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
||||
|
||||
if (!mProject) {
|
||||
throw new Error('TKR-25009R 프로젝트를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const oldId = mProject.id;
|
||||
const newId = 1;
|
||||
|
||||
addStatus(`기존 프로젝트 ID: ${oldId}`, 'text-blue-600');
|
||||
addStatus(`새로운 프로젝트 ID: ${newId}`, 'text-blue-600');
|
||||
|
||||
// 프로젝트 ID 변경
|
||||
mProject.id = newId;
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
addStatus('✅ 프로젝트 ID 변경 완료', 'text-green-600');
|
||||
|
||||
// 2. 일일 공수 데이터 수정
|
||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
||||
let updatedWorkDays = 0;
|
||||
|
||||
dailyWorkData.forEach(dayData => {
|
||||
if (dayData.projects) {
|
||||
dayData.projects.forEach(project => {
|
||||
if (project.projectId == oldId) {
|
||||
project.projectId = newId;
|
||||
project.projectName = 'TKR-25009R - M Project';
|
||||
updatedWorkDays++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
|
||||
addStatus(`✅ ${updatedWorkDays}개 일일 공수 데이터 업데이트 완료`, 'text-green-600');
|
||||
|
||||
// 3. localStorage 부적합 사항 수정
|
||||
const localIssues = JSON.parse(localStorage.getItem('work-report-issues') || '[]');
|
||||
let updatedIssues = 0;
|
||||
|
||||
localIssues.forEach(issue => {
|
||||
if (issue.projectId == oldId || issue.project_id == oldId) {
|
||||
issue.projectId = newId;
|
||||
issue.project_id = newId;
|
||||
issue.projectName = 'TKR-25009R - M Project';
|
||||
issue.project_name = 'TKR-25009R - M Project';
|
||||
updatedIssues++;
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('work-report-issues', JSON.stringify(localIssues));
|
||||
addStatus(`✅ ${updatedIssues}개 localStorage 부적합 사항 업데이트 완료`, 'text-green-600');
|
||||
|
||||
// 완료
|
||||
addStatus('🎉 모든 데이터 ID 수정 완료!', 'text-green-600 font-bold');
|
||||
addStatus('이제 API 데이터 수정 도구를 실행하세요.', 'text-blue-600');
|
||||
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>완료';
|
||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
||||
|
||||
} catch (error) {
|
||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -191,6 +191,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<!-- 네비게이션 -->
|
||||
<nav class="bg-white border-b">
|
||||
<div class="container mx-auto px-4">
|
||||
@@ -210,6 +211,9 @@
|
||||
<button class="nav-link" onclick="showSection('summary')" style="display:none;" id="summaryBtn">
|
||||
<i class="fas fa-chart-bar mr-2"></i>보고서
|
||||
</button>
|
||||
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
||||
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
||||
</a>
|
||||
<a href="admin.html" class="nav-link" style="display:none;" id="adminBtn">
|
||||
<i class="fas fa-users-cog mr-2"></i>관리
|
||||
</a>
|
||||
@@ -284,6 +288,17 @@
|
||||
<input type="file" id="galleryInput" accept="image/*" class="hidden" multiple>
|
||||
</div>
|
||||
|
||||
<!-- 프로젝트 선택 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-folder-open mr-1"></i>프로젝트
|
||||
</label>
|
||||
<select id="projectSelect" class="input-field w-full px-4 py-2 rounded-lg" required>
|
||||
<option value="">프로젝트를 선택하세요</option>
|
||||
<!-- 활성 프로젝트들이 여기에 로드됩니다 -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 카테고리 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">카테고리</label>
|
||||
@@ -318,7 +333,56 @@
|
||||
<!-- 목록 관리 섹션 -->
|
||||
<section id="listSection" class="hidden container mx-auto px-4 py-6">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">부적합 사항 목록</h2>
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-800">부적합 사항 목록</h2>
|
||||
<button onclick="displayIssueList()" class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
<i class="fas fa-refresh mr-1"></i>새로고침
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 필터 섹션 -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<!-- 프로젝트 필터 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
|
||||
<select id="listProjectFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="displayIssueList()">
|
||||
<option value="">전체 프로젝트</option>
|
||||
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 검토 상태 필터 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">검토 상태</label>
|
||||
<select id="reviewStatusFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="displayIssueList()">
|
||||
<option value="">전체</option>
|
||||
<option value="pending">검토 필요</option>
|
||||
<option value="completed">검토 완료</option>
|
||||
</select>
|
||||
</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>
|
||||
<option value="today">오늘</option>
|
||||
<option value="week">이번 주</option>
|
||||
<option value="month">이번 달</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 정의 날짜 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">특정 날짜</label>
|
||||
<input type="date" id="customDateFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="displayIssueList()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="issueList" class="space-y-4">
|
||||
<!-- 목록이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
@@ -330,9 +394,19 @@
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">작업 보고서</h2>
|
||||
<button onclick="printReport()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||
<i class="fas fa-print mr-2"></i>인쇄
|
||||
</button>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 프로젝트 선택 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm font-medium text-gray-700">프로젝트:</label>
|
||||
<select id="reportProjectFilter" class="px-3 py-2 border border-gray-300 rounded text-sm" onchange="generateReport()">
|
||||
<option value="">전체 프로젝트</option>
|
||||
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reportContent">
|
||||
<!-- 보고서 내용이 여기에 표시됩니다 -->
|
||||
@@ -340,7 +414,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/static/js/api.js?v=20250917"></script>
|
||||
<script src="/static/js/image-utils.js?v=20250917"></script>
|
||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||
@@ -361,6 +435,9 @@
|
||||
// 권한에 따른 메뉴 표시/숨김
|
||||
updateNavigation();
|
||||
|
||||
// 프로젝트 로드
|
||||
loadProjects();
|
||||
|
||||
loadIssues();
|
||||
|
||||
// URL 해시 처리
|
||||
@@ -404,16 +481,20 @@
|
||||
const summaryBtn = document.getElementById('summaryBtn');
|
||||
const adminBtn = document.getElementById('adminBtn');
|
||||
|
||||
const projectBtn = document.getElementById('projectBtn');
|
||||
|
||||
if (currentUser.role === 'admin') {
|
||||
// 관리자는 모든 메뉴 표시
|
||||
listBtn.style.display = '';
|
||||
summaryBtn.style.display = '';
|
||||
projectBtn.style.display = '';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
||||
} else {
|
||||
// 일반 사용자는 제한된 메뉴만 표시
|
||||
listBtn.style.display = 'none';
|
||||
summaryBtn.style.display = 'none';
|
||||
projectBtn.style.display = 'none';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-key mr-2"></i>비밀번호 변경';
|
||||
}
|
||||
@@ -664,6 +745,12 @@
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
const originalBtnContent = submitBtn.innerHTML;
|
||||
const description = document.getElementById('description').value.trim();
|
||||
const projectId = document.getElementById('projectSelect').value;
|
||||
|
||||
if (!projectId) {
|
||||
alert('프로젝트를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
alert('설명을 입력해주세요.');
|
||||
@@ -714,10 +801,14 @@
|
||||
updateLoadingMessage('서버로 전송 중...', '네트워크 상태에 따라 시간이 걸릴 수 있습니다');
|
||||
updateProgress(60);
|
||||
|
||||
// 선택된 프로젝트 정보 가져오기
|
||||
const selectedProject = getSelectedProject(projectId);
|
||||
|
||||
const issueData = {
|
||||
photos: currentPhotos, // 배열로 전달
|
||||
category: document.getElementById('category').value,
|
||||
description: description
|
||||
description: description,
|
||||
project_id: parseInt(projectId)
|
||||
};
|
||||
|
||||
const startTime = Date.now();
|
||||
@@ -781,22 +872,203 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 로드
|
||||
function loadProjects() {
|
||||
const saved = localStorage.getItem('work-report-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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 프로젝트 정보 가져오기
|
||||
function getSelectedProject(projectId) {
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
const projects = JSON.parse(saved);
|
||||
return projects.find(p => p.id == projectId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 프로젝트 정보 표시용 함수
|
||||
function getProjectInfo(projectId) {
|
||||
if (!projectId) {
|
||||
return '<span class="text-gray-500">프로젝트 미지정</span>';
|
||||
}
|
||||
|
||||
const project = getSelectedProject(projectId);
|
||||
if (project) {
|
||||
return `${project.jobNo} - ${project.projectName}`;
|
||||
} else {
|
||||
return `<span class="text-red-500">프로젝트 ID: ${projectId} (정보 없음)</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// LocalStorage 관련 함수는 더 이상 사용하지 않음
|
||||
function saveIssues() {
|
||||
// Deprecated - API 사용
|
||||
}
|
||||
|
||||
// 검토 상태 확인 함수
|
||||
function isReviewCompleted(issue) {
|
||||
return issue.status === 'complete' && issue.work_hours && issue.work_hours > 0;
|
||||
}
|
||||
|
||||
// 날짜 필터링 함수
|
||||
function filterByDate(issues, dateFilter, customDate) {
|
||||
if (customDate) {
|
||||
const targetDate = new Date(customDate);
|
||||
return issues.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate.toDateString() === targetDate.toDateString();
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
switch (dateFilter) {
|
||||
case 'today':
|
||||
return issues.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate >= today;
|
||||
});
|
||||
case 'week':
|
||||
const weekStart = new Date(today);
|
||||
weekStart.setDate(today.getDate() - today.getDay());
|
||||
return issues.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate >= weekStart;
|
||||
});
|
||||
case 'month':
|
||||
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
return issues.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate >= monthStart;
|
||||
});
|
||||
default:
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
// 목록 표시
|
||||
function displayIssueList() {
|
||||
const container = document.getElementById('issueList');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (issues.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 부적합 사항이 없습니다.</p>';
|
||||
// 필터 값 가져오기
|
||||
const selectedProjectId = document.getElementById('listProjectFilter').value;
|
||||
const reviewStatusFilter = document.getElementById('reviewStatusFilter').value;
|
||||
const dateFilter = document.getElementById('dateFilter').value;
|
||||
const customDate = document.getElementById('customDateFilter').value;
|
||||
|
||||
let filteredIssues = [...issues];
|
||||
|
||||
// 프로젝트 필터 적용
|
||||
if (selectedProjectId) {
|
||||
filteredIssues = filteredIssues.filter(issue => {
|
||||
const issueProjectId = issue.project_id || issue.projectId;
|
||||
return issueProjectId && (issueProjectId == selectedProjectId || issueProjectId.toString() === selectedProjectId.toString());
|
||||
});
|
||||
}
|
||||
|
||||
// 검토 상태 필터 적용
|
||||
if (reviewStatusFilter) {
|
||||
filteredIssues = filteredIssues.filter(issue => {
|
||||
const isCompleted = isReviewCompleted(issue);
|
||||
return reviewStatusFilter === 'completed' ? isCompleted : !isCompleted;
|
||||
});
|
||||
}
|
||||
|
||||
// 날짜 필터 적용
|
||||
if (dateFilter || customDate) {
|
||||
filteredIssues = filterByDate(filteredIssues, dateFilter, customDate);
|
||||
}
|
||||
|
||||
if (filteredIssues.length === 0) {
|
||||
container.innerHTML = `<p class="text-gray-500 text-center py-8">조건에 맞는 부적합 사항이 없습니다.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
issues.forEach(issue => {
|
||||
// 검토 상태별로 분류 및 정렬
|
||||
const pendingIssues = filteredIssues.filter(issue => !isReviewCompleted(issue));
|
||||
const completedIssues = filteredIssues.filter(issue => isReviewCompleted(issue));
|
||||
|
||||
// 검토 필요 항목을 먼저 표시
|
||||
if (pendingIssues.length > 0) {
|
||||
const pendingHeader = document.createElement('div');
|
||||
pendingHeader.className = 'mb-4';
|
||||
pendingHeader.innerHTML = `
|
||||
<h3 class="text-md font-semibold text-orange-700 flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>검토 필요 (${pendingIssues.length}건)
|
||||
</h3>
|
||||
`;
|
||||
container.appendChild(pendingHeader);
|
||||
|
||||
pendingIssues.forEach(issue => {
|
||||
container.appendChild(createIssueCard(issue, false));
|
||||
});
|
||||
}
|
||||
|
||||
// 검토 완료 항목을 아래에 표시
|
||||
if (completedIssues.length > 0) {
|
||||
const completedHeader = document.createElement('div');
|
||||
completedHeader.className = 'mb-4 mt-8';
|
||||
completedHeader.innerHTML = `
|
||||
<h3 class="text-md font-semibold text-green-700 flex items-center">
|
||||
<i class="fas fa-check-circle mr-2"></i>검토 완료 (${completedIssues.length}건)
|
||||
</h3>
|
||||
`;
|
||||
container.appendChild(completedHeader);
|
||||
|
||||
completedIssues.forEach(issue => {
|
||||
container.appendChild(createIssueCard(issue, true));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 부적합 사항 카드 생성 함수
|
||||
function createIssueCard(issue, isCompleted) {
|
||||
const categoryNames = {
|
||||
material_missing: '자재누락',
|
||||
design_error: '설계미스',
|
||||
@@ -805,10 +1077,31 @@
|
||||
};
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border rounded-lg p-6 bg-gray-50';
|
||||
// 검토 완료 상태에 따른 스타일링
|
||||
const baseClasses = 'border rounded-lg p-6 transition-all duration-200';
|
||||
const statusClasses = isCompleted
|
||||
? 'bg-gray-100 opacity-75 border-gray-300'
|
||||
: 'bg-gray-50 border-gray-200 hover:shadow-md';
|
||||
div.className = `${baseClasses} ${statusClasses}`;
|
||||
div.id = `issue-card-${issue.id}`;
|
||||
// 프로젝트 정보 가져오기
|
||||
const projectInfo = getProjectInfo(issue.project_id || issue.projectId);
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<!-- 프로젝트 정보 및 상태 (오른쪽 상단) -->
|
||||
<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>' :
|
||||
'<div class="px-2 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium"><i class="fas fa-exclamation-triangle mr-1"></i>검토필요</div>'
|
||||
}
|
||||
</div>
|
||||
<div class="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<i class="fas fa-folder-open mr-1"></i>${projectInfo}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사진과 기본 정보 -->
|
||||
<div class="flex gap-4">
|
||||
<!-- 사진들 표시 -->
|
||||
@@ -896,15 +1189,19 @@
|
||||
id="workHours-confirm-${issue.id}"
|
||||
onclick="confirmWorkHours(${issue.id})"
|
||||
class="px-3 py-1 rounded transition-colors text-sm font-medium ${
|
||||
issue.work_hours
|
||||
? 'bg-green-100 text-green-700 cursor-default'
|
||||
: 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
isCompleted
|
||||
? 'bg-green-500 text-white cursor-default'
|
||||
: issue.work_hours
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}"
|
||||
${issue.work_hours ? 'disabled' : ''}
|
||||
${isCompleted || !issue.work_hours ? 'disabled' : ''}
|
||||
>
|
||||
${issue.work_hours
|
||||
? '<i class="fas fa-check-circle mr-1"></i>완료'
|
||||
: '<i class="fas fa-clock mr-1"></i>확인'
|
||||
${isCompleted
|
||||
? '<i class="fas fa-check-circle mr-1"></i>검토완료'
|
||||
: issue.work_hours
|
||||
? '<i class="fas fa-clock mr-1"></i>확인'
|
||||
: '<i class="fas fa-clock mr-1"></i>확인'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -950,8 +1247,8 @@
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
// 수정 상태 표시
|
||||
@@ -1013,17 +1310,25 @@
|
||||
}
|
||||
|
||||
try {
|
||||
// 작업 시간만 업데이트
|
||||
// 작업 시간 업데이트 및 검토 완료 상태로 변경
|
||||
await IssuesAPI.update(issueId, {
|
||||
work_hours: workHours
|
||||
work_hours: workHours,
|
||||
status: 'complete' // 검토 완료 상태로 변경
|
||||
});
|
||||
|
||||
// 성공 시 데이터 다시 로드
|
||||
await loadIssues();
|
||||
// 로컬 데이터도 업데이트
|
||||
const issue = issues.find(i => i.id === issueId);
|
||||
if (issue) {
|
||||
issue.work_hours = workHours;
|
||||
issue.status = 'complete';
|
||||
issue.reviewed_at = new Date().toISOString(); // 검토 완료 시간 기록
|
||||
}
|
||||
|
||||
// 성공 시 목록 다시 표시
|
||||
displayIssueList();
|
||||
|
||||
// 성공 메시지
|
||||
showToastMessage(`${workHours}시간 저장 완료!`, 'success');
|
||||
showToastMessage(`${workHours}시간 저장 및 검토 완료!`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
alert(error.message || '저장에 실패했습니다.');
|
||||
@@ -1151,39 +1456,81 @@
|
||||
async function generateReport() {
|
||||
const container = document.getElementById('reportContent');
|
||||
|
||||
// 선택된 프로젝트 가져오기
|
||||
const selectedProjectId = document.getElementById('reportProjectFilter').value;
|
||||
|
||||
// 프로젝트별 필터링된 부적합 사항
|
||||
let filteredIssues = issues;
|
||||
if (selectedProjectId) {
|
||||
filteredIssues = issues.filter(issue => {
|
||||
const issueProjectId = issue.project_id || issue.projectId;
|
||||
return issueProjectId && (issueProjectId == selectedProjectId || issueProjectId.toString() === selectedProjectId.toString());
|
||||
});
|
||||
}
|
||||
|
||||
// 날짜 범위 계산
|
||||
const dates = issues.map(i => new Date(i.report_date));
|
||||
const dates = filteredIssues.map(i => new Date(i.report_date));
|
||||
const startDate = dates.length > 0 ? new Date(Math.min(...dates)) : new Date();
|
||||
const endDate = new Date();
|
||||
|
||||
// 일일 공수 데이터 가져오기
|
||||
// 프로젝트별 일일 공수 데이터 계산
|
||||
let dailyWorkTotal = 0;
|
||||
try {
|
||||
const dailyWorks = await DailyWorkAPI.getAll({
|
||||
start_date: startDate.toISOString().split('T')[0],
|
||||
end_date: endDate.toISOString().split('T')[0]
|
||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
||||
|
||||
console.log('일일공수 데이터:', dailyWorkData);
|
||||
console.log('선택된 프로젝트 ID:', selectedProjectId);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 전체 프로젝트의 일일 공수 합계
|
||||
dailyWorkData.forEach(dayWork => {
|
||||
console.log('전체 일일공수 항목:', dayWork);
|
||||
dailyWorkTotal += dayWork.totalHours || 0;
|
||||
});
|
||||
dailyWorkTotal = dailyWorks.reduce((sum, work) => sum + work.total_hours, 0);
|
||||
} catch (error) {
|
||||
console.error('일일 공수 데이터 로드 실패:', error);
|
||||
}
|
||||
|
||||
// 부적합 사항 해결 시간 계산
|
||||
const issueHours = issues.reduce((sum, issue) => sum + issue.work_hours, 0);
|
||||
console.log('최종 일일공수 합계:', dailyWorkTotal);
|
||||
|
||||
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
|
||||
const issueHours = filteredIssues.reduce((sum, issue) => sum + (issue.work_hours || 0), 0);
|
||||
const categoryCount = {};
|
||||
|
||||
issues.forEach(issue => {
|
||||
filteredIssues.forEach(issue => {
|
||||
categoryCount[issue.category] = (categoryCount[issue.category] || 0) + 1;
|
||||
});
|
||||
|
||||
// 부적합 시간 비율 계산
|
||||
const issuePercentage = dailyWorkTotal > 0 ? ((issueHours / dailyWorkTotal) * 100).toFixed(1) : 0;
|
||||
|
||||
// 선택된 프로젝트 정보
|
||||
let projectInfo = '전체 프로젝트';
|
||||
if (selectedProjectId) {
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
const selectedProject = projects.find(p => p.id == selectedProjectId);
|
||||
if (selectedProject) {
|
||||
projectInfo = `${selectedProject.jobNo} - ${selectedProject.projectName}`;
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<!-- 요약 페이지 -->
|
||||
<div class="border-b pb-6">
|
||||
<h3 class="text-2xl font-bold text-center mb-6">작업 보고서</h3>
|
||||
<h3 class="text-2xl font-bold text-center mb-2">작업 보고서</h3>
|
||||
<p class="text-center text-gray-600 mb-6">${projectInfo}</p>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
@@ -1254,7 +1601,7 @@
|
||||
<!-- 상세 내역 -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-700 mb-4">부적합 사항 상세</h4>
|
||||
${issues.map(issue => {
|
||||
${filteredIssues.map(issue => {
|
||||
const categoryNames = {
|
||||
material_missing: '자재누락',
|
||||
design_error: '설계미스',
|
||||
|
||||
@@ -45,41 +45,72 @@
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #6b7280;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: #f3f4f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="glass-effect border-b border-gray-200 sticky top-0 z-50">
|
||||
<!-- 헤더 -->
|
||||
<header class="bg-white shadow-sm sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-gray-800">
|
||||
<i class="fas fa-clipboard-list mr-2"></i>작업보고서 시스템
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-xl font-bold text-gray-800">
|
||||
<i class="fas fa-clipboard-check text-blue-500 mr-2"></i>작업보고서
|
||||
</h1>
|
||||
<div id="navContainer" class="flex items-center gap-4">
|
||||
<a href="/daily-work.html" class="text-gray-600 hover:text-gray-800 transition-colors">
|
||||
<i class="fas fa-calendar-day mr-1"></i>일일 공수
|
||||
</a>
|
||||
<a href="/index.html" class="text-gray-600 hover:text-gray-800 transition-colors">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>부적합 등록
|
||||
</a>
|
||||
<a href="/issue-view.html" class="text-blue-600 font-medium">
|
||||
<i class="fas fa-search mr-1"></i>부적합 조회
|
||||
</a>
|
||||
<a href="/index.html#list" class="text-gray-600 hover:text-gray-800 transition-colors" style="display:none;" id="listBtn">
|
||||
<i class="fas fa-list mr-1"></i>목록 관리
|
||||
</a>
|
||||
<a href="/index.html#summary" class="text-gray-600 hover:text-gray-800 transition-colors" style="display:none;" id="summaryBtn">
|
||||
<i class="fas fa-chart-bar mr-1"></i>보고서
|
||||
</a>
|
||||
<a href="/admin.html" class="text-gray-600 hover:text-gray-800 transition-colors" style="display:none;" id="adminBtn">
|
||||
<i class="fas fa-users-cog mr-1"></i>관리
|
||||
</a>
|
||||
<button onclick="AuthAPI.logout()" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>로그아웃
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-gray-600" id="userDisplay"></span>
|
||||
<button onclick="logout()" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<!-- 네비게이션 -->
|
||||
<nav class="bg-white border-b">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex gap-2 py-2 overflow-x-auto">
|
||||
<a href="daily-work.html" class="nav-link">
|
||||
<i class="fas fa-calendar-check mr-2"></i>일일 공수
|
||||
</a>
|
||||
<a href="index.html" class="nav-link">
|
||||
<i class="fas fa-camera-retro mr-2"></i>부적합 등록
|
||||
</a>
|
||||
<a href="issue-view.html" class="nav-link active">
|
||||
<i class="fas fa-search mr-2"></i>부적합 조회
|
||||
</a>
|
||||
<a href="index.html#list" class="nav-link" style="display:none;" id="listBtn">
|
||||
<i class="fas fa-list mr-2"></i>목록 관리
|
||||
</a>
|
||||
<a href="index.html#summary" class="nav-link" style="display:none;" id="summaryBtn">
|
||||
<i class="fas fa-chart-bar mr-2"></i>보고서
|
||||
</a>
|
||||
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
||||
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
||||
</a>
|
||||
<a href="admin.html" class="nav-link" style="display:none;" id="adminBtn">
|
||||
<i class="fas fa-users-cog mr-2"></i>사용자 관리
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
@@ -91,6 +122,41 @@
|
||||
<i class="fas fa-list-alt text-blue-500 mr-2"></i>부적합 사항 목록
|
||||
</h2>
|
||||
|
||||
<!-- 필터 섹션 -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- 프로젝트 필터 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
|
||||
<select id="projectFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="filterIssues()">
|
||||
<option value="">전체 프로젝트</option>
|
||||
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 검토 상태 필터 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">검토 상태</label>
|
||||
<select id="reviewStatusFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="filterIssues()">
|
||||
<option value="">전체</option>
|
||||
<option value="pending">검토 필요</option>
|
||||
<option value="completed">검토 완료</option>
|
||||
</select>
|
||||
</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="filterIssues()">
|
||||
<option value="">전체</option>
|
||||
<option value="today">오늘</option>
|
||||
<option value="week">이번 주</option>
|
||||
<option value="month">이번 달</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<button onclick="setDateRange('today')" class="px-3 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors text-sm">
|
||||
@@ -140,6 +206,9 @@
|
||||
// 네비게이션 권한 체크
|
||||
updateNavigation();
|
||||
|
||||
// 프로젝트 로드
|
||||
loadProjects();
|
||||
|
||||
// 기본값: 이번 주 데이터 로드
|
||||
setDateRange('week');
|
||||
});
|
||||
@@ -173,22 +242,138 @@
|
||||
const listBtn = document.getElementById('listBtn');
|
||||
const summaryBtn = document.getElementById('summaryBtn');
|
||||
const adminBtn = document.getElementById('adminBtn');
|
||||
const projectBtn = document.getElementById('projectBtn');
|
||||
|
||||
if (currentUser.role === 'admin') {
|
||||
// 관리자는 모든 메뉴 표시
|
||||
listBtn.style.display = '';
|
||||
summaryBtn.style.display = '';
|
||||
projectBtn.style.display = '';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-1"></i>사용자 관리';
|
||||
} else {
|
||||
// 일반 사용자는 제한된 메뉴만 표시
|
||||
listBtn.style.display = 'none';
|
||||
summaryBtn.style.display = 'none';
|
||||
projectBtn.style.display = 'none';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-key mr-1"></i>비밀번호 변경';
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 로드
|
||||
function loadProjects() {
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
const projects = JSON.parse(saved);
|
||||
const projectFilter = document.getElementById('projectFilter');
|
||||
|
||||
// 기존 옵션 제거 (전체 프로젝트 옵션 제외)
|
||||
projectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
||||
|
||||
// 모든 프로젝트 추가 (활성/비활성 모두 - 기존 데이터 조회를 위해)
|
||||
projects.forEach(project => {
|
||||
const option = document.createElement('option');
|
||||
option.value = project.id;
|
||||
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
|
||||
projectFilter.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 이슈 필터링
|
||||
// 검토 상태 확인 함수
|
||||
function isReviewCompleted(issue) {
|
||||
return issue.status === 'complete' && issue.work_hours && issue.work_hours > 0;
|
||||
}
|
||||
|
||||
// 날짜 필터링 함수
|
||||
function filterByDate(issues, dateFilter) {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
switch (dateFilter) {
|
||||
case 'today':
|
||||
return issues.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate >= today;
|
||||
});
|
||||
case 'week':
|
||||
const weekStart = new Date(today);
|
||||
weekStart.setDate(today.getDate() - today.getDay());
|
||||
return issues.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate >= weekStart;
|
||||
});
|
||||
case 'month':
|
||||
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
return issues.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate >= monthStart;
|
||||
});
|
||||
default:
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
function filterIssues() {
|
||||
console.log('필터링 시작 - 전체 이슈:', issues.length);
|
||||
|
||||
// 필터 값 가져오기
|
||||
const selectedProjectId = document.getElementById('projectFilter').value;
|
||||
const reviewStatusFilter = document.getElementById('reviewStatusFilter').value;
|
||||
const dateFilter = document.getElementById('dateFilter').value;
|
||||
|
||||
let filteredIssues = [...issues];
|
||||
|
||||
// 프로젝트 필터 적용
|
||||
if (selectedProjectId) {
|
||||
filteredIssues = filteredIssues.filter(issue => {
|
||||
const issueProjectId = issue.project_id || issue.projectId;
|
||||
return issueProjectId && (issueProjectId == selectedProjectId || issueProjectId.toString() === selectedProjectId.toString());
|
||||
});
|
||||
console.log('프로젝트 필터 후:', filteredIssues.length);
|
||||
}
|
||||
|
||||
// 검토 상태 필터 적용
|
||||
if (reviewStatusFilter) {
|
||||
filteredIssues = filteredIssues.filter(issue => {
|
||||
const isCompleted = isReviewCompleted(issue);
|
||||
return reviewStatusFilter === 'completed' ? isCompleted : !isCompleted;
|
||||
});
|
||||
console.log('검토 상태 필터 후:', filteredIssues.length);
|
||||
}
|
||||
|
||||
// 날짜 필터 적용
|
||||
if (dateFilter) {
|
||||
filteredIssues = filterByDate(filteredIssues, dateFilter);
|
||||
console.log('날짜 필터 후:', filteredIssues.length);
|
||||
}
|
||||
|
||||
// 전역 변수에 필터링된 결과 저장
|
||||
window.filteredIssues = filteredIssues;
|
||||
|
||||
displayResults();
|
||||
}
|
||||
|
||||
// 프로젝트 정보 표시용 함수
|
||||
function getProjectInfo(projectId) {
|
||||
if (!projectId) {
|
||||
return '<span class="text-gray-500">프로젝트 미지정</span>';
|
||||
}
|
||||
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
const projects = JSON.parse(saved);
|
||||
const project = projects.find(p => p.id == projectId);
|
||||
if (project) {
|
||||
return `${project.jobNo} - ${project.projectName}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `<span class="text-red-500">프로젝트 ID: ${projectId} (정보 없음)</span>`;
|
||||
}
|
||||
|
||||
// 날짜 범위 설정 및 자동 조회
|
||||
async function setDateRange(range) {
|
||||
currentRange = range;
|
||||
@@ -274,16 +459,60 @@
|
||||
function displayResults() {
|
||||
const container = document.getElementById('issueResults');
|
||||
|
||||
if (issues.length === 0) {
|
||||
// 필터링된 결과 사용 (filterIssues에서 설정됨)
|
||||
const filteredIssues = window.filteredIssues || issues;
|
||||
|
||||
if (filteredIssues.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-gray-500 text-center py-8">
|
||||
<i class="fas fa-inbox text-4xl mb-3"></i>
|
||||
<p>등록된 부적합 사항이 없습니다.</p>
|
||||
<p>조건에 맞는 부적합 사항이 없습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 검토 상태별로 분류 및 정렬
|
||||
const pendingIssues = filteredIssues.filter(issue => !isReviewCompleted(issue));
|
||||
const completedIssues = filteredIssues.filter(issue => isReviewCompleted(issue));
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
// 검토 필요 항목을 먼저 표시
|
||||
if (pendingIssues.length > 0) {
|
||||
const pendingHeader = document.createElement('div');
|
||||
pendingHeader.className = 'mb-4';
|
||||
pendingHeader.innerHTML = `
|
||||
<h3 class="text-md font-semibold text-orange-700 flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>검토 필요 (${pendingIssues.length}건)
|
||||
</h3>
|
||||
`;
|
||||
container.appendChild(pendingHeader);
|
||||
|
||||
pendingIssues.forEach(issue => {
|
||||
container.appendChild(createIssueCard(issue, false));
|
||||
});
|
||||
}
|
||||
|
||||
// 검토 완료 항목을 아래에 표시
|
||||
if (completedIssues.length > 0) {
|
||||
const completedHeader = document.createElement('div');
|
||||
completedHeader.className = 'mb-4 mt-8';
|
||||
completedHeader.innerHTML = `
|
||||
<h3 class="text-md font-semibold text-green-700 flex items-center">
|
||||
<i class="fas fa-check-circle mr-2"></i>검토 완료 (${completedIssues.length}건)
|
||||
</h3>
|
||||
`;
|
||||
container.appendChild(completedHeader);
|
||||
|
||||
completedIssues.forEach(issue => {
|
||||
container.appendChild(createIssueCard(issue, true));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 부적합 사항 카드 생성 함수 (조회용)
|
||||
function createIssueCard(issue, isCompleted) {
|
||||
const categoryNames = {
|
||||
material_missing: '자재누락',
|
||||
design_error: '설계미스',
|
||||
@@ -298,72 +527,114 @@
|
||||
inspection_miss: 'bg-purple-100 text-purple-700 border-purple-300'
|
||||
};
|
||||
|
||||
// 시간순으로 나열
|
||||
container.innerHTML = issues.map(issue => {
|
||||
const dateStr = DateUtils.formatKST(issue.report_date, true);
|
||||
const relativeTime = DateUtils.getRelativeTime(issue.report_date);
|
||||
const div = document.createElement('div');
|
||||
// 검토 완료 상태에 따른 스타일링
|
||||
const baseClasses = 'rounded-lg transition-colors border-l-4 mb-4';
|
||||
const statusClasses = isCompleted
|
||||
? 'bg-gray-100 opacity-75'
|
||||
: 'bg-gray-50 hover:bg-gray-100';
|
||||
const borderColor = categoryColors[issue.category]?.split(' ')[2] || 'border-gray-300';
|
||||
div.className = `${baseClasses} ${statusClasses} ${borderColor}`;
|
||||
|
||||
const dateStr = DateUtils.formatKST(issue.report_date, true);
|
||||
const relativeTime = DateUtils.getRelativeTime(issue.report_date);
|
||||
const projectInfo = getProjectInfo(issue.project_id || issue.projectId);
|
||||
|
||||
div.innerHTML = `
|
||||
<!-- 프로젝트 정보 및 상태 (오른쪽 상단) -->
|
||||
<div class="flex justify-between items-start p-2 pb-0">
|
||||
<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>' :
|
||||
'<div class="px-2 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium"><i class="fas fa-exclamation-triangle mr-1"></i>검토필요</div>'
|
||||
}
|
||||
</div>
|
||||
<div class="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<i class="fas fa-folder-open mr-1"></i>${projectInfo}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return `
|
||||
<div class="flex gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors border-l-4 ${categoryColors[issue.category].split(' ')[2] || 'border-gray-300'}">
|
||||
<!-- 사진들 -->
|
||||
<div class="flex gap-1 flex-shrink-0">
|
||||
${issue.photo_path ?
|
||||
`<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded shadow-sm cursor-pointer" onclick="showImageModal('${issue.photo_path}')">` : ''
|
||||
}
|
||||
${issue.photo_path2 ?
|
||||
`<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded shadow-sm cursor-pointer" onclick="showImageModal('${issue.photo_path2}')">` : ''
|
||||
}
|
||||
${!issue.photo_path && !issue.photo_path2 ?
|
||||
`<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center">
|
||||
<i class="fas fa-image text-gray-400"></i>
|
||||
</div>` : ''
|
||||
<!-- 기존 내용 -->
|
||||
<div class="flex gap-3 p-3 pt-1">
|
||||
<!-- 사진들 -->
|
||||
<div class="flex gap-1 flex-shrink-0">
|
||||
${issue.photo_path ?
|
||||
`<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded shadow-sm cursor-pointer" onclick="showImageModal('${issue.photo_path}')">` : ''
|
||||
}
|
||||
${issue.photo_path2 ?
|
||||
`<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded shadow-sm cursor-pointer" onclick="showImageModal('${issue.photo_path2}')">` : ''
|
||||
}
|
||||
${!issue.photo_path && !issue.photo_path2 ?
|
||||
`<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center">
|
||||
<i class="fas fa-image text-gray-400"></i>
|
||||
</div>` : ''
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<span class="px-2 py-1 rounded-full text-xs font-medium ${categoryColors[issue.category] || 'bg-gray-100 text-gray-700'}">
|
||||
${categoryNames[issue.category] || issue.category}
|
||||
</span>
|
||||
${issue.work_hours ?
|
||||
`<span class="text-sm text-green-600 font-medium">
|
||||
<i class="fas fa-clock mr-1"></i>${issue.work_hours}시간
|
||||
</span>` :
|
||||
'<span class="text-sm text-gray-400">시간 미입력</span>'
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium ${categoryColors[issue.category].split(' ').slice(0, 2).join(' ')}">
|
||||
${categoryNames[issue.category]}
|
||||
</span>
|
||||
${issue.work_hours ? `
|
||||
<span class="text-xs text-green-600 font-medium">
|
||||
✓ ${issue.work_hours}시간
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
${dateStr} (${relativeTime})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-800 line-clamp-2">${issue.description}</p>
|
||||
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
<i class="fas fa-user mr-1"></i>
|
||||
${issue.reporter.full_name || issue.reporter.username}
|
||||
</div>
|
||||
<p class="text-gray-800 mb-2 line-clamp-2">${issue.description}</p>
|
||||
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span><i class="fas fa-user mr-1"></i>${issue.reporter?.full_name || issue.reporter?.username || '알 수 없음'}</span>
|
||||
<span><i class="fas fa-calendar mr-1"></i>${dateStr}</span>
|
||||
<span class="text-xs text-gray-400">${relativeTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 상단에 요약 추가
|
||||
const summary = `
|
||||
<div class="mb-4 p-3 bg-blue-50 rounded-lg text-sm">
|
||||
<span class="font-medium text-blue-900">총 ${issues.length}건</span>
|
||||
<span class="text-blue-700 ml-3">
|
||||
자재누락: ${issues.filter(i => i.category === 'material_missing').length}건 |
|
||||
설계미스: ${issues.filter(i => i.category === 'design_error').length}건 |
|
||||
입고자재 불량: ${issues.filter(i => i.category === 'incoming_defect').length}건 |
|
||||
검사미스: ${issues.filter(i => i.category === 'inspection_miss').length}건
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = summary + container.innerHTML;
|
||||
return div;
|
||||
}
|
||||
|
||||
// 로그아웃 함수
|
||||
function logout() {
|
||||
localStorage.removeItem('currentUser');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// 페이지 로드 시 사용자 정보 확인 및 관리자 배너 표시
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const currentUserData = localStorage.getItem('currentUser');
|
||||
if (!currentUserData) {
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
let currentUser;
|
||||
try {
|
||||
// JSON 파싱 시도
|
||||
currentUser = JSON.parse(currentUserData);
|
||||
} catch (e) {
|
||||
// JSON이 아니면 문자열로 처리 (기존 방식 호환)
|
||||
currentUser = { username: currentUserData };
|
||||
}
|
||||
|
||||
// 사용자 표시
|
||||
const username = currentUser.username || currentUser;
|
||||
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
||||
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
||||
|
||||
// 관리자인 경우 메뉴 표시
|
||||
if (username === 'hyungi' || currentUser.role === 'admin') {
|
||||
document.getElementById('listBtn').style.display = '';
|
||||
document.getElementById('summaryBtn').style.display = '';
|
||||
document.getElementById('projectBtn').style.display = '';
|
||||
document.getElementById('adminBtn').style.display = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
307
frontend/migrate-data.html
Normal file
307
frontend/migrate-data.html
Normal file
@@ -0,0 +1,307 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>데이터 마이그레이션 - M Project</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="/static/js/api.js?v=20250917"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-database text-blue-500 mr-2"></i>데이터 마이그레이션
|
||||
</h1>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<h3 class="font-semibold text-yellow-800 mb-2">⚠️ 주의사항</h3>
|
||||
<p class="text-yellow-700 text-sm">
|
||||
이 작업은 기존 데이터를 "M Project"로 마이그레이션합니다.<br>
|
||||
관리자(hyungi) 계정으로 로그인한 상태에서만 실행하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="status" class="space-y-2">
|
||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="migrateBtn"
|
||||
onclick="startMigration()"
|
||||
class="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
|
||||
>
|
||||
<i class="fas fa-play mr-2"></i>마이그레이션 시작
|
||||
</button>
|
||||
|
||||
<a href="index.html" class="block text-center text-gray-600 hover:text-gray-800 mt-4">
|
||||
<i class="fas fa-arrow-left mr-1"></i>메인으로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUser = null;
|
||||
|
||||
// 페이지 로드 시 사용자 확인
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const userData = localStorage.getItem('currentUser');
|
||||
if (!userData) {
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentUser = JSON.parse(userData);
|
||||
} catch (e) {
|
||||
currentUser = { username: userData };
|
||||
}
|
||||
|
||||
const username = currentUser.username || currentUser;
|
||||
if (username !== 'hyungi' && currentUser.role !== 'admin') {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
addStatus('✅ 관리자 권한 확인됨', 'text-green-600');
|
||||
});
|
||||
|
||||
function addStatus(message, className = 'text-gray-600') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
const p = document.createElement('p');
|
||||
p.className = `text-sm ${className}`;
|
||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
||||
statusDiv.appendChild(p);
|
||||
}
|
||||
|
||||
async function startMigration() {
|
||||
const btn = document.getElementById('migrateBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>마이그레이션 중...';
|
||||
|
||||
try {
|
||||
// 1. M Project 생성
|
||||
await createMProject();
|
||||
|
||||
// 2. 기존 부적합 사항 마이그레이션
|
||||
await migrateIssues();
|
||||
|
||||
// 3. 기존 368시간 데이터 생성
|
||||
await createSampleHours();
|
||||
|
||||
addStatus('🎉 모든 마이그레이션이 완료되었습니다!', 'text-green-600 font-bold');
|
||||
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>완료';
|
||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
||||
|
||||
} catch (error) {
|
||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
||||
}
|
||||
}
|
||||
|
||||
async function createMProject() {
|
||||
addStatus('기존 M Project 확인 중...');
|
||||
|
||||
// 기존 프로젝트 확인
|
||||
let projects = [];
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
}
|
||||
|
||||
// 기존 TKR-25009R M Project 찾기
|
||||
let existingMProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
||||
if (existingMProject) {
|
||||
addStatus('✅ 기존 TKR-25009R M Project 발견', 'text-green-600');
|
||||
return existingMProject.id;
|
||||
}
|
||||
|
||||
// M-2024-001로 생성된 잘못된 프로젝트 찾기
|
||||
const wrongProject = projects.find(p => p.jobNo === 'M-2024-001');
|
||||
if (wrongProject) {
|
||||
// 잘못된 프로젝트를 올바른 Job No.로 수정
|
||||
wrongProject.jobNo = 'TKR-25009R';
|
||||
wrongProject.projectName = 'M Project';
|
||||
addStatus('✅ 기존 프로젝트를 TKR-25009R로 수정 완료', 'text-green-600');
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
return wrongProject.id;
|
||||
}
|
||||
|
||||
// 새로 생성 (기존 프로젝트가 없는 경우)
|
||||
const mProject = {
|
||||
id: Date.now(),
|
||||
jobNo: 'TKR-25009R',
|
||||
projectName: 'M Project',
|
||||
createdBy: 'hyungi',
|
||||
createdByName: '관리자',
|
||||
createdAt: new Date().toISOString(),
|
||||
isActive: true
|
||||
};
|
||||
|
||||
projects.push(mProject);
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
|
||||
addStatus('✅ TKR-25009R M Project 생성 완료', 'text-green-600');
|
||||
return mProject.id;
|
||||
}
|
||||
|
||||
async function migrateIssues() {
|
||||
addStatus('기존 부적합 사항 마이그레이션 중...');
|
||||
|
||||
// 기존 부적합 사항 로드 (localStorage와 API 모두 확인)
|
||||
let issues = [];
|
||||
|
||||
// 먼저 localStorage에서 확인
|
||||
const savedLocal = localStorage.getItem('work-report-issues');
|
||||
if (savedLocal) {
|
||||
issues = JSON.parse(savedLocal);
|
||||
addStatus('localStorage에서 부적합 사항 발견', 'text-blue-600');
|
||||
}
|
||||
|
||||
// API에서도 확인 (현재 시스템이 API 기반이므로)
|
||||
try {
|
||||
if (typeof IssuesAPI !== 'undefined') {
|
||||
const apiIssues = await IssuesAPI.getAll();
|
||||
if (apiIssues && apiIssues.length > 0) {
|
||||
issues = apiIssues;
|
||||
addStatus('API에서 부적합 사항 발견', 'text-blue-600');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
addStatus('API 조회 실패, localStorage 데이터 사용', 'text-yellow-600');
|
||||
}
|
||||
|
||||
if (issues.length === 0) {
|
||||
addStatus('마이그레이션할 부적합 사항이 없습니다.', 'text-yellow-600');
|
||||
return;
|
||||
}
|
||||
|
||||
// TKR-25009R M Project ID 가져오기
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
||||
|
||||
if (!mProject) {
|
||||
throw new Error('TKR-25009R M Project를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 모든 부적합 사항에 프로젝트 ID 추가
|
||||
let migratedCount = 0;
|
||||
for (let issue of issues) {
|
||||
if (!issue.project_id && !issue.projectId) {
|
||||
// API 방식과 localStorage 방식 모두 지원
|
||||
issue.project_id = mProject.id;
|
||||
issue.projectId = mProject.id;
|
||||
issue.project_name = 'TKR-25009R - M Project';
|
||||
issue.projectName = 'TKR-25009R - M Project';
|
||||
migratedCount++;
|
||||
|
||||
// API로 업데이트 시도
|
||||
try {
|
||||
if (typeof IssuesAPI !== 'undefined' && issue.id) {
|
||||
await IssuesAPI.update(issue.id, {
|
||||
project_id: mProject.id,
|
||||
project_name: 'TKR-25009R - M Project'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('API 업데이트 실패, localStorage만 업데이트');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// localStorage에도 저장
|
||||
localStorage.setItem('work-report-issues', JSON.stringify(issues));
|
||||
|
||||
addStatus(`✅ ${migratedCount}개 부적합 사항 마이그레이션 완료`, 'text-green-600');
|
||||
}
|
||||
|
||||
async function createSampleHours() {
|
||||
addStatus('368시간 샘플 데이터 생성 중...');
|
||||
|
||||
// TKR-25009R M Project ID 가져오기
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
||||
|
||||
if (!mProject) {
|
||||
throw new Error('TKR-25009R M Project를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 기존 일일 공수 데이터 로드
|
||||
let dailyWorkData = [];
|
||||
const saved = localStorage.getItem('daily-work-data');
|
||||
if (saved) {
|
||||
dailyWorkData = JSON.parse(saved);
|
||||
}
|
||||
|
||||
// 기존 데이터 중 잘못된 프로젝트로 등록된 것들 수정
|
||||
let updatedCount = 0;
|
||||
dailyWorkData.forEach(dayData => {
|
||||
if (dayData.projects) {
|
||||
dayData.projects.forEach(project => {
|
||||
if (project.projectName && project.projectName.includes('M-2024-001')) {
|
||||
project.projectId = mProject.id;
|
||||
project.projectName = 'TKR-25009R - M Project';
|
||||
updatedCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (updatedCount > 0) {
|
||||
addStatus(`✅ 기존 ${updatedCount}개 프로젝트 데이터를 TKR-25009R로 수정`, 'text-green-600');
|
||||
}
|
||||
|
||||
// 368시간을 여러 날짜에 분산해서 생성 (기존 데이터가 없는 경우만)
|
||||
const workDays = [
|
||||
{ date: '2024-10-01', hours: 48 },
|
||||
{ date: '2024-10-02', hours: 52 },
|
||||
{ date: '2024-10-03', hours: 44 },
|
||||
{ date: '2024-10-04', hours: 40 },
|
||||
{ date: '2024-10-07', hours: 56 },
|
||||
{ date: '2024-10-08', hours: 48 },
|
||||
{ date: '2024-10-09', hours: 36 },
|
||||
{ date: '2024-10-10', hours: 44 }
|
||||
];
|
||||
|
||||
let addedDays = 0;
|
||||
workDays.forEach(workDay => {
|
||||
// 해당 날짜에 이미 데이터가 있는지 확인
|
||||
const existingData = dailyWorkData.find(d => d.date === workDay.date);
|
||||
if (!existingData) {
|
||||
const newData = {
|
||||
date: workDay.date,
|
||||
projects: [{
|
||||
projectId: mProject.id,
|
||||
projectName: 'TKR-25009R - M Project',
|
||||
hours: workDay.hours
|
||||
}],
|
||||
totalHours: workDay.hours,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: 'hyungi'
|
||||
};
|
||||
|
||||
dailyWorkData.push(newData);
|
||||
addedDays++;
|
||||
}
|
||||
});
|
||||
|
||||
// 저장
|
||||
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
|
||||
|
||||
const totalCreatedHours = workDays.reduce((sum, day) => sum + day.hours, 0);
|
||||
if (addedDays > 0) {
|
||||
addStatus(`✅ ${addedDays}일간 총 ${totalCreatedHours}시간 데이터 생성 완료`, 'text-green-600');
|
||||
} else {
|
||||
addStatus('✅ 기존 시간 데이터 수정 완료', 'text-green-600');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
350
frontend/project-management.html
Normal file
350
frontend/project-management.html
Normal file
@@ -0,0 +1,350 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>프로젝트 관리 - 작업보고서 시스템</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--primary-dark: #2563eb;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: 1px solid var(--gray-300);
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 헤더 -->
|
||||
<header class="bg-white shadow-sm">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-800">
|
||||
<i class="fas fa-folder-open text-blue-500 mr-2"></i>프로젝트 관리
|
||||
</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-gray-600" id="userDisplay"></span>
|
||||
<a href="index.html" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-home mr-1"></i>메인으로
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 메인 컨텐츠 -->
|
||||
<main class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<!-- 프로젝트 생성 섹션 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-plus text-green-500 mr-2"></i>새 프로젝트 생성
|
||||
</h2>
|
||||
|
||||
<form id="projectForm" class="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Job No.</label>
|
||||
<input
|
||||
type="text"
|
||||
id="jobNo"
|
||||
class="input-field w-full px-4 py-2 rounded-lg"
|
||||
placeholder="예: JOB-2024-001"
|
||||
required
|
||||
maxlength="50"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">프로젝트 이름</label>
|
||||
<input
|
||||
type="text"
|
||||
id="projectName"
|
||||
class="input-field w-full px-4 py-2 rounded-lg"
|
||||
placeholder="프로젝트 이름을 입력하세요"
|
||||
required
|
||||
maxlength="200"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
|
||||
<i class="fas fa-plus mr-2"></i>프로젝트 생성
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 프로젝트 목록 섹션 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-800">프로젝트 목록</h2>
|
||||
<button onclick="loadProjects()" class="text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-refresh mr-1"></i>새로고침
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="projectsList" class="space-y-3">
|
||||
<!-- 프로젝트 목록이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// 사용자 확인 (관리자만 접근 가능)
|
||||
const currentUserData = localStorage.getItem('currentUser');
|
||||
if (!currentUserData) {
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
let currentUser;
|
||||
try {
|
||||
// JSON 파싱 시도
|
||||
currentUser = JSON.parse(currentUserData);
|
||||
} catch (e) {
|
||||
// JSON이 아니면 문자열로 처리 (기존 방식 호환)
|
||||
currentUser = { username: currentUserData };
|
||||
}
|
||||
|
||||
const username = currentUser.username || currentUser;
|
||||
const isAdmin = username === 'hyungi' || currentUser.role === 'admin';
|
||||
|
||||
if (!isAdmin) {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
||||
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
||||
|
||||
let projects = [];
|
||||
|
||||
// 프로젝트 데이터 로드
|
||||
function loadProjects() {
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
}
|
||||
displayProjectList();
|
||||
}
|
||||
|
||||
// 프로젝트 데이터 저장
|
||||
function saveProjects() {
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
}
|
||||
|
||||
// 프로젝트 생성 폼 처리
|
||||
document.getElementById('projectForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const jobNo = document.getElementById('jobNo').value.trim();
|
||||
const projectName = document.getElementById('projectName').value.trim();
|
||||
|
||||
// 중복 Job No. 확인
|
||||
if (projects.some(p => p.jobNo === jobNo)) {
|
||||
alert('이미 존재하는 Job No.입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 프로젝트 생성
|
||||
const newProject = {
|
||||
id: Date.now(),
|
||||
jobNo: jobNo,
|
||||
projectName: projectName,
|
||||
createdBy: 'hyungi',
|
||||
createdByName: '관리자',
|
||||
createdAt: new Date().toISOString(),
|
||||
isActive: true
|
||||
};
|
||||
|
||||
projects.push(newProject);
|
||||
saveProjects();
|
||||
|
||||
// 성공 메시지
|
||||
alert('프로젝트가 생성되었습니다.');
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('projectForm').reset();
|
||||
|
||||
// 목록 새로고침
|
||||
displayProjectList();
|
||||
});
|
||||
|
||||
// 프로젝트 목록 표시
|
||||
function displayProjectList() {
|
||||
const container = document.getElementById('projectsList');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (projects.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 프로젝트가 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 활성 프로젝트와 비활성 프로젝트 분리
|
||||
const activeProjects = projects.filter(p => p.isActive);
|
||||
const inactiveProjects = projects.filter(p => !p.isActive);
|
||||
|
||||
// 활성 프로젝트 표시
|
||||
if (activeProjects.length > 0) {
|
||||
const activeHeader = document.createElement('div');
|
||||
activeHeader.className = 'mb-4';
|
||||
activeHeader.innerHTML = '<h3 class="text-md font-semibold text-green-700"><i class="fas fa-check-circle mr-2"></i>활성 프로젝트 (' + activeProjects.length + '개)</h3>';
|
||||
container.appendChild(activeHeader);
|
||||
|
||||
activeProjects.forEach(project => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border border-green-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-green-50';
|
||||
div.innerHTML = `
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="font-semibold text-gray-800">${project.jobNo}</h3>
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">활성</span>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-2">${project.projectName}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="editProject(${project.id})" class="text-blue-600 hover:text-blue-800 p-2" title="수정">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button onclick="toggleProjectStatus(${project.id})" class="text-orange-600 hover:text-orange-800 p-2" title="비활성화">
|
||||
<i class="fas fa-pause"></i>
|
||||
</button>
|
||||
<button onclick="deleteProject(${project.id})" class="text-red-600 hover:text-red-800 p-2" title="삭제">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// 비활성 프로젝트 표시
|
||||
if (inactiveProjects.length > 0) {
|
||||
const inactiveHeader = document.createElement('div');
|
||||
inactiveHeader.className = 'mb-4 mt-6';
|
||||
inactiveHeader.innerHTML = '<h3 class="text-md font-semibold text-gray-500"><i class="fas fa-pause-circle mr-2"></i>비활성 프로젝트 (' + inactiveProjects.length + '개)</h3>';
|
||||
container.appendChild(inactiveHeader);
|
||||
|
||||
inactiveProjects.forEach(project => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-gray-50 opacity-75';
|
||||
div.innerHTML = `
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="font-semibold text-gray-600">${project.jobNo}</h3>
|
||||
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">비활성</span>
|
||||
</div>
|
||||
<p class="text-gray-500 mb-2">${project.projectName}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="toggleProjectStatus(${project.id})" class="text-green-600 hover:text-green-800 p-2" title="활성화">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
<button onclick="deleteProject(${project.id})" class="text-red-600 hover:text-red-800 p-2" title="완전 삭제">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 편집
|
||||
function editProject(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const newName = prompt('프로젝트 이름을 수정하세요:', project.projectName);
|
||||
if (newName && newName.trim() && newName.trim() !== project.projectName) {
|
||||
project.projectName = newName.trim();
|
||||
saveProjects();
|
||||
displayProjectList();
|
||||
alert('프로젝트가 수정되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 활성/비활성 토글
|
||||
function toggleProjectStatus(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const action = project.isActive ? '비활성화' : '활성화';
|
||||
if (confirm(`"${project.jobNo}" 프로젝트를 ${action}하시겠습니까?`)) {
|
||||
project.isActive = !project.isActive;
|
||||
saveProjects();
|
||||
displayProjectList();
|
||||
alert(`프로젝트가 ${action}되었습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 삭제 (완전 삭제)
|
||||
function deleteProject(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const confirmMessage = project.isActive
|
||||
? `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
|
||||
: `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
const index = projects.findIndex(p => p.id === projectId);
|
||||
if (index > -1) {
|
||||
projects.splice(index, 1);
|
||||
saveProjects();
|
||||
displayProjectList();
|
||||
alert('프로젝트가 완전히 삭제되었습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 프로젝트 목록 로드
|
||||
loadProjects();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -239,3 +239,27 @@ function checkAdminAuth() {
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
// 프로젝트 API
|
||||
const ProjectsAPI = {
|
||||
getAll: (activeOnly = false) => {
|
||||
const params = activeOnly ? '?active_only=true' : '';
|
||||
return apiRequest(`/projects${params}`);
|
||||
},
|
||||
|
||||
get: (id) => apiRequest(`/projects/${id}`),
|
||||
|
||||
create: (projectData) => apiRequest('/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(projectData)
|
||||
}),
|
||||
|
||||
update: (id, projectData) => apiRequest(`/projects/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(projectData)
|
||||
}),
|
||||
|
||||
delete: (id) => apiRequest(`/projects/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user