Skip to main content
Glama
+page.svelte12.7 kB
<script> import { onMount } from 'svelte'; let tasks = []; let loading = true; let error = null; let sortBy = 'updated'; // 'updated', 'created', 'title' onMount(async () => { await loadTasks(); }); async function loadTasks() { try { loading = true; const response = await fetch('/api/tasks'); if (response.ok) { tasks = await response.json(); } else { error = '작업 목록을 불러올 수 없습니다'; } } catch (e) { error = '작업 로딩 중 오류: ' + e.message; } finally { loading = false; } } async function updateTaskStatus(id, status) { try { const response = await fetch(`/api/tasks/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (response.ok) { await loadTasks(); } else { alert('작업 상태 변경 중 오류가 발생했습니다'); } } catch (e) { alert('상태 변경 중 오류: ' + e.message); } } async function deleteTask(id) { if (!confirm('이 작업을 삭제하시겠습니까?')) return; try { const response = await fetch(`/api/tasks/${id}`, { method: 'DELETE' }); if (response.ok) { await loadTasks(); } else { alert('작업 삭제 중 오류가 발생했습니다'); } } catch (e) { alert('삭제 중 오류: ' + e.message); } } function getStatusColor(status) { switch (status) { case 'completed': case 'done': return 'bg-green-100 text-green-800'; case 'in_progress': return 'bg-blue-100 text-blue-800'; case 'pending': return 'bg-gray-100 text-gray-800'; default: return 'bg-gray-100 text-gray-800'; } } function getStatusLabel(status) { switch (status) { case 'completed': case 'done': return '완료'; case 'in_progress': return '진행중'; case 'pending': return '대기중'; default: return status; } } function getPriorityColor(priority) { switch (priority) { case 'high': return 'bg-red-100 text-red-800'; case 'medium': return 'bg-yellow-100 text-yellow-800'; case 'low': return 'bg-green-100 text-green-800'; default: return 'bg-gray-100 text-gray-800'; } } function getPriorityLabel(priority) { switch (priority) { case 'high': return '높음'; case 'medium': return '보통'; case 'low': return '낮음'; default: return priority; } } // 정렬 로직 $: sortedTasks = tasks.sort((a, b) => { switch (sortBy) { case 'updated': return new Date(b.updatedAt || b.updated_at) - new Date(a.updatedAt || a.updated_at); case 'created': return new Date(b.createdAt || b.created_at) - new Date(a.createdAt || a.created_at); case 'title': return a.title.localeCompare(b.title); default: return new Date(b.updatedAt || b.updated_at) - new Date(a.updatedAt || a.updated_at); } }); // 상태별 그룹화 (정렬된 데이터 사용) $: tasksByStatus = { pending: sortedTasks.filter(t => t.status === 'pending'), in_progress: sortedTasks.filter(t => t.status === 'in_progress'), completed: sortedTasks.filter(t => t.status === 'completed' || t.status === 'done') }; </script> <svelte:head> <title>작업 관리 - WorkflowMCP</title> </svelte:head> <div class="space-y-6"> <div class="flex items-center justify-between"> <div> <h1 class="text-3xl font-bold text-gray-900">작업 관리</h1> <p class="text-gray-600 mt-1">프로젝트 작업을 관리합니다</p> </div> <div class="flex items-center gap-4"> <!-- 정렬 옵션 --> <div class="flex items-center gap-2"> <label class="text-sm text-gray-600">정렬:</label> <select bind:value={sortBy} class="text-sm border border-gray-300 rounded px-2 py-1 bg-white" > <option value="updated">최근 수정</option> <option value="created">최근 등록</option> <option value="title">제목 순</option> </select> </div> <a href="/tasks/new" class="btn btn-primary"> ✅ 새 작업 추가 </a> </div> </div> {#if loading} <div class="flex justify-center items-center h-64"> <div class="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div> </div> {:else if error} <div class="bg-red-50 border border-red-200 rounded-md p-4"> <div class="text-red-800">{error}</div> <button class="mt-2 text-sm text-red-600 hover:text-red-800" on:click={loadTasks} > 다시 시도 </button> </div> {:else if tasks.length === 0} <div class="text-center py-12"> <div class="text-gray-400 text-6xl mb-4">✅</div> <h3 class="text-lg font-medium text-gray-900 mb-2">작업이 없습니다</h3> <p class="text-gray-600 mb-6">첫 번째 작업을 추가해보세요</p> <a href="/tasks/new" class="btn btn-primary"> 새 작업 추가 </a> </div> {:else} <!-- 상태별 칸반 보드 --> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <!-- 대기중 --> <div class="bg-gray-50 rounded-lg p-4"> <h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center"> <span class="w-3 h-3 bg-gray-400 rounded-full mr-2"></span> 대기중 ({tasksByStatus.pending.length}) </h2> <div class="space-y-3"> {#each tasksByStatus.pending as task} <div class="card bg-white hover:shadow-md transition-shadow"> <div class="flex items-start justify-between mb-2"> <h3 class="font-medium text-gray-900 text-sm line-clamp-2"> {task.title} </h3> <span class="badge {getPriorityColor(task.priority)} ml-2 flex-shrink-0"> {getPriorityLabel(task.priority)} </span> </div> {#if task.description} <p class="text-sm text-gray-600 mb-3 line-clamp-2"> {task.description} </p> {/if} {#if task.due_date} <div class="text-xs text-gray-500 mb-3"> 마감: {new Date(task.due_date).toLocaleDateString('ko-KR')} </div> {/if} {#if task.plan_title} <div class="text-xs text-blue-600 mb-3"> 📅 {task.plan_title} </div> {/if} <div class="flex space-x-1"> <button class="flex-1 text-xs px-2 py-1 bg-blue-50 text-blue-700 rounded hover:bg-blue-100" on:click={() => updateTaskStatus(task.id, 'in_progress')} > 시작 </button> <a href="/tasks/{task.id}" class="text-xs px-2 py-1 bg-gray-50 text-gray-700 rounded hover:bg-gray-100" title="상세보기" > 상세 </a> <a href="/tasks/{task.id}/edit" class="text-xs px-2 py-1 bg-yellow-50 text-yellow-700 rounded hover:bg-yellow-100" title="편집" > 편집 </a> <button class="text-xs px-2 py-1 text-red-600 hover:text-red-800" on:click={() => deleteTask(task.id)} > 삭제 </button> </div> </div> {/each} </div> </div> <!-- 진행중 --> <div class="bg-blue-50 rounded-lg p-4"> <h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center"> <span class="w-3 h-3 bg-blue-500 rounded-full mr-2"></span> 진행중 ({tasksByStatus.in_progress.length}) </h2> <div class="space-y-3"> {#each tasksByStatus.in_progress as task} <div class="card bg-white hover:shadow-md transition-shadow border-l-4 border-blue-500"> <div class="flex items-start justify-between mb-2"> <h3 class="font-medium text-gray-900 text-sm line-clamp-2"> {task.title} </h3> <span class="badge {getPriorityColor(task.priority)} ml-2 flex-shrink-0"> {getPriorityLabel(task.priority)} </span> </div> {#if task.description} <p class="text-sm text-gray-600 mb-3 line-clamp-2"> {task.description} </p> {/if} {#if task.due_date} <div class="text-xs text-gray-500 mb-3"> 마감: {new Date(task.due_date).toLocaleDateString('ko-KR')} </div> {/if} {#if task.plan_title} <div class="text-xs text-blue-600 mb-3"> 📅 {task.plan_title} </div> {/if} <div class="flex space-x-1"> <button class="flex-1 text-xs px-2 py-1 bg-green-50 text-green-700 rounded hover:bg-green-100" on:click={() => updateTaskStatus(task.id, 'done')} > 완료 </button> <button class="flex-1 text-xs px-2 py-1 bg-gray-50 text-gray-700 rounded hover:bg-gray-100" on:click={() => updateTaskStatus(task.id, 'pending')} > 대기 </button> <a href="/tasks/{task.id}" class="text-xs px-2 py-1 bg-gray-50 text-gray-700 rounded hover:bg-gray-100" title="상세보기" > 상세 </a> <a href="/tasks/{task.id}/edit" class="text-xs px-2 py-1 bg-yellow-50 text-yellow-700 rounded hover:bg-yellow-100" title="편집" > 편집 </a> <button class="text-xs px-2 py-1 text-red-600 hover:text-red-800" on:click={() => deleteTask(task.id)} > 삭제 </button> </div> </div> {/each} </div> </div> <!-- 완료 --> <div class="bg-green-50 rounded-lg p-4"> <h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center"> <span class="w-3 h-3 bg-green-500 rounded-full mr-2"></span> 완료 ({tasksByStatus.completed.length}) </h2> <div class="space-y-3"> {#each tasksByStatus.completed as task} <div class="card bg-white hover:shadow-md transition-shadow border-l-4 border-green-500 opacity-75"> <div class="flex items-start justify-between mb-2"> <h3 class="font-medium text-gray-900 text-sm line-clamp-2"> {task.title} </h3> <span class="badge {getPriorityColor(task.priority)} ml-2 flex-shrink-0"> {getPriorityLabel(task.priority)} </span> </div> {#if task.description} <p class="text-sm text-gray-600 mb-3 line-clamp-2"> {task.description} </p> {/if} {#if task.due_date} <div class="text-xs text-gray-500 mb-3"> 마감: {new Date(task.due_date).toLocaleDateString('ko-KR')} </div> {/if} {#if task.plan_title} <div class="text-xs text-blue-600 mb-3"> 📅 {task.plan_title} </div> {/if} <div class="flex space-x-1"> <button class="flex-1 text-xs px-2 py-1 bg-blue-50 text-blue-700 rounded hover:bg-blue-100" on:click={() => updateTaskStatus(task.id, 'in_progress')} > 재시작 </button> <a href="/tasks/{task.id}" class="text-xs px-2 py-1 bg-gray-50 text-gray-700 rounded hover:bg-gray-100" title="상세보기" > 상세 </a> <a href="/tasks/{task.id}/edit" class="text-xs px-2 py-1 bg-yellow-50 text-yellow-700 rounded hover:bg-yellow-100" title="편집" > 편집 </a> <button class="text-xs px-2 py-1 text-red-600 hover:text-red-800" on:click={() => deleteTask(task.id)} > 삭제 </button> </div> </div> {/each} </div> </div> </div> <!-- 전체 통계 --> <div class="bg-gray-50 rounded-lg p-4"> <h3 class="text-sm font-medium text-gray-700 mb-2">전체 통계</h3> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div> <div class="text-gray-500">전체 작업</div> <div class="text-lg font-semibold text-gray-900">{tasks.length}</div> </div> <div> <div class="text-gray-500">진행중</div> <div class="text-lg font-semibold text-blue-600">{tasksByStatus.in_progress.length}</div> </div> <div> <div class="text-gray-500">완료</div> <div class="text-lg font-semibold text-green-600">{tasksByStatus.completed.length}</div> </div> <div> <div class="text-gray-500">완료율</div> <div class="text-lg font-semibold text-purple-600"> {tasks.length > 0 ? Math.round((tasksByStatus.completed.length / tasks.length) * 100) : 0}% </div> </div> </div> </div> {/if} </div> <style> .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .badge { display: inline-flex; align-items: center; padding: 0.25rem 0.5rem; border-radius: 0.375rem; font-size: 0.75rem; font-weight: 500; } </style>

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