Skip to main content
Glama
database.js35.8 kB
import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; import path from 'path'; import { fileURLToPath } from 'url'; import { readFile } from 'fs/promises'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let db = null; // Helper function to read JSON files from WorkflowMCP file storage async function readFileStorageData(dataType, itemId) { try { const filePath = path.join(__dirname, '../../../../data', dataType, `${itemId}.json`); const fileContent = await readFile(filePath, { encoding: 'utf-8' }); return JSON.parse(fileContent); } catch (error) { // Silently ignore missing files - some items may not have JSON file storage return null; } } // Database path - reference the main workflow-mcp database using file location // dashboard/src/lib/server -> ../../../../data/workflow.db const DB_PATH = path.resolve(__dirname, '../../../../data/workflow.db'); export async function getDatabase() { if (!db) { db = await open({ filename: DB_PATH, driver: sqlite3.Database }); // Enable WAL mode for better concurrent access await db.exec('PRAGMA journal_mode = WAL'); await db.exec('PRAGMA foreign_keys = ON'); } return db; } // PRD operations export async function getAllPRDs() { const database = await getDatabase(); const prdRows = await database.all(` SELECT p.*, COUNT(DISTINCT t.id) as task_count, COUNT(CASE WHEN t.status = 'completed' THEN 1 END) as completed_tasks FROM prds p LEFT JOIN designs d ON p.id = d.requirement_id LEFT JOIN tasks t ON d.id = t.design_id GROUP BY p.id ORDER BY p.created_at DESC `); // Merge with file storage data for (const prd of prdRows) { const fileData = await readFileStorageData('prds', prd.id); if (fileData) { // Merge file data into database record prd.description = fileData.description || prd.description; prd.requirements = fileData.requirements || (prd.requirements ? JSON.parse(prd.requirements) : []); prd.acceptance_criteria = fileData.acceptance_criteria || (prd.acceptance_criteria ? JSON.parse(prd.acceptance_criteria) : []); prd.businessObjective = fileData.businessObjective; prd.targetUsers = fileData.targetUsers; prd.successCriteria = fileData.successCriteria; prd.epics = fileData.epics; prd.userStories = fileData.userStories; prd.technicalConstraints = fileData.technicalConstraints; prd.assumptions = fileData.assumptions; prd.risks = fileData.risks; prd.timeline = fileData.timeline; prd.qualityGates = fileData.qualityGates; prd.tags = fileData.tags; prd.attachments = fileData.attachments; } } return prdRows; } export async function getPRDById(id) { const database = await getDatabase(); const prdRow = await database.get('SELECT * FROM prds WHERE id = ?', id); if (!prdRow) { return null; } // MCP 스타일의 향상된 PRD 조회 (PRDManager 패턴 적용) // 기본 PRD 데이터 포맷팅 const prd = { id: prdRow.id, title: prdRow.title, description: prdRow.description, version: prdRow.version || '1.0.0', status: prdRow.status, priority: prdRow.priority, createdAt: prdRow.created_at, updatedAt: prdRow.updated_at, createdBy: prdRow.created_by, lastModifiedBy: prdRow.last_modified_by, businessObjective: prdRow.business_objective, targetUsers: safeJsonParse(prdRow.target_users, ['General users']), successCriteria: safeJsonParse(prdRow.success_criteria, ['Success criteria to be defined']), epics: safeJsonParse(prdRow.epics, []), requirements: safeJsonParse(prdRow.requirements, []), userStories: safeJsonParse(prdRow.user_stories, []), technicalConstraints: safeJsonParse(prdRow.technical_constraints, []), assumptions: safeJsonParse(prdRow.assumptions, []), risks: safeJsonParse(prdRow.risks, []), timeline: safeJsonParse(prdRow.timeline, { phases: [] }), qualityGates: safeJsonParse(prdRow.quality_gates, []), tags: safeJsonParse(prdRow.tags, []), attachments: safeJsonParse(prdRow.attachments, []) }; // 파일 스토리지 데이터와 병합 (기존 기능 유지) const fileData = await readFileStorageData('prds', id); if (fileData) { // 파일 데이터로 필드 오버라이드 prd.description = fileData.description || prd.description; prd.requirements = fileData.requirements || prd.requirements; prd.businessObjective = fileData.businessObjective || prd.businessObjective; prd.targetUsers = fileData.targetUsers || prd.targetUsers; prd.successCriteria = fileData.successCriteria || prd.successCriteria; prd.epics = fileData.epics || prd.epics; prd.userStories = fileData.userStories || prd.userStories; prd.technicalConstraints = fileData.technicalConstraints || prd.technicalConstraints; prd.assumptions = fileData.assumptions || prd.assumptions; prd.risks = fileData.risks || prd.risks; prd.timeline = fileData.timeline || prd.timeline; prd.qualityGates = fileData.qualityGates || prd.qualityGates; prd.tags = fileData.tags || prd.tags; prd.attachments = fileData.attachments || prd.attachments; } // MCP 스타일 분석 데이터 추가 (PRDManager 패턴) prd.analytics = { totalRequirements: prd.requirements.length, requirementsByType: groupRequirementsByType(prd.requirements), requirementsByPriority: groupRequirementsByPriority(prd.requirements), estimatedHours: calculateTotalHours(prd.requirements), completionRate: 0 // 기본값, 향후 Task 연동 시 계산 }; return prd; } // MCP PRDManager 유틸리티 함수들 추가 function safeJsonParse(jsonString, defaultValue = null) { try { return jsonString ? JSON.parse(jsonString) : defaultValue; } catch (error) { console.warn('JSON 파싱 실패:', jsonString); return defaultValue; } } function groupRequirementsByType(requirements) { const grouped = {}; requirements.forEach(req => { const type = req.type || 'undefined'; if (!grouped[type]) grouped[type] = 0; grouped[type]++; }); return grouped; } function groupRequirementsByPriority(requirements) { const grouped = {}; requirements.forEach(req => { const priority = req.priority || 'undefined'; if (!grouped[priority]) grouped[priority] = 0; grouped[priority]++; }); return grouped; } function calculateTotalHours(requirements) { return requirements.reduce((total, req) => total + (req.estimatedHours || 0), 0); } export async function createPRD(prdData) { const database = await getDatabase(); // MCP 스타일의 향상된 PRD 생성 (PRDManager 로직 적용) const id = crypto.randomUUID(); // 기본 PRD 구조 생성 (MCP PRDManager 패턴) const prd = { id: id, title: prdData.title, description: prdData.description, version: '1.0.0', status: prdData.status || 'draft', priority: prdData.priority || 'medium', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), createdBy: 'dashboard', lastModifiedBy: 'dashboard', businessObjective: prdData.businessObjective || 'Business objective to be defined', targetUsers: prdData.targetUsers || ['General users'], successCriteria: prdData.successCriteria || ['Success criteria to be defined'], epics: [], requirements: [], userStories: [], technicalConstraints: prdData.technicalConstraints || [], assumptions: prdData.assumptions || [], risks: [], timeline: { phases: [] }, qualityGates: [], tags: prdData.tags || [], attachments: [] }; // 요구사항 처리 (MCP PRDManager 패턴) if (prdData.requirements && Array.isArray(prdData.requirements)) { prd.requirements = prdData.requirements.map(req => { if (typeof req === 'string') { // 문자열 요구사항을 객체로 변환 (MCP 방식) return { id: crypto.randomUUID(), title: req.substring(0, 100), description: req, type: 'functional', priority: prd.priority, moscow: 'Must', acceptanceCriteria: [`${req} 기능이 정상적으로 동작해야 함`], dependencies: [], estimatedHours: 0, tags: [] }; } else { // 이미 구조화된 요구사항 return { id: req.id || crypto.randomUUID(), title: req.title, description: req.description, type: req.type || 'functional', priority: req.priority || prd.priority, moscow: req.moscow || 'Must', acceptanceCriteria: req.acceptanceCriteria || [], dependencies: req.dependencies || [], estimatedHours: req.estimatedHours || 0, tags: req.tags || [] }; } }); } // 간단한 검증 (MCP 패턴) if (!prd.title || prd.title.trim().length === 0) { throw new Error('제목은 필수입니다'); } // 확장된 필드를 포함하여 데이터베이스에 저장 const result = await database.run( 'INSERT INTO prds (id, title, description, requirements, priority, status, version, created_at, updated_at, created_by, last_modified_by, business_objective, target_users, success_criteria, epics, user_stories, technical_constraints, assumptions, risks, timeline, quality_gates, tags, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, datetime("now"), datetime("now"), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ prd.id, prd.title, prd.description, JSON.stringify(prd.requirements), prd.priority, prd.status, prd.version, prd.createdBy, prd.lastModifiedBy, prd.businessObjective, JSON.stringify(prd.targetUsers), JSON.stringify(prd.successCriteria), JSON.stringify(prd.epics), JSON.stringify(prd.userStories), JSON.stringify(prd.technicalConstraints), JSON.stringify(prd.assumptions), JSON.stringify(prd.risks), JSON.stringify(prd.timeline), JSON.stringify(prd.qualityGates), JSON.stringify(prd.tags), JSON.stringify(prd.attachments) ] ); return id; } export async function updatePRD(id, prd) { const database = await getDatabase(); // MCP 스타일의 향상된 업데이트 (버전 관리 포함) const existingPRD = await database.get('SELECT * FROM prds WHERE id = ?', id); if (!existingPRD) { throw new Error('PRD를 찾을 수 없습니다'); } // 버전 관리 let newVersion = existingPRD.version; if (prd.title !== existingPRD.title || prd.requirements !== existingPRD.requirements) { const versionParts = (existingPRD.version || '1.0.0').split('.'); const major = parseInt(versionParts[0]) || 1; const minor = parseInt(versionParts[1]) || 0; const patch = parseInt(versionParts[2]) || 0; newVersion = `${major}.${minor}.${patch + 1}`; } return await database.run( 'UPDATE prds SET title = ?, description = ?, requirements = ?, priority = ?, status = ?, version = ?, updated_at = datetime("now"), last_modified_by = ? WHERE id = ?', [prd.title, prd.description, JSON.stringify(prd.requirements), prd.priority, prd.status, newVersion, 'dashboard', id] ); } export async function deletePRD(id) { const database = await getDatabase(); // MCP 스타일의 향상된 PRD 삭제 (PRDManager 패턴 적용) // 1. 삭제 전 PRD 존재 확인 const existingPRD = await database.get('SELECT * FROM prds WHERE id = ?', id); if (!existingPRD) { throw new Error(`PRD를 찾을 수 없습니다: ${id}`); } // 2. 의존성 체크 - 관련 설계(designs)가 있는지 확인 const relatedDesigns = await database.all( 'SELECT id, title FROM designs WHERE requirement_id = ?', id ); if (relatedDesigns.length > 0) { const designTitles = relatedDesigns.map(d => d.title).join(', '); throw new Error(`이 PRD와 연결된 설계가 있어 삭제할 수 없습니다: ${designTitles}`); } // 3. 관련 문서 링크 체크 const relatedDocuments = await database.all( 'SELECT document_id FROM document_links WHERE linked_entity_type = ? AND linked_entity_id = ?', ['prd', id] ); // 4. 트랜잭션으로 안전한 삭제 수행 await database.run('BEGIN TRANSACTION'); try { // 관련 문서 링크 삭제 if (relatedDocuments.length > 0) { await database.run( 'DELETE FROM document_links WHERE linked_entity_type = ? AND linked_entity_id = ?', ['prd', id] ); } // PRD 삭제 const result = await database.run('DELETE FROM prds WHERE id = ?', id); await database.run('COMMIT'); // 삭제 결과 검증 if (result.changes === 0) { throw new Error('PRD 삭제 중 오류가 발생했습니다'); } return { success: true, deletedPRD: existingPRD.title, relatedDocumentsUnlinked: relatedDocuments.length, message: `PRD "${existingPRD.title}" 및 관련 링크 ${relatedDocuments.length}개가 성공적으로 삭제되었습니다` }; } catch (error) { await database.run('ROLLBACK'); throw error; } } // Task operations export async function getAllTasks() { // MCP TaskManager로 완전히 대체된 함수 const { TaskManager } = await import('C:/dev/workflow-mcp/src/models/TaskManager.js'); try { const taskManager = new TaskManager(); await taskManager.ensureInitialized(); const result = await taskManager.listTasks(); return result.success ? result.tasks : []; } catch (error) { console.error('MCP TaskManager getAllTasks 실패:', error); // 폴백: 기본 데이터베이스 조회 const database = await getDatabase(); return await database.all(` SELECT t.*, pl.title as plan_title, p.title as prd_title FROM tasks t LEFT JOIN designs d ON t.design_id = d.id LEFT JOIN prds p ON d.requirement_id = p.id ORDER BY t.created_at DESC `); } } export async function getTaskById(id) { // MCP TaskManager로 완전히 대체된 함수 const { TaskManager } = await import('C:/dev/workflow-mcp/src/models/TaskManager.js'); try { const taskManager = new TaskManager(); await taskManager.ensureInitialized(); const result = await taskManager.getTask(id); if (result.success) { // 대시보드에 필요한 추가 정보 보강 (plan_title, prd_title) const task = result.task; if (task.planId) { const database = await getDatabase(); const planInfo = await database.get(` SELECT p.title as plan_title, pr.title as prd_title FROM designs d LEFT JOIN prds pr ON p.prd_id = pr.id WHERE p.id = ? `, task.planId); if (planInfo) { task.plan_title = planInfo.plan_title; task.prd_title = planInfo.prd_title; } } return task; } return null; } catch (error) { console.error('MCP TaskManager getTaskById 실패:', error); // 폴백: 기본 데이터베이스 조회 const database = await getDatabase(); return await database.get(` SELECT t.*, pl.title as plan_title, p.title as prd_title FROM tasks t LEFT JOIN designs d ON t.design_id = d.id LEFT JOIN prds p ON d.requirement_id = p.id WHERE t.id = ? `, id); } } export async function getTasksByPRDId(prdId) { const database = await getDatabase(); return await database.all(` SELECT t.*, pl.title as plan_title, p.title as prd_title FROM tasks t LEFT JOIN designs d ON t.design_id = d.id LEFT JOIN prds p ON d.requirement_id = p.id WHERE p.id = ? ORDER BY t.created_at ASC `, prdId); } export async function createTask(task) { // MCP TaskManager로 완전히 대체된 함수 const { TaskManager } = await import('C:/dev/workflow-mcp/src/models/TaskManager.js'); try { const taskManager = new TaskManager(); await taskManager.ensureInitialized(); // 대시보드 형식을 MCP 형식으로 변환 const taskData = { title: task.title, description: task.description, status: task.status, priority: task.priority, dueDate: task.due_date, planId: task.plan_id, assignee: task.assignee || null, estimatedHours: task.estimated_hours || 0, createdBy: 'dashboard' }; const result = await taskManager.createTask(taskData); if (result.success) { return result.task.id; } throw new Error(result.message || 'Task 생성 실패'); } catch (error) { console.error('MCP TaskManager createTask 실패:', error); // 폴백: 기본 데이터베이스 삽입 const database = await getDatabase(); const result = await database.run( 'INSERT INTO tasks (title, description, status, priority, due_date, plan_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, datetime("now"), datetime("now"))', [task.title, task.description, task.status, task.priority, task.due_date, task.plan_id] ); return result.lastID; } } export async function updateTask(id, task) { // MCP TaskManager로 완전히 대체된 함수 const { TaskManager } = await import('C:/dev/workflow-mcp/src/models/TaskManager.js'); try { const taskManager = new TaskManager(); await taskManager.ensureInitialized(); // 대시보드 형식을 MCP 형식으로 변환 const updates = { title: task.title, description: task.description, status: task.status, priority: task.priority, dueDate: task.due_date, planId: task.plan_id, assignee: task.assignee, estimatedHours: task.estimated_hours, actualHours: task.actual_hours, notes: task.notes }; const result = await taskManager.updateTask(id, updates); if (result.success) { return { changes: 1 }; // 대시보드 호환성을 위해 changes 형식 반환 } throw new Error(result.message || 'Task 업데이트 실패'); } catch (error) { console.error('MCP TaskManager updateTask 실패:', error); // 폴백: 기본 데이터베이스 업데이트 const database = await getDatabase(); return await database.run( 'UPDATE tasks SET title = ?, description = ?, status = ?, priority = ?, due_date = ?, plan_id = ?, updated_at = datetime("now") WHERE id = ?', [task.title, task.description, task.status, task.priority, task.due_date, task.plan_id, id] ); } } export async function updateTaskStatus(id, status) { // MCP TaskManager로 완전히 대체된 함수 const { TaskManager } = await import('C:/dev/workflow-mcp/src/models/TaskManager.js'); try { const taskManager = new TaskManager(); await taskManager.ensureInitialized(); const result = await taskManager.updateTask(id, { status }); if (result.success) { return { changes: 1 }; // 대시보드 호환성을 위해 changes 형식 반환 } throw new Error(result.message || 'Task 상태 업데이트 실패'); } catch (error) { console.error('MCP TaskManager updateTaskStatus 실패:', error); // 폴백: 기본 데이터베이스 업데이트 const database = await getDatabase(); return await database.run( 'UPDATE tasks SET status = ?, updated_at = datetime("now") WHERE id = ?', [status, id] ); } } export async function deleteTask(id) { const database = await getDatabase(); console.log('🗑️ DELETE Task 요청:', id); try { // 우선 Task가 존재하는지 확인 const existingTask = await database.get('SELECT * FROM tasks WHERE id = ?', id); if (!existingTask) { console.log('❌ Task not found:', id); throw new Error(`Task를 찾을 수 없습니다: ${id}`); } console.log('✅ Task 존재 확인:', existingTask.title); // 의존성 체크 - 다른 Task가 이 Task에 의존하는지 확인 const dependentTasks = await database.all( 'SELECT t.id, t.title FROM tasks t JOIN task_dependencies td ON t.id = td.dependent_task_id WHERE td.prerequisite_task_id = ?', id ); if (dependentTasks.length > 0) { const dependentTitles = dependentTasks.map(t => t.title).join(', '); console.log('⚠️ 의존성 있는 Task들:', dependentTitles); throw new Error(`이 Task에 의존하는 다른 Task가 있어 삭제할 수 없습니다: ${dependentTitles}`); } // 트랜잭션으로 삭제 수행 await database.run('BEGIN TRANSACTION'); try { // Task dependencies 먼저 삭제 (외래키 제약조건) await database.run('DELETE FROM task_dependencies WHERE dependent_task_id = ? OR prerequisite_task_id = ?', id, id); console.log('✅ Task dependencies 삭제 완료'); // Task 삭제 const result = await database.run('DELETE FROM tasks WHERE id = ?', id); console.log('✅ Task 삭제 완료:', result.changes); await database.run('COMMIT'); if (result.changes > 0) { console.log('🎉 Task 삭제 성공:', existingTask.title); return { changes: result.changes, deletedTask: existingTask.title }; } else { throw new Error('Task가 삭제되지 않았습니다'); } } catch (innerError) { await database.run('ROLLBACK'); throw innerError; } } catch (error) { console.error('❌ Task 삭제 실패:', error.message); throw error; } } // Plan operations export async function getAllPlans() { const database = await getDatabase(); const planRows = await database.all(` SELECT p.*, COUNT(DISTINCT t.id) as task_count, COUNT(CASE WHEN t.status = 'completed' THEN 1 END) as completed_tasks, d.id as document_id, d.title as document_title, d.content as document_content FROM designs d LEFT JOIN tasks t ON p.id = t.plan_id LEFT JOIN document_links dl ON p.id = dl.linked_entity_id AND dl.linked_entity_type = 'plan' LEFT JOIN documents d ON dl.document_id = d.id GROUP BY p.id ORDER BY p.created_at DESC `); // Merge with file storage data for (const plan of planRows) { const fileData = await readFileStorageData('designs', plan.id); if (fileData) { // Merge file data into database record plan.description = fileData.description || plan.description; plan.timeline = fileData.timeline || (plan.timeline ? JSON.parse(plan.timeline) : {}); plan.milestones = fileData.milestones || (plan.milestones ? JSON.parse(plan.milestones) : []); plan.resources = fileData.resources; plan.goals = fileData.goals; plan.constraints = fileData.constraints; plan.assumptions = fileData.assumptions; plan.risks = fileData.risks; plan.success_criteria = fileData.success_criteria; plan.deliverables = fileData.deliverables; plan.stakeholders = fileData.stakeholders; plan.communication_plan = fileData.communication_plan; plan.budget = fileData.budget; plan.notes = fileData.notes; plan.tags = fileData.tags; // Set document_content from file data if available plan.document_content = fileData; } } return planRows; } export async function getPlanById(id) { const database = await getDatabase(); const plan = await database.get('SELECT * FROM designs WHERE id = ?', id); if (plan) { // Merge with file storage data const fileData = await readFileStorageData('designs', id); if (fileData) { // Merge file data into database record plan.description = fileData.description || plan.description; plan.timeline = fileData.timeline || (plan.timeline ? JSON.parse(plan.timeline) : {}); plan.milestones = fileData.milestones || (plan.milestones ? JSON.parse(plan.milestones) : []); plan.resources = fileData.resources; plan.goals = fileData.goals; plan.constraints = fileData.constraints; plan.assumptions = fileData.assumptions; plan.risks = fileData.risks; plan.success_criteria = fileData.success_criteria; plan.deliverables = fileData.deliverables; plan.stakeholders = fileData.stakeholders; plan.communication_plan = fileData.communication_plan; plan.budget = fileData.budget; plan.notes = fileData.notes; plan.tags = fileData.tags; // Set document_content from file data if available plan.document_content = fileData; } } return plan; } export async function createPlan(plan) { const database = await getDatabase(); const result = await database.run( 'INSERT INTO designs (title, description, timeline, milestones, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, datetime("now"), datetime("now"))', [plan.title, plan.description, JSON.stringify(plan.timeline), JSON.stringify(plan.milestones), plan.status] ); return result.lastID; } export async function updatePlan(id, plan) { const database = await getDatabase(); return await database.run( 'UPDATE designs SET title = ?, description = ?, timeline = ?, milestones = ?, status = ?, updated_at = datetime("now") WHERE id = ?', [plan.title, plan.description, JSON.stringify(plan.timeline), JSON.stringify(plan.milestones), plan.status, id] ); } export async function deletePlan(id) { const database = await getDatabase(); return await database.run('DELETE FROM designs WHERE id = ?', id); } // Dashboard statistics export async function getDashboardStats() { const database = await getDatabase(); // 각 테이블에서 독립적으로 통계를 가져와서 조합 const [prdStats, taskStats, designStats] = await Promise.all([ // PRD 통계 database.get(` SELECT COUNT(*) as total_prds, COUNT(CASE WHEN status = 'active' THEN 1 END) as active_prds FROM prds `), // Task 통계 database.get(` SELECT COUNT(*) as total_tasks, COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_tasks, COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as in_progress_tasks, COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_tasks FROM tasks `), // Design 통계 (계획으로 표시) database.get(` SELECT COUNT(*) as total_plans, COUNT(CASE WHEN status = 'approved' THEN 1 END) as active_plans FROM designs `) ]); // 통계 조합 const stats = { total_prds: prdStats.total_prds, total_tasks: taskStats.total_tasks, total_plans: designStats.total_plans, completed_tasks: taskStats.completed_tasks, in_progress_tasks: taskStats.in_progress_tasks, pending_tasks: taskStats.pending_tasks, active_prds: prdStats.active_prds, active_plans: designStats.active_plans }; return stats; } // Task activity timeline export async function getTaskActivity(days = 30) { const database = await getDatabase(); return await database.all(` SELECT DATE(updated_at) as date, COUNT(*) as task_count, COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_count FROM tasks WHERE updated_at >= datetime('now', '-${days} days') GROUP BY DATE(updated_at) ORDER BY date ASC `); } // Priority distribution export async function getPriorityDistribution() { const database = await getDatabase(); return await database.all(` SELECT CASE WHEN LOWER(priority) = 'high' THEN 'High' WHEN LOWER(priority) = 'medium' THEN 'Medium' WHEN LOWER(priority) = 'low' THEN 'Low' WHEN LOWER(priority) = 'critical' THEN 'Critical' ELSE priority END as priority, COUNT(*) as count, COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed FROM tasks WHERE priority IS NOT NULL AND priority != '' GROUP BY LOWER(priority) `); } // ===================================== // Design Management Functions // ===================================== // Get all designs export async function getAllDesigns() { const database = await getDatabase(); console.log('🎨 getAllDesigns called'); try { const designs = await database.all(` SELECT d.*, r.title as requirement_title, COUNT(DISTINCT t.id) as task_count, COUNT(CASE WHEN t.status = 'completed' THEN 1 END) as completed_tasks FROM designs d LEFT JOIN prds r ON d.requirement_id = r.id LEFT JOIN tasks t ON d.id = t.design_id GROUP BY d.id ORDER BY d.created_at DESC `); console.log(`✅ Found ${designs.length} designs`); return designs; } catch (error) { console.error('❌ Error in getAllDesigns:', error); throw error; } } // Get design by ID export async function getDesignById(id) { const database = await getDatabase(); console.log('🎨 getDesignById called:', id); try { const design = await database.get(` SELECT d.*, r.title as requirement_title, COUNT(DISTINCT t.id) as task_count, COUNT(CASE WHEN t.status = 'completed' THEN 1 END) as completed_tasks FROM designs d LEFT JOIN prds r ON d.requirement_id = r.id LEFT JOIN tasks t ON d.id = t.design_id WHERE d.id = ? GROUP BY d.id `, id); if (design) { console.log('✅ Found design:', design.title); } else { console.log('❌ Design not found'); } return design; } catch (error) { console.error('❌ Error in getDesignById:', error); throw error; } } // Create design export async function createDesign(designData) { const database = await getDatabase(); console.log('🎨 createDesign called:', designData.title); try { // Generate ID if not provided const id = designData.id || `design-${Date.now()}`; const result = await database.run(` INSERT INTO designs ( id, title, description, requirement_id, status, design_type, priority, design_details, diagrams, acceptance_criteria, created_at, updated_at, created_by, version, tags, notes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, designData.title, designData.description || '', designData.requirement_id || null, designData.status || 'draft', designData.design_type || 'system', designData.priority || 'medium', designData.design_details || '{}', designData.diagrams || '', designData.acceptance_criteria || '[]', new Date().toISOString(), new Date().toISOString(), designData.created_by || 'system', 1, JSON.stringify(designData.tags || []), designData.notes || '' ]); console.log('✅ Design created:', result.changes); return { id, changes: result.changes }; } catch (error) { console.error('❌ Error in createDesign:', error); throw error; } } // Update design export async function updateDesign(id, updates) { const database = await getDatabase(); console.log('🎨 updateDesign called:', id); try { // Get existing design const existing = await database.get('SELECT * FROM designs WHERE id = ?', id); if (!existing) { throw new Error(`설계를 찾을 수 없습니다: ${id}`); } // Prepare update fields const updateFields = []; const updateValues = []; // Only update provided fields if (updates.title !== undefined) { updateFields.push('title = ?'); updateValues.push(updates.title); } if (updates.description !== undefined) { updateFields.push('description = ?'); updateValues.push(updates.description); } if (updates.requirement_id !== undefined) { updateFields.push('requirement_id = ?'); updateValues.push(updates.requirement_id); } if (updates.status !== undefined) { updateFields.push('status = ?'); updateValues.push(updates.status); } if (updates.design_type !== undefined) { updateFields.push('design_type = ?'); updateValues.push(updates.design_type); } if (updates.priority !== undefined) { updateFields.push('priority = ?'); updateValues.push(updates.priority); } if (updates.design_details !== undefined) { updateFields.push('design_details = ?'); updateValues.push(updates.design_details); } if (updates.diagrams !== undefined) { updateFields.push('diagrams = ?'); updateValues.push(updates.diagrams); } if (updates.acceptance_criteria !== undefined) { updateFields.push('acceptance_criteria = ?'); updateValues.push(updates.acceptance_criteria); } if (updates.tags !== undefined) { updateFields.push('tags = ?'); updateValues.push(JSON.stringify(updates.tags)); } if (updates.notes !== undefined) { updateFields.push('notes = ?'); updateValues.push(updates.notes); } // Always update version and timestamp updateFields.push('version = version + 1'); updateFields.push('updated_at = ?'); updateValues.push(new Date().toISOString()); updateValues.push(id); const sql = `UPDATE designs SET ${updateFields.join(', ')} WHERE id = ?`; const result = await database.run(sql, updateValues); console.log('✅ Design updated:', result.changes); return { changes: result.changes }; } catch (error) { console.error('❌ Error in updateDesign:', error); throw error; } } // Update design status export async function updateDesignStatus(id, status) { const database = await getDatabase(); console.log('🎨 updateDesignStatus called:', id, status); try { const result = await database.run(` UPDATE designs SET status = ?, updated_at = ?, version = version + 1 WHERE id = ? `, [status, new Date().toISOString(), id]); console.log('✅ Design status updated:', result.changes); return { changes: result.changes }; } catch (error) { console.error('❌ Error in updateDesignStatus:', error); throw error; } } // Delete design export async function deleteDesign(id) { const database = await getDatabase(); console.log('🗑️ DELETE Design 요청:', id); try { // 1. Design existence check const existingDesign = await database.get('SELECT * FROM designs WHERE id = ?', id); if (!existingDesign) { throw new Error(`설계를 찾을 수 없습니다: ${id}`); } console.log('✅ Design 존재 확인:', existingDesign.title); // 2. Dependency check - Tasks that depend on this design const dependentTasks = await database.all( 'SELECT id, title FROM tasks WHERE design_id = ?', id ); if (dependentTasks.length > 0) { const taskTitles = dependentTasks.map(t => t.title).join(', '); throw new Error(`이 설계에 의존하는 작업이 있어 삭제할 수 없습니다: ${taskTitles}`); } // 3. Transaction-based deletion await database.run('BEGIN TRANSACTION'); try { // Delete design const result = await database.run('DELETE FROM designs WHERE id = ?', id); console.log('✅ Design 삭제 완료:', result.changes); await database.run('COMMIT'); console.log('🎉 Design 삭제 성공:', existingDesign.title); return { changes: result.changes, deletedDesign: existingDesign.title }; } catch (error) { await database.run('ROLLBACK'); throw error; } } catch (error) { console.error('❌ Error in deleteDesign:', error); throw error; } }

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