/** * 마이그레이션: 작업 중 문제 신고 시스템 * - 신고 카테고리 테이블 (부적합/안전) * - 사전 정의 신고 항목 테이블 * - 문제 신고 메인 테이블 * - 상태 변경 이력 테이블 */ exports.up = async function(knex) { // 1. 신고 카테고리 테이블 생성 await knex.schema.createTable('issue_report_categories', function(table) { table.increments('category_id').primary().comment('카테고리 ID'); table.enum('category_type', ['nonconformity', 'safety']).notNullable().comment('카테고리 유형 (부적합/안전)'); table.string('category_name', 100).notNullable().comment('카테고리명'); table.text('description').nullable().comment('카테고리 설명'); table.integer('display_order').defaultTo(0).comment('표시 순서'); table.boolean('is_active').defaultTo(true).comment('활성 여부'); table.timestamp('created_at').defaultTo(knex.fn.now()); table.index('category_type', 'idx_irc_category_type'); table.index('is_active', 'idx_irc_is_active'); }); // 카테고리 초기 데이터 삽입 await knex('issue_report_categories').insert([ // 부적합 사항 { category_type: 'nonconformity', category_name: '자재누락', display_order: 1, is_active: true }, { category_type: 'nonconformity', category_name: '설계미스', display_order: 2, is_active: true }, { category_type: 'nonconformity', category_name: '입고불량', display_order: 3, is_active: true }, { category_type: 'nonconformity', category_name: '검사미스', display_order: 4, is_active: true }, { category_type: 'nonconformity', category_name: '기타 부적합', display_order: 99, is_active: true }, // 안전 관련 { category_type: 'safety', category_name: '보호구 미착용', display_order: 1, is_active: true }, { category_type: 'safety', category_name: '위험구역 출입', display_order: 2, is_active: true }, { category_type: 'safety', category_name: '안전시설 파손', display_order: 3, is_active: true }, { category_type: 'safety', category_name: '안전수칙 위반', display_order: 4, is_active: true }, { category_type: 'safety', category_name: '기타 안전', display_order: 99, is_active: true } ]); // 2. 사전 정의 신고 항목 테이블 생성 await knex.schema.createTable('issue_report_items', function(table) { table.increments('item_id').primary().comment('항목 ID'); table.integer('category_id').unsigned().notNullable().comment('소속 카테고리 ID'); table.string('item_name', 200).notNullable().comment('신고 항목명'); table.text('description').nullable().comment('항목 설명'); table.enum('severity', ['low', 'medium', 'high', 'critical']).defaultTo('medium').comment('심각도'); table.integer('display_order').defaultTo(0).comment('표시 순서'); table.boolean('is_active').defaultTo(true).comment('활성 여부'); table.timestamp('created_at').defaultTo(knex.fn.now()); table.foreign('category_id') .references('category_id') .inTable('issue_report_categories') .onDelete('CASCADE') .onUpdate('CASCADE'); table.index('category_id', 'idx_iri_category_id'); table.index('is_active', 'idx_iri_is_active'); }); // 사전 정의 항목 초기 데이터 삽입 await knex('issue_report_items').insert([ // 자재누락 (category_id: 1) { category_id: 1, item_name: '배관 자재 미입고', severity: 'high', display_order: 1 }, { category_id: 1, item_name: '피팅류 부족', severity: 'medium', display_order: 2 }, { category_id: 1, item_name: '밸브류 미입고', severity: 'high', display_order: 3 }, { category_id: 1, item_name: '가스켓/볼트류 부족', severity: 'low', display_order: 4 }, { category_id: 1, item_name: '서포트 자재 부족', severity: 'medium', display_order: 5 }, // 설계미스 (category_id: 2) { category_id: 2, item_name: '도면 치수 오류', severity: 'high', display_order: 1 }, { category_id: 2, item_name: '스펙 불일치', severity: 'high', display_order: 2 }, { category_id: 2, item_name: '누락된 상세도', severity: 'medium', display_order: 3 }, { category_id: 2, item_name: '간섭 발생', severity: 'critical', display_order: 4 }, // 입고불량 (category_id: 3) { category_id: 3, item_name: '외관 불량', severity: 'medium', display_order: 1 }, { category_id: 3, item_name: '치수 불량', severity: 'high', display_order: 2 }, { category_id: 3, item_name: '수량 부족', severity: 'medium', display_order: 3 }, { category_id: 3, item_name: '재질 불일치', severity: 'critical', display_order: 4 }, // 검사미스 (category_id: 4) { category_id: 4, item_name: '치수 검사 누락', severity: 'high', display_order: 1 }, { category_id: 4, item_name: '외관 검사 누락', severity: 'medium', display_order: 2 }, { category_id: 4, item_name: '용접 검사 누락', severity: 'critical', display_order: 3 }, { category_id: 4, item_name: '도장 검사 누락', severity: 'medium', display_order: 4 }, // 보호구 미착용 (category_id: 6) { category_id: 6, item_name: '안전모 미착용', severity: 'high', display_order: 1 }, { category_id: 6, item_name: '안전화 미착용', severity: 'high', display_order: 2 }, { category_id: 6, item_name: '보안경 미착용', severity: 'medium', display_order: 3 }, { category_id: 6, item_name: '안전대 미착용', severity: 'critical', display_order: 4 }, { category_id: 6, item_name: '귀마개 미착용', severity: 'low', display_order: 5 }, { category_id: 6, item_name: '안전장갑 미착용', severity: 'medium', display_order: 6 }, // 위험구역 출입 (category_id: 7) { category_id: 7, item_name: '통제구역 무단 출입', severity: 'critical', display_order: 1 }, { category_id: 7, item_name: '고소 작업 구역 무단 출입', severity: 'critical', display_order: 2 }, { category_id: 7, item_name: '밀폐공간 무단 진입', severity: 'critical', display_order: 3 }, { category_id: 7, item_name: '장비 가동 구역 무단 접근', severity: 'high', display_order: 4 }, // 안전시설 파손 (category_id: 8) { category_id: 8, item_name: '안전난간 파손', severity: 'high', display_order: 1 }, { category_id: 8, item_name: '경고 표지판 훼손', severity: 'medium', display_order: 2 }, { category_id: 8, item_name: '안전망 파손', severity: 'high', display_order: 3 }, { category_id: 8, item_name: '비상조명 고장', severity: 'medium', display_order: 4 }, { category_id: 8, item_name: '소화설비 파손', severity: 'critical', display_order: 5 }, // 안전수칙 위반 (category_id: 9) { category_id: 9, item_name: '지정 통로 미사용', severity: 'medium', display_order: 1 }, { category_id: 9, item_name: '고소 작업 안전 미준수', severity: 'critical', display_order: 2 }, { category_id: 9, item_name: '화기 작업 절차 미준수', severity: 'critical', display_order: 3 }, { category_id: 9, item_name: '정리정돈 미흡', severity: 'low', display_order: 4 }, { category_id: 9, item_name: '장비 조작 절차 미준수', severity: 'high', display_order: 5 } ]); // 3. 문제 신고 메인 테이블 생성 await knex.schema.createTable('work_issue_reports', function(table) { table.increments('report_id').primary().comment('신고 ID'); // 신고자 정보 table.integer('reporter_id').notNullable().comment('신고자 user_id'); table.datetime('report_date').defaultTo(knex.fn.now()).comment('신고 일시'); // 위치 정보 table.integer('factory_category_id').unsigned().nullable().comment('공장 카테고리 ID (지도 외 위치 시 null)'); table.integer('workplace_id').unsigned().nullable().comment('작업장 ID (지도 외 위치 시 null)'); table.string('custom_location', 200).nullable().comment('기타 위치 (지도 외 선택 시)'); // 작업 연결 정보 (선택적) table.integer('tbm_session_id').unsigned().nullable().comment('연결된 TBM 세션'); table.integer('visit_request_id').unsigned().nullable().comment('연결된 출입 신청'); // 신고 내용 table.integer('issue_category_id').unsigned().notNullable().comment('신고 카테고리 ID'); table.integer('issue_item_id').unsigned().nullable().comment('사전 정의 신고 항목 ID'); table.text('additional_description').nullable().comment('추가 설명'); // 사진 (최대 5장) table.string('photo_path1', 255).nullable().comment('사진 1'); table.string('photo_path2', 255).nullable().comment('사진 2'); table.string('photo_path3', 255).nullable().comment('사진 3'); table.string('photo_path4', 255).nullable().comment('사진 4'); table.string('photo_path5', 255).nullable().comment('사진 5'); // 상태 관리 table.enum('status', ['reported', 'received', 'in_progress', 'completed', 'closed']) .defaultTo('reported') .comment('상태: 신고→접수→처리중→완료→종료'); // 담당자 배정 table.string('assigned_department', 100).nullable().comment('담당 부서'); table.integer('assigned_user_id').nullable().comment('담당자 user_id'); table.datetime('assigned_at').nullable().comment('배정 일시'); table.integer('assigned_by').nullable().comment('배정자 user_id'); // 처리 정보 table.text('resolution_notes').nullable().comment('처리 내용'); table.string('resolution_photo_path1', 255).nullable().comment('처리 완료 사진 1'); table.string('resolution_photo_path2', 255).nullable().comment('처리 완료 사진 2'); table.datetime('resolved_at').nullable().comment('처리 완료 일시'); table.integer('resolved_by').nullable().comment('처리 완료자 user_id'); // 수정 이력 (JSON) table.json('modification_history').nullable().comment('수정 이력 추적'); // 타임스탬프 table.timestamp('created_at').defaultTo(knex.fn.now()); table.timestamp('updated_at').defaultTo(knex.fn.now()); // 외래키 table.foreign('reporter_id') .references('user_id') .inTable('users') .onDelete('RESTRICT') .onUpdate('CASCADE'); table.foreign('factory_category_id') .references('category_id') .inTable('workplace_categories') .onDelete('SET NULL') .onUpdate('CASCADE'); table.foreign('workplace_id') .references('workplace_id') .inTable('workplaces') .onDelete('SET NULL') .onUpdate('CASCADE'); table.foreign('issue_category_id') .references('category_id') .inTable('issue_report_categories') .onDelete('RESTRICT') .onUpdate('CASCADE'); table.foreign('issue_item_id') .references('item_id') .inTable('issue_report_items') .onDelete('SET NULL') .onUpdate('CASCADE'); table.foreign('assigned_user_id') .references('user_id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); table.foreign('assigned_by') .references('user_id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); table.foreign('resolved_by') .references('user_id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); // 인덱스 table.index('reporter_id', 'idx_wir_reporter_id'); table.index('status', 'idx_wir_status'); table.index('report_date', 'idx_wir_report_date'); table.index(['factory_category_id', 'workplace_id'], 'idx_wir_workplace'); table.index('issue_category_id', 'idx_wir_issue_category'); table.index('assigned_user_id', 'idx_wir_assigned_user'); }); // 4. 상태 변경 이력 테이블 생성 await knex.schema.createTable('work_issue_status_logs', function(table) { table.increments('log_id').primary().comment('로그 ID'); table.integer('report_id').unsigned().notNullable().comment('신고 ID'); table.string('previous_status', 50).nullable().comment('이전 상태'); table.string('new_status', 50).notNullable().comment('새 상태'); table.integer('changed_by').notNullable().comment('변경자 user_id'); table.text('change_reason').nullable().comment('변경 사유'); table.timestamp('changed_at').defaultTo(knex.fn.now()); table.foreign('report_id') .references('report_id') .inTable('work_issue_reports') .onDelete('CASCADE') .onUpdate('CASCADE'); table.foreign('changed_by') .references('user_id') .inTable('users') .onDelete('RESTRICT') .onUpdate('CASCADE'); table.index('report_id', 'idx_wisl_report_id'); table.index('changed_at', 'idx_wisl_changed_at'); }); console.log('작업 중 문제 신고 시스템 테이블 생성 완료'); }; exports.down = async function(knex) { // 역순으로 테이블 삭제 await knex.schema.dropTableIfExists('work_issue_status_logs'); await knex.schema.dropTableIfExists('work_issue_reports'); await knex.schema.dropTableIfExists('issue_report_items'); await knex.schema.dropTableIfExists('issue_report_categories'); console.log('작업 중 문제 신고 시스템 테이블 삭제 완료'); };