Files
TK-FB-Project/api.hyungi.net/db/migrations/20260204100000_create_daily_patrol_system.js
Hyungi Ahn 90d3e32992 feat: 일일순회점검 시스템 구축 및 관리 기능 개선
- 일일순회점검 시스템 신규 구현
  - DB 테이블: patrol_checklist_items, daily_patrol_sessions, patrol_check_records, workplace_items, item_types
  - API: /api/patrol/* 엔드포인트
  - 프론트엔드: 지도 기반 작업장 점검 UI

- 설비 관리 기능 개선
  - 구매 관련 필드 추가 (구매일, 가격, 공급업체 등)
  - 설비 코드 자동 생성 (TKP-XXX 형식)

- 작업장 관리 개선
  - 레이아웃 이미지 업로드 기능
  - 마커 위치 저장 기능

- 부서 관리 기능 추가
- 사이드바 네비게이션 카테고리 재구성
- 이미지 401 오류 수정 (정적 파일 경로 처리)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:41:41 +09:00

167 lines
9.8 KiB
JavaScript

/**
* 마이그레이션: 일일순회점검 시스템
* 작성일: 2026-02-04
*
* 생성 테이블:
* - patrol_checklist_items: 순회점검 체크리스트 마스터
* - daily_patrol_sessions: 순회점검 세션 기록
* - patrol_check_records: 순회점검 체크 결과
* - workplace_items: 작업장 물품 현황 (용기, 플레이트 등)
*/
exports.up = async function(knex) {
console.log('⏳ 일일순회점검 시스템 테이블 생성 중...');
// 1. 순회점검 체크리스트 마스터 테이블
await knex.schema.createTable('patrol_checklist_items', (table) => {
table.increments('item_id').primary();
table.integer('workplace_id').unsigned().nullable().comment('특정 작업장 전용 (NULL=공통)');
table.integer('category_id').unsigned().nullable().comment('특정 공장 전용 (NULL=공통)');
table.string('check_category', 50).notNullable().comment('분류 (안전, 정리정돈, 설비 등)');
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('workplace_id');
table.index('category_id');
table.index('check_category');
table.foreign('workplace_id').references('workplaces.workplace_id').onDelete('CASCADE');
table.foreign('category_id').references('workplace_categories.category_id').onDelete('CASCADE');
});
console.log('✅ patrol_checklist_items 테이블 생성 완료');
// 초기 순회점검 체크리스트 데이터
await knex('patrol_checklist_items').insert([
// 안전 관련
{ check_category: 'SAFETY', check_item: '소화기 상태 확인', display_order: 1, is_required: true },
{ check_category: 'SAFETY', check_item: '비상구 통로 확보 확인', display_order: 2, is_required: true },
{ check_category: 'SAFETY', check_item: '안전표지판 부착 상태', display_order: 3, is_required: true },
{ check_category: 'SAFETY', check_item: '위험물 관리 상태', display_order: 4, is_required: true },
// 정리정돈
{ check_category: 'ORGANIZATION', check_item: '작업장 정리정돈 상태', display_order: 10, is_required: true },
{ check_category: 'ORGANIZATION', check_item: '통로 장애물 여부', display_order: 11, is_required: true },
{ check_category: 'ORGANIZATION', check_item: '폐기물 처리 상태', display_order: 12, is_required: true },
{ check_category: 'ORGANIZATION', check_item: '자재 적재 상태', display_order: 13, is_required: true },
// 설비
{ check_category: 'EQUIPMENT', check_item: '설비 외관 이상 여부', display_order: 20, is_required: false },
{ check_category: 'EQUIPMENT', check_item: '설비 작동 상태', display_order: 21, is_required: false },
{ check_category: 'EQUIPMENT', check_item: '설비 청결 상태', display_order: 22, is_required: false },
// 환경
{ check_category: 'ENVIRONMENT', check_item: '조명 상태', display_order: 30, is_required: true },
{ check_category: 'ENVIRONMENT', check_item: '환기 상태', display_order: 31, is_required: true },
{ check_category: 'ENVIRONMENT', check_item: '누수/누유 여부', display_order: 32, is_required: true },
]);
console.log('✅ patrol_checklist_items 초기 데이터 입력 완료');
// 2. 순회점검 세션 테이블
await knex.schema.createTable('daily_patrol_sessions', (table) => {
table.increments('session_id').primary();
table.date('patrol_date').notNullable().comment('점검 날짜');
table.enum('patrol_time', ['morning', 'afternoon']).notNullable().comment('점검 시간대');
table.integer('inspector_id').notNullable().comment('순찰자 user_id'); // signed (users.user_id)
table.integer('category_id').unsigned().nullable().comment('공장 ID');
table.enum('status', ['in_progress', 'completed']).defaultTo('in_progress').comment('상태');
table.text('notes').nullable().comment('특이사항');
table.time('started_at').nullable().comment('점검 시작 시간');
table.time('completed_at').nullable().comment('점검 완료 시간');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.unique(['patrol_date', 'patrol_time', 'category_id']);
table.index(['patrol_date', 'patrol_time']);
table.index('inspector_id');
table.foreign('inspector_id').references('users.user_id');
table.foreign('category_id').references('workplace_categories.category_id').onDelete('SET NULL');
});
console.log('✅ daily_patrol_sessions 테이블 생성 완료');
// 3. 순회점검 체크 기록 테이블
await knex.schema.createTable('patrol_check_records', (table) => {
table.increments('record_id').primary();
table.integer('session_id').unsigned().notNullable().comment('순회점검 세션 ID');
table.integer('workplace_id').unsigned().notNullable().comment('작업장 ID');
table.integer('check_item_id').unsigned().notNullable().comment('체크항목 ID');
table.boolean('is_checked').defaultTo(false).comment('체크 여부');
table.enum('check_result', ['good', 'warning', 'bad']).nullable().comment('점검 결과');
table.text('note').nullable().comment('비고');
table.timestamp('checked_at').nullable().comment('체크 시간');
// 인덱스명 길이 제한으로 인해 수동으로 지정
table.unique(['session_id', 'workplace_id', 'check_item_id'], 'pcr_session_wp_item_unique');
table.index(['session_id', 'workplace_id'], 'pcr_session_wp_idx');
table.foreign('session_id').references('daily_patrol_sessions.session_id').onDelete('CASCADE');
table.foreign('workplace_id').references('workplaces.workplace_id').onDelete('CASCADE');
table.foreign('check_item_id').references('patrol_checklist_items.item_id').onDelete('CASCADE');
});
console.log('✅ patrol_check_records 테이블 생성 완료');
// 4. 작업장 물품 현황 테이블
await knex.schema.createTable('workplace_items', (table) => {
table.increments('item_id').primary();
table.integer('workplace_id').unsigned().notNullable().comment('작업장 ID');
table.integer('patrol_session_id').unsigned().nullable().comment('등록한 순회점검 세션');
table.integer('project_id').nullable().comment('관련 프로젝트'); // signed (projects.project_id)
table.enum('item_type', ['container', 'plate', 'material', 'tool', 'other']).notNullable().comment('물품 유형');
table.string('item_name', 100).nullable().comment('물품명/설명');
table.integer('quantity').defaultTo(1).comment('수량');
table.decimal('x_percent', 5, 2).nullable().comment('지도상 X 위치 (%)');
table.decimal('y_percent', 5, 2).nullable().comment('지도상 Y 위치 (%)');
table.decimal('width_percent', 5, 2).nullable().comment('지도상 너비 (%)');
table.decimal('height_percent', 5, 2).nullable().comment('지도상 높이 (%)');
table.boolean('is_active').defaultTo(true).comment('현재 존재 여부');
table.integer('created_by').notNullable().comment('등록자 user_id'); // signed (users.user_id)
table.timestamp('created_at').defaultTo(knex.fn.now());
table.integer('updated_by').nullable().comment('최종 수정자 user_id'); // signed (users.user_id)
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.index(['workplace_id', 'is_active']);
table.index('project_id');
table.foreign('workplace_id').references('workplaces.workplace_id').onDelete('CASCADE');
table.foreign('patrol_session_id').references('daily_patrol_sessions.session_id').onDelete('SET NULL');
table.foreign('project_id').references('projects.project_id').onDelete('SET NULL');
table.foreign('created_by').references('users.user_id');
table.foreign('updated_by').references('users.user_id');
});
console.log('✅ workplace_items 테이블 생성 완료');
// 물품 유형 코드 테이블 (선택적 확장용)
await knex.schema.createTable('item_types', (table) => {
table.string('type_code', 20).primary();
table.string('type_name', 50).notNullable().comment('유형명');
table.string('icon', 10).nullable().comment('아이콘 이모지');
table.string('color', 20).nullable().comment('표시 색상');
table.integer('display_order').defaultTo(0);
table.boolean('is_active').defaultTo(true);
});
await knex('item_types').insert([
{ type_code: 'container', type_name: '용기', icon: '📦', color: '#3b82f6', display_order: 1 },
{ type_code: 'plate', type_name: '플레이트', icon: '🔲', color: '#10b981', display_order: 2 },
{ type_code: 'material', type_name: '자재', icon: '🧱', color: '#f59e0b', display_order: 3 },
{ type_code: 'tool', type_name: '공구/장비', icon: '🔧', color: '#8b5cf6', display_order: 4 },
{ type_code: 'other', type_name: '기타', icon: '📍', color: '#6b7280', display_order: 5 },
]);
console.log('✅ item_types 테이블 생성 및 초기 데이터 완료');
console.log('✅ 모든 일일순회점검 시스템 테이블 생성 완료');
};
exports.down = async function(knex) {
console.log('⏳ 일일순회점검 시스템 테이블 제거 중...');
await knex.schema.dropTableIfExists('item_types');
await knex.schema.dropTableIfExists('workplace_items');
await knex.schema.dropTableIfExists('patrol_check_records');
await knex.schema.dropTableIfExists('daily_patrol_sessions');
await knex.schema.dropTableIfExists('patrol_checklist_items');
console.log('✅ 모든 일일순회점검 시스템 테이블 제거 완료');
};