feat(tkfb): 공정표 + 생산회의록 시스템 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-17 08:05:18 +09:00
parent b5dc9c2f20
commit d7cc568c01
15 changed files with 2916 additions and 0 deletions

View File

@@ -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')`);
};