From b19e08a8ddcde86180ae4d3228492ff2594dc5c4 Mon Sep 17 00:00:00 2001 From: Akira Date: Tue, 24 Feb 2026 13:52:21 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=83=AB=E5=87=A6=E7=90=86?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E7=94=BB=E9=9D=A2=E3=81=AB=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=83=90=E3=83=83=E3=82=AF=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 全メール(重要・通常問わず)に対してフィードバックボタンを追加 - PATCH /api/mail/emails//feedback/ エンドポイントを追加(JWT認証) - フィードバックモーダル: 重要/普通/今後通知しない/常に通知 の4択 - never_notify/always_notify 選択時はアドレス/ドメインの適用範囲を選択可能 - gmail_service アカウントのフィルタオプションを追加 Co-Authored-By: Claude Sonnet 4.6 --- backend/apps/mail/urls.py | 2 + backend/apps/mail/views.py | 41 ++++++ frontend/src/app/mail/history/page.tsx | 172 ++++++++++++++++++++----- 3 files changed, 185 insertions(+), 30 deletions(-) diff --git a/backend/apps/mail/urls.py b/backend/apps/mail/urls.py index 311d4ed..7b5eb61 100644 --- a/backend/apps/mail/urls.py +++ b/backend/apps/mail/urls.py @@ -12,6 +12,8 @@ urlpatterns = [ # メール記録(POST: APIキー認証)&履歴取得(GET: JWT認証) path('emails/', views.MailEmailView.as_view(), name='mail-emails'), + # 履歴画面からのフィードバック更新(JWT認証) + path('emails//feedback/', views.MailEmailFeedbackView.as_view(), name='mail-email-feedback'), # ダッシュボード用統計(JWT認証) path('stats/', views.MailStatsView.as_view(), name='mail-stats'), diff --git a/backend/apps/mail/views.py b/backend/apps/mail/views.py index d645009..afcf404 100644 --- a/backend/apps/mail/views.py +++ b/backend/apps/mail/views.py @@ -230,6 +230,47 @@ class FeedbackView(APIView): return Response({'status': 'ok'}) +# --------------------------------------------------------------------------- +# 履歴画面からのフィードバック更新(JWT認証) +# --------------------------------------------------------------------------- + +class MailEmailFeedbackView(APIView): + """ + PATCH /api/mail/emails//feedback/ 履歴画面から直接フィードバックを更新(JWT認証) + """ + permission_classes = [IsAuthenticated] + + def patch(self, request, pk): + mail_email = get_object_or_404(MailEmail, pk=pk) + + feedback = request.data.get('feedback') + valid_feedbacks = ['important', 'not_important', 'never_notify', 'always_notify'] + if feedback not in valid_feedbacks: + return Response( + {'error': f'feedback は {valid_feedbacks} のいずれかを指定してください'}, + status=status.HTTP_400_BAD_REQUEST + ) + + mail_email.feedback = feedback + mail_email.feedback_at = timezone.now() + mail_email.save(update_fields=['feedback', 'feedback_at']) + + if feedback in ('never_notify', 'always_notify'): + scope = request.data.get('scope') + if scope == 'address': + MailSender.objects.update_or_create( + email=mail_email.sender_email, + defaults={'domain': None, 'rule': feedback} + ) + elif scope == 'domain': + MailSender.objects.update_or_create( + domain=mail_email.sender_domain, + defaults={'email': None, 'rule': feedback} + ) + + return Response({'status': 'ok', 'feedback': feedback}) + + # --------------------------------------------------------------------------- # ルール管理(JWT認証) # --------------------------------------------------------------------------- diff --git a/frontend/src/app/mail/history/page.tsx b/frontend/src/app/mail/history/page.tsx index f1c2fc6..08fd0fa 100644 --- a/frontend/src/app/mail/history/page.tsx +++ b/frontend/src/app/mail/history/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { api } from '@/lib/api'; import Navbar from '@/components/Navbar'; -import { Mail, Loader2, ExternalLink, CheckCircle, XCircle, Clock, Shield } from 'lucide-react'; +import { Mail, Loader2, CheckCircle, XCircle, Clock, Shield, MessageSquare } from 'lucide-react'; interface MailEmail { id: number; @@ -34,6 +34,13 @@ const ACCOUNT_LABELS: Record = { gmail_service: 'Gmail (サービス用)', }; +const FEEDBACK_OPTIONS = [ + { value: 'important', label: '重要だった', color: 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100' }, + { value: 'not_important',label: '普通のメール', color: 'bg-gray-50 text-gray-600 border-gray-200 hover:bg-gray-100' }, + { value: 'never_notify', label: '今後通知しない', color: 'bg-orange-50 text-orange-700 border-orange-200 hover:bg-orange-100' }, + { value: 'always_notify',label: '常に通知してほしい', color: 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100' }, +]; + export default function MailHistoryPage() { const router = useRouter(); const [emails, setEmails] = useState([]); @@ -41,6 +48,12 @@ export default function MailHistoryPage() { const [filterAccount, setFilterAccount] = useState(''); const [filterVerdict, setFilterVerdict] = useState(''); + // フィードバックモーダル + const [feedbackModal, setFeedbackModal] = useState(null); + const [pendingFeedback, setPendingFeedback] = useState(''); + const [pendingScope, setPendingScope] = useState<'address' | 'domain'>('address'); + const [submitting, setSubmitting] = useState(false); + useEffect(() => { const fetchEmails = async () => { setLoading(true); @@ -68,6 +81,41 @@ export default function MailHistoryPage() { }); }; + const openFeedbackModal = (email: MailEmail) => { + setFeedbackModal(email); + setPendingFeedback(email.feedback ?? ''); + setPendingScope('address'); + }; + + const closeFeedbackModal = () => { + setFeedbackModal(null); + setPendingFeedback(''); + }; + + const submitFeedback = async () => { + if (!feedbackModal || !pendingFeedback) return; + setSubmitting(true); + try { + const body: Record = { feedback: pendingFeedback }; + if (['never_notify', 'always_notify'].includes(pendingFeedback)) { + body.scope = pendingScope; + } + await api.patch(`/mail/emails/${feedbackModal.id}/feedback/`, body); + setEmails(prev => prev.map(e => + e.id === feedbackModal.id + ? { ...e, feedback: pendingFeedback as MailEmail['feedback'], feedback_at: new Date().toISOString() } + : e + )); + closeFeedbackModal(); + } catch (error) { + console.error('Failed to submit feedback:', error); + } finally { + setSubmitting(false); + } + }; + + const needsScope = ['never_notify', 'always_notify'].includes(pendingFeedback); + return (
@@ -128,7 +176,6 @@ export default function MailHistoryPage() { 件名 LLM判定 フィードバック - @@ -156,35 +203,31 @@ export default function MailHistoryPage() { )} - {email.feedback ? ( - - {FEEDBACK_LABELS[email.feedback] ?? email.feedback} - - ) : email.llm_verdict === 'important' ? ( - - 未回答 - - ) : ( - - )} - - - {email.feedback_token && ( - + {email.feedback ? ( + + {FEEDBACK_LABELS[email.feedback] ?? email.feedback} + + ) : email.llm_verdict === 'important' ? ( + + 未回答 + + ) : ( + + )} + +
))} @@ -196,6 +239,75 @@ export default function MailHistoryPage() { )} + + {/* フィードバックモーダル */} + {feedbackModal && ( +
+
e.stopPropagation()}> +

フィードバック

+

{feedbackModal.sender_email}

+

{feedbackModal.subject}

+ +
+ {FEEDBACK_OPTIONS.map(opt => ( + + ))} +
+ + {needsScope && ( +
+

適用範囲:

+
+ + +
+
+ )} + +
+ + +
+
+
+ )} ); }