feat: 모든 페이지에 공통 헤더 적용 및 모바일 최적화

- 모든 HTML 페이지에 권한 기반 공통 헤더 적용
- 부적합 등록 페이지 모바일 최적화 (사진 업로드 UI 개선)
- 부적합 조회 페이지에 모바일 캘린더 날짜 필터 적용
- 사용자별 권한에 따른 동적 페이지 제목 및 메시지 표시

Page Updates:
- index.html: 모바일 친화적 사진 업로드 UI, 공통 헤더 적용
- issue-view.html: 터치/스와이프 캘린더 필터, 권한별 조회 제한
- daily-work.html: 공통 헤더 적용, 프로젝트 로딩 로직 개선
- project-management.html: 공통 헤더 적용, 권한 체크 강화
- admin.html: 페이지 권한 관리 UI 추가, 공통 헤더 적용

Mobile Optimizations:
- 터치 타겟 최소 44px 보장
- 스와이프 제스처 지원
- 반응형 레이아웃
- 모바일 전용 UI 컴포넌트
This commit is contained in:
Hyungi Ahn
2025-10-25 09:01:32 +09:00
parent 25123be806
commit 610a171b25
5 changed files with 615 additions and 363 deletions

View File

@@ -11,6 +11,9 @@
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 모바일 캘린더 스타일 -->
<link rel="stylesheet" href="/static/css/mobile-calendar.css">
<!-- Custom Styles -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@@ -66,69 +69,30 @@
</style>
</head>
<body>
<!-- 헤더 -->
<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-check text-blue-500 mr-2"></i>작업보고서
</h1>
<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" id="dailyWorkBtn" style="display: none;">
<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>
<button class="nav-link" style="display:none;" id="adminBtn" onclick="handleAdminClick()">
<i class="fas fa-users-cog mr-2"></i>사용자 관리
</button>
</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="mb-4">
<h1 id="pageTitle" class="text-xl font-bold text-gray-900 flex items-center">
<i class="fas fa-list-alt text-blue-500 mr-3"></i>
내 부적합 조회
</h1>
<p id="pageDescription" class="text-sm text-gray-600 mt-1">
내가 등록한 부적합 사항을 확인할 수 있습니다
</p>
</div>
<!-- 필터 섹션 -->
<div class="bg-gray-50 p-4 rounded-lg mb-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-4 mb-6">
<!-- 기본 필터들 -->
<div class="grid grid-cols-1 md:grid-cols-2 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()">
<label class="block text-sm font-medium text-gray-700 mb-2">📁 프로젝트</label>
<select id="projectFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" onchange="filterIssues()">
<option value="">전체 프로젝트</option>
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
</select>
@@ -136,41 +100,38 @@
<!-- 검토 상태 필터 -->
<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()">
<label class="block text-sm font-medium text-gray-700 mb-2">📋 검토 상태</label>
<select id="reviewStatusFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" onchange="filterIssues()">
<option value="">전체</option>
<option value="pending">검토 필요</option>
<option value="completed">검토 완료</option>
</select>
</div>
</div>
<!-- 날짜 필터 (캘린더) -->
<div>
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-medium text-gray-700">📅 기간 선택</label>
<button id="toggleCalendar" class="text-sm text-blue-600 hover:text-blue-800 flex items-center">
<span id="calendarToggleText">캘린더 열기</span>
<i class="fas fa-chevron-down ml-1" id="calendarToggleIcon"></i>
</button>
</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 id="calendarContainer" class="bg-white border border-gray-200 rounded-xl p-4" style="display: none;">
<!-- 모바일 캘린더가 여기에 렌더링됩니다 -->
</div>
<!-- 선택된 날짜 범위 표시 -->
<div id="currentDateRange" class="mt-2 text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-lg">
<i class="fas fa-calendar-alt mr-2"></i>
<span id="dateRangeText">이번 주</span>
</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">
오늘
</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>
@@ -187,25 +148,19 @@
</main>
<!-- Scripts -->
<script>
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
const script = document.createElement('script');
script.src = `/static/js/api.js?cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
script.setAttribute('cache-control', 'no-cache');
script.setAttribute('pragma', 'no-cache');
script.onload = function() {
console.log('✅ API 스크립트 로드 완료 (issue-view.html)');
// API 로드 후 초기화 시작
initializeIssueView();
};
document.head.appendChild(script);
</script>
<script src="/static/js/date-utils.js?v=20250917"></script>
<script src="/static/js/core/permissions.js?v=20251025"></script>
<script src="/static/js/components/common-header.js?v=20251025"></script>
<script src="/static/js/core/page-manager.js?v=20251025"></script>
<script src="/static/js/components/mobile-calendar.js?v=20251025"></script>
<script>
let currentUser = null;
let issues = [];
let projects = []; // 프로젝트 데이터 캐시
let currentRange = 'week'; // 기본값: 이번 주
let mobileCalendar = null; // 모바일 캘린더 인스턴스
let selectedStartDate = null;
let selectedEndDate = null;
// API 로드 후 초기화 함수
async function initializeIssueView() {
@@ -219,6 +174,22 @@
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_view');
// 사용자 역할에 따른 페이지 제목 설정
updatePageTitle(user);
// 페이지 접근 권한 체크 (부적합 조회 페이지)
setTimeout(() => {
if (!canAccessPage('issues_view')) {
alert('부적합 조회 페이지에 접근할 권한이 없습니다.');
window.location.href = '/index.html';
return;
}
}, 500);
} catch (error) {
console.error('인증 실패:', error);
localStorage.removeItem('access_token');
@@ -227,12 +198,14 @@
return;
}
// 네비게이션 권한 체크
updateNavigation();
// 네비게이션은 공통 헤더에서 처리됨
// 프로젝트 로드
await loadProjects();
// 캘린더 초기화
initializeMobileCalendar();
// 기본값: 이번 주 데이터 로드
setDateRange('week');
}
@@ -261,26 +234,88 @@
document.body.appendChild(modal);
}
// 네비게이션 권한 업데이트
function updateNavigation() {
const listBtn = document.getElementById('listBtn');
const summaryBtn = document.getElementById('summaryBtn');
const adminBtn = document.getElementById('adminBtn');
const projectBtn = document.getElementById('projectBtn');
// 네비게이션은 공통 헤더에서 처리됨
// 모바일 캘린더 초기화
function initializeMobileCalendar() {
// 캘린더 토글 버튼 이벤트
document.getElementById('toggleCalendar').addEventListener('click', () => {
const container = document.getElementById('calendarContainer');
const toggleText = document.getElementById('calendarToggleText');
const toggleIcon = document.getElementById('calendarToggleIcon');
if (container.style.display === 'none') {
container.style.display = 'block';
toggleText.textContent = '캘린더 닫기';
toggleIcon.classList.remove('fa-chevron-down');
toggleIcon.classList.add('fa-chevron-up');
// 캘린더 인스턴스가 없으면 생성
if (!mobileCalendar) {
mobileCalendar = new MobileCalendar('calendarContainer', {
onRangeSelect: (startDate, endDate) => {
handleDateRangeSelect(startDate, endDate);
}
});
}
} else {
container.style.display = 'none';
toggleText.textContent = '캘린더 열기';
toggleIcon.classList.remove('fa-chevron-up');
toggleIcon.classList.add('fa-chevron-down');
}
});
}
// 날짜 범위 선택 처리
function handleDateRangeSelect(startDate, endDate) {
selectedStartDate = startDate;
selectedEndDate = endDate;
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>사용자 관리';
// 날짜 범위 텍스트 업데이트
updateDateRangeText(startDate, endDate);
// 필터 적용
filterIssues();
}
// 날짜 범위 텍스트 업데이트
function updateDateRangeText(startDate, endDate) {
const dateRangeText = document.getElementById('dateRangeText');
if (!startDate && !endDate) {
dateRangeText.textContent = '전체 기간';
currentRange = 'all';
} else if (startDate && endDate) {
const start = startDate.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
const end = endDate.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
const daysDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
dateRangeText.textContent = `${start} ~ ${end} (${daysDiff}일)`;
currentRange = 'custom';
} else if (startDate) {
const start = startDate.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
dateRangeText.textContent = `${start} (선택 중...)`;
currentRange = 'custom';
}
}
// 사용자 역할에 따른 페이지 제목 업데이트
function updatePageTitle(user) {
const titleElement = document.getElementById('pageTitle');
const descriptionElement = document.getElementById('pageDescription');
if (user.role === 'admin') {
titleElement.innerHTML = `
<i class="fas fa-list-alt text-blue-500 mr-3"></i>
전체 부적합 조회
`;
descriptionElement.textContent = '모든 사용자가 등록한 부적합 사항을 관리할 수 있습니다';
} else {
// 일반 사용자는 제한된 메뉴만 표시 (관리 버튼 숨김)
listBtn.style.display = 'none';
summaryBtn.style.display = 'none';
projectBtn.style.display = 'none';
adminBtn.style.display = 'none';
titleElement.innerHTML = `
<i class="fas fa-list-alt text-blue-500 mr-3"></i>
내 부적합 조회
`;
descriptionElement.textContent = '내가 등록한 부적합 사항을 확인할 수 있습니다';
}
}
@@ -341,11 +376,50 @@
}
}
// 날짜 범위별 필터링 함수
function filterByDateRange(issues, range) {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
switch (range) {
case 'today':
return issues.filter(issue => {
const issueDate = new Date(issue.created_at);
const issueDay = new Date(issueDate.getFullYear(), issueDate.getMonth(), issueDate.getDate());
return issueDay.getTime() === today.getTime();
});
case 'week':
const weekStart = new Date(today);
weekStart.setDate(today.getDate() - today.getDay());
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
return issues.filter(issue => {
const issueDate = new Date(issue.created_at);
return issueDate >= weekStart && issueDate <= weekEnd;
});
case 'month':
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
monthEnd.setHours(23, 59, 59, 999);
return issues.filter(issue => {
const issueDate = new Date(issue.created_at);
return issueDate >= monthStart && issueDate <= monthEnd;
});
default:
return issues;
}
}
function filterIssues() {
// 필터 값 가져오기
const selectedProjectId = document.getElementById('projectFilter').value;
const reviewStatusFilter = document.getElementById('reviewStatusFilter').value;
const dateFilter = document.getElementById('dateFilter').value;
let filteredIssues = [...issues];
@@ -365,9 +439,20 @@
});
}
// 날짜 필터 적용
if (dateFilter) {
filteredIssues = filterByDate(filteredIssues, dateFilter);
// 날짜 범위 필터 적용 (캘린더에서 선택된 범위)
if (selectedStartDate && selectedEndDate) {
filteredIssues = filteredIssues.filter(issue => {
const issueDate = new Date(issue.created_at);
const startOfDay = new Date(selectedStartDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(selectedEndDate);
endOfDay.setHours(23, 59, 59, 999);
return issueDate >= startOfDay && issueDate <= endOfDay;
});
} else if (currentRange && currentRange !== 'all' && currentRange !== 'custom') {
// 빠른 선택 범위 적용
filteredIssues = filterByDateRange(filteredIssues, currentRange);
}
// 전역 변수에 필터링된 결과 저장
@@ -480,10 +565,22 @@
const filteredIssues = window.filteredIssues || issues;
if (filteredIssues.length === 0) {
const emptyMessage = currentUser.role === 'admin'
? '조건에 맞는 부적합 사항이 없습니다.'
: '아직 등록한 부적합 사항이 없습니다.<br><small class="text-sm">부적합 등록 페이지에서 새로운 부적합을 등록해보세요.</small>';
container.innerHTML = `
<div class="text-gray-500 text-center py-8">
<i class="fas fa-inbox text-4xl mb-3"></i>
<p>조건에 맞는 부적합 사항이 없습니다.</p>
<div class="text-gray-500 text-center py-12">
<i class="fas fa-inbox text-4xl mb-4 text-gray-400"></i>
<p class="text-lg mb-2">${emptyMessage}</p>
${currentUser.role !== 'admin' ? `
<div class="mt-4">
<a href="/index.html" class="inline-flex items-center px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-plus mr-2"></i>
부적합 등록하기
</a>
</div>
` : ''}
</div>
`;
return;
@@ -750,26 +847,23 @@
window.location.href = 'index.html';
}
// 네비게이션 업데이트 함수
function updateNavigation() {
if (!currentUser) {
window.location.href = 'index.html';
return;
}
// 사용자 표시
const displayName = currentUser.full_name || currentUser.username;
document.getElementById('userDisplay').textContent = `${displayName} (${currentUser.username})`;
// 관리자인 경우 메뉴 표시
if (currentUser.role === 'admin') {
document.getElementById('dailyWorkBtn').style.display = '';
document.getElementById('listBtn').style.display = '';
document.getElementById('summaryBtn').style.display = '';
document.getElementById('projectBtn').style.display = '';
document.getElementById('adminBtn').style.display = '';
}
}
// 네비게이션은 공통 헤더에서 처리됨
// API 스크립트 동적 로딩
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
const script = document.createElement('script');
script.src = `/static/js/api.js?cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
script.setAttribute('cache-control', 'no-cache');
script.setAttribute('pragma', 'no-cache');
script.onload = function() {
console.log('✅ API 스크립트 로드 완료 (issue-view.html)');
// API 로드 후 초기화 시작
initializeIssueView();
};
script.onerror = function() {
console.error('❌ API 스크립트 로드 실패');
};
document.head.appendChild(script);
</script>
</body>
</html>