Files
TK-FB-Project/docs/TBM_DEPLOYMENT_GUIDE.md
Hyungi Ahn 94bccf3b67 docs: TBM 배포 가이드 전면 개편 (v2.0)
기존 중구난방이던 문서를 체계적으로 재구성

## 주요 변경사항
- 목차 추가 (9개 섹션)
- 시스템 아키텍처 다이어그램 추가
- 배포 전 확인사항 섹션 신설
- 단계별 배포 절차 상세화
- 기능 명세 및 사용자 시나리오 추가
- API 명세 예시 코드 포함
- 프론트엔드 구현 세부 함수 코드 추가
- 테스트 가이드 확대 (API, 웹, 성능)
- 문제 해결 섹션 강화 (5개 카테고리)
- 데이터베이스 스키마 명시

## 문서 구조
1. 시스템 개요
2. 배포 전 확인사항
3. 배포 절차
4. 기능 명세
5. API 명세
6. 프론트엔드 구현
7. 테스트 가이드
8. 문제 해결
9. 변경 이력

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 10:14:17 +09:00

36 KiB

TBM 시스템 배포 가이드

📋 문서 개요

목적: TBM(Tool Box Meeting) 시스템 배포 및 사용 가이드 대상: 시스템 관리자, 개발자 버전: v1.2.0 최종 수정일: 2026-01-26


📑 목차

  1. 시스템 개요
  2. 배포 전 확인사항
  3. 배포 절차
  4. 기능 명세
  5. API 명세
  6. 프론트엔드 구현
  7. 테스트 가이드
  8. 문제 해결
  9. 변경 이력

시스템 개요

1.1 TBM이란?

TBM (Tool Box Meeting): 작업 시작 전 아침 안전 회의

  • 팀 구성 및 역할 분배
  • 안전 체크리스트 확인
  • 작업 내용 및 장소 공유
  • 특이사항 및 주의사항 전달

1.2 주요 기능

기능 설명 버전
TBM 세션 관리 날짜별 TBM 생성, 수정, 완료 v1.0.0
팀 구성 관리 팀장 지정, 팀원 선택, 출석 관리 v1.0.0
안전 체크리스트 17개 항목 카테고리별 체크 v1.0.0
작업 인계 팀장 조퇴/반차 시 인계 기능 v1.1.0
TBM 상세보기 세션 정보 통합 조회 v1.1.0
작업 보고서 연동 TBM 팀원 자동 선택 v1.1.0
대시보드 통합 빠른 작업 배너 (권한 기반) v1.2.0

1.3 시스템 아키텍처

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Dashboard     │────▶│   TBM Page       │────▶│  Work Report    │
│ (빠른 작업)      │     │ (TBM 관리)        │     │ (팀원 자동선택)  │
└─────────────────┘     └──────────────────┘     └─────────────────┘
         │                       │                         │
         │                       ▼                         │
         │              ┌──────────────────┐              │
         └─────────────▶│   TBM API        │◀─────────────┘
                        │ (17 endpoints)   │
                        └──────────────────┘
                                 │
                        ┌────────▼────────┐
                        │  MySQL Database │
                        │  (5 tables)     │
                        └─────────────────┘

배포 전 확인사항

2.1 환경 요구사항

  • Node.js: v14 이상
  • MySQL: v8.0 이상
  • PM2: 프로세스 관리 (선택)
  • 데이터베이스: hyungi 스키마 존재

2.2 필수 의존성

{
  "knex": "^2.x",
  "mysql2": "^3.x"
}

2.3 기존 시스템 확인

# 테이블 존재 확인
mysql -u root -p -e "SHOW TABLES LIKE 'workers'" hyungi
mysql -u root -p -e "SHOW TABLES LIKE 'users'" hyungi
mysql -u root -p -e "SHOW TABLES LIKE 'projects'" hyungi
mysql -u root -p -e "SHOW TABLES LIKE 'pages'" hyungi

# workers.worker_id 데이터 타입 확인 (INT(11) signed 필수)
mysql -u root -p -e "DESCRIBE workers" hyungi | grep worker_id

중요: worker_idsigned INT(11) 이어야 함 (unsigned 아님)


배포 절차

3.1 데이터베이스 백업 (필수)

# 배포 전 백업
mysqldump -u root -p hyungi > backup_before_tbm_$(date +%Y%m%d_%H%M%S).sql

3.2 마이그레이션 실행

3.2.1 마이그레이션 파일 확인

cd /path/to/api.hyungi.net
ls -l db/migrations/*tbm*

필수 파일:

  • 20260120000000_create_tbm_system.js - TBM 테이블 5개 생성
  • 20260120000001_add_tbm_page.js - pages 테이블에 TBM 페이지 등록

3.2.2 마이그레이션 실행

# 환경 변수 설정 (필요 시)
export DB_HOST=your_db_host
export DB_PORT=your_db_port
export DB_USER=your_db_user
export DB_PASSWORD=your_db_password
export DB_NAME=hyungi

# 마이그레이션 실행
npm run db:migrate

# 또는
npx knex migrate:latest --knexfile knexfile.js

3.2.3 마이그레이션 확인

# 상태 확인
npx knex migrate:status

# 테이블 생성 확인
mysql -u root -p -e "SHOW TABLES LIKE 'tbm%'" hyungi
mysql -u root -p -e "SHOW TABLES LIKE 'team_handovers'" hyungi

예상 결과:

+------------------------------+
| Tables_in_hyungi (tbm%)      |
+------------------------------+
| tbm_safety_checks            |
| tbm_safety_records           |
| tbm_sessions                 |
| tbm_team_assignments         |
+------------------------------+

3.2.4 초기 데이터 확인

# 안전 체크리스트 17개 항목 확인
mysql -u root -p -e "
  SELECT check_category, COUNT(*) as count
  FROM tbm_safety_checks
  GROUP BY check_category
" hyungi

예상 결과:

+----------------+-------+
| check_category | count |
+----------------+-------+
| PPE            |     5 |
| EQUIPMENT      |     4 |
| ENVIRONMENT    |     4 |
| EMERGENCY      |     3 |
+----------------+-------+

3.3 API 서버 재시작

# PM2 사용 시
pm2 restart api-hyungi

# 또는 직접 재시작
pm2 stop api-hyungi
pm2 start ecosystem.config.js --env production

# 로그 확인
pm2 logs api-hyungi --lines 50

3.4 페이지 권한 설정

# TBM 페이지 등록 확인
mysql -u root -p -e "SELECT * FROM pages WHERE page_key='tbm'\G" hyungi

예상 결과:

        page_id: [auto_increment]
       page_key: tbm
      page_name: TBM 관리
      page_path: /pages/work/tbm.html
       category: work
    description: Tool Box Meeting - 아침 안전 회의 및 팀 구성 관리
  is_admin_only: 0
  display_order: 10

3.5 웹 서버 재시작

# Nginx 재시작 (정적 파일 갱신)
docker restart tkfb_web

# 또는
sudo systemctl restart nginx

3.6 배포 체크리스트

  • 데이터베이스 백업 완료
  • 마이그레이션 실행 완료
  • 5개 테이블 생성 확인
  • tbm_safety_checks 초기 데이터 (17개) 확인
  • pages 테이블에 TBM 페이지 등록 확인
  • API 서버 재시작 완료
  • 웹 서버 재시작 완료
  • API 엔드포인트 테스트 (최소 1개)
  • TBM 페이지 접속 테스트
  • 권한 설정 테스트 (그룹장 계정)
  • 대시보드 빠른 작업 표시 확인

기능 명세

4.1 TBM 세션 관리

목적: 날짜별 아침 안전 회의 생성 및 관리

주요 기능:

  • TBM 세션 생성 (날짜, 팀장, 프로젝트, 장소, 작업 내용, 안전 특이사항)
  • 시작 시간/종료 시간 기록
  • 상태 관리 (draft, completed, cancelled)
  • 세션 수정 및 삭제

사용자 시나리오:

  1. 팀장이 매일 아침 TBM 페이지 접속
  2. "새 TBM 시작" 클릭
  3. 오늘 날짜, 프로젝트, 작업 장소, 작업 내용 입력
  4. 안전 특이사항 기록 (선택)
  5. "저장 및 팀 구성하기" 클릭

4.2 팀 구성 관리

목적: 작업 팀원 선택 및 역할 분배

주요 기능:

  • 다중 선택으로 팀원 지정
  • 각 팀원의 역할/담당 업무 기록 (선택)
  • 출석/결석 처리
  • 결석 사유 기록

사용자 시나리오:

  1. TBM 세션 저장 후 팀 구성 모달 자동 표시
  2. 체크박스로 팀원 선택 (전체 선택/해제 버튼 제공)
  3. 선택된 팀원 확인 (N명 표시)
  4. "팀 구성 완료" 클릭

작업 보고서 연동:

  • 작업 보고서 페이지에서 TBM 팀원이 자동으로 선택됨
  • 안내 메시지: "오늘 TBM에서 구성된 팀원 N명이 자동으로 선택되었습니다."

4.3 안전 체크리스트

목적: 작업 전 안전 점검 항목 확인

카테고리 및 항목 (총 17개):

카테고리 항목 수 예시
PPE (개인 보호 장비) 5 안전모, 안전화, 안전벨트, 보호안경, 안전장갑
EQUIPMENT (장비 점검) 4 공구 점검, 전기 장비, 사다리/비계, 안전 표지판
ENVIRONMENT (작업 환경) 4 작업 구역 정리, 위험물 확인, 환기, 조명
EMERGENCY (비상 대응) 3 비상 연락망, 응급 처치함, 대피 경로

주요 기능:

  • 카테고리별 체크리스트 표시
  • 각 항목 체크 (is_checked: true/false)
  • 특이사항/비고 입력 (선택)
  • 체크한 사람 및 시간 기록

사용자 시나리오:

  1. TBM 세션 카드에서 "안전 체크" 버튼 클릭
  2. 카테고리별로 항목 확인
  3. 각 항목 체크 및 특이사항 입력
  4. "안전 체크 완료" 클릭

4.4 작업 인계 시스템

목적: 팀장 조퇴/반차 시 작업 인계

인계 사유:

  • half_day: 반차
  • early_leave: 조퇴
  • emergency: 긴급 상황
  • other: 기타

주요 기능:

  • 인수자(다음 팀장) 선택
  • 인계할 팀원 선택 (기본: 전체 선택)
  • 인계 날짜/시간 기록
  • 인계 내용 작성
  • 인수자 확인 프로세스

사용자 시나리오:

  1. TBM 세션 카드에서 "인계" 버튼 클릭
  2. 인계 사유 선택 (반차, 조퇴, 긴급, 기타)
  3. 인수자(다음 팀장) 선택
  4. 인계할 팀원 선택 (체크박스)
  5. 인계 내용 입력 (특이사항, 주의사항 등)
  6. "인계 요청" 클릭
  7. 인수자가 확인 후 "인수 확인" 처리

4.5 TBM 상세보기

목적: TBM 세션의 모든 정보를 한눈에 확인

표시 정보:

  • 기본 정보: 날짜, 팀장, 프로젝트, 작업 장소, 작업 내용, 안전 특이사항, 시작/종료 시간
  • 팀 구성: 팀원 목록 (이름, 역할, 출석 여부)
  • 안전 체크: 카테고리별 체크리스트 상태 (체크됨/안됨, 특이사항)

성능 최적화:

// 병렬 API 호출로 로딩 속도 개선
const [sessionRes, teamRes, safetyRes] = await Promise.all([
  window.apiCall(`/tbm/sessions/${sessionId}`),
  window.apiCall(`/tbm/sessions/${sessionId}/team`),
  window.apiCall(`/tbm/sessions/${sessionId}/safety`)
]);

4.6 대시보드 빠른 작업

목적: 대시보드에서 TBM 페이지로 빠른 접근

주요 특징:

  • 권한 기반 표시: 페이지 접근 권한이 있는 사용자만 표시
  • 시각적 구분: 오렌지 그라데이션 배경 (#f59e0b#d97706)
  • 동적 권한 체크: 로그인 시 자동으로 권한 확인

권한 체크 로직:

async function checkTbmPageAccess() {
  const response = await window.apiCall(`/users/${currentUser.user_id}/page-access`);
  const tbmPage = response.data.pageAccess.find(p => p.page_key === 'tbm');
  if (tbmPage && tbmPage.can_access) {
    document.getElementById('tbmQuickAction').style.display = 'block';
  }
}

API 명세

5.1 TBM 세션 관리

POST /api/tbm/sessions

설명: TBM 세션 생성 요청 본문:

{
  "session_date": "2026-01-26",
  "leader_id": 1,
  "project_id": 5,
  "work_location": "현장 A동",
  "work_description": "배관 설치 작업",
  "safety_notes": "고소 작업 주의",
  "start_time": "08:00:00"
}

응답:

{
  "success": true,
  "message": "TBM 세션이 생성되었습니다",
  "data": { "session_id": 123 }
}

GET /api/tbm/sessions/date/:date

설명: 특정 날짜의 TBM 세션 목록 조회 예시: GET /api/tbm/sessions/date/2026-01-26 응답:

{
  "success": true,
  "data": [
    {
      "session_id": 123,
      "session_date": "2026-01-26",
      "leader_name": "김팀장",
      "project_name": "아파트 건설",
      "status": "draft"
    }
  ]
}

GET /api/tbm/sessions/:sessionId

설명: TBM 세션 상세 조회 예시: GET /api/tbm/sessions/123

PUT /api/tbm/sessions/:sessionId

설명: TBM 세션 수정

POST /api/tbm/sessions/:sessionId/complete

설명: TBM 세션 완료 처리 요청 본문:

{
  "end_time": "17:00:00"
}

5.2 팀 구성 관리

POST /api/tbm/sessions/:sessionId/team/batch

설명: 팀원 일괄 추가 요청 본문:

{
  "workers": [
    { "worker_id": 10, "assigned_role": "용접" },
    { "worker_id": 11, "assigned_role": "보조" }
  ]
}

GET /api/tbm/sessions/:sessionId/team

설명: 팀 구성 조회

DELETE /api/tbm/sessions/:sessionId/team/:workerId

설명: 팀원 제거

5.3 안전 체크리스트

GET /api/tbm/safety-checks

설명: 모든 안전 체크 항목 조회 (17개)

GET /api/tbm/sessions/:sessionId/safety

설명: 특정 세션의 안전 체크 기록 조회

POST /api/tbm/sessions/:sessionId/safety

설명: 안전 체크 일괄 저장 요청 본문:

{
  "checks": [
    { "check_id": 1, "is_checked": true, "notes": "" },
    { "check_id": 2, "is_checked": true, "notes": "안전화 교체 필요" }
  ]
}

5.4 작업 인계

POST /api/tbm/handovers

설명: 작업 인계 생성 요청 본문:

{
  "session_id": 123,
  "from_leader_id": 1,
  "to_leader_id": 2,
  "handover_date": "2026-01-26",
  "handover_time": "15:00:00",
  "reason": "early_leave",
  "handover_notes": "3시 조퇴로 인계합니다",
  "worker_ids": [10, 11, 12]
}

POST /api/tbm/handovers/:handoverId/confirm

설명: 작업 인계 확인 (인수자가 확인)

GET /api/tbm/handovers/pending

설명: 나에게 온 미확인 인계 건

5.5 통계 및 리포트

GET /api/tbm/statistics/tbm

설명: TBM 통계 쿼리 파라미터: startDate, endDate 예시: GET /api/tbm/statistics/tbm?startDate=2026-01-01&endDate=2026-01-31

GET /api/tbm/statistics/leaders

설명: 리더별 TBM 통계


프론트엔드 구현

6.1 파일 구조

web-ui/
├── pages/
│   ├── dashboard.html                  # 대시보드 (TBM 빠른 작업 추가됨)
│   └── work/
│       └── tbm.html                    # TBM 페이지
├── js/
│   ├── tbm.js                          # TBM JavaScript 로직
│   ├── modern-dashboard.js             # 대시보드 (TBM 권한 체크 추가됨)
│   └── daily-work-report.js            # 작업 보고서 (TBM 연동 추가됨)
└── css/
    └── project-management.css          # TBM 페이지 스타일

6.2 주요 함수

tbm.js

// TBM 세션 목록 표시
async function loadTbmSessions(date) {
  const response = await window.apiCall(`/tbm/sessions/date/${date}`);
  displayTbmSessions(response.data);
}

// TBM 세션 저장
async function saveTbmSession() {
  const sessionData = {
    session_date: document.getElementById('sessionDate').value,
    leader_id: document.getElementById('leaderId').value,
    // ...
  };
  const response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
  if (response.success) {
    openTeamModal(response.data.session_id);
  }
}

// 팀 구성 저장
async function saveTeamComposition() {
  const selectedWorkers = Array.from(
    document.querySelectorAll('input[name="worker"]:checked')
  ).map(cb => ({ worker_id: parseInt(cb.value) }));

  await window.apiCall(
    `/tbm/sessions/${currentSessionId}/team/batch`,
    'POST',
    { workers: selectedWorkers }
  );
}

// 안전 체크리스트 저장
async function saveSafetyChecklist() {
  const checks = [];
  document.querySelectorAll('.safety-check-item').forEach(item => {
    checks.push({
      check_id: item.dataset.checkId,
      is_checked: item.querySelector('input[type="checkbox"]').checked,
      notes: item.querySelector('textarea')?.value || ''
    });
  });

  await window.apiCall(
    `/tbm/sessions/${currentSessionId}/safety`,
    'POST',
    { checks }
  );
}

// TBM 상세보기 (병렬 API 호출)
async function viewTbmSession(sessionId) {
  const [sessionRes, teamRes, safetyRes] = await Promise.all([
    window.apiCall(`/tbm/sessions/${sessionId}`),
    window.apiCall(`/tbm/sessions/${sessionId}/team`),
    window.apiCall(`/tbm/sessions/${sessionId}/safety`)
  ]);

  // 데이터 표시
  displaySessionDetail(sessionRes.data, teamRes.data, safetyRes.data);
}

// 작업 인계
async function saveHandover() {
  const handoverData = {
    session_id: currentSessionId,
    from_leader_id: fromLeaderId,
    to_leader_id: document.getElementById('toLeaderId').value,
    handover_date: document.getElementById('handoverDate').value,
    handover_time: document.getElementById('handoverTime').value,
    reason: document.getElementById('handoverReason').value,
    handover_notes: document.getElementById('handoverNotes').value,
    worker_ids: Array.from(
      document.querySelectorAll('input[name="handoverWorker"]:checked')
    ).map(cb => parseInt(cb.value))
  };

  await window.apiCall('/tbm/handovers', 'POST', handoverData);
}

daily-work-report.js

// TBM 팀 구성 자동 로드
async function loadTbmTeamForDate(date) {
  try {
    const response = await window.apiCall(`/tbm/sessions/date/${date}`);

    if (response && response.success && response.data && response.data.length > 0) {
      // Draft 세션 우선 선택
      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;
        }
      }
    }

    return [];
  } catch (error) {
    console.error('❌ TBM 팀 구성 조회 오류:', error);
    return [];
  }
}

// 작업자 그리드 생성 (TBM 팀원 자동 선택)
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);
  }

  // 안내 메시지 표시
  if (tbmWorkerIds.length > 0) {
    const infoDiv = document.createElement('div');
    infoDiv.className = 'tbm-auto-select-info';
    infoDiv.innerHTML = `
      <strong>🛠️ TBM 팀 구성 자동 적용</strong><br>
      오늘 TBM에서 구성된 팀원 ${tbmWorkerIds.length}명이 자동으로 선택되었습니다.
    `;
    grid.appendChild(infoDiv);
  }

  // 작업자 버튼 생성
  workers.forEach(worker => {
    const btn = document.createElement('button');
    btn.className = 'worker-btn';
    btn.textContent = worker.worker_name;

    // TBM 팀원 자동 선택
    if (tbmWorkerIds.includes(worker.worker_id)) {
      btn.classList.add('selected');
      selectedWorkers.add(worker.worker_id);
    }

    btn.onclick = () => toggleWorker(worker.worker_id, btn);
    grid.appendChild(btn);
  });
}

modern-dashboard.js

// TBM 페이지 접근 권한 확인
async function checkTbmPageAccess() {
  try {
    if (!currentUser || !currentUser.user_id) {
      console.log('⚠️ TBM 페이지 권한 확인: 사용자 정보 없음');
      return;
    }

    console.log('🛠️ TBM 페이지 권한 확인 중...');

    // 사용자의 페이지 접근 권한 조회
    const response = await window.apiCall(
      `/users/${currentUser.user_id}/page-access`
    );

    if (response && response.success) {
      const pageAccess = response.data?.pageAccess || [];

      // 'tbm' 페이지 접근 권한 확인
      const tbmPage = pageAccess.find(p => p.page_key === 'tbm');
      const tbmQuickAction = document.getElementById('tbmQuickAction');

      if (tbmPage && tbmPage.can_access && tbmQuickAction) {
        tbmQuickAction.style.display = 'block';
        console.log('✅ TBM 페이지 접근 권한 있음 - 빠른 작업 버튼 표시');
      } else {
        console.log('❌ TBM 페이지 접근 권한 없음 - 빠른 작업 버튼 숨김');
      }
    }
  } catch (error) {
    console.error('❌ TBM 페이지 권한 확인 오류:', error);
  }
}

// 대시보드 초기화 시 호출
async function initializeDashboard() {
  // ...
  await loadDashboardData();
  checkAdminAccess();
  checkTbmPageAccess();  // TBM 권한 체크 추가
}

6.3 CSS 스타일

dashboard.html - TBM 빠른 작업 카드

<a href="/pages/work/tbm.html"
   class="quick-action-card"
   id="tbmQuickAction"
   style="display: none;
          background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
          color: white;">
  <div class="action-content">
    <h3 style="color: white;">🛠️ TBM 관리</h3>
    <p style="color: rgba(255, 255, 255, 0.9);">
      아침 안전 회의 및 팀 구성을 관리합니다
    </p>
  </div>
  <div class="action-arrow" style="color: white;"></div>
</a>

스타일 특징:

  • display: none: 기본 숨김 (권한 확인 후 표시)
  • background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%): 오렌지 그라데이션
  • color: white: 흰색 텍스트 (가독성)

테스트 가이드

7.1 API 테스트

# 환경 변수 설정 (토큰)
export TOKEN="your_jwt_token_here"

# 1. 안전 체크 항목 조회
curl -X GET "http://localhost:20005/api/tbm/safety-checks" \
  -H "Authorization: Bearer $TOKEN"

# 2. 오늘 날짜의 TBM 세션 조회
curl -X GET "http://localhost:20005/api/tbm/sessions/date/$(date +%Y-%m-%d)" \
  -H "Authorization: Bearer $TOKEN"

# 3. TBM 세션 생성
curl -X POST "http://localhost:20005/api/tbm/sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "session_date": "2026-01-26",
    "leader_id": 1,
    "project_id": 5,
    "work_location": "현장 A동",
    "work_description": "배관 설치 작업"
  }'

7.2 웹 페이지 테스트

7.2.1 권한 확인 테스트

  1. 관리자 계정으로 로그인
  2. /pages/admin/page-access.html 접속
  3. 그룹장 계정에 "TBM 관리" 페이지 권한 부여
  4. 그룹장 계정으로 재로그인
  5. 대시보드에서 "TBM 관리" 빠른 작업 카드 확인

7.2.2 TBM 세션 생성 테스트

  1. TBM 페이지 접속 (/pages/work/tbm.html)
  2. "새 TBM 시작" 클릭
  3. 폼 입력:
    • 날짜: 오늘
    • 팀장: 김그룹장
    • 프로젝트: 아파트 건설
    • 작업 장소: A동 5층
    • 작업 내용: 배관 설치
  4. "저장 및 팀 구성하기" 클릭
  5. 팀 구성 모달에서 팀원 3명 선택
  6. "팀 구성 완료" 클릭
  7. TBM 세션 카드가 목록에 표시되는지 확인

7.2.3 안전 체크리스트 테스트

  1. TBM 세션 카드에서 "안전 체크" 버튼 클릭
  2. PPE 카테고리 항목 5개 모두 체크
  3. EQUIPMENT 카테고리에서 "전기 장비 점검" 체크 후 특이사항 입력: "전압 확인 필요"
  4. "안전 체크 완료" 클릭
  5. 세션 카드에서 안전 체크 완료 표시 확인

7.2.4 작업 인계 테스트

  1. TBM 세션 카드에서 "인계" 버튼 클릭
  2. 인계 사유: "조퇴" 선택
  3. 인수자: 이그룹장 선택
  4. 팀원 2명 선택 (기본: 전체 선택됨)
  5. 인계 내용: "3시 조퇴로 인계합니다. 배관 작업 마무리 부탁드립니다."
  6. "인계 요청" 클릭
  7. 인계 완료 메시지 확인

7.2.5 작업 보고서 연동 테스트

  1. TBM 세션 생성 및 팀 구성 완료 (팀원 5명)
  2. "작업 보고서 작성" 페이지 이동
  3. 보고 날짜: TBM 세션과 동일한 날짜 선택
  4. "다음" 버튼 클릭 (작업자 선택 단계)
  5. 확인 사항:
    • TBM 팀원 5명이 자동으로 선택되어 있음
    • 안내 메시지: "오늘 TBM에서 구성된 팀원 5명이 자동으로 선택되었습니다."
  6. "다음" 버튼이 활성화되어 있는지 확인

7.2.6 TBM 상세보기 테스트

  1. TBM 세션 카드 클릭
  2. 상세보기 모달 확인:
    • 기본 정보: 날짜, 팀장, 프로젝트, 장소, 작업 내용
    • 팀 구성: 팀원 목록 및 역할
    • 안전 체크: 카테고리별 체크 상태 (/)
  3. 모달 닫기

7.3 성능 테스트

// 브라우저 개발자 도구 콘솔에서 실행

// TBM 상세보기 로딩 시간 측정
console.time('TBM Detail Load');
await viewTbmSession(123);
console.timeEnd('TBM Detail Load');
// 예상: < 500ms (병렬 API 호출로 최적화)

// 작업 보고서 팀원 자동 선택 시간 측정
console.time('TBM Team Auto-select');
await loadTbmTeamForDate('2026-01-26');
console.timeEnd('TBM Team Auto-select');
// 예상: < 300ms

문제 해결

8.1 마이그레이션 오류

오류: errno: 150 (외래 키 제약 조건 오류)

증상:

Error: ER_CANNOT_ADD_FOREIGN: Cannot add foreign key constraint
errno: 150

원인: 외래 키 컬럼과 참조 테이블의 PK 데이터 타입 불일치

해결:

  1. 참조 테이블의 PK 데이터 타입 확인:
DESCRIBE workers;  -- worker_id는 INT(11) signed
DESCRIBE users;    -- user_id는 INT(11) signed
  1. 마이그레이션 파일에서 외래 키 컬럼을 signed INT로 수정:
table.integer('leader_id').notNullable();  // unsigned 제거
  1. 마이그레이션 재실행:
npx knex migrate:rollback
npx knex migrate:latest

오류: 테이블 이미 존재

증상:

Error: Table 'tbm_sessions' already exists

해결:

# 마이그레이션 상태 확인
npx knex migrate:status

# 이미 실행된 마이그레이션이면 스킵
# 테이블 수동 삭제 후 재실행 (주의: 데이터 손실)
mysql -u root -p -e "DROP TABLE IF EXISTS tbm_safety_records, tbm_team_assignments, tbm_sessions, tbm_safety_checks, team_handovers" hyungi
npx knex migrate:latest

8.2 API 오류

오류: 404 Not Found

증상:

GET /api/tbm/sessions/date/2026-01-26 404

원인: 라우트 등록 누락 또는 서버 미재시작

해결:

  1. 라우트 등록 확인:
// api.hyungi.net/config/routes.js
const tbmRoutes = require('../routes/tbmRoutes');
app.use('/api/tbm', tbmRoutes);
  1. 서버 재시작:
pm2 restart api-hyungi
pm2 logs api-hyungi --lines 50

오류: 401 Unauthorized

증상:

{ "message": "인증 토큰이 필요합니다" }

원인: JWT 토큰 누락 또는 만료

해결:

  1. 로그인 후 새 토큰 발급
  2. 요청 헤더에 토큰 포함:
headers: {
  'Authorization': `Bearer ${token}`
}

8.3 프론트엔드 오류

오류: TBM 빠른 작업 카드가 표시되지 않음

증상: 권한이 있는 사용자인데도 대시보드에 TBM 카드가 보이지 않음

원인:

  1. 페이지 권한 미설정
  2. JavaScript 오류로 checkTbmPageAccess() 미실행
  3. API 응답 구조 변경

해결:

  1. 페이지 권한 확인:
SELECT u.username, pa.page_key, pa.can_access
FROM users u
LEFT JOIN user_page_access pa ON u.user_id = pa.user_id
LEFT JOIN pages p ON pa.page_id = p.page_id
WHERE u.user_id = 1 AND p.page_key = 'tbm';
  1. 브라우저 개발자 도구 콘솔 확인:
// 콘솔에 "✅ TBM 페이지 접근 권한 있음" 메시지가 있어야 함
// 오류가 있다면 확인
  1. API 응답 확인:
curl -X GET "http://localhost:20005/api/users/1/page-access" \
  -H "Authorization: Bearer $TOKEN"
  1. 캐시 삭제 및 강력 새로고침 (Ctrl+Shift+R / Cmd+Shift+R)

오류: TBM 팀원이 작업 보고서에 자동 선택되지 않음

증상: TBM 세션을 만들었는데 작업 보고서에서 팀원이 자동 선택되지 않음

원인:

  1. TBM 세션 날짜와 작업 보고서 날짜 불일치
  2. TBM 세션이 completed 상태 (draft 세션만 자동 선택됨)
  3. API 호출 오류

해결:

  1. 날짜 일치 확인:
// 콘솔에서 확인
const reportDate = document.getElementById('reportDate').value;
console.log('보고서 날짜:', reportDate);
  1. TBM 세션 상태 확인:
SELECT session_id, session_date, status
FROM tbm_sessions
WHERE session_date = '2026-01-26';
  1. 콘솔 로그 확인:
// "✅ TBM 팀 구성 로드 성공: N명" 메시지가 있어야 함
// "❌ TBM 팀 구성 조회 오류" 메시지가 있다면 API 오류

8.4 권한 문제

오류: TBM 페이지 접근 불가 (403 Forbidden)

증상: TBM 페이지 접속 시 권한 없음 메시지

원인: 사용자에게 TBM 페이지 접근 권한 미부여

해결:

  1. 관리자 계정으로 로그인
  2. /pages/admin/page-access.html 접속
  3. 권한 부여할 사용자 선택
  4. "TBM 관리" 페이지 체크
  5. "저장" 클릭
  6. 해당 사용자 재로그인

또는 SQL로 직접 권한 부여:

-- TBM 페이지 ID 확인
SELECT page_id FROM pages WHERE page_key = 'tbm';
-- 예: page_id = 15

-- 사용자에게 권한 부여
INSERT INTO user_page_access (user_id, page_id, can_access)
VALUES (3, 15, 1)
ON DUPLICATE KEY UPDATE can_access = 1;

8.5 데이터베이스 문제

오류: 안전 체크리스트 항목이 17개가 아님

증상: SELECT COUNT(*) FROM tbm_safety_checks 결과가 17이 아님

원인: 초기 데이터 삽입 실패

해결:

# 마이그레이션 롤백 후 재실행
npx knex migrate:rollback --step=1
npx knex migrate:latest

# 또는 수동으로 데이터 삽입
mysql -u root -p hyungi < api.hyungi.net/db/migrations/20260120000000_create_tbm_system.js

변경 이력

v1.2.0 (2026-01-26)

기능 추가:

  • 대시보드 TBM 빠른 작업 배너 추가
  • 페이지 접근 권한 기반 표시/숨김 처리
  • checkTbmPageAccess() 함수 추가 (modern-dashboard.js)
  • 오렌지 그라데이션 스타일링 (#f59e0b#d97706)

커밋: 67e9c08

v1.1.0 (2026-01-26)

기능 추가:

  • 작업 인계 시스템
    • 인계 모달 추가 (handoverModal)
    • 인계 사유 선택 (반차, 조퇴, 긴급, 기타)
    • 인수자 선택 및 팀원 인계
    • openHandoverModal(), saveHandover() 함수 추가
  • TBM 상세보기
    • 상세보기 모달 추가 (detailModal)
    • 병렬 API 호출로 성능 최적화 (Promise.all())
    • 카테고리별 안전 체크리스트 표시
    • viewTbmSession() 함수 추가
  • 작업 보고서 연동
    • TBM 팀원 자동 선택 기능
    • loadTbmTeamForDate() 함수 추가 (daily-work-report.js)
    • Draft 세션 우선 처리
    • 자동 선택 안내 메시지 표시

커밋: 4802069, f813868

v1.0.0 (2026-01-20)

초기 배포:

  • 데이터베이스 스키마 5개 테이블 생성
    • tbm_sessions, tbm_team_assignments, tbm_safety_checks, tbm_safety_records, team_handovers
  • 안전 체크리스트 초기 데이터 17개 항목
  • API 엔드포인트 17개 구현
  • TBM 페이지 (/pages/work/tbm.html)
  • 페이지 권한 시스템 연동
  • TBM JavaScript 기본 로직 (tbm.js)

커밋: 4d0c4c0


관련 문서


데이터베이스 스키마

tbm_sessions

CREATE TABLE `tbm_sessions` (
  `session_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `session_date` DATE NOT NULL COMMENT 'TBM 날짜',
  `leader_id` INT NOT NULL COMMENT '팀장 worker_id',
  `project_id` INT NULL COMMENT '프로젝트 ID',
  `work_location` VARCHAR(200) NULL COMMENT '작업 장소',
  `work_description` TEXT NULL COMMENT '작업 내용',
  `safety_notes` TEXT NULL COMMENT '안전 관련 특이사항',
  `status` ENUM('draft', 'completed', 'cancelled') DEFAULT 'draft',
  `start_time` TIME NULL,
  `end_time` TIME NULL,
  `created_by` INT NOT NULL COMMENT '생성자 user_id',
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX `idx_session_date_leader` (`session_date`, `leader_id`),
  FOREIGN KEY (`leader_id`) REFERENCES `workers` (`worker_id`),
  FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`) ON DELETE SET NULL,
  FOREIGN KEY (`created_by`) REFERENCES `users` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

tbm_team_assignments

CREATE TABLE `tbm_team_assignments` (
  `assignment_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `session_id` INT UNSIGNED NOT NULL,
  `worker_id` INT NOT NULL,
  `assigned_role` VARCHAR(100) NULL,
  `work_detail` TEXT NULL,
  `is_present` BOOLEAN DEFAULT TRUE,
  `absence_reason` TEXT NULL,
  `assigned_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY `uk_session_worker` (`session_id`, `worker_id`),
  FOREIGN KEY (`session_id`) REFERENCES `tbm_sessions` (`session_id`) ON DELETE CASCADE,
  FOREIGN KEY (`worker_id`) REFERENCES `workers` (`worker_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

tbm_safety_checks

CREATE TABLE `tbm_safety_checks` (
  `check_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `check_category` VARCHAR(50) NOT NULL COMMENT 'PPE, EQUIPMENT, ENVIRONMENT, EMERGENCY',
  `check_item` VARCHAR(200) NOT NULL,
  `description` TEXT NULL,
  `display_order` INT DEFAULT 0,
  `is_required` BOOLEAN DEFAULT TRUE,
  `is_active` BOOLEAN DEFAULT TRUE,
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX `idx_check_category` (`check_category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

초기 데이터 (17개):

  • PPE: 안전모, 안전화, 안전벨트, 보호안경, 안전장갑
  • EQUIPMENT: 공구 점검, 전기 장비, 사다리/비계, 안전 표지판
  • ENVIRONMENT: 작업 구역 정리, 위험물 확인, 환기, 조명
  • EMERGENCY: 비상 연락망, 응급 처치함, 대피 경로

tbm_safety_records

CREATE TABLE `tbm_safety_records` (
  `record_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `session_id` INT UNSIGNED NOT NULL,
  `check_id` INT UNSIGNED NOT NULL,
  `is_checked` BOOLEAN DEFAULT FALSE,
  `notes` TEXT NULL,
  `checked_by` INT NULL,
  `checked_at` TIMESTAMP NULL,
  UNIQUE KEY `uk_session_check` (`session_id`, `check_id`),
  FOREIGN KEY (`session_id`) REFERENCES `tbm_sessions` (`session_id`) ON DELETE CASCADE,
  FOREIGN KEY (`check_id`) REFERENCES `tbm_safety_checks` (`check_id`),
  FOREIGN KEY (`checked_by`) REFERENCES `users` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

team_handovers

CREATE TABLE `team_handovers` (
  `handover_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `session_id` INT UNSIGNED NOT NULL,
  `from_leader_id` INT NOT NULL,
  `to_leader_id` INT NOT NULL,
  `handover_date` DATE NOT NULL,
  `handover_time` TIME NULL,
  `reason` ENUM('half_day', 'early_leave', 'emergency', 'other') NOT NULL,
  `handover_notes` TEXT NULL,
  `worker_ids` TEXT NULL COMMENT 'JSON array',
  `is_confirmed` BOOLEAN DEFAULT FALSE,
  `confirmed_at` TIMESTAMP NULL,
  `confirmed_by` INT NULL,
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX `idx_session_handover_date` (`session_id`, `handover_date`),
  FOREIGN KEY (`session_id`) REFERENCES `tbm_sessions` (`session_id`) ON DELETE CASCADE,
  FOREIGN KEY (`from_leader_id`) REFERENCES `workers` (`worker_id`),
  FOREIGN KEY (`to_leader_id`) REFERENCES `workers` (`worker_id`),
  FOREIGN KEY (`confirmed_by`) REFERENCES `users` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

작성자: Claude 최종 수정일: 2026-01-26 문서 버전: 2.0