Files
ai-trade/services/insta-render/templates/minimal/card.html.j2
gahusb bee0add9dd feat(services/insta-render): card_renderer.py + templates (SP-3)
NAS insta-lab/app/card_renderer.py 이식 + DB 의존성 제거.
slate 데이터는 worker가 NAS API에서 fetch해 인자로 전달.
결과 PNG는 INSTA_MEDIA_ROOT (/mnt/nas/webpage/data/insta/)에 직접 저장.
Browser pool + Semaphore(1) reuse (동시 Chromium 1개).
templates는 NAS와 동기화 (default theme + minimal theme).

.gitignore에 services/ 추적 예외 추가 (코드는 추적, .env는 유지).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 02:05:33 +09:00

788 lines
25 KiB
Django/Jinja
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hedgy Card News {{ page_no }}/10</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700;900&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #d0d0d0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Noto Sans KR', sans-serif;
letter-spacing: -0.02em;
line-height: 1.3;
}
.card {
position: relative;
width: 1080px;
height: 1350px;
overflow: hidden;
border-radius: 48px;
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
}
/* ── shared overlay layer ── */
.mask {
position: absolute;
word-wrap: break-word;
overflow: hidden;
}
/* ═══════════════════════════════════════════
PAGE 1 insta_card_start.png
bg: #f2f2f0 (light warm white)
═══════════════════════════════════════════ */
.p1-headline-mask {
top: 222px; left: 48px;
width: 580px; height: 150px;
background: #f2f2f0;
padding: 8px;
}
.p1-headline-text {
position: absolute;
top: 222px; left: 48px;
width: 580px; height: 150px;
padding: 8px;
font-size: 108px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p1-body-mask {
top: 400px; left: 48px;
width: 460px; height: 120px;
background: #f2f2f0;
padding: 8px;
}
.p1-body-text {
position: absolute;
top: 400px; left: 48px;
width: 460px; height: 120px;
padding: 8px;
font-size: 34px;
font-weight: 500;
color: #4a4e5e;
word-wrap: break-word;
overflow: hidden;
}
.p1-cta-mask {
top: 562px; left: 48px;
width: 260px; height: 76px;
background: #2f6ef7;
border-radius: 38px;
padding: 8px;
}
.p1-cta-text {
position: absolute;
top: 562px; left: 48px;
width: 260px; height: 76px;
border-radius: 38px;
padding: 8px 24px;
font-size: 34px;
font-weight: 700;
color: #ffffff;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* ═══════════════════════════════════════════
PAGE 2 insta_card_keyword.png
bg: #3a3fdb (blue gradient)
═══════════════════════════════════════════ */
.p2-headline-mask {
top: 148px; left: 56px;
width: 880px; height: 200px;
background: #3a3fdb;
padding: 8px;
}
.p2-headline-text {
position: absolute;
top: 148px; left: 56px;
width: 880px; height: 200px;
padding: 8px;
font-size: 88px;
font-weight: 900;
color: #ffffff;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p2-body-mask {
top: 370px; left: 56px;
width: 880px; height: 80px;
background: #3a3fdb;
padding: 8px;
}
.p2-body-text {
position: absolute;
top: 370px; left: 56px;
width: 880px; height: 80px;
padding: 8px;
font-size: 38px;
font-weight: 500;
color: #e0e4ff;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
/* ═══════════════════════════════════════════
PAGE 3 insta_card_highlight.png
bg: #3a3fdb
═══════════════════════════════════════════ */
.p3-headline-mask {
top: 148px; left: 56px;
width: 880px; height: 260px;
background: #3a3fdb;
padding: 8px;
}
.p3-headline-text {
position: absolute;
top: 148px; left: 56px;
width: 880px; height: 260px;
padding: 8px;
font-size: 88px;
font-weight: 900;
color: #ffffff;
word-wrap: break-word;
overflow: hidden;
}
.p3-body-mask {
top: 430px; left: 56px;
width: 880px; height: 80px;
background: #3a3fdb;
padding: 8px;
}
.p3-body-text {
position: absolute;
top: 430px; left: 56px;
width: 880px; height: 80px;
padding: 8px;
font-size: 38px;
font-weight: 500;
color: #e0e4ff;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
/* ═══════════════════════════════════════════
PAGE 4 insta_card_observation.png
bg: #f2f2f0
═══════════════════════════════════════════ */
.p4-label-mask {
top: 72px; left: 64px;
width: 200px; height: 52px;
background: #f2f2f0;
padding: 8px;
}
.p4-label-text {
position: absolute;
top: 72px; left: 64px;
width: 200px; height: 52px;
padding: 8px;
font-size: 32px;
font-weight: 700;
color: #2f6ef7;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p4-headline-mask {
top: 148px; left: 56px;
width: 700px; height: 110px;
background: #f2f2f0;
padding: 8px;
}
.p4-headline-text {
position: absolute;
top: 148px; left: 56px;
width: 700px; height: 110px;
padding: 8px;
font-size: 72px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p4-body-mask {
top: 290px; left: 56px;
width: 700px; height: 180px;
background: #f2f2f0;
padding: 8px;
}
.p4-body-text {
position: absolute;
top: 290px; left: 56px;
width: 700px; height: 180px;
padding: 8px;
font-size: 36px;
font-weight: 400;
color: #3a3e50;
word-wrap: break-word;
overflow: hidden;
}
/* ═══════════════════════════════════════════
PAGE 5 insta_card_memo.png
bg: #f2f2f0
═══════════════════════════════════════════ */
.p5-label-mask {
top: 72px; left: 64px;
width: 200px; height: 52px;
background: #f2f2f0;
padding: 8px;
}
.p5-label-text {
position: absolute;
top: 72px; left: 64px;
width: 200px; height: 52px;
padding: 8px;
font-size: 32px;
font-weight: 700;
color: #2f6ef7;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p5-headline-mask {
top: 160px; left: 56px;
width: 700px; height: 110px;
background: #f2f2f0;
padding: 8px;
}
.p5-headline-text {
position: absolute;
top: 160px; left: 56px;
width: 700px; height: 110px;
padding: 8px;
font-size: 70px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p5-body-mask {
top: 308px; left: 56px;
width: 700px; height: 180px;
background: #f2f2f0;
padding: 8px;
}
.p5-body-text {
position: absolute;
top: 308px; left: 56px;
width: 700px; height: 180px;
padding: 8px;
font-size: 34px;
font-weight: 400;
color: #3a3e50;
word-wrap: break-word;
overflow: hidden;
}
/* ═══════════════════════════════════════════
PAGE 6 insta_card_oneline.png
bg: #f5f4f2
═══════════════════════════════════════════ */
.p6-headline-mask {
top: 188px; left: 96px;
width: 820px; height: 240px;
background: #f5f4f2;
padding: 8px;
}
.p6-headline-text {
position: absolute;
top: 188px; left: 96px;
width: 820px; height: 240px;
padding: 8px;
font-size: 68px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
}
.p6-body-mask {
top: 448px; left: 96px;
width: 620px; height: 120px;
background: #f5f4f2;
padding: 8px;
}
.p6-body-text {
position: absolute;
top: 448px; left: 96px;
width: 620px; height: 120px;
padding: 8px;
font-size: 34px;
font-weight: 400;
color: #5a5e70;
word-wrap: break-word;
overflow: hidden;
}
/* ═══════════════════════════════════════════
PAGE 7 insta_card_checklist.png
bg: #f5f4f2
═══════════════════════════════════════════ */
.p7-headline-mask {
top: 110px; left: 56px;
width: 740px; height: 110px;
background: #f5f4f2;
padding: 8px;
}
.p7-headline-text {
position: absolute;
top: 110px; left: 56px;
width: 740px; height: 110px;
padding: 8px;
font-size: 74px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
/* checklist items 4 rows */
.p7-item1-mask { top: 258px; left: 164px; width: 720px; height: 80px; background: #f5f4f2; padding: 8px; }
.p7-item1-text { position: absolute; top: 258px; left: 164px; width: 720px; height: 80px; padding: 8px; font-size: 40px; font-weight: 500; color: #2a2d3e; word-wrap: break-word; overflow: hidden; display: flex; align-items: center; }
.p7-item2-mask { top: 388px; left: 164px; width: 720px; height: 80px; background: #f5f4f2; padding: 8px; }
.p7-item2-text { position: absolute; top: 388px; left: 164px; width: 720px; height: 80px; padding: 8px; font-size: 40px; font-weight: 500; color: #2a2d3e; word-wrap: break-word; overflow: hidden; display: flex; align-items: center; }
.p7-item3-mask { top: 518px; left: 164px; width: 720px; height: 80px; background: #f5f4f2; padding: 8px; }
.p7-item3-text { position: absolute; top: 518px; left: 164px; width: 720px; height: 80px; padding: 8px; font-size: 40px; font-weight: 500; color: #2a2d3e; word-wrap: break-word; overflow: hidden; display: flex; align-items: center; }
.p7-item4-mask { top: 648px; left: 164px; width: 720px; height: 80px; background: #f5f4f2; padding: 8px; }
.p7-item4-text { position: absolute; top: 648px; left: 164px; width: 720px; height: 80px; padding: 8px; font-size: 40px; font-weight: 500; color: #2a2d3e; word-wrap: break-word; overflow: hidden; display: flex; align-items: center; }
/* ═══════════════════════════════════════════
PAGE 8 insta_card_study.png
bg: #f2f2f0
═══════════════════════════════════════════ */
.p8-label-mask {
top: 72px; left: 64px;
width: 200px; height: 52px;
background: #f2f2f0;
padding: 8px;
}
.p8-label-text {
position: absolute;
top: 72px; left: 64px;
width: 200px; height: 52px;
padding: 8px;
font-size: 32px;
font-weight: 700;
color: #2f6ef7;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p8-headline-mask {
top: 160px; left: 56px;
width: 700px; height: 110px;
background: #f2f2f0;
padding: 8px;
}
.p8-headline-text {
position: absolute;
top: 160px; left: 56px;
width: 700px; height: 110px;
padding: 8px;
font-size: 72px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p8-body-mask {
top: 306px; left: 56px;
width: 700px; height: 180px;
background: #f2f2f0;
padding: 8px;
}
.p8-body-text {
position: absolute;
top: 306px; left: 56px;
width: 700px; height: 180px;
padding: 8px;
font-size: 34px;
font-weight: 400;
color: #3a3e50;
word-wrap: break-word;
overflow: hidden;
}
/* ═══════════════════════════════════════════
PAGE 9 insta_card_cta.png
bg: #f5f4f2
═══════════════════════════════════════════ */
.p9-headline-mask {
top: 182px; left: 56px;
width: 970px; height: 120px;
background: #f5f4f2;
padding: 8px;
}
.p9-headline-text {
position: absolute;
top: 182px; left: 56px;
width: 970px; height: 120px;
padding: 8px;
font-size: 82px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.p9-cta-mask {
top: 332px; left: 180px;
width: 720px; height: 88px;
background: #2244cc;
border-radius: 44px;
padding: 8px;
}
.p9-cta-text {
position: absolute;
top: 332px; left: 180px;
width: 720px; height: 88px;
border-radius: 44px;
padding: 8px;
font-size: 42px;
font-weight: 700;
color: #ffffff;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.p9-body-mask {
top: 980px; left: 56px;
width: 860px; height: 60px;
background: #f5f4f2;
padding: 8px;
}
.p9-body-text {
position: absolute;
top: 980px; left: 56px;
width: 860px; height: 60px;
padding: 8px;
font-size: 30px;
font-weight: 400;
color: #5a5e70;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
/* ═══════════════════════════════════════════
PAGE 10 insta_card_finish.png
bg: #f2f2f0
═══════════════════════════════════════════ */
.p10-label-mask {
top: 72px; left: 64px;
width: 200px; height: 52px;
background: #f2f2f0;
padding: 8px;
}
.p10-label-text {
position: absolute;
top: 72px; left: 64px;
width: 200px; height: 52px;
padding: 8px;
font-size: 32px;
font-weight: 700;
color: #2f6ef7;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p10-headline-mask {
top: 155px; left: 56px;
width: 700px; height: 110px;
background: #f2f2f0;
padding: 8px;
}
.p10-headline-text {
position: absolute;
top: 155px; left: 56px;
width: 700px; height: 110px;
padding: 8px;
font-size: 72px;
font-weight: 900;
color: #1e2235;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
}
.p10-body-mask {
top: 302px; left: 56px;
width: 680px; height: 180px;
background: #f2f2f0;
padding: 8px;
}
.p10-body-text {
position: absolute;
top: 302px; left: 56px;
width: 680px; height: 180px;
padding: 8px;
font-size: 34px;
font-weight: 400;
color: #3a3e50;
word-wrap: break-word;
overflow: hidden;
}
/* checklist icon (page 7) */
.check-icon {
position: absolute;
width: 76px; height: 76px;
background: #3366ee;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.check-icon svg { width: 44px; height: 44px; }
/* quote mark (page 2 & 3) */
.quote-mark {
position: absolute;
font-size: 100px;
font-weight: 900;
color: #ffffff;
line-height: 1;
opacity: 0.95;
}
/* left bar (page 6) */
.left-bar {
position: absolute;
top: 196px; left: 64px;
width: 10px; height: 232px;
background: #7c5ce0;
border-radius: 5px;
}
</style>
</head>
<body>
{% if page_no == 1 %}
<!-- ══════════════════════════════════════
PAGE 1 · COVER · insta_card_start.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_start.png');">
<!-- headline mask + text -->
<div class="mask p1-headline-mask"></div>
<div class="mask p1-headline-text">{{ headline }}</div>
<!-- body mask + text -->
<div class="mask p1-body-mask"></div>
<div class="mask p1-body-text">{{ body }}</div>
<!-- cta mask + text -->
<div class="mask p1-cta-mask"></div>
<div class="mask p1-cta-text">{{ cta }}</div>
</div>
{% endif %}
{% if page_no == 2 %}
<!-- ══════════════════════════════════════
PAGE 2 · insta_card_keyword.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_keyword.png');">
<!-- quote mark mask -->
<div class="mask" style="top:60px;left:48px;width:120px;height:100px;background:#3a3fdb;padding:8px;"></div>
<div class="quote-mark" style="top:52px;left:50px;">"</div>
<!-- headline -->
<div class="mask p2-headline-mask"></div>
<div class="mask p2-headline-text">{{ headline }}</div>
<!-- body -->
<div class="mask p2-body-mask"></div>
<div class="mask p2-body-text">{{ body }}</div>
</div>
{% endif %}
{% if page_no == 3 %}
<!-- ══════════════════════════════════════
PAGE 3 · insta_card_highlight.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_highlight.png');">
<!-- quote mark mask -->
<div class="mask" style="top:60px;left:48px;width:120px;height:100px;background:#3a3fdb;padding:8px;"></div>
<div class="quote-mark" style="top:52px;left:50px;">"</div>
<!-- headline -->
<div class="mask p3-headline-mask"></div>
<div class="mask p3-headline-text">{{ headline }}</div>
<!-- body -->
<div class="mask p3-body-mask"></div>
<div class="mask p3-body-text">{{ body }}</div>
</div>
{% endif %}
{% if page_no == 4 %}
<!-- ══════════════════════════════════════
PAGE 4 · insta_card_observation.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_observation.png');">
<!-- day label -->
<div class="mask p4-label-mask"></div>
<div class="mask p4-label-text">{{ label }}</div>
<!-- headline -->
<div class="mask p4-headline-mask"></div>
<div class="mask p4-headline-text">{{ headline }}</div>
<!-- body -->
<div class="mask p4-body-mask"></div>
<div class="mask p4-body-text">{{ body }}</div>
</div>
{% endif %}
{% if page_no == 5 %}
<!-- ══════════════════════════════════════
PAGE 5 · insta_card_memo.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_memo.png');">
<!-- day label -->
<div class="mask p5-label-mask"></div>
<div class="mask p5-label-text">{{ label }}</div>
<!-- headline -->
<div class="mask p5-headline-mask"></div>
<div class="mask p5-headline-text">{{ headline }}</div>
<!-- body -->
<div class="mask p5-body-mask"></div>
<div class="mask p5-body-text">{{ body }}</div>
</div>
{% endif %}
{% if page_no == 6 %}
<!-- ══════════════════════════════════════
PAGE 6 · insta_card_oneline.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_oneline.png');">
<!-- purple left bar -->
<div class="left-bar"></div>
<!-- headline -->
<div class="mask p6-headline-mask"></div>
<div class="mask p6-headline-text">{{ headline }}</div>
<!-- body -->
<div class="mask p6-body-mask"></div>
<div class="mask p6-body-text">{{ body }}</div>
</div>
{% endif %}
{% if page_no == 7 %}
<!-- ══════════════════════════════════════
PAGE 7 · insta_card_checklist.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_checklist.png');">
<!-- section title -->
<div class="mask p7-headline-mask"></div>
<div class="mask p7-headline-text">{{ headline }}</div>
<!-- check icons -->
<div class="check-icon" style="top:252px;left:56px;">
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="check-icon" style="top:382px;left:56px;">
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="check-icon" style="top:512px;left:56px;">
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="check-icon" style="top:642px;left:56px;">
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<!-- checklist items -->
<div class="mask p7-item1-mask"></div>
<div class="mask p7-item1-text">{{ item1 }}</div>
<div class="mask p7-item2-mask"></div>
<div class="mask p7-item2-text">{{ item2 }}</div>
<div class="mask p7-item3-mask"></div>
<div class="mask p7-item3-text">{{ item3 }}</div>
<div class="mask p7-item4-mask"></div>
<div class="mask p7-item4-text">{{ item4 }}</div>
</div>
{% endif %}
{% if page_no == 8 %}
<!-- ══════════════════════════════════════
PAGE 8 · insta_card_study.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_study.png');">
<!-- day label -->
<div class="mask p8-label-mask"></div>
<div class="mask p8-label-text">{{ label }}</div>
<!-- headline -->
<div class="mask p8-headline-mask"></div>
<div class="mask p8-headline-text">{{ headline }}</div>
<!-- body -->
<div class="mask p8-body-mask"></div>
<div class="mask p8-body-text">{{ body }}</div>
</div>
{% endif %}
{% if page_no == 9 %}
<!-- ══════════════════════════════════════
PAGE 9 · insta_card_cta.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_cta.png');">
<!-- headline -->
<div class="mask p9-headline-mask"></div>
<div class="mask p9-headline-text">{{ headline }}</div>
<!-- cta button -->
<div class="mask p9-cta-mask"></div>
<div class="mask p9-cta-text">{{ cta }}</div>
<!-- body / next episode teaser -->
<div class="mask p9-body-mask"></div>
<div class="mask p9-body-text">{{ body }}</div>
</div>
{% endif %}
{% if page_no == 10 %}
<!-- ══════════════════════════════════════
PAGE 10 · insta_card_finish.png
══════════════════════════════════════ -->
<div class="card" style="background-image: url('pages/insta_card_finish.png');">
<!-- day label -->
<div class="mask p10-label-mask"></div>
<div class="mask p10-label-text">{{ label }}</div>
<!-- headline -->
<div class="mask p10-headline-mask"></div>
<div class="mask p10-headline-text">{{ headline }}</div>
<!-- body -->
<div class="mask p10-body-mask"></div>
<div class="mask p10-body-text">{{ body }}</div>
</div>
{% endif %}
</body>
</html>