diff --git a/backend/apps/mail/migrations/0002_alter_mailemail_feedback_alter_mailsender_rule.py b/backend/apps/mail/migrations/0002_alter_mailemail_feedback_alter_mailsender_rule.py new file mode 100644 index 0000000..42248c5 --- /dev/null +++ b/backend/apps/mail/migrations/0002_alter_mailemail_feedback_alter_mailsender_rule.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0 on 2026-02-22 00:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mail', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='mailemail', + name='feedback', + field=models.CharField(blank=True, choices=[('important', '重要だった'), ('not_important', '普通のメール'), ('never_notify', '今後通知しない'), ('always_notify', '常に通知してほしい')], max_length=20, null=True, verbose_name='フィードバック'), + ), + migrations.AlterField( + model_name='mailsender', + name='rule', + field=models.CharField(choices=[('always_notify', '常に通知'), ('never_notify', '通知しない')], default='never_notify', max_length=20, verbose_name='ルール'), + ), + ] diff --git a/backend/apps/mail/models.py b/backend/apps/mail/models.py index b2b075a..2f5c4a6 100644 --- a/backend/apps/mail/models.py +++ b/backend/apps/mail/models.py @@ -2,13 +2,19 @@ import uuid from django.db import models +SENDER_RULE_CHOICES = [ + ('always_notify', '常に通知'), + ('never_notify', '通知しない'), +] + + class MailSender(models.Model): - """送信者ルール(never_notify: 通知しない)""" + """送信者ルール""" email = models.EmailField(null=True, blank=True, verbose_name="メールアドレス") domain = models.CharField(max_length=255, null=True, blank=True, verbose_name="ドメイン") rule = models.CharField( max_length=20, - choices=[('never_notify', '通知しない')], + choices=SENDER_RULE_CHOICES, default='never_notify', verbose_name="ルール" ) @@ -45,6 +51,7 @@ FEEDBACK_CHOICES = [ ('important', '重要だった'), ('not_important', '普通のメール'), ('never_notify', '今後通知しない'), + ('always_notify', '常に通知してほしい'), ] diff --git a/backend/apps/mail/views.py b/backend/apps/mail/views.py index 5b30490..8d496c6 100644 --- a/backend/apps/mail/views.py +++ b/backend/apps/mail/views.py @@ -159,7 +159,7 @@ class FeedbackView(APIView): mail_email = self._get_mail_email(token) feedback = request.data.get('feedback') - valid_feedbacks = ['important', 'not_important', 'never_notify'] + valid_feedbacks = ['important', 'not_important', 'never_notify', 'always_notify'] if feedback not in valid_feedbacks: return Response( {'error': f'feedback は {valid_feedbacks} のいずれかを指定してください'}, @@ -171,18 +171,18 @@ class FeedbackView(APIView): mail_email.feedback_at = timezone.now() mail_email.save(update_fields=['feedback', 'feedback_at']) - # 「今後通知しない」の場合、送信者ルールを作成/更新 - if feedback == 'never_notify': + # 送信者ルールを伴うフィードバックの処理 + if feedback in ('never_notify', 'always_notify'): scope = request.data.get('scope') # 'address' or 'domain' if scope == 'address': MailSender.objects.update_or_create( email=mail_email.sender_email, - defaults={'domain': None, 'rule': 'never_notify'} + defaults={'domain': None, 'rule': feedback} ) elif scope == 'domain': MailSender.objects.update_or_create( domain=mail_email.sender_domain, - defaults={'email': None, 'rule': 'never_notify'} + defaults={'email': None, 'rule': feedback} ) return Response({'status': 'ok'}) diff --git a/frontend/src/app/mail/feedback/[token]/page.tsx b/frontend/src/app/mail/feedback/[token]/page.tsx index 7142845..4d7115d 100644 --- a/frontend/src/app/mail/feedback/[token]/page.tsx +++ b/frontend/src/app/mail/feedback/[token]/page.tsx @@ -3,6 +3,8 @@ import { useState, useEffect } from 'react'; import { useParams } from 'next/navigation'; +type FeedbackValue = 'important' | 'not_important' | 'never_notify' | 'always_notify'; + interface MailEmailFeedback { id: number; sender_email: string; @@ -10,11 +12,14 @@ interface MailEmailFeedback { subject: string; body_preview: string; received_at: string; - feedback: 'important' | 'not_important' | 'never_notify' | null; + feedback: FeedbackValue | null; } const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; +// スコープ選択が必要なフィードバック +const NEEDS_SCOPE: FeedbackValue[] = ['never_notify', 'always_notify']; + export default function FeedbackPage() { const params = useParams(); const token = params.token as string; @@ -23,7 +28,7 @@ export default function FeedbackPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [selected, setSelected] = useState<'important' | 'not_important' | 'never_notify' | null>(null); + const [selected, setSelected] = useState(null); const [showScopeChoice, setShowScopeChoice] = useState(false); const [scope, setScope] = useState<'address' | 'domain'>('address'); const [submitting, setSubmitting] = useState(false); @@ -34,18 +39,14 @@ export default function FeedbackPage() { try { const res = await fetch(`${API_URL}/api/mail/feedback/${token}/`); if (!res.ok) { - if (res.status === 404) { - setError('このフィードバックリンクは無効です'); - } else { - setError('メール情報の取得に失敗しました'); - } + setError(res.status === 404 ? 'このフィードバックリンクは無効です' : 'メール情報の取得に失敗しました'); return; } const data = await res.json(); setEmail(data); if (data.feedback) { setSelected(data.feedback); - if (data.feedback === 'never_notify') { + if (NEEDS_SCOPE.includes(data.feedback)) { setShowScopeChoice(true); } } @@ -58,19 +59,17 @@ export default function FeedbackPage() { fetchEmail(); }, [token]); - const handleSelect = (value: 'important' | 'not_important' | 'never_notify') => { + const handleSelect = (value: FeedbackValue) => { setSelected(value); - setShowScopeChoice(value === 'never_notify'); + setShowScopeChoice(NEEDS_SCOPE.includes(value)); setSubmitted(false); - if (value !== 'never_notify') { + setError(null); + if (!NEEDS_SCOPE.includes(value)) { submitFeedback(value, undefined); } }; - const submitFeedback = async ( - feedback: 'important' | 'not_important' | 'never_notify', - feedbackScope: 'address' | 'domain' | undefined - ) => { + const submitFeedback = async (feedback: FeedbackValue, feedbackScope: 'address' | 'domain' | undefined) => { setSubmitting(true); try { const body: Record = { feedback }; @@ -81,7 +80,7 @@ export default function FeedbackPage() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); - if (!res.ok) throw new Error('送信に失敗しました'); + if (!res.ok) throw new Error(); setSubmitted(true); setShowScopeChoice(false); setEmail((prev) => (prev ? { ...prev, feedback } : prev)); @@ -92,8 +91,10 @@ export default function FeedbackPage() { } }; - const handleNeverNotifyConfirm = () => { - submitFeedback('never_notify', scope); + const handleScopeConfirm = () => { + if (selected && NEEDS_SCOPE.includes(selected)) { + submitFeedback(selected, scope); + } }; const formatDate = (iso: string) => { @@ -124,10 +125,11 @@ export default function FeedbackPage() { if (!email) return null; - const feedbackLabel = { + const feedbackLabel: Record = { important: '✅ 重要だった', not_important: '📧 普通のメール', never_notify: '🔇 今後通知しない', + always_notify: '🔔 常に通知してほしい', }; return ( @@ -178,7 +180,7 @@ export default function FeedbackPage() { {/* 現在のフィードバック表示 */} {email.feedback && !submitted && (

- 現在の評価: {feedbackLabel[email.feedback]} (変更できます) + 現在の評価: {feedbackLabel[email.feedback]} (変更できます)

)} @@ -209,6 +211,20 @@ export default function FeedbackPage() { 📧 普通のメール(通知不要) + {/* 常に通知してほしい */} + + {/* 今後通知しない */} - {/* 今後通知しない → スコープ選択 */} - {showScopeChoice && ( -
-

通知をやめる範囲を選んでください

+ {/* スコープ選択(常に通知 / 今後通知しない で展開) */} + {showScopeChoice && selected && NEEDS_SCOPE.includes(selected) && ( +
+

+ {selected === 'always_notify' ? '常に通知する範囲を選んでください' : '通知をやめる範囲を選んでください'} +