From 0fe845741c8742254c4fe07984ba72be031fbfaf Mon Sep 17 00:00:00 2001 From: ooheunda Date: Thu, 2 Oct 2025 03:23:56 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feature:=20User=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=97=90=20newsletter=5Fsubscribed=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/admin.py | 18 +++++++++++++++++- .../0014_user_newsletter_subscribed.py | 19 +++++++++++++++++++ users/models.py | 3 +++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 users/migrations/0014_user_newsletter_subscribed.py diff --git a/users/admin.py b/users/admin.py index c6db206..50c4ba9 100644 --- a/users/admin.py +++ b/users/admin.py @@ -21,6 +21,7 @@ class UserAdmin(admin.ModelAdmin): "email", "group_id", "is_active", + "newsletter_subscribed", "created_at", "post_count", "get_qr_login_token", @@ -31,7 +32,7 @@ class UserAdmin(admin.ModelAdmin): empty_value_display = "-" ordering = ["-created_at"] - actions = ["make_inactive", "update_stats"] + actions = ["make_inactive", "update_stats", "make_unsubscribed"] def get_list_display(self, request): list_display = super().get_list_display(request) @@ -40,6 +41,7 @@ def get_list_display(self, request): "email": "이메일", "group_id": "그룹 ID", "is_active": "활성화 여부", + "newsletter_subscribed": "뉴스레터 구독 여부", "created_at": "생성일", } return list_display @@ -95,6 +97,20 @@ def make_inactive(self, request: HttpRequest, queryset: QuerySet[User]): messages.SUCCESS, ) + @admin.action(description="선택된 사용자 뉴스레터 구독 해제") + def make_unsubscribed( + self, request: HttpRequest, queryset: QuerySet[User] + ): + updated = queryset.update(newsletter_subscribed=False) + logger.info( + f"{request.user} 가 {updated} 명 사용자를 뉴스레터 구독 해제 했습니다." + ) + self.message_user( + request, + f"{updated} 명의 사용자가 뉴스레터 구독 해제되었습니다.", + messages.SUCCESS, + ) + @admin.action( description="선택된 사용자 실시간 통계 업데이트 (1명 정도만 진행, 이상 timeout 발생 위험)" ) diff --git a/users/migrations/0014_user_newsletter_subscribed.py b/users/migrations/0014_user_newsletter_subscribed.py new file mode 100644 index 0000000..3b9cda9 --- /dev/null +++ b/users/migrations/0014_user_newsletter_subscribed.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.6 on 2025-10-01 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0013_user_thumbnail"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="newsletter_subscribed", + field=models.BooleanField( + default=True, verbose_name="뉴스레터 구독 여부" + ), + ), + ] diff --git a/users/models.py b/users/models.py index 877b3fd..02b0b4d 100644 --- a/users/models.py +++ b/users/models.py @@ -47,6 +47,9 @@ class User(TimeStampedModel): is_active = models.BooleanField( default=True, null=False, verbose_name="활성 여부" ) + newsletter_subscribed = models.BooleanField( + default=True, null=False, verbose_name="뉴스레터 구독 여부" + ) def __str__(self) -> str: return f"{self.velog_uuid}" From ed6c8345c271e658b6ae4b7072039c4eb27a093c Mon Sep 17 00:00:00 2001 From: ooheunda Date: Thu, 2 Oct 2025 03:24:56 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feature:=20=EB=A9=94=EC=9D=BC=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EB=A7=8C=20=EB=B6=84=EC=84=9D/=EB=B0=9C=EC=86=A1=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- insight/tasks/weekly_newsletter_batch.py | 1 + insight/tasks/weekly_user_trend_analysis.py | 1 + insight/tests/conftest.py | 1 + insight/tests/tasks/test_weekly_newsletter_batch.py | 10 +++++++++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/insight/tasks/weekly_newsletter_batch.py b/insight/tasks/weekly_newsletter_batch.py index 2f040ab..1561be0 100644 --- a/insight/tasks/weekly_newsletter_batch.py +++ b/insight/tasks/weekly_newsletter_batch.py @@ -91,6 +91,7 @@ def _get_target_user_chunks(self) -> list[list[dict]]: User.objects.filter( is_active=True, email__isnull=False, + newsletter_subscribed=True, ) .values("id", "email", "username") .distinct("email") diff --git a/insight/tasks/weekly_user_trend_analysis.py b/insight/tasks/weekly_user_trend_analysis.py index f02cf0f..234ad80 100644 --- a/insight/tasks/weekly_user_trend_analysis.py +++ b/insight/tasks/weekly_user_trend_analysis.py @@ -282,6 +282,7 @@ async def _fetch_data( User.objects.filter( email__isnull=False, is_active=True, + newsletter_subscribed=True, ) .exclude(email="") .values("id", "username") diff --git a/insight/tests/conftest.py b/insight/tests/conftest.py index 7a749ce..448b7d7 100644 --- a/insight/tests/conftest.py +++ b/insight/tests/conftest.py @@ -47,6 +47,7 @@ def user(db): email="test@example.com", username="test_user", is_active=True, + newsletter_subscribed=True, ) diff --git a/insight/tests/tasks/test_weekly_newsletter_batch.py b/insight/tests/tasks/test_weekly_newsletter_batch.py index 794c9ab..1bee5e9 100644 --- a/insight/tests/tasks/test_weekly_newsletter_batch.py +++ b/insight/tests/tasks/test_weekly_newsletter_batch.py @@ -115,6 +115,11 @@ def test_get_target_user_chunks_success( chunks = newsletter_batch._get_target_user_chunks() + mock_filter.assert_called_once_with( + is_active=True, + email__isnull=False, + newsletter_subscribed=True, + ) assert len(chunks) == 1 assert len(chunks[0]) == 1 assert chunks[0][0]["email"] == user.email @@ -197,7 +202,10 @@ def test_build_newsletters_success( assert newsletters[0].user_id == user.id assert newsletters[0].email_message.to[0] == user.email # 제목 포맷 검증 - assert "벨로그 대시보드 주간 뉴스레터" in newsletters[0].email_message.subject + assert ( + "벨로그 대시보드 주간 뉴스레터" + in newsletters[0].email_message.subject + ) @patch("insight.tasks.weekly_newsletter_batch.logger") def test_send_newsletters_success( From 0ecfe5cd51b0a46741a655f53fc08274c38219a7 Mon Sep 17 00:00:00 2001 From: ooheunda Date: Sat, 4 Oct 2025 00:28:58 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feature:=20=ED=85=9C=ED=94=8C=EB=A6=BF?= =?UTF-8?q?=EC=97=90=20=EC=88=98=EC=8B=A0=20=EA=B1=B0=EB=B6=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=A0=8C=EB=8D=94=EB=A7=81=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- insight/schemas.py | 1 + insight/tasks/weekly_newsletter_batch.py | 9 +++-- templates/insights/index.html | 47 +++++++++++++++-------- templates/insights/user_weekly_trend.html | 18 --------- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/insight/schemas.py b/insight/schemas.py index 0239424..77e2363 100644 --- a/insight/schemas.py +++ b/insight/schemas.py @@ -8,6 +8,7 @@ class NewsletterContext: s_date: str e_date: str + user: dict is_expired_token_user: bool weekly_trend_html: str user_weekly_trend_html: str | None = None diff --git a/insight/tasks/weekly_newsletter_batch.py b/insight/tasks/weekly_newsletter_batch.py index 1561be0..5383bd3 100644 --- a/insight/tasks/weekly_newsletter_batch.py +++ b/insight/tasks/weekly_newsletter_batch.py @@ -223,6 +223,7 @@ def _get_user_weekly_trend_html( def _get_newsletter_html( self, + user: dict, is_expired_token_user: bool, weekly_trend_html: str, user_weekly_trend_html: str | None, @@ -235,6 +236,7 @@ def _get_newsletter_html( NewsletterContext( s_date=self.weekly_info["s_date"], e_date=self.weekly_info["e_date"], + user=user, is_expired_token_user=is_expired_token_user, weekly_trend_html=weekly_trend_html, user_weekly_trend_html=user_weekly_trend_html, @@ -295,6 +297,7 @@ def _build_newsletters( # 최종 뉴스레터 렌더링 html_body = self._get_newsletter_html( + user=user, is_expired_token_user=is_expired_token_user, weekly_trend_html=weekly_trend_html, user_weekly_trend_html=user_weekly_trend_html, @@ -464,9 +467,9 @@ def run(self) -> None: weekly_trend_html = self._get_weekly_trend_html() # 로컬 환경에선 뉴스레터 발송 건너뜀 - if settings.DEBUG: - logger.info("DEBUG mode: Skipping newsletter sending") - return + # if settings.DEBUG: + # logger.info("DEBUG mode: Skipping newsletter sending") + # return # ========================================================== # # STEP4: 청크별로 뉴스레터 발송 및 결과 저장 diff --git a/templates/insights/index.html b/templates/insights/index.html index 4330354..0502820 100644 --- a/templates/insights/index.html +++ b/templates/insights/index.html @@ -134,6 +134,23 @@ {{weekly_trend_html}} {% endif %} +

+ {% if user.username %} + {{user.username}}님의 활동 리포트 + {% else %} + 활동 리포트 + {% endif %} +

+ {% if not is_expired_token_user and user_weekly_trend_html %} {{user_weekly_trend_html}} {% endif %} @@ -141,22 +158,6 @@ {% if is_expired_token_user %}
-

- {% if user.username %} - {{user.username}}님의 활동 리포트 - {% else %} - 활동 리포트 - {% endif %} -

+ 수신 거부 +

diff --git a/templates/insights/user_weekly_trend.html b/templates/insights/user_weekly_trend.html index b846e8f..af0304a 100644 --- a/templates/insights/user_weekly_trend.html +++ b/templates/insights/user_weekly_trend.html @@ -2,24 +2,6 @@ class="user-weekly-trend" style="box-sizing: border-box; margin-top: 40px; color: #000000" > - {% if insight.trending_summary or insight.user_weekly_stats or insight.user_weekly_reminder %} -

- {% if user.username %} - {{user.username}}님의 활동 리포트 - {% else %} - 활동 리포트 - {% endif %} -

- {% endif %} {% if insight.user_weekly_stats %}
Date: Sat, 4 Oct 2025 01:44:30 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=EA=B9=8C=EB=A8=B9=EC=9D=80=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- insight/tasks/weekly_newsletter_batch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/insight/tasks/weekly_newsletter_batch.py b/insight/tasks/weekly_newsletter_batch.py index 5383bd3..7da31d2 100644 --- a/insight/tasks/weekly_newsletter_batch.py +++ b/insight/tasks/weekly_newsletter_batch.py @@ -467,9 +467,9 @@ def run(self) -> None: weekly_trend_html = self._get_weekly_trend_html() # 로컬 환경에선 뉴스레터 발송 건너뜀 - # if settings.DEBUG: - # logger.info("DEBUG mode: Skipping newsletter sending") - # return + if settings.DEBUG: + logger.info("DEBUG mode: Skipping newsletter sending") + return # ========================================================== # # STEP4: 청크별로 뉴스레터 발송 및 결과 저장 From 193a8fba589b3b22010feb3af497aa5a0a198463 Mon Sep 17 00:00:00 2001 From: ooheunda Date: Sat, 4 Oct 2025 01:55:15 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=EC=A7=80=EB=82=9C=20=ED=95=AB?= =?UTF-8?q?=ED=94=BD=EC=8A=A4=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- insight/tests/test_user_weekly_trend_admin.py | 2 +- insight/tests/test_weekly_trend_admin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/insight/tests/test_user_weekly_trend_admin.py b/insight/tests/test_user_weekly_trend_admin.py index c023be0..a25fde3 100644 --- a/insight/tests/test_user_weekly_trend_admin.py +++ b/insight/tests/test_user_weekly_trend_admin.py @@ -413,7 +413,7 @@ def test_processed_at_formatted_with_date( result = user_weekly_trend_admin.processed_at_formatted( user_weekly_trend ) - assert now.strftime("%Y-%m-%d %H:%M") == result + assert now.strftime("%Y-%m-%d %H:%M:%S") == result def test_processed_at_formatted_no_date( self, user_weekly_trend_admin, user_weekly_trend diff --git a/insight/tests/test_weekly_trend_admin.py b/insight/tests/test_weekly_trend_admin.py index e1d69ee..42200af 100644 --- a/insight/tests/test_weekly_trend_admin.py +++ b/insight/tests/test_weekly_trend_admin.py @@ -49,7 +49,7 @@ def test_processed_at_formatted_with_date( weekly_trend.save() result = weekly_trend_admin.processed_at_formatted(weekly_trend) - assert now.strftime("%Y-%m-%d %H:%M") == result + assert now.strftime("%Y-%m-%d %H:%M:%S") == result def test_processed_at_formatted_no_date( self, weekly_trend_admin, weekly_trend: WeeklyTrend From aaab5bcac43488ec6ed247642521253a39838b1a Mon Sep 17 00:00:00 2001 From: ooheunda Date: Mon, 6 Oct 2025 00:48:35 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EB=B0=8F=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EC=95=BD=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- insight/admin/user_weekly_trend_admin.py | 4 ++++ templates/insights/index.html | 2 +- templates/insights/user_weekly_trend.html | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/insight/admin/user_weekly_trend_admin.py b/insight/admin/user_weekly_trend_admin.py index 9638875..13a3ffe 100644 --- a/insight/admin/user_weekly_trend_admin.py +++ b/insight/admin/user_weekly_trend_admin.py @@ -145,6 +145,10 @@ def render_full_preview(self, obj: UserWeeklyTrend): { "s_date": obj.week_start_date, "e_date": obj.week_end_date, + "user": { + "username": obj.user.username if obj.user else "N/A", + "email": obj.user.email if obj.user else "N/A", + }, "is_expired_token_user": False, "weekly_trend_html": weekly_trend_html, "user_weekly_trend_html": user_weekly_trend_html, diff --git a/templates/insights/index.html b/templates/insights/index.html index 0502820..29294f3 100644 --- a/templates/insights/index.html +++ b/templates/insights/index.html @@ -137,7 +137,7 @@

{% if insight.user_weekly_stats %}
Date: Mon, 6 Oct 2025 00:51:08 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=EC=A3=BC=EC=84=9D=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/insights/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/insights/index.html b/templates/insights/index.html index 29294f3..f26f802 100644 --- a/templates/insights/index.html +++ b/templates/insights/index.html @@ -313,6 +313,7 @@ 개인정보처리방침   |   +