Skip to main content
Glama
+page.svelte8.95 kB
<script> import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import { _ } from 'svelte-i18n'; let prds = []; let loading = true; let error = null; let sortBy = 'created_desc'; // 기본 정렬: 최근 등록순 onMount(async () => { await loadPRDs(); }); async function loadPRDs() { try { loading = true; const response = await fetch(`/api/prds?sort=${sortBy}`); if (response.ok) { prds = await response.json(); } else { error = $_('prds.error_load_failed'); } } catch (e) { error = $_('prds.error_loading') + e.message; } finally { loading = false; } } // 정렬 변경 시 PRD 목록 재로드 async function handleSortChange() { await loadPRDs(); } async function deletePRD(id) { if (!confirm($_('prds.confirm_delete'))) return; try { const response = await fetch(`/api/prds/${id}`, { method: 'DELETE' }); if (response.ok) { await loadPRDs(); } else { const errorData = await response.json(); alert($_('prds.error_delete') + (errorData.error || 'Unknown error')); } } catch (e) { alert($_('prds.error_deleting') + e.message); } } function getStatusColor(status) { switch (status) { case 'active': return 'bg-green-100 text-green-800'; case 'inactive': return 'bg-gray-100 text-gray-800'; case 'draft': return 'bg-purple-100 text-purple-800'; case 'review': return 'bg-yellow-100 text-yellow-800'; case 'approved': return 'bg-emerald-100 text-emerald-800'; case 'completed': return 'bg-blue-100 text-blue-800'; default: return 'bg-gray-100 text-gray-800'; } } function getStatusLabel(status) { return $_(`prds.status_${status}`) || 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) { return $_(`prds.priority_${priority}`) || priority; } function formatDate(dateValue) { if (!dateValue) return '-'; try { let date; // ISO 문자열 형식인지 확인 (예: 2025-09-05T10:23:42.534Z) if (typeof dateValue === 'string' && dateValue.includes('T')) { date = new Date(dateValue); } // Unix timestamp 형식인지 확인 (예: 1757249412158.0) else if (typeof dateValue === 'string' && /^\d+\.?\d*$/.test(dateValue)) { date = new Date(parseFloat(dateValue)); } // 이미 숫자인 경우 else if (typeof dateValue === 'number') { date = new Date(dateValue); } // 기타 경우 직접 파싱 시도 else { date = new Date(dateValue); } // 유효한 날짜인지 확인 if (isNaN(date.getTime())) { return '-'; } // 날짜와 시간을 모두 표시 return date.toLocaleString('ko-KR', { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }); } catch (error) { console.error('Date formatting error:', error, dateValue); return '-'; } } </script> <svelte:head> <title>PRD 관리 - 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">{$_('prds.title')}</h1> <p class="text-gray-600 mt-1">{$_('prds.description')}</p> </div> <div class="flex items-center space-x-4"> <div class="flex items-center space-x-2"> <label for="sortBy" class="text-sm font-medium text-gray-700">{$_('prds.sort_label')}</label> <select id="sortBy" bind:value={sortBy} on:change={handleSortChange} class="form-select text-sm" > <option value="created_desc">{$_('prds.sort_created_desc')}</option> <option value="created_asc">{$_('prds.sort_created_asc')}</option> <option value="updated_desc">{$_('prds.sort_updated_desc')}</option> <option value="updated_asc">{$_('prds.sort_updated_asc')}</option> <option value="title_asc">{$_('prds.sort_title_asc')}</option> <option value="title_desc">{$_('prds.sort_title_desc')}</option> </select> </div> <a href="/prds/new" class="btn btn-primary"> {$_('prds.new_prd')} </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={loadPRDs} > {$_('prds.retry')} </button> </div> {:else if prds.length === 0} <div class="text-center py-12"> <h3 class="text-lg font-medium text-gray-900 mb-2">{$_('prds.no_prds')}</h3> <p class="text-gray-600 mb-6">{$_('prds.create_first')}</p> <a href="/prds/new" class="btn btn-primary"> {$_('prds.new_prd')} </a> </div> {:else} <!-- PRD 카드 그리드 --> <div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6"> {#each prds as prd} <div class="card hover:shadow-lg transition-shadow"> <div class="flex items-start justify-between mb-3"> <h3 class="text-lg font-semibold text-gray-900 line-clamp-2"> {prd.title} </h3> <div class="flex space-x-1 ml-2"> <span class="badge {getStatusColor(prd.status)}"> {getStatusLabel(prd.status)} </span> <span class="badge {getPriorityColor(prd.priority)}"> {getPriorityLabel(prd.priority)} </span> </div> </div> <p class="text-gray-600 text-sm mb-4 line-clamp-3"> {prd.description || $_('prds.no_description')} </p> <!-- 통계 --> <div class="flex items-center justify-between text-sm text-gray-500 mb-4"> <div class="flex items-center space-x-4"> <span class="flex items-center"> <span class="w-2 h-2 bg-blue-500 rounded-full mr-1"></span> {$_('prds.tasks')} {prd.task_count || 0}{$_('prds.tasks_suffix')} </span> <span class="flex items-center"> <span class="w-2 h-2 bg-green-500 rounded-full mr-1"></span> {$_('prds.completed')} {prd.completed_tasks || 0}{$_('prds.tasks_suffix')} </span> </div> </div> <!-- 날짜 정보 --> <div class="text-xs text-gray-400 mb-4"> <div>{$_('prds.created')}: {formatDate(prd.created_at)}</div> <div>{$_('prds.updated')}: {formatDate(prd.updated_at)}</div> </div> <!-- 액션 버튼 --> <div class="flex space-x-2"> <a href="/prds/{prd.id}" class="flex-1 text-center px-3 py-2 text-sm bg-blue-50 text-blue-700 rounded hover:bg-blue-100 transition-colors" > {$_('prds.view_details')} </a> <a href="/prds/{prd.id}/edit" class="flex-1 text-center px-3 py-2 text-sm bg-gray-50 text-gray-700 rounded hover:bg-gray-100 transition-colors" > {$_('prds.edit')} </a> <button class="px-3 py-2 text-sm bg-red-50 text-red-700 rounded hover:bg-red-100 transition-colors" on:click={() => deletePRD(prd.id)} > {$_('prds.delete')} </button> </div> </div> {/each} </div> <!-- 페이지 하단 통계 --> <div class="bg-gray-50 rounded-lg p-4"> <h3 class="text-sm font-medium text-gray-700 mb-2">{$_('prds.overall_stats')}</h3> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div> <div class="text-gray-500">{$_('prds.total_prds')}</div> <div class="text-lg font-semibold text-gray-900">{prds.length}</div> </div> <div> <div class="text-gray-500">{$_('prds.active_prds')}</div> <div class="text-lg font-semibold text-green-600"> {prds.filter(p => p.status === 'active').length} </div> </div> <div> <div class="text-gray-500">{$_('prds.total_tasks')}</div> <div class="text-lg font-semibold text-blue-600"> {prds.reduce((sum, p) => sum + (p.task_count || 0), 0)} </div> </div> <div> <div class="text-gray-500">{$_('prds.completed_tasks')}</div> <div class="text-lg font-semibold text-purple-600"> {prds.reduce((sum, p) => sum + (p.completed_tasks || 0), 0)} </div> </div> </div> </div> {/if} </div> <style> .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -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; } .form-select { @apply border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-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