@heripo/research-radar 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,6 +5,10 @@ var openai = require('@ai-sdk/openai');
5
5
  var core = require('@llm-newsletter-kit/core');
6
6
  var cheerio = require('cheerio');
7
7
  var TurndownService = require('turndown');
8
+ var jsdom = require('jsdom');
9
+ var safeMarkdown2Html = require('safe-markdown2html');
10
+ var DOMPurify = require('dompurify');
11
+ var juice = require('juice');
8
12
 
9
13
  function _interopNamespaceDefault(e) {
10
14
  var n = Object.create(null);
@@ -1727,6 +1731,74 @@ class AnalysisProvider {
1727
1731
  }
1728
1732
  }
1729
1733
 
1734
+ /**
1735
+ * Shared HTML fragments used by both newsletter and welcome email templates.
1736
+ *
1737
+ * These helpers extract identical HTML blocks to avoid duplication
1738
+ * while keeping template-specific styling separate.
1739
+ */
1740
+ const purify = DOMPurify(new jsdom.JSDOM('').window);
1741
+ /**
1742
+ * Sanitize user-supplied strings for safe HTML insertion.
1743
+ * Strips all HTML tags, leaving only plain text.
1744
+ */
1745
+ const sanitizeText = (str) => purify.sanitize(str, { ALLOWED_TAGS: [] });
1746
+ /**
1747
+ * Heripo light/dark logo block.
1748
+ * @param imgMarginBottom - Margin below the logo image (e.g., '8px', '12px')
1749
+ */
1750
+ const heripoLogoHtml = (imgMarginBottom) => `
1751
+ <div style="margin-bottom: 32px;">
1752
+ <div style="text-align: left; display: block;" class="light-logo">
1753
+ <img src="https://heripo.com/heripo-logo.png" width="150" alt="로고" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; margin-bottom: ${imgMarginBottom};" height="auto">
1754
+ </div>
1755
+ <!--[if !mso]><!-->
1756
+ <div style="text-align: left; display: none;" class="dark-logo">
1757
+ <img src="https://heripo.com/heripo-logo-dark.png" width="150" alt="다크모드 로고" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; margin-bottom: ${imgMarginBottom};" height="auto">
1758
+ </div>
1759
+ <!--<![endif]-->
1760
+ </div>`;
1761
+ /**
1762
+ * KRAS dual-logo header block.
1763
+ * Left: KRAS logo, Right: heripo lab logo (light/dark) + "제공" text.
1764
+ */
1765
+ const krasHeaderHtml = () => `
1766
+ <table cellpadding="0" cellspacing="0" width="100%" role="presentation" style="width: 100%; border-collapse: collapse; margin: 0 0 18px 0; mso-table-lspace: 0pt; mso-table-rspace: 0pt; margin-bottom: 20px; border: none;">
1767
+ <tr>
1768
+ <td align="left" valign="middle" width="50%" style="text-align: left; font-size: 15px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 0; border: none;">
1769
+ <img src="https://heripo.com/kras-logo.jpeg" width="200" alt="한국고고학회" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block;" height="auto">
1770
+ </td>
1771
+ <td align="right" valign="middle" width="50%" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 0; border: none; text-align: right; font-size: 0; line-height: 0; white-space: nowrap;">
1772
+ <div style="text-align: left; display: inline-block; vertical-align: middle; line-height: 0;" class="light-logo">
1773
+ <img src="https://heripo.com/heripolab-logo.png" width="120" alt="heripo lab" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; display: inline-block; vertical-align: middle;" height="auto">
1774
+ </div><!--[if !mso]><!--><div style="text-align: left; display: none; vertical-align: middle; line-height: 0;" class="dark-logo dark-logo-inline">
1775
+ <img src="https://heripo.com/heripolab-logo-dark.png" width="120" alt="heripo lab" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; display: inline-block; vertical-align: middle;" height="auto">
1776
+ </div><!--<![endif]--><span class="header-dark-text" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; color: #666666; line-height: 1; vertical-align: middle; padding-left: 4px;">제공</span>
1777
+ </td>
1778
+ </tr>
1779
+ </table>`;
1780
+ /**
1781
+ * Heripo platform introduction section.
1782
+ * Shared between newsletter and welcome email templates.
1783
+ *
1784
+ * Note: Each template may append its own additional paragraph after this block
1785
+ * (e.g., newsletter adds a line about source requests via GitHub Issues).
1786
+ */
1787
+ const platformIntroHtml = () => `
1788
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">heripo는 고고학 연구 환경의 실질적인 디지털 전환을 지향하는 연구 플랫폼입니다.</p>
1789
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">발굴조사보고서(PDF) 속에 갇힌 텍스트와 도면을 분석 가능한 구조화된 데이터로 전환하여, 연구자가 자료를 보다 체계적으로 탐색하고 재사용할 수 있는 인프라를 구축하고 있습니다.</p>
1790
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
1791
+ color: #444444; margin: 0 0 18px 0;">현재는 소프트웨어 엔지니어와 고고학 연구자가 함께하는 <strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>으로 운영 중이며, 2026년 1월 28일 핵심 엔진을 <strong><a href="https://github.com/heripo-lab/heripo-engine" target="_blank">오픈소스로 공개</a></strong>했습니다.</p>
1792
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">오픈소스로 공개된 핵심 기능은 <strong><a href="https://engine-demo.heripo.com" target="_blank">데모 사이트</a></strong>에서 직접 체험해 보실 수 있으며, 플랫폼 프로토타입 출시 시 구독자분들께 우선 안내해 드리겠습니다.</p>`;
1793
+ /**
1794
+ * "Powered by LLM Newsletter Kit · View Source" footer line.
1795
+ */
1796
+ const poweredByFooterHtml = () => `
1797
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #999999; margin: 0 0 12px 0;" class="footer-text">
1798
+ Powered by <a href="https://github.com/heripo-lab/llm-newsletter-kit-core" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; transition: color 0.2s; color: #999999; text-decoration: underline;" class="footer-link">LLM Newsletter Kit</a> ·
1799
+ <a href="https://github.com/heripo-lab/heripo-research-radar" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; transition: color 0.2s; color: #999999; text-decoration: underline;" class="footer-link">View Source</a>
1800
+ </p>`;
1801
+
1730
1802
  /**
1731
1803
  * Creates an HTML template for the newsletter email
1732
1804
  *
@@ -1739,16 +1811,18 @@ class AnalysisProvider {
1739
1811
  * - Platform introduction
1740
1812
  *
1741
1813
  * @param targets - Array of crawling targets to be listed in the newsletter footer
1814
+ * @param options - Optional template customization options
1742
1815
  * @returns Complete HTML string for the newsletter email
1743
1816
  *
1744
1817
  * @example
1745
1818
  * ```typescript
1746
- * const html = createNewsletterHtmlTemplate([
1747
- * { id: '1', name: 'Source 1', url: 'https://example.com', ... }
1748
- * ]);
1819
+ * const html = createNewsletterHtmlTemplate(
1820
+ * [{ id: '1', name: 'Source 1', url: 'https://example.com', ... }],
1821
+ * { isKrasNewsletter: true, krasNewsMarkdown: '## News...' },
1822
+ * );
1749
1823
  * ```
1750
1824
  */
1751
- const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
1825
+ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
1752
1826
  <html lang="ko" style="color-scheme: light dark; supported-color-schemes: light dark;">
1753
1827
  <head>
1754
1828
  <meta charset="UTF-8">
@@ -2126,6 +2200,18 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2126
2200
  background-color: #23201c !important;
2127
2201
  }
2128
2202
 
2203
+
2204
+ .header-dark-text {
2205
+ color: #eeeeee !important;
2206
+ }
2207
+
2208
+ .header-title-border {
2209
+ border-bottom-color: #E59866 !important;
2210
+ }
2211
+
2212
+ .dark-logo-inline {
2213
+ display: inline-block !important;
2214
+ }
2129
2215
  }
2130
2216
  </style>
2131
2217
  </head>
@@ -2141,16 +2227,50 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2141
2227
  <table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 800px;" class="container" role="presentation">
2142
2228
  <tr>
2143
2229
  <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 44px 44px 36px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
2144
- <div style="margin-bottom: 32px;">
2145
- <div style="text-align: left; display: block;" class="light-logo">
2146
- <img src="https://heripo.com/heripo-logo.png" width="150" alt="로고" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; margin-bottom: 12px;" height="auto">
2147
- </div>
2148
- <!--[if !mso]><!-->
2149
- <div style="text-align: left; display: none;" class="dark-logo">
2150
- <img src="https://heripo.com/heripo-logo-dark.png" width="150" alt="다크모드 로고" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; margin-bottom: 12px;" height="auto">
2151
- </div>
2152
- <!--<![endif]-->
2153
- </div>
2230
+ ${options?.isKrasNewsletter
2231
+ ? `${krasHeaderHtml()}
2232
+ <!-- 헤더: 제목/날짜 -->
2233
+ <table cellpadding="0" cellspacing="0" width="100%" role="presentation" class="header-title-border" style="width: 100%; border-collapse: collapse; margin: 0 0 18px 0; mso-table-lspace: 0pt; mso-table-rspace: 0pt; margin-bottom: 32px; border: none; border-bottom: 3px solid #D2691E;">
2234
+ <tr>
2235
+ <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 26px; font-weight: bold; color: #111111; line-height: 1.2; border: none;">
2236
+ 한국고고학회 뉴스레터
2237
+ </td>
2238
+ <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; color: #333333; white-space: nowrap; border: none; width: 1px;" width="1">
2239
+ ${options?.displayDate ?? ''}
2240
+ </td>
2241
+ </tr>
2242
+ </table>
2243
+ `
2244
+ : `${heripoLogoHtml('12px')}
2245
+ `}
2246
+
2247
+ ${options?.krasNewsMarkdown
2248
+ ? safeMarkdown2Html(`## 학회 소식
2249
+ ${options.krasNewsMarkdown}
2250
+
2251
+ ---
2252
+ `, {
2253
+ window: new jsdom.JSDOM('').window,
2254
+ linkTargetBlank: true,
2255
+ fixMalformedUrls: true,
2256
+ fixBoldSyntax: true,
2257
+ convertStrikethrough: true,
2258
+ })
2259
+ : ''}
2260
+
2261
+ ${options?.heripolabNewsMarkdown
2262
+ ? safeMarkdown2Html(`## heripo lab 소식
2263
+ ${options.heripolabNewsMarkdown}
2264
+
2265
+ ---
2266
+ `, {
2267
+ window: new jsdom.JSDOM('').window,
2268
+ linkTargetBlank: true,
2269
+ fixMalformedUrls: true,
2270
+ fixBoldSyntax: true,
2271
+ convertStrikethrough: true,
2272
+ })
2273
+ : ''}
2154
2274
 
2155
2275
  {{NEWSLETTER_CONTENT}}
2156
2276
 
@@ -2166,7 +2286,7 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2166
2286
  </ul>
2167
2287
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
2168
2288
  <h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;">📅 발행 정책</h2>
2169
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong>heripo 리서치 레이더</strong>는 매일 발행을 원칙으로 하되, 독자분들께 의미 있는 정보를 제공하기 위해 다음과 같은 발행 기준을 적용합니다:</p>
2289
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong>${options?.isKrasNewsletter ? '한국고고학회' : 'heripo 리서치 레이더'}</strong>는 매일 발행을 원칙으로 하되, 독자분들께 의미 있는 정보를 제공하기 위해 다음과 같은 발행 기준을 적용합니다:</p>
2170
2290
  <ul style="padding-left: 24px; margin: 0 0 18px 0;">
2171
2291
  <li style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 8px;"><strong>정상 발행</strong>: 새로운 소식이 ${newsletterConfig.publicationCriteria.minimumArticleCountForIssue + 1}개 이상이거나, ${newsletterConfig.publicationCriteria.minimumArticleCountForIssue}개 이하여도 중요도 ${newsletterConfig.publicationCriteria.priorityArticleScoreThreshold}점 이상의 핵심 소식이 포함된 경우</li>
2172
2292
  <li style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 8px;"><strong>이월 발행</strong>: 새로운 소식이 ${newsletterConfig.publicationCriteria.minimumArticleCountForIssue}개 이하이면서 중요한 내용(${newsletterConfig.publicationCriteria.priorityArticleScoreThreshold}점 이상)이 없을 경우, 다음 호로 이월하여 더 풍성한 내용으로 제공</li>
@@ -2175,11 +2295,7 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2175
2295
  <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">이러한 정책을 통해 매일 의미 없는 소식으로 독자분들의 시간을 낭비하지 않고, 정말 중요한 정보를 적절한 타이밍에 제공하고자 합니다.</p>
2176
2296
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
2177
2297
  <h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;">🔍 heripo(헤리포) 플랫폼 소개</h2>
2178
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">heripo는 고고학 연구 환경의 실질적인 디지털 전환을 지향하는 연구 플랫폼입니다.</p>
2179
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">발굴조사보고서(PDF) 속에 갇힌 텍스트와 도면을 분석 가능한 구조화된 데이터로 전환하여, 연구자가 자료를 보다 체계적으로 탐색하고 재사용할 수 있는 인프라를 구축하고 있습니다.</p>
2180
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
2181
- color: #444444; margin: 0 0 18px 0;">현재는 소프트웨어 엔지니어와 고고학 연구자가 함께하는 <strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>으로 운영 중이며, 2026년 1월 28일 핵심 엔진을 <strong><a href="https://github.com/heripo-lab/heripo-engine" target="_blank">오픈소스로 공개</a></strong>했습니다.</p>
2182
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">오픈소스로 공개된 핵심 기능은 <strong><a href="https://engine-demo.heripo.com" target="_blank">데모 사이트</a></strong>에서 직접 체험해 보실 수 있으며, 플랫폼 프로토타입 출시 시 구독자분들께 우선 안내해 드리겠습니다.</p>
2298
+ ${platformIntroHtml()}
2183
2299
  <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
2184
2300
  color: #444444; margin: 0 0 18px 0;">보고 계신 뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>
2185
2301
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
@@ -2192,12 +2308,9 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2192
2308
  <tr>
2193
2309
  <td align="center" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 30px 20px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.5; color: #888888;">
2194
2310
  <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 10px 0;" class="footer-text">heripo lab | newsletter@heripo.com</p>
2195
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 10px 0;" class="footer-text">이 메일은 heripo.com에서 리서치 레이더를 구독하신 분들에게 발송됩니다.<br>
2311
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 10px 0;" class="footer-text">${options?.isKrasNewsletter ? '이 메일은 heripo.com에서 뉴스레터를 구독하신 분들과 한국고고학회 회원에게 발송됩니다.' : '이 메일은 heripo.com에서 리서치 레이더를 구독하신 분들에게 발송됩니다.'}<br>
2196
2312
  더 이상 이메일을 받고 싶지 않으시면 <a href="{{{RESEND_UNSUBSCRIBE_URL}}}" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-weight: bold; transition: color 0.2s; color: #888888; text-decoration: underline;" class="footer-link">여기에서 수신 거부</a>하세요.</p>
2197
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #999999; margin: 0;" class="footer-text">
2198
- Powered by <a href="https://github.com/heripo-lab/llm-newsletter-kit-core" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; transition: color 0.2s; color: #999999; text-decoration: underline;" class="footer-link">LLM Newsletter Kit</a> ·
2199
- <a href="https://github.com/heripo-lab/heripo-research-radar" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; transition: color 0.2s; color: #999999; text-decoration: underline;" class="footer-link">View Source</a>
2200
- </p>
2313
+ ${poweredByFooterHtml()}
2201
2314
  </td>
2202
2315
  </tr>
2203
2316
  </table>
@@ -2224,16 +2337,26 @@ class ContentGenerateProvider {
2224
2337
  newsletterRepository;
2225
2338
  _issueOrder = null;
2226
2339
  model;
2227
- constructor(google, articleRepository, newsletterRepository) {
2340
+ /** HTML template with markers for title and content injection */
2341
+ htmlTemplate;
2342
+ /** Newsletter brand name (defaults to config, can be overridden via constructor) */
2343
+ newsletterBrandName;
2344
+ constructor(google, articleRepository, newsletterRepository, templateOptions, brandName) {
2228
2345
  this.google = google;
2229
2346
  this.articleRepository = articleRepository;
2230
2347
  this.newsletterRepository = newsletterRepository;
2231
2348
  this.model = this.google('gemini-3-pro-preview');
2349
+ this.newsletterBrandName = brandName ?? newsletterConfig.brandName;
2350
+ this.htmlTemplate = {
2351
+ html: createNewsletterHtmlTemplate(crawlingTargetGroups.flatMap((group) => group.targets), templateOptions),
2352
+ markers: {
2353
+ title: 'NEWSLETTER_TITLE',
2354
+ content: 'NEWSLETTER_CONTENT',
2355
+ },
2356
+ };
2232
2357
  }
2233
2358
  /** LLM temperature setting for content generation */
2234
2359
  temperature = llmConfig.generation.temperature;
2235
- /** Newsletter brand name */
2236
- newsletterBrandName = newsletterConfig.brandName;
2237
2360
  /** Subscribe page URL */
2238
2361
  subscribePageUrl = newsletterConfig.subscribePageUrl;
2239
2362
  /** Publication criteria (minimum article count, priority score threshold) */
@@ -2261,14 +2384,6 @@ class ContentGenerateProvider {
2261
2384
  async fetchArticleCandidates() {
2262
2385
  return this.articleRepository.findCandidatesForNewsletter();
2263
2386
  }
2264
- /** HTML template with markers for title and content injection */
2265
- htmlTemplate = {
2266
- html: createNewsletterHtmlTemplate(crawlingTargetGroups.flatMap((group) => group.targets)),
2267
- markers: {
2268
- title: 'NEWSLETTER_TITLE',
2269
- content: 'NEWSLETTER_CONTENT',
2270
- },
2271
- };
2272
2387
  /**
2273
2388
  * Save generated newsletter to the repository
2274
2389
  * @param input - Newsletter data and used articles
@@ -2317,8 +2432,35 @@ class CrawlingProvider {
2317
2432
  * Date service implementation
2318
2433
  * - Provides current date and display date strings
2319
2434
  * - Always returns Korea Standard Time (KST, Asia/Seoul) regardless of server timezone
2435
+ * - Accepts optional publishDate to override the current date (e.g., for next-day publishing)
2320
2436
  */
2321
2437
  class DateService {
2438
+ targetDate;
2439
+ /**
2440
+ * @param publishDate - Optional ISO date string (YYYY-MM-DD) to use instead of current date.
2441
+ * When provided, the newsletter will use this date as its publication date.
2442
+ * @throws {Error} If publishDate is not in YYYY-MM-DD format or is not a real calendar date.
2443
+ */
2444
+ constructor(publishDate) {
2445
+ if (publishDate !== undefined) {
2446
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(publishDate)) {
2447
+ throw new Error(`Invalid publishDate format: "${publishDate}". Expected YYYY-MM-DD (e.g., "2025-02-12").`);
2448
+ }
2449
+ const date = new Date(publishDate + 'T00:00:00+09:00');
2450
+ // Round-trip check: format back to YYYY-MM-DD in KST and compare.
2451
+ // Catches invalid dates like "2025-02-30" that JS silently normalizes to "2025-03-02".
2452
+ const roundTrip = date.toLocaleDateString('en-CA', {
2453
+ timeZone: 'Asia/Seoul',
2454
+ });
2455
+ if (roundTrip !== publishDate) {
2456
+ throw new Error(`Invalid publishDate: "${publishDate}" is not a real calendar date.`);
2457
+ }
2458
+ this.targetDate = date;
2459
+ }
2460
+ else {
2461
+ this.targetDate = new Date();
2462
+ }
2463
+ }
2322
2464
  /**
2323
2465
  * Get current date in ISO format (YYYY-MM-DD)
2324
2466
  * - Always returns date in Korea Standard Time (UTC+9)
@@ -2327,7 +2469,7 @@ class DateService {
2327
2469
  getCurrentISODateString() {
2328
2470
  // Use Intl.DateTimeFormat to get date in Korea timezone
2329
2471
  // 'en-CA' locale returns YYYY-MM-DD format by default
2330
- const kstDate = new Date().toLocaleDateString('en-CA', {
2472
+ const kstDate = this.targetDate.toLocaleDateString('en-CA', {
2331
2473
  timeZone: 'Asia/Seoul',
2332
2474
  });
2333
2475
  return kstDate;
@@ -2344,7 +2486,7 @@ class DateService {
2344
2486
  month: 'long',
2345
2487
  day: 'numeric',
2346
2488
  });
2347
- return formatter.format(new Date());
2489
+ return formatter.format(this.targetDate);
2348
2490
  }
2349
2491
  }
2350
2492
 
@@ -2418,13 +2560,31 @@ function createNewsletterGenerator(dependencies) {
2418
2560
  const google$1 = google.createGoogleGenerativeAI({
2419
2561
  apiKey: dependencies.googleGenerativeAIApiKey,
2420
2562
  });
2421
- const dateService = new DateService();
2563
+ const dateService = new DateService(dependencies.publishDate);
2422
2564
  const taskService = new TaskService(dependencies.taskRepository);
2423
2565
  const crawlingProvider = new CrawlingProvider(dependencies.articleRepository);
2424
2566
  const analysisProvider = new AnalysisProvider(openai$1, dependencies.articleRepository, dependencies.tagRepository);
2425
- const contentGenerateProvider = new ContentGenerateProvider(google$1, dependencies.articleRepository, dependencies.newsletterRepository);
2567
+ // Inject display date from DateService into template options
2568
+ const templateOptions = dependencies.templateOptions
2569
+ ? {
2570
+ ...dependencies.templateOptions,
2571
+ displayDate: dateService.getDisplayDateString(),
2572
+ }
2573
+ : undefined;
2574
+ let resolvedContentOptions = { ...contentOptions };
2575
+ let resolvedBrandName = newsletterConfig.brandName;
2576
+ if (templateOptions?.isKrasNewsletter) {
2577
+ resolvedContentOptions = {
2578
+ ...resolvedContentOptions,
2579
+ expertField: ['고고학 우선적 문화유산'],
2580
+ freeFormIntro: true,
2581
+ titleContext: templateOptions.titleContext || undefined,
2582
+ };
2583
+ resolvedBrandName = '한국고고학회 뉴스레터';
2584
+ }
2585
+ const contentGenerateProvider = new ContentGenerateProvider(google$1, dependencies.articleRepository, dependencies.newsletterRepository, templateOptions, resolvedBrandName);
2426
2586
  return new core.GenerateNewsletter({
2427
- contentOptions,
2587
+ contentOptions: resolvedContentOptions,
2428
2588
  dateService,
2429
2589
  taskService,
2430
2590
  crawlingProvider,
@@ -2467,6 +2627,259 @@ async function generateNewsletter(dependencies) {
2467
2627
  return generator.generate();
2468
2628
  }
2469
2629
 
2630
+ /**
2631
+ * Generates a welcome email HTML string with CSS inlined via juice.
2632
+ *
2633
+ * API is designed to match the original heripo-web `generateWelcomeHTML(id, name)` usage,
2634
+ * with an optional third parameter for KRAS mode and site URL override.
2635
+ *
2636
+ * @param id - Subscriber ID (used for unsubscribe links in default mode)
2637
+ * @param name - Subscriber display name
2638
+ * @param options - Optional configuration for KRAS mode and site URL
2639
+ * @returns Complete HTML string with CSS inlined (ready to send as email)
2640
+ *
2641
+ * @example
2642
+ * ```typescript
2643
+ * // Default heripo branding (same as original heripo-web usage):
2644
+ * const html = generateWelcomeHTML('subscriber-123', '홍길동');
2645
+ *
2646
+ * // KRAS mode:
2647
+ * const krasHtml = generateWelcomeHTML('subscriber-123', '홍길동', {
2648
+ * isKrasNewsletter: true,
2649
+ * });
2650
+ * ```
2651
+ */
2652
+ function generateWelcomeHTML(id, name, options) {
2653
+ const isKras = options?.isKrasNewsletter ?? false;
2654
+ const siteUrl = options?.siteUrl ?? 'https://heripo.com';
2655
+ const safeName = sanitizeText(name);
2656
+ const unsubscribeUrl = isKras
2657
+ ? '{{{RESEND_UNSUBSCRIBE_URL}}}'
2658
+ : `${siteUrl}/research-radar/unsubscribe?id=${id}`;
2659
+ return juice(createWelcomeHtmlRaw(safeName, isKras, siteUrl, unsubscribeUrl));
2660
+ }
2661
+ function createWelcomeHtmlRaw(name, isKras, siteUrl, unsubscribeUrl) {
2662
+ const title = isKras
2663
+ ? '한국고고학회 뉴스레터 구독 완료'
2664
+ : 'heripo 리서치 레이더 구독 완료';
2665
+ const headerHtml = isKras
2666
+ ? `${krasHeaderHtml()}
2667
+ <h1 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.2; margin: 0 0
2668
+ 18px 0; letter-spacing: -0.5px; margin-top: 0; font-size: 32px; font-weight: bold; color: #111111; border-bottom: 3px solid #D2691E; padding-bottom: 8px;">${name}님, 한국고고학회 뉴스레터를 구독해주셔서 감사합니다.</h1>`
2669
+ : `${heripoLogoHtml('8px')}
2670
+
2671
+ <h1 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.2; margin: 0 0
2672
+ 18px 0; letter-spacing: -0.5px; margin-top: 0; font-size: 32px; font-weight: bold; color: #111111; border-bottom: 3px solid #D2691E; padding-bottom: 8px;">${name}님, heripo 리서치 레이더에 오신 것을 환영합니다!</h1>`;
2673
+ const feedbackHeading = `${name}님의 목소리가 heripo의 미래를 만듭니다`;
2674
+ const feedbackText = 'heripo';
2675
+ const newsletterLine = isKras
2676
+ ? `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>`
2677
+ : `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>`;
2678
+ const warningHtml = isKras
2679
+ ? `
2680
+ <blockquote style="background-color: #fef2f2; border-left: 5px solid #dc2626; margin: 24px 0; padding: 20px; border-radius: 4px;">
2681
+ <h3 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: bold; line-height: 1.3; color: #dc2626; margin: 0 0 10px 0; letter-spacing: -0.1px;">⚠️ 본인이 신청하지 않으셨다면</h3>
2682
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 10px;">만약 본인이 직접 구독 신청을 하지 않으셨다면, 다른 분이 실수로 이메일 주소를 입력했을 가능성이 있습니다. 이 경우 아래 링크를 통해 즉시 수신을 거부하실 수 있습니다.</p>
2683
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0;"><a href="${unsubscribeUrl}" style="color: #dc2626; font-weight: bold;">🚫 수신 거부하기</a></p>
2684
+ </blockquote>`
2685
+ : `
2686
+ <blockquote style="background-color: #fef2f2; border-left: 5px solid #dc2626; margin: 24px 0; padding: 20px; border-radius: 4px;">
2687
+ <h3 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: bold; line-height: 1.3; color: #dc2626; margin: 0 0 10px 0; letter-spacing: -0.1px;">⚠️ 본인이 신청하지 않으셨다면</h3>
2688
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 10px;">만약 본인이 직접 구독 신청을 하지 않으셨다면, 다른 분이 실수로 이메일 주소를 입력했을 가능성이 있습니다. 이 경우 아래 링크를 통해 즉시 수신을 거부하실 수 있습니다.</p>
2689
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0;"><a href="${unsubscribeUrl}" style="color: #dc2626; font-weight: bold;">🚫 수신 거부하기</a></p>
2690
+ </blockquote>`;
2691
+ const footerDisclaimerText = isKras
2692
+ ? '이 이메일은 heripo.com에서 한국고고학회 뉴스레터를 구독하신 분들에게 발송됩니다.'
2693
+ : '이 이메일은 heripo.com에서 리서치 레이더를 구독하신 분들에게 발송됩니다.';
2694
+ const footerUnsubscribeHtml = isKras
2695
+ ? `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📱 구독 관리: <a href="${unsubscribeUrl}" class="footer-link">구독 해지</a></p>`
2696
+ : `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📱 구독 관리: <a href="${unsubscribeUrl}" class="footer-link">구독 해지</a></p>`;
2697
+ return `<!DOCTYPE html>
2698
+ <html lang="ko" style="color-scheme: light dark; supported-color-schemes: light dark;">
2699
+ <head>
2700
+ <meta charset="UTF-8">
2701
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2702
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
2703
+ <meta name="color-scheme" content="light dark">
2704
+ <meta name="supported-color-schemes" content="light dark">
2705
+ <title>${title}</title>
2706
+ <style type="text/css">
2707
+ a:hover {
2708
+ color: #D2691E;
2709
+ }
2710
+ .button-link {
2711
+ color: #fff !important;
2712
+ text-decoration: none !important;
2713
+ font-weight: bold;
2714
+ font-size: 16px;
2715
+ display: inline-block;
2716
+ padding: 10px 28px;
2717
+ border-radius: 4px;
2718
+ background: #D2691E;
2719
+ border: none;
2720
+ }
2721
+ .button-link:hover {
2722
+ background: #b85a1a;
2723
+ }
2724
+ @media screen and (max-width: 800px) {
2725
+ .container {
2726
+ width: 100% !important;
2727
+ max-width: 100% !important;
2728
+ padding: 0 !important;
2729
+ }
2730
+
2731
+ .content-cell {
2732
+ padding: 20px !important;
2733
+ }
2734
+ }
2735
+ @media screen and (max-width: 600px) {
2736
+ h1 {
2737
+ font-size: 24px !important;
2738
+ }
2739
+
2740
+ h2 {
2741
+ font-size: 20px !important;
2742
+ }
2743
+ }
2744
+ @media (prefers-color-scheme: dark) {
2745
+ body,
2746
+ .dark-mode-bg {
2747
+ background-color: #121212 !important;
2748
+ }
2749
+
2750
+ .dark-mode-content-bg {
2751
+ background-color: #1e1e1e !important;
2752
+ box-shadow: 0 4px 10px rgba(0,0,0,0.25) !important;
2753
+ }
2754
+
2755
+ h1,
2756
+ h2,
2757
+ h3,
2758
+ h4,
2759
+ h5,
2760
+ h6 {
2761
+ color: #FFFFFF !important;
2762
+ }
2763
+
2764
+ h2,
2765
+ h3,
2766
+ h4 {
2767
+ background: #1e1e1e !important;
2768
+ }
2769
+
2770
+ p,
2771
+ li {
2772
+ color: #FFFFFF !important;
2773
+ }
2774
+
2775
+ a:not(.button-link) {
2776
+ color: #4da6ff !important;
2777
+ text-decoration: underline !important;
2778
+ }
2779
+
2780
+ a.button-link {
2781
+ color: #fff !important;
2782
+ }
2783
+
2784
+ blockquote {
2785
+ background-color: #2b2b2b !important;
2786
+ }
2787
+
2788
+ blockquote p {
2789
+ color: #bbbbbb !important;
2790
+ }
2791
+
2792
+ .footer-text {
2793
+ color: #999999 !important;
2794
+ }
2795
+
2796
+ .footer-link {
2797
+ color: #999999 !important;
2798
+ text-decoration: underline !important;
2799
+ }
2800
+
2801
+ .dark-logo {
2802
+ display: block !important;
2803
+ }
2804
+
2805
+ .light-logo {
2806
+ display: none !important;
2807
+ }
2808
+
2809
+ .welcome-notice {
2810
+ background-color: #2b2b2b !important;
2811
+ }
2812
+
2813
+ .header-dark-text {
2814
+ color: #eeeeee !important;
2815
+ }
2816
+
2817
+ .dark-logo-inline {
2818
+ display: inline-block !important;
2819
+ }
2820
+ }
2821
+ </style>
2822
+ </head>
2823
+ <body style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f4; font-size: 16px; line-height: 1.7; letter-spacing: 0.01em; height: 100%; width: 100%; margin: 0; padding: 0;">
2824
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
2825
+ <tr>
2826
+ <td bgcolor="#f4f4f4" align="center" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 20px 0;" class="dark-mode-bg">
2827
+ <!--[if (gte mso 9)|(IE)]>
2828
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="800">
2829
+ <tr>
2830
+ <td align="center" valign="top" width="800">
2831
+ <![endif]-->
2832
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 800px;" class="container" role="presentation">
2833
+ <tr>
2834
+ <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 48px 44px 44px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
2835
+ ${headerHtml}
2836
+
2837
+ <h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">💬 ${feedbackHeading}</h2>
2838
+
2839
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">가장 큰 응원은 ${feedbackText}를 직접 사용해보시고, 솔직한 피드백을 주시는 것입니다.</p>
2840
+
2841
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong style="color: #D2691E; font-weight: bold;">"이런 기능이 있다면 좋겠다"</strong> 혹은 <strong style="color: #D2691E; font-weight: bold;">"이런 점은 불편하다"</strong>와 같은 의견을 언제든 보내주세요.</p>
2842
+
2843
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">여러분의 피드백 하나하나가 ${feedbackText}의 다음 발걸음을 결정합니다.</p>
2844
+ ${isKras
2845
+ ? `
2846
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>은 한국고고학회와 함께 뉴스레터 발행 및 고고학의 디지털 전환을 추진하고 있습니다. 앞으로도 연구 현장에 실질적으로 도움이 되는 정보와 기술을 제공해 드리겠습니다.</p>
2847
+ `
2848
+ : ''}
2849
+ <hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 28px 0 20px;">
2850
+
2851
+ <h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">🔍 heripo(헤리포) 플랫폼 소개</h2>
2852
+ ${platformIntroHtml()}
2853
+ ${newsletterLine}
2854
+ ${warningHtml}
2855
+
2856
+ <hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 32px 0;">
2857
+
2858
+ <div style="color: #6b7280; font-size: 14px;" class="footer-text">
2859
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📧 피드백 및 문의: <a href="${siteUrl}/contact" class="footer-link">문의하기</a></p>
2860
+ ${footerUnsubscribeHtml}
2861
+
2862
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 15px;"><em>정보 검색에 쏟던 시간을 연구와 창의적 기획에 집중하는 시간으로 바꿔보세요.</em></p>
2863
+
2864
+ ${poweredByFooterHtml()}
2865
+
2866
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.7; color: #9ca3af; margin: 0;">${footerDisclaimerText}<br>더 이상 이메일을 받고 싶지 않으시면 <a href="${unsubscribeUrl}" target="_blank" style="color: #888888; text-decoration: underline;" class="footer-link">여기에서 수신 거부</a>하세요.</p>
2867
+ </div>
2868
+ </td>
2869
+ </tr>
2870
+ </table>
2871
+ <!--[if (gte mso 9)|(IE)]>
2872
+ </td>
2873
+ </tr>
2874
+ </table>
2875
+ <![endif]-->
2876
+ </td>
2877
+ </tr>
2878
+ </table>
2879
+ </body>
2880
+ </html>`;
2881
+ }
2882
+
2470
2883
  exports.AnalysisProvider = AnalysisProvider;
2471
2884
  exports.ContentGenerateProvider = ContentGenerateProvider;
2472
2885
  exports.CrawlingProvider = CrawlingProvider;
@@ -2475,6 +2888,7 @@ exports.TaskService = TaskService;
2475
2888
  exports.contentOptions = contentOptions;
2476
2889
  exports.crawlingTargetGroups = crawlingTargetGroups;
2477
2890
  exports.generateNewsletter = generateNewsletter;
2891
+ exports.generateWelcomeHTML = generateWelcomeHTML;
2478
2892
  exports.llmConfig = llmConfig;
2479
2893
  exports.newsletterConfig = newsletterConfig;
2480
2894
  //# sourceMappingURL=index.cjs.map