/** * 마이그레이션: 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 시스템 테이블 제거 완료'); };