操作
機能 #194
未完了チケット詳細画面 - AI支援による子課題自動作成機能
開始日:
2025-06-04
期日:
進捗率:
0%
予定工数:
説明
機能要望概要¶
対象画面: チケット詳細画面(例: https://task.call2arm.com/redmine-ui/tickets/192)
設置箇所: 詳細情報欄の下部
要望内容: AI支援による子課題自動作成機能
機能仕様¶
1. ボタン設置場所¶
CSSセレクタ: #root > div > div.min-h-screen.bg-gray-50 > div.flex > main > div > div > div.bg-white.rounded-lg.shadow-sm > div:nth-child(2) > div > div.md\:col-span-1 > div:nth-child(1)
HTML構造:
<div className="ticket-details-section">
{/* 既存の詳細情報 */}
<div className="detail-info">
<div className="project-info">プロジェクト: Redmineの設定</div>
<div className="tracker-info">トラッカー: 機能</div>
<div className="assignee-info">担当者: Redmine Admin</div>
<div className="progress-info">進捗: 0%</div>
</div>
{/* 新規追加: 子課題作成ボタン */}
<div className="child-ticket-actions mt-4 pt-4 border-t">
<button
className="btn btn-primary flex items-center gap-2"
onClick={handleCreateChildTickets}
>
🎯 子課題を作成
</button>
</div>
</div>
2. AI対話による課題分解プロセス¶
A. 初期質問フォーム¶
<div className="child-ticket-wizard">
<div className="wizard-header">
<h3 className="text-lg font-bold">AI支援による子課題作成</h3>
<p className="text-sm text-gray-600">親課題の内容を分析して、効果的な子課題を提案します</p>
</div>
<div className="parent-ticket-context bg-gray-50 p-4 rounded-lg mb-6">
<h4 className="font-semibold">親課題情報</h4>
<div className="text-sm">
<p><strong>件名:</strong> {parentTicket.subject}</p>
<p><strong>説明:</strong> {truncateText(parentTicket.description, 200)}</p>
<p><strong>プロジェクト:</strong> {parentTicket.project.name}</p>
</div>
</div>
<div className="wizard-questions space-y-4">
<div className="question-group">
<label className="block font-medium mb-2">🎯 目的は何ですか?</label>
<textarea
className="w-full border rounded-lg p-3"
placeholder="この課題を解決することで何を達成したいですか?"
value={formData.purpose}
onChange={(e) => setFormData({...formData, purpose: e.target.value})}
/>
</div>
<div className="question-group">
<label className="block font-medium mb-2">📋 誰が報告を受ける人ですか?</label>
<select
className="w-full border rounded-lg p-3"
value={formData.reporter}
onChange={(e) => setFormData({...formData, reporter: e.target.value})}
>
<option value="">選択してください</option>
{projectMembers.map(member => (
<option key={member.id} value={member.id}>{member.name}</option>
))}
</select>
</div>
<div className="question-group">
<label className="block font-medium mb-2">👥 誰が作業をする人ですか?</label>
<div className="space-y-2">
<select
className="w-full border rounded-lg p-3"
multiple
value={formData.assignees}
onChange={handleAssigneeChange}
>
{projectMembers.map(member => (
<option key={member.id} value={member.id}>{member.name}</option>
))}
</select>
<textarea
className="w-full border rounded-lg p-3"
placeholder="複数人いる場合は作業分担を教えてください"
value={formData.workDivision}
onChange={(e) => setFormData({...formData, workDivision: e.target.value})}
/>
</div>
</div>
<div className="question-group">
<label className="block font-medium mb-2">🎯 どんな結果をゴールに設定しますか?</label>
<textarea
className="w-full border rounded-lg p-3"
placeholder="期待する成果・完了条件を具体的に記述してください"
value={formData.goal}
onChange={(e) => setFormData({...formData, goal: e.target.value})}
/>
<div className="mt-2">
<label className="block text-sm font-medium mb-1">📅 納期はいつですか?</label>
<input
type="date"
className="border rounded-lg p-2"
value={formData.dueDate}
onChange={(e) => setFormData({...formData, dueDate: e.target.value})}
/>
</div>
</div>
<div className="question-group">
<label className="block font-medium mb-2">🛣️ マイルストーンとして何が必要ですか?</label>
<textarea
className="w-full border rounded-lg p-3"
placeholder="ゴール達成のための主要な節目・チェックポイントを教えてください"
value={formData.milestones}
onChange={(e) => setFormData({...formData, milestones: e.target.value})}
/>
</div>
<div className="question-group">
<label className="block font-medium mb-2">📄 中間成果物、成果物は何が必要ですか?</label>
<textarea
className="w-full border rounded-lg p-3"
placeholder="ドキュメント、設計書、コード、テスト結果など具体的な成果物を記述してください"
value={formData.deliverables}
onChange={(e) => setFormData({...formData, deliverables: e.target.value})}
/>
</div>
<div className="question-group">
<label className="block font-medium mb-2">⚙️ そのためにどんな作業が必要ですか?</label>
<textarea
className="w-full border rounded-lg p-3"
placeholder="必要な作業・タスクを思いつく限り列挙してください"
value={formData.requiredWork}
onChange={(e) => setFormData({...formData, requiredWork: e.target.value})}
/>
</div>
<div className="question-group">
<label className="block font-medium mb-2">🎫 何個くらいのチケットに分けますか?</label>
<div className="flex items-center gap-4">
<input
type="number"
className="border rounded-lg p-2 w-20"
min="1"
max="20"
value={formData.ticketCount}
onChange={(e) => setFormData({...formData, ticketCount: e.target.value})}
/>
<span className="text-sm text-gray-600">個</span>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.autoCount}
onChange={(e) => setFormData({...formData, autoCount: e.target.checked})}
/>
<span className="ml-2 text-sm">おまかせ(AIが最適な数を提案)</span>
</label>
</div>
</div>
</div>
<div className="wizard-actions mt-6 flex gap-3">
<button
className="btn btn-primary flex items-center gap-2"
onClick={generateChildTicketProposal}
disabled={!isFormValid()}
>
🤖 AIに子課題を提案してもらう
</button>
<button
className="btn btn-secondary"
onClick={closeWizard}
>
キャンセル
</button>
</div>
</div>
B. AI提案結果表示¶
<div className="ai-proposal-results">
<div className="proposal-header">
<h3 className="text-lg font-bold">🤖 AIによる子課題提案</h3>
<p className="text-sm text-gray-600">以下の子課題構成を提案します。必要に応じて編集してから作成してください。</p>
</div>
{/* 提案概要 */}
<div className="proposal-summary bg-blue-50 p-4 rounded-lg mb-6">
<h4 className="font-semibold mb-2">📋 提案概要</h4>
<div className="text-sm space-y-1">
<p><strong>総チケット数:</strong> {proposalData.totalTickets}個</p>
<p><strong>推定工期:</strong> {proposalData.estimatedDuration}</p>
<p><strong>主要マイルストーン:</strong> {proposalData.majorMilestones}</p>
</div>
</div>
{/* 子課題リスト */}
<div className="proposed-tickets space-y-4">
{proposalData.childTickets.map((ticket, index) => (
<div key={index} className="ticket-proposal border rounded-lg p-4">
<div className="ticket-header flex justify-between items-start mb-3">
<div className="flex-1">
<input
type="text"
className="w-full font-semibold text-lg border-b border-gray-300 focus:border-blue-500 outline-none"
value={ticket.subject}
onChange={(e) => updateTicketProposal(index, 'subject', e.target.value)}
/>
</div>
<div className="flex items-center gap-2 ml-4">
<span className="text-xs bg-gray-100 px-2 py-1 rounded">#{index + 1}</span>
<button
className="text-red-500 hover:text-red-700"
onClick={() => removeTicketProposal(index)}
>
🗑️
</button>
</div>
</div>
<div className="ticket-details grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">説明</label>
<textarea
className="w-full border rounded p-2 text-sm"
rows="3"
value={ticket.description}
onChange={(e) => updateTicketProposal(index, 'description', e.target.value)}
/>
</div>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium mb-1">担当者</label>
<select
className="w-full border rounded p-2"
value={ticket.assignee}
onChange={(e) => updateTicketProposal(index, 'assignee', e.target.value)}
>
<option value="">未割当て</option>
{projectMembers.map(member => (
<option key={member.id} value={member.id}>{member.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-sm font-medium mb-1">優先度</label>
<select
className="w-full border rounded p-2"
value={ticket.priority}
onChange={(e) => updateTicketProposal(index, 'priority', e.target.value)}
>
<option value="2">通常</option>
<option value="3">高め</option>
<option value="4">緊急</option>
<option value="1">低め</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">期限</label>
<input
type="date"
className="w-full border rounded p-2"
value={ticket.dueDate}
onChange={(e) => updateTicketProposal(index, 'dueDate', e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">推定工数</label>
<div className="flex items-center gap-2">
<input
type="number"
className="border rounded p-2 w-20"
step="0.5"
value={ticket.estimatedHours}
onChange={(e) => updateTicketProposal(index, 'estimatedHours', e.target.value)}
/>
<span className="text-sm text-gray-600">時間</span>
</div>
</div>
</div>
</div>
{/* 依存関係表示 */}
{ticket.dependencies && ticket.dependencies.length > 0 && (
<div className="dependencies mt-3 p-2 bg-yellow-50 rounded">
<span className="text-sm font-medium">📎 依存関係:</span>
<span className="text-sm ml-2">
{ticket.dependencies.map(dep => `#${dep}`).join(', ')} の完了後に着手
</span>
</div>
)}
</div>
))}
</div>
{/* アクション */}
<div className="proposal-actions mt-6 flex gap-3">
<button
className="btn btn-success flex items-center gap-2"
onClick={createChildTickets}
>
✅ この提案で子課題を作成
</button>
<button
className="btn btn-secondary flex items-center gap-2"
onClick={addTicketProposal}
>
➕ チケットを追加
</button>
<button
className="btn btn-primary flex items-center gap-2"
onClick={regenerateProposal}
>
🔄 再提案
</button>
<button
className="btn btn-secondary"
onClick={backToQuestions}
>
← 質問に戻る
</button>
</div>
</div>
3. AI提案ロジック¶
A. プロンプト設計¶
const generateChildTicketsPrompt = (parentTicket, formData) => {
return `
あなたはプロジェクト管理の専門家です。以下の親課題を効果的な子課題に分解してください。
【親課題情報】
件名: ${parentTicket.subject}
説明: ${parentTicket.description}
プロジェクト: ${parentTicket.project.name}
【要件】
目的: ${formData.purpose}
担当者: ${formData.assignees.map(id => getUserName(id)).join(', ')}
作業分担: ${formData.workDivision}
ゴール: ${formData.goal}
納期: ${formData.dueDate}
マイルストーン: ${formData.milestones}
成果物: ${formData.deliverables}
必要作業: ${formData.requiredWork}
希望チケット数: ${formData.autoCount ? 'おまかせ' : formData.ticketCount + '個'}
【提案要求】
1. 効果的な子課題の分解(件名・説明・担当者・期限・工数見積もり)
2. 作業の依存関係の明確化
3. リスク軽減のための並行作業の提案
4. 各子課題の成果物と完了条件
JSON形式で回答してください:
{
"totalTickets": number,
"estimatedDuration": "string",
"majorMilestones": "string",
"childTickets": [
{
"subject": "string",
"description": "string",
"assignee": number,
"priority": number,
"dueDate": "YYYY-MM-DD",
"estimatedHours": number,
"dependencies": [number] // 他のチケットのインデックス
}
]
}
`;
};
B. API実装¶
// 子課題提案API
app.post('/api/tickets/:id/generate-children', async (req, res) => {
const { parentTicketId } = req.params;
const formData = req.body;
try {
// 親課題情報取得
const parentTicket = await getTicket(parentTicketId);
// Claude APIで子課題提案生成
const prompt = generateChildTicketsPrompt(parentTicket, formData);
const aiResponse = await claudeAPI.generateResponse(prompt);
// 提案データの解析・検証
const proposalData = JSON.parse(aiResponse);
res.json({
success: true,
proposal: proposalData,
parentTicket: parentTicket
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// 子課題一括作成API
app.post('/api/tickets/:id/create-children', async (req, res) => {
const { parentTicketId } = req.params;
const { childTickets } = req.body;
try {
const createdTickets = [];
for (const childTicket of childTickets) {
const newTicket = await createTicket({
...childTicket,
parent_issue_id: parentTicketId,
project_id: parentTicket.project.id
});
createdTickets.push(newTicket);
}
res.json({
success: true,
createdTickets: createdTickets
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
4. 期待効果¶
- プロジェクト分解の効率化: 大きな課題の構造化
- 作業計画の最適化: AI支援による現実的なスケジュール提案
- 担当者配分の改善: スキルと工数を考慮した人員配置
- 進捗管理の向上: 明確なマイルストーンと依存関係
- 品質向上: 成果物と完了条件の明確化
5. 関連チケット¶
6. 優先度¶
- 緊急度: 中(プロジェクト管理効率化)
- 重要度: 高(AI支援による業務改善の中核機能)
表示するデータがありません
操作