- 실시간 작업장 현황을 지도로 시각화 - 작업장 관리 페이지에서 정의한 구역 정보 활용 - TBM 작업자 및 방문자 현황 표시 주요 변경사항: - dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거) - workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현 - modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가 시각화 방식: - 인원 없음: 회색 테두리 + 작업장 이름 - 내부 작업자: 파란색 영역 + 인원 수 - 외부 방문자: 보라색 영역 + 인원 수 - 둘 다: 초록색 영역 + 총 인원 수 기술 구현: - Canvas API 기반 사각형 영역 렌더링 - map-regions API를 통한 데이터 일관성 보장 - 클릭 이벤트로 상세 정보 모달 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
154 lines
5.5 KiB
JavaScript
154 lines
5.5 KiB
JavaScript
/**
|
|
* 마이그레이션: 출입 신청 및 안전교육 시스템
|
|
* - 방문 목적 타입 테이블
|
|
* - 출입 신청 테이블
|
|
* - 안전교육 기록 테이블
|
|
*/
|
|
|
|
exports.up = async function(knex) {
|
|
// 1. 방문 목적 타입 테이블 생성
|
|
await knex.schema.createTable('visit_purpose_types', function(table) {
|
|
table.increments('purpose_id').primary().comment('방문 목적 ID');
|
|
table.string('purpose_name', 100).notNullable().comment('방문 목적명');
|
|
table.integer('display_order').defaultTo(0).comment('표시 순서');
|
|
table.boolean('is_active').defaultTo(true).comment('활성 여부');
|
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
});
|
|
|
|
// 초기 데이터 삽입
|
|
await knex('visit_purpose_types').insert([
|
|
{ purpose_name: '외주작업', display_order: 1, is_active: true },
|
|
{ purpose_name: '검사', display_order: 2, is_active: true },
|
|
{ purpose_name: '견학', display_order: 3, is_active: true },
|
|
{ purpose_name: '기타', display_order: 99, is_active: true }
|
|
]);
|
|
|
|
// 2. 출입 신청 테이블 생성
|
|
await knex.schema.createTable('workplace_visit_requests', function(table) {
|
|
table.increments('request_id').primary().comment('신청 ID');
|
|
|
|
// 신청자 정보
|
|
table.integer('requester_id').notNullable().comment('신청자 user_id');
|
|
|
|
// 방문자 정보
|
|
table.string('visitor_company', 200).notNullable().comment('방문자 소속 (회사명 또는 "일용직")');
|
|
table.integer('visitor_count').defaultTo(1).comment('방문 인원');
|
|
|
|
// 방문 장소
|
|
table.integer('category_id').unsigned().notNullable().comment('방문 구역 (공장)');
|
|
table.integer('workplace_id').unsigned().notNullable().comment('방문 작업장');
|
|
|
|
// 방문 일시
|
|
table.date('visit_date').notNullable().comment('방문 날짜');
|
|
table.time('visit_time').notNullable().comment('방문 시간');
|
|
|
|
// 방문 목적
|
|
table.integer('purpose_id').unsigned().notNullable().comment('방문 목적 ID');
|
|
table.text('notes').nullable().comment('비고');
|
|
|
|
// 상태 관리
|
|
table.enum('status', ['pending', 'approved', 'rejected', 'training_completed'])
|
|
.defaultTo('pending')
|
|
.comment('신청 상태');
|
|
|
|
// 승인 정보
|
|
table.integer('approved_by').nullable().comment('승인자 user_id');
|
|
table.timestamp('approved_at').nullable().comment('승인 시간');
|
|
table.text('rejection_reason').nullable().comment('반려 사유');
|
|
|
|
// 타임스탬프
|
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
|
|
|
// 외래키
|
|
table.foreign('requester_id')
|
|
.references('user_id')
|
|
.inTable('users')
|
|
.onDelete('RESTRICT')
|
|
.onUpdate('CASCADE');
|
|
|
|
table.foreign('category_id')
|
|
.references('category_id')
|
|
.inTable('workplace_categories')
|
|
.onDelete('RESTRICT')
|
|
.onUpdate('CASCADE');
|
|
|
|
table.foreign('workplace_id')
|
|
.references('workplace_id')
|
|
.inTable('workplaces')
|
|
.onDelete('RESTRICT')
|
|
.onUpdate('CASCADE');
|
|
|
|
table.foreign('purpose_id')
|
|
.references('purpose_id')
|
|
.inTable('visit_purpose_types')
|
|
.onDelete('RESTRICT')
|
|
.onUpdate('CASCADE');
|
|
|
|
table.foreign('approved_by')
|
|
.references('user_id')
|
|
.inTable('users')
|
|
.onDelete('SET NULL')
|
|
.onUpdate('CASCADE');
|
|
|
|
// 인덱스
|
|
table.index('visit_date', 'idx_visit_date');
|
|
table.index('status', 'idx_status');
|
|
table.index(['visit_date', 'status'], 'idx_visit_date_status');
|
|
});
|
|
|
|
// 3. 안전교육 기록 테이블 생성
|
|
await knex.schema.createTable('safety_training_records', function(table) {
|
|
table.increments('training_id').primary().comment('교육 기록 ID');
|
|
|
|
table.integer('request_id').unsigned().notNullable().comment('출입 신청 ID');
|
|
|
|
// 교육 진행 정보
|
|
table.integer('trainer_id').notNullable().comment('교육 진행자 user_id');
|
|
table.date('training_date').notNullable().comment('교육 날짜');
|
|
table.time('training_start_time').notNullable().comment('교육 시작 시간');
|
|
table.time('training_end_time').nullable().comment('교육 종료 시간');
|
|
|
|
// 교육 내용
|
|
table.text('training_topics').nullable().comment('교육 내용 (JSON 배열)');
|
|
|
|
// 서명 데이터 (Base64 이미지)
|
|
table.text('signature_data', 'longtext').nullable().comment('교육 이수자 서명 (Base64 PNG)');
|
|
|
|
// 완료 정보
|
|
table.timestamp('completed_at').nullable().comment('교육 완료 시간');
|
|
|
|
// 타임스탬프
|
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
|
|
|
// 외래키
|
|
table.foreign('request_id')
|
|
.references('request_id')
|
|
.inTable('workplace_visit_requests')
|
|
.onDelete('CASCADE')
|
|
.onUpdate('CASCADE');
|
|
|
|
table.foreign('trainer_id')
|
|
.references('user_id')
|
|
.inTable('users')
|
|
.onDelete('RESTRICT')
|
|
.onUpdate('CASCADE');
|
|
|
|
// 인덱스
|
|
table.index('training_date', 'idx_training_date');
|
|
table.index('request_id', 'idx_request_id');
|
|
});
|
|
|
|
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 생성 완료');
|
|
};
|
|
|
|
exports.down = async function(knex) {
|
|
// 역순으로 테이블 삭제
|
|
await knex.schema.dropTableIfExists('safety_training_records');
|
|
await knex.schema.dropTableIfExists('workplace_visit_requests');
|
|
await knex.schema.dropTableIfExists('visit_purpose_types');
|
|
|
|
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 삭제 완료');
|
|
};
|