- 목록 관리 페이지에 고급 필터링 시스템 추가 - 프로젝트별, 검토상태별, 날짜별 필터링 - 검토 완료/필요 항목 시각적 구분 및 정렬 - 해결 시간 입력 + 확인 버튼으로 검토 완료 처리 - 부적합 조회 페이지에 동일한 필터링 기능 적용 - 검토 상태에 따른 카드 스타일링 (음영 처리) - JavaScript 템플릿 리터럴 오류 수정 - 보고서 페이지 프로젝트별 분석 기능 추가 - 프로젝트 선택 드롭다운 추가 - 총 작업 공수를 프로젝트별 일일공수 데이터로 계산 - 부적합 처리 시간, 카테고리 분석, 상세 목록 모두 프로젝트별 필터링 - localStorage 키 이름 통일 (daily-work-data)
351 lines
15 KiB
HTML
351 lines
15 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>
|
|
<!-- 헤더 -->
|
|
<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">
|
|
<!-- 프로젝트 생성 섹션 -->
|
|
<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>
|
|
|
|
<script>
|
|
// 사용자 확인 (관리자만 접근 가능)
|
|
const currentUserData = localStorage.getItem('currentUser');
|
|
if (!currentUserData) {
|
|
alert('로그인이 필요합니다.');
|
|
window.location.href = 'index.html';
|
|
}
|
|
|
|
let currentUser;
|
|
try {
|
|
// JSON 파싱 시도
|
|
currentUser = JSON.parse(currentUserData);
|
|
} catch (e) {
|
|
// JSON이 아니면 문자열로 처리 (기존 방식 호환)
|
|
currentUser = { username: currentUserData };
|
|
}
|
|
|
|
const username = currentUser.username || currentUser;
|
|
const isAdmin = username === 'hyungi' || currentUser.role === 'admin';
|
|
|
|
if (!isAdmin) {
|
|
alert('관리자만 접근 가능합니다.');
|
|
window.location.href = 'index.html';
|
|
}
|
|
|
|
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
|
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
|
|
|
let projects = [];
|
|
|
|
// 프로젝트 데이터 로드
|
|
function loadProjects() {
|
|
const saved = localStorage.getItem('work-report-projects');
|
|
if (saved) {
|
|
projects = JSON.parse(saved);
|
|
}
|
|
displayProjectList();
|
|
}
|
|
|
|
// 프로젝트 데이터 저장
|
|
function saveProjects() {
|
|
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
|
}
|
|
|
|
// 프로젝트 생성 폼 처리
|
|
document.getElementById('projectForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
const jobNo = document.getElementById('jobNo').value.trim();
|
|
const projectName = document.getElementById('projectName').value.trim();
|
|
|
|
// 중복 Job No. 확인
|
|
if (projects.some(p => p.jobNo === jobNo)) {
|
|
alert('이미 존재하는 Job No.입니다.');
|
|
return;
|
|
}
|
|
|
|
// 프로젝트 생성
|
|
const newProject = {
|
|
id: Date.now(),
|
|
jobNo: jobNo,
|
|
projectName: projectName,
|
|
createdBy: 'hyungi',
|
|
createdByName: '관리자',
|
|
createdAt: new Date().toISOString(),
|
|
isActive: true
|
|
};
|
|
|
|
projects.push(newProject);
|
|
saveProjects();
|
|
|
|
// 성공 메시지
|
|
alert('프로젝트가 생성되었습니다.');
|
|
|
|
// 폼 초기화
|
|
document.getElementById('projectForm').reset();
|
|
|
|
// 목록 새로고침
|
|
displayProjectList();
|
|
});
|
|
|
|
// 프로젝트 목록 표시
|
|
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.isActive);
|
|
const inactiveProjects = projects.filter(p => !p.isActive);
|
|
|
|
// 활성 프로젝트 표시
|
|
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.jobNo}</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.projectName}</p>
|
|
<div class="flex items-center gap-4 text-sm text-gray-500">
|
|
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
|
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).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.jobNo}</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.projectName}</p>
|
|
<div class="flex items-center gap-4 text-sm text-gray-400">
|
|
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
|
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).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);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 프로젝트 편집
|
|
function editProject(projectId) {
|
|
const project = projects.find(p => p.id === projectId);
|
|
if (!project) return;
|
|
|
|
const newName = prompt('프로젝트 이름을 수정하세요:', project.projectName);
|
|
if (newName && newName.trim() && newName.trim() !== project.projectName) {
|
|
project.projectName = newName.trim();
|
|
saveProjects();
|
|
displayProjectList();
|
|
alert('프로젝트가 수정되었습니다.');
|
|
}
|
|
}
|
|
|
|
// 프로젝트 활성/비활성 토글
|
|
function toggleProjectStatus(projectId) {
|
|
const project = projects.find(p => p.id === projectId);
|
|
if (!project) return;
|
|
|
|
const action = project.isActive ? '비활성화' : '활성화';
|
|
if (confirm(`"${project.jobNo}" 프로젝트를 ${action}하시겠습니까?`)) {
|
|
project.isActive = !project.isActive;
|
|
saveProjects();
|
|
displayProjectList();
|
|
alert(`프로젝트가 ${action}되었습니다.`);
|
|
}
|
|
}
|
|
|
|
// 프로젝트 삭제 (완전 삭제)
|
|
function deleteProject(projectId) {
|
|
const project = projects.find(p => p.id === projectId);
|
|
if (!project) return;
|
|
|
|
const confirmMessage = project.isActive
|
|
? `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
|
|
: `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
|
|
|
|
if (confirm(confirmMessage)) {
|
|
const index = projects.findIndex(p => p.id === projectId);
|
|
if (index > -1) {
|
|
projects.splice(index, 1);
|
|
saveProjects();
|
|
displayProjectList();
|
|
alert('프로젝트가 완전히 삭제되었습니다.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 페이지 로드 시 프로젝트 목록 로드
|
|
loadProjects();
|
|
</script>
|
|
</body>
|
|
</html>
|