모든 작업자가 개인 계정으로 로그인하여 본인의 연차와 출근 기록을 확인할 수 있는 시스템을 구축했습니다. 주요 기능: - 작업자-계정 1:1 통합 (기존 작업자 자동 계정 생성) - 연차 관리 시스템 (연도별 잔액 관리) - 출근 기록 시스템 (일일 근태 기록) - 나의 대시보드 페이지 (개인 정보 조회) 데이터베이스: - workers 테이블에 salary, base_annual_leave 컬럼 추가 - work_attendance_types, vacation_types 테이블 생성 - daily_attendance_records 테이블 생성 - worker_vacation_balance 테이블 생성 - 기존 작업자 자동 계정 생성 (username: 이름 기반) - Guest 역할 추가 백엔드 API: - 한글→영문 변환 유틸리티 (hangulToRoman.js) - UserRoutes에 개인 정보 조회 API 추가 - GET /api/users/me (내 정보) - GET /api/users/me/attendance-records (출근 기록) - GET /api/users/me/vacation-balance (연차 잔액) - GET /api/users/me/work-reports (작업 보고서) - GET /api/users/me/monthly-stats (월별 통계) 프론트엔드: - 나의 대시보드 페이지 (my-dashboard.html) - 연차 정보 위젯 (총/사용/잔여) - 월별 출근 캘린더 - 근무 시간 통계 - 최근 작업 보고서 목록 - 네비게이션 바에 "나의 대시보드" 메뉴 추가 배포 시 주의사항: - 마이그레이션 실행 필요 - 자동 생성된 계정 초기 비밀번호: 1234 - 작업자들에게 첫 로그인 후 비밀번호 변경 안내 필요 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
113 lines
5.5 KiB
JavaScript
113 lines
5.5 KiB
JavaScript
/**
|
|
* 마이그레이션: 출근/근태 관련 테이블 생성
|
|
* 작성일: 2026-01-19
|
|
*
|
|
* 생성 테이블:
|
|
* - work_attendance_types: 출근 유형 (정상, 지각, 조퇴, 결근, 휴가)
|
|
* - vacation_types: 휴가 유형 (연차, 반차, 병가, 경조사)
|
|
* - daily_attendance_records: 일일 출근 기록
|
|
* - worker_vacation_balance: 작업자 연차 잔액 (연도별)
|
|
*/
|
|
|
|
exports.up = async function(knex) {
|
|
console.log('⏳ 출근/근태 관련 테이블 생성 중...');
|
|
|
|
// 1. 출근 유형 테이블
|
|
await knex.schema.createTable('work_attendance_types', (table) => {
|
|
table.increments('id').primary();
|
|
table.string('type_code', 20).unique().notNullable().comment('유형 코드');
|
|
table.string('type_name', 50).notNullable().comment('유형 이름');
|
|
table.text('description').nullable().comment('설명');
|
|
table.boolean('is_active').defaultTo(true).comment('활성 여부');
|
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
|
});
|
|
console.log('✅ work_attendance_types 테이블 생성 완료');
|
|
|
|
// 초기 데이터 입력
|
|
await knex('work_attendance_types').insert([
|
|
{ type_code: 'NORMAL', type_name: '정상 출근', description: '정상 출근' },
|
|
{ type_code: 'LATE', type_name: '지각', description: '지각' },
|
|
{ type_code: 'EARLY_LEAVE', type_name: '조퇴', description: '조퇴' },
|
|
{ type_code: 'ABSENT', type_name: '결근', description: '무단 결근' },
|
|
{ type_code: 'VACATION', type_name: '휴가', description: '승인된 휴가' }
|
|
]);
|
|
console.log('✅ work_attendance_types 초기 데이터 입력 완료');
|
|
|
|
// 2. 휴가 유형 테이블
|
|
await knex.schema.createTable('vacation_types', (table) => {
|
|
table.increments('id').primary();
|
|
table.string('type_code', 20).unique().notNullable().comment('휴가 코드');
|
|
table.string('type_name', 50).notNullable().comment('휴가 이름');
|
|
table.decimal('deduct_days', 3, 1).defaultTo(1.0).comment('차감 일수');
|
|
table.boolean('is_active').defaultTo(true).comment('활성 여부');
|
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
|
});
|
|
console.log('✅ vacation_types 테이블 생성 완료');
|
|
|
|
// 초기 데이터 입력
|
|
await knex('vacation_types').insert([
|
|
{ type_code: 'ANNUAL', type_name: '연차', deduct_days: 1.0 },
|
|
{ type_code: 'HALF_ANNUAL', type_name: '반차', deduct_days: 0.5 },
|
|
{ type_code: 'SICK', type_name: '병가', deduct_days: 1.0 },
|
|
{ type_code: 'SPECIAL', type_name: '경조사', deduct_days: 0 }
|
|
]);
|
|
console.log('✅ vacation_types 초기 데이터 입력 완료');
|
|
|
|
// 3. 일일 출근 기록 테이블
|
|
await knex.schema.createTable('daily_attendance_records', (table) => {
|
|
table.increments('id').primary();
|
|
table.integer('worker_id').unsigned().notNullable().comment('작업자 ID');
|
|
table.date('record_date').notNullable().comment('기록 날짜');
|
|
table.integer('attendance_type_id').unsigned().notNullable().comment('출근 유형 ID');
|
|
table.integer('vacation_type_id').unsigned().nullable().comment('휴가 유형 ID');
|
|
table.time('check_in_time').nullable().comment('출근 시간');
|
|
table.time('check_out_time').nullable().comment('퇴근 시간');
|
|
table.decimal('total_work_hours', 4, 2).defaultTo(0).comment('총 근무 시간');
|
|
table.boolean('is_overtime_approved').defaultTo(false).comment('초과근무 승인 여부');
|
|
table.text('notes').nullable().comment('비고');
|
|
table.integer('created_by').unsigned().notNullable().comment('등록자 user_id');
|
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
|
|
|
// 인덱스 및 제약조건
|
|
table.unique(['worker_id', 'record_date']);
|
|
table.foreign('worker_id').references('workers.worker_id').onDelete('CASCADE');
|
|
table.foreign('attendance_type_id').references('work_attendance_types.id');
|
|
table.foreign('vacation_type_id').references('vacation_types.id');
|
|
table.foreign('created_by').references('users.user_id');
|
|
});
|
|
console.log('✅ daily_attendance_records 테이블 생성 완료');
|
|
|
|
// 4. 작업자 연차 잔액 테이블
|
|
await knex.schema.createTable('worker_vacation_balance', (table) => {
|
|
table.increments('id').primary();
|
|
table.integer('worker_id').unsigned().notNullable().comment('작업자 ID');
|
|
table.integer('year').notNullable().comment('연도');
|
|
table.decimal('total_annual_leave', 4, 1).defaultTo(15.0).comment('총 연차');
|
|
table.decimal('used_annual_leave', 4, 1).defaultTo(0).comment('사용 연차');
|
|
// remaining_annual_leave는 애플리케이션 레벨에서 계산
|
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
|
|
|
// 인덱스 및 제약조건
|
|
table.unique(['worker_id', 'year']);
|
|
table.foreign('worker_id').references('workers.worker_id').onDelete('CASCADE');
|
|
});
|
|
console.log('✅ worker_vacation_balance 테이블 생성 완료');
|
|
|
|
console.log('✅ 모든 출근/근태 관련 테이블 생성 완료');
|
|
};
|
|
|
|
exports.down = async function(knex) {
|
|
console.log('⏳ 출근/근태 관련 테이블 제거 중...');
|
|
|
|
await knex.schema.dropTableIfExists('worker_vacation_balance');
|
|
await knex.schema.dropTableIfExists('daily_attendance_records');
|
|
await knex.schema.dropTableIfExists('vacation_types');
|
|
await knex.schema.dropTableIfExists('work_attendance_types');
|
|
|
|
console.log('✅ 모든 출근/근태 관련 테이블 제거 완료');
|
|
};
|