"""
novel_blog_converter.py
소설 연재 파이프라인 — 에피소드 → Blogger-ready HTML 변환 모듈
역할: 에피소드 dict + 소설 설정 → 장르별 테마 HTML 생성
출력: data/novels/{novel_id}/episodes/ep{N:03d}_blog.html
"""
import json
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
BASE_DIR = Path(__file__).parent.parent.parent
sys.path.insert(0, str(BASE_DIR / 'bots'))
logger = logging.getLogger(__name__)
BLOG_BASE_URL = 'https://the4thpath.com'
# ─── 장르별 컬러 테마 ─────────────────────────────────────────────────────────
GENRE_THEMES = {
'sci-fi': {
'bg': '#0a0f1e',
'accent': '#00bcd4',
'accent_dim': '#007c8c',
'card_bg': '#0e1628',
'text': '#cfe8ef',
'meta': '#6fa8bc',
'nav_bg': '#0c1220',
},
'thriller': {
'bg': '#0a0a0d',
'accent': '#bf3a3a',
'accent_dim': '#8a2222',
'card_bg': '#141418',
'text': '#e8e0e0',
'meta': '#a08080',
'nav_bg': '#111115',
},
'fantasy': {
'bg': '#0f0a1e',
'accent': '#c8a84e',
'accent_dim': '#8a7030',
'card_bg': '#180f2e',
'text': '#e8e0f0',
'meta': '#9a8ab0',
'nav_bg': '#130c22',
},
'romance': {
'bg': '#ffffff',
'accent': '#d85a30',
'accent_dim': '#a04020',
'card_bg': '#fff5f0',
'text': '#2a1a14',
'meta': '#8a5a4a',
'nav_bg': '#fff0ea',
},
'default': {
'bg': '#0a0a0d',
'accent': '#c8a84e',
'accent_dim': '#8a7030',
'card_bg': '#141418',
'text': '#e8e0d0',
'meta': '#a09070',
'nav_bg': '#111115',
},
}
def _get_theme(genre: str) -> dict:
"""장르 문자열에서 테마 결정 (부분 매칭 포함)"""
genre_lower = genre.lower()
for key in GENRE_THEMES:
if key in genre_lower:
return GENRE_THEMES[key]
return GENRE_THEMES['default']
def _build_json_ld(episode: dict, novel_config: dict, post_url: str = '') -> str:
"""Schema.org Article JSON-LD 생성"""
schema = {
'@context': 'https://schema.org',
'@type': 'Article',
'headline': f"{novel_config.get('title_ko', '')} {episode.get('episode_num', 0)}화 — {episode.get('title', '')}",
'description': episode.get('hook', ''),
'datePublished': datetime.now(timezone.utc).isoformat(),
'dateModified': datetime.now(timezone.utc).isoformat(),
'author': {
'@type': 'Person',
'name': 'The 4th Path'
},
'publisher': {
'@type': 'Organization',
'name': 'The 4th Path',
'logo': {
'@type': 'ImageObject',
'url': f'{BLOG_BASE_URL}/logo.png'
}
},
'mainEntityOfPage': {
'@type': 'WebPage',
'@id': post_url or BLOG_BASE_URL
},
'genre': novel_config.get('genre', ''),
'isPartOf': {
'@type': 'CreativeWorkSeries',
'name': novel_config.get('title_ko', ''),
'position': episode.get('episode_num', 0)
}
}
return (
''
)
def _body_to_html(body_text: str) -> str:
"""소설 본문 텍스트 → HTML 단락 변환 (빈 줄 기준 분리)"""
paragraphs = []
for para in body_text.split('\n\n'):
para = para.strip()
if not para:
continue
# 대화문 들여쓰기 처리
lines = para.split('\n')
html_lines = []
for line in lines:
line = line.strip()
if not line:
continue
# HTML 특수문자 이스케이프
line = (line.replace('&', '&')
.replace('<', '<')
.replace('>', '>'))
# 대화문 (따옴표 시작) 스타일 적용
if line.startswith('"') or line.startswith('"') or line.startswith('"'):
html_lines.append(
f'{line}'
)
else:
html_lines.append(line)
paragraphs.append('
\n'.join(html_lines))
return '\n'.join(
f'
{p}
' for p in paragraphs if p ) def convert( episode: dict, novel_config: dict, prev_url: str = '', next_url: str = '', save_file: bool = True ) -> str: """ 에피소드 + 소설 설정 → Blogger-ready HTML. data/novels/{novel_id}/episodes/ep{N:03d}_blog.html 저장. 반환: HTML 문자열 """ novel_id = novel_config.get('novel_id', episode.get('novel_id', 'unknown')) ep_num = episode.get('episode_num', 0) title = episode.get('title', f'에피소드 {ep_num}') body_text = episode.get('body', '') hook = episode.get('hook', '') genre = novel_config.get('genre', '') title_ko = novel_config.get('title_ko', '') logger.info(f"[{novel_id}] 에피소드 {ep_num} 블로그 변환 시작") theme = _get_theme(genre) bg = theme['bg'] accent = theme['accent'] accent_dim = theme['accent_dim'] card_bg = theme['card_bg'] text_color = theme['text'] meta_color = theme['meta'] nav_bg = theme['nav_bg'] # 다음 에피소드 예정일 (publish_schedule 파싱 — 간단 처리) next_date_str = '다음 회 예고' try: schedule = novel_config.get('publish_schedule', '') # "매주 월/목 09:00" 형식에서 요일 추출 if schedule: next_date_str = schedule.replace('매주 ', '').replace('09:00', '').strip() except Exception: pass # 본문 HTML body_html = _body_to_html(body_text) # JSON-LD post_url = '' json_ld = _build_json_ld(episode, novel_config, post_url) # 이전/다음 네비게이션 prev_link = ( f'← {ep_num - 1}화' if prev_url and ep_num > 1 else f'첫 번째 에피소드' ) next_link = ( f'{ep_num + 1}화 →' if next_url else f'다음 회 업데이트 예정' ) # 전체 HTML 조립 html = f"""{json_ld}{title_ko}
다음 에피소드 예고 · {next_date_str}
{hook if hook else '다음 회를 기대해 주세요.'}
{title_ko} 정보
장르: {genre} · 목표 {novel_config.get('episode_count_target', 20)}화 완결
연재 일정: {novel_config.get('publish_schedule', '')} · The 4th Path