Files
TK-FB-Project/web-ui/pages/work/tbm.html
Hyungi Ahn 480206912b feat: TBM 시스템 완성 - 작업 인계, 상세보기, 작업보고서 연동
## 주요 기능 추가

### 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>
2026-01-20 15:46:02 +09:00

341 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>