0. tistory-card-stat ์ ๋ง๋ค๋ ค๊ณ ํ ์ด์
ํฐ์คํ ๋ฆฌ๋ฅผ ๊ธฐ์ ๋ธ๋ก๊ทธ๋ก ํ์ฉํ๊ณ ์๋๋ฐ, ๋ด๊ฐ ํฐ์คํ ๋ฆฌ์ ์ด ์ต์ ๊ธ ๋ด์ฉ์ ๊นํ๋ธ ํ๋กํ์ ๊ฐ์ ธ์ค๊ณ ์ถ์๋ค. ์๋๋ค Velog์ ๊ฒฝ์ฐ, ์ด๋ฌํ ๊ฐ์ฆ์ ํด์ํด์ค ๋๋ฌด๋ ์ธ๋ จ๋ ์นด๋๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ค.
๋ง๋์ ๋ถ ๊นํ๋ธ - enugyeole-velog-readme-stat
์ด๋ ๊ฒ ์๊ธฐ velgo ๊ณ์ ์ ๋ฃ์ผ๋ฉด ๋ด์ฉ์ด ๋ณด์ด๋๋ก ํด์ฃผ๊ณ ์๋ค. ํฐ์คํ ๋ฆฌ๋ ๊ด๋ จ๋ ์คํ ์์ค๊ฐ ์๋์ง ํ์ธํ๊ธฐ ์ํด ๊นํ๋ธ๋ฅผ ๋ค ๋ค์ก์ง๋ง ์กด์ฌํ์ง ์์๋ค. ์ ์กด์ฌํ์ง๋ง, ๋ ์ด์ ์๋น์ค๋ฅผ ํ์ง ์๊ณ ์๋ ์ค์ด์๋ค. ๊ทธ๋ฆฟ ์์ ํด๋น์ด๋ผ๋ ๊ฐ๋ฐ์ ๋ถ ๊ป์ Gihub Profile ์ ์ฉ card-stat์ ๋ง๋ค์์ผ๋, ๋ ์ด์ ์๋น์ค ํ๊ณ ์์ง ์์๋ค.
์ ๊ทธ๋ฐ์ง ๋ดค๋๋, ์ด์ ๊ฐ ๋ค์๊ณผ ๊ฐ์๋ค.
ํฐ์คํ ๋ฆฌ API๊ฐ ์๋น์ค๋ฅผ ์ข
๋ฃํด๋ฒ๋ ธ๋ค! ๊ทธ๋์ ๋ ์ด์ ๊ทธ๋ฆฟ ์์ ํด๋น ๋ถ์ด ๋ง๋ ์นด๋๊ฐ ๋์ํ์ง ์๋ ๊ฒ์ด๋ค!
๋ฌผ๋ก Python์ผ๋ก ๊ทธ๋ฅ aํ๊ทธ ํํ์ ์ต์ ๊ธ ๋ถ๋ฌ์ค๊ธฐ๋ ๊ฐ๋ฅํ์ง๋ง, velog card ์ฒ๋ผ ์ธ๋ จ๋ ๊ธฐ๋ฅ์ ๋ง๋ค๊ณ ์ถ์๋ค.
1. ํ์ฌ๊น์ง ๊ตฌํ ์ฌํญ
(0) ์ํฉ ํ์
API๋ฅผ ํ์ฉํด ๋ธ๋ก๊ทธ ๋ด ์ฌ๋ฌ ๊ธ์ ๋ณด์ฌ์ค ์ ์๋ Velog์ ๋ฌ๋ฆฌ, ํฐ์คํ ๋ฆฌ๋ Open API ์๋น์ค ์์ฒด๋ฅผ ์ข ๋ฃํด๋ฒ๋ ค์ ์ ํ์ง๊ฐ ๋ง์ด ์์๋ค. ํ์ฉํ ์ ์๋ ๊ฒ์ ๋จ์ง TISTORY RSS ๋ฟ์ด์๋ค. ์์ ์ ํฐ์คํ ๋ฆฌ ํ ํ๋ฉด ๋ค์ /rss๋ฅผ ์น๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋์ฌ ๊ฒ์ด๋ค.
์ฌ๊ธฐ์ ์ป์ ์ ์๋ ๊ฒ์, ๋ด ๋ธ๋ก๊ทธ์ ๋ํ ๋ฉํ๋ฐ์ดํฐ
, ์ต์ ๊ธ 10๊ฐ
์ ๋์๋ค.
(1) ์ํคํ ์ฒ
(2) Git-Action ์ฉ Yaml
name: Update blog posts
on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
jobs:
update-readme:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
pip install feedparser
pip install cairosvg
echo "์ค์น ์๋ฃ"
- name: Generate SVG blog cards
run: python scripts/generate_svg_cards.py
- name: Update ReadMe
run: python scripts/update_readme.py
- name: Commit and push if changed
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add README.md scripts/svg_cards/*.svg
git commit -m "FEAT: Update recent blog posts" || echo "No changes to commit"
git push https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git HEAD:main
๋๊ฒ ๋จ์ํ ์ฝ๋๋ค. (์ง๋๋ฐ๋ ์ค๋ ๊ฑธ๋ ธ์ง๋ง...)
Python ์ค์น
โ generate_svg_card ์คํ
โ update_readme ์คํ
(์๋ก ๋ง๋ svg ํ์ผ readme์ ์ ์ฌ)
(3) generate_svg_card (ํฐ์คํ ๋ฆฌ ์นด๋ ์์ฑ๊ธฐ)
import feedparser
import os
from datetime import datetime
from xml.sax.saxutils import escape
import base64
RSS_URL = "https://dalcheonroadhead.tistory.com/rss"
MAX_ITEMS = 5
CARD_WIDTH = 600
def get_base64_image(path):
with open(path, "rb") as img:
return base64.b64encode(img.read()).decode("utf-8")
base_path = os.path.dirname(__file__)
svg_path = os.path.join(base_path, "svg_cards")
image_path = os.path.join(base_path, "asset", "tistory_background.png")
background_base64 = get_base64_image(image_path)
print("[DEBUG] base_path:", base_path)
print("[DEBUG] image_path:", image_path)
print("[DEBUG] ํ์ผ ์กด์ฌ ์ฌ๋ถ:", os.path.exists(image_path))
SVG_TEMPLATE = """
<svg width="600" height="200" xmlns="http://www.w3.org/2000/svg">
<image href="data:image/png;base64,{background_base64}" x="0" y="0" width="100%" height="100%" />
<text x="24" y="40" font-size="14" font-weight="bold" fill="#FFF2CE" text-anchor="{anchor}">dalchoenroadhead.tistory.com</text>
<text x="24" y="80" font-size="18" font-weight="bold" fill="#FFF2CE" text-anchor="{anchor}">{title}</text>
{tags_svg}
<text x="24" y="180" font-size="14" fill="#FFF2CE" text-anchor="{anchor}">{pub_date}</text>
</svg>
"""
def format_tags(tags, x_offset):
svg_tags = []
current_x = x_offset
for tag in tags:
width = max(len(tag) * 8 + 15, 35)
svg_tags.append(f'<rect x="{current_x}" y="125" rx="8" ry="8" width="{width}" height="20" fill="#FFF2CE"/>')
svg_tags.append(f'<text x="{current_x + 6}" y="139" font-weight="bold" font-size="10" fill="#FF6969">{escape(tag)}</text>')
current_x += width + 10
return "\n ".join(svg_tags)
def main():
os.makedirs(svg_path, exist_ok=True)
feed = feedparser.parse(RSS_URL) # Parsing ํ๊ธฐ
for i, entry in enumerate(feed.entries[:MAX_ITEMS]):
try:
title = escape(entry.title)
if hasattr(entry, "published_parsed") and entry.published_parsed:
date = datetime(*entry.published_parsed[:6]).strftime("%Y-%m-%d")
else:
date = "Unknown"
tags = [tag.term for tag in entry.get("tags", [])] if "tags" in entry else []
print(f"[DEBUG] {i+1}. {title} ({date}) - {tags}")
svg = SVG_TEMPLATE.format(
title=title,
pub_date=date,
tags_svg=format_tags(tags, 24),
anchor="start",
background_base64 = background_base64
)
filepath = os.path.join(svg_path, f"card_{i+1}.svg")
with open(filepath, "w", encoding="utf-8") as f:
f.write(svg)
print(f"[OK] SVG ์์ฑ ์๋ฃ → {filepath}")
except Exception as e:
print(f"[ERROR] {i+1}๋ฒ entry ์์ฑ ์คํจ: {e}")
if __name__ == "__main__":
main()
๋ด ํฐ์คํ ๋ฆฌ ๋ธ๋ก๊ทธ์์ ์ต์ ๊ธ์ ๊ฐ์ ธ์์
- parsing ํ ๋ค์,
- background ์ ์ฉํด์ ์นด๋ ํํ์ svg๋ก ๋ง๋ ๋ค.
- ์ดํ ์ ํด์ง ํด๋ ์์น์ svg ์นด๋๋ฅผ ์ ์ฅํ๋ค.
์์ ๋ฐฐ๊ฒฝ์ด ๋ด๊ฐ ๋ง๋๋ svg ์นด๋์ BackGround ์ด๋ค.
(4) update_readMe.py
import os
import feedparser
from xml.sax.saxutils import escape
# ํฐ์คํ ๋ฆฌ RSS์์ ์ต๊ทผ ๊ธ 5๊ฐ ํ์ฑ
feed = feedparser.parse("https://dalcheonroadhead.tistory.com/rss")
entries = feed.entries[:5]
SVG_DIR = "svg_cards"
README_PATH = os.path.join(os.path.dirname(__file__), "..", "README.md")
START_TAG = "<!-- BLOG-POST-START -->"
END_TAG = "<!-- BLOG-POST-END -->"
# GitHub Repository ์ด๋ฆ ์ถ์ถ (์: dalcheonroadhead/svg-blog)
repo_name = os.environ.get("GITHUB_REPOSITORY", "dalcheonroadhead/dalcheonroadhead")
# SVG → PNG ๋ณํ ๋ฐ README์ ๋ค์ด๊ฐ <img> ๋ผ์ธ ์ค๋น
svg_lines = []
for i, entry in enumerate(entries):
link = escape(entry.link)
svg_url = f"https://raw.githubusercontent.com/{repo_name}/main/scripts/{SVG_DIR}/card_{i+1}.svg"
svg_lines.append(f'''
<a href="{link}" target="_blank">
<img src="{svg_url}" width="600" height="200"/>
</a>
''')
# README.md ๋ด์ฉ ๊ฐฑ์
with open(README_PATH, "r", encoding="utf-8") as f:
content = f.read()
start = content.find(START_TAG)
end = content.find(END_TAG)
if start != -1 and end != -1:
new_block = START_TAG + "\n" + "\n".join(svg_lines) + "\n" + END_TAG
updated = content[:start] + new_block + content[end + len(END_TAG):]
with open(README_PATH, "w", encoding="utf-8") as f:
f.write(updated)
- ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ ๋ค์ ์ต์ ๊ธ URL ์ฃผ์
a ํ๊ทธ
๋ก ๊ฐ์๋ค. - ReadMe ๋ด์์ START_TAG ์ฃผ์๊ณผ END_TAG ์ฃผ์ ์ฌ์ด์ ๊ฐ์ ๋ฃ๋๋ก ํ์๋ค.
ํฐ์คํ ๋ฆฌ ์นด๋์ ์์น๋ฅผ ์ํ๋ ๊ณณ์ ๋ฃ๋ ๊ฒ์ด ๊ตฌํ ์ ํ๋ ๋ถ๋ถ์ด์๋๋ฐ, ํ์๋ readme๋ readme ๋๋ก ๊พธ๋ฏธ๊ณ , ์ต์ ๊ธ์ ์ต์ ๊ธ๋๋ก ๋ฃ๊ณ ์ถ์๋ค. ๊ทธ๋์ ์ ๋ ๊ฒ ์ฃผ์ ์ฌ์ด์ ๊ฐ์ ๋ฃ๋ ์์ผ๋ก ๊ตฌํํ๋ค. ์ด๋ ๊ฒ ๊ตฌํํ๋, readMe ๋ด์ ์ต์ ๊ธ ํฌ์คํธ๊ฐ ์ฌ๋ผ๊ฐ ์๋ฆฌ๋ฅผ ์ ํ ์ ์์ด์ ์ข์๋ค.
(5) ๊ฒฐ๊ณผ
์์ ๊ฐ์ด ์ด์๊ฒ ๋ค์ด๊ฐ๋ค. ์ ์ฒด ํ์ธํ๊ณ ์ถ์ ๋ถ๋ค์ ์ ๊นํ๋ธ ํ๋กํ์ผ๋ก ์์ฃผ์๊ธธ ๋ฐ๋๋ค.
2. ๋ค์ ๊ตฌํ
- Java/Spring์ด ๋ฌด๊ฑฐ์์ ๊ฐ๋จํ๊ฒ Python Script๋ก ๊ตฌํํ์ง๋ง, ์๋ฌด๋๋ Serveless ์ฑ์ผ๋ก ๊ตฌํํ๋ ค๋ฉด, Spring์ผ๋ก ํ๋ ๊ฒ ์ข์ง ์์๊น ์๊ฐ์ ํ๋ค.
(์์๋ณด๋ Vercel์์ Spring ์ง์์ ์ํ๋ค... ใ ) - ์ฌ์ฉ์ ์ด๋ฆ์ ๋์ ์ผ๋ก ๋ฐ์ ์ ๋ ๊ฒ ์ด๋ฏธ์ง ํ์ผ๋ก ๋ฐ๋ก ์ฃผ๋ ๋ฐฉ์์ ๋ํด์ ์ด๋ป๊ฒ ๊ตฌํํ๋ฉด ์ข์์ง ๋ ์์๋ด์ผ๊ฒ ๋ค.
3. ์ถ์
๋ง์ฝ ํ์ฌ๊น์ง ๊ตฌํํ ๋ถ๋ถ์ ์ง์ ๋ฆฌ๋๋ฏธ๋ก ์ฐ๊ณ ์ถ์ ๋ถ๋ค์ ํ์ผ ๋๋ฆฌ๊ฒ ๋ค.