feat: TBM 모바일 시스템 + 작업 분할/이동 + 권한 통합

TBM 시스템:
- 4단계 워크플로우 (draft→세부편집→완료→작업보고)
- 모바일 전용 TBM 페이지 (tbm-mobile.html) + 3단계 생성 위자드
- 작업자 작업 분할 (work_hours + split_seq)
- 작업자 이동 보내기/빼오기 (tbm_transfers 테이블)
- 생성 시 중복 배정 방지 (당일 배정 현황 조회)
- 데스크탑 TBM 페이지 세부편집 기능 추가

작업보고서:
- 모바일 전용 작업보고서 페이지 (report-create-mobile.html)
- TBM에서 사전 등록된 work_hours 자동 반영

권한 시스템:
- tkuser user_page_permissions 테이블과 system1 페이지 접근 연동
- pageAccessRoutes를 userRoutes보다 먼저 등록 (라우트 우선순위 수정)
- TKUSER_DEFAULT_ACCESS 폴백 추가 (개인→부서→기본값 3단계)
- 권한 캐시키 갱신 (userPageAccess_v2)

기타:
- app-init.js 캐시 버스팅 (v=5)
- iOS Safari touch-action: manipulation 적용
- KST 타임존 날짜 버그 수정 (toISOString UTC 이슈)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-25 07:46:21 +09:00
parent d36303101e
commit 7637be33f3
65 changed files with 9470 additions and 240 deletions

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/png" href="/img/favicon.png">
<!-- 최적화된 로딩: API 설정 → 앱 초기화 (병렬 컴포넌트 로딩) -->
<script src="/js/api-base.js"></script>
<script src="/js/app-init.js?v=3" defer></script>
<script src="/js/app-init.js?v=5" defer></script>
<!-- instant.page: 링크 호버 시 페이지 프리로딩 -->
<script src="https://instant.page/5.2.0" type="module"></script>
</head>
@@ -144,9 +144,9 @@
</div>
</main>
<!-- TBM 생성/수정 모달 -->
<!-- TBM 생성 모달 (간소화) -->
<div id="tbmModal" class="tbm-modal-overlay" style="display: none;">
<div class="tbm-modal" style="max-width: 1000px;">
<div class="tbm-modal" style="max-width: 800px;">
<div class="tbm-modal-header">
<h2 class="tbm-modal-title" id="modalTitle">
<span>&#128221;</span>
@@ -159,7 +159,7 @@
<form id="tbmForm" onsubmit="event.preventDefault(); saveTbmSession();">
<input type="hidden" id="sessionId">
<!-- 고정 정보 섹션 -->
<!-- 기본 정보 섹션 -->
<div class="tbm-form-section">
<h3 class="tbm-form-section-title">
<span>&#128197;</span>
@@ -177,32 +177,44 @@
<input type="hidden" id="leaderId">
</div>
</div>
<div class="tbm-form-row">
<div class="tbm-form-group">
<label class="tbm-form-label">프로젝트</label>
<select id="newTbmProjectId" class="tbm-form-input">
<option value="">선택 안함</option>
</select>
</div>
<div class="tbm-form-group">
<label class="tbm-form-label">공정<span class="tbm-form-required">*</span></label>
<select id="newTbmWorkTypeId" class="tbm-form-input" required>
<option value="">공정 선택...</option>
</select>
</div>
</div>
</div>
<!-- 작업자 및 작업 정보 섹션 -->
<!-- 작업자 선택 섹션 -->
<div class="tbm-form-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 class="tbm-form-section-title" style="margin: 0; border: 0; padding: 0;">
<span>&#128101;</span>
작업자 및 작업 정보
작업자 선택
<span id="newTbmWorkerCount" style="color: #3b82f6; font-size: 0.875rem;">(0명)</span>
</h3>
<div style="display: flex; gap: 0.5rem;">
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="openBulkSettingModal()">
일괄 설정
</button>
<button type="button" class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="openWorkerSelectionModal()">
<span class="tbm-btn-icon">+</span>
작업자 선택
</button>
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="selectAllNewTbmWorkers()">전체 선택</button>
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="deselectAllNewTbmWorkers()">전체 해제</button>
</div>
</div>
<!-- 작업자 카드 리스트 -->
<div id="workerTaskList" class="tbm-worker-list">
<!-- 작업자 카드들이 여기에 동적으로 추가됩니다 -->
<div class="tbm-empty-state" id="workerListEmpty" style="padding: 2rem; border: 2px dashed #d1d5db; border-radius: 10px;">
<div class="tbm-empty-icon">&#128101;</div>
<p class="tbm-empty-description" style="margin: 0;">작업자를 선택해주세요</p>
<div id="newTbmWorkerGrid" class="tbm-worker-select-grid">
<!-- 작업자 체크박스 그리드가 여기에 동적으로 생성됩니다 -->
</div>
<div class="tbm-alert tbm-alert-info" style="margin-top: 1rem;">
<span class="tbm-alert-icon">&#128161;</span>
<div class="tbm-alert-content">
<div class="tbm-alert-text">저장 후 카드를 클릭하면 작업자별 <strong>작업/작업장</strong>을 입력할 수 있습니다.</div>
</div>
</div>
</div>
@@ -400,6 +412,9 @@
<div class="tbm-workplace-map-container">
<canvas id="workplaceMapCanvas"></canvas>
</div>
<button type="button" class="landscape-trigger-btn" id="landscapeTriggerBtn" onclick="openLandscapeMap()" style="display:none;">
&#128250; 전체화면 지도
</button>
</div>
<!-- 리스트 기반 선택 (모바일에서 토글) -->
@@ -525,11 +540,18 @@
<label class="tbm-form-label">종료 시간</label>
<input type="time" id="endTime" class="tbm-form-input">
</div>
<div class="tbm-form-group" style="margin-top: 1rem;">
<label class="tbm-form-label">작업자 근태</label>
<div id="completeAttendanceList" style="max-height: 300px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 0.5rem;">
<div style="text-align:center; color:#9ca3af; padding:1rem;">로딩 중...</div>
</div>
</div>
</div>
<div class="tbm-modal-footer">
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeCompleteModal()">취소</button>
<button type="button" class="tbm-btn tbm-btn-success" onclick="completeTbmSession()">
<button type="button" class="tbm-btn tbm-btn-success" id="completeModalBtn" onclick="completeTbmSession()">
<span class="tbm-btn-icon">&#10003;</span>
완료
</button>
@@ -657,6 +679,19 @@
</div>
</div>
<!-- 가로모드 전체화면 지도 오버레이 -->
<div id="landscapeOverlay" class="landscape-overlay" style="display:none;">
<div id="landscapeInner" class="landscape-inner">
<div class="landscape-header">
<h3>&#127981; 작업장 선택</h3>
<button type="button" class="landscape-close-btn" onclick="closeLandscapeMap()">×</button>
</div>
<div class="landscape-canvas-wrap">
<canvas id="landscapeCanvas"></canvas>
</div>
</div>
</div>
<!-- 토스트 알림 -->
<div class="toast-container" id="toastContainer"></div>
</div>
@@ -667,7 +702,7 @@
<script src="/js/tbm/api.js?v=1"></script>
<!-- 기존 UI 로직 (점진적 마이그레이션) -->
<script type="module" src="/js/tbm.js?v=8"></script>
<script type="module" src="/js/tbm.js?v=10"></script>
<!-- 모바일 하단 네비게이션 -->
<div id="mobile-nav-container"></div>