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:
@@ -175,6 +175,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 페이지 권한 관리 섹션 (관리자용) -->
|
||||
<div id="pagePermissionSection" class="mt-6">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-shield-alt text-purple-500 mr-2"></i>페이지 접근 권한 관리
|
||||
</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">사용자 선택</label>
|
||||
<select id="permissionUserSelect" class="input-field w-full max-w-xs px-3 py-2 rounded-lg">
|
||||
<option value="">사용자를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="pagePermissionGrid" class="hidden">
|
||||
<h3 class="text-md font-medium text-gray-700 mb-3">페이지별 접근 권한</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- 페이지 권한 체크박스들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t">
|
||||
<button
|
||||
id="savePermissionsBtn"
|
||||
class="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-save mr-2"></i>권한 저장
|
||||
</button>
|
||||
<span id="permissionSaveStatus" class="ml-3 text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 변경 섹션 (사용자용) -->
|
||||
<div id="passwordChangeSection" class="hidden mt-6">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 max-w-md mx-auto">
|
||||
@@ -228,7 +261,7 @@
|
||||
<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.src = `/static/js/api.js?v=20251025-2&cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
|
||||
script.setAttribute('cache-control', 'no-cache');
|
||||
script.setAttribute('pragma', 'no-cache');
|
||||
script.onload = function() {
|
||||
@@ -239,6 +272,9 @@
|
||||
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>
|
||||
let currentUser = null;
|
||||
let users = [];
|
||||
@@ -255,6 +291,10 @@
|
||||
const user = await AuthAPI.getCurrentUser();
|
||||
currentUser = user;
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
|
||||
// 공통 헤더 초기화
|
||||
await window.commonHeader.init(user, 'users_manage');
|
||||
|
||||
} catch (error) {
|
||||
console.error('인증 실패:', error);
|
||||
localStorage.removeItem('access_token');
|
||||
@@ -427,6 +467,202 @@
|
||||
alert(error.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 권한 관리 기능
|
||||
let selectedUserId = null;
|
||||
let currentPermissions = {};
|
||||
|
||||
// AuthAPI를 사용하여 사용자 목록 로드
|
||||
async function loadUsers() {
|
||||
try {
|
||||
users = await AuthAPI.getUsers();
|
||||
displayUsers();
|
||||
updatePermissionUserSelect(); // 권한 관리 드롭다운 업데이트
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 로드 실패:', error);
|
||||
document.getElementById('userList').innerHTML = `
|
||||
<div class="text-red-500 text-center py-8">
|
||||
<i class="fas fa-exclamation-triangle text-3xl"></i>
|
||||
<p>사용자 목록을 불러올 수 없습니다.</p>
|
||||
<p class="text-sm mt-2">오류: ${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 권한 관리 사용자 선택 드롭다운 업데이트
|
||||
function updatePermissionUserSelect() {
|
||||
const select = document.getElementById('permissionUserSelect');
|
||||
select.innerHTML = '<option value="">사용자를 선택하세요</option>';
|
||||
|
||||
// 일반 사용자만 표시 (admin 제외)
|
||||
const regularUsers = users.filter(user => user.role === 'user');
|
||||
regularUsers.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.id;
|
||||
option.textContent = `${user.full_name || user.username} (${user.username})`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 선택 시 페이지 권한 그리드 표시
|
||||
document.getElementById('permissionUserSelect').addEventListener('change', async (e) => {
|
||||
selectedUserId = e.target.value;
|
||||
|
||||
if (selectedUserId) {
|
||||
await loadUserPagePermissions(selectedUserId);
|
||||
showPagePermissionGrid();
|
||||
} else {
|
||||
hidePagePermissionGrid();
|
||||
}
|
||||
});
|
||||
|
||||
// 사용자의 페이지 권한 로드
|
||||
async function loadUserPagePermissions(userId) {
|
||||
try {
|
||||
// 기본 페이지 목록 가져오기
|
||||
const defaultPages = {
|
||||
'issues_create': { title: '부적합 등록', defaultAccess: true },
|
||||
'issues_view': { title: '부적합 조회', defaultAccess: true },
|
||||
'issues_manage': { title: '부적합 관리', defaultAccess: false },
|
||||
'projects_manage': { title: '프로젝트 관리', defaultAccess: false },
|
||||
'daily_work': { title: '일일 공수', defaultAccess: false },
|
||||
'reports': { title: '보고서', defaultAccess: false }
|
||||
};
|
||||
|
||||
// 기본값으로 초기화
|
||||
currentPermissions = {};
|
||||
Object.keys(defaultPages).forEach(pageName => {
|
||||
currentPermissions[pageName] = defaultPages[pageName].defaultAccess;
|
||||
});
|
||||
|
||||
// 실제 API 호출로 사용자별 설정된 권한 가져오기
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}/page-permissions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const permissions = await response.json();
|
||||
permissions.forEach(perm => {
|
||||
currentPermissions[perm.page_name] = perm.can_access;
|
||||
});
|
||||
console.log('사용자 권한 로드 완료:', currentPermissions);
|
||||
} else {
|
||||
console.warn('사용자 권한 로드 실패, 기본값 사용');
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.warn('API 호출 실패, 기본값 사용:', apiError);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 권한 그리드 표시
|
||||
function showPagePermissionGrid() {
|
||||
const grid = document.getElementById('pagePermissionGrid');
|
||||
const gridContainer = grid.querySelector('.grid');
|
||||
|
||||
// 페이지 권한 체크박스 생성
|
||||
const pages = {
|
||||
'issues_create': '부적합 등록',
|
||||
'issues_view': '부적합 조회',
|
||||
'issues_manage': '부적합 관리',
|
||||
'projects_manage': '프로젝트 관리',
|
||||
'daily_work': '일일 공수',
|
||||
'reports': '보고서'
|
||||
};
|
||||
|
||||
gridContainer.innerHTML = Object.entries(pages).map(([pageName, title]) => `
|
||||
<div class="flex items-center p-3 border rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="perm_${pageName}"
|
||||
${currentPermissions[pageName] ? 'checked' : ''}
|
||||
class="mr-3 h-4 w-4 text-purple-600 rounded focus:ring-purple-500"
|
||||
>
|
||||
<label for="perm_${pageName}" class="text-sm font-medium text-gray-700">
|
||||
${title}
|
||||
</label>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
grid.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 페이지 권한 그리드 숨기기
|
||||
function hidePagePermissionGrid() {
|
||||
document.getElementById('pagePermissionGrid').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 권한 저장
|
||||
document.getElementById('savePermissionsBtn').addEventListener('click', async () => {
|
||||
if (!selectedUserId) return;
|
||||
|
||||
const saveBtn = document.getElementById('savePermissionsBtn');
|
||||
const statusSpan = document.getElementById('permissionSaveStatus');
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>저장 중...';
|
||||
statusSpan.textContent = '';
|
||||
|
||||
try {
|
||||
// 체크박스 상태 수집
|
||||
const pages = ['issues_create', 'issues_view', 'issues_manage', 'projects_manage', 'daily_work', 'reports'];
|
||||
const permissions = {};
|
||||
|
||||
pages.forEach(pageName => {
|
||||
const checkbox = document.getElementById(`perm_${pageName}`);
|
||||
permissions[pageName] = checkbox.checked;
|
||||
});
|
||||
|
||||
// 실제 API 호출로 권한 저장
|
||||
const permissionArray = Object.entries(permissions).map(([pageName, canAccess]) => ({
|
||||
page_name: pageName,
|
||||
can_access: canAccess
|
||||
}));
|
||||
|
||||
const response = await fetch('/api/page-permissions/bulk-grant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: parseInt(selectedUserId),
|
||||
permissions: permissionArray
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || '권한 저장에 실패했습니다.');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('권한 저장 완료:', result);
|
||||
|
||||
statusSpan.textContent = '✅ 권한이 저장되었습니다.';
|
||||
statusSpan.className = 'ml-3 text-sm text-green-600';
|
||||
|
||||
setTimeout(() => {
|
||||
statusSpan.textContent = '';
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('권한 저장 실패:', error);
|
||||
statusSpan.textContent = '❌ 권한 저장에 실패했습니다.';
|
||||
statusSpan.className = 'ml-3 text-sm text-red-600';
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fas fa-save mr-2"></i>권한 저장';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -93,51 +93,7 @@
|
||||
</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-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 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="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 class="container mx-auto px-4 py-6 max-w-2xl">
|
||||
@@ -230,6 +186,9 @@
|
||||
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>
|
||||
let currentUser = null;
|
||||
let projects = [];
|
||||
@@ -248,6 +207,19 @@
|
||||
const user = await AuthAPI.getCurrentUser();
|
||||
currentUser = user;
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
|
||||
// 공통 헤더 초기화
|
||||
await window.commonHeader.init(user, 'daily_work');
|
||||
|
||||
// 페이지 접근 권한 체크 (일일 공수 페이지)
|
||||
setTimeout(() => {
|
||||
if (!canAccessPage('daily_work')) {
|
||||
alert('일일 공수 페이지에 접근할 권한이 없습니다.');
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('인증 실패:', error);
|
||||
localStorage.removeItem('access_token');
|
||||
@@ -256,14 +228,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 표시
|
||||
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
||||
// 사용자 정보는 공통 헤더에서 표시됨
|
||||
|
||||
// 네비게이션 권한 체크
|
||||
updateNavigation();
|
||||
// 네비게이션은 공통 헤더에서 처리됨
|
||||
|
||||
// 프로젝트 및 일일 공수 데이터 로드
|
||||
loadProjects();
|
||||
await loadProjects();
|
||||
loadDailyWorkData();
|
||||
|
||||
// 오늘 날짜로 초기화
|
||||
@@ -281,35 +251,43 @@
|
||||
console.log('📄 DOM 로드 완료 - API 스크립트 로딩 대기 중...');
|
||||
});
|
||||
|
||||
// 네비게이션 권한 업데이트
|
||||
function updateNavigation() {
|
||||
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 loadProjects() {
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
async function loadProjects() {
|
||||
try {
|
||||
// API에서 최신 프로젝트 데이터 가져오기
|
||||
const response = await fetch('/api/projects/', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
projects = await response.json();
|
||||
console.log('프로젝트 로드 완료:', projects.length, '개');
|
||||
console.log('활성 프로젝트:', projects.filter(p => p.is_active).length, '개');
|
||||
|
||||
// localStorage에도 캐시 저장
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
} else {
|
||||
console.error('프로젝트 로드 실패:', response.status);
|
||||
// 실패 시 localStorage에서 로드
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
console.log('캐시에서 프로젝트 로드:', projects.length, '개');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('프로젝트 로드 오류:', error);
|
||||
// 오류 시 localStorage에서 로드
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
console.log('캐시에서 프로젝트 로드:', projects.length, '개');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +306,7 @@
|
||||
|
||||
// 활성 프로젝트만 필터링
|
||||
function getActiveProjects() {
|
||||
return projects.filter(p => p.isActive);
|
||||
return projects.filter(p => p.is_active);
|
||||
}
|
||||
|
||||
// 프로젝트 입력 항목 추가
|
||||
|
||||
@@ -208,108 +208,86 @@
|
||||
|
||||
<!-- 메인 화면 -->
|
||||
<div id="mainScreen" class="hidden 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-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 id="navContainer" 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>
|
||||
<button class="nav-link active" onclick="showSection('report')">
|
||||
<i class="fas fa-camera-retro mr-2"></i>부적합 등록
|
||||
</button>
|
||||
<a href="issue-view.html" class="nav-link">
|
||||
<i class="fas fa-search mr-2"></i>부적합 조회
|
||||
</a>
|
||||
<button class="nav-link" onclick="showSection('list')" style="display:none;" id="listBtn">
|
||||
<i class="fas fa-list mr-2"></i>목록 관리
|
||||
</button>
|
||||
<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>
|
||||
<button class="nav-link" style="display:none;" id="adminBtn" onclick="handleAdminClick()">
|
||||
<i class="fas fa-users-cog mr-2"></i>관리
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
|
||||
|
||||
<!-- 부적합 등록 섹션 (모바일 최적화) -->
|
||||
<section id="reportSection" class="container mx-auto px-4 py-6 max-w-lg">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>부적합 사항 등록
|
||||
</h2>
|
||||
<section id="reportSection" class="container mx-auto px-3 py-4 max-w-md">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="mb-4">
|
||||
<h1 class="text-xl font-bold text-gray-900 flex items-center">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 mr-3"></i>
|
||||
부적합 등록
|
||||
</h1>
|
||||
<p class="text-sm text-gray-600 mt-1">현장에서 발견한 부적합 사항을 등록해주세요</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
|
||||
<!-- 진행 상태 표시 -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between text-xs text-gray-500 mb-2">
|
||||
<span>등록 진행률</span>
|
||||
<span id="progressText">0/6</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div id="progressBar" class="bg-blue-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="reportForm" class="space-y-4">
|
||||
<!-- 사진 업로드 (선택사항, 최대 2장) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
사진 <span class="text-gray-500 text-xs">(선택사항, 최대 2장)</span>
|
||||
</label>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
📸 사진 첨부
|
||||
</label>
|
||||
<span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
||||
선택사항 • 최대 2장
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 사진 미리보기 영역 -->
|
||||
<div id="photoPreviewContainer" class="grid grid-cols-2 gap-3 mb-3" style="display: none;">
|
||||
<div id="photoPreviewContainer" class="grid grid-cols-2 gap-2 mb-3" style="display: none;">
|
||||
<!-- 첫 번째 사진 -->
|
||||
<div id="photo1Container" class="relative hidden">
|
||||
<img id="previewImg1" class="w-full h-32 object-cover rounded-lg">
|
||||
<button type="button" onclick="removePhoto(0)" class="absolute top-1 right-1 p-1 bg-red-500 text-white rounded-full text-xs hover:bg-red-600">
|
||||
<img id="previewImg1" class="w-full h-24 object-cover rounded-xl border-2 border-gray-200">
|
||||
<button type="button" onclick="removePhoto(0)" class="absolute -top-1 -right-1 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 flex items-center justify-center shadow-lg">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 두 번째 사진 -->
|
||||
<div id="photo2Container" class="relative hidden">
|
||||
<img id="previewImg2" class="w-full h-32 object-cover rounded-lg">
|
||||
<button type="button" onclick="removePhoto(1)" class="absolute top-1 right-1 p-1 bg-red-500 text-white rounded-full text-xs hover:bg-red-600">
|
||||
<img id="previewImg2" class="w-full h-24 object-cover rounded-xl border-2 border-gray-200">
|
||||
<button type="button" onclick="removePhoto(1)" class="absolute -top-1 -right-1 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 flex items-center justify-center shadow-lg">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 업로드 버튼들 -->
|
||||
<div class="flex gap-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<!-- 카메라 촬영 버튼 -->
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
id="cameraUpload"
|
||||
class="flex-1 border-2 border-dashed border-blue-300 rounded-lg p-5 text-center cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition-all"
|
||||
class="flex flex-col items-center justify-center p-4 border-2 border-dashed border-blue-300 rounded-xl text-center cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition-all active:scale-95"
|
||||
onclick="openCamera()"
|
||||
>
|
||||
<i class="fas fa-camera text-4xl text-blue-500 mb-2"></i>
|
||||
<p class="text-gray-700 font-medium text-sm">📷 카메라</p>
|
||||
<p class="text-gray-500 text-xs mt-1">즉시 촬영</p>
|
||||
</div>
|
||||
<i class="fas fa-camera text-2xl text-blue-500 mb-2"></i>
|
||||
<span class="text-sm font-medium text-gray-700">카메라</span>
|
||||
<span class="text-xs text-gray-500">즉시 촬영</span>
|
||||
</button>
|
||||
|
||||
<!-- 갤러리 선택 버튼 -->
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
id="galleryUpload"
|
||||
class="flex-1 border-2 border-dashed border-green-300 rounded-lg p-5 text-center cursor-pointer hover:border-green-500 hover:bg-green-50 transition-all"
|
||||
class="flex flex-col items-center justify-center p-4 border-2 border-dashed border-green-300 rounded-xl text-center cursor-pointer hover:border-green-500 hover:bg-green-50 transition-all active:scale-95"
|
||||
onclick="openGallery()"
|
||||
>
|
||||
<i class="fas fa-images text-4xl text-green-500 mb-2"></i>
|
||||
<p class="text-gray-700 font-medium text-sm">🖼️ 갤러리</p>
|
||||
<p class="text-gray-500 text-xs mt-1">사진 선택</p>
|
||||
</div>
|
||||
<i class="fas fa-images text-2xl text-green-500 mb-2"></i>
|
||||
<span class="text-sm font-medium text-gray-700">갤러리</span>
|
||||
<span class="text-xs text-gray-500">사진 선택</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 현재 상태 표시 -->
|
||||
@@ -484,6 +462,11 @@
|
||||
</script>
|
||||
<script src="/static/js/image-utils.js?v=20250917"></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/core/page-preloader.js?v=20251025"></script>
|
||||
<script src="/static/js/core/keyboard-shortcuts.js?v=20251025"></script>
|
||||
<script>
|
||||
let currentUser = null;
|
||||
let currentPhotos = [];
|
||||
@@ -504,13 +487,22 @@
|
||||
// localStorage에도 백업 저장
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
|
||||
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
||||
// 공통 헤더 초기화
|
||||
await window.commonHeader.init(user, 'issues_create');
|
||||
|
||||
// 페이지 접근 권한 체크 (부적합 등록 페이지)
|
||||
setTimeout(() => {
|
||||
if (!canAccessPage('issues_create')) {
|
||||
alert('부적합 등록 페이지에 접근할 권한이 없습니다.');
|
||||
window.location.href = '/issue-view.html';
|
||||
return;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// 사용자 정보는 공통 헤더에서 표시됨
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainScreen').classList.remove('hidden');
|
||||
|
||||
// 권한에 따른 메뉴 표시/숨김
|
||||
updateNavigation();
|
||||
|
||||
// 프로젝트 로드
|
||||
await loadProjects();
|
||||
|
||||
@@ -547,12 +539,11 @@
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
||||
|
||||
document.getElementById('userDisplay').textContent = currentUser.full_name || currentUser.username;
|
||||
// 사용자 정보는 공통 헤더에서 표시됨
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainScreen').classList.remove('hidden');
|
||||
|
||||
// 권한에 따른 메뉴 표시/숨김
|
||||
updateNavigation();
|
||||
// 공통 헤더에서 권한 기반 메뉴 처리됨
|
||||
|
||||
// 프로젝트 로드
|
||||
await loadProjects();
|
||||
@@ -571,32 +562,7 @@
|
||||
AuthAPI.logout();
|
||||
}
|
||||
|
||||
// 네비게이션 권한 업데이트
|
||||
function updateNavigation() {
|
||||
const listBtn = document.getElementById('listBtn');
|
||||
const summaryBtn = document.getElementById('summaryBtn');
|
||||
const adminBtn = document.getElementById('adminBtn');
|
||||
const projectBtn = document.getElementById('projectBtn');
|
||||
const dailyWorkBtn = document.getElementById('dailyWorkBtn');
|
||||
|
||||
if (currentUser.role === 'admin') {
|
||||
// 관리자는 모든 메뉴 표시 (비밀번호 변경은 사용자 관리 페이지에서)
|
||||
listBtn.style.display = '';
|
||||
summaryBtn.style.display = '';
|
||||
projectBtn.style.display = '';
|
||||
dailyWorkBtn.style.display = '';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
||||
} else {
|
||||
// 일반 사용자는 제한된 메뉴만 표시 (비밀번호 변경 버튼 표시)
|
||||
listBtn.style.display = 'none';
|
||||
summaryBtn.style.display = 'none';
|
||||
projectBtn.style.display = 'none';
|
||||
dailyWorkBtn.style.display = 'none';
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-key mr-2"></i>비밀번호 변경';
|
||||
}
|
||||
}
|
||||
// 네비게이션은 공통 헤더에서 처리됨
|
||||
|
||||
// 관리 버튼 클릭 처리
|
||||
function handleAdminClick() {
|
||||
@@ -627,19 +593,7 @@
|
||||
// 선택된 섹션 표시
|
||||
document.getElementById(section + 'Section').classList.remove('hidden');
|
||||
|
||||
// 네비게이션 활성화 상태 변경
|
||||
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
|
||||
|
||||
// event가 있는 경우에만 활성화 처리
|
||||
if (typeof event !== 'undefined' && event.target && event.target.closest) {
|
||||
event.target.closest('.nav-link').classList.add('active');
|
||||
} else {
|
||||
// URL 해시로 접근한 경우 해당 버튼 찾아서 활성화
|
||||
const targetButton = document.querySelector(`[onclick="showSection('${section}')"]`);
|
||||
if (targetButton) {
|
||||
targetButton.classList.add('active');
|
||||
}
|
||||
}
|
||||
// 네비게이션 활성화는 공통 헤더에서 처리됨
|
||||
|
||||
// 부적합 등록 섹션으로 전환 시 프로젝트 다시 로드 (모바일 대응)
|
||||
if (section === 'report') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -49,22 +49,7 @@
|
||||
</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">
|
||||
@@ -177,6 +162,9 @@
|
||||
console.log('📱 프로젝트 관리 - 캐시 버스터:', cacheBuster);
|
||||
</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>
|
||||
// 사용자 확인 (관리자만 접근 가능)
|
||||
let currentUser = null;
|
||||
@@ -214,17 +202,19 @@
|
||||
const authSuccess = await initAuth();
|
||||
if (!authSuccess) return;
|
||||
|
||||
const username = currentUser.username || currentUser;
|
||||
const isAdmin = username === 'hyungi' || currentUser.role === 'admin';
|
||||
// 공통 헤더 초기화
|
||||
await window.commonHeader.init(currentUser, 'projects_manage');
|
||||
|
||||
if (!isAdmin) {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
// 페이지 접근 권한 체크 (프로젝트 관리 페이지)
|
||||
setTimeout(() => {
|
||||
if (!canAccessPage('projects_manage')) {
|
||||
alert('프로젝트 관리 페이지에 접근할 권한이 없습니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
||||
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
||||
// 사용자 정보는 공통 헤더에서 표시됨
|
||||
|
||||
// 프로젝트 로드
|
||||
loadProjects();
|
||||
|
||||
Reference in New Issue
Block a user