- 모든 HTML 파일에서 cdn.tailwindcss.com 대신 /lib/css/tailwind.min.css 사용 - 중복되는 인라인 스타일을 /css/common.css로 통합 - 외부 네트워크 의존성 제거로 페이지 로딩 지연 해결 변경된 파일: - DashBoard/index.html - Todo/index.html - Jobreport/index.html - Kuntae/index.html - Common.html - login.html - Project/index.html 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1751 lines
89 KiB
HTML
1751 lines
89 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="0">
|
|
<title>업무일지</title>
|
|
<link href="/lib/css/tailwind.min.css" rel="stylesheet">
|
|
<link href="/css/common.css" rel="stylesheet">
|
|
<style>
|
|
/* 스크롤바 스타일링 */
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: var(--scrollbar-width, 16px);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
border-radius: 8px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
/* 셀렉트 박스 옵션 스타일링 */
|
|
select option {
|
|
background-color: #1f2937 !important;
|
|
color: #f9fafb !important;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
select option:hover {
|
|
background-color: #374151 !important;
|
|
}
|
|
|
|
select option:checked {
|
|
background-color: #3b82f6 !important;
|
|
}
|
|
|
|
select option:focus {
|
|
background-color: #374151 !important;
|
|
}
|
|
</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="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>
|
|
<div>
|
|
<p class="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
|
<p class="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 animate-slide-up">
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-primary-500/20 rounded-lg">
|
|
<i data-feather="calendar" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">총 업무일수</p>
|
|
<p id="totalDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-success-500/20 rounded-lg">
|
|
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">오늘 근무시간</p>
|
|
<p id="todayHours" class="text-2xl font-bold text-green-300">0h</p>
|
|
<p id="todayProgress" class="text-sm text-white/60">(목표 8시간의 0%)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-warning-500/20 rounded-lg">
|
|
<i data-feather="zap" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">총 초과근무</p>
|
|
<p id="totalOT" class="text-2xl font-bold text-white">0h</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-purple-500/20 rounded-lg">
|
|
<i data-feather="folder" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">진행중 프로젝트</p>
|
|
<p id="activeProjects" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 필터 및 검색 -->
|
|
<div class="glass-effect rounded-lg mb-6 animate-slide-up">
|
|
<div class="p-5">
|
|
<!-- 상단: 헤더 및 액션 버튼들 -->
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<button id="toggleFilterBtn" class="text-sm font-semibold text-white flex items-center hover:text-white/80 transition-colors">
|
|
<i data-feather="filter" class="w-4 h-4 mr-2"></i>
|
|
필터 및 검색
|
|
<i id="filterToggleIcon" data-feather="chevron-down" class="w-4 h-4 ml-2 transform rotate-180 transition-transform"></i>
|
|
</button>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button id="clearFilterBtn" class="bg-white/10 hover:bg-white/20 text-white text-sm px-3 py-1.5 rounded-md flex items-center transition-colors" title="필터 초기화">
|
|
<i data-feather="refresh-cw" class="w-3.5 h-3.5 mr-1.5"></i>
|
|
초기화
|
|
</button>
|
|
<button id="exportBtn" class="bg-success-500 hover:bg-success-600 text-white text-sm px-3 py-1.5 rounded-md flex items-center transition-colors" title="엑셀 다운로드">
|
|
<i data-feather="download" class="w-3.5 h-3.5 mr-1.5"></i>
|
|
엑셀
|
|
</button>
|
|
<button id="addJobBtn" class="bg-primary-500 hover:bg-primary-600 text-white text-sm px-3 py-1.5 rounded-md flex items-center transition-colors" title="업무일지 추가">
|
|
<i data-feather="plus" class="w-3.5 h-3.5 mr-1.5"></i>
|
|
추가
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 필터 컨트롤 -->
|
|
<div id="filterContent" class="space-y-3 mt-4 pt-4 border-t border-white/10 hidden">
|
|
<!-- 첫번째 줄: 조회기간, 상태, 타입 -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
<div class="sm:col-span-1 lg:col-span-2">
|
|
<label class="block text-xs font-medium text-white/70 mb-1.5">조회기간</label>
|
|
<div class="flex items-center space-x-2">
|
|
<input type="date" id="startDate" class="flex-1 bg-white/10 border border-white/20 rounded-md px-3 py-1.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-transparent">
|
|
<span class="text-white/50 text-sm">~</span>
|
|
<input type="date" id="endDate" class="flex-1 bg-white/10 border border-white/20 rounded-md px-3 py-1.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-white/70 mb-1.5">상태</label>
|
|
<select id="statusFilter" class="w-full bg-white/10 border border-white/20 rounded-md px-3 py-1.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-transparent">
|
|
<option value="">전체</option>
|
|
<option value="진행중">진행중</option>
|
|
<option value="완료">완료</option>
|
|
<option value="대기">대기</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-white/70 mb-1.5">타입</label>
|
|
<select id="typeFilter" class="w-full bg-white/10 border border-white/20 rounded-md px-3 py-1.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-transparent">
|
|
<option value="">전체</option>
|
|
<option value="개발">개발</option>
|
|
<option value="유지보수">유지보수</option>
|
|
<option value="분석">분석</option>
|
|
<option value="테스트">테스트</option>
|
|
<option value="문서작업">문서작업</option>
|
|
<option value="회의">회의</option>
|
|
<option value="기타">기타</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 두번째 줄: 사용자, 프로젝트, 검색 -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-white/70 mb-1.5">사용자</label>
|
|
<select id="userFilter" class="w-full bg-white/10 border border-white/20 rounded-md px-3 py-1.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-transparent">
|
|
<option value="">전체</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-white/70 mb-1.5">프로젝트</label>
|
|
<select id="projectFilter" class="w-full bg-white/10 border border-white/20 rounded-md px-3 py-1.5 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-transparent">
|
|
<option value="">전체</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-white/70 mb-1.5">검색</label>
|
|
<div class="relative">
|
|
<i data-feather="search" class="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40"></i>
|
|
<input type="text" id="searchInput" placeholder="업무 내용 검색..." class="w-full bg-white/10 border border-white/20 rounded-md pl-9 pr-3 py-1.5 text-white placeholder-white/40 text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 데이터 테이블 -->
|
|
<div class="glass-effect rounded-lg overflow-hidden animate-slide-up custom-scrollbar">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-white/20">
|
|
<thead class="bg-white/10">
|
|
<tr>
|
|
<th class="px-6 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="pdate">
|
|
날짜 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
|
</th>
|
|
<th class="px-6 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="status">
|
|
상태 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
|
</th>
|
|
<th class="px-6 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="hrs">
|
|
시간 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
|
</th>
|
|
<th class="px-6 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="projectName">
|
|
프로젝트명 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
|
</th>
|
|
<th class="px-6 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider">업무내용</th>
|
|
<th class="px-6 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="requestpart">
|
|
요청부서 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
|
</th>
|
|
<th class="px-6 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors hidden" data-sort="type">
|
|
타입 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="jobTableBody" class="divide-y divide-white/10">
|
|
<!-- 데이터가 여기에 동적으로 로드됩니다 -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- 로딩 상태 -->
|
|
<div id="loadingState" class="hidden p-8 text-center">
|
|
<div class="inline-flex items-center">
|
|
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span class="text-white/80">데이터를 불러오는 중...</span>
|
|
</div>
|
|
</div>
|
|
<!-- 빈 상태 -->
|
|
<div id="emptyState" class="hidden p-8 text-center">
|
|
<i data-feather="inbox" class="w-12 h-12 text-white/60 mx-auto mb-4"></i>
|
|
<p class="text-white/70">업무일지 데이터가 없습니다.</p>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 페이지네이션 -->
|
|
<div id="pagination" class="mt-6 flex items-center justify-between glass-effect rounded-lg p-4">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-sm text-white/80">페이지당 행 수:</span>
|
|
<select id="pageSize" class="bg-white/20 border border-white/30 rounded-md px-2 py-1 text-sm text-white">
|
|
<option value="10">10</option>
|
|
<option value="25" selected>25</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<button id="prevPage" class="px-3 py-1 border border-white/30 rounded-md text-sm text-white hover:bg-white/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
이전
|
|
</button>
|
|
<span id="pageInfo" class="text-sm text-white/80">1 / 1</span>
|
|
<button id="nextPage" class="px-3 py-1 border border-white/30 rounded-md text-sm text-white hover:bg-white/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
다음
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 상세 모달 -->
|
|
<div id="detailModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden z-50">
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="glass-effect rounded-2xl w-full max-w-6xl 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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
</svg>
|
|
업무일지 편집
|
|
</h2>
|
|
<button id="closeModal" class="text-white/70 hover:text-white transition-colors">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 모달 내용 -->
|
|
<div class="p-6">
|
|
<div id="modalContent" class="space-y-4">
|
|
<!-- 모달 내용이 여기에 동적으로 로드됩니다 -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모달 푸터는 동적으로 생성됨 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 업무형태 선택 모달 -->
|
|
<div id="jobTypeModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden z-50">
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="glass-effect rounded-2xl w-full max-w-4xl animate-slide-up">
|
|
<!-- 모달 헤더 -->
|
|
<div class="p-6 border-b border-white/10">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-bold text-white flex items-center">
|
|
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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"></path>
|
|
</svg>
|
|
업무형태 선택
|
|
</h2>
|
|
<button id="closeJobTypeModal" class="text-white/70 hover:text-white transition-colors">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모달 내용 -->
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<!-- 프로세스 목록 -->
|
|
<div>
|
|
<h3 class="text-white font-semibold mb-3">프로세스</h3>
|
|
<div id="processListForJobType" class="space-y-1 max-h-96 overflow-y-auto">
|
|
<!-- 프로세스 목록이 여기에 로드됩니다 -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 분류 목록 -->
|
|
<div>
|
|
<h3 class="text-white font-semibold mb-3">분류</h3>
|
|
<div id="groupList" class="space-y-1 max-h-96 overflow-y-auto">
|
|
<!-- 분류 목록이 여기에 로드됩니다 -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 항목 목록 -->
|
|
<div>
|
|
<h3 class="text-white font-semibold mb-3">항목</h3>
|
|
<div id="itemList" class="space-y-1 max-h-96 overflow-y-auto">
|
|
<!-- 항목 목록이 여기에 로드됩니다 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모달 푸터 -->
|
|
<div class="px-6 py-4 border-t border-white/10 flex justify-end space-x-3 bg-black/10 rounded-b-2xl">
|
|
<button type="button" onclick="closeJobTypeModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button type="button" onclick="confirmJobTypeSelection()" class="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors">
|
|
선택
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 공통 네비게이션 -->
|
|
<script src="/js/common-navigation.js"></script>
|
|
|
|
<script>
|
|
// Machine HostObject 초기화
|
|
const machine = window.chrome.webview.hostObjects.machine;
|
|
|
|
// 전역 변수
|
|
let jobData = [];
|
|
let filteredData = [];
|
|
let statusCodes = []; // 공용코드 12번 (상태 코드)
|
|
let requestDeptCodes = []; // 공용코드 13번 (요청부서)
|
|
let packageCodes = []; // 공용코드 14번 (패키지)
|
|
let jobTypeCodes = []; // 공용코드 15번 (업무형태 - 트리구조)
|
|
let processCodes = []; // 공용코드 16번 (프로세스)
|
|
let currentPage = 1;
|
|
let pageSize = 25;
|
|
let sortColumn = 'pdate';
|
|
let sortDirection = 'desc';
|
|
|
|
// 초기화
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// 네비게이션 초기화
|
|
initNavigation('jobreport');
|
|
initializeApp();
|
|
loadJobData();
|
|
|
|
// 새로운 위치의 버튼들에 이벤트 리스너 추가
|
|
setTimeout(() => {
|
|
const addJobBtn = document.getElementById('addJobBtn');
|
|
const exportBtn = document.getElementById('exportBtn');
|
|
const toggleFilterBtn = document.getElementById('toggleFilterBtn');
|
|
|
|
if (addJobBtn) {
|
|
addJobBtn.addEventListener('click', showAddJobModal);
|
|
}
|
|
if (exportBtn) {
|
|
exportBtn.addEventListener('click', exportToExcel);
|
|
}
|
|
if (toggleFilterBtn) {
|
|
toggleFilterBtn.addEventListener('click', toggleFilter);
|
|
}
|
|
}, 100);
|
|
|
|
// ESC 키로 모달 닫기
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape' || event.key === 'Esc') {
|
|
const modal = document.getElementById('detailModal');
|
|
if (modal && !modal.classList.contains('hidden')) {
|
|
closeModal();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
function initializeApp() {
|
|
// 조회기간 기본값 설정 (오늘부터 -2주)
|
|
const now = new Date();
|
|
const today = now.toISOString().split('T')[0];
|
|
const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
|
|
|
|
document.getElementById('startDate').value = twoWeeksAgo;
|
|
document.getElementById('endDate').value = today;
|
|
|
|
// 사용자 목록 로드
|
|
console.log('사용자 목록 로드 시작...');
|
|
loadUserList();
|
|
|
|
// 상태코드 로드 (공용코드 12번)
|
|
console.log('상태코드 로드 시작...');
|
|
loadStatusCodes();
|
|
|
|
// 추가 공용코드 로드
|
|
loadRequestDeptCodes(); // 공용코드 13번
|
|
loadPackageCodes(); // 공용코드 14번
|
|
loadJobTypeCodes(); // 공용코드 15번
|
|
loadProcessCodes(); // 공용코드 16번
|
|
|
|
// 이벤트 리스너 등록
|
|
document.getElementById('startDate').addEventListener('change', loadJobData);
|
|
document.getElementById('endDate').addEventListener('change', loadJobData);
|
|
document.getElementById('statusFilter').addEventListener('change', filterData);
|
|
document.getElementById('typeFilter').addEventListener('change', filterData);
|
|
document.getElementById('userFilter').addEventListener('change', loadJobData);
|
|
document.getElementById('projectFilter').addEventListener('change', filterData);
|
|
document.getElementById('searchInput').addEventListener('input', filterData);
|
|
document.getElementById('clearFilterBtn').addEventListener('click', clearFilters);
|
|
document.getElementById('pageSize').addEventListener('change', function() {
|
|
pageSize = parseInt(this.value);
|
|
currentPage = 1;
|
|
renderTable();
|
|
});
|
|
document.getElementById('prevPage').addEventListener('click', function() {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
renderTable();
|
|
}
|
|
});
|
|
document.getElementById('nextPage').addEventListener('click', function() {
|
|
const maxPage = Math.ceil(filteredData.length / pageSize);
|
|
if (currentPage < maxPage) {
|
|
currentPage++;
|
|
renderTable();
|
|
}
|
|
});
|
|
document.getElementById('closeModal').addEventListener('click', closeModal);
|
|
|
|
// 정렬 이벤트 리스너
|
|
document.querySelectorAll('[data-sort]').forEach(th => {
|
|
th.addEventListener('click', function() {
|
|
const column = this.getAttribute('data-sort');
|
|
if (sortColumn === column) {
|
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortColumn = column;
|
|
sortDirection = 'asc';
|
|
}
|
|
sortData();
|
|
renderTable();
|
|
});
|
|
});
|
|
|
|
// 모달 외부 클릭 시 닫기
|
|
document.getElementById('detailModal').addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// Feather 아이콘 초기화
|
|
feather.replace();
|
|
}
|
|
|
|
|
|
async function loadJobData() {
|
|
showLoading(true);
|
|
let responseData = null;
|
|
try {
|
|
// 조회기간 파라미터 가져오기
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
const selectedUser = document.getElementById('userFilter').value;
|
|
|
|
console.log('Loading job data with params:', { startDate, endDate, selectedUser });
|
|
|
|
// machine.Jobreport_GetList(sd, ed, uid, cate, doit) 호출
|
|
const jsonStr = await machine.Jobreport_GetList(
|
|
startDate || '',
|
|
endDate || '',
|
|
selectedUser || '',
|
|
'', // cate (빈 값)
|
|
'' // doit (빈 값)
|
|
);
|
|
|
|
responseData = JSON.parse(jsonStr);
|
|
console.log('Parsed response:', responseData);
|
|
|
|
// MachineBridge 응답 형식: { Success: true, Data: [...] }
|
|
if (responseData.Success && Array.isArray(responseData.Data)) {
|
|
jobData = responseData.Data;
|
|
} else if (responseData.error) {
|
|
throw new Error(responseData.error);
|
|
} else if (Array.isArray(responseData)) {
|
|
// 하위 호환성: 배열이 직접 반환되는 경우
|
|
jobData = responseData;
|
|
} else {
|
|
// 데이터가 없는 경우 빈 배열로 초기화
|
|
console.warn('Response has no data, initializing empty array');
|
|
jobData = [];
|
|
}
|
|
|
|
filteredData = [...jobData];
|
|
|
|
updateStatistics();
|
|
updateProjectFilter();
|
|
sortData();
|
|
renderTable();
|
|
} catch (error) {
|
|
console.error('Error loading job data:', error);
|
|
console.error('Response data:', responseData);
|
|
jobData = []; // 에러 시 빈 배열로 초기화
|
|
filteredData = [];
|
|
showError('데이터를 불러오는데 실패했습니다: ' + error.message);
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
function updateStatistics() {
|
|
const totalDays = new Set(jobData.map(item => item.pdate)).size;
|
|
const totalOT = jobData.reduce((sum, item) => sum + (parseFloat(item.ot) || 0), 0);
|
|
const activeProjects = new Set(jobData.filter(item => item.status === '진행중').map(item => item.projectName)).size;
|
|
|
|
// 오늘 날짜 계산
|
|
const now = new Date();
|
|
const today = now.toISOString().split('T')[0]; // YYYY-MM-DD 형식 (2025-07-31)
|
|
|
|
const todayData = jobData.filter(item => {
|
|
if (!item.pdate) return false;
|
|
|
|
const itemDate = item.pdate.toString();
|
|
|
|
// 정확한 날짜 비교: 서버에서 오는 날짜 형식에 맞춰 비교
|
|
if (itemDate.length >= 10) {
|
|
// YYYY-MM-DD 형식인 경우
|
|
return itemDate.substring(0, 10) === today;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
// 오늘 데이터가 있을 때만 시간을 계산, 없으면 명시적으로 0
|
|
let todayHours = 0;
|
|
if (todayData.length > 0) {
|
|
todayHours = todayData.reduce((sum, item) => sum + (parseFloat(item.hrs) || 0), 0);
|
|
}
|
|
const todayProgress = (todayHours / 8) * 100; // 8시간 기준으로 퍼센트 계산
|
|
|
|
document.getElementById('totalDays').textContent = totalDays;
|
|
document.getElementById('totalOT').textContent = totalOT.toFixed(1) + 'h';
|
|
document.getElementById('activeProjects').textContent = activeProjects;
|
|
|
|
// 오늘 근무시간 정보 업데이트
|
|
const todayHoursElement = document.getElementById('todayHours');
|
|
const todayProgressElement = document.getElementById('todayProgress');
|
|
|
|
if (todayHoursElement && todayProgressElement) {
|
|
todayHoursElement.textContent = todayHours.toFixed(1) + 'h';
|
|
todayProgressElement.textContent = `(목표 8시간의 ${todayProgress.toFixed(0)}%)`;
|
|
|
|
// 8시간 미만이면 빨간색, 8시간 이상이면 초록색으로 표시
|
|
if (todayHours < 8) {
|
|
todayHoursElement.className = 'text-2xl font-bold text-red-300';
|
|
} else {
|
|
todayHoursElement.className = 'text-2xl font-bold text-green-300';
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateProjectFilter() {
|
|
const projectSelect = document.getElementById('projectFilter');
|
|
const projects = [...new Set(jobData.map(item => item.projectName).filter(Boolean))];
|
|
|
|
// 기존 옵션 제거 (전체 옵션 제외)
|
|
while (projectSelect.children.length > 1) {
|
|
projectSelect.removeChild(projectSelect.lastChild);
|
|
}
|
|
|
|
// 새 옵션 추가
|
|
projects.forEach(project => {
|
|
const option = document.createElement('option');
|
|
option.value = project;
|
|
option.textContent = project;
|
|
projectSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
async function loadUserList() {
|
|
const userSelect = document.getElementById('userFilter');
|
|
|
|
// 기존 옵션 제거 (전체 옵션 제외)
|
|
while (userSelect.children.length > 1) {
|
|
userSelect.removeChild(userSelect.lastChild);
|
|
}
|
|
|
|
try {
|
|
// machine.Jobreport_GetUsers() 호출
|
|
const jsonStr = await machine.Jobreport_GetUsers();
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
console.log('사용자 목록 데이터:', data);
|
|
|
|
// 응답 형식 처리: { Success: true, Data: [...] } 또는 배열 직접
|
|
const users = (data.Success && data.Data) ? data.Data : (Array.isArray(data) ? data : []);
|
|
|
|
if (users.length > 0) {
|
|
users.forEach(user => {
|
|
const option = document.createElement('option');
|
|
option.value = user.id;
|
|
option.textContent = `${user.name} [${user.id}] ${user.process}`;
|
|
userSelect.appendChild(option);
|
|
});
|
|
console.log('사용자 목록 로드 완료:', userSelect.children.length - 1, '명');
|
|
} else {
|
|
console.warn('사용자 목록 데이터가 없습니다:', data);
|
|
}
|
|
} catch (error) {
|
|
console.error('사용자 목록 로드 중 오류:', error);
|
|
// 에러 시 기본 사용자 옵션 추가
|
|
const option = document.createElement('option');
|
|
option.value = FCOMMON?.info?.Login?.no || '';
|
|
option.textContent = '현재 사용자';
|
|
userSelect.appendChild(option);
|
|
}
|
|
}
|
|
|
|
// 공용코드 12번(상태코드) 로드
|
|
async function loadStatusCodes() {
|
|
try {
|
|
const jsonStr = await machine.Common_GetList('12');
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
console.log('상태코드 원본 데이터:', data);
|
|
|
|
// 응답 형식 처리: { Success: true, Data: [...] } 또는 배열 직접
|
|
const codes = (data.Success && data.Data) ? data.Data : (Array.isArray(data) ? data : []);
|
|
|
|
if (codes.length > 0) {
|
|
statusCodes = codes;
|
|
console.log('상태코드 로드 완료:', statusCodes.length, '개');
|
|
console.log('첫번째 상태코드 샘플:', statusCodes[0]);
|
|
} else {
|
|
console.warn('상태코드 데이터가 없습니다:', data);
|
|
// 기본 상태코드 설정
|
|
statusCodes = [
|
|
{ svalue: '진행 중' },
|
|
{ svalue: '완료' },
|
|
{ svalue: '대기' },
|
|
{ svalue: '보류' }
|
|
];
|
|
}
|
|
} catch (error) {
|
|
console.error('상태코드 로드 중 오류:', error);
|
|
// 에러 시 기본 상태코드 사용
|
|
statusCodes = [
|
|
{ svalue: '진행 중' },
|
|
{ svalue: '완료' },
|
|
{ svalue: '대기' },
|
|
{ svalue: '보류' }
|
|
];
|
|
}
|
|
}
|
|
|
|
// 상태코드 드롭다운 옵션 생성 헬퍼 함수
|
|
function generateStatusOptions(selectedValue = '') {
|
|
console.log('generateStatusOptions 호출 - selectedValue:', selectedValue);
|
|
console.log('현재 statusCodes:', statusCodes);
|
|
|
|
let options = '<option value="">선택하세요</option>';
|
|
statusCodes.forEach((status, index) => {
|
|
const value = status.svalue || status.code || '';
|
|
const text = status.svalue || status.memo || status.code || '';
|
|
const selected = value === selectedValue ? 'selected' : '';
|
|
console.log(`옵션 ${index}: value="${value}", text="${text}", selected=${selected}`);
|
|
options += `<option value="${value}" ${selected}>${text}</option>`;
|
|
});
|
|
return options;
|
|
}
|
|
|
|
// 공용코드 13번(요청부서) 로드
|
|
async function loadRequestDeptCodes() {
|
|
try {
|
|
const jsonStr = await machine.Common_GetList('13');
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
// 응답 형식 처리: { Success: true, Data: [...] } 또는 배열 직접
|
|
const codes = (data.Success && data.Data) ? data.Data : (Array.isArray(data) ? data : []);
|
|
|
|
if (codes.length > 0) {
|
|
requestDeptCodes = codes;
|
|
console.log('요청부서 코드 로드 완료:', requestDeptCodes.length, '개');
|
|
} else {
|
|
requestDeptCodes = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('요청부서 코드 로드 중 오류:', error);
|
|
requestDeptCodes = [];
|
|
}
|
|
}
|
|
|
|
// 공용코드 14번(패키지) 로드
|
|
async function loadPackageCodes() {
|
|
try {
|
|
const jsonStr = await machine.Common_GetList('14');
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
// 응답 형식 처리: { Success: true, Data: [...] } 또는 배열 직접
|
|
const codes = (data.Success && data.Data) ? data.Data : (Array.isArray(data) ? data : []);
|
|
|
|
if (codes.length > 0) {
|
|
packageCodes = codes;
|
|
console.log('패키지 코드 로드 완료:', packageCodes.length, '개');
|
|
} else {
|
|
packageCodes = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('패키지 코드 로드 중 오류:', error);
|
|
packageCodes = [];
|
|
}
|
|
}
|
|
|
|
// 공용코드 15번(업무형태 - 트리구조) 로드
|
|
async function loadJobTypeCodes() {
|
|
try {
|
|
const jsonStr = await machine.Common_GetList('15');
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
console.log('업무형태 원본 데이터:', data);
|
|
|
|
// 응답 형식 처리: { Success: true, Data: [...] } 또는 배열 직접
|
|
const codes = (data.Success && data.Data) ? data.Data : (Array.isArray(data) ? data : []);
|
|
|
|
if (codes.length > 0) {
|
|
jobTypeCodes = codes;
|
|
console.log('업무형태 코드 로드 완료:', jobTypeCodes.length, '개');
|
|
console.log('업무형태 첫번째 샘플:', jobTypeCodes[0]);
|
|
console.log('업무형태 데이터 구조 확인:');
|
|
console.log(' - svalue2 (프로세스):', jobTypeCodes[0].svalue2);
|
|
console.log(' - svalue (분류):', jobTypeCodes[0].svalue);
|
|
console.log(' - memo (항목):', jobTypeCodes[0].memo);
|
|
} else {
|
|
console.warn('업무형태 데이터가 없습니다:', data);
|
|
jobTypeCodes = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('업무형태 코드 로드 중 오류:', error);
|
|
jobTypeCodes = [];
|
|
}
|
|
}
|
|
|
|
// 공용코드 16번(프로세스) 로드
|
|
async function loadProcessCodes() {
|
|
try {
|
|
const jsonStr = await machine.Common_GetList('16');
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
// 응답 형식 처리: { Success: true, Data: [...] } 또는 배열 직접
|
|
const codes = (data.Success && data.Data) ? data.Data : (Array.isArray(data) ? data : []);
|
|
|
|
if (codes.length > 0) {
|
|
processCodes = codes;
|
|
console.log('프로세스 코드 로드 완료:', processCodes.length, '개');
|
|
} else {
|
|
processCodes = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('프로세스 코드 로드 중 오류:', error);
|
|
processCodes = [];
|
|
}
|
|
}
|
|
|
|
// 드롭다운 옵션 생성 헬퍼 함수들
|
|
function generateRequestDeptOptions(selectedValue = '') {
|
|
let options = '<option value="">선택하세요</option>';
|
|
requestDeptCodes.forEach(dept => {
|
|
const value = dept.svalue || '';
|
|
const selected = value === selectedValue ? 'selected' : '';
|
|
options += `<option value="${value}" ${selected}>${value}</option>`;
|
|
});
|
|
return options;
|
|
}
|
|
|
|
function generatePackageOptions(selectedValue = '') {
|
|
let options = '<option value="">선택하세요</option>';
|
|
packageCodes.forEach(pkg => {
|
|
const value = pkg.svalue || '';
|
|
const selected = value === selectedValue ? 'selected' : '';
|
|
options += `<option value="${value}" ${selected}>${value}</option>`;
|
|
});
|
|
return options;
|
|
}
|
|
|
|
function generateProcessOptions(selectedValue = '') {
|
|
let options = '<option value="">선택하세요</option>';
|
|
processCodes.forEach(proc => {
|
|
const value = proc.svalue || '';
|
|
const selected = value === selectedValue ? 'selected' : '';
|
|
options += `<option value="${value}" ${selected}>${value}</option>`;
|
|
});
|
|
return options;
|
|
}
|
|
|
|
function filterData() {
|
|
const statusFilter = document.getElementById('statusFilter').value;
|
|
const typeFilter = document.getElementById('typeFilter').value;
|
|
const projectFilter = document.getElementById('projectFilter').value;
|
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
|
|
filteredData = jobData.filter(item => {
|
|
const statusMatch = !statusFilter || item.status === statusFilter;
|
|
const typeMatch = !typeFilter || item.type === typeFilter;
|
|
const projectMatch = !projectFilter || item.projectName === projectFilter;
|
|
const searchMatch = !searchTerm ||
|
|
(item.description && item.description.toLowerCase().includes(searchTerm)) ||
|
|
(item.projectName && item.projectName.toLowerCase().includes(searchTerm)) ||
|
|
(item.requestpart && item.requestpart.toLowerCase().includes(searchTerm));
|
|
|
|
return statusMatch && typeMatch && projectMatch && searchMatch;
|
|
});
|
|
|
|
currentPage = 1;
|
|
sortData();
|
|
renderTable();
|
|
}
|
|
|
|
function sortData() {
|
|
filteredData.sort((a, b) => {
|
|
let aVal = a[sortColumn];
|
|
let bVal = b[sortColumn];
|
|
|
|
// 날짜 정렬
|
|
if (sortColumn === 'pdate') {
|
|
aVal = new Date(aVal);
|
|
bVal = new Date(bVal);
|
|
}
|
|
// 숫자 정렬
|
|
else if (['hrs', 'ot'].includes(sortColumn)) {
|
|
aVal = parseFloat(aVal) || 0;
|
|
bVal = parseFloat(bVal) || 0;
|
|
}
|
|
// 문자열 정렬
|
|
else {
|
|
aVal = (aVal || '').toString().toLowerCase();
|
|
bVal = (bVal || '').toString().toLowerCase();
|
|
}
|
|
|
|
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
|
|
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
// 텍스트 자르기 함수 (50자 제한)
|
|
function truncateText(text, maxLength = 50) {
|
|
if (!text) return '-';
|
|
if (text.length <= maxLength) return text;
|
|
return text.substring(0, maxLength) + '...';
|
|
}
|
|
|
|
function renderTable() {
|
|
const tbody = document.getElementById('jobTableBody');
|
|
const startIndex = (currentPage - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const pageData = filteredData.slice(startIndex, endIndex);
|
|
|
|
if (pageData.length === 0) {
|
|
showEmptyState();
|
|
return;
|
|
}
|
|
|
|
hideEmptyState();
|
|
tbody.innerHTML = '';
|
|
|
|
let lastDate = null;
|
|
|
|
pageData.forEach((item, index) => {
|
|
const currentDate = item.pdate ? item.pdate.substring(0, 10) : null;
|
|
|
|
// 날짜 구분선 숨김
|
|
// if (lastDate && currentDate && lastDate !== currentDate) {
|
|
// const separatorRow = document.createElement('tr');
|
|
// separatorRow.className = 'border-t-2 border-white/20';
|
|
// separatorRow.innerHTML = `
|
|
// <td colspan="7" class="px-6 py-2">
|
|
// <div class="flex items-center">
|
|
// <div class="flex-grow border-t border-white/30"></div>
|
|
// <span class="px-4 text-xs text-white/60 bg-white/10 rounded-full">
|
|
// ${formatDate(currentDate)}
|
|
// </span>
|
|
// <div class="flex-grow border-t border-white/30"></div>
|
|
// </div>
|
|
// </td>
|
|
// `;
|
|
// tbody.appendChild(separatorRow);
|
|
// }
|
|
|
|
const row = document.createElement('tr');
|
|
row.className = 'hover:bg-white/10 cursor-pointer transition-colors';
|
|
row.setAttribute('data-item-id', item.idx || index);
|
|
row.addEventListener('click', () => showDetailModal(item));
|
|
|
|
const statusColor = getStatusColor(item.status);
|
|
|
|
// 근무시간 표시 로직
|
|
const hrs = parseFloat(item.hrs) || 0;
|
|
const ot = parseFloat(item.ot) || 0;
|
|
let workTimeDisplay = '';
|
|
|
|
if (hrs > 0) {
|
|
workTimeDisplay = hrs.toFixed(1);
|
|
if (ot > 0) {
|
|
workTimeDisplay += '+' + ot.toFixed(1);
|
|
}
|
|
} else {
|
|
workTimeDisplay = '-';
|
|
}
|
|
|
|
// 프로젝트 연결 상태에 따른 버튼 표시
|
|
const pidx = parseInt(item.pidx) || 0;
|
|
const hasProject = pidx > 0;
|
|
const projectButton = hasProject ?
|
|
`<button onclick="showProjectDetail(${pidx}, event)" class="mr-2 p-1 rounded-full bg-blue-500/20 hover:bg-blue-500/40 text-blue-300 hover:text-blue-200 transition-colors" title="프로젝트 상세보기">
|
|
<i data-feather="folder" class="w-3 h-3"></i>
|
|
</button>` : '';
|
|
|
|
row.innerHTML = `
|
|
<td class="px-6 py-2 whitespace-nowrap text-sm text-white">
|
|
${formatDate(item.pdate)}
|
|
</td>
|
|
<td class="px-6 py-2 whitespace-nowrap">
|
|
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${statusColor}">
|
|
${item.status || '-'}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-2 whitespace-nowrap text-sm text-white">
|
|
${workTimeDisplay}
|
|
</td>
|
|
<td class="px-6 py-2 whitespace-nowrap text-sm text-white">
|
|
<div class="flex items-center">
|
|
${projectButton}
|
|
<span title="${item.projectName || ''}">${truncateText(item.projectName)}</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-2 text-sm text-white">
|
|
<div class="max-w-xs truncate" title="${item.description || ''}">
|
|
${item.description || '-'}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-2 whitespace-nowrap text-sm text-white">
|
|
${item.requestpart || '-'}
|
|
</td>
|
|
<td class="px-6 py-2 whitespace-nowrap text-sm text-white hidden">
|
|
${item.type || '-'}
|
|
</td>
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
lastDate = currentDate;
|
|
});
|
|
|
|
updatePagination();
|
|
feather.replace();
|
|
}
|
|
|
|
function getStatusColor(status) {
|
|
switch (status) {
|
|
case '진행중':
|
|
case '진행 중': return 'bg-orange-100 text-orange-800';
|
|
case '진행 완료':
|
|
case '완료': return 'bg-green-100 text-green-800';
|
|
case '대기': return 'bg-yellow-100 text-yellow-800';
|
|
default: return 'bg-gray-100 text-gray-800';
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return '-';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('ko-KR');
|
|
}
|
|
|
|
function updatePagination() {
|
|
const maxPage = Math.ceil(filteredData.length / pageSize);
|
|
const startItem = (currentPage - 1) * pageSize + 1;
|
|
const endItem = Math.min(currentPage * pageSize, filteredData.length);
|
|
|
|
document.getElementById('pageInfo').textContent = `${currentPage} / ${maxPage}`;
|
|
document.getElementById('prevPage').disabled = currentPage <= 1;
|
|
document.getElementById('nextPage').disabled = currentPage >= maxPage;
|
|
}
|
|
|
|
// 필터 토글 함수
|
|
function toggleFilter() {
|
|
const filterContent = document.getElementById('filterContent');
|
|
const toggleIcon = document.getElementById('filterToggleIcon');
|
|
|
|
if (filterContent.classList.contains('hidden')) {
|
|
filterContent.classList.remove('hidden');
|
|
toggleIcon.classList.remove('rotate-180');
|
|
} else {
|
|
filterContent.classList.add('hidden');
|
|
toggleIcon.classList.add('rotate-180');
|
|
}
|
|
}
|
|
|
|
function showAddJobModal() {
|
|
const modal = document.getElementById('detailModal');
|
|
const content = document.getElementById('modalContent');
|
|
|
|
// 오늘 날짜를 기본값으로 설정
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
content.innerHTML = `
|
|
<form id="editJobForm" class="space-y-6">
|
|
<input type="hidden" id="editIdx" value="">
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
|
<!-- 좌측: 기본 정보 (2열) -->
|
|
<div class="lg:col-span-2">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">날짜 *</label>
|
|
<input type="date" id="editPdate" value="${today}"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">상태 *</label>
|
|
<select id="editStatus" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
${generateStatusOptions('진행 중')}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">요청부서</label>
|
|
<select id="editRequestpart" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
${generateRequestDeptOptions('')}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">패키지</label>
|
|
<select id="editPackage" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
${generatePackageOptions('')}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">프로세스</label>
|
|
<select id="editJobProcess" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
${generateProcessOptions('')}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">업무형태</label>
|
|
<button type="button" id="editJobTypeDisplay" onclick="showJobTypeSelector()"
|
|
class="w-full bg-blue-500 hover:bg-blue-600 border border-blue-400 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-400 transition-all text-left">
|
|
선택하세요
|
|
</button>
|
|
<input type="hidden" id="editJobType" value="">
|
|
<input type="hidden" id="editJobGrp" value="">
|
|
<input type="hidden" id="editJobProcess2" value="">
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">근무시간 (시간) *</label>
|
|
<input type="number" id="editHrs" value="8" step="any"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 (시간)</label>
|
|
<input type="number" id="editOt" value="" step="any"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 시작시간</label>
|
|
<input type="time" id="editOtStart" value=""
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 종료시간</label>
|
|
<input type="time" id="editOtEnd" value=""
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 우측: 프로젝트명과 업무내용 -->
|
|
<div class="lg:col-span-3 space-y-4">
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">프로젝트명 *</label>
|
|
<input type="text" id="editProjectName" value=""
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
</div>
|
|
<div class="flex-grow">
|
|
<label class="block text-white/70 text-sm font-medium mb-2">업무내용 *</label>
|
|
<textarea id="editDescription" rows="15" required
|
|
class="w-full h-full min-h-[360px] bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all resize-vertical"
|
|
placeholder="상세한 업무 내용을 입력하세요..."></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- 모달 푸터 -->
|
|
<div class="px-6 py-4 border-t border-white/10 flex justify-end space-x-3 bg-black/10 rounded-b-2xl">
|
|
<button type="button" onclick="closeModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button type="submit" form="editJobForm" class="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors">
|
|
저장
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
modal.classList.remove('hidden');
|
|
feather.replace();
|
|
|
|
// 폼 제출 이벤트 리스너 추가
|
|
document.getElementById('editJobForm').addEventListener('submit', handleAddSubmit);
|
|
}
|
|
|
|
async function showDetailModal(item) {
|
|
// 문자열로 전달된 경우 JSON 파싱
|
|
if (typeof item === 'string') {
|
|
try {
|
|
item = JSON.parse(item);
|
|
} catch (e) {
|
|
console.error('Error parsing item:', e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 전체 상세 정보를 서버에서 가져오기
|
|
try {
|
|
const jsonStr = await machine.Jobreport_GetDetail(item.idx);
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
// 응답 형식 처리: { Success: true, Data: {...} } 또는 객체 직접
|
|
if (data.Success && data.Data) {
|
|
item = data.Data;
|
|
} else if (!data.error && typeof data === 'object') {
|
|
// 하위 호환성: 객체가 직접 반환되는 경우
|
|
item = data;
|
|
}
|
|
// error가 있으면 기존 item 유지
|
|
} catch (error) {
|
|
console.warn('Failed to load full details, using truncated data:', error);
|
|
}
|
|
|
|
const modal = document.getElementById('detailModal');
|
|
const content = document.getElementById('modalContent');
|
|
|
|
content.innerHTML = `
|
|
<form id="editJobForm" class="space-y-6">
|
|
<input type="hidden" id="editIdx" value="${item.idx || ''}">
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
|
<!-- 좌측: 기본 정보 (2열) -->
|
|
<div class="lg:col-span-2">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">날짜 *</label>
|
|
<input type="date" id="editPdate" value="${item.pdate || ''}"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">상태 *</label>
|
|
<select id="editStatus" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
${generateStatusOptions(item.status)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">요청부서</label>
|
|
<select id="editRequestpart" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
${generateRequestDeptOptions(item.requestpart)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">패키지</label>
|
|
<select id="editPackage" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
${generatePackageOptions(item.package)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">프로세스</label>
|
|
<select id="editJobProcess" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
${generateProcessOptions(item.jobprocess)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">업무형태</label>
|
|
<button type="button" id="editJobTypeDisplay" onclick="showJobTypeSelector()"
|
|
class="w-full bg-blue-500 hover:bg-blue-600 border border-blue-400 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-400 transition-all text-left">
|
|
${item.jobtype ? (item.jobprocess2 + ' | ' + item.jobgrp + ' | ' + item.jobtype) : '선택하세요'}
|
|
</button>
|
|
<input type="hidden" id="editJobType" value="${item.jobtype || ''}">
|
|
<input type="hidden" id="editJobGrp" value="${item.jobgrp || ''}">
|
|
<input type="hidden" id="editJobProcess2" value="${item.jobprocess2 || ''}">
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">근무시간 (시간) *</label>
|
|
<input type="number" id="editHrs" value="${item.hrs || ''}" step="any"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 (시간)</label>
|
|
<input type="number" id="editOt" value="${item.ot || ''}" step="any"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 시작시간</label>
|
|
<input type="time" id="editOtStart" value="${item.otStart || ''}"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
</div>
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 종료시간</label>
|
|
<input type="time" id="editOtEnd" value="${item.otEnd || ''}"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 우측: 프로젝트명과 업무내용 -->
|
|
<div class="lg:col-span-3 space-y-4">
|
|
<div>
|
|
<label class="block text-white/70 text-sm font-medium mb-2">프로젝트명 *</label>
|
|
<input type="text" id="editProjectName" value="${item.projectName || ''}"
|
|
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
|
</div>
|
|
<div class="flex-grow">
|
|
<label class="block text-white/70 text-sm font-medium mb-2">업무내용 *</label>
|
|
<textarea id="editDescription" rows="15" required
|
|
class="w-full h-full min-h-[360px] bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all resize-vertical"
|
|
placeholder="상세한 업무 내용을 입력하세요...">${item.description || ''}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- 모달 푸터 -->
|
|
<div class="px-6 py-4 border-t border-white/10 flex justify-between items-center bg-black/10 rounded-b-2xl">
|
|
<button type="button" id="deleteJobBtn" class="bg-danger-500 hover:bg-danger-600 text-white px-4 py-2 rounded-lg transition-colors">
|
|
삭제
|
|
</button>
|
|
<div class="flex space-x-3">
|
|
<button type="button" onclick="closeModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button type="submit" form="editJobForm" class="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
modal.classList.remove('hidden');
|
|
feather.replace();
|
|
|
|
// 폼 제출 이벤트 리스너 추가
|
|
document.getElementById('editJobForm').addEventListener('submit', handleEditSubmit);
|
|
document.getElementById('deleteJobBtn').addEventListener('click', handleDeleteJob);
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('detailModal').classList.add('hidden');
|
|
}
|
|
|
|
// 업무형태 선택 관련 함수들
|
|
let selectedJobType = { process: '', group: '', item: '' };
|
|
|
|
function showJobTypeSelector() {
|
|
console.log('showJobTypeSelector 호출됨');
|
|
console.log('jobTypeCodes 데이터:', jobTypeCodes);
|
|
console.log('jobTypeCodes 길이:', jobTypeCodes.length);
|
|
|
|
if (jobTypeCodes.length === 0) {
|
|
alert('업무형태 데이터를 불러오는 중입니다. 잠시 후 다시 시도해주세요.');
|
|
return;
|
|
}
|
|
|
|
// 모달 열기
|
|
document.getElementById('jobTypeModal').classList.remove('hidden');
|
|
|
|
// 프로세스 목록 렌더링
|
|
renderProcessListForJobType();
|
|
|
|
// 닫기 버튼 이벤트
|
|
document.getElementById('closeJobTypeModal').addEventListener('click', closeJobTypeModal);
|
|
}
|
|
|
|
function closeJobTypeModal() {
|
|
document.getElementById('jobTypeModal').classList.add('hidden');
|
|
}
|
|
|
|
function renderProcessListForJobType() {
|
|
const processListDiv = document.getElementById('processListForJobType');
|
|
|
|
console.log('renderProcessListForJobType 호출됨');
|
|
console.log('jobTypeCodes:', jobTypeCodes);
|
|
|
|
if (!jobTypeCodes || jobTypeCodes.length === 0) {
|
|
processListDiv.innerHTML = '<p class="text-white/50 text-sm p-4">데이터가 없습니다.</p>';
|
|
return;
|
|
}
|
|
|
|
// 프로세스 목록 추출 (svalue2 기준으로 그룹화)
|
|
const processes = [...new Set(jobTypeCodes.map(item => item.svalue2 || 'N/A'))].sort();
|
|
console.log('프로세스 목록:', processes);
|
|
|
|
if (processes.length === 0) {
|
|
processListDiv.innerHTML = '<p class="text-white/50 text-sm p-4">프로세스가 없습니다.</p>';
|
|
return;
|
|
}
|
|
|
|
processListDiv.innerHTML = processes.map(proc => `
|
|
<button type="button"
|
|
onclick="selectProcess('${proc}')"
|
|
class="w-full text-left px-4 py-2 rounded-lg hover:bg-white/20 text-white transition-colors">
|
|
${proc}
|
|
</button>
|
|
`).join('');
|
|
|
|
console.log('프로세스 버튼 생성 완료');
|
|
}
|
|
|
|
function selectProcess(processName) {
|
|
selectedJobType.process = processName;
|
|
selectedJobType.group = '';
|
|
selectedJobType.item = '';
|
|
|
|
// 선택된 프로세스 하이라이트
|
|
document.querySelectorAll('#processListForJobType button').forEach(btn => {
|
|
btn.classList.remove('bg-white/30');
|
|
});
|
|
event.target.classList.add('bg-white/30');
|
|
|
|
// 분류 목록 렌더링
|
|
renderGroupList(processName);
|
|
|
|
// 항목 목록 초기화
|
|
document.getElementById('itemList').innerHTML = '<p class="text-white/50 text-sm">분류를 선택하세요</p>';
|
|
}
|
|
|
|
function renderGroupList(processName) {
|
|
const groupListDiv = document.getElementById('groupList');
|
|
|
|
// 선택된 프로세스의 분류 목록 추출
|
|
const groups = [...new Set(
|
|
jobTypeCodes
|
|
.filter(item => (item.svalue2 || 'N/A') === processName)
|
|
.map(item => item.svalue || 'N/A')
|
|
)].sort();
|
|
|
|
groupListDiv.innerHTML = groups.map(grp => `
|
|
<button type="button"
|
|
onclick="selectGroup('${grp}')"
|
|
class="w-full text-left px-4 py-2 rounded-lg hover:bg-white/20 text-white transition-colors">
|
|
${grp}
|
|
</button>
|
|
`).join('');
|
|
}
|
|
|
|
function selectGroup(groupName) {
|
|
selectedJobType.group = groupName;
|
|
selectedJobType.item = '';
|
|
|
|
// 선택된 분류 하이라이트
|
|
document.querySelectorAll('#groupList button').forEach(btn => {
|
|
btn.classList.remove('bg-white/30');
|
|
});
|
|
event.target.classList.add('bg-white/30');
|
|
|
|
// 항목 목록 렌더링
|
|
renderItemList(selectedJobType.process, groupName);
|
|
}
|
|
|
|
function renderItemList(processName, groupName) {
|
|
const itemListDiv = document.getElementById('itemList');
|
|
|
|
// 선택된 프로세스와 분류의 항목 목록 추출
|
|
const items = jobTypeCodes
|
|
.filter(item => (item.svalue2 || 'N/A') === processName && (item.svalue || 'N/A') === groupName)
|
|
.map(item => item.memo || '')
|
|
.sort();
|
|
|
|
itemListDiv.innerHTML = items.map(itm => `
|
|
<button type="button"
|
|
onclick="selectItem('${itm}')"
|
|
class="w-full text-left px-4 py-2 rounded-lg hover:bg-white/20 text-white transition-colors">
|
|
${itm}
|
|
</button>
|
|
`).join('');
|
|
}
|
|
|
|
function selectItem(itemName) {
|
|
selectedJobType.item = itemName;
|
|
|
|
// 선택된 항목 하이라이트
|
|
document.querySelectorAll('#itemList button').forEach(btn => {
|
|
btn.classList.remove('bg-white/30');
|
|
});
|
|
event.target.classList.add('bg-white/30');
|
|
}
|
|
|
|
function confirmJobTypeSelection() {
|
|
if (!selectedJobType.process || !selectedJobType.group || !selectedJobType.item) {
|
|
alert('프로세스, 분류, 항목을 모두 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
// 선택된 값을 폼에 반영
|
|
const displayText = `${selectedJobType.process} | ${selectedJobType.group} | ${selectedJobType.item}`;
|
|
document.getElementById('editJobTypeDisplay').textContent = displayText;
|
|
document.getElementById('editJobType').value = selectedJobType.item;
|
|
document.getElementById('editJobGrp').value = selectedJobType.group;
|
|
document.getElementById('editJobProcess2').value = selectedJobType.process;
|
|
|
|
// 모달 닫기
|
|
closeJobTypeModal();
|
|
}
|
|
|
|
async function handleAddSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
try {
|
|
// 폼 데이터 수집
|
|
const jdate = document.getElementById('editPdate').value;
|
|
const cate = document.getElementById('editJobType').value || '';
|
|
const title = document.getElementById('editProjectName').value;
|
|
const doit = document.getElementById('editDescription').value;
|
|
const remark = document.getElementById('editStatus').value || '';
|
|
const jfrom = document.getElementById('editOtStart').value || '';
|
|
const jto = document.getElementById('editOtEnd').value || '';
|
|
|
|
console.log('Adding job with params:', { jdate, cate, title, doit, remark, jfrom, jto });
|
|
|
|
// machine.Jobreport_Add(jdate, cate, title, doit, remark, jfrom, jto) 호출
|
|
const jsonStr = await machine.Jobreport_Add(jdate, cate, title, doit, remark, jfrom, jto);
|
|
const result = JSON.parse(jsonStr);
|
|
|
|
console.log('Add result:', result);
|
|
|
|
// 응답 형식 처리: { Success: true } 또는 { success: true }
|
|
if (result.Success === false || result.success === false) {
|
|
throw new Error(result.Message || result.message || '추가에 실패했습니다.');
|
|
}
|
|
|
|
// 성공시 alert 없이 바로 진행
|
|
// 모달 닫기
|
|
closeModal();
|
|
|
|
// 데이터 새로고침
|
|
await loadJobData();
|
|
|
|
} catch (error) {
|
|
console.error('Error adding job:', error);
|
|
alert('업무일지 추가 중 오류가 발생했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function handleEditSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
try {
|
|
// 폼 데이터 수집
|
|
const idx = document.getElementById('editIdx').value;
|
|
const jdate = document.getElementById('editPdate').value;
|
|
const cate = document.getElementById('editJobType').value || '';
|
|
const title = document.getElementById('editProjectName').value;
|
|
const doit = document.getElementById('editDescription').value;
|
|
const remark = document.getElementById('editStatus').value || '';
|
|
const jfrom = document.getElementById('editOtStart').value || '';
|
|
const jto = document.getElementById('editOtEnd').value || '';
|
|
|
|
console.log('Editing job with params:', { idx, jdate, cate, title, doit, remark, jfrom, jto });
|
|
|
|
// machine.Jobreport_Edit(idx, jdate, cate, title, doit, remark, jfrom, jto) 호출
|
|
const jsonStr = await machine.Jobreport_Edit(idx, jdate, cate, title, doit, remark, jfrom, jto);
|
|
const result = JSON.parse(jsonStr);
|
|
|
|
console.log('Edit result:', result);
|
|
|
|
// 응답 형식 처리: { Success: true } 또는 { success: true }
|
|
if (result.Success === false || result.success === false) {
|
|
throw new Error(result.Message || result.message || '수정에 실패했습니다.');
|
|
}
|
|
|
|
// 성공시 alert 없이 바로 진행
|
|
// 모달 닫기
|
|
closeModal();
|
|
|
|
// 데이터 새로고침
|
|
await loadJobData();
|
|
|
|
} catch (error) {
|
|
console.error('Error updating job:', error);
|
|
alert('업무일지 수정 중 오류가 발생했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function handleDeleteJob() {
|
|
const idx = document.getElementById('editIdx').value;
|
|
|
|
if (!idx) {
|
|
alert('삭제할 수 없는 항목입니다.');
|
|
return;
|
|
}
|
|
|
|
if (!confirm('정말로 이 업무일지를 삭제하시겠습니까?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('Deleting job with idx:', idx);
|
|
|
|
// machine.Jobreport_Delete(idx) 호출
|
|
const jsonStr = await machine.Jobreport_Delete(idx);
|
|
const result = JSON.parse(jsonStr);
|
|
|
|
console.log('Delete result:', result);
|
|
|
|
// 응답 형식 처리: { Success: true } 또는 { success: true }
|
|
if (result.Success === false || result.success === false) {
|
|
throw new Error(result.Message || result.message || '삭제에 실패했습니다.');
|
|
}
|
|
|
|
// 성공시 alert 없이 바로 모달 닫기
|
|
closeModal();
|
|
await loadJobData();
|
|
|
|
} catch (error) {
|
|
console.error('Error deleting job:', error);
|
|
alert('업무일지 삭제 중 오류가 발생했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function showSelectedJobDetail(item) {
|
|
const detailContainer = document.getElementById('selectedJobDetail');
|
|
const contentContainer = document.getElementById('selectedJobContent');
|
|
|
|
// 기존 선택된 행의 스타일 제거
|
|
document.querySelectorAll('#jobTableBody tr').forEach(row => {
|
|
row.classList.remove('bg-blue-50', 'border-l-4', 'border-blue-500');
|
|
});
|
|
|
|
// 현재 행에 선택 스타일 적용
|
|
const currentRow = event.target.closest('tr');
|
|
if (currentRow) {
|
|
currentRow.classList.add('bg-blue-50', 'border-l-4', 'border-blue-500');
|
|
}
|
|
|
|
contentContainer.innerHTML = `
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">날짜</label>
|
|
<p class="mt-1 text-sm text-gray-900">${formatDate(item.pdate)}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">상태</label>
|
|
<p class="mt-1">
|
|
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}">
|
|
${item.status || '-'}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">프로젝트명</label>
|
|
<p class="mt-1 text-sm text-gray-900" title="${item.projectName || ''}">${truncateText(item.projectName)}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">요청부서</label>
|
|
<p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">타입</label>
|
|
<p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">근무시간</label>
|
|
<p class="mt-1 text-sm text-gray-900">${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">초과근무</label>
|
|
<p class="mt-1 text-sm text-gray-900">${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">초과근무 시간</label>
|
|
<p class="mt-1 text-sm text-gray-900">${item.otStart && item.otEnd ? `${item.otStart} ~ ${item.otEnd}` : '-'}</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">업무내용</label>
|
|
<div class="bg-gray-50 rounded-lg p-4 cursor-pointer hover:bg-gray-100 transition-colors"
|
|
onclick="showDetailModal(${JSON.stringify(item).replace(/"/g, '"')})">
|
|
<p class="text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p>
|
|
<div class="mt-2 text-xs text-gray-500 flex items-center">
|
|
<i data-feather="maximize-2" class="w-4 h-4 mr-1"></i>
|
|
클릭하여 전체 내용 보기
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
detailContainer.classList.remove('hidden');
|
|
feather.replace();
|
|
|
|
// 부드러운 스크롤로 상세 내용으로 이동
|
|
detailContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
|
|
function showLoading(show) {
|
|
const loadingState = document.getElementById('loadingState');
|
|
const tableBody = document.getElementById('jobTableBody');
|
|
|
|
if (show) {
|
|
loadingState.classList.remove('hidden');
|
|
tableBody.style.display = 'none';
|
|
} else {
|
|
loadingState.classList.add('hidden');
|
|
tableBody.style.display = 'table-row-group';
|
|
}
|
|
}
|
|
|
|
function showEmptyState() {
|
|
document.getElementById('emptyState').classList.remove('hidden');
|
|
document.getElementById('jobTableBody').style.display = 'none';
|
|
}
|
|
|
|
function hideEmptyState() {
|
|
document.getElementById('emptyState').classList.add('hidden');
|
|
document.getElementById('jobTableBody').style.display = 'table-row-group';
|
|
}
|
|
|
|
function showError(message) {
|
|
// 간단한 에러 알림 (실제 구현에서는 더 나은 알림 시스템 사용)
|
|
alert(message);
|
|
}
|
|
|
|
function clearFilters() {
|
|
// 조회기간을 오늘부터 -2주로 초기화
|
|
const now = new Date();
|
|
const today = now.toISOString().split('T')[0];
|
|
const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
|
|
|
|
document.getElementById('startDate').value = twoWeeksAgo;
|
|
document.getElementById('endDate').value = today;
|
|
|
|
// 다른 필터들 초기화
|
|
document.getElementById('statusFilter').value = '';
|
|
document.getElementById('typeFilter').value = '';
|
|
document.getElementById('userFilter').value = '';
|
|
document.getElementById('projectFilter').value = '';
|
|
document.getElementById('searchInput').value = '';
|
|
|
|
// 서버에서 새로운 데이터 가져오기
|
|
loadJobData();
|
|
}
|
|
|
|
function showProjectDetail(pidx, event) {
|
|
// 이벤트 버블링 방지 (행 클릭 이벤트와 충돌 방지)
|
|
event.stopPropagation();
|
|
|
|
// 임시 메시지 표시
|
|
alert('프로젝트 보기 기능 준비중');
|
|
|
|
// TODO: 추후 프로젝트 상세 페이지로 이동
|
|
// window.open(`/Project/Detail/${pidx}`, '_blank');
|
|
}
|
|
|
|
function exportToExcel() {
|
|
if (filteredData.length === 0) {
|
|
alert('내보낼 데이터가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 조회기간 정보 가져오기
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
const periodText = startDate && endDate ? `_${startDate}_${endDate}` : '';
|
|
|
|
// CSV 형식으로 데이터 변환
|
|
const headers = ['날짜', '상태', '프로젝트명', '요청부서', '타입', '업무내용', '근무시간', '초과근무'];
|
|
const csvContent = [
|
|
headers.join(','),
|
|
...filteredData.map(item => [
|
|
formatDate(item.pdate),
|
|
item.status || '',
|
|
item.projectName || '',
|
|
item.requestpart || '',
|
|
item.type || '',
|
|
`"${(item.description || '').replace(/"/g, '""')}"`,
|
|
item.hrs || '',
|
|
item.ot || ''
|
|
].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', `업무일지${periodText}_${new Date().toISOString().split('T')[0]}.csv`);
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |