feat: 모바일 UX 대폭 개선 + PWA 구현 + 로그인 루프 수정

- 모바일 하단 네비: 메뉴 제거, 4개 핵심 기능(홈/TBM/작업보고/출근) SVG 아이콘
- 모바일 사이드바 스킵: 768px 이하에서 사이드바 미로드, 레이아웃 오프셋 해결
- 모바일 헤더: 햄버거 메뉴 숨김, 본문 margin/overflow 정리
- TBM 모바일: 풀스크린 모달, 저장 버튼 하단 고정, 터치 UX 개선
- PWA: manifest.json, sw.js(network-first), 앱 아이콘, iOS 메타태그, 킬스위치
- 로그인 무한루프 수정: 토큰 만료 검증, 쿠키 정리, loginPage 경로 수정
- 신고 메뉴 tkreport 리다이렉트: navbar + sidebar cross-system-link 적용
- TBM API: 작업장별 안전점검 체크리스트 조회 엔드포인트 추가
- 안전점검 체크리스트 관리 UI 개선
- tkuser: 이슈유형 관리 기능 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-24 08:20:50 +09:00
parent 3cc29c03a8
commit d36303101e
60 changed files with 1418 additions and 270 deletions

View File

@@ -7,7 +7,7 @@
<link rel="preload" href="https://cdn.tailwindcss.com" as="script">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/tkuser.css?v=20260223">
<link rel="stylesheet" href="/static/css/tkuser.css?v=20260224">
</head>
<body>
<!-- Header -->
@@ -43,6 +43,9 @@
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('workplaces')">
<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" onclick="switchTab('workers')">
<i class="fas fa-hard-hat mr-2"></i>작업자
</button>
<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>
@@ -315,6 +318,56 @@
</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-people-group 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">
@@ -869,6 +922,71 @@
</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">
@@ -1270,18 +1388,18 @@
</div>
<!-- JS: Core (config, token, api, toast, helpers, init) -->
<script src="/static/js/tkuser-core.js?v=20260223"></script>
<script src="/static/js/tkuser-core.js?v=20260224"></script>
<!-- JS: Tabs -->
<script src="/static/js/tkuser-tabs.js?v=20260223"></script>
<script src="/static/js/tkuser-tabs.js?v=20260224"></script>
<!-- JS: Individual modules -->
<script src="/static/js/tkuser-users.js?v=20260223"></script>
<script src="/static/js/tkuser-projects.js?v=20260223"></script>
<script src="/static/js/tkuser-departments.js?v=20260223"></script>
<script src="/static/js/tkuser-issue-types.js?v=20260223"></script>
<script src="/static/js/tkuser-workplaces.js?v=20260223"></script>
<script src="/static/js/tkuser-tasks.js?v=20260223"></script>
<script src="/static/js/tkuser-vacations.js?v=20260223"></script>
<script src="/static/js/tkuser-layout-map.js?v=20260223"></script>
<script src="/static/js/tkuser-users.js?v=20260224"></script>
<script src="/static/js/tkuser-projects.js?v=20260224"></script>
<script src="/static/js/tkuser-departments.js?v=20260224"></script>
<script src="/static/js/tkuser-issue-types.js?v=20260224"></script>
<script src="/static/js/tkuser-workplaces.js?v=20260224"></script>
<script src="/static/js/tkuser-tasks.js?v=20260224"></script>
<script src="/static/js/tkuser-vacations.js?v=20260224"></script>
<script src="/static/js/tkuser-layout-map.js?v=20260224"></script>
<!-- Boot -->
<script>init();</script>
</body>