feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
const schemaSql = fs.readFileSync(path.join(__dirname, '../../hyungi_schema_v2.sql'), 'utf8');
|
||||
return knex.raw(schemaSql);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
// down 마이그레이션은 모든 테이블을 역순으로 삭제하도록 구현합니다.
|
||||
const tables = [
|
||||
'cutting_plans',
|
||||
'daily_issue_reports',
|
||||
'daily_work_reports',
|
||||
'codes',
|
||||
'code_types',
|
||||
'factory_info',
|
||||
'equipment_list',
|
||||
'pipe_specs',
|
||||
'tasks',
|
||||
'worker_groups',
|
||||
'workers',
|
||||
'projects',
|
||||
'password_change_logs',
|
||||
'login_logs',
|
||||
'users'
|
||||
];
|
||||
|
||||
// 외래 키 제약 조건을 먼저 비활성화합니다.
|
||||
return knex.raw('SET FOREIGN_KEY_CHECKS = 0;')
|
||||
.then(() => {
|
||||
// 각 테이블을 순회하며 drop table if exists를 실행합니다.
|
||||
return tables.reduce((promise, tableName) => {
|
||||
return promise.then(() => knex.schema.dropTableIfExists(tableName));
|
||||
}, Promise.resolve());
|
||||
})
|
||||
.finally(() => {
|
||||
// 외래 키 제약 조건을 다시 활성화합니다.
|
||||
return knex.raw('SET FOREIGN_KEY_CHECKS = 1;');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table('projects', function (table) {
|
||||
table.boolean('is_active').defaultTo(true).after('pm');
|
||||
table.string('project_status').defaultTo('active').after('is_active');
|
||||
table.date('completed_date').nullable().after('project_status');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table('projects', function (table) {
|
||||
table.dropColumn('is_active');
|
||||
table.dropColumn('project_status');
|
||||
table.dropColumn('completed_date');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
// 1. roles 테이블 생성
|
||||
.createTable('roles', function(table) {
|
||||
table.increments('id').primary();
|
||||
table.string('name', 50).notNullable().unique();
|
||||
table.string('description', 255);
|
||||
table.timestamps(true, true);
|
||||
})
|
||||
// 2. permissions 테이블 생성
|
||||
.createTable('permissions', function(table) {
|
||||
table.increments('id').primary();
|
||||
table.string('name', 100).notNullable().unique(); // 예: 'user:create'
|
||||
table.string('description', 255);
|
||||
table.timestamps(true, true);
|
||||
})
|
||||
// 3. role_permissions (역할-권한) 조인 테이블 생성
|
||||
.createTable('role_permissions', function(table) {
|
||||
table.integer('role_id').unsigned().notNullable().references('id').inTable('roles').onDelete('CASCADE');
|
||||
table.integer('permission_id').unsigned().notNullable().references('id').inTable('permissions').onDelete('CASCADE');
|
||||
table.primary(['role_id', 'permission_id']);
|
||||
})
|
||||
// 4. users 테이블에 role_id 추가 및 기존 컬럼 삭제
|
||||
.table('users', function(table) {
|
||||
table.integer('role_id').unsigned().references('id').inTable('roles').onDelete('SET NULL').after('email');
|
||||
// 기존 컬럼들은 삭제 또는 비활성화 (데이터 보존을 위해 일단 이름 변경)
|
||||
table.renameColumn('role', '_role_old');
|
||||
table.renameColumn('access_level', '_access_level_old');
|
||||
})
|
||||
// 5. user_permissions (사용자-개별 권한) 조인 테이블 생성
|
||||
.createTable('user_permissions', function(table) {
|
||||
table.integer('user_id').notNullable().references('user_id').inTable('users').onDelete('CASCADE');
|
||||
table.integer('permission_id').unsigned().notNullable().references('id').inTable('permissions').onDelete('CASCADE');
|
||||
table.primary(['user_id', 'permission_id']);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.dropTableIfExists('user_permissions')
|
||||
.dropTableIfExists('role_permissions')
|
||||
.dropTableIfExists('permissions')
|
||||
.dropTableIfExists('roles')
|
||||
.table('users', function(table) {
|
||||
table.dropColumn('role_id');
|
||||
table.renameColumn('_role_old', 'role');
|
||||
table.renameColumn('_access_level_old', 'access_level');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = async function(knex) {
|
||||
// 1. Roles 생성
|
||||
await knex('roles').insert([
|
||||
{ id: 1, name: 'System Admin', description: '시스템 전체 관리자. 모든 권한을 가짐.' },
|
||||
{ id: 2, name: 'Admin', description: '관리자. 사용자 및 프로젝트 관리 등 대부분의 권한을 가짐.' },
|
||||
{ id: 3, name: 'Leader', description: '그룹장. 팀원 작업 현황 조회 등 중간 관리자 권한.' },
|
||||
{ id: 4, name: 'Worker', description: '일반 작업자. 자신의 작업 보고서 작성 및 조회 권한.' },
|
||||
]);
|
||||
|
||||
// 2. Permissions 생성 (예시)
|
||||
const permissions = [
|
||||
// User
|
||||
{ name: 'user:create', description: '사용자 생성' },
|
||||
{ name: 'user:read', description: '사용자 정보 조회' },
|
||||
{ name: 'user:update', description: '사용자 정보 수정' },
|
||||
{ name: 'user:delete', description: '사용자 삭제' },
|
||||
// Project
|
||||
{ name: 'project:create', description: '프로젝트 생성' },
|
||||
{ name: 'project:read', description: '프로젝트 조회' },
|
||||
{ name: 'project:update', description: '프로젝트 수정' },
|
||||
{ name: 'project:delete', description: '프로젝트 삭제' },
|
||||
// Work Report
|
||||
{ name: 'work-report:create', description: '작업 보고서 생성' },
|
||||
{ name: 'work-report:read-own', description: '자신의 작업 보고서 조회' },
|
||||
{ name: 'work-report:read-team', description: '팀의 작업 보고서 조회' },
|
||||
{ name: 'work-report:read-all', description: '모든 작업 보고서 조회' },
|
||||
{ name: 'work-report:update', description: '작업 보고서 수정' },
|
||||
{ name: 'work-report:delete', description: '작업 보고서 삭제' },
|
||||
// System
|
||||
{ name: 'system:read-logs', description: '시스템 로그 조회' },
|
||||
{ name: 'system:manage-settings', description: '시스템 설정 관리' },
|
||||
];
|
||||
await knex('permissions').insert(permissions);
|
||||
|
||||
// 3. Role-Permissions 매핑
|
||||
const allPermissions = await knex('permissions').select('id', 'name');
|
||||
const permissionMap = allPermissions.reduce((acc, p) => {
|
||||
acc[p.name] = p.id;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const rolePermissions = {
|
||||
// System Admin (모든 권한)
|
||||
'System Admin': allPermissions.map(p => p.id),
|
||||
// Admin
|
||||
'Admin': [
|
||||
permissionMap['user:create'], permissionMap['user:read'], permissionMap['user:update'], permissionMap['user:delete'],
|
||||
permissionMap['project:create'], permissionMap['project:read'], permissionMap['project:update'], permissionMap['project:delete'],
|
||||
permissionMap['work-report:read-all'], permissionMap['work-report:update'], permissionMap['work-report:delete'],
|
||||
],
|
||||
// Leader
|
||||
'Leader': [
|
||||
permissionMap['user:read'],
|
||||
permissionMap['project:read'],
|
||||
permissionMap['work-report:read-team'],
|
||||
permissionMap['work-report:read-own'],
|
||||
permissionMap['work-report:create'],
|
||||
],
|
||||
// Worker
|
||||
'Worker': [
|
||||
permissionMap['work-report:create'],
|
||||
permissionMap['work-report:read-own'],
|
||||
],
|
||||
};
|
||||
|
||||
const rolePermissionInserts = [];
|
||||
for (const roleName in rolePermissions) {
|
||||
const roleId = (await knex('roles').where('name', roleName).first()).id;
|
||||
rolePermissions[roleName].forEach(permissionId => {
|
||||
rolePermissionInserts.push({ role_id: roleId, permission_id: permissionId });
|
||||
});
|
||||
}
|
||||
await knex('role_permissions').insert(rolePermissionInserts);
|
||||
|
||||
// 4. 기존 사용자에게 역할 부여 (예: 기존 admin -> Admin, leader -> Leader, user -> Worker)
|
||||
await knex.raw(`
|
||||
UPDATE users SET role_id =
|
||||
CASE
|
||||
WHEN _role_old = 'system' THEN (SELECT id FROM roles WHERE name = 'System Admin')
|
||||
WHEN _role_old = 'admin' THEN (SELECT id FROM roles WHERE name = 'Admin')
|
||||
WHEN _role_old = 'leader' THEN (SELECT id FROM roles WHERE name = 'Leader')
|
||||
ELSE (SELECT id FROM roles WHERE name = 'Worker')
|
||||
END
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = async function(knex) {
|
||||
await knex('role_permissions').del();
|
||||
await knex('user_permissions').del();
|
||||
await knex('roles').del();
|
||||
await knex('permissions').del();
|
||||
|
||||
// 역할 롤백 (단순화된 버전)
|
||||
await knex.raw("UPDATE users SET _role_old = 'user' WHERE role_id IS NOT NULL");
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = async function (knex) {
|
||||
const hasHireDate = await knex.schema.hasColumn('workers', 'hire_date');
|
||||
|
||||
if (!hasHireDate) {
|
||||
await knex.schema.alterTable('workers', function (table) {
|
||||
// Modify status to ENUM
|
||||
// Note: Knex might not support modifying to ENUM easily across DBs, but valid for MySQL
|
||||
// We use raw SQL for status modification to be safe with existing data
|
||||
|
||||
// Add new columns
|
||||
table.string('phone_number', 20).nullable().comment('전화번호');
|
||||
table.string('email', 100).nullable().comment('이메일');
|
||||
table.date('hire_date').nullable().comment('입사일');
|
||||
table.string('department', 100).nullable().comment('부서');
|
||||
table.text('notes').nullable().comment('비고');
|
||||
});
|
||||
|
||||
// Update status column using raw query
|
||||
await knex.raw(`
|
||||
ALTER TABLE workers
|
||||
MODIFY COLUMN status ENUM('active', 'inactive') DEFAULT 'active' COMMENT '작업자 상태 (active: 활성, inactive: 비활성)'
|
||||
`);
|
||||
|
||||
// Add indexes
|
||||
await knex.raw(`CREATE INDEX IF NOT EXISTS idx_workers_status ON workers(status)`);
|
||||
await knex.raw(`CREATE INDEX IF NOT EXISTS idx_workers_hire_date ON workers(hire_date)`);
|
||||
|
||||
// Set NULL status to active
|
||||
await knex('workers').whereNull('status').update({ status: 'active' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = async function (knex) {
|
||||
// We generally don't want to lose data on rollback of this critical schema fix,
|
||||
// but technically we should revert changes.
|
||||
// For safety, we might skip dropping columns or implement it carefully.
|
||||
|
||||
const hasHireDate = await knex.schema.hasColumn('workers', 'hire_date');
|
||||
if (hasHireDate) {
|
||||
await knex.schema.alterTable('workers', function (table) {
|
||||
table.dropColumn('phone_number');
|
||||
table.dropColumn('email');
|
||||
table.dropColumn('hire_date');
|
||||
table.dropColumn('department');
|
||||
table.dropColumn('notes');
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
ALTER TABLE workers
|
||||
MODIFY COLUMN status VARCHAR(20) DEFAULT 'active' COMMENT '상태 (active, inactive)'
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 권한 시스템 단순화 및 페이지 접근 권한 추가
|
||||
* - Leader와 Worker를 User로 통합
|
||||
* - 페이지 접근 권한 테이블 생성
|
||||
* - Admin이 사용자별 페이지 접근 권한을 설정할 수 있도록 함
|
||||
*
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = async function(knex) {
|
||||
// 1. 페이지 목록 테이블 생성
|
||||
await knex.schema.createTable('pages', function(table) {
|
||||
table.increments('id').primary();
|
||||
table.string('page_key', 100).notNullable().unique(); // 예: 'worker-management', 'project-management'
|
||||
table.string('page_name', 100).notNullable(); // 예: '작업자 관리', '프로젝트 관리'
|
||||
table.string('page_path', 255).notNullable(); // 예: '/pages/management/worker-management.html'
|
||||
table.string('category', 50); // 예: 'management', 'dashboard', 'admin'
|
||||
table.string('description', 255);
|
||||
table.boolean('is_admin_only').defaultTo(false); // Admin 전용 페이지 여부
|
||||
table.integer('display_order').defaultTo(0); // 표시 순서
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
// 2. 사용자별 페이지 접근 권한 테이블 생성
|
||||
await knex.schema.createTable('user_page_access', function(table) {
|
||||
table.integer('user_id').notNullable()
|
||||
.references('user_id').inTable('users').onDelete('CASCADE');
|
||||
table.integer('page_id').unsigned().notNullable()
|
||||
.references('id').inTable('pages').onDelete('CASCADE');
|
||||
table.boolean('can_access').defaultTo(true); // 접근 가능 여부
|
||||
table.timestamp('granted_at').defaultTo(knex.fn.now());
|
||||
table.integer('granted_by') // 권한을 부여한 Admin의 user_id
|
||||
.references('user_id').inTable('users').onDelete('SET NULL');
|
||||
table.primary(['user_id', 'page_id']);
|
||||
});
|
||||
|
||||
// 3. 기본 페이지 목록 삽입
|
||||
await knex('pages').insert([
|
||||
// Dashboard
|
||||
{ page_key: 'dashboard-user', page_name: '사용자 대시보드', page_path: '/pages/dashboard/user.html', category: 'dashboard', is_admin_only: false, display_order: 1 },
|
||||
{ page_key: 'dashboard-leader', page_name: '그룹장 대시보드', page_path: '/pages/dashboard/group-leader.html', category: 'dashboard', is_admin_only: false, display_order: 2 },
|
||||
|
||||
// Management
|
||||
{ page_key: 'worker-management', page_name: '작업자 관리', page_path: '/pages/management/worker-management.html', category: 'management', is_admin_only: false, display_order: 10 },
|
||||
{ page_key: 'project-management', page_name: '프로젝트 관리', page_path: '/pages/management/project-management.html', category: 'management', is_admin_only: false, display_order: 11 },
|
||||
{ page_key: 'work-management', page_name: '작업 관리', page_path: '/pages/management/work-management.html', category: 'management', is_admin_only: false, display_order: 12 },
|
||||
{ page_key: 'code-management', page_name: '코드 관리', page_path: '/pages/management/code-management.html', category: 'management', is_admin_only: false, display_order: 13 },
|
||||
|
||||
// Common
|
||||
{ page_key: 'daily-work-report', page_name: '작업 현황 확인', page_path: '/pages/common/daily-work-report-viewer.html', category: 'common', is_admin_only: false, display_order: 20 },
|
||||
|
||||
// Admin
|
||||
{ page_key: 'user-management', page_name: '사용자 관리', page_path: '/pages/admin/manage-user.html', category: 'admin', is_admin_only: true, display_order: 100 },
|
||||
]);
|
||||
|
||||
// 4. roles 테이블 업데이트: Leader와 Worker를 User로 통합
|
||||
// Leader와 Worker 역할을 가진 사용자를 모두 User로 변경
|
||||
const userRoleId = await knex('roles').where('name', 'Worker').first().then(r => r.id);
|
||||
const leaderRoleId = await knex('roles').where('name', 'Leader').first().then(r => r ? r.id : null);
|
||||
|
||||
if (leaderRoleId) {
|
||||
// Leader를 User로 변경
|
||||
await knex('users').where('role_id', leaderRoleId).update({ role_id: userRoleId });
|
||||
}
|
||||
|
||||
// 5. role_permissions 업데이트: Worker 권한을 확장하여 모든 일반 기능 사용 가능하게
|
||||
const allPermissions = await knex('permissions').select('id', 'name');
|
||||
const permissionMap = allPermissions.reduce((acc, p) => {
|
||||
acc[p.name] = p.id;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Worker 역할의 기존 권한 삭제
|
||||
await knex('role_permissions').where('role_id', userRoleId).del();
|
||||
|
||||
// Worker(이제 User) 역할에 모든 일반 권한 부여 (Admin/System 권한 제외)
|
||||
const userPermissions = [
|
||||
permissionMap['user:read'],
|
||||
permissionMap['project:read'],
|
||||
permissionMap['project:create'],
|
||||
permissionMap['project:update'],
|
||||
permissionMap['work-report:create'],
|
||||
permissionMap['work-report:read-own'],
|
||||
permissionMap['work-report:read-team'],
|
||||
permissionMap['work-report:read-all'],
|
||||
permissionMap['work-report:update'],
|
||||
permissionMap['work-report:delete'],
|
||||
].filter(Boolean); // undefined 제거
|
||||
|
||||
const rolePermissionInserts = userPermissions.map(permissionId => ({
|
||||
role_id: userRoleId,
|
||||
permission_id: permissionId
|
||||
}));
|
||||
|
||||
await knex('role_permissions').insert(rolePermissionInserts);
|
||||
|
||||
// 6. Leader 역할 삭제 (더 이상 사용하지 않음)
|
||||
if (leaderRoleId) {
|
||||
await knex('role_permissions').where('role_id', leaderRoleId).del();
|
||||
await knex('roles').where('id', leaderRoleId).del();
|
||||
}
|
||||
|
||||
// 7. Worker 역할 이름을 'User'로 변경
|
||||
await knex('roles').where('id', userRoleId).update({
|
||||
name: 'User',
|
||||
description: '일반 사용자. 작업 보고서 및 프로젝트 관리 등 모든 일반 기능을 사용할 수 있음.'
|
||||
});
|
||||
|
||||
// 8. 모든 일반 사용자에게 모든 페이지 접근 권한 부여 (Admin 페이지 제외)
|
||||
const normalPages = await knex('pages').where('is_admin_only', false).select('id');
|
||||
const normalUsers = await knex('users').where('role_id', userRoleId).select('user_id');
|
||||
|
||||
const userPageAccessInserts = [];
|
||||
normalUsers.forEach(user => {
|
||||
normalPages.forEach(page => {
|
||||
userPageAccessInserts.push({
|
||||
user_id: user.user_id,
|
||||
page_id: page.id,
|
||||
can_access: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (userPageAccessInserts.length > 0) {
|
||||
await knex('user_page_access').insert(userPageAccessInserts);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = async function(knex) {
|
||||
// 테이블 삭제 (역순)
|
||||
await knex.schema.dropTableIfExists('user_page_access');
|
||||
await knex.schema.dropTableIfExists('pages');
|
||||
|
||||
// User 역할을 다시 Worker로 변경
|
||||
const userRoleId = await knex('roles').where('name', 'User').first().then(r => r ? r.id : null);
|
||||
if (userRoleId) {
|
||||
await knex('roles').where('id', userRoleId).update({
|
||||
name: 'Worker',
|
||||
description: '일반 작업자. 자신의 작업 보고서 작성 및 조회 권한.'
|
||||
});
|
||||
}
|
||||
|
||||
// Leader 역할 재생성
|
||||
await knex('roles').insert([
|
||||
{ id: 3, name: 'Leader', description: '그룹장. 팀원 작업 현황 조회 등 중간 관리자 권한.' }
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.alterTable('workers', (table) => {
|
||||
// 재직 상태 (employed: 재직, resigned: 퇴사)
|
||||
table.enum('employment_status', ['employed', 'resigned'])
|
||||
.defaultTo('employed')
|
||||
.notNullable()
|
||||
.comment('재직 상태 (employed: 재직, resigned: 퇴사)');
|
||||
});
|
||||
|
||||
console.log('✅ workers 테이블에 employment_status 컬럼 추가 완료');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.alterTable('workers', (table) => {
|
||||
table.dropColumn('employment_status');
|
||||
});
|
||||
|
||||
console.log('✅ workers 테이블에서 employment_status 컬럼 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 마이그레이션: Workers 테이블에 급여 및 기본 연차 컬럼 추가
|
||||
* 작성일: 2026-01-19
|
||||
*
|
||||
* 변경사항:
|
||||
* - salary 컬럼 추가 (NULL 허용, 선택 사항)
|
||||
* - base_annual_leave 컬럼 추가 (기본값: 15일)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ Workers 테이블에 salary, base_annual_leave 컬럼 추가 중...');
|
||||
|
||||
await knex.schema.alterTable('workers', (table) => {
|
||||
// 급여 정보 (선택 사항, NULL 허용)
|
||||
table.decimal('salary', 12, 2).nullable().comment('급여 (선택)');
|
||||
|
||||
// 기본 연차 일수 (기본값: 15일)
|
||||
table.integer('base_annual_leave').defaultTo(15).notNullable().comment('기본 연차 일수');
|
||||
});
|
||||
|
||||
console.log('✅ Workers 테이블 컬럼 추가 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ Workers 테이블에서 salary, base_annual_leave 컬럼 제거 중...');
|
||||
|
||||
await knex.schema.alterTable('workers', (table) => {
|
||||
table.dropColumn('salary');
|
||||
table.dropColumn('base_annual_leave');
|
||||
});
|
||||
|
||||
console.log('✅ Workers 테이블 컬럼 제거 완료');
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 마이그레이션: 출근/근태 관련 테이블 생성
|
||||
* 작성일: 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('✅ 모든 출근/근태 관련 테이블 제거 완료');
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 마이그레이션: 기존 작업자에게 계정 자동 생성
|
||||
* 작성일: 2026-01-19
|
||||
*
|
||||
* 작업 내용:
|
||||
* 1. 계정이 없는 기존 작업자 조회
|
||||
* 2. 각 작업자에 대해 users 테이블에 계정 생성
|
||||
* 3. username은 이름 기반으로 자동 생성 (예: 홍길동 → hong.gildong)
|
||||
* 4. 초기 비밀번호는 '1234'로 통일 (첫 로그인 시 변경 권장)
|
||||
* 5. 현재 연도 연차 잔액 초기화 (workers.annual_leave 사용)
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { generateUniqueUsername } = require('../../utils/hangulToRoman');
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ 기존 작업자들에게 계정 자동 생성 중...');
|
||||
|
||||
// 1. 계정이 없는 작업자 조회
|
||||
const workersWithoutAccount = await knex('workers')
|
||||
.leftJoin('users', 'workers.worker_id', 'users.worker_id')
|
||||
.whereNull('users.user_id')
|
||||
.select(
|
||||
'workers.worker_id',
|
||||
'workers.worker_name',
|
||||
'workers.email',
|
||||
'workers.status',
|
||||
'workers.annual_leave'
|
||||
);
|
||||
|
||||
console.log(`📊 계정이 없는 작업자: ${workersWithoutAccount.length}명`);
|
||||
|
||||
if (workersWithoutAccount.length === 0) {
|
||||
console.log('ℹ️ 계정이 필요한 작업자가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 각 작업자에 대해 계정 생성
|
||||
const initialPassword = '1234'; // 초기 비밀번호
|
||||
const hashedPassword = await bcrypt.hash(initialPassword, 10);
|
||||
|
||||
// User 역할 ID 조회
|
||||
const userRole = await knex('roles')
|
||||
.where('name', 'User')
|
||||
.first();
|
||||
|
||||
if (!userRole) {
|
||||
throw new Error('User 역할이 존재하지 않습니다. 권한 마이그레이션을 먼저 실행하세요.');
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const worker of workersWithoutAccount) {
|
||||
try {
|
||||
// username 생성 (중복 체크 포함)
|
||||
const username = await generateUniqueUsername(worker.worker_name, knex);
|
||||
|
||||
// 계정 생성
|
||||
await knex('users').insert({
|
||||
username: username,
|
||||
password: hashedPassword,
|
||||
name: worker.worker_name,
|
||||
email: worker.email,
|
||||
worker_id: worker.worker_id,
|
||||
role_id: userRole.id,
|
||||
is_active: worker.status === 'active' ? 1 : 0,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
|
||||
console.log(`✅ ${worker.worker_name} (ID: ${worker.worker_id}) → username: ${username}`);
|
||||
successCount++;
|
||||
|
||||
// 현재 연도 연차 잔액 초기화
|
||||
const currentYear = new Date().getFullYear();
|
||||
await knex('worker_vacation_balance').insert({
|
||||
worker_id: worker.worker_id,
|
||||
year: currentYear,
|
||||
total_annual_leave: worker.annual_leave || 15,
|
||||
used_annual_leave: 0,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${worker.worker_name} 계정 생성 실패:`, error.message);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 작업 완료: 성공 ${successCount}명, 실패 ${errorCount}명`);
|
||||
console.log(`🔐 초기 비밀번호: ${initialPassword} (모든 계정 공통)`);
|
||||
console.log('⚠️ 사용자들에게 첫 로그인 후 비밀번호를 변경하도록 안내해주세요!');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ 자동 생성된 계정 제거 중...');
|
||||
|
||||
// 이 마이그레이션으로 생성된 계정은 구분하기 어려우므로
|
||||
// rollback 시 주의가 필요합니다.
|
||||
console.log('⚠️ 경고: 이 마이그레이션의 rollback은 권장하지 않습니다.');
|
||||
console.log('ℹ️ 필요시 수동으로 users 테이블을 관리하세요.');
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 마이그레이션: 게스트 역할 추가
|
||||
* 작성일: 2026-01-19
|
||||
*
|
||||
* 변경사항:
|
||||
* - Guest 역할 추가 (계정 없이 특정 기능 접근 가능)
|
||||
* - 게스트 전용 페이지 추가 (신고 채널 등)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ 게스트 역할 추가 중...');
|
||||
|
||||
// 1. Guest 역할 추가
|
||||
const [guestRoleId] = await knex('roles').insert({
|
||||
name: 'Guest',
|
||||
description: '게스트 (계정 없이 특정 기능 접근 가능)',
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
console.log(`✅ Guest 역할 추가 완료 (ID: ${guestRoleId})`);
|
||||
|
||||
// 2. 게스트 전용 페이지 추가
|
||||
await knex('pages').insert({
|
||||
page_key: 'guest_report',
|
||||
page_name: '신고 채널',
|
||||
page_path: '/pages/guest/report.html',
|
||||
category: 'guest',
|
||||
is_admin_only: false,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
console.log('✅ 게스트 전용 페이지 추가 완료 (신고 채널)');
|
||||
|
||||
console.log('✅ 게스트 역할 추가 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ 게스트 역할 제거 중...');
|
||||
|
||||
// 페이지 제거
|
||||
await knex('pages')
|
||||
.where('page_key', 'guest_report')
|
||||
.delete();
|
||||
|
||||
// 역할 제거
|
||||
await knex('roles')
|
||||
.where('name', 'Guest')
|
||||
.delete();
|
||||
|
||||
console.log('✅ 게스트 역할 제거 완료');
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 마이그레이션: TBM (Tool Box Meeting) 시스템
|
||||
* 작성일: 2026-01-20
|
||||
*
|
||||
* 생성 테이블:
|
||||
* - tbm_sessions: TBM 세션 (아침 미팅 기록)
|
||||
* - tbm_team_assignments: TBM 팀 구성 (리더가 선택한 작업자들)
|
||||
* - tbm_safety_checks: TBM 안전 체크리스트
|
||||
* - tbm_safety_records: TBM 안전 체크 기록
|
||||
* - team_handovers: 작업 인계 기록 (반차/조퇴 시)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ TBM 시스템 테이블 생성 중...');
|
||||
|
||||
// 1. TBM 세션 테이블 (아침 미팅)
|
||||
await knex.schema.createTable('tbm_sessions', (table) => {
|
||||
table.increments('session_id').primary();
|
||||
table.date('session_date').notNullable().comment('TBM 날짜');
|
||||
table.integer('leader_id').notNullable().comment('팀장 worker_id');
|
||||
table.integer('project_id').nullable().comment('프로젝트 ID');
|
||||
table.string('work_location', 200).nullable().comment('작업 장소');
|
||||
table.text('work_description').nullable().comment('작업 내용');
|
||||
table.text('safety_notes').nullable().comment('안전 관련 특이사항');
|
||||
table.enum('status', ['draft', 'completed', 'cancelled']).defaultTo('draft').comment('상태');
|
||||
table.time('start_time').nullable().comment('TBM 시작 시간');
|
||||
table.time('end_time').nullable().comment('TBM 종료 시간');
|
||||
table.integer('created_by').notNullable().comment('생성자 user_id');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.index(['session_date', 'leader_id']);
|
||||
table.foreign('leader_id').references('workers.worker_id');
|
||||
table.foreign('project_id').references('projects.project_id').onDelete('SET NULL');
|
||||
table.foreign('created_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ tbm_sessions 테이블 생성 완료');
|
||||
|
||||
// 2. TBM 팀 구성 테이블 (리더가 선택한 팀원들)
|
||||
await knex.schema.createTable('tbm_team_assignments', (table) => {
|
||||
table.increments('assignment_id').primary();
|
||||
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
|
||||
table.integer('worker_id').notNullable().comment('팀원 worker_id');
|
||||
table.string('assigned_role', 100).nullable().comment('역할/담당');
|
||||
table.text('work_detail').nullable().comment('세부 작업 내용');
|
||||
table.boolean('is_present').defaultTo(true).comment('출석 여부');
|
||||
table.text('absence_reason').nullable().comment('결석 사유');
|
||||
table.timestamp('assigned_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.unique(['session_id', 'worker_id']);
|
||||
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
|
||||
table.foreign('worker_id').references('workers.worker_id');
|
||||
});
|
||||
console.log('✅ tbm_team_assignments 테이블 생성 완료');
|
||||
|
||||
// 3. TBM 안전 체크리스트 마스터 테이블
|
||||
await knex.schema.createTable('tbm_safety_checks', (table) => {
|
||||
table.increments('check_id').primary();
|
||||
table.string('check_category', 50).notNullable().comment('카테고리 (장비, PPE, 환경 등)');
|
||||
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('check_category');
|
||||
});
|
||||
console.log('✅ tbm_safety_checks 테이블 생성 완료');
|
||||
|
||||
// 초기 안전 체크리스트 데이터
|
||||
await knex('tbm_safety_checks').insert([
|
||||
// PPE (개인 보호 장비)
|
||||
{ check_category: 'PPE', check_item: '안전모 착용 확인', display_order: 1, is_required: true },
|
||||
{ check_category: 'PPE', check_item: '안전화 착용 확인', display_order: 2, is_required: true },
|
||||
{ check_category: 'PPE', check_item: '안전조끼 착용 확인', display_order: 3, is_required: true },
|
||||
{ check_category: 'PPE', check_item: '안전벨트 착용 확인 (고소작업 시)', display_order: 4, is_required: false },
|
||||
{ check_category: 'PPE', check_item: '보안경/마스크 착용 확인', display_order: 5, is_required: false },
|
||||
|
||||
// 장비 점검
|
||||
{ check_category: 'EQUIPMENT', check_item: '작업 도구 점검 완료', display_order: 10, is_required: true },
|
||||
{ check_category: 'EQUIPMENT', check_item: '전동공구 안전 점검', display_order: 11, is_required: true },
|
||||
{ check_category: 'EQUIPMENT', check_item: '사다리/비계 안전 확인', display_order: 12, is_required: false },
|
||||
{ check_category: 'EQUIPMENT', check_item: '차량/중장비 점검 완료', display_order: 13, is_required: false },
|
||||
|
||||
// 작업 환경
|
||||
{ check_category: 'ENVIRONMENT', check_item: '작업 장소 정리정돈 확인', display_order: 20, is_required: true },
|
||||
{ check_category: 'ENVIRONMENT', check_item: '위험 구역 표시 확인', display_order: 21, is_required: true },
|
||||
{ check_category: 'ENVIRONMENT', check_item: '기상 상태 확인 (우천, 강풍 등)', display_order: 22, is_required: true },
|
||||
{ check_category: 'ENVIRONMENT', check_item: '작업 동선 안전 확인', display_order: 23, is_required: true },
|
||||
|
||||
// 비상 대응
|
||||
{ check_category: 'EMERGENCY', check_item: '비상연락망 공유 완료', display_order: 30, is_required: true },
|
||||
{ check_category: 'EMERGENCY', check_item: '소화기 위치 확인', display_order: 31, is_required: true },
|
||||
{ check_category: 'EMERGENCY', check_item: '응급처치 키트 위치 확인', display_order: 32, is_required: true },
|
||||
]);
|
||||
console.log('✅ tbm_safety_checks 초기 데이터 입력 완료');
|
||||
|
||||
// 4. TBM 안전 체크 기록 테이블
|
||||
await knex.schema.createTable('tbm_safety_records', (table) => {
|
||||
table.increments('record_id').primary();
|
||||
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
|
||||
table.integer('check_id').unsigned().notNullable().comment('체크 항목 ID');
|
||||
table.boolean('is_checked').defaultTo(false).comment('체크 여부');
|
||||
table.text('notes').nullable().comment('비고/특이사항');
|
||||
table.integer('checked_by').nullable().comment('체크한 user_id');
|
||||
table.timestamp('checked_at').nullable().comment('체크 시간');
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.unique(['session_id', 'check_id']);
|
||||
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
|
||||
table.foreign('check_id').references('tbm_safety_checks.check_id');
|
||||
table.foreign('checked_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ tbm_safety_records 테이블 생성 완료');
|
||||
|
||||
// 5. 작업 인계 테이블 (반차/조퇴 시)
|
||||
await knex.schema.createTable('team_handovers', (table) => {
|
||||
table.increments('handover_id').primary();
|
||||
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
|
||||
table.integer('from_leader_id').notNullable().comment('인계자 worker_id');
|
||||
table.integer('to_leader_id').notNullable().comment('인수자 worker_id');
|
||||
table.date('handover_date').notNullable().comment('인계 날짜');
|
||||
table.time('handover_time').nullable().comment('인계 시간');
|
||||
table.enum('reason', ['half_day', 'early_leave', 'emergency', 'other']).notNullable().comment('인계 사유');
|
||||
table.text('handover_notes').nullable().comment('인계 내용');
|
||||
table.text('worker_ids').nullable().comment('인계하는 작업자 IDs (JSON array)');
|
||||
table.boolean('is_confirmed').defaultTo(false).comment('인수 확인 여부');
|
||||
table.timestamp('confirmed_at').nullable().comment('인수 확인 시간');
|
||||
table.integer('confirmed_by').nullable().comment('인수 확인자 user_id');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.index(['session_id', 'handover_date']);
|
||||
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
|
||||
table.foreign('from_leader_id').references('workers.worker_id');
|
||||
table.foreign('to_leader_id').references('workers.worker_id');
|
||||
table.foreign('confirmed_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ team_handovers 테이블 생성 완료');
|
||||
|
||||
console.log('✅ 모든 TBM 시스템 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ TBM 시스템 테이블 제거 중...');
|
||||
|
||||
await knex.schema.dropTableIfExists('team_handovers');
|
||||
await knex.schema.dropTableIfExists('tbm_safety_records');
|
||||
await knex.schema.dropTableIfExists('tbm_safety_checks');
|
||||
await knex.schema.dropTableIfExists('tbm_team_assignments');
|
||||
await knex.schema.dropTableIfExists('tbm_sessions');
|
||||
|
||||
console.log('✅ 모든 TBM 시스템 테이블 제거 완료');
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 마이그레이션: TBM 페이지 등록
|
||||
* 작성일: 2026-01-20
|
||||
*
|
||||
* pages 테이블에 TBM 페이지 추가
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ TBM 페이지 등록 중...');
|
||||
|
||||
// TBM 페이지 추가
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'tbm',
|
||||
page_name: 'TBM 관리',
|
||||
page_path: '/pages/work/tbm.html',
|
||||
category: 'work',
|
||||
description: 'Tool Box Meeting - 아침 안전 회의 및 팀 구성 관리',
|
||||
is_admin_only: false,
|
||||
display_order: 10
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ TBM 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ TBM 페이지 제거 중...');
|
||||
|
||||
await knex('pages').where('page_key', 'tbm').del();
|
||||
|
||||
console.log('✅ TBM 페이지 제거 완료');
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 작업장 카테고리(공장) 테이블 생성 마이그레이션
|
||||
* 대분류: 제 1공장, 제 2공장 등
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('workplace_categories', function(table) {
|
||||
table.increments('category_id').primary().comment('카테고리 ID');
|
||||
table.string('category_name', 100).notNullable().comment('카테고리명 (예: 제 1공장)');
|
||||
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()).comment('생성일시');
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정일시');
|
||||
|
||||
// 인덱스
|
||||
table.index('is_active');
|
||||
table.index('display_order');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('workplace_categories');
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 작업장(작업 구역) 테이블 생성 마이그레이션
|
||||
* 소분류: 서스작업장, 조립구역 등
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('workplaces', function(table) {
|
||||
table.increments('workplace_id').primary().comment('작업장 ID');
|
||||
table.integer('category_id').unsigned().nullable().comment('카테고리 ID (공장)');
|
||||
table.string('workplace_name', 255).notNullable().comment('작업장명');
|
||||
table.text('description').nullable().comment('설명');
|
||||
table.boolean('is_active').defaultTo(true).comment('활성화 여부');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now()).comment('생성일시');
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정일시');
|
||||
|
||||
// 외래키
|
||||
table.foreign('category_id')
|
||||
.references('category_id')
|
||||
.inTable('workplace_categories')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
// 인덱스
|
||||
table.index('category_id');
|
||||
table.index('is_active');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('workplaces');
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 작업 테이블 생성 (공정=work_types에 속함)
|
||||
*
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('tasks', function(table) {
|
||||
table.increments('task_id').primary().comment('작업 ID');
|
||||
table.integer('work_type_id').nullable().comment('공정 ID (work_types 참조)');
|
||||
table.string('task_name', 255).notNullable().comment('작업명');
|
||||
table.text('description').nullable().comment('작업 설명');
|
||||
table.boolean('is_active').defaultTo(true).comment('활성화 여부');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now()).comment('생성일시');
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정일시');
|
||||
|
||||
// 외래키 (work_types 테이블 참조)
|
||||
table.foreign('work_type_id')
|
||||
.references('id')
|
||||
.inTable('work_types')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
// 인덱스
|
||||
table.index('work_type_id');
|
||||
table.index('is_active');
|
||||
}).then(() => {
|
||||
console.log('✅ tasks 테이블 생성 완료');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('tasks');
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* TBM 세션에 공정/작업 컬럼 추가
|
||||
*
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table('tbm_sessions', function(table) {
|
||||
table.integer('work_type_id').nullable().comment('공정 ID (work_types 참조)');
|
||||
table.integer('task_id').unsigned().nullable().comment('작업 ID (tasks 참조)');
|
||||
|
||||
// 외래키 추가
|
||||
table.foreign('work_type_id')
|
||||
.references('id')
|
||||
.inTable('work_types')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('task_id')
|
||||
.references('task_id')
|
||||
.inTable('tasks')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
// 인덱스 추가
|
||||
table.index('work_type_id');
|
||||
table.index('task_id');
|
||||
}).then(() => {
|
||||
console.log('✅ tbm_sessions 테이블에 work_type_id, task_id 컬럼 추가 완료');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table('tbm_sessions', function(table) {
|
||||
table.dropForeign('work_type_id');
|
||||
table.dropForeign('task_id');
|
||||
table.dropColumn('work_type_id');
|
||||
table.dropColumn('task_id');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 마이그레이션: tbm_team_assignments 테이블 확장
|
||||
* 작업자별 프로젝트/공정/작업/작업장 정보 저장 가능하도록 컬럼 추가 및 외래키 설정
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. workplace_category_id와 workplace_id를 UNSIGNED로 변경
|
||||
await knex.raw(`
|
||||
ALTER TABLE tbm_team_assignments
|
||||
MODIFY COLUMN workplace_category_id INT UNSIGNED NULL COMMENT '작업자별 작업장 대분류 (공장)',
|
||||
MODIFY COLUMN workplace_id INT UNSIGNED NULL COMMENT '작업자별 작업장 ID'
|
||||
`);
|
||||
|
||||
// 2. 외래키 제약조건 추가
|
||||
return knex.schema.alterTable('tbm_team_assignments', function(table) {
|
||||
// 외래키 제약조건 추가
|
||||
table.foreign('workplace_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');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('tbm_team_assignments', function(table) {
|
||||
// 외래키 제약조건 제거
|
||||
table.dropForeign('workplace_category_id');
|
||||
table.dropForeign('workplace_id');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 마이그레이션: tbm_sessions 테이블에서 불필요한 컬럼 제거
|
||||
* work_description, safety_notes, start_time 컬럼 제거
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('tbm_sessions', function(table) {
|
||||
table.dropColumn('work_description');
|
||||
table.dropColumn('safety_notes');
|
||||
table.dropColumn('start_time');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('tbm_sessions', function(table) {
|
||||
table.text('work_description').nullable().comment('작업 내용');
|
||||
table.text('safety_notes').nullable().comment('안전 관련 특이사항');
|
||||
table.time('start_time').nullable().comment('시작 시간');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 마이그레이션: 작업장 지도 이미지 기능 추가
|
||||
* - workplace_categories에 layout_image 필드 추가
|
||||
* - workplace_map_regions 테이블 생성 (클릭 가능한 영역 정의)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. workplace_categories 테이블에 layout_image 필드 추가
|
||||
await knex.schema.alterTable('workplace_categories', function(table) {
|
||||
table.string('layout_image', 500).nullable().comment('공장 배치도 이미지 경로');
|
||||
});
|
||||
|
||||
// 2. 작업장 지도 클릭 영역 정의 테이블 생성
|
||||
await knex.schema.createTable('workplace_map_regions', function(table) {
|
||||
table.increments('region_id').primary().comment('영역 ID');
|
||||
table.integer('workplace_id').unsigned().notNullable().comment('작업장 ID');
|
||||
table.integer('category_id').unsigned().notNullable().comment('공장 카테고리 ID');
|
||||
|
||||
// 좌표 정보 (비율 기반: 0~100%)
|
||||
table.decimal('x_start', 5, 2).notNullable().comment('시작 X 좌표 (%)');
|
||||
table.decimal('y_start', 5, 2).notNullable().comment('시작 Y 좌표 (%)');
|
||||
table.decimal('x_end', 5, 2).notNullable().comment('끝 X 좌표 (%)');
|
||||
table.decimal('y_end', 5, 2).notNullable().comment('끝 Y 좌표 (%)');
|
||||
|
||||
table.string('shape', 20).defaultTo('rect').comment('영역 모양 (rect, circle, polygon)');
|
||||
table.text('polygon_points').nullable().comment('다각형인 경우 좌표 배열 (JSON)');
|
||||
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 외래키
|
||||
table.foreign('workplace_id')
|
||||
.references('workplace_id')
|
||||
.inTable('workplaces')
|
||||
.onDelete('CASCADE')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('category_id')
|
||||
.references('category_id')
|
||||
.inTable('workplace_categories')
|
||||
.onDelete('CASCADE')
|
||||
.onUpdate('CASCADE');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 테이블 삭제
|
||||
await knex.schema.dropTableIfExists('workplace_map_regions');
|
||||
|
||||
// 필드 제거
|
||||
await knex.schema.alterTable('workplace_categories', function(table) {
|
||||
table.dropColumn('layout_image');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 마이그레이션: 작업장 용도 및 표시 순서 필드 추가
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('workplaces', function(table) {
|
||||
table.string('workplace_purpose', 50).nullable().comment('작업장 용도 (작업구역, 설비, 휴게시설, 회의실 등)');
|
||||
table.integer('display_priority').defaultTo(0).comment('표시 우선순위 (숫자가 작을수록 먼저 표시)');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('workplaces', function(table) {
|
||||
table.dropColumn('workplace_purpose');
|
||||
table.dropColumn('display_priority');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* leader_id를 nullable로 변경
|
||||
* 관리자가 TBM을 입력할 때 leader_id를 NULL로 설정하고 created_by를 사용
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. 외래 키 제약조건 삭제 (존재하는 경우에만)
|
||||
try {
|
||||
await knex.raw('ALTER TABLE tbm_sessions DROP FOREIGN KEY tbm_sessions_leader_id_foreign');
|
||||
} catch (err) {
|
||||
console.log('외래 키가 이미 존재하지 않음 (정상)');
|
||||
}
|
||||
|
||||
// 2. leader_id를 nullable로 변경 (UNSIGNED 제거하여 workers.worker_id와 타입 일치)
|
||||
await knex.raw('ALTER TABLE tbm_sessions MODIFY leader_id INT(11) NULL');
|
||||
|
||||
// 3. 외래 키 제약조건 다시 추가 (nullable 허용)
|
||||
await knex.raw('ALTER TABLE tbm_sessions ADD CONSTRAINT tbm_sessions_leader_id_foreign FOREIGN KEY (leader_id) REFERENCES workers(worker_id) ON DELETE SET NULL');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 1. 외래 키 제약조건 삭제
|
||||
await knex.raw('ALTER TABLE tbm_sessions DROP FOREIGN KEY tbm_sessions_leader_id_foreign');
|
||||
|
||||
// 2. leader_id를 NOT NULL로 되돌림
|
||||
await knex.raw('ALTER TABLE tbm_sessions MODIFY leader_id INT(11) NOT NULL');
|
||||
|
||||
// 3. 외래 키 제약조건 다시 추가
|
||||
await knex.raw('ALTER TABLE tbm_sessions ADD CONSTRAINT tbm_sessions_leader_id_foreign FOREIGN KEY (leader_id) REFERENCES workers(worker_id) ON DELETE CASCADE');
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* daily_work_reports 테이블에 TBM 연동 필드 추가
|
||||
* - TBM 세션 및 팀 배정과 연결
|
||||
* - 작업 시간 및 오류 시간 추적
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.table('daily_work_reports', (table) => {
|
||||
// TBM 연동 필드
|
||||
table.integer('tbm_session_id').unsigned().nullable()
|
||||
.comment('연결된 TBM 세션 ID');
|
||||
table.integer('tbm_assignment_id').unsigned().nullable()
|
||||
.comment('연결된 TBM 팀 배정 ID');
|
||||
|
||||
// 작업 시간 추적
|
||||
table.time('start_time').nullable()
|
||||
.comment('작업 시작 시간');
|
||||
table.time('end_time').nullable()
|
||||
.comment('작업 종료 시간');
|
||||
table.decimal('total_hours', 5, 2).nullable()
|
||||
.comment('총 작업 시간');
|
||||
table.decimal('regular_hours', 5, 2).nullable()
|
||||
.comment('정규 작업 시간 (총 시간 - 오류 시간)');
|
||||
table.decimal('error_hours', 5, 2).nullable()
|
||||
.comment('부적합 사항 처리 시간');
|
||||
|
||||
// 외래 키 제약조건
|
||||
table.foreign('tbm_session_id')
|
||||
.references('session_id')
|
||||
.inTable('tbm_sessions')
|
||||
.onDelete('SET NULL');
|
||||
|
||||
table.foreign('tbm_assignment_id')
|
||||
.references('assignment_id')
|
||||
.inTable('tbm_team_assignments')
|
||||
.onDelete('SET NULL');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.table('daily_work_reports', (table) => {
|
||||
// 외래 키 제약조건 삭제
|
||||
table.dropForeign('tbm_session_id');
|
||||
table.dropForeign('tbm_assignment_id');
|
||||
|
||||
// 컬럼 삭제
|
||||
table.dropColumn('tbm_session_id');
|
||||
table.dropColumn('tbm_assignment_id');
|
||||
table.dropColumn('start_time');
|
||||
table.dropColumn('end_time');
|
||||
table.dropColumn('total_hours');
|
||||
table.dropColumn('regular_hours');
|
||||
table.dropColumn('error_hours');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 현재 사용 중인 페이지를 pages 테이블에 업데이트
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 기존 페이지 모두 삭제
|
||||
await knex('pages').del();
|
||||
|
||||
// 현재 사용 중인 페이지들을 등록
|
||||
await knex('pages').insert([
|
||||
// 공통 페이지
|
||||
{
|
||||
page_key: 'dashboard',
|
||||
page_name: '대시보드',
|
||||
page_path: '/pages/dashboard.html',
|
||||
category: 'common',
|
||||
description: '전체 현황 대시보드',
|
||||
is_admin_only: 0,
|
||||
display_order: 1
|
||||
},
|
||||
|
||||
// 작업 관련 페이지
|
||||
{
|
||||
page_key: 'work.tbm',
|
||||
page_name: 'TBM',
|
||||
page_path: '/pages/work/tbm.html',
|
||||
category: 'work',
|
||||
description: 'TBM (Tool Box Meeting) 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 10
|
||||
},
|
||||
{
|
||||
page_key: 'work.report_create',
|
||||
page_name: '작업보고서 작성',
|
||||
page_path: '/pages/work/report-create.html',
|
||||
category: 'work',
|
||||
description: '일일 작업보고서 작성',
|
||||
is_admin_only: 0,
|
||||
display_order: 11
|
||||
},
|
||||
{
|
||||
page_key: 'work.report_view',
|
||||
page_name: '작업보고서 조회',
|
||||
page_path: '/pages/work/report-view.html',
|
||||
category: 'work',
|
||||
description: '작업보고서 조회 및 검색',
|
||||
is_admin_only: 0,
|
||||
display_order: 12
|
||||
},
|
||||
{
|
||||
page_key: 'work.analysis',
|
||||
page_name: '작업 분석',
|
||||
page_path: '/pages/work/analysis.html',
|
||||
category: 'work',
|
||||
description: '작업 통계 및 분석',
|
||||
is_admin_only: 0,
|
||||
display_order: 13
|
||||
},
|
||||
|
||||
// Admin 페이지
|
||||
{
|
||||
page_key: 'admin.accounts',
|
||||
page_name: '계정 관리',
|
||||
page_path: '/pages/admin/accounts.html',
|
||||
category: 'admin',
|
||||
description: '사용자 계정 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 20
|
||||
},
|
||||
{
|
||||
page_key: 'admin.page_access',
|
||||
page_name: '페이지 권한 관리',
|
||||
page_path: '/pages/admin/page-access.html',
|
||||
category: 'admin',
|
||||
description: '사용자별 페이지 접근 권한 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 21
|
||||
},
|
||||
{
|
||||
page_key: 'admin.workers',
|
||||
page_name: '작업자 관리',
|
||||
page_path: '/pages/admin/workers.html',
|
||||
category: 'admin',
|
||||
description: '작업자 정보 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 22
|
||||
},
|
||||
{
|
||||
page_key: 'admin.projects',
|
||||
page_name: '프로젝트 관리',
|
||||
page_path: '/pages/admin/projects.html',
|
||||
category: 'admin',
|
||||
description: '프로젝트 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 23
|
||||
},
|
||||
{
|
||||
page_key: 'admin.workplaces',
|
||||
page_name: '작업장 관리',
|
||||
page_path: '/pages/admin/workplaces.html',
|
||||
category: 'admin',
|
||||
description: '작업장소 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 24
|
||||
},
|
||||
{
|
||||
page_key: 'admin.codes',
|
||||
page_name: '코드 관리',
|
||||
page_path: '/pages/admin/codes.html',
|
||||
category: 'admin',
|
||||
description: '시스템 코드 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 25
|
||||
},
|
||||
{
|
||||
page_key: 'admin.tasks',
|
||||
page_name: '작업 관리',
|
||||
page_path: '/pages/admin/tasks.html',
|
||||
category: 'admin',
|
||||
description: '작업 유형 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 26
|
||||
},
|
||||
|
||||
// 프로필 페이지
|
||||
{
|
||||
page_key: 'profile.info',
|
||||
page_name: '내 정보',
|
||||
page_path: '/pages/profile/info.html',
|
||||
category: 'profile',
|
||||
description: '내 프로필 정보',
|
||||
is_admin_only: 0,
|
||||
display_order: 30
|
||||
},
|
||||
{
|
||||
page_key: 'profile.password',
|
||||
page_name: '비밀번호 변경',
|
||||
page_path: '/pages/profile/password.html',
|
||||
category: 'profile',
|
||||
description: '비밀번호 변경',
|
||||
is_admin_only: 0,
|
||||
display_order: 31
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 현재 사용 중인 페이지 목록 업데이트 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages').del();
|
||||
console.log('✅ 페이지 목록 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 작업장 테이블에 레이아웃 이미지 컬럼 추가
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-01-28
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.table('workplaces', (table) => {
|
||||
table.string('layout_image', 500).nullable().comment('작업장 레이아웃 이미지 경로');
|
||||
});
|
||||
|
||||
console.log('✅ workplaces 테이블에 layout_image 컬럼 추가 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.table('workplaces', (table) => {
|
||||
table.dropColumn('layout_image');
|
||||
});
|
||||
|
||||
console.log('✅ workplaces 테이블에서 layout_image 컬럼 제거 완료');
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 설비 관리 테이블 생성
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-01-28
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.createTable('equipments', (table) => {
|
||||
table.increments('equipment_id').primary().comment('설비 ID');
|
||||
table.string('equipment_code', 50).notNullable().unique().comment('설비 코드 (예: CNC-01, LATHE-A)');
|
||||
table.string('equipment_name', 100).notNullable().comment('설비명');
|
||||
table.string('equipment_type', 50).nullable().comment('설비 유형 (예: CNC, 선반, 밀링 등)');
|
||||
table.string('model_name', 100).nullable().comment('모델명');
|
||||
table.string('manufacturer', 100).nullable().comment('제조사');
|
||||
table.date('installation_date').nullable().comment('설치일');
|
||||
table.string('serial_number', 100).nullable().comment('시리얼 번호');
|
||||
table.text('specifications').nullable().comment('사양 정보 (JSON 형태로 저장 가능)');
|
||||
table.enum('status', ['active', 'maintenance', 'inactive']).defaultTo('active').comment('설비 상태');
|
||||
table.text('notes').nullable().comment('비고');
|
||||
|
||||
// 작업장 연결
|
||||
table.integer('workplace_id').unsigned().nullable().comment('연결된 작업장 ID');
|
||||
table.foreign('workplace_id').references('workplace_id').inTable('workplaces').onDelete('SET NULL');
|
||||
|
||||
// 지도상 위치 정보 (백분율 기반)
|
||||
table.decimal('map_x_percent', 5, 2).nullable().comment('지도상 X 위치 (%)');
|
||||
table.decimal('map_y_percent', 5, 2).nullable().comment('지도상 Y 위치 (%)');
|
||||
table.decimal('map_width_percent', 5, 2).nullable().comment('지도상 영역 너비 (%)');
|
||||
table.decimal('map_height_percent', 5, 2).nullable().comment('지도상 영역 높이 (%)');
|
||||
|
||||
// 타임스탬프
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now()).comment('생성일시');
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정일시');
|
||||
|
||||
// 인덱스
|
||||
table.index('workplace_id');
|
||||
table.index('equipment_type');
|
||||
table.index('status');
|
||||
});
|
||||
|
||||
console.log('✅ equipments 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('equipments');
|
||||
console.log('✅ equipments 테이블 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Migration: Create vacation_requests table
|
||||
* Purpose: Track vacation request workflow (request, approval/rejection)
|
||||
* Date: 2026-01-29
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// Create vacation_requests table
|
||||
await knex.schema.createTable('vacation_requests', (table) => {
|
||||
table.increments('request_id').primary().comment('휴가 신청 ID');
|
||||
|
||||
// 작업자 정보
|
||||
table.integer('worker_id').notNullable().comment('작업자 ID');
|
||||
table.foreign('worker_id').references('worker_id').inTable('workers').onDelete('CASCADE');
|
||||
|
||||
// 휴가 정보
|
||||
table.integer('vacation_type_id').unsigned().notNullable().comment('휴가 유형 ID');
|
||||
table.foreign('vacation_type_id').references('id').inTable('vacation_types').onDelete('RESTRICT');
|
||||
|
||||
table.date('start_date').notNullable().comment('휴가 시작일');
|
||||
table.date('end_date').notNullable().comment('휴가 종료일');
|
||||
table.decimal('days_used', 4, 1).notNullable().comment('사용 일수 (0.5일 단위)');
|
||||
|
||||
table.text('reason').nullable().comment('휴가 사유');
|
||||
|
||||
// 신청 및 승인 정보
|
||||
table.enum('status', ['pending', 'approved', 'rejected'])
|
||||
.notNullable()
|
||||
.defaultTo('pending')
|
||||
.comment('승인 상태: pending(대기), approved(승인), rejected(거부)');
|
||||
|
||||
table.integer('requested_by').notNullable().comment('신청자 user_id');
|
||||
table.foreign('requested_by').references('user_id').inTable('users').onDelete('RESTRICT');
|
||||
|
||||
table.integer('reviewed_by').nullable().comment('승인/거부자 user_id');
|
||||
table.foreign('reviewed_by').references('user_id').inTable('users').onDelete('SET NULL');
|
||||
|
||||
table.timestamp('reviewed_at').nullable().comment('승인/거부 일시');
|
||||
table.text('review_note').nullable().comment('승인/거부 메모');
|
||||
|
||||
// 타임스탬프
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now()).comment('신청 일시');
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정 일시');
|
||||
|
||||
// 인덱스
|
||||
table.index('worker_id', 'idx_vacation_requests_worker');
|
||||
table.index('status', 'idx_vacation_requests_status');
|
||||
table.index(['start_date', 'end_date'], 'idx_vacation_requests_dates');
|
||||
});
|
||||
|
||||
console.log('✅ vacation_requests 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('vacation_requests');
|
||||
console.log('✅ vacation_requests 테이블 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Migration: Register attendance management pages
|
||||
* Purpose: Add 4 new pages to pages table for attendance management system
|
||||
* Date: 2026-01-29
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 페이지 등록 (실제 pages 테이블 컬럼에 맞춤)
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'daily-attendance',
|
||||
page_name: '일일 출퇴근 입력',
|
||||
page_path: '/pages/common/daily-attendance.html',
|
||||
description: '일일 출퇴근 기록 입력 페이지 (관리자/조장)',
|
||||
category: 'common',
|
||||
is_admin_only: false,
|
||||
display_order: 50
|
||||
},
|
||||
{
|
||||
page_key: 'monthly-attendance',
|
||||
page_name: '월별 출퇴근 현황',
|
||||
page_path: '/pages/common/monthly-attendance.html',
|
||||
description: '월별 출퇴근 현황 조회 페이지',
|
||||
category: 'common',
|
||||
is_admin_only: false,
|
||||
display_order: 51
|
||||
},
|
||||
{
|
||||
page_key: 'vacation-management',
|
||||
page_name: '휴가 관리',
|
||||
page_path: '/pages/common/vacation-management.html',
|
||||
description: '휴가 신청 및 승인 관리 페이지',
|
||||
category: 'common',
|
||||
is_admin_only: false,
|
||||
display_order: 52
|
||||
},
|
||||
{
|
||||
page_key: 'attendance-report-comparison',
|
||||
page_name: '출퇴근-작업보고서 대조',
|
||||
page_path: '/pages/admin/attendance-report-comparison.html',
|
||||
description: '출퇴근 기록과 작업보고서 대조 페이지 (관리자)',
|
||||
category: 'admin',
|
||||
is_admin_only: true,
|
||||
display_order: 120
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 출퇴근 관리 페이지 4개 등록 완료');
|
||||
|
||||
// Admin 사용자(user_id=1)에게 페이지 접근 권한 부여
|
||||
const adminUserId = 1;
|
||||
const pages = await knex('pages')
|
||||
.whereIn('page_key', [
|
||||
'daily-attendance',
|
||||
'monthly-attendance',
|
||||
'vacation-management',
|
||||
'attendance-report-comparison'
|
||||
])
|
||||
.select('id');
|
||||
|
||||
const accessRecords = pages.map(page => ({
|
||||
user_id: adminUserId,
|
||||
page_id: page.id,
|
||||
can_access: true,
|
||||
granted_by: adminUserId
|
||||
}));
|
||||
|
||||
await knex('user_page_access').insert(accessRecords);
|
||||
console.log('✅ Admin 사용자에게 출퇴근 관리 페이지 접근 권한 부여 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 페이지 삭제 (user_page_access는 FK CASCADE로 자동 삭제됨)
|
||||
await knex('pages')
|
||||
.whereIn('page_key', [
|
||||
'daily-attendance',
|
||||
'monthly-attendance',
|
||||
'vacation-management',
|
||||
'attendance-report-comparison'
|
||||
])
|
||||
.delete();
|
||||
|
||||
console.log('✅ 출퇴근 관리 페이지 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 출퇴근 출근 여부 필드 추가
|
||||
* 아침 출근 확인용 간단한 필드
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 컬럼 존재 여부 확인
|
||||
const hasColumn = await knex.schema.hasColumn('daily_attendance_records', 'is_present');
|
||||
|
||||
if (!hasColumn) {
|
||||
await knex.schema.table('daily_attendance_records', (table) => {
|
||||
// 출근 여부 (아침에 체크)
|
||||
table.boolean('is_present').defaultTo(true).comment('출근 여부');
|
||||
});
|
||||
|
||||
// 기존 데이터는 모두 출근으로 처리
|
||||
await knex('daily_attendance_records')
|
||||
.whereNotNull('id')
|
||||
.update({ is_present: true });
|
||||
|
||||
console.log('✅ is_present 컬럼 추가 완료');
|
||||
} else {
|
||||
console.log('⏭️ is_present 컬럼이 이미 존재합니다');
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
const hasColumn = await knex.schema.hasColumn('daily_attendance_records', 'is_present');
|
||||
|
||||
if (hasColumn) {
|
||||
await knex.schema.table('daily_attendance_records', (table) => {
|
||||
table.dropColumn('is_present');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 휴가 관리 페이지 분리 및 등록
|
||||
* - 기존 vacation-management.html을 2개 페이지로 분리
|
||||
* - vacation-request.html: 작업자 휴가 신청 및 본인 내역 확인
|
||||
* - vacation-management.html: 관리자 휴가 승인/직접입력/전체내역 (3개 탭)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 기존 vacation-management 페이지 삭제
|
||||
await knex('pages')
|
||||
.where('page_key', 'vacation-management')
|
||||
.del();
|
||||
|
||||
// 새로운 휴가 관리 페이지 2개 등록
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'vacation-request',
|
||||
page_name: '휴가 신청',
|
||||
page_path: '/pages/common/vacation-request.html',
|
||||
category: 'common',
|
||||
description: '작업자가 휴가를 신청하고 본인의 신청 내역을 확인하는 페이지',
|
||||
is_admin_only: 0,
|
||||
display_order: 51
|
||||
},
|
||||
{
|
||||
page_key: 'vacation-management',
|
||||
page_name: '휴가 관리',
|
||||
page_path: '/pages/common/vacation-management.html',
|
||||
category: 'common',
|
||||
description: '관리자가 휴가 승인, 직접 입력, 전체 내역을 관리하는 페이지',
|
||||
is_admin_only: 1,
|
||||
display_order: 52
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 분리 완료 (기존 1개 → 신규 2개)');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 새로운 페이지 삭제
|
||||
await knex('pages')
|
||||
.whereIn('page_key', ['vacation-request', 'vacation-management'])
|
||||
.del();
|
||||
|
||||
// 기존 vacation-management 페이지 복원
|
||||
await knex('pages').insert({
|
||||
page_key: 'vacation-management',
|
||||
page_name: '휴가 관리',
|
||||
page_path: '/pages/common/vacation-management.html.old',
|
||||
category: 'common',
|
||||
description: '휴가 신청 및 승인 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 50
|
||||
});
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* vacation_types 테이블 확장
|
||||
* - 특별 휴가 유형 추가 기능
|
||||
* - 차감 우선순위 관리
|
||||
* - 시스템 기본 휴가 보호
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// vacation_types 테이블 확장
|
||||
await knex.schema.table('vacation_types', (table) => {
|
||||
table.boolean('is_special').defaultTo(false).comment('특별 휴가 여부 (장기근속, 출산 등)');
|
||||
table.integer('priority').defaultTo(99).comment('차감 우선순위 (낮을수록 먼저 차감)');
|
||||
table.text('description').nullable().comment('휴가 설명');
|
||||
table.boolean('is_system').defaultTo(true).comment('시스템 기본 휴가 (삭제 불가)');
|
||||
});
|
||||
|
||||
// 기존 휴가 유형에 우선순위 설정
|
||||
await knex('vacation_types').where('type_code', 'ANNUAL').update({
|
||||
priority: 10,
|
||||
is_system: true,
|
||||
description: '근로기준법에 따른 연차 유급휴가'
|
||||
});
|
||||
|
||||
await knex('vacation_types').where('type_code', 'HALF_ANNUAL').update({
|
||||
priority: 10,
|
||||
is_system: true,
|
||||
description: '반일 연차 (0.5일)'
|
||||
});
|
||||
|
||||
await knex('vacation_types').where('type_code', 'SICK').update({
|
||||
priority: 20,
|
||||
is_system: true,
|
||||
description: '병가'
|
||||
});
|
||||
|
||||
await knex('vacation_types').where('type_code', 'SPECIAL').update({
|
||||
priority: 0,
|
||||
is_system: true,
|
||||
description: '경조사 휴가 (무급)'
|
||||
});
|
||||
|
||||
console.log('✅ vacation_types 테이블 확장 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 컬럼 삭제
|
||||
await knex.schema.table('vacation_types', (table) => {
|
||||
table.dropColumn('is_special');
|
||||
table.dropColumn('priority');
|
||||
table.dropColumn('description');
|
||||
table.dropColumn('is_system');
|
||||
});
|
||||
|
||||
console.log('✅ vacation_types 테이블 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* vacation_balance_details 테이블 생성 및 데이터 마이그레이션
|
||||
* - 작업자별, 휴가 유형별, 연도별 휴가 잔액 관리
|
||||
* - 기존 worker_vacation_balance 데이터 이관
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// vacation_balance_details 테이블 생성
|
||||
await knex.schema.createTable('vacation_balance_details', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.integer('worker_id').notNullable().comment('작업자 ID');
|
||||
table.integer('vacation_type_id').unsigned().notNullable().comment('휴가 유형 ID');
|
||||
table.integer('year').notNullable().comment('연도');
|
||||
table.decimal('total_days', 4, 1).defaultTo(0).comment('총 발생 일수');
|
||||
table.decimal('used_days', 4, 1).defaultTo(0).comment('사용 일수');
|
||||
table.text('notes').nullable().comment('비고');
|
||||
table.integer('created_by').notNullable().comment('생성자 ID');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스
|
||||
table.unique(['worker_id', 'vacation_type_id', 'year'], 'unique_worker_vacation_year');
|
||||
table.index(['worker_id', 'year'], 'idx_worker_year');
|
||||
table.index('vacation_type_id', 'idx_vacation_type');
|
||||
|
||||
// 외래키
|
||||
table.foreign('worker_id').references('worker_id').inTable('workers').onDelete('CASCADE');
|
||||
table.foreign('vacation_type_id').references('id').inTable('vacation_types').onDelete('RESTRICT');
|
||||
table.foreign('created_by').references('user_id').inTable('users');
|
||||
});
|
||||
|
||||
// remaining_days를 generated column으로 추가 (Raw SQL)
|
||||
await knex.raw(`
|
||||
ALTER TABLE vacation_balance_details
|
||||
ADD COLUMN remaining_days DECIMAL(4,1)
|
||||
GENERATED ALWAYS AS (total_days - used_days) STORED
|
||||
COMMENT '잔여 일수'
|
||||
`);
|
||||
|
||||
console.log('✅ vacation_balance_details 테이블 생성 완료');
|
||||
|
||||
// 기존 worker_vacation_balance 데이터를 vacation_balance_details로 마이그레이션
|
||||
const existingBalances = await knex('worker_vacation_balance').select('*');
|
||||
|
||||
if (existingBalances.length > 0) {
|
||||
// ANNUAL 휴가 유형 ID 조회
|
||||
const annualType = await knex('vacation_types')
|
||||
.where('type_code', 'ANNUAL')
|
||||
.first();
|
||||
|
||||
if (!annualType) {
|
||||
throw new Error('ANNUAL 휴가 유형을 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 관리자 사용자 ID 조회 (created_by 용)
|
||||
// role_id 1 = System Admin, 2 = Admin
|
||||
const adminUser = await knex('users')
|
||||
.whereIn('role_id', [1, 2])
|
||||
.first();
|
||||
|
||||
const createdById = adminUser ? adminUser.user_id : 1;
|
||||
|
||||
// 데이터 변환 및 삽입
|
||||
const balanceDetails = existingBalances.map(balance => ({
|
||||
worker_id: balance.worker_id,
|
||||
vacation_type_id: annualType.id,
|
||||
year: balance.year,
|
||||
total_days: balance.total_annual_leave || 0,
|
||||
used_days: balance.used_annual_leave || 0,
|
||||
notes: 'Migrated from worker_vacation_balance',
|
||||
created_by: createdById,
|
||||
created_at: balance.created_at,
|
||||
updated_at: balance.updated_at
|
||||
}));
|
||||
|
||||
await knex('vacation_balance_details').insert(balanceDetails);
|
||||
|
||||
console.log(`✅ ${balanceDetails.length}건의 기존 휴가 데이터 마이그레이션 완료`);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// vacation_balance_details 테이블 삭제
|
||||
await knex.schema.dropTableIfExists('vacation_balance_details');
|
||||
|
||||
console.log('✅ vacation_balance_details 테이블 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 새로운 휴가 관리 페이지 등록
|
||||
* - annual-vacation-overview: 연간 연차 현황 (차트)
|
||||
* - vacation-allocation: 휴가 발생 입력 및 관리
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'annual-vacation-overview',
|
||||
page_name: '연간 연차 현황',
|
||||
page_path: '/pages/common/annual-vacation-overview.html',
|
||||
category: 'common',
|
||||
description: '모든 작업자의 연간 연차 현황을 차트로 시각화',
|
||||
is_admin_only: 1,
|
||||
display_order: 54
|
||||
},
|
||||
{
|
||||
page_key: 'vacation-allocation',
|
||||
page_name: '휴가 발생 입력',
|
||||
page_path: '/pages/common/vacation-allocation.html',
|
||||
category: 'common',
|
||||
description: '작업자별 휴가 발생 입력 및 특별 휴가 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 55
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 휴가 관리 신규 페이지 2개 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages')
|
||||
.whereIn('page_key', ['annual-vacation-overview', 'vacation-allocation'])
|
||||
.del();
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 마이그레이션: 출입 신청 및 안전교육 시스템
|
||||
* - 방문 목적 타입 테이블
|
||||
* - 출입 신청 테이블
|
||||
* - 안전교육 기록 테이블
|
||||
*/
|
||||
|
||||
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('✅ 출입 신청 및 안전교육 시스템 테이블 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 마이그레이션: 출입 신청 및 안전관리 페이지 등록
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. 출입 신청 페이지 등록
|
||||
await knex('pages').insert({
|
||||
page_key: 'visit-request',
|
||||
page_name: '출입 신청',
|
||||
page_path: '/pages/work/visit-request.html',
|
||||
category: 'work',
|
||||
description: '작업장 출입 신청 및 안전교육 신청',
|
||||
is_admin_only: 0,
|
||||
display_order: 15
|
||||
});
|
||||
|
||||
// 2. 안전관리 대시보드 페이지 등록
|
||||
await knex('pages').insert({
|
||||
page_key: 'safety-management',
|
||||
page_name: '안전관리',
|
||||
page_path: '/pages/admin/safety-management.html',
|
||||
category: 'admin',
|
||||
description: '출입 신청 승인 및 안전교육 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 60
|
||||
});
|
||||
|
||||
// 3. 안전교육 진행 페이지 등록
|
||||
await knex('pages').insert({
|
||||
page_key: 'safety-training-conduct',
|
||||
page_name: '안전교육 진행',
|
||||
page_path: '/pages/admin/safety-training-conduct.html',
|
||||
category: 'admin',
|
||||
description: '안전교육 실시 및 서명 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 61
|
||||
});
|
||||
|
||||
console.log('✅ 출입 신청 및 안전관리 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages').whereIn('page_key', [
|
||||
'visit-request',
|
||||
'safety-management',
|
||||
'safety-training-conduct'
|
||||
]).delete();
|
||||
|
||||
console.log('✅ 출입 신청 및 안전관리 페이지 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 마이그레이션: 작업 중 문제 신고 시스템
|
||||
* - 신고 카테고리 테이블 (부적합/안전)
|
||||
* - 사전 정의 신고 항목 테이블
|
||||
* - 문제 신고 메인 테이블
|
||||
* - 상태 변경 이력 테이블
|
||||
*/
|
||||
|
||||
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('작업 중 문제 신고 시스템 테이블 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 마이그레이션: 문제 신고 관련 페이지 등록
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 문제 신고 등록 페이지
|
||||
await knex('pages').insert({
|
||||
page_key: 'issue-report',
|
||||
page_name: '문제 신고',
|
||||
page_path: '/pages/work/issue-report.html',
|
||||
category: 'work',
|
||||
description: '작업 중 문제(부적합/안전) 신고 등록',
|
||||
is_admin_only: 0,
|
||||
display_order: 16
|
||||
});
|
||||
|
||||
// 신고 목록 페이지
|
||||
await knex('pages').insert({
|
||||
page_key: 'issue-list',
|
||||
page_name: '신고 목록',
|
||||
page_path: '/pages/work/issue-list.html',
|
||||
category: 'work',
|
||||
description: '문제 신고 목록 조회 및 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 17
|
||||
});
|
||||
|
||||
// 신고 상세 페이지
|
||||
await knex('pages').insert({
|
||||
page_key: 'issue-detail',
|
||||
page_name: '신고 상세',
|
||||
page_path: '/pages/work/issue-detail.html',
|
||||
category: 'work',
|
||||
description: '문제 신고 상세 조회',
|
||||
is_admin_only: 0,
|
||||
display_order: 18
|
||||
});
|
||||
|
||||
console.log('✅ 문제 신고 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages').whereIn('page_key', [
|
||||
'issue-report',
|
||||
'issue-list',
|
||||
'issue-detail'
|
||||
]).delete();
|
||||
|
||||
console.log('✅ 문제 신고 페이지 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 작업보고서 부적합 상세 테이블 마이그레이션
|
||||
*
|
||||
* 기존: error_hours, error_type_id (단일 값)
|
||||
* 변경: 여러 부적합 원인 + 각 원인별 시간 저장 가능
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
// 1. work_report_defects 테이블 생성
|
||||
.createTable('work_report_defects', function(table) {
|
||||
table.increments('defect_id').primary();
|
||||
table.integer('report_id').notNullable()
|
||||
.comment('daily_work_reports의 id');
|
||||
table.integer('error_type_id').notNullable()
|
||||
.comment('error_types의 id (부적합 원인)');
|
||||
table.decimal('defect_hours', 4, 1).notNullable().defaultTo(0)
|
||||
.comment('해당 원인의 부적합 시간');
|
||||
table.text('note').nullable()
|
||||
.comment('추가 메모');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 외래키
|
||||
table.foreign('report_id').references('id').inTable('daily_work_reports').onDelete('CASCADE');
|
||||
table.foreign('error_type_id').references('id').inTable('error_types');
|
||||
|
||||
// 인덱스
|
||||
table.index('report_id');
|
||||
table.index('error_type_id');
|
||||
|
||||
// 같은 보고서에 같은 원인이 중복되지 않도록
|
||||
table.unique(['report_id', 'error_type_id']);
|
||||
})
|
||||
// 2. 기존 데이터 마이그레이션 (error_hours > 0인 경우)
|
||||
.then(function() {
|
||||
return knex.raw(`
|
||||
INSERT INTO work_report_defects (report_id, error_type_id, defect_hours, created_at)
|
||||
SELECT id, error_type_id, error_hours, created_at
|
||||
FROM daily_work_reports
|
||||
WHERE error_hours > 0 AND error_type_id IS NOT NULL
|
||||
`);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('work_report_defects');
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 안전 체크리스트 확장 마이그레이션
|
||||
*
|
||||
* 1. tbm_safety_checks 테이블 확장 (check_type, weather_condition, task_id)
|
||||
* 2. weather_conditions 테이블 생성 (날씨 조건 코드)
|
||||
* 3. tbm_weather_records 테이블 생성 (세션별 날씨 기록)
|
||||
* 4. 초기 날씨별 체크항목 데이터
|
||||
*
|
||||
* @since 2026-02-02
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
// 1. tbm_safety_checks 테이블 확장
|
||||
.alterTable('tbm_safety_checks', function(table) {
|
||||
table.enum('check_type', ['basic', 'weather', 'task']).defaultTo('basic').after('check_category');
|
||||
table.string('weather_condition', 50).nullable().after('check_type');
|
||||
table.integer('task_id').unsigned().nullable().after('weather_condition');
|
||||
|
||||
// 인덱스 추가
|
||||
table.index('check_type');
|
||||
table.index('weather_condition');
|
||||
table.index('task_id');
|
||||
})
|
||||
|
||||
// 2. weather_conditions 테이블 생성
|
||||
.createTable('weather_conditions', function(table) {
|
||||
table.string('condition_code', 50).primary();
|
||||
table.string('condition_name', 100).notNullable();
|
||||
table.text('description').nullable();
|
||||
table.string('icon', 50).nullable();
|
||||
table.decimal('temp_threshold_min', 4, 1).nullable(); // 최소 기온 기준
|
||||
table.decimal('temp_threshold_max', 4, 1).nullable(); // 최대 기온 기준
|
||||
table.decimal('wind_threshold', 4, 1).nullable(); // 풍속 기준 (m/s)
|
||||
table.decimal('precip_threshold', 5, 1).nullable(); // 강수량 기준 (mm)
|
||||
table.boolean('is_active').defaultTo(true);
|
||||
table.integer('display_order').defaultTo(0);
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
})
|
||||
|
||||
// 3. tbm_weather_records 테이블 생성
|
||||
.createTable('tbm_weather_records', function(table) {
|
||||
table.increments('record_id').primary();
|
||||
table.integer('session_id').unsigned().notNullable();
|
||||
table.date('weather_date').notNullable();
|
||||
table.decimal('temperature', 4, 1).nullable(); // 기온 (섭씨)
|
||||
table.integer('humidity').nullable(); // 습도 (%)
|
||||
table.decimal('wind_speed', 4, 1).nullable(); // 풍속 (m/s)
|
||||
table.decimal('precipitation', 5, 1).nullable(); // 강수량 (mm)
|
||||
table.string('sky_condition', 50).nullable(); // 하늘 상태
|
||||
table.string('weather_condition', 50).nullable(); // 주요 날씨 상태
|
||||
table.json('weather_conditions').nullable(); // 복수 조건 ['rain', 'wind']
|
||||
table.string('data_source', 50).defaultTo('api'); // 데이터 출처
|
||||
table.timestamp('fetched_at').nullable();
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 외래키
|
||||
table.foreign('session_id').references('session_id').inTable('tbm_sessions').onDelete('CASCADE');
|
||||
|
||||
// 인덱스
|
||||
table.index('weather_date');
|
||||
table.unique(['session_id']);
|
||||
})
|
||||
|
||||
// 4. 초기 데이터 삽입
|
||||
.then(function() {
|
||||
// 기존 체크항목을 'basic' 유형으로 업데이트
|
||||
return knex('tbm_safety_checks').update({ check_type: 'basic' });
|
||||
})
|
||||
.then(function() {
|
||||
// 날씨 조건 코드 삽입
|
||||
return knex('weather_conditions').insert([
|
||||
{ condition_code: 'clear', condition_name: '맑음', description: '맑은 날씨', icon: 'sunny', display_order: 1 },
|
||||
{ condition_code: 'rain', condition_name: '비', description: '비 오는 날씨', icon: 'rainy', precip_threshold: 0.1, display_order: 2 },
|
||||
{ condition_code: 'snow', condition_name: '눈', description: '눈 오는 날씨', icon: 'snowy', display_order: 3 },
|
||||
{ condition_code: 'heat', condition_name: '폭염', description: '기온 35도 이상', icon: 'hot', temp_threshold_min: 35, display_order: 4 },
|
||||
{ condition_code: 'cold', condition_name: '한파', description: '기온 영하 10도 이하', icon: 'cold', temp_threshold_max: -10, display_order: 5 },
|
||||
{ condition_code: 'wind', condition_name: '강풍', description: '풍속 10m/s 이상', icon: 'windy', wind_threshold: 10, display_order: 6 },
|
||||
{ condition_code: 'fog', condition_name: '안개', description: '시정 1km 미만', icon: 'foggy', display_order: 7 },
|
||||
{ condition_code: 'dust', condition_name: '미세먼지', description: '미세먼지 나쁨 이상', icon: 'dusty', display_order: 8 }
|
||||
]);
|
||||
})
|
||||
.then(function() {
|
||||
// 날씨별 안전 체크항목 삽입
|
||||
return knex('tbm_safety_checks').insert([
|
||||
// 비 (rain)
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'rain', check_item: '우의/우산 준비 확인', description: '비 오는 날 우의 또는 우산 준비 여부', is_required: true, display_order: 1 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'rain', check_item: '미끄럼 방지 조치 확인', description: '빗물로 인한 미끄러움 방지 조치', is_required: true, display_order: 2 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'rain', check_item: '전기 작업 중단 여부 확인', description: '우천 시 전기 작업 중단 필요성 확인', is_required: true, display_order: 3 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'rain', check_item: '배수 상태 확인', description: '작업장 배수 상태 점검', is_required: false, display_order: 4 },
|
||||
|
||||
// 눈 (snow)
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'snow', check_item: '제설 작업 완료 확인', description: '작업장 주변 제설 작업 완료 여부', is_required: true, display_order: 1 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'snow', check_item: '동파 방지 조치 확인', description: '배관 및 설비 동파 방지 조치', is_required: true, display_order: 2 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'snow', check_item: '미끄럼 방지 모래/염화칼슘 비치', description: '미끄럼 방지를 위한 모래 또는 염화칼슘 비치', is_required: true, display_order: 3 },
|
||||
|
||||
// 폭염 (heat)
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'heat', check_item: '그늘막/휴게소 확보', description: '무더위 휴식을 위한 그늘막 또는 휴게소 확보', is_required: true, display_order: 1 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'heat', check_item: '음료수/식염 포도당 비치', description: '열사병 예방을 위한 음료수 및 염분 보충제 비치', is_required: true, display_order: 2 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'heat', check_item: '무더위 휴식 시간 확보', description: '10~15시 사이 충분한 휴식 시간 확보', is_required: true, display_order: 3 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'heat', check_item: '작업자 건강 상태 확인', description: '열사병 증상 체크 및 건강 상태 확인', is_required: true, display_order: 4 },
|
||||
|
||||
// 한파 (cold)
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'cold', check_item: '방한복/방한장갑 착용 확인', description: '동상 방지를 위한 방한복 및 방한장갑 착용', is_required: true, display_order: 1 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'cold', check_item: '난방시설 가동 확인', description: '휴게 공간 난방시설 가동 상태 확인', is_required: true, display_order: 2 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'cold', check_item: '온열 음료 비치', description: '체온 유지를 위한 따뜻한 음료 비치', is_required: false, display_order: 3 },
|
||||
|
||||
// 강풍 (wind)
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'wind', check_item: '고소 작업 중단 여부 확인', description: '강풍 시 고소 작업 중단 필요성 확인', is_required: true, display_order: 1 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'wind', check_item: '자재/장비 결박 확인', description: '바람에 날릴 수 있는 자재 및 장비 고정', is_required: true, display_order: 2 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'wind', check_item: '가설물 안전 점검', description: '가설 구조물 및 비계 안전 상태 점검', is_required: true, display_order: 3 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'wind', check_item: '크레인 작업 중단 여부 확인', description: '강풍 시 크레인 작업 중단 필요성 확인', is_required: true, display_order: 4 },
|
||||
|
||||
// 안개 (fog)
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'fog', check_item: '경광등/조명 확보', description: '시정 확보를 위한 경광등 및 조명 설치', is_required: true, display_order: 1 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'fog', check_item: '차량 운행 주의 안내', description: '안개로 인한 차량 운행 주의 안내', is_required: true, display_order: 2 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'fog', check_item: '작업 구역 표시 강화', description: '시인성 확보를 위한 작업 구역 표시 강화', is_required: false, display_order: 3 },
|
||||
|
||||
// 미세먼지 (dust)
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'dust', check_item: '보호 마스크 착용 확인', description: 'KF94 이상 마스크 착용 여부 확인', is_required: true, display_order: 1 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'dust', check_item: '실외 작업 시간 조정', description: '미세먼지 농도에 따른 실외 작업 시간 조정', is_required: true, display_order: 2 },
|
||||
{ check_category: 'WEATHER', check_type: 'weather', weather_condition: 'dust', check_item: '호흡기 질환자 실내 배치', description: '호흡기 질환 작업자 실내 작업 배치', is_required: false, display_order: 3 }
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.dropTableIfExists('tbm_weather_records')
|
||||
.dropTableIfExists('weather_conditions')
|
||||
.then(function() {
|
||||
return knex.schema.alterTable('tbm_safety_checks', function(table) {
|
||||
table.dropIndex('check_type');
|
||||
table.dropIndex('weather_condition');
|
||||
table.dropIndex('task_id');
|
||||
table.dropColumn('check_type');
|
||||
table.dropColumn('weather_condition');
|
||||
table.dropColumn('task_id');
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* 페이지 구조 재구성 마이그레이션
|
||||
* - 페이지 경로 업데이트 (safety/, attendance/ 폴더로 이동)
|
||||
* - 카테고리 재분류
|
||||
* - 역할별 기본 페이지 권한 테이블 생성
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. 페이지 경로 업데이트 - safety 폴더로 이동된 페이지들
|
||||
const safetyPageUpdates = [
|
||||
{
|
||||
old_key: 'issue-report',
|
||||
new_key: 'safety.issue_report',
|
||||
new_path: '/pages/safety/issue-report.html',
|
||||
new_category: 'safety',
|
||||
new_name: '이슈 신고'
|
||||
},
|
||||
{
|
||||
old_key: 'issue-list',
|
||||
new_key: 'safety.issue_list',
|
||||
new_path: '/pages/safety/issue-list.html',
|
||||
new_category: 'safety',
|
||||
new_name: '이슈 목록'
|
||||
},
|
||||
{
|
||||
old_key: 'issue-detail',
|
||||
new_key: 'safety.issue_detail',
|
||||
new_path: '/pages/safety/issue-detail.html',
|
||||
new_category: 'safety',
|
||||
new_name: '이슈 상세'
|
||||
},
|
||||
{
|
||||
old_key: 'visit-request',
|
||||
new_key: 'safety.visit_request',
|
||||
new_path: '/pages/safety/visit-request.html',
|
||||
new_category: 'safety',
|
||||
new_name: '방문 요청'
|
||||
},
|
||||
{
|
||||
old_key: 'safety-management',
|
||||
new_key: 'safety.management',
|
||||
new_path: '/pages/safety/management.html',
|
||||
new_category: 'safety',
|
||||
new_name: '안전 관리'
|
||||
},
|
||||
{
|
||||
old_key: 'safety-training-conduct',
|
||||
new_key: 'safety.training_conduct',
|
||||
new_path: '/pages/safety/training-conduct.html',
|
||||
new_category: 'safety',
|
||||
new_name: '안전교육 진행'
|
||||
}
|
||||
];
|
||||
|
||||
// 2. 페이지 경로 업데이트 - attendance 폴더로 이동된 페이지들
|
||||
const attendancePageUpdates = [
|
||||
{
|
||||
old_key: 'daily-attendance',
|
||||
new_key: 'attendance.daily',
|
||||
new_path: '/pages/attendance/daily.html',
|
||||
new_category: 'attendance',
|
||||
new_name: '일일 출퇴근'
|
||||
},
|
||||
{
|
||||
old_key: 'monthly-attendance',
|
||||
new_key: 'attendance.monthly',
|
||||
new_path: '/pages/attendance/monthly.html',
|
||||
new_category: 'attendance',
|
||||
new_name: '월간 근태'
|
||||
},
|
||||
{
|
||||
old_key: 'annual-vacation-overview',
|
||||
new_key: 'attendance.annual_overview',
|
||||
new_path: '/pages/attendance/annual-overview.html',
|
||||
new_category: 'attendance',
|
||||
new_name: '연간 휴가 현황'
|
||||
},
|
||||
{
|
||||
old_key: 'vacation-request',
|
||||
new_key: 'attendance.vacation_request',
|
||||
new_path: '/pages/attendance/vacation-request.html',
|
||||
new_category: 'attendance',
|
||||
new_name: '휴가 신청'
|
||||
},
|
||||
{
|
||||
old_key: 'vacation-management',
|
||||
new_key: 'attendance.vacation_management',
|
||||
new_path: '/pages/attendance/vacation-management.html',
|
||||
new_category: 'attendance',
|
||||
new_name: '휴가 관리'
|
||||
},
|
||||
{
|
||||
old_key: 'vacation-allocation',
|
||||
new_key: 'attendance.vacation_allocation',
|
||||
new_path: '/pages/attendance/vacation-allocation.html',
|
||||
new_category: 'attendance',
|
||||
new_name: '휴가 발생 입력'
|
||||
}
|
||||
];
|
||||
|
||||
// 3. admin 폴더 내 파일명 변경
|
||||
const adminPageUpdates = [
|
||||
{
|
||||
old_key: 'attendance-report-comparison',
|
||||
new_key: 'admin.attendance_report',
|
||||
new_path: '/pages/admin/attendance-report.html',
|
||||
new_category: 'admin',
|
||||
new_name: '출퇴근-보고서 대조'
|
||||
}
|
||||
];
|
||||
|
||||
// 모든 업데이트 실행
|
||||
const allUpdates = [...safetyPageUpdates, ...attendancePageUpdates, ...adminPageUpdates];
|
||||
|
||||
for (const update of allUpdates) {
|
||||
await knex('pages')
|
||||
.where('page_key', update.old_key)
|
||||
.update({
|
||||
page_key: update.new_key,
|
||||
page_path: update.new_path,
|
||||
category: update.new_category,
|
||||
page_name: update.new_name
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 안전 체크리스트 관리 페이지 추가 (새로 생성된 페이지)
|
||||
const existingChecklistPage = await knex('pages')
|
||||
.where('page_key', 'safety.checklist_manage')
|
||||
.orWhere('page_key', 'safety-checklist-manage')
|
||||
.first();
|
||||
|
||||
if (!existingChecklistPage) {
|
||||
await knex('pages').insert({
|
||||
page_key: 'safety.checklist_manage',
|
||||
page_name: '안전 체크리스트 관리',
|
||||
page_path: '/pages/safety/checklist-manage.html',
|
||||
category: 'safety',
|
||||
description: '안전 체크리스트 항목 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 50
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 휴가 승인/직접입력 페이지 추가 (새로 생성된 페이지인 경우)
|
||||
const vacationPages = [
|
||||
{
|
||||
page_key: 'attendance.vacation_approval',
|
||||
page_name: '휴가 승인 관리',
|
||||
page_path: '/pages/attendance/vacation-approval.html',
|
||||
category: 'attendance',
|
||||
description: '휴가 신청 승인/거부',
|
||||
is_admin_only: 1,
|
||||
display_order: 65
|
||||
},
|
||||
{
|
||||
page_key: 'attendance.vacation_input',
|
||||
page_name: '휴가 직접 입력',
|
||||
page_path: '/pages/attendance/vacation-input.html',
|
||||
category: 'attendance',
|
||||
description: '관리자 휴가 직접 입력',
|
||||
is_admin_only: 1,
|
||||
display_order: 66
|
||||
}
|
||||
];
|
||||
|
||||
for (const page of vacationPages) {
|
||||
const existing = await knex('pages').where('page_key', page.page_key).first();
|
||||
if (!existing) {
|
||||
await knex('pages').insert(page);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. role_default_pages 테이블 생성 (역할별 기본 페이지 권한)
|
||||
const tableExists = await knex.schema.hasTable('role_default_pages');
|
||||
if (!tableExists) {
|
||||
await knex.schema.createTable('role_default_pages', (table) => {
|
||||
table.integer('role_id').unsigned().notNullable()
|
||||
.references('id').inTable('roles').onDelete('CASCADE');
|
||||
table.integer('page_id').unsigned().notNullable()
|
||||
.references('id').inTable('pages').onDelete('CASCADE');
|
||||
table.primary(['role_id', 'page_id']);
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
// 7. 기본 역할-페이지 매핑 데이터 삽입
|
||||
// 역할 조회
|
||||
const roles = await knex('roles').select('id', 'name');
|
||||
const pages = await knex('pages').select('id', 'page_key', 'category');
|
||||
|
||||
const roleMap = {};
|
||||
roles.forEach(r => { roleMap[r.name] = r.id; });
|
||||
|
||||
const pageMap = {};
|
||||
pages.forEach(p => { pageMap[p.page_key] = p.id; });
|
||||
|
||||
// Worker 역할 기본 페이지 (대시보드, 작업보고서, 휴가신청)
|
||||
const workerPages = [
|
||||
'dashboard',
|
||||
'work.report_create',
|
||||
'work.report_view',
|
||||
'attendance.vacation_request'
|
||||
];
|
||||
|
||||
// Leader 역할 기본 페이지 (Worker + TBM, 안전, 근태 일부)
|
||||
const leaderPages = [
|
||||
...workerPages,
|
||||
'work.tbm',
|
||||
'work.analysis',
|
||||
'safety.issue_report',
|
||||
'safety.issue_list',
|
||||
'attendance.daily',
|
||||
'attendance.monthly'
|
||||
];
|
||||
|
||||
// SafetyManager 역할 기본 페이지 (Leader + 안전 전체)
|
||||
const safetyManagerPages = [
|
||||
...leaderPages,
|
||||
'safety.issue_detail',
|
||||
'safety.visit_request',
|
||||
'safety.management',
|
||||
'safety.training_conduct',
|
||||
'safety.checklist_manage'
|
||||
];
|
||||
|
||||
// 역할별 페이지 매핑 삽입
|
||||
const rolePageMappings = [];
|
||||
|
||||
if (roleMap['Worker']) {
|
||||
workerPages.forEach(pageKey => {
|
||||
if (pageMap[pageKey]) {
|
||||
rolePageMappings.push({ role_id: roleMap['Worker'], page_id: pageMap[pageKey] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (roleMap['Leader']) {
|
||||
leaderPages.forEach(pageKey => {
|
||||
if (pageMap[pageKey]) {
|
||||
rolePageMappings.push({ role_id: roleMap['Leader'], page_id: pageMap[pageKey] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (roleMap['SafetyManager']) {
|
||||
safetyManagerPages.forEach(pageKey => {
|
||||
if (pageMap[pageKey]) {
|
||||
rolePageMappings.push({ role_id: roleMap['SafetyManager'], page_id: pageMap[pageKey] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 중복 제거 후 삽입
|
||||
for (const mapping of rolePageMappings) {
|
||||
const existing = await knex('role_default_pages')
|
||||
.where('role_id', mapping.role_id)
|
||||
.where('page_id', mapping.page_id)
|
||||
.first();
|
||||
|
||||
if (!existing) {
|
||||
await knex('role_default_pages').insert(mapping);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('페이지 구조 재구성 완료');
|
||||
console.log(`- 업데이트된 페이지: ${allUpdates.length}개`);
|
||||
console.log(`- 역할별 기본 페이지 매핑: ${rolePageMappings.length}개`);
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 1. role_default_pages 테이블 삭제
|
||||
await knex.schema.dropTableIfExists('role_default_pages');
|
||||
|
||||
// 2. 페이지 경로 원복 - safety → work/admin
|
||||
const safetyRevert = [
|
||||
{ new_key: 'safety.issue_report', old_key: 'issue-report', old_path: '/pages/work/issue-report.html', old_category: 'work' },
|
||||
{ new_key: 'safety.issue_list', old_key: 'issue-list', old_path: '/pages/work/issue-list.html', old_category: 'work' },
|
||||
{ new_key: 'safety.issue_detail', old_key: 'issue-detail', old_path: '/pages/work/issue-detail.html', old_category: 'work' },
|
||||
{ new_key: 'safety.visit_request', old_key: 'visit-request', old_path: '/pages/work/visit-request.html', old_category: 'work' },
|
||||
{ new_key: 'safety.management', old_key: 'safety-management', old_path: '/pages/admin/safety-management.html', old_category: 'admin' },
|
||||
{ new_key: 'safety.training_conduct', old_key: 'safety-training-conduct', old_path: '/pages/admin/safety-training-conduct.html', old_category: 'admin' },
|
||||
];
|
||||
|
||||
// 3. 페이지 경로 원복 - attendance → common
|
||||
const attendanceRevert = [
|
||||
{ new_key: 'attendance.daily', old_key: 'daily-attendance', old_path: '/pages/common/daily-attendance.html', old_category: 'common' },
|
||||
{ new_key: 'attendance.monthly', old_key: 'monthly-attendance', old_path: '/pages/common/monthly-attendance.html', old_category: 'common' },
|
||||
{ new_key: 'attendance.annual_overview', old_key: 'annual-vacation-overview', old_path: '/pages/common/annual-vacation-overview.html', old_category: 'common' },
|
||||
{ new_key: 'attendance.vacation_request', old_key: 'vacation-request', old_path: '/pages/common/vacation-request.html', old_category: 'common' },
|
||||
{ new_key: 'attendance.vacation_management', old_key: 'vacation-management', old_path: '/pages/common/vacation-management.html', old_category: 'common' },
|
||||
{ new_key: 'attendance.vacation_allocation', old_key: 'vacation-allocation', old_path: '/pages/common/vacation-allocation.html', old_category: 'common' },
|
||||
];
|
||||
|
||||
// 4. admin 파일명 원복
|
||||
const adminRevert = [
|
||||
{ new_key: 'admin.attendance_report', old_key: 'attendance-report-comparison', old_path: '/pages/admin/attendance-report-comparison.html', old_category: 'admin' }
|
||||
];
|
||||
|
||||
const allReverts = [...safetyRevert, ...attendanceRevert, ...adminRevert];
|
||||
|
||||
for (const revert of allReverts) {
|
||||
await knex('pages')
|
||||
.where('page_key', revert.new_key)
|
||||
.update({
|
||||
page_key: revert.old_key,
|
||||
page_path: revert.old_path,
|
||||
category: revert.old_category
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 새로 추가된 페이지 삭제
|
||||
await knex('pages').whereIn('page_key', [
|
||||
'safety.checklist_manage',
|
||||
'attendance.vacation_approval',
|
||||
'attendance.vacation_input'
|
||||
]).del();
|
||||
|
||||
console.log('페이지 구조 재구성 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 작업보고서 부적합에 카테고리/아이템 컬럼 추가
|
||||
*
|
||||
* 변경사항:
|
||||
* 1. work_report_defects 테이블에 category_id, item_id 컬럼 추가
|
||||
* 2. issue_report_categories, issue_report_items 테이블 참조
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
.alterTable('work_report_defects', function(table) {
|
||||
// 카테고리 ID 추가
|
||||
table.integer('category_id').unsigned().nullable()
|
||||
.comment('issue_report_categories의 category_id (직접 입력 시)')
|
||||
.after('issue_report_id');
|
||||
|
||||
// 아이템 ID 추가
|
||||
table.integer('item_id').unsigned().nullable()
|
||||
.comment('issue_report_items의 item_id (직접 입력 시)')
|
||||
.after('category_id');
|
||||
|
||||
// 외래키 추가
|
||||
table.foreign('category_id')
|
||||
.references('category_id')
|
||||
.inTable('issue_report_categories')
|
||||
.onDelete('SET NULL');
|
||||
|
||||
table.foreign('item_id')
|
||||
.references('item_id')
|
||||
.inTable('issue_report_items')
|
||||
.onDelete('SET NULL');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.alterTable('work_report_defects', function(table) {
|
||||
table.dropForeign('category_id');
|
||||
table.dropForeign('item_id');
|
||||
table.dropColumn('category_id');
|
||||
table.dropColumn('item_id');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 작업보고서 부적합을 신고 시스템과 연동
|
||||
*
|
||||
* 변경사항:
|
||||
* 1. work_report_defects 테이블에 issue_report_id 컬럼 추가
|
||||
* 2. error_type_id를 NULL 허용으로 변경 (신고 연동 시 불필요)
|
||||
* 3. work_issue_reports.report_id (unsigned int)와 타입 일치 필요
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
.alterTable('work_report_defects', function(table) {
|
||||
// 1. issue_report_id 컬럼 추가 (unsigned int로 work_issue_reports.report_id와 타입 일치)
|
||||
table.integer('issue_report_id').unsigned().nullable()
|
||||
.comment('work_issue_reports의 report_id (신고된 이슈 연결)')
|
||||
.after('error_type_id');
|
||||
|
||||
// 2. 외래키 추가 (work_issue_reports.report_id 참조)
|
||||
table.foreign('issue_report_id')
|
||||
.references('report_id')
|
||||
.inTable('work_issue_reports')
|
||||
.onDelete('SET NULL');
|
||||
|
||||
// 3. 인덱스 추가
|
||||
table.index('issue_report_id');
|
||||
})
|
||||
// 4. error_type_id를 NULL 허용으로 변경
|
||||
.then(function() {
|
||||
return knex.raw(`
|
||||
ALTER TABLE work_report_defects
|
||||
MODIFY COLUMN error_type_id INT NULL
|
||||
COMMENT 'error_types의 id (부적합 원인) - 레거시, issue_report_id 사용 권장'
|
||||
`);
|
||||
})
|
||||
// 5. 유니크 제약 수정 (issue_report_id도 고려)
|
||||
.then(function() {
|
||||
// 기존 유니크 제약 삭제
|
||||
return knex.raw(`
|
||||
ALTER TABLE work_report_defects
|
||||
DROP INDEX work_report_defects_report_id_error_type_id_unique
|
||||
`).catch(() => {
|
||||
// 인덱스가 없을 수 있음 - 무시
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
// 새 유니크 제약 추가 (report_id + issue_report_id 조합)
|
||||
return knex.raw(`
|
||||
ALTER TABLE work_report_defects
|
||||
ADD UNIQUE INDEX work_report_defects_report_issue_unique (report_id, issue_report_id)
|
||||
`).catch(() => {
|
||||
// 이미 존재할 수 있음 - 무시
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.alterTable('work_report_defects', function(table) {
|
||||
// 외래키 및 인덱스 삭제
|
||||
table.dropForeign('issue_report_id');
|
||||
table.dropIndex('issue_report_id');
|
||||
table.dropColumn('issue_report_id');
|
||||
})
|
||||
// error_type_id를 다시 NOT NULL로 변경
|
||||
.then(function() {
|
||||
return knex.raw(`
|
||||
ALTER TABLE work_report_defects
|
||||
MODIFY COLUMN error_type_id INT NOT NULL
|
||||
COMMENT 'error_types의 id (부적합 원인)'
|
||||
`);
|
||||
})
|
||||
// 기존 유니크 제약 복원
|
||||
.then(function() {
|
||||
return knex.raw(`
|
||||
ALTER TABLE work_report_defects
|
||||
DROP INDEX IF EXISTS work_report_defects_report_issue_unique
|
||||
`).catch(() => {});
|
||||
})
|
||||
.then(function() {
|
||||
return knex.raw(`
|
||||
ALTER TABLE work_report_defects
|
||||
ADD UNIQUE INDEX work_report_defects_report_id_error_type_id_unique (report_id, error_type_id)
|
||||
`).catch(() => {});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 알림 수신자 설정 테이블
|
||||
-- 알림 유형별로 지정된 사용자에게만 알림이 전송됨
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_recipients (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
notification_type ENUM('repair', 'safety', 'nonconformity', 'equipment', 'maintenance', 'system') NOT NULL COMMENT '알림 유형',
|
||||
user_id INT NOT NULL COMMENT '수신자 ID',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL COMMENT '등록자',
|
||||
UNIQUE KEY unique_type_user (notification_type, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
|
||||
INDEX idx_nr_type (notification_type),
|
||||
INDEX idx_nr_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='알림 수신자 설정';
|
||||
|
||||
-- 알림 유형 설명:
|
||||
-- repair: 설비 수리 신청
|
||||
-- safety: 안전 신고
|
||||
-- nonconformity: 부적합 신고
|
||||
-- equipment: 설비 관련
|
||||
-- maintenance: 정기점검
|
||||
-- system: 시스템 알림
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 설비 테이블에 구입처 및 구입가격 컬럼 추가
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-02-04
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 컬럼 존재 여부 확인
|
||||
const hasSupplier = await knex.schema.hasColumn('equipments', 'supplier');
|
||||
const hasPurchasePrice = await knex.schema.hasColumn('equipments', 'purchase_price');
|
||||
|
||||
if (!hasSupplier || !hasPurchasePrice) {
|
||||
await knex.schema.alterTable('equipments', (table) => {
|
||||
if (!hasSupplier) {
|
||||
table.string('supplier', 100).nullable().after('manufacturer').comment('구입처');
|
||||
}
|
||||
if (!hasPurchasePrice) {
|
||||
table.decimal('purchase_price', 15, 0).nullable().after('supplier').comment('구입가격');
|
||||
}
|
||||
});
|
||||
console.log('✅ equipments 테이블에 supplier, purchase_price 컬럼 추가 완료');
|
||||
} else {
|
||||
console.log('ℹ️ supplier, purchase_price 컬럼이 이미 존재합니다. 스킵합니다.');
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.alterTable('equipments', (table) => {
|
||||
table.dropColumn('supplier');
|
||||
table.dropColumn('purchase_price');
|
||||
});
|
||||
|
||||
console.log('✅ equipments 테이블에서 supplier, purchase_price 컬럼 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 마이그레이션: 일일순회점검 시스템
|
||||
* 작성일: 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('✅ 모든 일일순회점검 시스템 테이블 제거 완료');
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 설비 테이블 컬럼 추가 (phpMyAdmin용)
|
||||
-- 현재 구조: equipment_id, factory_id, equipment_name, model, status, purchase_date, description, created_at, updated_at
|
||||
|
||||
-- 필요한 컬럼 추가
|
||||
ALTER TABLE equipments ADD COLUMN equipment_code VARCHAR(50) NULL COMMENT '관리번호' AFTER equipment_id;
|
||||
ALTER TABLE equipments ADD COLUMN specifications TEXT NULL COMMENT '규격' AFTER model;
|
||||
ALTER TABLE equipments ADD COLUMN serial_number VARCHAR(100) NULL COMMENT '시리얼번호(S/N)' AFTER specifications;
|
||||
ALTER TABLE equipments ADD COLUMN supplier VARCHAR(100) NULL COMMENT '구입처' AFTER purchase_date;
|
||||
ALTER TABLE equipments ADD COLUMN purchase_price DECIMAL(15, 0) NULL COMMENT '구입가격' AFTER supplier;
|
||||
ALTER TABLE equipments ADD COLUMN manufacturer VARCHAR(100) NULL COMMENT '제조사(메이커)' AFTER purchase_price;
|
||||
|
||||
-- equipment_code에 유니크 인덱스 추가
|
||||
ALTER TABLE equipments ADD UNIQUE INDEX idx_equipment_code (equipment_code);
|
||||
@@ -0,0 +1,138 @@
|
||||
-- 설비 관리 전체 설정 스크립트
|
||||
-- 1. 새 컬럼 추가 (supplier, purchase_price)
|
||||
-- 2. 65개 설비 데이터 입력
|
||||
--
|
||||
-- 실행: mysql -u [user] -p [database] < 20260204_equipment_full_setup.sql
|
||||
|
||||
-- ============================================
|
||||
-- STEP 1: 새 컬럼 추가
|
||||
-- ============================================
|
||||
|
||||
-- 컬럼이 이미 존재하는지 확인 후 추가
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'equipments';
|
||||
|
||||
-- supplier 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname
|
||||
AND table_name = @tablename
|
||||
AND column_name = 'supplier';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN supplier VARCHAR(100) NULL COMMENT ''구입처'' AFTER manufacturer',
|
||||
'SELECT ''supplier column already exists''');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- purchase_price 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname
|
||||
AND table_name = @tablename
|
||||
AND column_name = 'purchase_price';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN purchase_price DECIMAL(15, 0) NULL COMMENT ''구입가격'' AFTER supplier',
|
||||
'SELECT ''purchase_price column already exists''');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT '컬럼 추가 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 2: 기존 데이터 삭제 (선택사항)
|
||||
-- ============================================
|
||||
-- 주의: 기존 데이터가 있으면 삭제됩니다
|
||||
DELETE FROM equipments WHERE equipment_code LIKE 'TKP-%';
|
||||
|
||||
-- ============================================
|
||||
-- STEP 3: 65개 설비 데이터 입력
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO equipments (equipment_code, equipment_name, model_name, specifications, serial_number, installation_date, supplier, purchase_price, manufacturer, status) VALUES
|
||||
('TKP-001', 'AIR COMPRESSOR', 'AR10E', '7.5KW(10HP)', 'K603023Y', '2016-06-01', '지티씨', NULL, '경원', 'active'),
|
||||
('TKP-002', 'TURN TABLE', 'YCT-200T', '220V', NULL, '2016-05-30', '형진종합공구', 3600000, '유체기계', 'active'),
|
||||
('TKP-003', 'BAND SAW(中)', 'CY300W', '1500W*380V', '20150943', '2016-05-30', '형진종합공구', 4800000, '유림싸이겐', 'active'),
|
||||
('TKP-004', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2016-05-30', '형진종합공구', 2700000, '렉스', 'active'),
|
||||
('TKP-005', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2019-05-30', NULL, NULL, '렉스', 'active'),
|
||||
('TKP-006', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-001', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-007', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-002', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-008', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-003', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-009', 'CO2용접기', 'COD-500A', '500A', '10880', '2016-05-30', '형진종합공구', 2000000, '대성용접기', 'active'),
|
||||
('TKP-010', 'O2용접기', 'GSORK', '220V', NULL, '2016-05-30', '형진종합공구', 620000, '재현오토닉스', 'active'),
|
||||
('TKP-011', 'PIPE BEVELLING MACHINE', 'S-200LT_MT(테이블포함)', '2" ~ 8"', 'KR-17030007', '2017-03-29', 'DCS ENG', 12000000, 'DCS ENG', 'active'),
|
||||
('TKP-012', 'CO2용접기', '500MX', '220/380V,500A', NULL, '2017-08-02', '현대용접기', 1800000, '현대용접기', 'active'),
|
||||
('TKP-013', '프라즈마', 'Perfect-150AP', '220/380/440V,140A', NULL, '2017-08-02', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-014', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-015', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-016', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-005', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-017', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-006', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-018', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-007', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-019', '전해연마기', 'ONB-8000VP', '220V/MAX1200W', '8022701', '2018-03-13', '오토기전', 1450000, '메탈브라이트(오토기전)', 'active'),
|
||||
('TKP-020', '지게차', '50DA-9F', '5000KGS', 'HHKHFV36JJ0000061', '2018-05-10', '현대지게차', 45000000, '현대지게차', 'active'),
|
||||
('TKP-021', '조방', NULL, '3658*12190', NULL, '2018-05-11', '천우기계공업/삼덕금속', 14200000, '테크니컬코리아', 'active'),
|
||||
('TKP-022', 'BAND SAW(大)', 'WBS-RC500AN', '3,300kgs / 7.88kw', 'BC50A18-005F001', '2018-05-31', '원공사', 36000000, '원공사', 'active'),
|
||||
('TKP-023', 'AIR COMPRESSOR', 'AR20E', '0.95Mpa', 'AR020FE358', '2018-06-05', '경원기계', NULL, '경원기계', 'active'),
|
||||
('TKP-024', 'TURN TABLE', 'YCT-200TA', '220V', NULL, '2018-06-12', '청운종합공구', 3245000, '유체기계', 'active'),
|
||||
('TKP-025', 'TIG용접기', 'Perfect-500WT', '500A/AC DC', 'ADKAC017B-006', '2018-06-12', '현대용접기', 2400000, '퍼펙트대대', 'active'),
|
||||
('TKP-026', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAI018B-002', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-027', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-009', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-028', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-001', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-029', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1806077', '2018-07-06', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-030', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807028', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-031', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807029', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-032', '만능탭 드릴링머신', 'SF-TDM32', '1.5KW', NULL, '2018-11-09', '㈜애스앤애프', 2927000, '㈜애스앤에프', 'active'),
|
||||
('TKP-033', '지게차', '30D-9B', '2850KGS', 'HHKHHN51KK0000864', '2019-03-06', '현대지게차', 29400000, '현대지게차', 'active'),
|
||||
('TKP-034', '갠츄리크레인', 'CRANE - DHG', '50/10Ton x SP20M x T/L50M x H15M', NULL, '2019-05-09', '유진산업기계', 249000000, '반도호이스트', 'active'),
|
||||
('TKP-035', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-036', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-037', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-038', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-039', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-040', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-041', 'AIR CONDITIONER', '코끼리 냉장고', NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-042', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-043', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-044', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-045', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-046', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-047', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-048', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-049', '자동용접기', NULL, NULL, NULL, '2019-01-01', 'Swage-Lok', 50000000, 'Swage-Lok', 'active'),
|
||||
('TKP-050', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'NITTO', 'active'),
|
||||
('TKP-051', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'Key-Yang', 'active'),
|
||||
('TKP-052', 'Tube Bending M/C', NULL, NULL, NULL, '2020-01-01', NULL, NULL, 'REMS', 'active'),
|
||||
('TKP-053', 'Unit Test Panel', NULL, NULL, NULL, '2021-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-054', '고소작업대', NULL, '500 Kg', NULL, '2021-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-055', '용접봉 건조기', '주문제작', '박스형', NULL, '2022-05-04', '진원하이텍', 2300000, '진원하이텍', 'active'),
|
||||
('TKP-056', 'C&T 가공기', 'MS-CTK469', NULL, NULL, '2022-07-20', 'Swage-Lok', 7347600, 'Swage-Lok', 'active'),
|
||||
('TKP-057', '테이블형 튜브 벤딩기', 'MS-BTT-K', '1/2", 1/4"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-058', '자동용접기 헤드', 'SWS-10H-D-15', '1/2"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-059', '천장주행크레인', 'HC-75D-13105', '7.5ton', NULL, '2023-06-09', '에이치앤씨', 22800000, '에이치앤씨', 'active'),
|
||||
('TKP-060', 'AED', 'CU-SP1 Plus', '저출력심장충격기', NULL, '2023-11-09', '제이메디', 1600000, '제이메디', 'active'),
|
||||
('TKP-061', '베벨머신', 'S-150', 'O.D 20mm ~ 170mm', NULL, '2023-12-12', 'DCSENG', 16000000, 'DCSENG', 'active'),
|
||||
('TKP-062', '피막제거기', 'CM4_OD_GC', '최대 폭 48mm, 최대 깊이 15mm, 6"이상', NULL, '2023-12-12', 'DCSENG', 2000000, 'DCSENG', 'active'),
|
||||
('TKP-063', '피막제거기', 'S-CM4_OD', '최대 폭 48mm, 최대 깊이 15mm, 1" 이상', NULL, '2023-12-12', 'DCSENG', 1200000, 'DCSENG', 'active'),
|
||||
('TKP-064', '텅스텐 가공기', 'S-TGR', '0.89kg, 0.25~3.2', NULL, '2023-12-12', 'DCSENG', 800000, 'DCSENG', 'active'),
|
||||
('TKP-065', '전동대차', 'LPM15', '2.0 ton', NULL, '2023-12-20', '두산산업차량', 2800000, '두산산업차량', 'active');
|
||||
|
||||
-- ============================================
|
||||
-- STEP 4: 결과 확인
|
||||
-- ============================================
|
||||
SELECT '===== 설비 데이터 입력 완료 =====' AS status;
|
||||
SELECT COUNT(*) AS total_equipments FROM equipments;
|
||||
SELECT
|
||||
SUM(CASE WHEN purchase_price IS NOT NULL THEN purchase_price ELSE 0 END) AS total_purchase_value,
|
||||
COUNT(CASE WHEN purchase_price IS NOT NULL THEN 1 END) AS equipments_with_price
|
||||
FROM equipments;
|
||||
|
||||
-- 최신 10개 설비 확인
|
||||
SELECT equipment_code, equipment_name, supplier,
|
||||
FORMAT(purchase_price, 0) AS purchase_price_formatted,
|
||||
manufacturer
|
||||
FROM equipments
|
||||
ORDER BY equipment_code DESC
|
||||
LIMIT 10;
|
||||
@@ -0,0 +1,73 @@
|
||||
-- 설비 데이터 입력 (실제 테이블 구조에 맞춤)
|
||||
-- 먼저 20260204_equipment_add_columns.sql 실행 후 이 파일 실행
|
||||
|
||||
-- 기존 TKP 데이터 삭제
|
||||
DELETE FROM equipments WHERE equipment_code LIKE 'TKP-%';
|
||||
|
||||
-- 65개 설비 데이터 입력
|
||||
INSERT INTO equipments (equipment_code, equipment_name, model, specifications, serial_number, purchase_date, supplier, purchase_price, manufacturer, status) VALUES
|
||||
('TKP-001', 'AIR COMPRESSOR', 'AR10E', '7.5KW(10HP)', 'K603023Y', '2016-06-01', '지티씨', NULL, '경원', 'active'),
|
||||
('TKP-002', 'TURN TABLE', 'YCT-200T', '220V', NULL, '2016-05-30', '형진종합공구', 3600000, '유체기계', 'active'),
|
||||
('TKP-003', 'BAND SAW(中)', 'CY300W', '1500W*380V', '20150943', '2016-05-30', '형진종합공구', 4800000, '유림싸이겐', 'active'),
|
||||
('TKP-004', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2016-05-30', '형진종합공구', 2700000, '렉스', 'active'),
|
||||
('TKP-005', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2019-05-30', NULL, NULL, '렉스', 'active'),
|
||||
('TKP-006', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-001', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-007', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-002', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-008', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-003', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-009', 'CO2용접기', 'COD-500A', '500A', '10880', '2016-05-30', '형진종합공구', 2000000, '대성용접기', 'active'),
|
||||
('TKP-010', 'O2용접기', 'GSORK', '220V', NULL, '2016-05-30', '형진종합공구', 620000, '재현오토닉스', 'active'),
|
||||
('TKP-011', 'PIPE BEVELLING MACHINE', 'S-200LT_MT(테이블포함)', '2" ~ 8"', 'KR-17030007', '2017-03-29', 'DCS ENG', 12000000, 'DCS ENG', 'active'),
|
||||
('TKP-012', 'CO2용접기', '500MX', '220/380V,500A', NULL, '2017-08-02', '현대용접기', 1800000, '현대용접기', 'active'),
|
||||
('TKP-013', '프라즈마', 'Perfect-150AP', '220/380/440V,140A', NULL, '2017-08-02', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-014', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-015', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-016', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-005', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-017', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-006', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-018', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-007', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-019', '전해연마기', 'ONB-8000VP', '220V/MAX1200W', '8022701', '2018-03-13', '오토기전', 1450000, '메탈브라이트(오토기전)', 'active'),
|
||||
('TKP-020', '지게차', '50DA-9F', '5000KGS', 'HHKHFV36JJ0000061', '2018-05-10', '현대지게차', 45000000, '현대지게차', 'active'),
|
||||
('TKP-021', '조방', NULL, '3658*12190', NULL, '2018-05-11', '천우기계공업/삼덕금속', 14200000, '테크니컬코리아', 'active'),
|
||||
('TKP-022', 'BAND SAW(大)', 'WBS-RC500AN', '3,300kgs / 7.88kw', 'BC50A18-005F001', '2018-05-31', '원공사', 36000000, '원공사', 'active'),
|
||||
('TKP-023', 'AIR COMPRESSOR', 'AR20E', '0.95Mpa', 'AR020FE358', '2018-06-05', '경원기계', NULL, '경원기계', 'active'),
|
||||
('TKP-024', 'TURN TABLE', 'YCT-200TA', '220V', NULL, '2018-06-12', '청운종합공구', 3245000, '유체기계', 'active'),
|
||||
('TKP-025', 'TIG용접기', 'Perfect-500WT', '500A/AC DC', 'ADKAC017B-006', '2018-06-12', '현대용접기', 2400000, '퍼펙트대대', 'active'),
|
||||
('TKP-026', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAI018B-002', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-027', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-009', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-028', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-001', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-029', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1806077', '2018-07-06', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-030', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807028', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-031', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807029', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-032', '만능탭 드릴링머신', 'SF-TDM32', '1.5KW', NULL, '2018-11-09', '㈜애스앤애프', 2927000, '㈜애스앤에프', 'active'),
|
||||
('TKP-033', '지게차', '30D-9B', '2850KGS', 'HHKHHN51KK0000864', '2019-03-06', '현대지게차', 29400000, '현대지게차', 'active'),
|
||||
('TKP-034', '갠츄리크레인', 'CRANE - DHG', '50/10Ton x SP20M x T/L50M x H15M', NULL, '2019-05-09', '유진산업기계', 249000000, '반도호이스트', 'active'),
|
||||
('TKP-035', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-036', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-037', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-038', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-039', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-040', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-041', 'AIR CONDITIONER', '코끼리 냉장고', NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-042', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-043', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-044', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-045', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-046', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-047', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-048', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-049', '자동용접기', NULL, NULL, NULL, '2019-01-01', 'Swage-Lok', 50000000, 'Swage-Lok', 'active'),
|
||||
('TKP-050', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'NITTO', 'active'),
|
||||
('TKP-051', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'Key-Yang', 'active'),
|
||||
('TKP-052', 'Tube Bending M/C', NULL, NULL, NULL, '2020-01-01', NULL, NULL, 'REMS', 'active'),
|
||||
('TKP-053', 'Unit Test Panel', NULL, NULL, NULL, '2021-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-054', '고소작업대', NULL, '500 Kg', NULL, '2021-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-055', '용접봉 건조기', '주문제작', '박스형', NULL, '2022-05-04', '진원하이텍', 2300000, '진원하이텍', 'active'),
|
||||
('TKP-056', 'C&T 가공기', 'MS-CTK469', NULL, NULL, '2022-07-20', 'Swage-Lok', 7347600, 'Swage-Lok', 'active'),
|
||||
('TKP-057', '테이블형 튜브 벤딩기', 'MS-BTT-K', '1/2", 1/4"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-058', '자동용접기 헤드', 'SWS-10H-D-15', '1/2"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-059', '천장주행크레인', 'HC-75D-13105', '7.5ton', NULL, '2023-06-09', '에이치앤씨', 22800000, '에이치앤씨', 'active'),
|
||||
('TKP-060', 'AED', 'CU-SP1 Plus', '저출력심장충격기', NULL, '2023-11-09', '제이메디', 1600000, '제이메디', 'active'),
|
||||
('TKP-061', '베벨머신', 'S-150', 'O.D 20mm ~ 170mm', NULL, '2023-12-12', 'DCSENG', 16000000, 'DCSENG', 'active'),
|
||||
('TKP-062', '피막제거기', 'CM4_OD_GC', '최대 폭 48mm, 최대 깊이 15mm, 6"이상', NULL, '2023-12-12', 'DCSENG', 2000000, 'DCSENG', 'active'),
|
||||
('TKP-063', '피막제거기', 'S-CM4_OD', '최대 폭 48mm, 최대 깊이 15mm, 1" 이상', NULL, '2023-12-12', 'DCSENG', 1200000, 'DCSENG', 'active'),
|
||||
('TKP-064', '텅스텐 가공기', 'S-TGR', '0.89kg, 0.25~3.2', NULL, '2023-12-12', 'DCSENG', 800000, 'DCSENG', 'active'),
|
||||
('TKP-065', '전동대차', 'LPM15', '2.0 ton', NULL, '2023-12-20', '두산산업차량', 2800000, '두산산업차량', 'active');
|
||||
@@ -0,0 +1,78 @@
|
||||
-- 설비 관리 설정 (phpMyAdmin용 단순 버전)
|
||||
-- phpMyAdmin에서 가져오기로 실행
|
||||
|
||||
-- ============================================
|
||||
-- STEP 2: 기존 TKP 데이터 삭제
|
||||
-- ============================================
|
||||
DELETE FROM equipments WHERE equipment_code LIKE 'TKP-%';
|
||||
|
||||
-- ============================================
|
||||
-- STEP 3: 65개 설비 데이터 입력
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO equipments (equipment_code, equipment_name, model_name, specifications, serial_number, installation_date, supplier, purchase_price, manufacturer, status) VALUES
|
||||
('TKP-001', 'AIR COMPRESSOR', 'AR10E', '7.5KW(10HP)', 'K603023Y', '2016-06-01', '지티씨', NULL, '경원', 'active'),
|
||||
('TKP-002', 'TURN TABLE', 'YCT-200T', '220V', NULL, '2016-05-30', '형진종합공구', 3600000, '유체기계', 'active'),
|
||||
('TKP-003', 'BAND SAW(中)', 'CY300W', '1500W*380V', '20150943', '2016-05-30', '형진종합공구', 4800000, '유림싸이겐', 'active'),
|
||||
('TKP-004', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2016-05-30', '형진종합공구', 2700000, '렉스', 'active'),
|
||||
('TKP-005', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2019-05-30', NULL, NULL, '렉스', 'active'),
|
||||
('TKP-006', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-001', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-007', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-002', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-008', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-003', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-009', 'CO2용접기', 'COD-500A', '500A', '10880', '2016-05-30', '형진종합공구', 2000000, '대성용접기', 'active'),
|
||||
('TKP-010', 'O2용접기', 'GSORK', '220V', NULL, '2016-05-30', '형진종합공구', 620000, '재현오토닉스', 'active'),
|
||||
('TKP-011', 'PIPE BEVELLING MACHINE', 'S-200LT_MT(테이블포함)', '2" ~ 8"', 'KR-17030007', '2017-03-29', 'DCS ENG', 12000000, 'DCS ENG', 'active'),
|
||||
('TKP-012', 'CO2용접기', '500MX', '220/380V,500A', NULL, '2017-08-02', '현대용접기', 1800000, '현대용접기', 'active'),
|
||||
('TKP-013', '프라즈마', 'Perfect-150AP', '220/380/440V,140A', NULL, '2017-08-02', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-014', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-015', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-016', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-005', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-017', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-006', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-018', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-007', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-019', '전해연마기', 'ONB-8000VP', '220V/MAX1200W', '8022701', '2018-03-13', '오토기전', 1450000, '메탈브라이트(오토기전)', 'active'),
|
||||
('TKP-020', '지게차', '50DA-9F', '5000KGS', 'HHKHFV36JJ0000061', '2018-05-10', '현대지게차', 45000000, '현대지게차', 'active'),
|
||||
('TKP-021', '조방', NULL, '3658*12190', NULL, '2018-05-11', '천우기계공업/삼덕금속', 14200000, '테크니컬코리아', 'active'),
|
||||
('TKP-022', 'BAND SAW(大)', 'WBS-RC500AN', '3,300kgs / 7.88kw', 'BC50A18-005F001', '2018-05-31', '원공사', 36000000, '원공사', 'active'),
|
||||
('TKP-023', 'AIR COMPRESSOR', 'AR20E', '0.95Mpa', 'AR020FE358', '2018-06-05', '경원기계', NULL, '경원기계', 'active'),
|
||||
('TKP-024', 'TURN TABLE', 'YCT-200TA', '220V', NULL, '2018-06-12', '청운종합공구', 3245000, '유체기계', 'active'),
|
||||
('TKP-025', 'TIG용접기', 'Perfect-500WT', '500A/AC DC', 'ADKAC017B-006', '2018-06-12', '현대용접기', 2400000, '퍼펙트대대', 'active'),
|
||||
('TKP-026', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAI018B-002', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-027', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-009', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-028', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-001', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-029', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1806077', '2018-07-06', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-030', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807028', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-031', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807029', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-032', '만능탭 드릴링머신', 'SF-TDM32', '1.5KW', NULL, '2018-11-09', '㈜애스앤애프', 2927000, '㈜애스앤에프', 'active'),
|
||||
('TKP-033', '지게차', '30D-9B', '2850KGS', 'HHKHHN51KK0000864', '2019-03-06', '현대지게차', 29400000, '현대지게차', 'active'),
|
||||
('TKP-034', '갠츄리크레인', 'CRANE - DHG', '50/10Ton x SP20M x T/L50M x H15M', NULL, '2019-05-09', '유진산업기계', 249000000, '반도호이스트', 'active'),
|
||||
('TKP-035', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-036', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-037', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-038', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-039', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-040', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-041', 'AIR CONDITIONER', '코끼리 냉장고', NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-042', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-043', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-044', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-045', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-046', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-047', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-048', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-049', '자동용접기', NULL, NULL, NULL, '2019-01-01', 'Swage-Lok', 50000000, 'Swage-Lok', 'active'),
|
||||
('TKP-050', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'NITTO', 'active'),
|
||||
('TKP-051', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'Key-Yang', 'active'),
|
||||
('TKP-052', 'Tube Bending M/C', NULL, NULL, NULL, '2020-01-01', NULL, NULL, 'REMS', 'active'),
|
||||
('TKP-053', 'Unit Test Panel', NULL, NULL, NULL, '2021-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-054', '고소작업대', NULL, '500 Kg', NULL, '2021-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-055', '용접봉 건조기', '주문제작', '박스형', NULL, '2022-05-04', '진원하이텍', 2300000, '진원하이텍', 'active'),
|
||||
('TKP-056', 'C&T 가공기', 'MS-CTK469', NULL, NULL, '2022-07-20', 'Swage-Lok', 7347600, 'Swage-Lok', 'active'),
|
||||
('TKP-057', '테이블형 튜브 벤딩기', 'MS-BTT-K', '1/2", 1/4"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-058', '자동용접기 헤드', 'SWS-10H-D-15', '1/2"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-059', '천장주행크레인', 'HC-75D-13105', '7.5ton', NULL, '2023-06-09', '에이치앤씨', 22800000, '에이치앤씨', 'active'),
|
||||
('TKP-060', 'AED', 'CU-SP1 Plus', '저출력심장충격기', NULL, '2023-11-09', '제이메디', 1600000, '제이메디', 'active'),
|
||||
('TKP-061', '베벨머신', 'S-150', 'O.D 20mm ~ 170mm', NULL, '2023-12-12', 'DCSENG', 16000000, 'DCSENG', 'active'),
|
||||
('TKP-062', '피막제거기', 'CM4_OD_GC', '최대 폭 48mm, 최대 깊이 15mm, 6"이상', NULL, '2023-12-12', 'DCSENG', 2000000, 'DCSENG', 'active'),
|
||||
('TKP-063', '피막제거기', 'S-CM4_OD', '최대 폭 48mm, 최대 깊이 15mm, 1" 이상', NULL, '2023-12-12', 'DCSENG', 1200000, 'DCSENG', 'active'),
|
||||
('TKP-064', '텅스텐 가공기', 'S-TGR', '0.89kg, 0.25~3.2', NULL, '2023-12-12', 'DCSENG', 800000, 'DCSENG', 'active'),
|
||||
('TKP-065', '전동대차', 'LPM15', '2.0 ton', NULL, '2023-12-20', '두산산업차량', 2800000, '두산산업차량', 'active');
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 설비 사진 테이블 생성
|
||||
-- 실행: docker exec -i tkfb_db mysql -u hyungi -p'your_password' hyungi < db/migrations/20260205001000_create_equipment_photos.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS equipment_photos (
|
||||
photo_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT UNSIGNED NOT NULL,
|
||||
photo_path VARCHAR(255) NOT NULL COMMENT '이미지 경로',
|
||||
description VARCHAR(200) COMMENT '사진 설명',
|
||||
display_order INT DEFAULT 0 COMMENT '표시 순서',
|
||||
uploaded_by INT COMMENT '업로드한 사용자 ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_eq_photos_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES equipments(equipment_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_eq_photos_user FOREIGN KEY (uploaded_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
INDEX idx_eq_photos_equipment_id (equipment_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,174 @@
|
||||
-- 설비 임시이동 필드 추가 및 신고 시스템 연동
|
||||
-- 실행: docker exec -i tkfb_db mysql -u hyungi -p'your_password' hyungi < db/migrations/20260205002000_add_equipment_move_fields.sql
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
|
||||
-- ============================================
|
||||
-- STEP 1: equipments 테이블에 임시이동 필드 추가
|
||||
-- ============================================
|
||||
|
||||
-- current_workplace_id 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_workplace_id';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_workplace_id INT UNSIGNED NULL COMMENT ''현재 임시 위치 - 작업장 ID'' AFTER map_height_percent',
|
||||
'SELECT ''current_workplace_id already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_x_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_x_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_x_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 X%'' AFTER current_workplace_id',
|
||||
'SELECT ''current_map_x_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_y_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_y_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_y_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 Y%'' AFTER current_map_x_percent',
|
||||
'SELECT ''current_map_y_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_width_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_width_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_width_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 너비%'' AFTER current_map_y_percent',
|
||||
'SELECT ''current_map_width_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_height_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_height_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_height_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 높이%'' AFTER current_map_width_percent',
|
||||
'SELECT ''current_map_height_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- is_temporarily_moved 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'is_temporarily_moved';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN is_temporarily_moved BOOLEAN DEFAULT FALSE COMMENT ''임시 이동 상태'' AFTER current_map_height_percent',
|
||||
'SELECT ''is_temporarily_moved already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- moved_at 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'moved_at';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN moved_at DATETIME NULL COMMENT ''이동 일시'' AFTER is_temporarily_moved',
|
||||
'SELECT ''moved_at already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- moved_by 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'moved_by';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN moved_by INT NULL COMMENT ''이동 처리자'' AFTER moved_at',
|
||||
'SELECT ''moved_by already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Foreign Key: current_workplace_id -> workplaces
|
||||
SELECT COUNT(*) INTO @fk_exists
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND constraint_name = 'fk_eq_current_workplace';
|
||||
|
||||
SET @sql = IF(@fk_exists = 0,
|
||||
'ALTER TABLE equipments ADD CONSTRAINT fk_eq_current_workplace FOREIGN KEY (current_workplace_id) REFERENCES workplaces(workplace_id) ON DELETE SET NULL',
|
||||
'SELECT ''fk_eq_current_workplace already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT 'equipments 임시이동 필드 추가 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 2: work_issue_reports에 equipment_id 필드 추가
|
||||
-- ============================================
|
||||
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'work_issue_reports' AND column_name = 'equipment_id';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE work_issue_reports ADD COLUMN equipment_id INT UNSIGNED NULL COMMENT ''관련 설비 ID'' AFTER visit_request_id',
|
||||
'SELECT ''equipment_id already exists in work_issue_reports''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Foreign Key
|
||||
SELECT COUNT(*) INTO @fk_exists
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = @dbname AND table_name = 'work_issue_reports' AND constraint_name = 'fk_wir_equipment';
|
||||
|
||||
SET @sql = IF(@fk_exists = 0 AND @col_exists = 0,
|
||||
'ALTER TABLE work_issue_reports ADD CONSTRAINT fk_wir_equipment FOREIGN KEY (equipment_id) REFERENCES equipments(equipment_id) ON DELETE SET NULL',
|
||||
'SELECT ''fk_wir_equipment already exists or column not added''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Index
|
||||
SELECT COUNT(*) INTO @idx_exists
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = @dbname AND table_name = 'work_issue_reports' AND index_name = 'idx_wir_equipment_id';
|
||||
|
||||
SET @sql = IF(@idx_exists = 0 AND @col_exists = 0,
|
||||
'ALTER TABLE work_issue_reports ADD INDEX idx_wir_equipment_id (equipment_id)',
|
||||
'SELECT ''idx_wir_equipment_id already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT 'work_issue_reports equipment_id 추가 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 3: 설비 수리 카테고리 추가
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO issue_report_categories (category_type, category_name, description, display_order, is_active)
|
||||
SELECT 'nonconformity', '설비 수리', '설비 고장 및 수리 요청', 10, 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_categories WHERE category_name = '설비 수리'
|
||||
);
|
||||
|
||||
-- 설비 수리 카테고리에 기본 항목 추가
|
||||
SET @category_id = (SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리' LIMIT 1);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '기계 고장', '기계 작동 불가 또는 이상', 'high', 1, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '기계 고장'
|
||||
);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '부품 교체 필요', '소모품 또는 부품 교체 필요', 'medium', 2, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '부품 교체 필요'
|
||||
);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '정기 점검 필요', '예방 정비 또는 정기 점검', 'low', 3, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '정기 점검 필요'
|
||||
);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '외부 수리 필요', '전문 업체 수리가 필요한 경우', 'high', 4, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '외부 수리 필요'
|
||||
);
|
||||
|
||||
SELECT '설비 수리 카테고리 및 항목 추가 완료' AS status;
|
||||
@@ -0,0 +1,86 @@
|
||||
-- 설비 외부반출 테이블 생성 및 상태 ENUM 확장
|
||||
-- 실행: docker exec -i tkfb_db mysql -u hyungi -p'your_password' hyungi < db/migrations/20260205003000_create_equipment_external_logs.sql
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
|
||||
-- ============================================
|
||||
-- STEP 1: equipment_external_logs 테이블 생성
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS equipment_external_logs (
|
||||
log_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT UNSIGNED NOT NULL COMMENT '설비 ID',
|
||||
log_type ENUM('export', 'return') NOT NULL COMMENT '반출/반입',
|
||||
export_date DATE COMMENT '반출일',
|
||||
expected_return_date DATE COMMENT '반입 예정일',
|
||||
actual_return_date DATE COMMENT '실제 반입일',
|
||||
destination VARCHAR(200) COMMENT '반출처 (수리업체명 등)',
|
||||
reason TEXT COMMENT '반출 사유',
|
||||
notes TEXT COMMENT '비고',
|
||||
exported_by INT COMMENT '반출 담당자',
|
||||
returned_by INT COMMENT '반입 담당자',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_eel_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES equipments(equipment_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_eel_exported_by FOREIGN KEY (exported_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_eel_returned_by FOREIGN KEY (returned_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
INDEX idx_eel_equipment_id (equipment_id),
|
||||
INDEX idx_eel_log_type (log_type),
|
||||
INDEX idx_eel_export_date (export_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SELECT 'equipment_external_logs 테이블 생성 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 2: equipments 테이블 status ENUM 확장
|
||||
-- ============================================
|
||||
|
||||
-- 현재 status 컬럼의 ENUM 값 확인 후 확장
|
||||
-- 기존: active, maintenance, repair_needed, inactive
|
||||
-- 추가: external (외부 반출), repair_external (수리 외주)
|
||||
|
||||
ALTER TABLE equipments
|
||||
MODIFY COLUMN status ENUM(
|
||||
'active', -- 정상 가동
|
||||
'maintenance', -- 점검 중
|
||||
'repair_needed', -- 수리 필요
|
||||
'inactive', -- 비활성
|
||||
'external', -- 외부 반출
|
||||
'repair_external' -- 수리 외주 (외부 수리)
|
||||
) DEFAULT 'active' COMMENT '설비 상태';
|
||||
|
||||
SELECT 'equipments status ENUM 확장 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 3: 설비 이동 이력 테이블 생성 (선택)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS equipment_move_logs (
|
||||
log_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT UNSIGNED NOT NULL COMMENT '설비 ID',
|
||||
move_type ENUM('temporary', 'return') NOT NULL COMMENT '임시이동/복귀',
|
||||
from_workplace_id INT UNSIGNED COMMENT '이전 작업장',
|
||||
to_workplace_id INT UNSIGNED COMMENT '이동 작업장',
|
||||
from_x_percent DECIMAL(5,2) COMMENT '이전 X좌표',
|
||||
from_y_percent DECIMAL(5,2) COMMENT '이전 Y좌표',
|
||||
to_x_percent DECIMAL(5,2) COMMENT '이동 X좌표',
|
||||
to_y_percent DECIMAL(5,2) COMMENT '이동 Y좌표',
|
||||
reason TEXT COMMENT '이동 사유',
|
||||
moved_by INT COMMENT '이동 처리자',
|
||||
moved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_eml_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES equipments(equipment_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_eml_from_workplace FOREIGN KEY (from_workplace_id)
|
||||
REFERENCES workplaces(workplace_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_eml_to_workplace FOREIGN KEY (to_workplace_id)
|
||||
REFERENCES workplaces(workplace_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_eml_moved_by FOREIGN KEY (moved_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
INDEX idx_eml_equipment_id (equipment_id),
|
||||
INDEX idx_eml_move_type (move_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SELECT 'equipment_move_logs 테이블 생성 완료' AS status;
|
||||
@@ -0,0 +1,46 @@
|
||||
-- 알림 시스템 테이블 생성
|
||||
-- 실행: docker exec -i tkfb_db mysql -u hyungi -p'your_password' hyungi < db/migrations/20260205004000_create_notifications.sql
|
||||
|
||||
-- ============================================
|
||||
-- STEP 1: notifications 테이블 생성
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
notification_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NULL COMMENT '특정 사용자에게만 표시 (NULL이면 전체)',
|
||||
type ENUM('repair', 'safety', 'system', 'equipment', 'maintenance') NOT NULL DEFAULT 'system',
|
||||
title VARCHAR(200) NOT NULL,
|
||||
message TEXT,
|
||||
link_url VARCHAR(500) COMMENT '클릭시 이동할 URL',
|
||||
reference_type VARCHAR(50) COMMENT '연관 테이블 (equipment_repair_requests 등)',
|
||||
reference_id INT COMMENT '연관 레코드 ID',
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
read_at DATETIME NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL,
|
||||
INDEX idx_notifications_user (user_id),
|
||||
INDEX idx_notifications_type (type),
|
||||
INDEX idx_notifications_is_read (is_read),
|
||||
INDEX idx_notifications_created (created_at DESC)
|
||||
);
|
||||
|
||||
-- 수리 요청 테이블 수정 (알림 연동을 위해)
|
||||
-- equipment_repair_requests 테이블이 없으면 생성
|
||||
CREATE TABLE IF NOT EXISTS equipment_repair_requests (
|
||||
request_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT UNSIGNED NOT NULL,
|
||||
repair_type VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
urgency ENUM('low', 'normal', 'high', 'urgent') DEFAULT 'normal',
|
||||
status ENUM('pending', 'in_progress', 'completed', 'cancelled') DEFAULT 'pending',
|
||||
requested_by INT,
|
||||
assigned_to INT NULL,
|
||||
completed_at DATETIME NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (equipment_id) REFERENCES equipments(equipment_id) ON DELETE CASCADE,
|
||||
INDEX idx_err_equipment (equipment_id),
|
||||
INDEX idx_err_status (status),
|
||||
INDEX idx_err_created (created_at DESC)
|
||||
);
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 마이그레이션: TBM 기반 작업보고서의 work_type_id를 task_id로 수정
|
||||
*
|
||||
* 문제: TBM에서 작업보고서 생성 시 work_type_id(공정 ID)가 저장됨
|
||||
* 해결: tbm_team_assignments 테이블의 task_id로 업데이트
|
||||
*
|
||||
* 실행: node db/migrations/20260205_fix_work_type_id_data.js
|
||||
*/
|
||||
|
||||
const { getDb } = require('../../dbPool');
|
||||
|
||||
async function migrate() {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...\n');
|
||||
|
||||
try {
|
||||
// 1. 수정 대상 확인 (TBM 기반이면서 work_type_id가 task_id와 다른 경우)
|
||||
const [checkResult] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id as current_work_type_id,
|
||||
ta.task_id as correct_task_id,
|
||||
ta.work_type_id as tbm_work_type_id,
|
||||
w.worker_name,
|
||||
dwr.report_date
|
||||
FROM daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
INNER JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
AND ta.task_id IS NOT NULL
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
ORDER BY dwr.report_date DESC
|
||||
`);
|
||||
|
||||
console.log(`📊 수정 대상: ${checkResult.length}개 레코드\n`);
|
||||
|
||||
if (checkResult.length === 0) {
|
||||
console.log('✅ 수정할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 수정 대상 샘플 출력
|
||||
console.log('📋 수정 대상 샘플 (최대 10개):');
|
||||
console.log('─'.repeat(80));
|
||||
checkResult.slice(0, 10).forEach(row => {
|
||||
console.log(` ID: ${row.id} | ${row.worker_name} | ${row.report_date}`);
|
||||
console.log(` 현재 work_type_id: ${row.current_work_type_id} → 올바른 task_id: ${row.correct_task_id}`);
|
||||
});
|
||||
if (checkResult.length > 10) {
|
||||
console.log(` ... 외 ${checkResult.length - 10}개`);
|
||||
}
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
// 2. 업데이트 실행
|
||||
const [updateResult] = await db.query(`
|
||||
UPDATE daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
SET dwr.work_type_id = ta.task_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
AND ta.task_id IS NOT NULL
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
`);
|
||||
|
||||
console.log(`\n✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
|
||||
|
||||
// 3. 수정 결과 확인
|
||||
const [verifyResult] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id,
|
||||
ta.task_id,
|
||||
t.task_name,
|
||||
wt.name as work_type_name
|
||||
FROM daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
console.log('\n📋 수정 후 샘플 확인:');
|
||||
console.log('─'.repeat(80));
|
||||
verifyResult.forEach(row => {
|
||||
console.log(` ID: ${row.id} | work_type_id: ${row.work_type_id} | task: ${row.task_name || 'N/A'} | 공정: ${row.work_type_name || 'N/A'}`);
|
||||
});
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 마이그레이션 실패:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 실행
|
||||
migrate()
|
||||
.then(() => {
|
||||
console.log('\n🎉 마이그레이션 완료!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('\n💥 마이그레이션 실패:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
-- ============================================
|
||||
-- error_types → issue_report_items 마이그레이션
|
||||
-- 실행 전 반드시 백업하세요!
|
||||
-- ============================================
|
||||
|
||||
-- STEP 1: 현재 상태 확인
|
||||
-- ============================================
|
||||
SELECT 'Before Migration' as status;
|
||||
SELECT error_type_id, COUNT(*) as cnt FROM daily_work_reports WHERE error_type_id IS NOT NULL GROUP BY error_type_id;
|
||||
|
||||
|
||||
-- STEP 2: 매핑 업데이트 실행
|
||||
-- ============================================
|
||||
-- 주의: 순서가 중요! (충돌 방지를 위해 큰 숫자부터)
|
||||
|
||||
-- 6 (검사불량) → 14 (치수 검사 누락)
|
||||
UPDATE daily_work_reports SET error_type_id = 14 WHERE error_type_id = 6;
|
||||
|
||||
-- 5 (설비고장) → 38 (기계 고장)
|
||||
UPDATE daily_work_reports SET error_type_id = 38 WHERE error_type_id = 5;
|
||||
|
||||
-- 4 (작업불량) → 43 (NDE 불합격)
|
||||
UPDATE daily_work_reports SET error_type_id = 43 WHERE error_type_id = 4;
|
||||
|
||||
-- 3 (입고지연) → 1 (배관 자재 미입고) - 이미 1이므로 충돌 가능, 임시값 사용
|
||||
UPDATE daily_work_reports SET error_type_id = 99991 WHERE error_type_id = 3;
|
||||
|
||||
-- 2 (외주작업 불량) → 10 (외관 불량)
|
||||
UPDATE daily_work_reports SET error_type_id = 10 WHERE error_type_id = 2;
|
||||
|
||||
-- 1 (설계미스) → 6 (도면 치수 오류) - 6은 이미 업데이트됨
|
||||
UPDATE daily_work_reports SET error_type_id = 6 WHERE error_type_id = 1;
|
||||
|
||||
-- 임시값 복원: 99991 → 1
|
||||
UPDATE daily_work_reports SET error_type_id = 1 WHERE error_type_id = 99991;
|
||||
|
||||
|
||||
-- STEP 3: 마이그레이션 결과 확인
|
||||
-- ============================================
|
||||
SELECT 'After Migration' as status;
|
||||
SELECT
|
||||
dwr.error_type_id,
|
||||
iri.item_name,
|
||||
irc.category_name,
|
||||
COUNT(*) as cnt
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
|
||||
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
WHERE dwr.error_type_id IS NOT NULL
|
||||
GROUP BY dwr.error_type_id, iri.item_name, irc.category_name;
|
||||
|
||||
|
||||
-- STEP 4: error_types 테이블 삭제 (선택사항)
|
||||
-- ============================================
|
||||
-- 마이그레이션 확인 후 주석 해제하여 실행
|
||||
-- DROP TABLE IF EXISTS error_types;
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 마이그레이션: error_types에서 issue_report_items로 전환
|
||||
*
|
||||
* 기존 daily_work_reports.error_type_id가 error_types.id를 참조하던 것을
|
||||
* issue_report_items.item_id를 참조하도록 변경
|
||||
*
|
||||
* 기존 error_types 데이터:
|
||||
* id=1: 설계미스
|
||||
* id=2: 외주작업 불량
|
||||
* id=3: 입고지연
|
||||
*
|
||||
* 새 issue_report_categories 데이터:
|
||||
* category_id=1: 자재누락 (nonconformity)
|
||||
* category_id=2: 설계미스 (nonconformity)
|
||||
* category_id=3: 입고불량 (nonconformity)
|
||||
*
|
||||
* 매핑 전략:
|
||||
* - error_types.id=1 (설계미스) → issue_report_items에서 '설계미스' 카테고리의 첫 번째 항목
|
||||
* - error_types.id=2 (외주작업 불량) → issue_report_items에서 '입고불량' 카테고리의 '외관 불량' 항목
|
||||
* - error_types.id=3 (입고지연) → issue_report_items에서 '자재누락' 카테고리의 첫 번째 항목
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('=== error_type_id 마이그레이션 시작 ===');
|
||||
|
||||
// 1. 기존 error_types 데이터와 새 issue_report_items 매핑 테이블 조회
|
||||
const [categories] = await knex.raw(`
|
||||
SELECT category_id, category_name
|
||||
FROM issue_report_categories
|
||||
WHERE category_type = 'nonconformity'
|
||||
`);
|
||||
console.log('부적합 카테고리:', categories);
|
||||
|
||||
const [items] = await knex.raw(`
|
||||
SELECT iri.item_id, iri.item_name, iri.category_id, irc.category_name
|
||||
FROM issue_report_items iri
|
||||
JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
WHERE irc.category_type = 'nonconformity'
|
||||
ORDER BY iri.category_id, iri.display_order
|
||||
`);
|
||||
console.log('부적합 항목:', items);
|
||||
|
||||
// 2. 매핑 정의 (기존 error_type_id → 새 issue_report_items.item_id)
|
||||
// 설계미스 카테고리 찾기
|
||||
const designMissCategory = categories.find(c => c.category_name === '설계미스');
|
||||
const incomingDefectCategory = categories.find(c => c.category_name === '입고불량');
|
||||
const materialShortageCategory = categories.find(c => c.category_name === '자재누락');
|
||||
|
||||
// 각 카테고리의 첫 번째 항목 찾기
|
||||
const designMissItem = items.find(i => i.category_id === designMissCategory?.category_id);
|
||||
const incomingDefectItem = items.find(i => i.category_id === incomingDefectCategory?.category_id);
|
||||
const materialShortageItem = items.find(i => i.category_id === materialShortageCategory?.category_id);
|
||||
|
||||
console.log('매핑 결과:');
|
||||
console.log(' - 설계미스(1) → item_id:', designMissItem?.item_id);
|
||||
console.log(' - 외주작업불량(2) → item_id:', incomingDefectItem?.item_id);
|
||||
console.log(' - 입고지연(3) → item_id:', materialShortageItem?.item_id);
|
||||
|
||||
// 3. 기존 데이터 업데이트
|
||||
if (designMissItem) {
|
||||
const [result1] = await knex.raw(`
|
||||
UPDATE daily_work_reports
|
||||
SET error_type_id = ?
|
||||
WHERE error_type_id = 1
|
||||
`, [designMissItem.item_id]);
|
||||
console.log('설계미스(1) 업데이트:', result1.affectedRows, '건');
|
||||
}
|
||||
|
||||
if (incomingDefectItem) {
|
||||
const [result2] = await knex.raw(`
|
||||
UPDATE daily_work_reports
|
||||
SET error_type_id = ?
|
||||
WHERE error_type_id = 2
|
||||
`, [incomingDefectItem.item_id]);
|
||||
console.log('외주작업불량(2) 업데이트:', result2.affectedRows, '건');
|
||||
}
|
||||
|
||||
if (materialShortageItem) {
|
||||
const [result3] = await knex.raw(`
|
||||
UPDATE daily_work_reports
|
||||
SET error_type_id = ?
|
||||
WHERE error_type_id = 3
|
||||
`, [materialShortageItem.item_id]);
|
||||
console.log('입고지연(3) 업데이트:', result3.affectedRows, '건');
|
||||
}
|
||||
|
||||
// 4. 매핑 안된 나머지 데이터 확인 (4 이상의 error_type_id)
|
||||
const [unmapped] = await knex.raw(`
|
||||
SELECT DISTINCT error_type_id, COUNT(*) as cnt
|
||||
FROM daily_work_reports
|
||||
WHERE error_type_id IS NOT NULL
|
||||
AND error_type_id NOT IN (?, ?, ?)
|
||||
GROUP BY error_type_id
|
||||
`, [
|
||||
designMissItem?.item_id || 0,
|
||||
incomingDefectItem?.item_id || 0,
|
||||
materialShortageItem?.item_id || 0
|
||||
]);
|
||||
|
||||
if (unmapped.length > 0) {
|
||||
console.log('⚠️ 매핑되지 않은 error_type_id 발견:', unmapped);
|
||||
console.log(' 이 데이터는 수동으로 확인 필요');
|
||||
}
|
||||
|
||||
console.log('=== error_type_id 마이그레이션 완료 ===');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 롤백은 복잡하므로 로그만 출력
|
||||
console.log('⚠️ 이 마이그레이션은 자동 롤백을 지원하지 않습니다.');
|
||||
console.log(' 데이터 복구가 필요한 경우 백업에서 복원해주세요.');
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
-- ============================================
|
||||
-- error_types → issue_report_items 마이그레이션
|
||||
-- ============================================
|
||||
|
||||
-- STEP 1: 현재 데이터 확인
|
||||
-- ============================================
|
||||
|
||||
-- 기존 error_types 확인
|
||||
SELECT * FROM error_types;
|
||||
|
||||
-- 새 issue_report_categories 확인 (부적합만)
|
||||
SELECT * FROM issue_report_categories WHERE category_type = 'nonconformity';
|
||||
|
||||
-- 새 issue_report_items 확인 (부적합만)
|
||||
SELECT
|
||||
iri.item_id,
|
||||
iri.item_name,
|
||||
iri.category_id,
|
||||
irc.category_name
|
||||
FROM issue_report_items iri
|
||||
JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
WHERE irc.category_type = 'nonconformity'
|
||||
ORDER BY irc.display_order, iri.display_order;
|
||||
|
||||
-- 현재 daily_work_reports에서 사용 중인 error_type_id 확인
|
||||
SELECT
|
||||
error_type_id,
|
||||
COUNT(*) as cnt,
|
||||
et.name as old_error_name
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
WHERE error_type_id IS NOT NULL
|
||||
GROUP BY error_type_id
|
||||
ORDER BY error_type_id;
|
||||
|
||||
|
||||
-- STEP 2: 매핑 업데이트 (실제 item_id 확인 후 수정 필요!)
|
||||
-- ============================================
|
||||
|
||||
-- 먼저 위 쿼리로 실제 item_id 값을 확인하세요!
|
||||
-- 아래는 예시입니다. 실제 값으로 수정해서 사용하세요.
|
||||
|
||||
-- 예시: 설계미스(error_type_id=1) → 설계미스 카테고리의 '도면 치수 오류' 항목
|
||||
-- UPDATE daily_work_reports SET error_type_id = 6 WHERE error_type_id = 1;
|
||||
|
||||
-- 예시: 외주작업 불량(error_type_id=2) → 입고불량 카테고리의 '외관 불량' 항목
|
||||
-- UPDATE daily_work_reports SET error_type_id = 10 WHERE error_type_id = 2;
|
||||
|
||||
-- 예시: 입고지연(error_type_id=3) → 자재누락 카테고리의 '배관 자재 미입고' 항목
|
||||
-- UPDATE daily_work_reports SET error_type_id = 1 WHERE error_type_id = 3;
|
||||
|
||||
|
||||
-- STEP 3: 매핑 검증
|
||||
-- ============================================
|
||||
|
||||
-- 업데이트 후 확인
|
||||
SELECT
|
||||
dwr.error_type_id,
|
||||
iri.item_name,
|
||||
irc.category_name,
|
||||
COUNT(*) as cnt
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
|
||||
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
WHERE dwr.error_type_id IS NOT NULL
|
||||
GROUP BY dwr.error_type_id, iri.item_name, irc.category_name;
|
||||
|
||||
|
||||
-- STEP 4: error_types 테이블 삭제 (매핑 완료 후)
|
||||
-- ============================================
|
||||
|
||||
-- 주의: 반드시 STEP 2, 3 완료 후 실행!
|
||||
-- DROP TABLE IF EXISTS error_types;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 설비 테이블에 구입처 및 구입가격 컬럼 추가
|
||||
-- 실행: mysql -u [user] -p [database] < add_equipment_purchase_fields.sql
|
||||
|
||||
-- 컬럼 추가
|
||||
ALTER TABLE equipments
|
||||
ADD COLUMN supplier VARCHAR(100) NULL COMMENT '구입처' AFTER manufacturer,
|
||||
ADD COLUMN purchase_price DECIMAL(15, 0) NULL COMMENT '구입가격' AFTER supplier;
|
||||
|
||||
-- 확인
|
||||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'equipments'
|
||||
AND COLUMN_NAME IN ('supplier', 'purchase_price');
|
||||
@@ -0,0 +1,77 @@
|
||||
-- 설비 데이터 입력
|
||||
-- 실행 전 먼저 add_equipment_purchase_fields.sql 실행 필요
|
||||
-- 실행: mysql -u [user] -p [database] < insert_equipment_data.sql
|
||||
|
||||
-- 기존 데이터 삭제 (필요시 주석 해제)
|
||||
-- TRUNCATE TABLE equipments;
|
||||
|
||||
INSERT INTO equipments (equipment_code, equipment_name, model_name, specifications, serial_number, installation_date, supplier, purchase_price, manufacturer, status) VALUES
|
||||
('TKP-001', 'AIR COMPRESSOR', 'AR10E', '7.5KW(10HP)', 'K603023Y', '2016-06-01', '지티씨', NULL, '경원', 'active'),
|
||||
('TKP-002', 'TURN TABLE', 'YCT-200T', '220V', NULL, '2016-05-30', '형진종합공구', 3600000, '유체기계', 'active'),
|
||||
('TKP-003', 'BAND SAW(中)', 'CY300W', '1500W*380V', '20150943', '2016-05-30', '형진종합공구', 4800000, '유림싸이겐', 'active'),
|
||||
('TKP-004', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2016-05-30', '형진종합공구', 2700000, '렉스', 'active'),
|
||||
('TKP-005', 'BAND SAW(小)', 'XB-180WA', '180(VICE)', NULL, '2019-05-30', NULL, NULL, '렉스', 'active'),
|
||||
('TKP-006', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-001', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-007', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-002', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-008', 'TIG용접기', 'DAESUNG-500DT', '500A', 'TEAG0168-003', '2016-05-30', '형진종합공구', 2200000, '대성용접기', 'active'),
|
||||
('TKP-009', 'CO2용접기', 'COD-500A', '500A', '10880', '2016-05-30', '형진종합공구', 2000000, '대성용접기', 'active'),
|
||||
('TKP-010', 'O2용접기', 'GSORK', '220V', NULL, '2016-05-30', '형진종합공구', 620000, '재현오토닉스', 'active'),
|
||||
('TKP-011', 'PIPE BEVELLING MACHINE', 'S-200LT_MT(테이블포함)', '2" ~ 8"', 'KR-17030007', '2017-03-29', 'DCS ENG', 12000000, 'DCS ENG', 'active'),
|
||||
('TKP-012', 'CO2용접기', '500MX', '220/380V,500A', NULL, '2017-08-02', '현대용접기', 1800000, '현대용접기', 'active'),
|
||||
('TKP-013', '프라즈마', 'Perfect-150AP', '220/380/440V,140A', NULL, '2017-08-02', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-014', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-015', '터닝로라', 'JK-5-TR', '5TON/380V', NULL, '2017-09-08', '정일기공', 5700000, '정일기공', 'active'),
|
||||
('TKP-016', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-005', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-017', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-006', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-018', 'TIG용접기', 'Perfect-500PT', '500A/350A', 'TJAD017B-007', '2017-10-18', '현대용접기', 1600000, '퍼펙트대대', 'active'),
|
||||
('TKP-019', '전해연마기', 'ONB-8000VP', '220V/MAX1200W', '8022701', '2018-03-13', '오토기전', 1450000, '메탈브라이트(오토기전)', 'active'),
|
||||
('TKP-020', '지게차', '50DA-9F', '5000KGS', 'HHKHFV36JJ0000061', '2018-05-10', '현대지게차', 45000000, '현대지게차', 'active'),
|
||||
('TKP-021', '조방', NULL, '3658*12190', NULL, '2018-05-11', '천우기계공업/삼덕금속', 14200000, '테크니컬코리아', 'active'),
|
||||
('TKP-022', 'BAND SAW(大)', 'WBS-RC500AN', '3,300kgs / 7.88kw', 'BC50A18-005F001', '2018-05-31', '원공사', 36000000, '원공사', 'active'),
|
||||
('TKP-023', 'AIR COMPRESSOR', 'AR20E', '0.95Mpa', 'AR020FE358', '2018-06-05', '경원기계', NULL, '경원기계', 'active'),
|
||||
('TKP-024', 'TURN TABLE', 'YCT-200TA', '220V', NULL, '2018-06-12', '청운종합공구', 3245000, '유체기계', 'active'),
|
||||
('TKP-025', 'TIG용접기', 'Perfect-500WT', '500A/AC DC', 'ADKAC017B-006', '2018-06-12', '현대용접기', 2400000, '퍼펙트대대', 'active'),
|
||||
('TKP-026', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAI018B-002', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-027', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-009', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-028', 'TIG용접기', 'Perfect-500PT', '500A/DC', 'TAAA018B-001', '2018-06-12', '현대용접기', 1900000, '퍼펙트대대', 'active'),
|
||||
('TKP-029', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1806077', '2018-07-06', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-030', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807028', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-031', 'ELECTRIC CHAIN HOIST', 'DSM-2S', '3Ph-60Hz-380V', 'K1807029', '2018-07-10', '청운종합공구', 2300000, '대산', 'active'),
|
||||
('TKP-032', '만능탭 드릴링머신', 'SF-TDM32', '1.5KW', NULL, '2018-11-09', '㈜애스앤애프', 2927000, '㈜애스앤에프', 'active'),
|
||||
('TKP-033', '지게차', '30D-9B', '2850KGS', 'HHKHHN51KK0000864', '2019-03-06', '현대지게차', 29400000, '현대지게차', 'active'),
|
||||
('TKP-034', '갠츄리크레인', 'CRANE - DHG', '50/10Ton x SP20M x T/L50M x H15M', NULL, '2019-05-09', '유진산업기계', 249000000, '반도호이스트', 'active'),
|
||||
('TKP-035', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-036', 'OVER HEAD CRANE', 'CRANE - DHO', '20Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 58000000, '반도호이스트', 'active'),
|
||||
('TKP-037', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-038', 'OVER HEAD CRANE', 'CRANE - DHO', '5Ton x SP24.0M x T/L67M x H11M', NULL, '2019-05-09', '유진산업기계', 29000000, '반도호이스트', 'active'),
|
||||
('TKP-039', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-040', '고소작업대', NULL, '250 Kg', NULL, '2019-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-041', 'AIR CONDITIONER', '코끼리 냉장고', NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-042', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-043', 'AIR CONDITIONER', NULL, NULL, NULL, '2019-01-01', NULL, NULL, '㈜에스엔에프', 'active'),
|
||||
('TKP-044', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-045', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-046', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-047', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-048', '용접흄집진기', NULL, '5 HP / 60 m3/Min', NULL, '2019-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-049', '자동용접기', NULL, NULL, NULL, '2019-01-01', 'Swage-Lok', 50000000, 'Swage-Lok', 'active'),
|
||||
('TKP-050', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'NITTO', 'active'),
|
||||
('TKP-051', 'Magnetic Drill', NULL, NULL, NULL, '2020-01-01', '청운종합공구', NULL, 'Key-Yang', 'active'),
|
||||
('TKP-052', 'Tube Bending M/C', NULL, NULL, NULL, '2020-01-01', NULL, NULL, 'REMS', 'active'),
|
||||
('TKP-053', 'Unit Test Panel', NULL, NULL, NULL, '2021-01-01', NULL, NULL, NULL, 'active'),
|
||||
('TKP-054', '고소작업대', NULL, '500 Kg', NULL, '2021-01-01', NULL, NULL, '㈜쓰리제이테크', 'active'),
|
||||
('TKP-055', '용접봉 건조기', '주문제작', '박스형', NULL, '2022-05-04', '진원하이텍', 2300000, '진원하이텍', 'active'),
|
||||
('TKP-056', 'C&T 가공기', 'MS-CTK469', NULL, NULL, '2022-07-20', 'Swage-Lok', 7347600, 'Swage-Lok', 'active'),
|
||||
('TKP-057', '테이블형 튜브 벤딩기', 'MS-BTT-K', '1/2", 1/4"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-058', '자동용접기 헤드', 'SWS-10H-D-15', '1/2"', NULL, '2022-06-03', 'Swage-Lok', 20000000, 'Swage-Lok', 'active'),
|
||||
('TKP-059', '천장주행크레인', 'HC-75D-13105', '7.5ton', NULL, '2023-06-09', '에이치앤씨', 22800000, '에이치앤씨', 'active'),
|
||||
('TKP-060', 'AED', 'CU-SP1 Plus', '저출력심장충격기', NULL, '2023-11-09', '제이메디', 1600000, '제이메디', 'active'),
|
||||
('TKP-061', '베벨머신', 'S-150', 'O.D 20mm ~ 170mm', NULL, '2023-12-12', 'DCSENG', 16000000, 'DCSENG', 'active'),
|
||||
('TKP-062', '피막제거기', 'CM4_OD_GC', '최대 폭 48mm, 최대 깊이 15mm, 6"이상', NULL, '2023-12-12', 'DCSENG', 2000000, 'DCSENG', 'active'),
|
||||
('TKP-063', '피막제거기', 'S-CM4_OD', '최대 폭 48mm, 최대 깊이 15mm, 1" 이상', NULL, '2023-12-12', 'DCSENG', 1200000, 'DCSENG', 'active'),
|
||||
('TKP-064', '텅스텐 가공기', 'S-TGR', '0.89kg, 0.25~3.2', NULL, '2023-12-12', 'DCSENG', 800000, 'DCSENG', 'active'),
|
||||
('TKP-065', '전동대차', 'LPM15', '2.0 ton', NULL, '2023-12-20', '두산산업차량', 2800000, '두산산업차량', 'active');
|
||||
|
||||
-- 입력 확인
|
||||
SELECT COUNT(*) AS total_count FROM equipments;
|
||||
SELECT equipment_code, equipment_name, supplier, purchase_price, manufacturer FROM equipments ORDER BY equipment_code LIMIT 10;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- =====================================================
|
||||
-- daily_attendance_records 테이블 운영 DB 동기화
|
||||
-- 실행 전 백업 권장
|
||||
-- =====================================================
|
||||
|
||||
-- 1. is_present 컬럼 추가 (출근 체크용)
|
||||
ALTER TABLE `daily_attendance_records`
|
||||
ADD COLUMN IF NOT EXISTS `is_present` TINYINT(1) DEFAULT 1 COMMENT '출근 여부' AFTER `is_overtime_approved`;
|
||||
|
||||
-- 기존 데이터는 모두 출근으로 설정
|
||||
UPDATE `daily_attendance_records` SET `is_present` = 1 WHERE `is_present` IS NULL;
|
||||
|
||||
-- 2. created_by 컬럼 추가 (등록자)
|
||||
ALTER TABLE `daily_attendance_records`
|
||||
ADD COLUMN IF NOT EXISTS `created_by` INT NULL COMMENT '등록자 user_id' AFTER `is_present`;
|
||||
|
||||
-- 기존 데이터는 시스템(1)으로 설정
|
||||
UPDATE `daily_attendance_records` SET `created_by` = 1 WHERE `created_by` IS NULL;
|
||||
|
||||
-- 3. check_in_time, check_out_time 컬럼 추가 (선택사항)
|
||||
ALTER TABLE `daily_attendance_records`
|
||||
ADD COLUMN IF NOT EXISTS `check_in_time` TIME NULL COMMENT '출근 시간' AFTER `vacation_type_id`;
|
||||
|
||||
ALTER TABLE `daily_attendance_records`
|
||||
ADD COLUMN IF NOT EXISTS `check_out_time` TIME NULL COMMENT '퇴근 시간' AFTER `check_in_time`;
|
||||
|
||||
-- 4. notes 컬럼 추가
|
||||
ALTER TABLE `daily_attendance_records`
|
||||
ADD COLUMN IF NOT EXISTS `notes` TEXT NULL COMMENT '비고' AFTER `is_overtime_approved`;
|
||||
|
||||
-- =====================================================
|
||||
-- 확인용 쿼리
|
||||
-- =====================================================
|
||||
-- DESCRIBE `daily_attendance_records`;
|
||||
Reference in New Issue
Block a user