feat: 작업보고서 시스템 완성
- 일일 공수 입력 기능 - 부적합 사항 등록 (이미지 선택사항) - 날짜별 부적합 조회 (시간순 나열) - 목록 관리 (인라인 편집, 작업시간 확인 버튼) - 보고서 생성 (총 공수/부적합 시간 분리) - JWT 인증 및 권한 관리 - Docker 기반 배포 환경 구성
This commit is contained in:
315
frontend/issue-view.html
Normal file
315
frontend/issue-view.html
Normal file
@@ -0,0 +1,315 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>부적합 사항 조회 - 작업보고서</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 50%, #f0f9ff 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="glass-effect border-b border-gray-200 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>작업보고서 시스템
|
||||
</h1>
|
||||
<div 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">
|
||||
<i class="fas fa-list mr-1"></i>목록 관리
|
||||
</a>
|
||||
<a href="/index.html#summary" class="text-gray-600 hover:text-gray-800 transition-colors">
|
||||
<i class="fas fa-chart-bar 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>로그아웃
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<!-- 날짜 선택 섹션 (간소화) -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
|
||||
<div class="flex flex-wrap gap-3 items-center">
|
||||
<h2 class="text-lg font-semibold text-gray-800">
|
||||
<i class="fas fa-list-alt text-blue-500 mr-2"></i>부적합 사항 목록
|
||||
</h2>
|
||||
|
||||
<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">
|
||||
오늘
|
||||
</button>
|
||||
<button onclick="setDateRange('week')" class="px-3 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors text-sm">
|
||||
이번 주
|
||||
</button>
|
||||
<button onclick="setDateRange('month')" class="px-3 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors text-sm">
|
||||
이번 달
|
||||
</button>
|
||||
<button onclick="setDateRange('all')" class="px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm">
|
||||
전체
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 결과 섹션 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div id="issueResults" class="space-y-3">
|
||||
<!-- 결과가 여기에 표시됩니다 -->
|
||||
<div class="text-gray-500 text-center py-8">
|
||||
<i class="fas fa-spinner fa-spin text-3xl mb-3"></i>
|
||||
<p>데이터를 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script>
|
||||
let currentUser = null;
|
||||
let issues = [];
|
||||
let currentRange = 'week'; // 기본값: 이번 주
|
||||
|
||||
// 페이지 로드 시
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
const user = TokenManager.getUser();
|
||||
if (!user) {
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
}
|
||||
currentUser = user;
|
||||
|
||||
// 기본값: 이번 주 데이터 로드
|
||||
setDateRange('week');
|
||||
});
|
||||
|
||||
// 날짜 범위 설정 및 자동 조회
|
||||
async function setDateRange(range) {
|
||||
currentRange = range;
|
||||
|
||||
// 버튼 스타일 업데이트
|
||||
document.querySelectorAll('button[onclick^="setDateRange"]').forEach(btn => {
|
||||
if (btn.textContent.includes('전체') && range === 'all') {
|
||||
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
|
||||
} else if (btn.textContent.includes('오늘') && range === 'today') {
|
||||
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
|
||||
} else if (btn.textContent.includes('이번 주') && range === 'week') {
|
||||
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
|
||||
} else if (btn.textContent.includes('이번 달') && range === 'month') {
|
||||
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
|
||||
} else {
|
||||
btn.className = 'px-3 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors text-sm';
|
||||
}
|
||||
});
|
||||
|
||||
await loadIssues(range);
|
||||
}
|
||||
|
||||
// 부적합 사항 로드
|
||||
async function loadIssues(range) {
|
||||
const container = document.getElementById('issueResults');
|
||||
container.innerHTML = `
|
||||
<div class="text-gray-500 text-center py-8">
|
||||
<i class="fas fa-spinner fa-spin text-3xl mb-3"></i>
|
||||
<p>데이터를 불러오는 중...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
// 모든 이슈 가져오기
|
||||
const allIssues = await IssuesAPI.getAll();
|
||||
|
||||
// 날짜 필터링
|
||||
const today = new Date();
|
||||
today.setHours(23, 59, 59, 999);
|
||||
let startDate = new Date();
|
||||
|
||||
switch(range) {
|
||||
case 'today':
|
||||
startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case 'week':
|
||||
startDate.setDate(today.getDate() - 7);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case 'month':
|
||||
startDate.setMonth(today.getMonth() - 1);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case 'all':
|
||||
startDate = new Date(2020, 0, 1); // 충분히 과거 날짜
|
||||
break;
|
||||
}
|
||||
|
||||
// 필터링 및 정렬 (최신순)
|
||||
issues = allIssues
|
||||
.filter(issue => {
|
||||
const issueDate = new Date(issue.report_date);
|
||||
return issueDate >= startDate && issueDate <= today;
|
||||
})
|
||||
.sort((a, b) => new Date(b.report_date) - new Date(a.report_date));
|
||||
|
||||
// 결과 표시
|
||||
displayResults();
|
||||
|
||||
} catch (error) {
|
||||
console.error('조회 실패:', error);
|
||||
container.innerHTML = `
|
||||
<div class="text-red-500 text-center py-8">
|
||||
<i class="fas fa-exclamation-circle text-3xl mb-3"></i>
|
||||
<p>데이터를 불러오는데 실패했습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 결과 표시 (시간순 나열)
|
||||
function displayResults() {
|
||||
const container = document.getElementById('issueResults');
|
||||
|
||||
if (issues.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>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryNames = {
|
||||
material_missing: '자재누락',
|
||||
dimension_defect: '치수불량',
|
||||
incoming_defect: '입고자재 불량'
|
||||
};
|
||||
|
||||
const categoryColors = {
|
||||
material_missing: 'bg-yellow-100 text-yellow-700 border-yellow-300',
|
||||
dimension_defect: 'bg-orange-100 text-orange-700 border-orange-300',
|
||||
incoming_defect: 'bg-red-100 text-red-700 border-red-300'
|
||||
};
|
||||
|
||||
// 시간순으로 나열
|
||||
container.innerHTML = issues.map(issue => {
|
||||
const date = new Date(issue.report_date);
|
||||
const dateStr = date.toLocaleDateString('ko-KR', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
weekday: 'short'
|
||||
});
|
||||
const timeStr = date.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
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'}">
|
||||
<!-- 사진 -->
|
||||
${issue.photo_path ?
|
||||
`<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded shadow-sm flex-shrink-0">` :
|
||||
`<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-image text-gray-400"></i>
|
||||
</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} ${timeStr}
|
||||
</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>
|
||||
</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 === 'dimension_defect').length}건 |
|
||||
입고자재 불량: ${issues.filter(i => i.category === 'incoming_defect').length}건
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = summary + container.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user