feat: 모바일 신고 시스템 구축 + tkqc 연동 + tkuser 이슈유형 관리
- tkreport 모바일 신고 페이지 (5단계 위자드: 유형→위치→프로젝트→항목→사진) - 프로젝트 DB 연동 (아코디언 UI: TBM등록/활성프로젝트/모름) - 클라이언트 이미지 리사이징 (1280px, JPEG 80%) - nginx client_max_body_size 50m, /api/projects/ 프록시 추가 - 부적합 신고 → tkqc 자동 연동 (사진 base64 전달, SSO 토큰 유지) - work_issue_reports에 project_id 컬럼 추가 - imageUploadService 경로 수정 (public/uploads → uploads, Docker 볼륨 일치) - tkuser 이슈유형 탭, 휴가관리, nginx 프록시 업데이트 - tkqc 대시보드/수신함/관리함/폐기함 UI 업데이트 - system1 랜딩페이지 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,8 +64,8 @@
|
||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('departments')">
|
||||
<i class="fas fa-sitemap mr-2"></i>부서
|
||||
</button>
|
||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('workers')">
|
||||
<i class="fas fa-hard-hat mr-2"></i>작업자
|
||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('issueTypes')">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>이슈 유형
|
||||
</button>
|
||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('tasks')">
|
||||
<i class="fas fa-tasks mr-2"></i>작업
|
||||
@@ -270,57 +270,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- ============ 작업자 탭 ============ -->
|
||||
<div id="tab-workers" class="hidden">
|
||||
<div class="grid lg:grid-cols-5 gap-6">
|
||||
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm p-5">
|
||||
<h2 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-hard-hat text-slate-400 mr-2"></i>작업자 등록</h2>
|
||||
<form id="addWorkerForm" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">이름 <span class="text-red-400">*</span></label>
|
||||
<input type="text" id="newWorkerName" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="작업자 이름" required>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">직종</label>
|
||||
<select id="newJobType" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="">선택</option>
|
||||
<option value="leader">반장</option>
|
||||
<option value="worker">작업자</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">부서</label>
|
||||
<select id="newWorkerDept" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="">선택</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">전화번호</label>
|
||||
<input type="text" id="newWorkerPhone" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="010-0000-0000">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">입사일</label>
|
||||
<input type="date" id="newWorkerHireDate" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
|
||||
<input type="text" id="newWorkerNotes" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="메모">
|
||||
</div>
|
||||
<button type="submit" class="w-full px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium">
|
||||
<i class="fas fa-plus mr-1"></i>추가
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="lg:col-span-3 bg-white rounded-xl shadow-sm p-5">
|
||||
<h2 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-hard-hat text-slate-400 mr-2"></i>작업자 목록</h2>
|
||||
<div id="workerList" class="space-y-2 max-h-[520px] overflow-y-auto">
|
||||
<div class="text-gray-400 text-center py-8"><i class="fas fa-spinner fa-spin text-2xl"></i><p class="mt-2 text-sm">로딩 중...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 부서 탭 ============ -->
|
||||
<div id="tab-departments" class="hidden">
|
||||
<div class="grid lg:grid-cols-5 gap-6">
|
||||
@@ -359,6 +308,102 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 이슈 유형 탭 ============ -->
|
||||
<div id="tab-issueTypes" class="hidden">
|
||||
<div class="grid lg:grid-cols-5 gap-6">
|
||||
<!-- 좌측: 등록 폼 -->
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<!-- 카테고리 등록 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-5">
|
||||
<h2 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-layer-group text-slate-400 mr-2"></i>카테고리 등록</h2>
|
||||
<form id="addIssueCategoryForm" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">유형 <span class="text-red-400">*</span></label>
|
||||
<div class="flex gap-3">
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||||
<input type="radio" name="newIssueCatType" value="nonconformity" checked class="accent-slate-700"> 부적합
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||||
<input type="radio" name="newIssueCatType" value="safety" class="accent-slate-700"> 안전
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||||
<input type="radio" name="newIssueCatType" value="facility" class="accent-slate-700"> 시설설비
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">카테고리명 <span class="text-red-400">*</span></label>
|
||||
<input type="text" id="newIssueCatName" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="카테고리 이름" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">설명</label>
|
||||
<input type="text" id="newIssueCatDesc" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="카테고리 설명">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">표시순서</label>
|
||||
<input type="number" id="newIssueCatOrder" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="0" min="0">
|
||||
</div>
|
||||
<button type="submit" class="w-full px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium">
|
||||
<i class="fas fa-plus mr-1"></i>카테고리 추가
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- 아이템 등록 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-5">
|
||||
<h2 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-list-ul text-slate-400 mr-2"></i>아이템 등록</h2>
|
||||
<form id="addIssueItemForm" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">카테고리 <span class="text-red-400">*</span></label>
|
||||
<select id="newIssueItemCategory" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
|
||||
<option value="">선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">아이템명 <span class="text-red-400">*</span></label>
|
||||
<input type="text" id="newIssueItemName" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="아이템 이름" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">설명</label>
|
||||
<input type="text" id="newIssueItemDesc" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="아이템 설명">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">심각도</label>
|
||||
<select id="newIssueItemSeverity" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="low">낮음</option>
|
||||
<option value="medium" selected>보통</option>
|
||||
<option value="high">높음</option>
|
||||
<option value="critical">심각</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">표시순서</label>
|
||||
<input type="number" id="newIssueItemOrder" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium">
|
||||
<i class="fas fa-plus mr-1"></i>아이템 추가
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 우측: 카테고리/아이템 목록 -->
|
||||
<div class="lg:col-span-3 bg-white rounded-xl shadow-sm p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-base font-semibold text-gray-800"><i class="fas fa-exclamation-triangle text-slate-400 mr-2"></i>이슈 유형 목록</h2>
|
||||
<div class="flex gap-1 bg-gray-100 rounded-lg p-0.5">
|
||||
<button id="issueTypeToggleNonconformity" onclick="switchIssueType('nonconformity')" class="px-3 py-1 rounded-md text-xs font-medium bg-slate-700 text-white">부적합</button>
|
||||
<button id="issueTypeToggleSafety" onclick="switchIssueType('safety')" class="px-3 py-1 rounded-md text-xs font-medium text-gray-500 hover:bg-gray-200">안전</button>
|
||||
<button id="issueTypeToggleFacility" onclick="switchIssueType('facility')" class="px-3 py-1 rounded-md text-xs font-medium text-gray-500 hover:bg-gray-200">시설설비</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="issueCategoryList" class="space-y-2 max-h-[600px] overflow-y-auto">
|
||||
<div class="text-gray-400 text-center py-8"><i class="fas fa-spinner fa-spin text-2xl"></i><p class="mt-2 text-sm">로딩 중...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 작업 탭 ============ -->
|
||||
<div id="tab-tasks" class="hidden">
|
||||
<div class="flex gap-6" style="height: calc(100vh - 9rem);">
|
||||
@@ -780,71 +825,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 편집 모달 -->
|
||||
<div id="editWorkerModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-semibold text-gray-900">작업자 수정</h3>
|
||||
<button onclick="closeWorkerModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<form id="editWorkerForm" class="space-y-3">
|
||||
<input type="hidden" id="editWorkerId">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">이름</label>
|
||||
<input type="text" id="editWorkerName" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">직종</label>
|
||||
<select id="editJobType" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="">선택</option>
|
||||
<option value="leader">반장</option>
|
||||
<option value="worker">작업자</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">부서</label>
|
||||
<select id="editWorkerDept" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="">선택</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">전화번호</label>
|
||||
<input type="text" id="editWorkerPhone" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">입사일</label>
|
||||
<input type="date" id="editWorkerHireDate" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
|
||||
<input type="text" id="editWorkerNotes" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">상태</label>
|
||||
<select id="editWorkerStatus" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="active">재직</option>
|
||||
<option value="inactive">비활성</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">재직상태</label>
|
||||
<select id="editEmploymentStatus" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="employed">재직</option>
|
||||
<option value="resigned">퇴직</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-3">
|
||||
<button type="button" onclick="closeWorkerModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-sm">취소</button>
|
||||
<button type="submit" class="flex-1 px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium"><i class="fas fa-save mr-1"></i>저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 부서 편집 모달 -->
|
||||
<div id="editDepartmentModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||
@@ -889,6 +869,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 이슈 카테고리 수정 모달 -->
|
||||
<div id="editIssueCategoryModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-semibold text-gray-900">카테고리 수정</h3>
|
||||
<button onclick="closeIssueCategoryModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<form id="editIssueCategoryForm" class="space-y-3">
|
||||
<input type="hidden" id="editIssueCatId">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">카테고리명</label>
|
||||
<input type="text" id="editIssueCatName" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">설명</label>
|
||||
<input type="text" id="editIssueCatDesc" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">표시순서</label>
|
||||
<input type="number" id="editIssueCatOrder" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" min="0">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">활성</label>
|
||||
<select id="editIssueCatActive" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="1">활성</option>
|
||||
<option value="0">비활성</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-3">
|
||||
<button type="button" onclick="closeIssueCategoryModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-sm">취소</button>
|
||||
<button type="submit" class="flex-1 px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium"><i class="fas fa-save mr-1"></i>저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 이슈 아이템 수정 모달 -->
|
||||
<div id="editIssueItemModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-semibold text-gray-900">아이템 수정</h3>
|
||||
<button onclick="closeIssueItemModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<form id="editIssueItemForm" class="space-y-3">
|
||||
<input type="hidden" id="editIssueItemId">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">카테고리</label>
|
||||
<select id="editIssueItemCategory" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
|
||||
<option value="">선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">아이템명</label>
|
||||
<input type="text" id="editIssueItemName" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">설명</label>
|
||||
<input type="text" id="editIssueItemDesc" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">심각도</label>
|
||||
<select id="editIssueItemSeverity" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="low">낮음</option>
|
||||
<option value="medium">보통</option>
|
||||
<option value="high">높음</option>
|
||||
<option value="critical">심각</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">표시순서</label>
|
||||
<input type="number" id="editIssueItemOrder" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">활성</label>
|
||||
<select id="editIssueItemActive" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="1">활성</option>
|
||||
<option value="0">비활성</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-3">
|
||||
<button type="button" onclick="closeIssueItemModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-sm">취소</button>
|
||||
<button type="submit" class="flex-1 px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium"><i class="fas fa-save mr-1"></i>저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업장 편집 모달 -->
|
||||
<div id="editWorkplaceModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||
@@ -1211,8 +1282,8 @@
|
||||
[mainEl, navInner, headerInner].forEach(el => { el.classList.remove(wideClass); el.classList.add(defaultClass); });
|
||||
}
|
||||
if (name === 'projects' && !projectsLoaded) loadProjects();
|
||||
if (name === 'workers' && !workersLoaded) loadWorkers();
|
||||
if (name === 'departments' && !departmentsLoaded) loadDepartments();
|
||||
if (name === 'issueTypes' && !issueTypesLoaded) loadIssueTypes();
|
||||
if (name === 'workplaces' && !workplacesLoaded) loadWorkplaces();
|
||||
if (name === 'tasks' && !tasksLoaded) loadTasksTab();
|
||||
if (name === 'vacations' && !vacationsLoaded) loadVacationsTab();
|
||||
@@ -2087,122 +2158,6 @@
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
/* ===== Workers CRUD ===== */
|
||||
let workers = [], workersLoaded = false, departmentsForSelect = [];
|
||||
|
||||
const JOB_TYPE = { leader: '반장', worker: '작업자' };
|
||||
function jobTypeBadge(t) {
|
||||
if (t === 'leader') return '<span class="px-1.5 py-0.5 rounded text-xs bg-amber-50 text-amber-600">반장</span>';
|
||||
if (t === 'worker') return '<span class="px-1.5 py-0.5 rounded text-xs bg-blue-50 text-blue-600">작업자</span>';
|
||||
return t ? `<span class="px-1.5 py-0.5 rounded text-xs bg-gray-50 text-gray-500">${t}</span>` : '';
|
||||
}
|
||||
function workerStatusBadge(s) {
|
||||
if (s === 'inactive') return '<span class="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-400">비활성</span>';
|
||||
return '<span class="px-1.5 py-0.5 rounded text-xs bg-emerald-50 text-emerald-600">재직</span>';
|
||||
}
|
||||
|
||||
async function loadDepartmentsForSelect() {
|
||||
try {
|
||||
const r = await api('/departments'); departmentsForSelect = (r.data || r).filter(d => d.is_active !== 0 && d.is_active !== false);
|
||||
populateDeptSelects();
|
||||
} catch(e) { console.warn('부서 로드 실패:', e); }
|
||||
}
|
||||
function populateDeptSelects() {
|
||||
['newWorkerDept','editWorkerDept'].forEach(id => {
|
||||
const sel = document.getElementById(id); if (!sel) return;
|
||||
const val = sel.value;
|
||||
sel.innerHTML = '<option value="">선택</option>';
|
||||
departmentsForSelect.forEach(d => { const o = document.createElement('option'); o.value = d.department_id; o.textContent = d.department_name; sel.appendChild(o); });
|
||||
sel.value = val;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadWorkers() {
|
||||
await loadDepartmentsForSelect();
|
||||
try {
|
||||
const r = await api('/workers'); workers = r.data || r;
|
||||
workersLoaded = true;
|
||||
displayWorkers();
|
||||
} catch (err) {
|
||||
document.getElementById('workerList').innerHTML = `<div class="text-red-500 text-center py-6"><i class="fas fa-exclamation-triangle text-xl"></i><p class="text-sm mt-2">${err.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function displayWorkers() {
|
||||
const c = document.getElementById('workerList');
|
||||
if (!workers.length) { c.innerHTML = '<p class="text-gray-400 text-center py-4 text-sm">등록된 작업자가 없습니다.</p>'; return; }
|
||||
c.innerHTML = workers.map(w => `
|
||||
<div class="flex items-center justify-between p-2.5 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-800 truncate"><i class="fas fa-hard-hat mr-1.5 text-gray-400 text-xs"></i>${w.worker_name}</div>
|
||||
<div class="text-xs text-gray-500 flex items-center gap-1.5 mt-0.5 flex-wrap">
|
||||
${jobTypeBadge(w.job_type)}
|
||||
${w.department_name ? `<span class="px-1.5 py-0.5 rounded bg-green-50 text-green-600">${w.department_name}</span>` : ''}
|
||||
${workerStatusBadge(w.status)}
|
||||
${w.phone_number ? `<span class="text-gray-400">${w.phone_number}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 ml-2 flex-shrink-0">
|
||||
<button onclick="editWorker(${w.worker_id})" class="p-1.5 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded" title="편집"><i class="fas fa-pen-to-square text-xs"></i></button>
|
||||
${w.status !== 'inactive' ? `<button onclick="deactivateWorker(${w.worker_id},'${(w.worker_name||'').replace(/'/g,"\\'")}')" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="비활성화"><i class="fas fa-ban text-xs"></i></button>` : ''}
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('addWorkerForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api('/workers', { method: 'POST', body: JSON.stringify({
|
||||
worker_name: document.getElementById('newWorkerName').value.trim(),
|
||||
job_type: document.getElementById('newJobType').value || null,
|
||||
department_id: document.getElementById('newWorkerDept').value ? parseInt(document.getElementById('newWorkerDept').value) : null,
|
||||
phone_number: document.getElementById('newWorkerPhone').value.trim() || null,
|
||||
hire_date: document.getElementById('newWorkerHireDate').value || null,
|
||||
notes: document.getElementById('newWorkerNotes').value.trim() || null
|
||||
})});
|
||||
showToast('작업자가 추가되었습니다.'); document.getElementById('addWorkerForm').reset(); await loadWorkers();
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
function editWorker(id) {
|
||||
const w = workers.find(x => x.worker_id === id); if (!w) return;
|
||||
document.getElementById('editWorkerId').value = w.worker_id;
|
||||
document.getElementById('editWorkerName').value = w.worker_name;
|
||||
document.getElementById('editJobType').value = w.job_type || '';
|
||||
document.getElementById('editWorkerDept').value = w.department_id || '';
|
||||
document.getElementById('editWorkerPhone').value = w.phone_number || '';
|
||||
document.getElementById('editWorkerHireDate').value = formatDate(w.hire_date);
|
||||
document.getElementById('editWorkerNotes').value = w.notes || '';
|
||||
document.getElementById('editWorkerStatus').value = w.status || 'active';
|
||||
document.getElementById('editEmploymentStatus').value = w.employment_status || 'employed';
|
||||
populateDeptSelects();
|
||||
document.getElementById('editWorkerDept').value = w.department_id || '';
|
||||
document.getElementById('editWorkerModal').classList.remove('hidden');
|
||||
}
|
||||
function closeWorkerModal() { document.getElementById('editWorkerModal').classList.add('hidden'); }
|
||||
|
||||
document.getElementById('editWorkerForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api(`/workers/${document.getElementById('editWorkerId').value}`, { method: 'PUT', body: JSON.stringify({
|
||||
worker_name: document.getElementById('editWorkerName').value.trim(),
|
||||
job_type: document.getElementById('editJobType').value || null,
|
||||
department_id: document.getElementById('editWorkerDept').value ? parseInt(document.getElementById('editWorkerDept').value) : null,
|
||||
phone_number: document.getElementById('editWorkerPhone').value.trim() || null,
|
||||
hire_date: document.getElementById('editWorkerHireDate').value || null,
|
||||
notes: document.getElementById('editWorkerNotes').value.trim() || null,
|
||||
status: document.getElementById('editWorkerStatus').value,
|
||||
employment_status: document.getElementById('editEmploymentStatus').value
|
||||
})});
|
||||
showToast('수정되었습니다.'); closeWorkerModal(); await loadWorkers();
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
async function deactivateWorker(id, name) {
|
||||
if (!confirm(`"${name}" 작업자를 비활성화?`)) return;
|
||||
try { await api(`/workers/${id}`, { method: 'DELETE' }); showToast('작업자 비활성화 완료'); await loadWorkers(); } catch(e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
/* ===== Departments CRUD ===== */
|
||||
let departments = [], departmentsLoaded = false;
|
||||
|
||||
@@ -2295,6 +2250,194 @@
|
||||
try { await api(`/departments/${id}`, { method: 'DELETE' }); showToast('부서 비활성화 완료'); await loadDepartments(); } catch(e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
/* ===== Issue Types CRUD ===== */
|
||||
let issueCategories = [], issueItems = [], issueTypesLoaded = false;
|
||||
let currentIssueType = 'nonconformity';
|
||||
|
||||
function severityBadge(severity) {
|
||||
const colors = { low: 'bg-gray-100 text-gray-600', medium: 'bg-yellow-100 text-yellow-700', high: 'bg-orange-100 text-orange-700', critical: 'bg-red-100 text-red-700' };
|
||||
const labels = { low: '낮음', medium: '보통', high: '높음', critical: '심각' };
|
||||
return `<span class="px-1.5 py-0.5 rounded text-xs ${colors[severity]||''}">${labels[severity]||severity}</span>`;
|
||||
}
|
||||
|
||||
async function loadIssueTypes() {
|
||||
try {
|
||||
const [catRes, itemRes] = await Promise.all([
|
||||
api('/work-issues/categories'),
|
||||
api('/work-issues/items')
|
||||
]);
|
||||
issueCategories = catRes.data || catRes;
|
||||
issueItems = itemRes.data || itemRes;
|
||||
issueTypesLoaded = true;
|
||||
populateIssueCategorySelect();
|
||||
displayIssueCategories();
|
||||
} catch (err) {
|
||||
document.getElementById('issueCategoryList').innerHTML = `<div class="text-red-500 text-center py-6"><i class="fas fa-exclamation-triangle text-xl"></i><p class="text-sm mt-2">${err.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function switchIssueType(type) {
|
||||
currentIssueType = type;
|
||||
['nonconformity','safety','facility'].forEach(t => {
|
||||
const btn = document.getElementById('issueTypeToggle' + t.charAt(0).toUpperCase() + t.slice(1));
|
||||
if (btn) btn.className = 'px-3 py-1 rounded-md text-xs font-medium ' + (type === t ? 'bg-slate-700 text-white' : 'text-gray-500 hover:bg-gray-200');
|
||||
});
|
||||
displayIssueCategories();
|
||||
}
|
||||
|
||||
function populateIssueCategorySelect() {
|
||||
['newIssueItemCategory', 'editIssueItemCategory'].forEach(id => {
|
||||
const sel = document.getElementById(id); if (!sel) return;
|
||||
const val = sel.value;
|
||||
sel.innerHTML = '<option value="">선택</option>';
|
||||
issueCategories.filter(c => c.is_active !== 0 && c.is_active !== false).sort((a,b) => (a.display_order||0) - (b.display_order||0)).forEach(c => {
|
||||
const typeLabel = c.category_type === 'nonconformity' ? '[부적합]' : c.category_type === 'safety' ? '[안전]' : '[시설설비]';
|
||||
const o = document.createElement('option'); o.value = c.category_id; o.textContent = `${typeLabel} ${c.category_name}`; sel.appendChild(o);
|
||||
});
|
||||
sel.value = val;
|
||||
});
|
||||
}
|
||||
|
||||
function displayIssueCategories() {
|
||||
const c = document.getElementById('issueCategoryList');
|
||||
const filtered = issueCategories.filter(cat => cat.category_type === currentIssueType).sort((a,b) => (a.display_order||0) - (b.display_order||0));
|
||||
if (!filtered.length) { c.innerHTML = '<p class="text-gray-400 text-center py-4 text-sm">등록된 카테고리가 없습니다.</p>'; return; }
|
||||
c.innerHTML = filtered.map(cat => {
|
||||
const items = issueItems.filter(it => it.category_id === cat.category_id).sort((a,b) => (a.display_order||0) - (b.display_order||0));
|
||||
const isInactive = cat.is_active === 0 || cat.is_active === false;
|
||||
return `
|
||||
<div class="border rounded-lg ${isInactive ? 'opacity-60' : ''}">
|
||||
<div class="group-header flex items-center justify-between p-3 rounded-t-lg" onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('.chevron').classList.toggle('fa-chevron-down'); this.querySelector('.chevron').classList.toggle('fa-chevron-right');">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-chevron-down chevron text-gray-400 text-xs w-3"></i>
|
||||
<span class="text-sm font-medium text-gray-800">${cat.category_name}</span>
|
||||
<span class="px-1.5 py-0.5 rounded text-xs ${items.length > 0 ? 'bg-blue-50 text-blue-600' : 'bg-gray-50 text-gray-400'}">${items.length}개</span>
|
||||
${isInactive ? '<span class="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-400">비활성</span>' : ''}
|
||||
</div>
|
||||
<div class="flex gap-1" onclick="event.stopPropagation()">
|
||||
<button onclick="editIssueCategory(${cat.category_id})" class="p-1.5 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded" title="편집"><i class="fas fa-pen-to-square text-xs"></i></button>
|
||||
<button onclick="deleteIssueCategory(${cat.category_id},'${(cat.category_name||'').replace(/'/g,"\\'")}')" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="삭제"><i class="fas fa-trash text-xs"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t">
|
||||
${items.length ? items.map(it => {
|
||||
const itemInactive = it.is_active === 0 || it.is_active === false;
|
||||
return `
|
||||
<div class="flex items-center justify-between px-4 py-2 hover:bg-gray-50 ${itemInactive ? 'opacity-60' : ''}">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-gray-300 text-xs">├</span>
|
||||
<span class="text-sm text-gray-700 truncate">${it.item_name}</span>
|
||||
${severityBadge(it.severity)}
|
||||
${itemInactive ? '<span class="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-400">비활성</span>' : ''}
|
||||
</div>
|
||||
<div class="flex gap-1 flex-shrink-0">
|
||||
<button onclick="editIssueItem(${it.item_id})" class="p-1 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded" title="편집"><i class="fas fa-pen-to-square text-xs"></i></button>
|
||||
<button onclick="deleteIssueItem(${it.item_id},'${(it.item_name||'').replace(/'/g,"\\'")}')" class="p-1 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="삭제"><i class="fas fa-trash text-xs"></i></button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') : '<p class="text-gray-400 text-center py-3 text-xs">아이템 없음</p>'}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
document.getElementById('addIssueCategoryForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const type = document.querySelector('input[name="newIssueCatType"]:checked').value;
|
||||
try {
|
||||
await api('/work-issues/categories', { method: 'POST', body: JSON.stringify({
|
||||
category_name: document.getElementById('newIssueCatName').value.trim(),
|
||||
category_type: type,
|
||||
description: document.getElementById('newIssueCatDesc').value.trim() || null,
|
||||
display_order: parseInt(document.getElementById('newIssueCatOrder').value) || 0
|
||||
})});
|
||||
showToast('카테고리가 추가되었습니다.');
|
||||
document.getElementById('addIssueCategoryForm').reset();
|
||||
await loadIssueTypes();
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
document.getElementById('addIssueItemForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api('/work-issues/items', { method: 'POST', body: JSON.stringify({
|
||||
category_id: parseInt(document.getElementById('newIssueItemCategory').value),
|
||||
item_name: document.getElementById('newIssueItemName').value.trim(),
|
||||
description: document.getElementById('newIssueItemDesc').value.trim() || null,
|
||||
severity: document.getElementById('newIssueItemSeverity').value,
|
||||
display_order: parseInt(document.getElementById('newIssueItemOrder').value) || 0
|
||||
})});
|
||||
showToast('아이템이 추가되었습니다.');
|
||||
document.getElementById('addIssueItemForm').reset();
|
||||
await loadIssueTypes();
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
function editIssueCategory(id) {
|
||||
const cat = issueCategories.find(c => c.category_id === id); if (!cat) return;
|
||||
document.getElementById('editIssueCatId').value = cat.category_id;
|
||||
document.getElementById('editIssueCatName').value = cat.category_name;
|
||||
document.getElementById('editIssueCatDesc').value = cat.description || '';
|
||||
document.getElementById('editIssueCatOrder').value = cat.display_order || 0;
|
||||
document.getElementById('editIssueCatActive').value = (cat.is_active === 0 || cat.is_active === false) ? '0' : '1';
|
||||
document.getElementById('editIssueCategoryModal').classList.remove('hidden');
|
||||
}
|
||||
function closeIssueCategoryModal() { document.getElementById('editIssueCategoryModal').classList.add('hidden'); }
|
||||
|
||||
document.getElementById('editIssueCategoryForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api(`/work-issues/categories/${document.getElementById('editIssueCatId').value}`, { method: 'PUT', body: JSON.stringify({
|
||||
category_name: document.getElementById('editIssueCatName').value.trim(),
|
||||
description: document.getElementById('editIssueCatDesc').value.trim() || null,
|
||||
display_order: parseInt(document.getElementById('editIssueCatOrder').value) || 0,
|
||||
is_active: document.getElementById('editIssueCatActive').value === '1'
|
||||
})});
|
||||
showToast('카테고리가 수정되었습니다.'); closeIssueCategoryModal(); await loadIssueTypes();
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
async function deleteIssueCategory(id, name) {
|
||||
const items = issueItems.filter(it => it.category_id === id);
|
||||
if (items.length > 0) { showToast(`"${name}" 카테고리에 ${items.length}개 아이템이 있어 삭제할 수 없습니다. 아이템을 먼저 삭제하세요.`, 'error'); return; }
|
||||
if (!confirm(`"${name}" 카테고리를 삭제하시겠습니까?`)) return;
|
||||
try { await api(`/work-issues/categories/${id}`, { method: 'DELETE' }); showToast('카테고리가 삭제되었습니다.'); await loadIssueTypes(); } catch(e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
function editIssueItem(id) {
|
||||
const it = issueItems.find(x => x.item_id === id); if (!it) return;
|
||||
populateIssueCategorySelect();
|
||||
document.getElementById('editIssueItemId').value = it.item_id;
|
||||
document.getElementById('editIssueItemCategory').value = it.category_id;
|
||||
document.getElementById('editIssueItemName').value = it.item_name;
|
||||
document.getElementById('editIssueItemDesc').value = it.description || '';
|
||||
document.getElementById('editIssueItemSeverity').value = it.severity || 'medium';
|
||||
document.getElementById('editIssueItemOrder').value = it.display_order || 0;
|
||||
document.getElementById('editIssueItemActive').value = (it.is_active === 0 || it.is_active === false) ? '0' : '1';
|
||||
document.getElementById('editIssueItemModal').classList.remove('hidden');
|
||||
}
|
||||
function closeIssueItemModal() { document.getElementById('editIssueItemModal').classList.add('hidden'); }
|
||||
|
||||
document.getElementById('editIssueItemForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api(`/work-issues/items/${document.getElementById('editIssueItemId').value}`, { method: 'PUT', body: JSON.stringify({
|
||||
category_id: parseInt(document.getElementById('editIssueItemCategory').value),
|
||||
item_name: document.getElementById('editIssueItemName').value.trim(),
|
||||
description: document.getElementById('editIssueItemDesc').value.trim() || null,
|
||||
severity: document.getElementById('editIssueItemSeverity').value,
|
||||
display_order: parseInt(document.getElementById('editIssueItemOrder').value) || 0,
|
||||
is_active: document.getElementById('editIssueItemActive').value === '1'
|
||||
})});
|
||||
showToast('아이템이 수정되었습니다.'); closeIssueItemModal(); await loadIssueTypes();
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
async function deleteIssueItem(id, name) {
|
||||
if (!confirm(`"${name}" 아이템을 삭제하시겠습니까?`)) return;
|
||||
try { await api(`/work-issues/items/${id}`, { method: 'DELETE' }); showToast('아이템이 삭제되었습니다.'); await loadIssueTypes(); } catch(e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
/* ===== Workplaces CRUD ===== */
|
||||
let workplaces = [], workplacesLoaded = false, workplaceCategories = [];
|
||||
let selectedWorkplaceId = null, selectedWorkplaceName = '';
|
||||
|
||||
Reference in New Issue
Block a user