Skip to main content
Glama
+page.svelte11.9 kB
<script> import { onMount } from 'svelte'; import { goto } from '$app/navigation'; let form = { title: '', description: '', status: 'pending', priority: 'medium', due_date: '', design_id: null, prd_id: null, project_id: null, // 프로젝트 연결 추가 assignee: '', estimated_hours: 0, actual_hours: 0, tags: '', notes: '', details: '', acceptance_criteria: '', test_strategy: '', created_by: 'dashboard' }; let designs = []; let prds = []; let projects = []; // 프로젝트 목록 추가 let loading = false; let error = null; onMount(async () => { await Promise.all([loadDesigns(), loadPRDs(), loadProjects()]); }); async function loadDesigns() { try { const response = await fetch('/api/designs'); if (response.ok) { designs = await response.json(); } } catch (e) { console.error('Failed to load designs:', e); } } async function loadPRDs() { try { const response = await fetch('/api/prds'); if (response.ok) { prds = await response.json(); } } catch (e) { console.error('Failed to load PRDs:', e); } } async function loadProjects() { try { const response = await fetch('/api/projects'); if (response.ok) { projects = await response.json(); } } catch (e) { console.error('Failed to load projects:', e); } } async function handleSubmit() { if (!form.title.trim()) { error = '제목을 입력해주세요'; return; } try { loading = true; error = null; const taskData = { ...form, title: form.title.trim(), description: form.description?.trim() || '', design_id: form.design_id || null, due_date: form.due_date || null, tags: form.tags ? form.tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [], estimated_hours: Number(form.estimated_hours) || 0, actual_hours: Number(form.actual_hours) || 0, acceptance_criteria: form.acceptance_criteria ? form.acceptance_criteria.split('\n').filter(line => line.trim()) : [] }; const response = await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(taskData) }); if (response.ok) { const result = await response.json(); goto('/tasks'); } else { const errorData = await response.json(); error = errorData.message || '작업 생성 중 오류가 발생했습니다'; } } catch (e) { error = '네트워크 오류: ' + e.message; } finally { loading = false; } } // 오늘 날짜를 기본값으로 설정 const today = new Date(); const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000); const defaultDueDate = nextWeek.toISOString().split('T')[0]; </script> <svelte:head> <title>새 작업 추가 - WorkflowMCP</title> </svelte:head> <div class="max-w-3xl mx-auto 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> <a href="/tasks" class="btn btn-secondary"> ← 목록으로 </a> </div> {#if error} <div class="bg-red-50 border border-red-200 rounded-md p-4"> <div class="text-red-800">{error}</div> </div> {/if} <form on:submit|preventDefault={handleSubmit} class="space-y-6"> <!-- 기본 정보 --> <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">기본 정보</h2> <div class="space-y-4"> <div> <label for="title" class="block text-sm font-medium text-gray-700 mb-1"> 제목 * </label> <input id="title" type="text" bind:value={form.title} class="form-input w-full" placeholder="작업 제목을 입력하세요" required /> </div> <div> <label for="description" class="block text-sm font-medium text-gray-700 mb-1"> 설명 </label> <textarea id="description" bind:value={form.description} rows="4" class="form-textarea w-full" placeholder="작업에 대한 상세 설명을 입력하세요" ></textarea> </div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="priority" class="block text-sm font-medium text-gray-700 mb-1"> 우선순위 </label> <select id="priority" bind:value={form.priority} class="form-select w-full"> <option value="low">낮음</option> <option value="medium">보통</option> <option value="high">높음</option> </select> </div> <div> <label for="status" class="block text-sm font-medium text-gray-700 mb-1"> 상태 </label> <select id="status" bind:value={form.status} class="form-select w-full"> <option value="pending">대기중</option> <option value="in_progress">진행중</option> <option value="completed">완료</option> </select> </div> </div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="due_date" class="block text-sm font-medium text-gray-700 mb-1"> 마감 일자 </label> <input id="due_date" type="date" bind:value={form.due_date} class="form-input w-full" placeholder={defaultDueDate} /> </div> <div> <label for="prd_id" class="block text-sm font-medium text-gray-700 mb-1"> 연결된 요구사항 </label> <select id="prd_id" bind:value={form.prd_id} class="form-select w-full"> <option value={null}>요구사항 선택 (선택사항)</option> {#each prds as prd} <option value={prd.id}>{prd.title}</option> {/each} </select> </div> </div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="design_id" class="block text-sm font-medium text-gray-700 mb-1"> 연결된 설계 </label> <select id="design_id" bind:value={form.design_id} class="form-select w-full"> <option value={null}>설계 선택 (선택사항)</option> {#each designs as design} <option value={design.id}>{design.title}</option> {/each} </select> </div> <div> <label for="project_id" class="block text-sm font-medium text-gray-700 mb-1"> 연결된 프로젝트 </label> <select id="project_id" bind:value={form.project_id} class="form-select w-full"> <option value={null}>프로젝트 선택 (선택사항)</option> {#each projects as project} <option value={project.id}>{project.name}</option> {/each} </select> </div> </div> </div> </div> <!-- 추가 정보 --> <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">추가 정보</h2> <div class="space-y-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="assignee" class="block text-sm font-medium text-gray-700 mb-1"> 담당자 </label> <input id="assignee" type="text" bind:value={form.assignee} class="form-input w-full" placeholder="담당자 이름을 입력하세요" /> </div> <div> <label for="estimated_hours" class="block text-sm font-medium text-gray-700 mb-1"> 예상 소요 시간 (시간) </label> <input id="estimated_hours" type="number" min="0" step="0.5" bind:value={form.estimated_hours} class="form-input w-full" placeholder="0" /> </div> </div> <div> <label for="tags" class="block text-sm font-medium text-gray-700 mb-1"> 태그 </label> <input id="tags" type="text" bind:value={form.tags} class="form-input w-full" placeholder="태그를 쉼표로 구분하여 입력하세요 (예: 개발, 버그수정, 긴급)" /> <p class="text-sm text-gray-500 mt-1">쉼표(,)로 구분하여 여러 태그를 입력할 수 있습니다</p> </div> <div> <label for="details" class="block text-sm font-medium text-gray-700 mb-1"> 상세 내용 </label> <textarea id="details" bind:value={form.details} rows="3" class="form-textarea w-full" placeholder="작업의 상세한 요구사항이나 구현 방법을 입력하세요" ></textarea> </div> <div> <label for="acceptance_criteria" class="block text-sm font-medium text-gray-700 mb-1"> 완료 기준 </label> <textarea id="acceptance_criteria" bind:value={form.acceptance_criteria} rows="3" class="form-textarea w-full" placeholder="작업 완료를 판단하는 기준을 줄바꿈으로 구분하여 입력하세요" ></textarea> <p class="text-sm text-gray-500 mt-1">각 기준을 새 줄에 입력하면 목록으로 저장됩니다</p> </div> <div> <label for="test_strategy" class="block text-sm font-medium text-gray-700 mb-1"> 테스트 전략 </label> <textarea id="test_strategy" bind:value={form.test_strategy} rows="3" class="form-textarea w-full" placeholder="이 작업을 테스트하는 방법을 입력하세요" ></textarea> </div> <div> <label for="notes" class="block text-sm font-medium text-gray-700 mb-1"> 메모 </label> <textarea id="notes" bind:value={form.notes} rows="2" class="form-textarea w-full" placeholder="기타 메모사항을 입력하세요" ></textarea> </div> </div> </div> <!-- 미리보기 --> <div class="card bg-gray-50"> <h2 class="text-xl font-semibold text-gray-900 mb-4">미리보기</h2> <div class="bg-white p-4 rounded-lg border"> <div class="flex items-start justify-between mb-2"> <h3 class="font-medium text-gray-900"> {form.title || '작업 제목'} </h3> <div class="flex space-x-2"> <span class="badge {form.status === 'completed' ? 'bg-green-100 text-green-800' : form.status === 'in_progress' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'}"> {form.status === 'completed' ? '완료' : form.status === 'in_progress' ? '진행중' : '대기중'} </span> <span class="badge {form.priority === 'high' ? 'bg-red-100 text-red-800' : form.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800'}"> {form.priority === 'high' ? '높음' : form.priority === 'medium' ? '보통' : '낮음'} </span> </div> </div> {#if form.description} <p class="text-gray-600 text-sm mb-3">{form.description}</p> {/if} {#if form.due_date} <div class="text-xs text-gray-500 mb-2"> 마감: {new Date(form.due_date).toLocaleDateString('ko-KR')} </div> {/if} {#if form.project_id} {@const selectedProject = projects.find(p => p.id == form.project_id)} {#if selectedProject} <div class="text-xs text-green-600 mb-2"> 📁 프로젝트: {selectedProject.name} </div> {/if} {/if} {#if form.design_id} {@const selectedDesign = designs.find(d => d.id == form.design_id)} {#if selectedDesign} <div class="text-xs text-blue-600"> 🎨 {selectedDesign.title} </div> {/if} {/if} </div> </div> <!-- 액션 버튼 --> <div class="flex justify-end space-x-3"> <a href="/tasks" class="btn btn-secondary">취소</a> <button type="submit" class="btn btn-primary" disabled={loading || !form.title.trim()} > {loading ? '생성 중...' : '작업 생성'} </button> </div> </form> </div> <style> .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