Skip to main content
Glama
TaskManager.js18.7 kB
/** * Task Manager - 작업 관리 클래스 (완전 구현) * 작업의 생성, 조회, 수정, 삭제 및 비즈니스 로직 처리 */ import { v4 as uuidv4 } from 'uuid'; import { SQLiteTaskStorage } from './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; } } /** * 우선순위 값을 표준 형식으로 정규화 * @param {string} priority - 입력 우선순위 ('high', 'High', 'HIGH' 등) * @returns {string} 정규화된 우선순위 ('High', 'Medium', 'Low') */ normalizePriority(priority) { if (!priority) return null; const normalized = priority.toLowerCase(); switch (normalized) { case 'high': return TaskPriority.HIGH; case 'medium': return TaskPriority.MEDIUM; case 'low': return TaskPriority.LOW; default: return null; } } /** * 새로운 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: this.normalizePriority(taskData.priority) || TaskPriority.MEDIUM, assignee: taskData.assignee || null, estimatedHours: taskData.estimatedHours || taskData.estimated_hours || 0, actualHours: taskData.actualHours || taskData.actual_hours || 0, dueDate: taskData.dueDate || taskData.due_date || null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), planId: taskData.planId || taskData.design_id || null, prd_id: taskData.prd_id || null, // PRD 연결 정보 추가 version: 1, createdBy: taskData.createdBy || taskData.created_by || 'system', tags: taskData.tags || [], notes: taskData.notes || '', // 추가 메타데이터 details: taskData.details || '', acceptanceCriteria: taskData.acceptanceCriteria || taskData.acceptance_criteria || [], testStrategy: taskData.testStrategy || taskData.test_strategy || '', 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); // PRD 연결 정보가 있으면 prd_task_links 테이블에 저장 if (task.prd_id) { await this.createPrdTaskLink(task.prd_id, task.id, 'direct', task.createdBy); } 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, sortBy = 'updated_desc') { 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); } // 정렬 적용 switch (sortBy) { case 'updated_asc': filteredTasks.sort((a, b) => new Date(a.updatedAt || a.updated_at) - new Date(b.updatedAt || b.updated_at)); break; case 'created_desc': filteredTasks.sort((a, b) => new Date(b.createdAt || b.created_at) - new Date(a.createdAt || a.created_at)); break; case 'created_asc': filteredTasks.sort((a, b) => new Date(a.createdAt || a.created_at) - new Date(b.createdAt || b.created_at)); break; case 'title_asc': filteredTasks.sort((a, b) => a.title.localeCompare(b.title)); break; case 'title_desc': filteredTasks.sort((a, b) => b.title.localeCompare(a.title)); break; case 'updated_desc': default: filteredTasks.sort((a, b) => new Date(b.updatedAt || b.updated_at) - new Date(a.updatedAt || a.updated_at)); break; } // 요약 정보와 함께 반환 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), sortBy, message: `Task ${tasksWithSummary.length}개 조회 완료 (정렬: ${sortBy})` }; } 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); // PRD 연결 정보가 변경되었으면 prd_task_links 테이블 업데이트 if (updates.prd_id !== undefined && updates.prd_id !== existingTask.prd_id) { // 기존 연결 삭제 if (existingTask.prd_id) { await this.deletePrdTaskLink(existingTask.prd_id, taskId); } // 새 연결 생성 if (updates.prd_id) { await this.createPrdTaskLink(updates.prd_id, taskId, 'direct', 'dashboard'); } } 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; } /** * PRD-Task 연결 생성 * @param {string} prdId - PRD ID * @param {string} taskId - Task ID * @param {string} linkType - 연결 타입 ('direct', 'derived', 'related') * @param {string} createdBy - 생성자 */ async createPrdTaskLink(prdId, taskId, linkType = 'direct', createdBy = 'system') { try { await this.storage.createPrdTaskLink(prdId, taskId, linkType, createdBy); } catch (error) { console.error('PRD-Task 연결 생성 오류:', error); // 연결 생성 실패는 메인 작업 실패로 이어지지 않도록 로그만 남김 } } /** * PRD-Task 연결 삭제 * @param {string} prdId - PRD ID * @param {string} taskId - Task ID */ async deletePrdTaskLink(prdId, taskId) { try { await this.storage.deletePrdTaskLink(prdId, taskId); } catch (error) { console.error('PRD-Task 연결 삭제 오류:', error); // 연결 삭제 실패는 메인 작업 실패로 이어지지 않도록 로그만 남김 } } /** * Task의 PRD 연결 정보 조회 * @param {string} taskId - Task ID * @returns {Array} PRD 연결 목록 */ async getTaskPrdLinks(taskId) { try { return await this.storage.getTaskPrdLinks(taskId); } catch (error) { console.error('Task PRD 연결 정보 조회 오류:', error); return []; } } /** * 특정 PRD에 연결된 모든 작업 조회 (직접 + 간접 연결) * @param {string} prdId - PRD ID * @returns {Object} 연결된 작업 목록 */ async getTasksByPRD(prdId) { await this.ensureInitialized(); try { // 1. 직접 연결된 작업 조회 (prd_task_links 테이블) const directTasks = await this.storage.getTasksByPRDDirect(prdId); // 2. 간접 연결된 작업 조회 (PRD -> 설계 -> 작업) const indirectTasks = await this.storage.getTasksByPRDIndirect(prdId); // 3. 연결 타입 정보 추가 const allTasks = [ ...directTasks.map(task => ({ ...task, linkType: 'direct' })), ...indirectTasks.map(task => ({ ...task, linkType: 'indirect' })) ]; // 4. 중복 제거 (같은 작업이 직접/간접 모두에 연결된 경우) const uniqueTasks = []; const seenTaskIds = new Set(); for (const task of allTasks) { if (!seenTaskIds.has(task.id)) { uniqueTasks.push(task); seenTaskIds.add(task.id); } } // 5. 요약 정보와 함께 반환 const tasksWithSummary = uniqueTasks.map(task => ({ ...task, summary: { isOverdue: this.isTaskOverdue(task), daysRemaining: this.calculateDaysRemaining(task), completionPercentage: this.calculateCompletionPercentage(task) } })); return { success: true, tasks: tasksWithSummary, total: tasksWithSummary.length, statusBreakdown: this.getStatusBreakdown(tasksWithSummary), message: `PRD "${prdId}"에 연결된 작업 ${tasksWithSummary.length}개 조회 완료` }; } catch (error) { throw new Error(`PRD별 작업 목록 조회 실패: ${error.message}`); } } /** * 작업의 추가 연결들을 업데이트 * @param {string} taskId - 작업 ID * @param {Array} additionalConnections - 추가 연결 배열 * @returns {Object} 성공 여부 */ async updateAdditionalConnections(taskId, additionalConnections) { await this.ensureInitialized(); try { // 기존 추가 연결들 삭제 (직접 컬럼 제외) await this.storage.deleteAdditionalConnections(taskId); // 새 연결들 추가 for (const connection of additionalConnections) { if (connection.entity_type && connection.entity_id) { await this.storage.addAdditionalConnection(taskId, connection); } } return { success: true, message: '추가 연결 업데이트 완료' }; } catch (error) { throw new Error(`추가 연결 업데이트 실패: ${error.message}`); } } /** * 작업의 추가 연결들을 조회 * @param {string} taskId - 작업 ID * @returns {Array} 추가 연결 배열 */ async getAdditionalConnections(taskId) { await this.ensureInitialized(); try { return await this.storage.getAdditionalConnections(taskId); } catch (error) { throw new Error(`추가 연결 조회 실패: ${error.message}`); } } }

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