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>
This commit is contained in:
Hyungi Ahn
2026-01-20 15:46:02 +09:00
parent f8138685a1
commit 480206912b
3 changed files with 433 additions and 8 deletions

View File

@@ -294,11 +294,68 @@ async function loadErrorTypes() {
}
}
// TBM 팀 구성 자동 불러오기
async function loadTbmTeamForDate(date) {
try {
console.log('🛠️ TBM 팀 구성 조회 중:', date);
const response = await window.apiCall(`/tbm/sessions/date/${date}`);
if (response && response.success && response.data && response.data.length > 0) {
// 가장 최근 세션 선택 (진행중인 세션 우선)
const draftSessions = response.data.filter(s => s.status === 'draft');
const targetSession = draftSessions.length > 0 ? draftSessions[0] : response.data[0];
if (targetSession) {
// 팀 구성 조회
const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`);
if (teamRes && teamRes.success && teamRes.data) {
const teamWorkerIds = teamRes.data.map(m => m.worker_id);
console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}`);
return teamWorkerIds;
}
}
}
console.log(' 해당 날짜의 TBM 팀 구성이 없습니다.');
return [];
} catch (error) {
console.error('❌ TBM 팀 구성 조회 오류:', error);
return [];
}
}
// 작업자 그리드 생성
function populateWorkerGrid() {
async function populateWorkerGrid() {
const grid = document.getElementById('workerGrid');
grid.innerHTML = '';
// 선택된 날짜의 TBM 팀 구성 불러오기
const reportDate = document.getElementById('reportDate').value;
let tbmWorkerIds = [];
if (reportDate) {
tbmWorkerIds = await loadTbmTeamForDate(reportDate);
}
// TBM 팀 구성이 있으면 안내 메시지 표시
if (tbmWorkerIds.length > 0) {
const infoDiv = document.createElement('div');
infoDiv.style.cssText = `
padding: 1rem;
background: #eff6ff;
border: 1px solid #3b82f6;
border-radius: 0.5rem;
margin-bottom: 1rem;
color: #1e40af;
font-size: 0.875rem;
`;
infoDiv.innerHTML = `
<strong>🛠️ TBM 팀 구성 자동 적용</strong><br>
오늘 TBM에서 구성된 팀원 ${tbmWorkerIds.length}명이 자동으로 선택되었습니다.
`;
grid.appendChild(infoDiv);
}
workers.forEach(worker => {
const btn = document.createElement('button');
btn.type = 'button';
@@ -306,12 +363,24 @@ function populateWorkerGrid() {
btn.textContent = worker.worker_name;
btn.dataset.id = worker.worker_id;
// TBM 팀 구성에 포함된 작업자는 자동 선택
if (tbmWorkerIds.includes(worker.worker_id)) {
btn.classList.add('selected');
selectedWorkers.add(worker.worker_id);
}
btn.addEventListener('click', () => {
toggleWorkerSelection(worker.worker_id, btn);
});
grid.appendChild(btn);
});
// 자동 선택된 작업자가 있으면 다음 단계 버튼 활성화
const nextBtn = document.getElementById('nextStep2');
if (nextBtn) {
nextBtn.disabled = selectedWorkers.size === 0;
}
}
// 작업자 선택 토글