- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성 - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동 - common/ → attendance/: 근태/휴가 관련 페이지 이동 - admin/ 정리: safety-* 파일들을 safety/로 이동 - 사이드바 네비게이션 메뉴 구현 - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리 - 접기/펼치기 기능 및 상태 저장 - 관리자 전용 메뉴 자동 표시/숨김 - 날씨 API 연동 (기상청 단기예보) - TBM 및 navbar에 현재 날씨 표시 - weatherService.js 추가 - 안전 체크리스트 확장 - 기본/날씨별/작업별 체크 유형 추가 - checklist-manage.html 페이지 추가 - 이슈 신고 시스템 구현 - workIssueController, workIssueModel, workIssueRoutes 추가 - DB 마이그레이션 파일 추가 (실행 대기) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
267 lines
13 KiB
JavaScript
267 lines
13 KiB
JavaScript
/**
|
|
* 마이그레이션: 작업 중 문제 신고 시스템
|
|
* - 신고 카테고리 테이블 (부적합/안전)
|
|
* - 사전 정의 신고 항목 테이블
|
|
* - 문제 신고 메인 테이블
|
|
* - 상태 변경 이력 테이블
|
|
*/
|
|
|
|
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('작업 중 문제 신고 시스템 테이블 삭제 완료');
|
|
};
|