Files
M-Project/frontend/project-management.html
Hyungi Ahn 610a171b25 feat: 모든 페이지에 공통 헤더 적용 및 모바일 최적화
- 모든 HTML 페이지에 권한 기반 공통 헤더 적용
- 부적합 등록 페이지 모바일 최적화 (사진 업로드 UI 개선)
- 부적합 조회 페이지에 모바일 캘린더 날짜 필터 적용
- 사용자별 권한에 따른 동적 페이지 제목 및 메시지 표시

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

Mobile Optimizations:
- 터치 타겟 최소 44px 보장
- 스와이프 제스처 지원
- 반응형 레이아웃
- 모바일 전용 UI 컴포넌트
2025-10-25 09:01:32 +09:00

464 lines
21 KiB
HTML

<!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;
--warning: #f59e0b;
--danger: #ef4444;
--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);
}
.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);
}
</style>
</head>
<body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-8 max-w-4xl">
<!-- 프로젝트 생성 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-plus text-green-500 mr-2"></i>새 프로젝트 생성
</h2>
<form id="projectForm" class="grid md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Job No.</label>
<input
type="text"
id="jobNo"
class="input-field w-full px-4 py-2 rounded-lg"
placeholder="예: JOB-2024-001"
required
maxlength="50"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">프로젝트 이름</label>
<input
type="text"
id="projectName"
class="input-field w-full px-4 py-2 rounded-lg"
placeholder="프로젝트 이름을 입력하세요"
required
maxlength="200"
>
</div>
<div class="md:col-span-2">
<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
<i class="fas fa-plus mr-2"></i>프로젝트 생성
</button>
</div>
</form>
</div>
<!-- 프로젝트 목록 섹션 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800">프로젝트 목록</h2>
<button onclick="loadProjects()" class="text-blue-600 hover:text-blue-800">
<i class="fas fa-refresh mr-1"></i>새로고침
</button>
</div>
<div id="projectsList" class="space-y-3">
<!-- 프로젝트 목록이 여기에 표시됩니다 -->
</div>
</div>
</main>
<!-- API 스크립트 먼저 로드 (최강 캐시 무력화) -->
<script>
// 브라우저 캐시 완전 무력화
const timestamp = new Date().getTime();
const random1 = Math.random() * 1000000;
const random2 = Math.floor(Math.random() * 1000000);
const cacheBuster = `${timestamp}-${random1}-${random2}`;
const script = document.createElement('script');
script.src = `/static/js/api.js?force-reload=${cacheBuster}&no-cache=${timestamp}&bust=${random2}`;
script.onload = function() {
console.log('✅ API 스크립트 로드 완료');
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
console.log('🌐 현재 hostname:', window.location.hostname);
console.log('🔗 현재 protocol:', window.location.protocol);
// API 로드 후 인증 체크 시작
setTimeout(checkAdminAccess, 100);
};
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
script.setAttribute('pragma', 'no-cache');
script.setAttribute('expires', '0');
document.head.appendChild(script);
console.log('🚀 캐시 버스터:', cacheBuster);
</script>
<!-- API 스크립트 동적 로딩 -->
<script>
// 최강 캐시 무력화로 API 스크립트 로드
const timestamp = new Date().getTime();
const random1 = Math.random() * 1000000;
const random2 = Math.floor(Math.random() * 1000000);
const cacheBuster = `${timestamp}-${random1}-${random2}`;
// 기존 api.js 스크립트 제거
const existingScripts = document.querySelectorAll('script[src*="api.js"]');
existingScripts.forEach(script => script.remove());
const script = document.createElement('script');
script.src = `/static/js/api.js?v=${timestamp}&cb=${random1}&bust=${random2}&force=${Date.now()}`;
script.onload = function() {
console.log('✅ API 스크립트 로드 완료 (project-management.html)');
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
// API 로드 후 인증 확인 시작
checkAdminAccess();
};
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
script.setAttribute('pragma', 'no-cache');
script.setAttribute('expires', '0');
script.crossOrigin = 'anonymous';
document.head.appendChild(script);
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;
async function initAuth() {
console.log('인증 초기화 시작');
const token = localStorage.getItem('access_token');
console.log('토큰 존재:', !!token);
if (!token) {
console.log('토큰 없음 - 로그인 페이지로 이동');
alert('로그인이 필요합니다.');
window.location.href = 'index.html';
return false;
}
try {
console.log('API로 사용자 정보 가져오는 중...');
const user = await AuthAPI.getCurrentUser();
console.log('사용자 정보:', user);
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
return true;
} catch (error) {
console.error('인증 실패:', error);
localStorage.removeItem('access_token');
localStorage.removeItem('currentUser');
alert('로그인이 필요합니다.');
window.location.href = 'index.html';
return false;
}
}
async function checkAdminAccess() {
const authSuccess = await initAuth();
if (!authSuccess) return;
// 공통 헤더 초기화
await window.commonHeader.init(currentUser, 'projects_manage');
// 페이지 접근 권한 체크 (프로젝트 관리 페이지)
setTimeout(() => {
if (!canAccessPage('projects_manage')) {
alert('프로젝트 관리 페이지에 접근할 권한이 없습니다.');
window.location.href = 'index.html';
return;
}
}, 500);
// 사용자 정보는 공통 헤더에서 표시됨
// 프로젝트 로드
loadProjects();
}
let projects = [];
// 프로젝트 데이터 로드 (API 기반)
async function loadProjects() {
console.log('프로젝트 로드 시작 (API)');
try {
// API에서 프로젝트 로드
const apiProjects = await ProjectsAPI.getAll(false);
// API 데이터를 그대로 사용 (필드명 통일)
projects = apiProjects;
console.log('API에서 프로젝트 로드:', projects.length, '개');
} catch (error) {
console.error('API 로드 실패:', error);
projects = [];
}
displayProjectList();
}
// 프로젝트 데이터 저장 (더 이상 사용하지 않음 - API 기반)
// function saveProjects() {
// localStorage.setItem('work-report-projects', JSON.stringify(projects));
// }
// 프로젝트 생성 폼 처리
document.getElementById('projectForm').addEventListener('submit', async (e) => {
e.preventDefault();
const jobNo = document.getElementById('jobNo').value.trim();
const projectName = document.getElementById('projectName').value.trim();
// 중복 Job No. 확인
if (projects.some(p => p.job_no === jobNo)) {
alert('이미 존재하는 Job No.입니다.');
return;
}
try {
// API를 통한 프로젝트 생성
const newProject = await ProjectsAPI.create({
job_no: jobNo,
project_name: projectName
});
// 성공 메시지
alert('프로젝트가 생성되었습니다.');
// 폼 초기화
document.getElementById('projectForm').reset();
// 목록 새로고침
await loadProjects();
displayProjectList();
} catch (error) {
console.error('프로젝트 생성 실패:', error);
alert('프로젝트 생성에 실패했습니다: ' + error.message);
}
});
// 프로젝트 목록 표시
function displayProjectList() {
const container = document.getElementById('projectsList');
container.innerHTML = '';
if (projects.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 프로젝트가 없습니다.</p>';
return;
}
// 활성 프로젝트와 비활성 프로젝트 분리
const activeProjects = projects.filter(p => p.is_active);
const inactiveProjects = projects.filter(p => !p.is_active);
console.log('전체 프로젝트:', projects.length, '개');
console.log('활성 프로젝트:', activeProjects.length, '개');
console.log('비활성 프로젝트:', inactiveProjects.length, '개');
console.log('비활성 프로젝트 목록:', inactiveProjects);
// 활성 프로젝트 표시
if (activeProjects.length > 0) {
const activeHeader = document.createElement('div');
activeHeader.className = 'mb-4';
activeHeader.innerHTML = '<h3 class="text-md font-semibold text-green-700"><i class="fas fa-check-circle mr-2"></i>활성 프로젝트 (' + activeProjects.length + '개)</h3>';
container.appendChild(activeHeader);
activeProjects.forEach(project => {
const div = document.createElement('div');
div.className = 'border border-green-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-green-50';
div.innerHTML = `
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="font-semibold text-gray-800">${project.job_no}</h3>
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">활성</span>
</div>
<p class="text-gray-600 mb-2">${project.project_name}</p>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
</div>
</div>
<div class="flex gap-2">
<button onclick="editProject(${project.id})" class="text-blue-600 hover:text-blue-800 p-2" title="수정">
<i class="fas fa-edit"></i>
</button>
<button onclick="toggleProjectStatus(${project.id})" class="text-orange-600 hover:text-orange-800 p-2" title="비활성화">
<i class="fas fa-pause"></i>
</button>
<button onclick="deleteProject(${project.id})" class="text-red-600 hover:text-red-800 p-2" title="삭제">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
container.appendChild(div);
});
}
// 비활성 프로젝트 표시
if (inactiveProjects.length > 0) {
const inactiveHeader = document.createElement('div');
inactiveHeader.className = 'mb-4 mt-6';
inactiveHeader.innerHTML = '<h3 class="text-md font-semibold text-gray-500"><i class="fas fa-pause-circle mr-2"></i>비활성 프로젝트 (' + inactiveProjects.length + '개)</h3>';
container.appendChild(inactiveHeader);
inactiveProjects.forEach(project => {
const div = document.createElement('div');
div.className = 'border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-gray-50 opacity-75';
div.innerHTML = `
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="font-semibold text-gray-600">${project.job_no}</h3>
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">비활성</span>
</div>
<p class="text-gray-500 mb-2">${project.project_name}</p>
<div class="flex items-center gap-4 text-sm text-gray-400">
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
</div>
</div>
<div class="flex gap-2">
<button onclick="toggleProjectStatus(${project.id})" class="text-green-600 hover:text-green-800 p-2" title="활성화">
<i class="fas fa-play"></i>
</button>
<button onclick="deleteProject(${project.id})" class="text-red-600 hover:text-red-800 p-2" title="완전 삭제">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
container.appendChild(div);
});
}
}
// 프로젝트 편집
async function editProject(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const newName = prompt('프로젝트 이름을 수정하세요:', project.project_name);
if (newName && newName.trim() && newName.trim() !== project.project_name) {
try {
// API를 통한 프로젝트 업데이트
await ProjectsAPI.update(projectId, {
project_name: newName.trim()
});
// 목록 새로고침
await loadProjects();
displayProjectList();
alert('프로젝트가 수정되었습니다.');
} catch (error) {
console.error('프로젝트 수정 실패:', error);
alert('프로젝트 수정에 실패했습니다: ' + error.message);
}
}
}
// 프로젝트 활성/비활성 토글
async function toggleProjectStatus(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const action = project.is_active ? '비활성화' : '활성화';
if (confirm(`"${project.job_no}" 프로젝트를 ${action}하시겠습니까?`)) {
try {
// API를 통한 프로젝트 상태 업데이트
await ProjectsAPI.update(projectId, {
is_active: !project.is_active
});
// 목록 새로고침
await loadProjects();
displayProjectList();
alert(`프로젝트가 ${action}되었습니다.`);
} catch (error) {
console.error('프로젝트 상태 변경 실패:', error);
alert('프로젝트 상태 변경에 실패했습니다: ' + error.message);
}
}
}
// 프로젝트 삭제 (완전 삭제)
async function deleteProject(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const confirmMessage = project.is_active
? `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
: `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
if (confirm(confirmMessage)) {
try {
// API를 통한 프로젝트 삭제
await ProjectsAPI.delete(projectId);
// 목록 새로고침
await loadProjects();
displayProjectList();
alert('프로젝트가 완전히 삭제되었습니다.');
} catch (error) {
console.error('프로젝트 삭제 실패:', error);
alert('프로젝트 삭제에 실패했습니다: ' + error.message);
}
}
}
// DOMContentLoaded 이벤트 제거 - API 스크립트 로드 후 checkAdminAccess() 호출됨
</script>
</body>
</html>