Files
TK-FB-Project/api.hyungi.net/db/migrations/20260130000001_create_work_issue_system.js
Hyungi Ahn 74d3a78aa3 feat: 페이지 구조 재구성 및 사이드바 네비게이션 구현
- 페이지 폴더 재구성: 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>
2026-02-02 14:27:22 +09:00

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('작업 중 문제 신고 시스템 테이블 삭제 완료');
};