Files
M-Project/frontend/daily-work.html
hyungi 44e2fb2e44 feat: 사진 업로드 기능 개선 및 카테고리 업데이트
- 사진 2장까지 업로드 지원
- 카메라 촬영 + 갤러리 선택 분리
- 이미지 압축 및 최적화 (ImageUtils)
- iPhone .mpo 파일 JPEG 변환 지원
- 카테고리 변경: 치수불량 → 설계미스, 검사미스 추가
- KST 시간대 설정
- URL 해시 처리로 목록관리 페이지 이동 개선
- 로그인 OAuth2 form-data 형식 수정
- 업로드 속도 개선 및 프로그레스바 추가
2025-09-18 07:00:28 +09:00

483 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;
--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);
}
.btn-success {
background-color: var(--success);
color: white;
transition: all 0.2s;
}
.btn-success:hover {
background-color: #059669;
transform: translateY(-1px);
}
.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);
}
.work-card {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.work-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-card {
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
}
.nav-link {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
color: #4b5563;
transition: all 0.2s;
white-space: nowrap;
}
.nav-link:hover {
background-color: #f3f4f6;
color: #1f2937;
}
.nav-link.active {
background-color: #3b82f6;
color: white;
}
</style>
</head>
<body>
<div class="min-h-screen bg-gray-50">
<!-- 헤더 -->
<header class="bg-white shadow-sm sticky top-0 z-50">
<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>작업보고서 시스템
</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>
</div>
</header>
<!-- 네비게이션 -->
<nav class="bg-white border-b">
<div class="container mx-auto px-4">
<div id="navContainer" class="flex gap-2 py-2 overflow-x-auto">
<a href="daily-work.html" class="nav-link active">
<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">
<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="admin.html" class="nav-link" style="display:none;" id="adminBtn">
<i class="fas fa-users-cog mr-2"></i>관리
</a>
</div>
</div>
</nav>
<!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-6 max-w-2xl">
<!-- 입력 카드 -->
<div class="work-card p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-6">
<i class="fas fa-edit text-blue-500 mr-2"></i>공수 입력
</h2>
<form id="dailyWorkForm" class="space-y-6">
<!-- 날짜 선택 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-calendar mr-1"></i>날짜
</label>
<input
type="date"
id="workDate"
class="input-field w-full px-4 py-3 rounded-lg text-lg"
required
>
</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>잔업 여부
</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()"
>
<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>
</div>
</div>
<!-- 총 공수 표시 -->
<div class="bg-blue-50 rounded-lg p-4">
<div class="flex justify-between items-center">
<span class="text-gray-700 font-medium">예상 총 공수</span>
<span class="text-2xl font-bold text-blue-600" id="totalHours">0시간</span>
</div>
</div>
<!-- 저장 버튼 -->
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-medium text-lg">
<i class="fas fa-save mr-2"></i>저장하기
</button>
</form>
</div>
<!-- 최근 입력 내역 -->
<div class="work-card p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-history text-gray-500 mr-2"></i>최근 입력 내역
</h3>
<div id="recentEntries" class="space-y-3">
<!-- 최근 입력 내역이 여기에 표시됩니다 -->
</div>
</div>
</main>
</div>
<script src="/static/js/api.js?v=20250917"></script>
<script src="/static/js/date-utils.js?v=20250917"></script>
<script>
let currentUser = null;
let dailyWorks = [];
// 페이지 로드 시 인증 체크
window.addEventListener('DOMContentLoaded', async () => {
const user = TokenManager.getUser();
if (!user) {
window.location.href = '/index.html';
return;
}
currentUser = user;
// 네비게이션 권한 체크
updateNavigation();
// 오늘 날짜로 초기화
document.getElementById('workDate').valueAsDate = new Date();
// 최근 내역 로드
await loadRecentEntries();
});
// 네비게이션 권한 업데이트
function updateNavigation() {
const listBtn = document.getElementById('listBtn');
const summaryBtn = document.getElementById('summaryBtn');
const adminBtn = document.getElementById('adminBtn');
if (currentUser.role === 'admin') {
// 관리자는 모든 메뉴 표시
listBtn.style.display = '';
summaryBtn.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';
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');
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');
} 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 = '';
}
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);
let overtimeWorkers = 0;
let overtimeHours = 0;
if (!document.getElementById('overtimeSection').classList.contains('hidden')) {
overtimeWorkers = parseInt(document.getElementById('overtimeWorkers').value) || 0;
overtimeHours = parseFloat(document.getElementById('overtimeHours').value) || 0;
}
try {
// API 호출
await DailyWorkAPI.create({
date: new Date(workDate).toISOString(),
worker_count: workerCount,
overtime_workers: overtimeWorkers,
overtime_hours: overtimeHours
});
// 성공 메시지
showSuccessMessage();
// 폼 초기화
resetForm();
// 최근 내역 갱신
await loadRecentEntries();
} catch (error) {
alert(error.message || '저장에 실패했습니다.');
}
});
// 최근 데이터 로드
async function loadRecentEntries() {
try {
// 최근 7일간 데이터 가져오기
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
dailyWorks = await DailyWorkAPI.getAll({
start_date: startDate.toISOString().split('T')[0],
end_date: endDate.toISOString().split('T')[0],
limit: 7
});
displayRecentEntries();
} catch (error) {
console.error('데이터 로드 실패:', error);
dailyWorks = [];
displayRecentEntries();
}
}
// 성공 메시지
function showSuccessMessage() {
const button = document.querySelector('button[type="submit"]');
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check-circle mr-2"></i>저장 완료!';
button.classList.remove('btn-primary');
button.classList.add('btn-success');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-primary');
}, 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() {
const container = document.getElementById('recentEntries');
if (dailyWorks.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);
return `
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div>
<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>
<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>
</div>
`;
}).join('');
}
// 일일 공수 삭제 (관리자만)
async function deleteDailyWork(workId) {
if (!currentUser || currentUser.role !== 'admin') {
alert('관리자만 삭제할 수 있습니다.');
return;
}
if (!confirm('정말로 이 일일 공수 기록을 삭제하시겠습니까?')) {
return;
}
try {
await DailyWorkAPI.delete(workId);
// 성공 시 목록 다시 로드
await loadRecentEntries();
alert('삭제되었습니다.');
} catch (error) {
alert(error.message || '삭제에 실패했습니다.');
}
}
</script>
</body>
</html>