feat(tkuser): 탭 카테고리 그룹핑 + 설비 관리 탭 추가 + tkfb admin 페이지 통합

- tkuser 탭을 5개 카테고리로 그룹핑 (인력/현장/업무/거래/시스템)
- 설비 관리 탭 신규 추가 (CRUD, 필터, 상세 보기)
- tkfb 사이드바 admin 메뉴 6개를 tkuser 외부 링크로 교체
- tkfb admin HTML 6개를 tkuser 리다이렉트로 변경
- gateway 알림 벨 링크를 tkuser로 변경
- _tkuserBase 헬퍼로 개발/운영 환경 자동 분기

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-17 15:35:24 +09:00
parent f548a95767
commit 862a2683d3
14 changed files with 520 additions and 3049 deletions

View File

@@ -33,16 +33,12 @@
<!-- Tab Navigation -->
<nav id="tabNav" class="bg-white border-b shadow-sm sticky top-14 z-40 hidden">
<div id="tabNavInner" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex gap-1 py-2 overflow-x-auto">
<div class="flex gap-1 py-2 overflow-x-auto items-center">
<!-- 인력 관리 -->
<span class="tab-group-label">인력</span>
<button class="tab-btn active px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="users" onclick="switchTab('users', event)">
<i class="fas fa-users mr-2"></i>사용자
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="projects" onclick="switchTab('projects', event)">
<i class="fas fa-folder-open mr-2"></i>프로젝트
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="workplaces" onclick="switchTab('workplaces', event)">
<i class="fas fa-building mr-2"></i>작업장
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="workers" onclick="switchTab('workers', event)">
<i class="fas fa-hard-hat mr-2"></i>작업자
</button>
@@ -52,15 +48,33 @@
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="permissions" onclick="switchTab('permissions', event)">
<i class="fas fa-shield-alt mr-2"></i>권한
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="issueTypes" onclick="switchTab('issueTypes', event)">
<i class="fas fa-exclamation-triangle mr-2"></i>이슈 유형
<span class="tab-divider"></span>
<!-- 현장 관리 -->
<span class="tab-group-label">현장</span>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="projects" onclick="switchTab('projects', event)">
<i class="fas fa-folder-open mr-2"></i>프로젝트
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="workplaces" onclick="switchTab('workplaces', event)">
<i class="fas fa-building mr-2"></i>작업장
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="equipments" onclick="switchTab('equipments', event)">
<i class="fas fa-cogs mr-2"></i>설비
</button>
<span class="tab-divider"></span>
<!-- 업무 설정 -->
<span class="tab-group-label">업무</span>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="tasks" onclick="switchTab('tasks', event)">
<i class="fas fa-tasks mr-2"></i>작업
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="issueTypes" onclick="switchTab('issueTypes', event)">
<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" data-tab="vacations" onclick="switchTab('vacations', event)">
<i class="fas fa-umbrella-beach mr-2"></i>휴가
</button>
<span class="tab-divider"></span>
<!-- 거래/물품 -->
<span class="tab-group-label">거래</span>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="partners" onclick="switchTab('partners', event)">
<i class="fas fa-truck mr-2"></i>협력업체
</button>
@@ -70,6 +84,9 @@
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="consumables" onclick="switchTab('consumables', event)">
<i class="fas fa-box-open mr-2"></i>소모품
</button>
<span class="tab-divider"></span>
<!-- 시스템 -->
<span class="tab-group-label">시스템</span>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="notificationRecipients" onclick="switchTab('notificationRecipients', event)">
<i class="fas fa-bell mr-2"></i>알림 수신자
</button>
@@ -912,6 +929,49 @@
</div>
</div>
</div>
<!-- ============ 설비 탭 ============ -->
<div id="tab-equipments" 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">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-800"><i class="fas fa-cogs text-emerald-500 mr-2"></i>설비 목록</h2>
<button id="btnAddEquipment" onclick="openAddEquipmentTkuser()" class="hidden px-3 py-1.5 bg-slate-700 text-white rounded-lg text-xs hover:bg-slate-800">
<i class="fas fa-plus mr-1"></i>설비 등록
</button>
</div>
<div class="flex gap-2 mb-3">
<input type="text" id="equipmentSearchTkuser" class="input-field flex-1 px-3 py-1.5 rounded-lg text-sm" placeholder="설비명, 코드, 제조사 검색..." oninput="filterEquipmentsTkuser()">
<select id="equipmentFilterWorkplace" class="input-field px-2 py-1.5 rounded-lg text-sm" onchange="filterEquipmentsTkuser()">
<option value="">전체 작업장</option>
</select>
</div>
<div class="flex gap-2 mb-3">
<select id="equipmentFilterType" class="input-field flex-1 px-2 py-1.5 rounded-lg text-sm" onchange="filterEquipmentsTkuser()">
<option value="">전체 유형</option>
</select>
<select id="equipmentFilterStatus" class="input-field px-2 py-1.5 rounded-lg text-sm" onchange="filterEquipmentsTkuser()">
<option value="">전체 상태</option>
<option value="active">활성</option>
<option value="maintenance">정비중</option>
<option value="inactive">비활성</option>
</select>
</div>
<div id="equipmentsListTkuser" class="space-y-2 max-h-[60vh] overflow-y-auto">
<p class="text-gray-400 text-center py-4 text-sm">탭을 선택하면 데이터를 불러옵니다.</p>
</div>
</div>
<!-- 설비 상세 -->
<div class="lg:col-span-3">
<div id="equipmentDetailTkuser" class="hidden"></div>
<div id="equipmentEmptyTkuser" class="text-center text-gray-400 py-16">
<i class="fas fa-cogs text-4xl mb-3"></i>
<p>설비를 선택하면 상세 정보를 볼 수 있습니다</p>
</div>
</div>
</div>
</div>
</main>
<!-- 사용자 편집 모달 -->
@@ -2020,6 +2080,159 @@
</div>
</div>
<!-- 설비 등록 모달 -->
<div id="addEquipmentModalTkuser" class="hidden fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center p-4" onclick="if(event.target===this)closeAddEquipmentTkuser()">
<div class="bg-white rounded-xl shadow-xl max-w-lg w-full p-6 max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">설비 등록</h3>
<button onclick="closeAddEquipmentTkuser()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
</div>
<form id="addEquipmentFormTkuser">
<div class="grid grid-cols-2 gap-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="newEquipmentCodeTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</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="newEquipmentNameTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">설비유형</label>
<input type="text" id="newEquipmentTypeTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="예: 용접기, 절단기">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">작업장</label>
<select id="newEquipmentWorkplaceTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
<option value="">선택</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">제조사</label>
<input type="text" id="newEquipmentManufacturerTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">모델명</label>
<input type="text" id="newEquipmentModelTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">시리얼번호</label>
<input type="text" id="newEquipmentSerialTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">설치일</label>
<input type="date" id="newEquipmentInstallDateTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">공급업체</label>
<input type="text" id="newEquipmentSupplierTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">구매가격</label>
<input type="number" id="newEquipmentPriceTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" min="0">
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">상태</label>
<select id="newEquipmentStatusTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
<option value="active">활성</option>
<option value="maintenance">정비중</option>
<option value="inactive">비활성</option>
</select>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">사양</label>
<textarea id="newEquipmentSpecsTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="2"></textarea>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
<textarea id="newEquipmentNotesTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="2"></textarea>
</div>
</div>
<div class="flex justify-end mt-4 gap-2">
<button type="button" onclick="closeAddEquipmentTkuser()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
<button type="submit" class="px-4 py-2 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-800">등록</button>
</div>
</form>
</div>
</div>
<!-- 설비 수정 모달 -->
<div id="editEquipmentModalTkuser" class="hidden fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center p-4" onclick="if(event.target===this)closeEditEquipmentTkuser()">
<div class="bg-white rounded-xl shadow-xl max-w-lg w-full p-6 max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">설비 수정</h3>
<button onclick="closeEditEquipmentTkuser()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
</div>
<form id="editEquipmentFormTkuser">
<input type="hidden" id="editEquipmentIdTkuser">
<div class="grid grid-cols-2 gap-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="editEquipmentCodeTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</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="editEquipmentNameTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">설비유형</label>
<input type="text" id="editEquipmentTypeTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="예: 용접기, 절단기">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">작업장</label>
<select id="editEquipmentWorkplaceTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
<option value="">선택</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">제조사</label>
<input type="text" id="editEquipmentManufacturerTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">모델명</label>
<input type="text" id="editEquipmentModelTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">시리얼번호</label>
<input type="text" id="editEquipmentSerialTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">설치일</label>
<input type="date" id="editEquipmentInstallDateTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">공급업체</label>
<input type="text" id="editEquipmentSupplierTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">구매가격</label>
<input type="number" id="editEquipmentPriceTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" min="0">
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">상태</label>
<select id="editEquipmentStatusTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm">
<option value="active">활성</option>
<option value="maintenance">정비중</option>
<option value="inactive">비활성</option>
</select>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">사양</label>
<textarea id="editEquipmentSpecsTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="2"></textarea>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
<textarea id="editEquipmentNotesTkuser" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="2"></textarea>
</div>
</div>
<div class="flex justify-end mt-4 gap-2">
<button type="button" onclick="closeEditEquipmentTkuser()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
<button type="submit" class="px-4 py-2 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-800">저장</button>
</div>
</form>
</div>
</div>
<!-- 사진 확대 모달 -->
<div id="photoViewModal" class="fixed inset-0 bg-black bg-opacity-80 hidden z-[60] flex items-center justify-center p-4 cursor-pointer" onclick="this.classList.add('hidden')">
<img id="photoViewImage" class="max-w-full max-h-[90vh] rounded-lg shadow-2xl">
@@ -2041,6 +2254,7 @@
<script src="/static/js/tkuser-partners.js?v=2026031601"></script>
<script src="/static/js/tkuser-vendors.js?v=2026031401"></script>
<script src="/static/js/tkuser-consumables.js?v=2026031602"></script>
<script src="/static/js/tkuser-equipments.js?v=2026031701"></script>
<script src="/static/js/tkuser-notificationRecipients.js?v=2026031701"></script>
<!-- Boot -->
<script>init();</script>