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>
|
||||
|
||||
Reference in New Issue
Block a user