Files
Groupware/Project/Web/wwwroot/Project/index.html
ChiKyun Kim a11780f725 프로젝트 관리 시스템 구현 및 UI 개선
- 새로운 프로젝트 관리 화면 추가 (ProjectController, ProjectModel)
- 로그인된 사용자 기반 필터링 기능 구현
- 프로젝트 CRUD 기능 완성 (생성, 조회, 수정, 삭제)
- 컬럼 표시/숨김 기능으로 사용자 정의 뷰 지원
- 상태별 프로젝트 현황 대시보드
- 엑셀 내보내기 기능
- 반응형 디자인 및 glass-effect UI 적용
- 할일관리/근태관리와 일관된 레이아웃 구조

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-04 15:20:58 +09:00

1077 lines
59 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>
<script src="https://unpkg.com/feather-icons"></script>
<style>
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.animate-slide-up {
animation: slideUp 0.5s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.bg-primary-500 {
background-color: #3b82f6;
}
.bg-primary-600 {
background-color: #2563eb;
}
.hover\:bg-primary-600:hover {
background-color: #2563eb;
}
.bg-green-500 {
background-color: #10b981;
}
.bg-green-600 {
background-color: #059669;
}
.hover\:bg-green-600:hover {
background-color: #059669;
}
.bg-red-500 {
background-color: #ef4444;
}
.bg-red-600 {
background-color: #dc2626;
}
.hover\:bg-red-600:hover {
background-color: #dc2626;
}
select option {
background-color: #1f2937;
color: white;
padding: 8px;
}
select option:hover {
background-color: #374151;
}
select option:checked {
background-color: #3b82f6;
}
select option:focus {
background-color: #374151;
}
.hidden-column {
display: none;
}
.show-all-columns .hidden-column {
display: table-cell;
}
.column-toggle-btn {
position: relative;
}
.column-dropdown {
position: absolute;
top: 100%;
right: 0;
background: rgba(31, 41, 55, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px;
min-width: 200px;
z-index: 1000;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.column-dropdown label {
display: flex;
items-center: space-x-2;
padding: 6px 0;
cursor: pointer;
color: white;
font-size: 14px;
}
.column-dropdown input[type="checkbox"] {
margin-right: 8px;
}
.filter-section {
max-height: 500px;
transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
overflow: hidden;
opacity: 1;
}
.filter-section.hidden {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
}
.filter-toggle-btn {
transition: transform 0.3s ease;
}
.filter-toggle-btn.rotated {
transform: rotate(180deg);
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
<div class="container mx-auto px-4 py-8">
<!-- 프로젝트 목록 -->
<div class="glass-effect rounded-lg overflow-hidden animate-slide-up">
<div class="px-6 py-4 border-b border-white/10 flex items-center justify-between">
<h2 class="text-xl font-semibold text-white flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
프로젝트 목록
</h2>
<div class="flex space-x-3">
<button id="addProjectBtnHeader" class="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center text-sm" title="프로젝트 추가">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
프로젝트 추가
</button>
<button id="filterToggleBtn" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors flex items-center text-sm" title="필터 표시/숨김">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707L13 14v6a1 1 0 01-.707.293l-4-1A1 1 0 018 19v-5L0.293 7.293A1 1 0 010 6.586V4z"></path>
</svg>
필터
<svg class="w-4 h-4 ml-2 filter-toggle-btn transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
</div>
</div>
<!-- 상태별 카드 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 p-4 border-b border-white/10">
<div class="bg-white/10 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-blue-300 mb-1" id="progressCount">0</div>
<div class="text-sm text-white/60">진행</div>
</div>
<div class="bg-white/10 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-green-300 mb-1" id="completedCount">0</div>
<div class="text-sm text-white/60">완료</div>
</div>
<div class="bg-white/10 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-yellow-300 mb-1" id="waitingCount">0</div>
<div class="text-sm text-white/60">대기</div>
</div>
<div class="bg-white/10 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-red-300 mb-1" id="stoppedCount">0</div>
<div class="text-sm text-white/60">중단</div>
</div>
</div>
<!-- 필터 영역 -->
<div class="p-4 filter-section hidden border-b border-white/10">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- 좌측: 필터 컨트롤 -->
<div class="lg:col-span-2 space-y-3">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="block text-xs font-medium text-white/80 mb-1">상태</label>
<select id="statusFilter" class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
<option value="진행">진행</option>
<option value="완료">완료</option>
<option value="대기">대기</option>
<option value="중단">중단</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-white/80 mb-1">검색</label>
<input type="text" id="searchInput" placeholder="프로젝트명 검색..." class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
</div>
<div>
<label class="block text-xs font-medium text-white/80 mb-1">담당자</label>
<select id="managerFilter" class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
<option value="my">내 프로젝트</option>
<option value="all">전체 프로젝트</option>
</select>
</div>
</div>
</div>
<!-- 우측: 액션 버튼들 -->
<div class="flex flex-wrap gap-2 justify-end">
<div class="column-toggle-btn relative">
<button id="columnToggleBtn" class="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors text-sm" title="컬럼 표시/숨김">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2H9z"></path>
</svg>
컬럼 설정
</button>
<div id="columnDropdown" class="column-dropdown hidden">
<div class="text-sm font-medium text-white/80 mb-2">표시할 컬럼 선택</div>
<label><input type="checkbox" checked disabled> 상태</label>
<label><input type="checkbox" checked disabled> 프로젝트명</label>
<label><input type="checkbox" checked disabled> 자산번호</label>
<label><input type="checkbox" checked disabled> 장비모델</label>
<label><input type="checkbox" checked disabled> 우선순위</label>
<label><input type="checkbox" checked disabled> 요청국가</label>
<label><input type="checkbox" checked disabled> 프로젝트관리자</label>
<label><input type="checkbox" checked disabled> 시작일</label>
<label><input type="checkbox" checked disabled> 완료일</label>
<hr class="border-white/20 my-2">
<label><input type="checkbox" id="col-serial"> 시리얼번호</label>
<label><input type="checkbox" id="col-plant"> 요청공장</label>
<label><input type="checkbox" id="col-line"> 요청라인</label>
<label><input type="checkbox" id="col-package"> 요청부서패키지</label>
<label><input type="checkbox" id="col-staff"> 요청자</label>
<label><input type="checkbox" id="col-process"> 프로젝트공정</label>
<label><input type="checkbox" id="col-expire"> 만료일</label>
<label><input type="checkbox" id="col-delivery"> 출고일</label>
<label><input type="checkbox" id="col-design"> 설계담당</label>
<label><input type="checkbox" id="col-electric"> 전장담당</label>
<label><input type="checkbox" id="col-program"> 프로그램담당</label>
<label><input type="checkbox" id="col-budget-due"> 예산만기일</label>
<label><input type="checkbox" id="col-budget"> 예산</label>
<label><input type="checkbox" id="col-jasmin"> 웹관리번호</label>
</div>
</div>
<button id="exportBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors text-sm" title="엑셀 다운로드">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
엑셀 다운로드
</button>
<button id="clearFilterBtn" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg flex items-center transition-colors text-sm" title="필터 초기화">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
필터 초기화
</button>
</div>
</div>
</div>
</div>
<!-- 데이터 테이블 -->
<div class="px-4 py-3 bg-white/5">
<div class="text-sm text-white/70 font-medium">프로젝트 현황</div>
</div>
<div class="overflow-x-auto custom-scrollbar">
<table class="w-full divide-y divide-white/20">
<thead class="bg-white/10">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">상태</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 min-w-32">프로젝트명</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">자산번호</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-28">장비모델</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">우선순위</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">요청국가</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-28">프로젝트관리자</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">시작일</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">완료일</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-serial">시리얼번호</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20 hidden-column col-plant">요청공장</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20 hidden-column col-line">요청라인</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-28 hidden-column col-package">요청부서패키지</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20 hidden-column col-staff">요청자</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-process">프로젝트공정</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-expire">만료일</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-delivery">출고일</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-design">설계담당</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-electric">전장담당</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-program">프로그램담당</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24 hidden-column col-budget-due">예산만기일</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20 hidden-column col-budget">예산</th>
<th class="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider w-24 hidden-column col-jasmin">웹관리번호</th>
</tr>
</thead>
<tbody id="projectTableBody" class="divide-y divide-white/10">
<tr>
<td colspan="23" class="px-6 py-4 text-center text-white/60">데이터를 불러오는 중...</td>
</tr>
</tbody>
</table>
</div>
<!-- 페이징 -->
<div class="px-4 py-4 border-t border-white/10">
<div class="flex items-center justify-between">
<div class="text-sm text-white/60">
<span id="totalCount">0</span>개 중 <span id="currentRange">0-0</span>개 표시
</div>
<div class="flex items-center space-x-2">
<button id="prevPage" class="px-3 py-1 bg-white/20 hover:bg-white/30 text-white rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<div class="flex space-x-1">
<div id="pageNumbers" class="flex space-x-1"></div>
</div>
<button id="nextPage" class="px-3 py-1 bg-white/20 hover:bg-white/30 text-white rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- 프로젝트 추가/편집 모달 -->
<div id="projectModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="glass-effect rounded-lg w-full max-w-2xl animate-slide-up">
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h3 id="modalTitle" class="text-xl font-semibold">프로젝트 추가</h3>
<button onclick="closeProjectModal()" class="text-white/60 hover:text-white">
<i data-feather="x" class="w-6 h-6"></i>
</button>
</div>
<form id="projectForm">
<input type="hidden" id="projectIdx" name="idx">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-white/80 mb-2">프로젝트명 *</label>
<input type="text" id="projectName" name="name" required class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
</div>
<div>
<label class="block text-sm font-medium text-white/80 mb-2">부서</label>
<input type="text" id="projectProcess" name="process" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-white/80 mb-2">시작일</label>
<input type="date" id="projectSdate" name="sdate" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
</div>
<div>
<label class="block text-sm font-medium text-white/80 mb-2">종료일</label>
<input type="date" id="projectEdate" name="edate" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-white/80 mb-2">개발완료일</label>
<input type="date" id="projectDdate" name="ddate" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
</div>
<div>
<label class="block text-sm font-medium text-white/80 mb-2">운영개시일</label>
<input type="date" id="projectOdate" name="odate" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-white/80 mb-2">담당자</label>
<input type="text" id="projectUserManager" name="userManager" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
</div>
<div>
<label class="block text-sm font-medium text-white/80 mb-2">상태</label>
<select id="projectStatus" name="status" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent">
<option value="진행">진행</option>
<option value="완료">완료</option>
<option value="대기">대기</option>
<option value="중단">중단</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-white/80 mb-2">메모</label>
<textarea id="projectMemo" name="memo" rows="3" class="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"></textarea>
</div>
<div class="flex justify-between items-center">
<button type="button" id="deleteProjectBtn" onclick="deleteCurrentProject()" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md transition-colors hidden">
삭제
</button>
<div class="flex space-x-2">
<button type="button" onclick="closeProjectModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-md transition-colors">
취소
</button>
<button type="submit" class="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-md transition-colors">
저장
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 삭제 확인 모달 -->
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="glass-effect rounded-lg w-full max-w-md animate-slide-up">
<div class="p-6">
<div class="flex items-center mb-4">
<i data-feather="alert-triangle" class="w-8 h-8 text-red-400 mr-3"></i>
<h3 class="text-lg font-semibold">삭제 확인</h3>
</div>
<p class="text-white/80 mb-6">선택한 프로젝트를 삭제하시겠습니까?<br><span class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</span></p>
<div class="flex justify-end space-x-2">
<button onclick="cancelDelete()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-md transition-colors">
취소
</button>
<button onclick="confirmDelete()" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md transition-colors">
삭제
</button>
</div>
</div>
</div>
</div>
</div>
<script>
let projects = [];
let currentProjectIdx = null;
let sortColumn = 'name';
let sortDirection = 'asc';
let currentUser = '';
let currentPage = 1;
let itemsPerPage = 10;
let filteredProjects = [];
// 초기화
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// 네비게이션 클래스
class CommonNavigation {
constructor(currentPage = '') {
this.currentPage = currentPage;
this.createNavigation();
}
async createNavigation() {
try {
const response = await fetch('/Common/GetNavigationMenu');
const data = await response.json();
if (data.Success && data.Data) {
this.createNavigationFromData(data.Data);
} else {
this.createFallbackNavigation();
}
} catch (error) {
console.error('Navigation initialization failed:', error);
this.createFallbackNavigation();
}
}
createNavigationFromData(menuItems) {
const nav = document.createElement('nav');
nav.className = 'glass-effect border-b border-white/10';
nav.innerHTML = this.getNavigationHTML(menuItems);
document.body.insertBefore(nav, document.body.firstChild);
}
createFallbackNavigation() {
this.createNavigation();
}
createNavigation() {
const nav = document.createElement('nav');
nav.className = 'glass-effect border-b border-white/10';
nav.innerHTML = this.getNavigationHTML();
document.body.insertBefore(nav, document.body.firstChild);
}
getNavigationHTML(menuItems = null) {
if (!menuItems) {
menuItems = [
{ key: 'dashboard', title: '대시보드', url: '/Dashboard/', icon: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z' },
{ key: 'common', title: '공용코드', url: '/Common', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
{ key: 'jobreport', title: '업무일지', url: '/Jobreport/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2' },
{ key: 'kuntae', title: '근태관리', url: '/Kuntae/', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
{ key: 'todo', title: '할일관리', url: '/Todo/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4' },
{ key: 'project', title: '프로젝트', url: '/Project/', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' }
];
}
return `
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
<span class="text-xl font-bold text-white">GroupWare</span>
</div>
<nav class="hidden md:flex space-x-1">
${menuItems.map(item => `
<a href="${item.url}" class="px-3 py-2 rounded-md text-sm font-medium transition-colors ${
this.currentPage === item.key
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
}">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
</svg>
${item.title}
</a>
`).join('')}
</nav>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-white/60">
<span id="currentUser">사용자</span>
</div>
</div>
</div>
</div>
`;
}
}
function initNavigation(currentPage = '') {
try {
new CommonNavigation(currentPage);
} catch (error) {
console.error('Navigation initialization failed:', error);
new CommonNavigation(currentPage);
}
}
function initializeApp() {
initNavigation('project');
getCurrentUser();
loadProjects();
setupEventListeners();
}
function getCurrentUser() {
// 현재 로그인된 사용자 정보 가져오기
fetch('/Common/GetCurrentUser')
.then(response => response.json())
.then(data => {
if (data.Success && data.Data) {
currentUser = data.Data.userName || data.Data.name || '사용자';
document.getElementById('currentUser').textContent = currentUser;
} else {
currentUser = '사용자';
document.getElementById('currentUser').textContent = currentUser;
}
})
.catch(error => {
console.error('Error getting current user:', error);
currentUser = '사용자';
document.getElementById('currentUser').textContent = currentUser;
});
}
function setupEventListeners() {
// 필터 이벤트
document.getElementById('statusFilter').addEventListener('change', loadProjects);
document.getElementById('searchInput').addEventListener('input', filterData);
document.getElementById('managerFilter').addEventListener('change', loadProjects);
// 버튼 이벤트
document.getElementById('addProjectBtnHeader').addEventListener('click', showAddProjectModal);
document.getElementById('exportBtn').addEventListener('click', exportToExcel);
document.getElementById('clearFilterBtn').addEventListener('click', clearFilters);
// 컬럼 토글 이벤트
document.getElementById('columnToggleBtn').addEventListener('click', toggleColumnDropdown);
setupColumnToggles();
// 필터 토글 이벤트
document.getElementById('filterToggleBtn').addEventListener('click', toggleFilterSection);
// 페이징 이벤트
document.getElementById('prevPage').addEventListener('click', () => changePage(currentPage - 1));
document.getElementById('nextPage').addEventListener('click', () => changePage(currentPage + 1));
// 폼 제출
document.getElementById('projectForm').addEventListener('submit', handleFormSubmit);
// 드롭다운 외부 클릭 시 닫기
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('columnDropdown');
const toggleBtn = document.getElementById('columnToggleBtn');
if (!dropdown.contains(event.target) && !toggleBtn.contains(event.target)) {
dropdown.classList.add('hidden');
}
});
}
function loadProjects() {
const status = document.getElementById('statusFilter').value;
const userFilter = document.getElementById('managerFilter').value;
fetch(`/Project/GetProjects?status=${status}&userFilter=${userFilter}`)
.then(response => response.json())
.then(data => {
if (!data.Success) {
console.error('Error:', data.Message);
document.getElementById('projectTableBody').innerHTML = '<tr><td colspan="23" class="px-6 py-4 text-center text-red-400">데이터를 불러오는데 실패했습니다.</td></tr>';
return;
}
projects = data.Data || [];
if (data.CurrentUser) {
currentUser = data.CurrentUser;
document.getElementById('currentUser').textContent = currentUser;
}
currentPage = 1;
filterData();
updateStatusCounts();
})
.catch(error => {
console.error('Error:', error);
document.getElementById('projectTableBody').innerHTML = '<tr><td colspan="23" class="px-6 py-4 text-center text-red-400">데이터를 불러오는데 실패했습니다.</td></tr>';
});
}
function updateStatusCounts() {
const counts = {
'진행': 0,
'완료': 0,
'대기': 0,
'중단': 0
};
projects.forEach(project => {
const status = project.상태 || '진행';
counts[status] = (counts[status] || 0) + 1;
});
document.getElementById('progressCount').textContent = counts['진행'] || 0;
document.getElementById('completedCount').textContent = counts['완료'] || 0;
document.getElementById('waitingCount').textContent = counts['대기'] || 0;
document.getElementById('stoppedCount').textContent = counts['중단'] || 0;
}
function renderProjects() {
const tbody = document.getElementById('projectTableBody');
if (filteredProjects.length === 0) {
tbody.innerHTML = '<tr><td colspan="23" class="px-6 py-4 text-center text-white/60">데이터가 없습니다.</td></tr>';
updatePagination();
return;
}
// 페이징 처리
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedProjects = filteredProjects.slice(startIndex, endIndex);
tbody.innerHTML = paginatedProjects.map(project => `
<tr onclick="editProject(${project.idx})" class="hover:bg-white/10 cursor-pointer transition-colors">
<td class="px-4 py-4 text-sm border-r border-white/20">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusClass(project.상태)}">
${project.상태 || '진행'}
</span>
</td>
<td class="px-4 py-4 text-sm border-r border-white/20 font-medium">${project.프로젝트명 || ''}</td>
<td class="px-4 py-4 text-sm border-r border-white/20">${project.자산번호 || ''}</td>
<td class="px-4 py-4 text-sm border-r border-white/20">${project.장비모델 || ''}</td>
<td class="px-4 py-4 text-sm border-r border-white/20">${project.우선순위 || ''}</td>
<td class="px-4 py-4 text-sm border-r border-white/20">${project.요청국가 || ''}</td>
<td class="px-4 py-4 text-sm border-r border-white/20">${project.프로젝트관리자 || ''}</td>
<td class="px-4 py-4 text-sm border-r border-white/20">${formatDateToMonthDay(project.시작일)}</td>
<td class="px-4 py-4 text-sm border-r border-white/20">${formatDateToMonthDay(project.완료일)}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-serial">${project.시리얼번호 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-plant">${project.요청공장 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-line">${project.요청라인 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-package">${project.요청부서패키지 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-staff">${project.요청자 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-process">${project.프로젝트공정 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-expire">${formatDateToMonthDay(project.만료일)}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-delivery">${formatDateToMonthDay(project.출고일)}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-design">${project.설계담당 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-electric">${project.전장담당 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-program">${project.프로그램담당 || ''}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-budget-due">${formatDateToMonthDay(project.예산만기일)}</td>
<td class="px-3 py-4 text-sm border-r border-white/20 hidden-column col-budget">${project.예산 || ''}</td>
<td class="px-3 py-4 text-sm hidden-column col-jasmin">${project.웹관리번호 ? `<a href="https://scwa.amkor.co.kr/jasmine/view/${project.웹관리번호}" target="_blank" class="text-blue-300 hover:text-blue-200 underline">${project.웹관리번호}</a>` : ''}</td>
</tr>
`).join('');
updatePagination();
}
function filterProjects() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
return projects.filter(project => {
const matchesSearch = !searchTerm ||
(project.프로젝트명 && project.프로젝트명.toLowerCase().includes(searchTerm)) ||
(project.프로젝트공정 && project.프로젝트공정.toLowerCase().includes(searchTerm)) ||
(project.자산번호 && project.자산번호.toLowerCase().includes(searchTerm)) ||
(project.장비모델 && project.장비모델.toLowerCase().includes(searchTerm)) ||
(project.시리얼번호 && project.시리얼번호.toLowerCase().includes(searchTerm)) ||
(project.프로젝트관리자 && project.프로젝트관리자.toLowerCase().includes(searchTerm)) ||
(project.설계담당 && project.설계담당.toLowerCase().includes(searchTerm)) ||
(project.전장담당 && project.전장담당.toLowerCase().includes(searchTerm)) ||
(project.프로그램담당 && project.프로그램담당.toLowerCase().includes(searchTerm));
return matchesSearch;
});
}
function filterData() {
filteredProjects = filterProjects();
currentPage = 1;
renderProjects();
updatePagination();
}
function updatePagination() {
const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage + 1;
const endIndex = Math.min(currentPage * itemsPerPage, filteredProjects.length);
// 총 개수와 현재 범위 업데이트
document.getElementById('totalCount').textContent = filteredProjects.length;
document.getElementById('currentRange').textContent = `${startIndex}-${endIndex}`;
// 이전/다음 버튼 상태 업데이트
document.getElementById('prevPage').disabled = currentPage <= 1;
document.getElementById('nextPage').disabled = currentPage >= totalPages;
// 페이지 번호 생성
const pageNumbersContainer = document.getElementById('pageNumbers');
pageNumbersContainer.innerHTML = '';
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
const pageButton = document.createElement('button');
pageButton.className = `px-3 py-1 rounded-md text-sm transition-colors ${
i === currentPage
? 'bg-primary-500 text-white'
: 'bg-white/20 hover:bg-white/30 text-white'
}`;
pageButton.textContent = i;
pageButton.addEventListener('click', () => changePage(i));
pageNumbersContainer.appendChild(pageButton);
}
}
function changePage(page) {
const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
if (page >= 1 && page <= totalPages) {
currentPage = page;
renderProjects();
}
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '진행';
document.getElementById('managerFilter').value = 'my';
loadProjects();
}
function showAddProjectModal() {
currentProjectIdx = null;
document.getElementById('modalTitle').textContent = '프로젝트 추가';
document.getElementById('projectForm').reset();
document.getElementById('deleteProjectBtn').classList.add('hidden');
document.getElementById('projectModal').classList.remove('hidden');
}
function editProject(idx) {
currentProjectIdx = idx;
fetch(`/Project/GetProject?id=${idx}`)
.then(response => response.json())
.then(data => {
if (!data.Success) {
alert('프로젝트 정보를 불러오는데 실패했습니다: ' + data.Message);
return;
}
const project = data.Data;
document.getElementById('modalTitle').textContent = '프로젝트 편집';
document.getElementById('projectIdx').value = project.idx;
document.getElementById('projectName').value = project.프로젝트명 || '';
document.getElementById('projectProcess').value = project.프로젝트공정 || '';
document.getElementById('projectSdate').value = project.시작일 || '';
document.getElementById('projectEdate').value = project.완료일 || '';
document.getElementById('projectDdate').value = project.만료일 || '';
document.getElementById('projectOdate').value = project.출고일 || '';
document.getElementById('projectUserManager').value = project.프로젝트관리자 || '';
document.getElementById('projectStatus').value = project.상태 || '진행';
document.getElementById('projectMemo').value = project.memo || '';
document.getElementById('deleteProjectBtn').classList.remove('hidden');
document.getElementById('projectModal').classList.remove('hidden');
})
.catch(error => {
console.error('Error:', error);
alert('프로젝트 정보를 불러오는데 실패했습니다.');
});
}
function closeProjectModal() {
document.getElementById('projectModal').classList.add('hidden');
document.getElementById('deleteModal').classList.add('hidden');
}
function handleFormSubmit(e) {
e.preventDefault();
const projectData = {
idx: currentProjectIdx ? parseInt(document.getElementById('projectIdx').value) : 0,
name: document.getElementById('projectName').value,
process: document.getElementById('projectProcess').value,
sdate: document.getElementById('projectSdate').value || null,
edate: document.getElementById('projectEdate').value || null,
ddate: document.getElementById('projectDdate').value || null,
odate: document.getElementById('projectOdate').value || null,
userManager: document.getElementById('projectUserManager').value,
status: document.getElementById('projectStatus').value,
memo: document.getElementById('projectMemo').value
};
const url = currentProjectIdx ? '/Project/UpdateProject' : '/Project/CreateProject';
const method = currentProjectIdx ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(projectData)
})
.then(response => response.json())
.then(data => {
if (!data.Success) {
alert('오류: ' + data.Message);
} else {
alert(data.Message);
closeProjectModal();
loadProjects();
}
})
.catch(error => {
console.error('Error:', error);
alert('저장 중 오류가 발생했습니다.');
});
}
function deleteCurrentProject() {
document.getElementById('deleteModal').classList.remove('hidden');
}
function cancelDelete() {
document.getElementById('deleteModal').classList.add('hidden');
}
function confirmDelete() {
if (!currentProjectIdx) return;
fetch(`/Project/DeleteProject?id=${currentProjectIdx}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (!data.Success) {
alert('오류: ' + data.Message);
} else {
alert(data.Message);
closeProjectModal();
loadProjects();
}
})
.catch(error => {
console.error('Error:', error);
alert('삭제 중 오류가 발생했습니다.');
});
}
function getStatusClass(status) {
switch(status) {
case '진행':
return 'bg-blue-500/20 text-blue-300';
case '완료':
return 'bg-green-500/20 text-green-300';
case '대기':
return 'bg-yellow-500/20 text-yellow-300';
case '중단':
return 'bg-red-500/20 text-red-300';
default:
return 'bg-gray-500/20 text-gray-300';
}
}
function exportToExcel() {
const filteredProjects = filterProjects();
if (filteredProjects.length === 0) {
alert('내보낼 데이터가 없습니다.');
return;
}
// CSV 형식으로 데이터 변환
const headers = ['상태', '자산번호', '장비모델', '시리얼번호', '우선순위', '요청국가', '요청공장', '요청라인', '요청부서패키지', '요청자', '프로젝트공정', '시작일', '완료일', '만료일', '출고일', '프로젝트명', '프로젝트관리자', '설계담당', '전장담당', '프로그램담당', '예산만기일', '예산', '웹관리번호'];
const csvContent = [
headers.join(','),
...filteredProjects.map(project => [
project.상태 || '',
project.자산번호 || '',
project.장비모델 || '',
project.시리얼번호 || '',
project.우선순위 || '',
project.요청국가 || '',
project.요청공장 || '',
project.요청라인 || '',
project.요청부서패키지 || '',
project.요청자 || '',
project.프로젝트공정 || '',
project.시작일 || '',
project.완료일 || '',
project.만료일 || '',
project.출고일 || '',
project.프로젝트명 || '',
project.프로젝트관리자 || '',
project.설계담당 || '',
project.전장담당 || '',
project.프로그램담당 || '',
project.예산만기일 || '',
project.예산 || '',
project.웹관리번호 || ''
].join(','))
].join('\n');
// 파일 다운로드
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `프로젝트목록_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function toggleColumnDropdown(event) {
event.stopPropagation();
const dropdown = document.getElementById('columnDropdown');
dropdown.classList.toggle('hidden');
}
function setupColumnToggles() {
const columnMappings = {
'col-serial': 'col-serial',
'col-plant': 'col-plant',
'col-line': 'col-line',
'col-package': 'col-package',
'col-staff': 'col-staff',
'col-process': 'col-process',
'col-expire': 'col-expire',
'col-delivery': 'col-delivery',
'col-design': 'col-design',
'col-electric': 'col-electric',
'col-program': 'col-program',
'col-budget-due': 'col-budget-due',
'col-budget': 'col-budget',
'col-jasmin': 'col-jasmin'
};
Object.keys(columnMappings).forEach(checkboxId => {
const checkbox = document.getElementById(checkboxId);
if (checkbox) {
checkbox.addEventListener('change', function() {
toggleColumn(columnMappings[checkboxId], this.checked);
});
}
});
}
function toggleColumn(columnClass, show) {
const elements = document.querySelectorAll('.' + columnClass);
elements.forEach(element => {
if (show) {
element.classList.remove('hidden-column');
} else {
element.classList.add('hidden-column');
}
});
}
function formatDateToMonthDay(dateString) {
if (!dateString) return '';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return dateString; // 유효하지 않은 날짜면 원본 반환
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${month}-${day}`;
} catch (error) {
return dateString; // 에러 발생시 원본 반환
}
}
function toggleFilterSection() {
const filterSection = document.querySelector('.filter-section');
const toggleIcon = document.querySelector('.filter-toggle-btn');
filterSection.classList.toggle('hidden');
toggleIcon.classList.toggle('rotated');
}
</script>
</body>
</html>