Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions insight/tasks/weekly_newsletter_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ def _get_weekly_trend_html(self) -> str:

# 템플릿 렌더링이 제대로 되지 않은 경우 배치 종료
if (
" 주의 트렌딩 글" not in weekly_trend_html
or "트렌드 분석" not in weekly_trend_html
"이번 주의 트렌딩 글" not in weekly_trend_html
or "주간 트렌드 분석" not in weekly_trend_html
):
logger.error(
f"Failed to build weekly trend HTML for newsletter #{weekly_trend['id']}"
Expand Down
6 changes: 3 additions & 3 deletions insight/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ def sample_newsletter(user):
email_message=EmailMessage(
to=[user.email],
from_email=settings.DEFAULT_FROM_EMAIL,
subject="Test Newsletter",
text_body="Test content",
html_body="<div>Test content</div>",
subject="벨로그 대시보드 주간 뉴스레터 #1",
text_body="Weekly Report Test content",
html_body="<div>Weekly Report<br/>Velog Dashboard<br/>활동 리포트<br/>대시보드 보러가기</div>",
),
)

Expand Down
2 changes: 2 additions & 0 deletions insight/tests/tasks/test_weekly_newsletter_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ def test_build_newsletters_success(
assert len(newsletters) == 1
assert newsletters[0].user_id == user.id
assert newsletters[0].email_message.to[0] == user.email
# 제목 포맷 검증
assert "벨로그 대시보드 주간 뉴스레터" in newsletters[0].email_message.subject

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_send_newsletters_success(
Expand Down
35 changes: 21 additions & 14 deletions insight/tests/tasks/test_weekly_newsletter_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ def test_get_weekly_trend_html_success(

assert trending_summary[0]["title"] in weekly_trend_html
assert trend_analysis["insights"] in weekly_trend_html
assert "이 주의 트렌딩 글" in weekly_trend_html
assert "트렌드 분석" in weekly_trend_html
assert "벨로그 주간 트렌드" in weekly_trend_html
assert "이번 주의 트렌딩 글" in weekly_trend_html
assert "주간 트렌드 분석" in weekly_trend_html

@patch("insight.tasks.weekly_newsletter_batch.logger")
@pytest.mark.django_db
Expand Down Expand Up @@ -124,12 +125,11 @@ def test_get_user_weekly_trend_html_success(

assert trending_summary[0]["title"] in user_weekly_trend_html
assert trend_analysis["insights"] in user_weekly_trend_html
assert (
f'<b>{user_weekly_stats["new_posts"]}개</b>의 글을 작성'
in user_weekly_trend_html
)
assert f'{user_weekly_stats["new_posts"]}개의 글' in user_weekly_trend_html
assert "마지막으로 글을 작성하신지" not in user_weekly_trend_html
assert user.username in user_weekly_trend_html
assert "이번주에 작성한 글" in user_weekly_trend_html
assert "주간 내 활동 분석" in user_weekly_trend_html

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_get_user_weekly_trend_html_inactive_user(
Expand All @@ -147,13 +147,16 @@ def test_get_user_weekly_trend_html_inactive_user(
insight_data = inactive_user_weekly_trend.insight
user_weekly_reminder = insight_data.get("user_weekly_reminder")

assert "이번주에 쓴 글" not in user_weekly_trend_html
assert "내 글을 분석해보면?" not in user_weekly_trend_html
assert "글을 작성하지 않으셨네요" in user_weekly_trend_html
assert (
f'마지막으로 글을 작성하신지 {user_weekly_reminder["days_ago"]}일이 지났어요'
in user_weekly_trend_html
)
assert "이번주에 작성한 글" not in user_weekly_trend_html
assert "주간 내 활동 분석" not in user_weekly_trend_html
# days_ago가 있는 경우와 없는 경우 모두 처리
if user_weekly_reminder.get("days_ago"):
assert (
f'😭 마지막으로 글을 작성하신지 {user_weekly_reminder["days_ago"]}일이 지났어요!'
in user_weekly_trend_html
)
else:
assert "😭 글을 작성하지 않으셨네요!" in user_weekly_trend_html

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_get_user_weekly_trend_html_exception(
Expand Down Expand Up @@ -193,6 +196,8 @@ def test_get_newsletter_html_success(self, mock_logger, newsletter_batch):
assert weekly_trend_html in newsletter_html
assert user_weekly_trend_html in newsletter_html
assert "대시보드 보러가기" in newsletter_html
assert "Weekly Report" in newsletter_html
assert "Velog Dashboard" in newsletter_html

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_get_newsletter_html_expired_token_user(
Expand All @@ -210,10 +215,12 @@ def test_get_newsletter_html_expired_token_user(
)

# 템플릿 렌더링 검증
assert "토큰이 만료" in newsletter_html
assert "🚨 잠시만요, 토큰이 만료된 것 같아요!" in newsletter_html
assert "토큰이 만료되어 정상적으로 통계를 수집할 수 없었어요" in newsletter_html
assert weekly_trend_html in newsletter_html
assert user_weekly_trend_html not in newsletter_html
assert "대시보드 보러가기" in newsletter_html
assert "활동 리포트" in newsletter_html

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_get_newsletter_html_exception(
Expand Down
219 changes: 152 additions & 67 deletions templates/insights/index.html
Original file line number Diff line number Diff line change
@@ -1,74 +1,159 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Velog Dashboard Weekly</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f5f5f5; font-family: Arial, sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5;">
<tr>
<td align="center" style="padding: 20px;">
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; background-color: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);">
<tr>
<td style="padding: 30px;">
<div class="header" style="text-align: center; margin-bottom: 20px;">
<h1 style="text-decoration: underline; margin-bottom: 0; font-size: 32px; color: #333333;">
<span style="color: #63e6be;">Velog Dashboard</span> Weekly
</h1>
<p style="font-size: 14px; color: #666;">
{{s_date}} ~ {{e_date}} 사이의 트렌드를 전달해드려요.
</p>
</div>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Velog Dashboard Weekly Report</title>
</head>
<body style="margin: 0; padding: 0; background-color: #ebebeb; font-family: 'Apple SD Gothic Neo', 'Malgun Gothic', '맑은 고딕', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;">
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="background-color: #ebebeb; box-sizing: border-box;"
>
<tr>
<td
align="center"
style="padding: 30px 20px; box-sizing: border-box;"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="max-width: 600px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);"
>
<tr>
<td style="padding: 50px 25px; box-sizing: border-box;">
<!-- Header -->
<div
class="header"
style="text-align: center; margin-bottom: 30px; box-sizing: border-box;"
>
<div style="margin-bottom: 8px; box-sizing: border-box;">
<h2 style="font-size: 24px; font-weight: 900; color: #63E6BE; margin: 0; letter-spacing: 0; box-sizing: border-box;">
Velog Dashboard
</h2>
<h1 style="font-size: 32px; font-weight: 900; color: #000000; margin: 0; letter-spacing: 0; box-sizing: border-box;">
Weekly Report
</h1>
</div>
<p style="font-size: 12px; font-weight: 500; color: #acacac; margin: 0; letter-spacing: 0; box-sizing: border-box;">
<span style="font-weight: 700; text-decoration: underline">{{s_date}} ~ {{e_date}}</span> 사이의 트렌드를 전달해드려요
</p>
</div>

<div style="text-align: center; margin: 40px 0;">
<a
href="https://velog-dashboard.kro.kr/?utm_source=email&utm_medium=weekly_analysis&utm_campaign=dashboard_cta"
style="display: inline-block; background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%); color: white; text-decoration: none; padding: 16px 32px; border-radius: 50px; font-weight: 600; font-size: 16px; box-shadow: 0 4px 15px rgba(32, 201, 151, 0.3); transition: all 0.3s ease; border: none;">
📊 velog dashboard에서 전체 통계 체크하기!
</a>
</div>
<!-- CTA Button -->
<div style="text-align: center; margin: 30px 0; box-sizing: border-box;">
<a
href="https://velog-dashboard.kro.kr/?utm_source=email&utm_medium=weekly_analysis&utm_campaign=dashboard_cta"
style="display: inline-block; background: linear-gradient(90deg, #63e6be 0%, #7abbec 100%); color: #ffffff; text-decoration: none; padding: 16px 32px; border-radius: 100px; font-weight: 900; font-size: 12px; letter-spacing: 0; line-height: 14.4px; box-sizing: border-box;"
>
Velog Dashboard에서 전체 통계 체크하기!
</a>
</div>

<div class="insights" style="color: #333333; font-size: 14px; line-height: 1.6;">
{% if is_expired_token_user %}
<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 6px; margin: 30px 0;">
<p style="font-size: 16px; font-weight: bold; color: #574202; margin: 0 0 10px 0;">
🚨 잠깐, 토큰이 만료 되신 것 같아요!
</p>
<p style="color: #51441b; margin: 0;">
토큰이 만료되면 더 이상 통계를 수집할 수 없어요. 토큰을 재발급 받으시려면
<a href="https://velog-dashboard.kro.kr/?utm_source=email&utm_medium=weekly_analysis&utm_campaign=token_expire" style="color: #63e6be; font-weight: bold;">여기</a>에서 재로그인 해주세요.
</p>
</div>
{% endif %}
<div
class="insights"
style="color: #333333; font-size: 14px; line-height: 1.6; box-sizing: border-box;"
>
{% if weekly_trend_html %}
{{weekly_trend_html}}
{% endif %}

{% if not is_expired_token_user and user_weekly_trend_html %}
{{user_weekly_trend_html}}
{% endif %}
Comment on lines +60 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

크리티컬: 사전 렌더된 HTML을 안전하게 삽입하기 위해 |safe 필터가 필요합니다

현재 {{weekly_trend_html}}, {{user_weekly_trend_html}}는 이스케이프되어 태그가 그대로 노출될 수 있습니다. 이메일 레이아웃이 붕괴하고 text_body 추출에도 악영향을 줍니다. 반드시 |safe를 적용하세요.

다음 변경을 적용해 주세요:

-                  {% if weekly_trend_html %} 
-                    {{weekly_trend_html}}
+                  {% if weekly_trend_html %} 
+                    {{weekly_trend_html|safe}}
                   {% endif %} 
                   
-                  {% if not is_expired_token_user and user_weekly_trend_html %} 
-                    {{user_weekly_trend_html}} 
+                  {% if not is_expired_token_user and user_weekly_trend_html %} 
+                    {{user_weekly_trend_html|safe}} 
                   {% endif %} 
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{% if weekly_trend_html %}
{{weekly_trend_html}}
{% endif %}
{% if not is_expired_token_user and user_weekly_trend_html %}
{{user_weekly_trend_html}}
{% endif %}
{% if weekly_trend_html %}
{{ weekly_trend_html|safe }}
{% endif %}
{% if not is_expired_token_user and user_weekly_trend_html %}
{{ user_weekly_trend_html|safe }}
{% endif %}
🤖 Prompt for AI Agents
In templates/insights/index.html around lines 60 to 66, the variables
weekly_trend_html and user_weekly_trend_html are rendered without the |safe
filter, causing their HTML content to be escaped and displayed as plain text. To
fix this, add the |safe filter to both variables in the template expressions to
ensure the pre-rendered HTML is inserted safely and correctly without escaping.


{% if weekly_trend_html %}
{{weekly_trend_html}}
{% endif %}

{% if not is_expired_token_user and user_weekly_trend_html %}
{{user_weekly_trend_html}}
{% endif %}
</div>
{% if is_expired_token_user %}
<!-- Token Expired Warning -->
<div style="margin-bottom: 40px; box-sizing: border-box;">
<h2 style="font-size: 24px; font-weight: 900; color: #000000; margin-bottom: 20px; letter-spacing: 0; box-sizing: border-box;">
{% if user.username %}
{{user.username}}님의 활동 리포트
{% else %}
활동 리포트
{% endif %}
</h2>
<div style="background-color: #fffbd7; border-radius: 8px; padding: 20px; margin: 20px 0; box-sizing: border-box;">
<p style="font-size: 16px; font-weight: 800; color: #000000; margin: 0 0 10px 0; line-height: 20px; box-sizing: border-box;">
🚨 잠시만요, 토큰이 만료된 것 같아요!
</p>
<p style="color: #999999; margin: 0; font-size: 13px; font-weight: 500; line-height: 20px; letter-spacing: 0; box-sizing: border-box;">
토큰이 만료되어 정상적으로 통계를 수집할 수 없었어요.<br />
토큰을 재발급받으시려면
<a
href="https://velog-dashboard.kro.kr/?utm_source=email&utm_medium=weekly_analysis&utm_campaign=token_expire"
style="color: #999999; font-weight: bold; text-decoration: underline; box-sizing: border-box;"
>
여기
</a>
에서 다시 로그인해주세요.
</p>
</div>
</div>
{% endif %}

<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
</div>
</td>
</tr>
</table>

<div class="footer" style="margin-top: 20px; text-align: center;">
<a href="https://velog-dashboard.kro.kr/main?utm_source=email&utm_medium=weekly_analysis&utm_campaign=dashboard_cta" style="color: #63e6be; font-size: 12px; margin: 0 5px; text-decoration: none;">
대시보드 보러가기
</a>
<a href="https://nuung.notion.site/terms-of-service" style="color: #666; font-size: 12px; margin: 0 5px; text-decoration: none;">
서비스 이용약관
</a>
<a href="https://nuung.notion.site/privacy-policy" style="color: #666; font-size: 12px; margin: 0 5px; text-decoration: none;">
개인정보처리방침
</a>
</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer - Outside content box -->
<div
class="footer"
style="margin-top: 20px; text-align: center; width: 100%; max-width: 600px;"
>
<div style="display: flex; align-items: center; justify-content: center; margin-bottom: 10px; gap: 10px;">
<svg
width="20"
height="20"
style="border-radius: 2px"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
width="20"
height="20"
rx="0.46875"
fill="#1EC996"
/>
<path
d="M4.52499 5.81977L5.63155 12.4487H5.70632L9.36355 5.81977H10.1646L5.89451 13.4137H5.07206L3.7239 5.81977H4.52499ZM12.2818 13.4137H9.55815L11.0191 5.81977H13.7749C16.1034 5.81977 17.2495 7.24625 16.7955 9.60623C16.3374 11.9872 14.6424 13.4137 12.2818 13.4137ZM11.4206 12.0606H12.4781C13.9521 12.0606 14.8664 11.3054 15.1933 9.60623C15.5182 7.91753 14.8924 7.17283 13.4291 7.17283H12.361L11.4206 12.0606Z"
fill="white"
/>
</svg>

<span style="font-size: 15px; font-weight: 700; color: #4d4d4d; letter-spacing: 0; line-height: 18px;">
Velog Dashboard
</span>
</div>
<p style="font-size: 12px; font-weight: 500; color: #4d4d4d; margin: 0; letter-spacing: 0; line-height: 14.4px;">
<a
href="https://velog-dashboard.kro.kr/main?utm_source=email&utm_medium=weekly_analysis&utm_campaign=dashboard_cta"
style="color: #4d4d4d; text-decoration: underline; box-sizing: border-box;"
>
대시보드 보러가기
</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a
href="https://nuung.notion.site/terms-of-service"
style="color: #4d4d4d; text-decoration: underline; box-sizing: border-box;"
>
서비스 이용약관
</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a
href="https://nuung.notion.site/privacy-policy"
style="color: #4d4d4d; text-decoration: underline; box-sizing: border-box;"
>
개인정보처리방침
</a>
</p>
</div>
</td>
</tr>
</table>
</body>
</html>
</body>
</html>
Loading