## 주요 기능 추가 ### 1. 작업 인계 시스템 (반차/조퇴 시) - **인계 모달** (`handoverModal`) - 인계 사유 선택 (반차/조퇴/긴급/기타) - 인수자 (다른 팀장) 선택 - 인계 날짜/시간 입력 - 인계할 팀원 선택 (체크박스) - 인계 내용 메모 - **API 연동** - POST /api/tbm/handovers (인계 요청 생성) - 세션 정보와 팀 구성 자동 조회 - from_leader_id 자동 설정 - **UI 개선** - TBM 카드에 "📤 인계" 버튼 추가 - 인계할 팀원 목록 자동 로드 - 현재 팀장 제외한 리더만 표시 ### 2. TBM 상세보기 모달 - **상세 정보 표시** (`detailModal`) - 기본 정보 (팀장, 날짜, 프로젝트, 작업 장소, 작업 내용) - 안전 특이사항 (노란색 강조) - 팀 구성 (그리드 레이아웃) - 안전 체크리스트 (카테고리별 그룹화) - **안전 체크 시각화** - ✅/❌ 아이콘으로 체크 상태 표시 - 체크됨: 초록색 배경 - 미체크: 빨간색 배경 - 카테고리별 구분 (PPE/EQUIPMENT/ENVIRONMENT/EMERGENCY) - **병렬 API 호출** - Promise.all로 세션/팀/안전체크 동시 조회 - 로딩 성능 최적화 ### 3. 작업 보고서와 TBM 연동 - **TBM 팀 구성 자동 불러오기** - `loadTbmTeamForDate()` 함수 추가 - 선택한 날짜의 TBM 세션 자동 조회 - 진행중(draft) 세션 우선 선택 - 팀 구성 정보 자동 로드 - **작업자 자동 선택** - TBM에서 구성한 팀원 자동 선택 - 선택된 작업자 시각적 표시 (.selected 클래스) - 다음 단계 버튼 자동 활성화 - **안내 메시지** - "🛠️ TBM 팀 구성 자동 적용" 알림 - 자동 선택된 팀원 수 표시 - 파란색 강조 스타일 ### 4. UI/UX 개선 - TBM 카드 버튼 레이아웃 개선 (flex-wrap) - 인계 버튼 오렌지색 (#f59e0b) - 모달 스크롤 가능 (max-height: 70vh) - 반응형 그리드 (auto-fill, minmax) ## 기술 구현 ### 함수 추가 - `viewTbmSession()`: 상세보기 (병렬 API 호출) - `openHandoverModal()`: 인계 모달 (팀 구성 자동 로드) - `saveHandover()`: 인계 저장 (worker_ids JSON array) - `loadTbmTeamForDate()`: TBM 팀 구성 조회 - `closeDetailModal()`, `closeHandoverModal()`: 모달 닫기 ### 수정 함수 - `populateWorkerGrid()`: TBM 연동 추가 (async/await) - `displayTbmSessions()`: 인계 버튼 추가 ## 파일 변경사항 - web-ui/pages/work/tbm.html (모달 2개 추가, 약 110줄) - web-ui/js/tbm.js (함수 추가, 약 250줄 증가) - web-ui/js/daily-work-report.js (TBM 연동, 약 60줄 추가) ## 사용 시나리오 ### 시나리오 1: TBM → 작업보고서 1. 아침 TBM에서 팀 구성 (예: 5명 선택) 2. 작업 보고서 작성 시 날짜 선택 3. **자동으로 5명 선택됨** ✨ 4. 바로 작업 내역 입력 가능 ### 시나리오 2: 조퇴 시 인계 1. TBM 카드에서 "📤 인계" 클릭 2. 사유 선택 (조퇴), 인수자 선택 3. 인계할 팀원 선택 (기본 전체 선택) 4. 인계 요청 → DB 저장 ### 시나리오 3: TBM 상세 확인 1. TBM 카드 클릭 2. 기본 정보, 팀 구성, 안전 체크 한눈에 확인 3. 안전 체크 완료 여부 시각적 확인 ## 데이터 흐름 ``` TBM 시작 ↓ 팀 구성 저장 (tbm_team_assignments) ↓ 작업 보고서 작성 시 ↓ GET /api/tbm/sessions/date/:date ↓ GET /api/tbm/sessions/:id/team ↓ 팀원 자동 선택 ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
341 lines
16 KiB
HTML
341 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>TBM 관리 | (주)테크니컬코리아</title>
|
||
<link rel="stylesheet" href="/css/design-system.css">
|
||
<link rel="stylesheet" href="/css/common.css?v=2">
|
||
<link rel="stylesheet" href="/css/project-management.css?v=3">
|
||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||
<script src="/js/auth-check.js?v=1" defer></script>
|
||
<script type="module" src="/js/api-config.js?v=3"></script>
|
||
</head>
|
||
<body>
|
||
<div class="work-report-container">
|
||
<!-- 네비게이션 바 -->
|
||
<div id="navbar-container"></div>
|
||
|
||
<!-- 메인 콘텐츠 -->
|
||
<main class="work-report-main">
|
||
<div class="dashboard-main">
|
||
<div class="page-header">
|
||
<div class="page-title-section">
|
||
<h1 class="page-title">
|
||
<span class="title-icon">🛠️</span>
|
||
TBM (Tool Box Meeting) 관리
|
||
</h1>
|
||
<p class="page-description">아침 안전 회의 및 팀 구성 관리</p>
|
||
</div>
|
||
|
||
<div class="page-actions">
|
||
<input type="date" id="tbmDate" class="form-control" style="display: inline-block; width: auto;">
|
||
<button class="btn btn-secondary" onclick="loadTodayTbm()">
|
||
<span class="btn-icon">📅</span>
|
||
오늘
|
||
</button>
|
||
<button class="btn btn-primary" onclick="openNewTbmModal()">
|
||
<span class="btn-icon">➕</span>
|
||
새 TBM 시작
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TBM 세션 목록 -->
|
||
<div class="projects-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">TBM 세션 목록</h2>
|
||
<div class="project-stats">
|
||
<span class="stat-item">
|
||
<span class="stat-icon">📋</span>
|
||
총 <span id="totalSessions">0</span>개
|
||
</span>
|
||
<span class="stat-item" style="color: #16a34a;">
|
||
<span class="stat-icon">✅</span>
|
||
완료 <span id="completedSessions">0</span>개
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="projects-grid" id="tbmSessionsGrid">
|
||
<!-- TBM 세션 카드들이 여기에 동적으로 생성됩니다 -->
|
||
</div>
|
||
|
||
<!-- Empty State -->
|
||
<div class="empty-state" id="emptyState" style="display: none;">
|
||
<div class="empty-icon">🛠️</div>
|
||
<h3>등록된 TBM 세션이 없습니다.</h3>
|
||
<p>\"새 TBM 시작\" 버튼을 눌러 오늘의 TBM을 시작해보세요.</p>
|
||
<button class="btn btn-primary" onclick="openNewTbmModal()">
|
||
➕ 첫 TBM 시작하기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- TBM 생성/수정 모달 -->
|
||
<div id="tbmModal" class="modal-overlay" style="display: none;">
|
||
<div class="modal-container" style="max-width: 800px;">
|
||
<div class="modal-header">
|
||
<h2 id="modalTitle">새 TBM 시작</h2>
|
||
<button class="modal-close-btn" onclick="closeTbmModal()">×</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<form id="tbmForm" onsubmit="event.preventDefault(); saveTbmSession();">
|
||
<input type="hidden" id="sessionId">
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">TBM 날짜 *</label>
|
||
<input type="date" id="sessionDate" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">팀장 *</label>
|
||
<select id="leaderId" class="form-control" required>
|
||
<option value="">팀장 선택...</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">프로젝트</label>
|
||
<select id="projectId" class="form-control">
|
||
<option value="">프로젝트 선택...</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">작업 장소</label>
|
||
<input type="text" id="workLocation" class="form-control" placeholder="작업 현장 위치">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">작업 내용</label>
|
||
<textarea id="workDescription" class="form-control" rows="3" placeholder="오늘 진행할 작업 내용을 입력하세요"></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">안전 관련 특이사항</label>
|
||
<textarea id="safetyNotes" class="form-control" rows="2" placeholder="안전 주의사항이나 특이사항"></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">시작 시간</label>
|
||
<input type="time" id="startTime" class="form-control">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeTbmModal()">취소</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveTbmSession()">
|
||
💾 저장 및 팀 구성하기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 팀 구성 모달 -->
|
||
<div id="teamModal" class="modal-overlay" style="display: none;">
|
||
<div class="modal-container" style="max-width: 900px;">
|
||
<div class="modal-header">
|
||
<h2>팀 구성</h2>
|
||
<button class="modal-close-btn" onclick="closeTeamModal()">×</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<div class="section-header" style="margin-bottom: 1rem;">
|
||
<h3 style="font-size: 1rem; font-weight: 600;">작업자 선택</h3>
|
||
<div>
|
||
<button class="btn btn-sm btn-secondary" onclick="selectAllWorkers()">전체 선택</button>
|
||
<button class="btn btn-sm btn-secondary" onclick="deselectAllWorkers()">전체 해제</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="workerSelectionGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem; max-height: 400px; overflow-y: auto; padding: 0.5rem; border: 1px solid #e5e7eb; border-radius: 0.5rem;">
|
||
<!-- 작업자 체크박스 목록이 여기에 생성됩니다 -->
|
||
</div>
|
||
|
||
<div style="margin-top: 1.5rem;">
|
||
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem;">선택된 팀원 <span id="selectedCount">0</span>명</h3>
|
||
<div id="selectedWorkersList" style="display: flex; flex-wrap: wrap; gap: 0.5rem; min-height: 50px; padding: 0.75rem; background: #f9fafb; border-radius: 0.5rem;">
|
||
<p style="margin: 0; color: #9ca3af; font-size: 0.875rem;">작업자를 선택해주세요</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeTeamModal()">취소</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveTeamComposition()">
|
||
👥 팀 구성 완료
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 안전 체크리스트 모달 -->
|
||
<div id="safetyModal" class="modal-overlay" style="display: none;">
|
||
<div class="modal-container" style="max-width: 700px;">
|
||
<div class="modal-header">
|
||
<h2>안전 체크리스트</h2>
|
||
<button class="modal-close-btn" onclick="closeSafetyModal()">×</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<div id="safetyChecklistContainer" style="max-height: 500px; overflow-y: auto;">
|
||
<!-- 안전 체크리스트가 여기에 생성됩니다 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeSafetyModal()">취소</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveSafetyChecklist()">
|
||
✅ 안전 체크 완료
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TBM 완료 모달 -->
|
||
<div id="completeModal" class="modal-overlay" style="display: none;">
|
||
<div class="modal-container" style="max-width: 500px;">
|
||
<div class="modal-header">
|
||
<h2>TBM 완료</h2>
|
||
<button class="modal-close-btn" onclick="closeCompleteModal()">×</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<p style="margin-bottom: 1rem;">이 TBM 세션을 완료 처리하시겠습니까?</p>
|
||
<p style="color: #6b7280; font-size: 0.875rem;">완료 후에는 수정할 수 없습니다.</p>
|
||
|
||
<div class="form-group" style="margin-top: 1.5rem;">
|
||
<label class="form-label">종료 시간</label>
|
||
<input type="time" id="endTime" class="form-control">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeCompleteModal()">취소</button>
|
||
<button type="button" class="btn btn-primary" onclick="completeTbmSession()">
|
||
✅ 완료
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 작업 인계 모달 -->
|
||
<div id="handoverModal" class="modal-overlay" style="display: none;">
|
||
<div class="modal-container" style="max-width: 600px;">
|
||
<div class="modal-header">
|
||
<h2>작업 인계</h2>
|
||
<button class="modal-close-btn" onclick="closeHandoverModal()">×</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<form id="handoverForm">
|
||
<input type="hidden" id="handoverSessionId">
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">인계 사유 *</label>
|
||
<select id="handoverReason" class="form-control" required>
|
||
<option value="">사유 선택...</option>
|
||
<option value="half_day">반차</option>
|
||
<option value="early_leave">조퇴</option>
|
||
<option value="emergency">긴급 상황</option>
|
||
<option value="other">기타</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">인수자 (다음 팀장) *</label>
|
||
<select id="toLeaderId" class="form-control" required>
|
||
<option value="">인수자 선택...</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">인계 날짜 *</label>
|
||
<input type="date" id="handoverDate" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">인계 시간</label>
|
||
<input type="time" id="handoverTime" class="form-control">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">인계 내용</label>
|
||
<textarea id="handoverNotes" class="form-control" rows="4" placeholder="인수자에게 전달할 내용을 입력하세요"></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label" style="margin-bottom: 0.75rem; display: block;">인계할 팀원 선택</label>
|
||
<div id="handoverTeamList" style="max-height: 200px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 0.5rem;">
|
||
<!-- 팀원 체크박스 목록 -->
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeHandoverModal()">취소</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveHandover()">
|
||
📤 인계 요청
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TBM 상세보기 모달 -->
|
||
<div id="detailModal" class="modal-overlay" style="display: none;">
|
||
<div class="modal-container" style="max-width: 900px;">
|
||
<div class="modal-header">
|
||
<h2>TBM 상세 정보</h2>
|
||
<button class="modal-close-btn" onclick="closeDetailModal()">×</button>
|
||
</div>
|
||
|
||
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||
<!-- 세션 기본 정보 -->
|
||
<div class="section" style="margin-bottom: 1.5rem;">
|
||
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">기본 정보</h3>
|
||
<div id="detailBasicInfo" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem;">
|
||
<!-- 동적 생성 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 팀 구성 -->
|
||
<div class="section" style="margin-bottom: 1.5rem;">
|
||
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">팀 구성</h3>
|
||
<div id="detailTeamMembers" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem;">
|
||
<!-- 동적 생성 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 안전 체크 -->
|
||
<div class="section">
|
||
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">안전 체크리스트</h3>
|
||
<div id="detailSafetyChecks">
|
||
<!-- 동적 생성 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeDetailModal()">닫기</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 토스트 알림 -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
</div>
|
||
|
||
<script type="module" src="/js/load-navbar.js?v=5"></script>
|
||
<script type="module" src="/js/tbm.js?v=2"></script>
|
||
</body>
|
||
</html>
|