Files
TK-FB-Project/deploy/tkfb-package/api.hyungi.net/db/migrations/20260120000000_create_tbm_system.js
Hyungi Ahn 2b1c7bfb88 feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가)
- 출근/근태 시스템 개선 (연차 조회, 근무현황)
- 작업분석 대분류 그룹화 및 마이그레이션 스크립트
- 모바일 네비게이션 UI 추가
- NAS 배포 도구 및 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 14:41:01 +09:00

159 lines
8.8 KiB
JavaScript

/**
* 마이그레이션: TBM (Tool Box Meeting) 시스템
* 작성일: 2026-01-20
*
* 생성 테이블:
* - tbm_sessions: TBM 세션 (아침 미팅 기록)
* - tbm_team_assignments: TBM 팀 구성 (리더가 선택한 작업자들)
* - tbm_safety_checks: TBM 안전 체크리스트
* - tbm_safety_records: TBM 안전 체크 기록
* - team_handovers: 작업 인계 기록 (반차/조퇴 시)
*/
exports.up = async function(knex) {
console.log('⏳ TBM 시스템 테이블 생성 중...');
// 1. TBM 세션 테이블 (아침 미팅)
await knex.schema.createTable('tbm_sessions', (table) => {
table.increments('session_id').primary();
table.date('session_date').notNullable().comment('TBM 날짜');
table.integer('leader_id').notNullable().comment('팀장 worker_id');
table.integer('project_id').nullable().comment('프로젝트 ID');
table.string('work_location', 200).nullable().comment('작업 장소');
table.text('work_description').nullable().comment('작업 내용');
table.text('safety_notes').nullable().comment('안전 관련 특이사항');
table.enum('status', ['draft', 'completed', 'cancelled']).defaultTo('draft').comment('상태');
table.time('start_time').nullable().comment('TBM 시작 시간');
table.time('end_time').nullable().comment('TBM 종료 시간');
table.integer('created_by').notNullable().comment('생성자 user_id');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
// 인덱스 및 제약조건
table.index(['session_date', 'leader_id']);
table.foreign('leader_id').references('workers.worker_id');
table.foreign('project_id').references('projects.project_id').onDelete('SET NULL');
table.foreign('created_by').references('users.user_id');
});
console.log('✅ tbm_sessions 테이블 생성 완료');
// 2. TBM 팀 구성 테이블 (리더가 선택한 팀원들)
await knex.schema.createTable('tbm_team_assignments', (table) => {
table.increments('assignment_id').primary();
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
table.integer('worker_id').notNullable().comment('팀원 worker_id');
table.string('assigned_role', 100).nullable().comment('역할/담당');
table.text('work_detail').nullable().comment('세부 작업 내용');
table.boolean('is_present').defaultTo(true).comment('출석 여부');
table.text('absence_reason').nullable().comment('결석 사유');
table.timestamp('assigned_at').defaultTo(knex.fn.now());
// 인덱스 및 제약조건
table.unique(['session_id', 'worker_id']);
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
table.foreign('worker_id').references('workers.worker_id');
});
console.log('✅ tbm_team_assignments 테이블 생성 완료');
// 3. TBM 안전 체크리스트 마스터 테이블
await knex.schema.createTable('tbm_safety_checks', (table) => {
table.increments('check_id').primary();
table.string('check_category', 50).notNullable().comment('카테고리 (장비, PPE, 환경 등)');
table.string('check_item', 200).notNullable().comment('체크 항목');
table.text('description').nullable().comment('설명');
table.integer('display_order').defaultTo(0).comment('표시 순서');
table.boolean('is_required').defaultTo(true).comment('필수 체크 여부');
table.boolean('is_active').defaultTo(true).comment('활성 여부');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.index('check_category');
});
console.log('✅ tbm_safety_checks 테이블 생성 완료');
// 초기 안전 체크리스트 데이터
await knex('tbm_safety_checks').insert([
// PPE (개인 보호 장비)
{ check_category: 'PPE', check_item: '안전모 착용 확인', display_order: 1, is_required: true },
{ check_category: 'PPE', check_item: '안전화 착용 확인', display_order: 2, is_required: true },
{ check_category: 'PPE', check_item: '안전조끼 착용 확인', display_order: 3, is_required: true },
{ check_category: 'PPE', check_item: '안전벨트 착용 확인 (고소작업 시)', display_order: 4, is_required: false },
{ check_category: 'PPE', check_item: '보안경/마스크 착용 확인', display_order: 5, is_required: false },
// 장비 점검
{ check_category: 'EQUIPMENT', check_item: '작업 도구 점검 완료', display_order: 10, is_required: true },
{ check_category: 'EQUIPMENT', check_item: '전동공구 안전 점검', display_order: 11, is_required: true },
{ check_category: 'EQUIPMENT', check_item: '사다리/비계 안전 확인', display_order: 12, is_required: false },
{ check_category: 'EQUIPMENT', check_item: '차량/중장비 점검 완료', display_order: 13, is_required: false },
// 작업 환경
{ check_category: 'ENVIRONMENT', check_item: '작업 장소 정리정돈 확인', display_order: 20, is_required: true },
{ check_category: 'ENVIRONMENT', check_item: '위험 구역 표시 확인', display_order: 21, is_required: true },
{ check_category: 'ENVIRONMENT', check_item: '기상 상태 확인 (우천, 강풍 등)', display_order: 22, is_required: true },
{ check_category: 'ENVIRONMENT', check_item: '작업 동선 안전 확인', display_order: 23, is_required: true },
// 비상 대응
{ check_category: 'EMERGENCY', check_item: '비상연락망 공유 완료', display_order: 30, is_required: true },
{ check_category: 'EMERGENCY', check_item: '소화기 위치 확인', display_order: 31, is_required: true },
{ check_category: 'EMERGENCY', check_item: '응급처치 키트 위치 확인', display_order: 32, is_required: true },
]);
console.log('✅ tbm_safety_checks 초기 데이터 입력 완료');
// 4. TBM 안전 체크 기록 테이블
await knex.schema.createTable('tbm_safety_records', (table) => {
table.increments('record_id').primary();
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
table.integer('check_id').unsigned().notNullable().comment('체크 항목 ID');
table.boolean('is_checked').defaultTo(false).comment('체크 여부');
table.text('notes').nullable().comment('비고/특이사항');
table.integer('checked_by').nullable().comment('체크한 user_id');
table.timestamp('checked_at').nullable().comment('체크 시간');
// 인덱스 및 제약조건
table.unique(['session_id', 'check_id']);
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
table.foreign('check_id').references('tbm_safety_checks.check_id');
table.foreign('checked_by').references('users.user_id');
});
console.log('✅ tbm_safety_records 테이블 생성 완료');
// 5. 작업 인계 테이블 (반차/조퇴 시)
await knex.schema.createTable('team_handovers', (table) => {
table.increments('handover_id').primary();
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
table.integer('from_leader_id').notNullable().comment('인계자 worker_id');
table.integer('to_leader_id').notNullable().comment('인수자 worker_id');
table.date('handover_date').notNullable().comment('인계 날짜');
table.time('handover_time').nullable().comment('인계 시간');
table.enum('reason', ['half_day', 'early_leave', 'emergency', 'other']).notNullable().comment('인계 사유');
table.text('handover_notes').nullable().comment('인계 내용');
table.text('worker_ids').nullable().comment('인계하는 작업자 IDs (JSON array)');
table.boolean('is_confirmed').defaultTo(false).comment('인수 확인 여부');
table.timestamp('confirmed_at').nullable().comment('인수 확인 시간');
table.integer('confirmed_by').nullable().comment('인수 확인자 user_id');
table.timestamp('created_at').defaultTo(knex.fn.now());
// 인덱스 및 제약조건
table.index(['session_id', 'handover_date']);
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
table.foreign('from_leader_id').references('workers.worker_id');
table.foreign('to_leader_id').references('workers.worker_id');
table.foreign('confirmed_by').references('users.user_id');
});
console.log('✅ team_handovers 테이블 생성 완료');
console.log('✅ 모든 TBM 시스템 테이블 생성 완료');
};
exports.down = async function(knex) {
console.log('⏳ TBM 시스템 테이블 제거 중...');
await knex.schema.dropTableIfExists('team_handovers');
await knex.schema.dropTableIfExists('tbm_safety_records');
await knex.schema.dropTableIfExists('tbm_safety_checks');
await knex.schema.dropTableIfExists('tbm_team_assignments');
await knex.schema.dropTableIfExists('tbm_sessions');
console.log('✅ 모든 TBM 시스템 테이블 제거 완료');
};