Skip to main content
Glama
TaskManager.js11.6 kB
/** * Task Manager - 작업 관리 클래스 (완전 구현) * 작업의 생성, 조회, 수정, 삭제 및 비즈니스 로직 처리 */ import { v4 as uuidv4 } from 'uuid'; import { SQLiteTaskStorage } from '../database/SQLiteTaskStorage.js'; // Task 상태 enum export const TaskStatus = { PENDING: 'pending', IN_PROGRESS: 'in_progress', DONE: 'done', BLOCKED: 'blocked' }; // 우선순위 enum export const TaskPriority = { HIGH: 'High', MEDIUM: 'Medium', LOW: 'Low' }; export class TaskManager { constructor() { this.storage = new SQLiteTaskStorage(); this.initialized = false; } async ensureInitialized() { if (!this.initialized) { await this.storage.initialize(); this.initialized = true; } } /** * 새로운 Task 생성 * @param {Object} taskData - Task 기본 데이터 * @returns {Object} 생성된 Task 객체 */ async createTask(taskData) { await this.ensureInitialized(); try { // 기본 Task 구조 생성 const task = { id: uuidv4(), title: taskData.title, description: taskData.description || '', status: taskData.status || TaskStatus.PENDING, priority: taskData.priority || TaskPriority.MEDIUM, assignee: taskData.assignee || null, estimatedHours: taskData.estimatedHours || 0, actualHours: taskData.actualHours || 0, dueDate: taskData.dueDate || null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), planId: taskData.planId || null, version: 1, createdBy: taskData.createdBy || 'system', tags: taskData.tags || [], notes: taskData.notes || '', // 추가 메타데이터 details: taskData.details || '', acceptanceCriteria: taskData.acceptanceCriteria || [], testStrategy: taskData.testStrategy || '', dependencies: [] }; // 필수 필드 검증 if (!task.title || task.title.trim().length === 0) { throw new Error('제목은 필수입니다'); } // 우선순위 검증 if (!Object.values(TaskPriority).includes(task.priority)) { task.priority = TaskPriority.MEDIUM; } // 상태 검증 if (!Object.values(TaskStatus).includes(task.status)) { task.status = TaskStatus.PENDING; } // Task 저장 await this.storage.saveTask(task); return { success: true, task: task, message: `Task "${task.title}" 생성 완료` }; } catch (error) { throw new Error(`Task 생성 실패: ${error.message}`); } } /** * Task 목록 조회 * @param {string} status - 필터링할 상태 (옵션) * @param {string} assignee - 담당자 필터 (옵션) * @returns {Array} Task 목록 */ async listTasks(status = null, assignee = null) { await this.ensureInitialized(); try { const allTasks = await this.storage.listAllTasks(); let filteredTasks = allTasks; if (status) { filteredTasks = filteredTasks.filter(task => task.status === status); } if (assignee) { filteredTasks = filteredTasks.filter(task => task.assignee === assignee); } // 요약 정보와 함께 반환 const tasksWithSummary = filteredTasks.map(task => ({ ...task, summary: { isOverdue: this.isTaskOverdue(task), daysRemaining: this.calculateDaysRemaining(task), completionPercentage: this.calculateCompletionPercentage(task), dependentTasksCount: task.dependencies ? task.dependencies.length : 0 } })); return { success: true, tasks: tasksWithSummary, total: tasksWithSummary.length, statusBreakdown: this.getStatusBreakdown(tasksWithSummary), message: `Task ${tasksWithSummary.length}개 조회 완료` }; } catch (error) { throw new Error(`Task 목록 조회 실패: ${error.message}`); } } /** * 특정 Task 상세 조회 * @param {string} taskId - Task ID * @returns {Object} Task 상세 정보 */ async getTask(taskId) { await this.ensureInitialized(); try { const task = await this.storage.getTask(taskId); if (!task) { throw new Error(`Task를 찾을 수 없습니다: ${taskId}`); } // 의존성 작업 정보 조회 const dependencyTasks = []; if (task.dependencies && task.dependencies.length > 0) { for (const depId of task.dependencies) { const depTask = await this.storage.getTask(depId); if (depTask) { dependencyTasks.push({ id: depTask.id, title: depTask.title, status: depTask.status }); } } } return { success: true, task: { ...task, dependencyTasks: dependencyTasks, analytics: { isOverdue: this.isTaskOverdue(task), daysRemaining: this.calculateDaysRemaining(task), completionPercentage: this.calculateCompletionPercentage(task), timeTracking: { estimated: task.estimatedHours, actual: task.actualHours, variance: task.actualHours - task.estimatedHours } } }, message: `Task "${task.title}" 조회 완료` }; } catch (error) { throw new Error(`Task 조회 실패: ${error.message}`); } } /** * Task 업데이트 * @param {string} taskId - Task ID * @param {Object} updates - 업데이트할 필드들 * @returns {Object} 업데이트된 Task */ async updateTask(taskId, updates) { await this.ensureInitialized(); try { const existingTask = await this.storage.getTask(taskId); if (!existingTask) { throw new Error(`Task를 찾을 수 없습니다: ${taskId}`); } // 업데이트된 Task 생성 const updatedTask = { ...existingTask, ...updates, id: taskId, // ID는 변경 불가 updatedAt: new Date().toISOString(), version: existingTask.version + 1 }; // 상태가 변경된 경우 추가 로직 if (updates.status && updates.status !== existingTask.status) { updatedTask.statusChangedAt = new Date().toISOString(); // 완료 상태로 변경 시 완료 시간 기록 if (updates.status === TaskStatus.DONE) { updatedTask.completedAt = new Date().toISOString(); } } // 검증 if (updates.title !== undefined && (!updates.title || updates.title.trim().length === 0)) { throw new Error('제목은 비어있을 수 없습니다'); } // 저장 await this.storage.saveTask(updatedTask); return { success: true, task: updatedTask, message: `Task "${updatedTask.title}" 업데이트 완료` }; } catch (error) { throw new Error(`Task 업데이트 실패: ${error.message}`); } } /** * Task 삭제 * @param {string} taskId - Task ID * @returns {Object} 삭제 결과 */ async deleteTask(taskId) { await this.ensureInitialized(); try { const existingTask = await this.storage.getTask(taskId); if (!existingTask) { throw new Error(`Task를 찾을 수 없습니다: ${taskId}`); } // 의존성 체크 - 다른 Task가 이 Task에 의존하는지 확인 const allTasks = await this.storage.listAllTasks(); const dependentTasks = allTasks.filter(task => task.dependencies && task.dependencies.includes(taskId) ); if (dependentTasks.length > 0) { const dependentTitles = dependentTasks.map(t => t.title).join(', '); throw new Error(`이 Task에 의존하는 다른 Task가 있어 삭제할 수 없습니다: ${dependentTitles}`); } // 삭제 수행 const deleted = await this.storage.deleteTask(taskId); if (!deleted) { throw new Error('Task 삭제 중 오류가 발생했습니다'); } return { success: true, deletedTask: existingTask.title, message: `Task "${existingTask.title}"이 성공적으로 삭제되었습니다` }; } catch (error) { throw new Error(`Task 삭제 실패: ${error.message}`); } } /** * Task 의존성 추가 * @param {string} taskId - 의존하는 Task ID * @param {string} prerequisiteTaskId - 선행 Task ID * @returns {Object} 결과 */ async addTaskDependency(taskId, prerequisiteTaskId) { await this.ensureInitialized(); try { const task = await this.storage.getTask(taskId); const prerequisite = await this.storage.getTask(prerequisiteTaskId); if (!task) throw new Error(`Task를 찾을 수 없습니다: ${taskId}`); if (!prerequisite) throw new Error(`선행 Task를 찾을 수 없습니다: ${prerequisiteTaskId}`); // 순환 의존성 체크 if (await this.wouldCreateCircularDependency(taskId, prerequisiteTaskId)) { throw new Error('순환 의존성이 발생합니다'); } // 의존성 추가 if (!task.dependencies) task.dependencies = []; if (!task.dependencies.includes(prerequisiteTaskId)) { task.dependencies.push(prerequisiteTaskId); await this.storage.saveTask(task); } return { success: true, message: `Task "${task.title}"에 선행 작업 "${prerequisite.title}" 의존성이 추가되었습니다` }; } catch (error) { throw new Error(`의존성 추가 실패: ${error.message}`); } } // 유틸리티 메서드들 /** * Task가 기한을 넘겼는지 확인 */ isTaskOverdue(task) { if (!task.dueDate || task.status === TaskStatus.DONE) return false; return new Date(task.dueDate) < new Date(); } /** * 남은 일수 계산 */ calculateDaysRemaining(task) { if (!task.dueDate || task.status === TaskStatus.DONE) return null; const today = new Date(); const dueDate = new Date(task.dueDate); const diffTime = dueDate - today; return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * 완성도 계산 (실제 시간 기준) */ calculateCompletionPercentage(task) { if (task.status === TaskStatus.DONE) return 100; if (task.status === TaskStatus.PENDING) return 0; if (task.estimatedHours === 0) return task.status === TaskStatus.IN_PROGRESS ? 50 : 0; const percentage = Math.min((task.actualHours / task.estimatedHours) * 100, 95); return Math.round(percentage); } /** * 상태별 분류 */ getStatusBreakdown(tasks) { const breakdown = {}; Object.values(TaskStatus).forEach(status => { breakdown[status] = tasks.filter(task => task.status === status).length; }); return breakdown; } /** * 순환 의존성 체크 */ async wouldCreateCircularDependency(taskId, prerequisiteTaskId) { // 간단한 구현: prerequisite의 의존성들을 확인 const prerequisite = await this.storage.getTask(prerequisiteTaskId); if (!prerequisite.dependencies) return false; // 직접적인 순환 체크 if (prerequisite.dependencies.includes(taskId)) return true; // 간접적인 순환 체크 (1단계만) for (const depId of prerequisite.dependencies) { const dep = await this.storage.getTask(depId); if (dep && dep.dependencies && dep.dependencies.includes(taskId)) return true; } return false; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/foswmine/workflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server