feat(tkfb): 공정표 + 생산회의록 시스템 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 공정표 + 생산회의록 시스템 마이그레이션
|
||||
*
|
||||
* 테이블 8개: schedule_phases, schedule_task_templates, schedule_entries,
|
||||
* schedule_entry_dependencies, schedule_milestones, meeting_minutes,
|
||||
* meeting_attendees, meeting_agenda_items
|
||||
*/
|
||||
|
||||
exports.up = async (knex) => {
|
||||
// 1. 공정 단계
|
||||
await knex.schema.createTable('schedule_phases', (table) => {
|
||||
table.increments('phase_id').primary();
|
||||
table.string('phase_name', 100).notNullable();
|
||||
table.integer('display_order').defaultTo(0);
|
||||
table.string('color', 20).defaultTo('#3B82F6');
|
||||
table.boolean('is_active').defaultTo(true);
|
||||
});
|
||||
|
||||
// 2. 작업 템플릿
|
||||
await knex.schema.createTable('schedule_task_templates', (table) => {
|
||||
table.increments('template_id').primary();
|
||||
table.integer('phase_id').unsigned().notNullable()
|
||||
.references('phase_id').inTable('schedule_phases').onDelete('CASCADE');
|
||||
table.string('task_name', 200).notNullable();
|
||||
table.integer('default_duration_days').defaultTo(7);
|
||||
table.integer('display_order').defaultTo(0);
|
||||
table.boolean('is_active').defaultTo(true);
|
||||
});
|
||||
|
||||
// 3. 공정표 항목
|
||||
await knex.schema.createTable('schedule_entries', (table) => {
|
||||
table.increments('entry_id').primary();
|
||||
table.integer('project_id').unsigned().notNullable()
|
||||
.references('project_id').inTable('projects');
|
||||
table.integer('phase_id').unsigned().notNullable()
|
||||
.references('phase_id').inTable('schedule_phases');
|
||||
table.string('task_name', 200).notNullable();
|
||||
table.date('start_date').notNullable();
|
||||
table.date('end_date').notNullable();
|
||||
table.integer('progress').defaultTo(0);
|
||||
table.string('status', 20).defaultTo('planned');
|
||||
table.string('assignee', 100).nullable();
|
||||
table.text('notes').nullable();
|
||||
table.integer('display_order').defaultTo(0);
|
||||
table.integer('created_by').unsigned().nullable()
|
||||
.references('user_id').inTable('sso_users');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// 4. 작업 의존관계 (다대다)
|
||||
await knex.schema.createTable('schedule_entry_dependencies', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.integer('entry_id').unsigned().notNullable()
|
||||
.references('entry_id').inTable('schedule_entries').onDelete('CASCADE');
|
||||
table.integer('depends_on_entry_id').unsigned().notNullable()
|
||||
.references('entry_id').inTable('schedule_entries').onDelete('CASCADE');
|
||||
table.unique(['entry_id', 'depends_on_entry_id']);
|
||||
});
|
||||
|
||||
// 5. 마일스톤
|
||||
await knex.schema.createTable('schedule_milestones', (table) => {
|
||||
table.increments('milestone_id').primary();
|
||||
table.integer('project_id').unsigned().notNullable()
|
||||
.references('project_id').inTable('projects');
|
||||
table.integer('entry_id').unsigned().nullable()
|
||||
.references('entry_id').inTable('schedule_entries').onDelete('SET NULL');
|
||||
table.string('milestone_name', 200).notNullable();
|
||||
table.date('milestone_date').notNullable();
|
||||
table.string('milestone_type', 30).defaultTo('deadline');
|
||||
table.string('status', 20).defaultTo('upcoming');
|
||||
table.text('notes').nullable();
|
||||
table.integer('created_by').unsigned().nullable()
|
||||
.references('user_id').inTable('sso_users');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// 6. 생산회의록
|
||||
await knex.schema.createTable('meeting_minutes', (table) => {
|
||||
table.increments('meeting_id').primary();
|
||||
table.date('meeting_date').notNullable();
|
||||
table.string('meeting_time', 10).nullable();
|
||||
table.string('title', 300).notNullable();
|
||||
table.string('location', 200).nullable();
|
||||
table.text('summary').nullable();
|
||||
table.string('status', 20).defaultTo('draft');
|
||||
table.integer('created_by').unsigned().nullable()
|
||||
.references('user_id').inTable('sso_users');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// 7. 회의 참석자 (정규화)
|
||||
await knex.schema.createTable('meeting_attendees', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.integer('meeting_id').unsigned().notNullable()
|
||||
.references('meeting_id').inTable('meeting_minutes').onDelete('CASCADE');
|
||||
table.integer('user_id').unsigned().notNullable()
|
||||
.references('user_id').inTable('sso_users');
|
||||
table.unique(['meeting_id', 'user_id']);
|
||||
});
|
||||
|
||||
// 8. 회의 안건
|
||||
await knex.schema.createTable('meeting_agenda_items', (table) => {
|
||||
table.increments('item_id').primary();
|
||||
table.integer('meeting_id').unsigned().notNullable()
|
||||
.references('meeting_id').inTable('meeting_minutes').onDelete('CASCADE');
|
||||
table.integer('project_id').unsigned().nullable()
|
||||
.references('project_id').inTable('projects');
|
||||
table.integer('milestone_id').unsigned().nullable()
|
||||
.references('milestone_id').inTable('schedule_milestones').onDelete('SET NULL');
|
||||
table.string('item_type', 30).defaultTo('other');
|
||||
table.text('content').notNullable();
|
||||
table.text('decision').nullable();
|
||||
table.text('action_required').nullable();
|
||||
table.integer('responsible_user_id').unsigned().nullable()
|
||||
.references('user_id').inTable('sso_users');
|
||||
table.date('due_date').nullable();
|
||||
table.string('status', 20).defaultTo('open');
|
||||
table.integer('display_order').defaultTo(0);
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// Seed: 공정 단계
|
||||
await knex('schedule_phases').insert([
|
||||
{ phase_name: 'Outsourcing', display_order: 1, color: '#3B82F6' },
|
||||
{ phase_name: 'BASE', display_order: 2, color: '#10B981' },
|
||||
{ phase_name: 'SHOP', display_order: 3, color: '#F59E0B' },
|
||||
{ phase_name: 'PV/Heat Exchanger', display_order: 4, color: '#8B5CF6' },
|
||||
]);
|
||||
|
||||
// Seed: 작업 템플릿
|
||||
const phases = await knex('schedule_phases').select('phase_id', 'phase_name');
|
||||
const phaseMap = {};
|
||||
phases.forEach(p => { phaseMap[p.phase_name] = p.phase_id; });
|
||||
|
||||
const templates = [
|
||||
// Outsourcing
|
||||
{ phase_id: phaseMap['Outsourcing'], task_name: '용기입고', default_duration_days: 14, display_order: 1 },
|
||||
{ phase_id: phaseMap['Outsourcing'], task_name: 'PTK', default_duration_days: 10, display_order: 2 },
|
||||
{ phase_id: phaseMap['Outsourcing'], task_name: 'Tray and Instrument Wiring', default_duration_days: 7, display_order: 3 },
|
||||
{ phase_id: phaseMap['Outsourcing'], task_name: 'Painting', default_duration_days: 5, display_order: 4 },
|
||||
{ phase_id: phaseMap['Outsourcing'], task_name: 'Instrument Wiring', default_duration_days: 7, display_order: 5 },
|
||||
{ phase_id: phaseMap['Outsourcing'], task_name: 'Packing', default_duration_days: 3, display_order: 6 },
|
||||
// BASE
|
||||
{ phase_id: phaseMap['BASE'], task_name: 'Base Fabrication', default_duration_days: 14, display_order: 1 },
|
||||
{ phase_id: phaseMap['BASE'], task_name: 'Base 제작', default_duration_days: 10, display_order: 2 },
|
||||
{ phase_id: phaseMap['BASE'], task_name: '용기설치', default_duration_days: 5, display_order: 3 },
|
||||
// SHOP
|
||||
{ phase_id: phaseMap['SHOP'], task_name: '배관자재입고', default_duration_days: 7, display_order: 1 },
|
||||
{ phase_id: phaseMap['SHOP'], task_name: 'Pre-Fabrication for Piping', default_duration_days: 10, display_order: 2 },
|
||||
{ phase_id: phaseMap['SHOP'], task_name: '1st Piping Assembly', default_duration_days: 14, display_order: 3 },
|
||||
{ phase_id: phaseMap['SHOP'], task_name: 'Hydro. Test', default_duration_days: 3, display_order: 4 },
|
||||
{ phase_id: phaseMap['SHOP'], task_name: 'Re-Assembly', default_duration_days: 7, display_order: 5 },
|
||||
{ phase_id: phaseMap['SHOP'], task_name: 'Tubing', default_duration_days: 5, display_order: 6 },
|
||||
{ phase_id: phaseMap['SHOP'], task_name: 'FAT', default_duration_days: 2, display_order: 7 },
|
||||
];
|
||||
await knex('schedule_task_templates').insert(templates);
|
||||
|
||||
// 페이지 접근 권한 등록
|
||||
await knex.raw(`
|
||||
INSERT IGNORE INTO pages (page_key, page_name, page_path, description, is_active) VALUES
|
||||
('work.schedule', '공정표', '/pages/work/schedule.html', '프로젝트 공정표 Gantt 뷰', 1),
|
||||
('work.meetings', '생산회의록', '/pages/work/meetings.html', '생산회의록 관리', 1),
|
||||
('work.meeting_detail', '회의록 상세', '/pages/work/meeting-detail.html', '회의록 상세/작성', 1)
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = async (knex) => {
|
||||
await knex.schema.dropTableIfExists('meeting_agenda_items');
|
||||
await knex.schema.dropTableIfExists('meeting_attendees');
|
||||
await knex.schema.dropTableIfExists('meeting_minutes');
|
||||
await knex.schema.dropTableIfExists('schedule_milestones');
|
||||
await knex.schema.dropTableIfExists('schedule_entry_dependencies');
|
||||
await knex.schema.dropTableIfExists('schedule_entries');
|
||||
await knex.schema.dropTableIfExists('schedule_task_templates');
|
||||
await knex.schema.dropTableIfExists('schedule_phases');
|
||||
|
||||
await knex.raw(`DELETE FROM pages WHERE page_key IN ('work.schedule', 'work.meetings', 'work.meeting_detail')`);
|
||||
};
|
||||
Reference in New Issue
Block a user