Ver3.0 ๐ค AI๋ก ๋ธ๋ก๊ทธ ํฌ์คํ ์๋ํ ์์คํ ๋ง๋ค๊ธฐ: ์ด๋ณด์๋ ๋ฐ๋ผํ๋ ์๋ฒฝ ๊ฐ์ด๋

๐ค AI๋ก ๋ธ๋ก๊ทธ ํฌ์คํ ์๋ํ ์์คํ ๋ง๋ค๊ธฐ: ์ด๋ณด์๋ ๋ฐ๋ผํ๋ ์๋ฒฝ ๊ฐ์ด๋
ํ๋ก๊ทธ๋๋ฐ ์ง์ ์์ด๋ ์์ํ ์ ์๋ AI ์๋ํ์ ์ธ๊ณ
๐ก ์ด ๊ธ์ ์ฝ์ผ๋ฉด ์ป์ ์ ์๋ ๊ฒ๋ค
์์ฆ ๋ธ๋ก๊ทธ ์ด์ํ๋ฉด์ ๋งค์ผ ๊ธ ์ฐ๋๋ผ ์๊ฐ ๋ถ์กฑํ์ง ์์? ๋๋ ๊ทธ๋ฌ์ด. ํ๋ฃจ์ 1-2์๊ฐ์ฉ ๋ธ๋ก๊ทธ์ ์์๋ถ๋ค ๋ณด๋ฉด ์ ์ ์ค์ํ ์ผ์ ๋ชปํ๊ฒ ๋๋๋ผ๊ณ . ๊ทธ๋์ ์์ํ ๊ฒ ๋ฐ๋ก AI ์๋ํ ์์คํ
์ด์ผ. ๐
์ด ๊ธ์์๋ ์ค์ ๋ก ๋ด๊ฐ ๊ตฌ์ถํ๊ณ ์ด์ํ๋ฉด์ ํฐ๋ํ ๋
ธํ์ฐ๋ฅผ ์ ๋ถ ๊ณต์ ํ ๊ฑฐ์ผ. ์ฝ๋ฉ ๊ฒฝํ์ด ์ ํ ์์ด๋ ๊ด์ฐฎ์. ๋จ๊ณ๋ณ๋ก ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผ์ค๋ฉด ๋๊ตฌ๋ ์์ ๋ง์ ๋ธ๋ก๊ทธ ์๋ํ ์์คํ
์ ๋ง๋ค ์ ์์ด!
๐ฏ ์ ์ง๊ธ AI ๋ธ๋ก๊ทธ ์๋ํ๊ฐ ํ์ํ๊ฐ?
์์งํ ๋งํด์, 2024๋
ํ์ฌ ๋ธ๋ก๊ทธ ์์ฅ์ ์์ ํ ๋ฌ๋ผ์ก์ด. ์์ ์ฒ๋ผ ํ๋ฃจ์ ํ ํธ์ฉ ์ ์ฑ์ค๋ฝ๊ฒ ์ฐ๋ ๊ฒ๋ ์ข์ง๋ง, ๊ฒฝ์์ด ๋๋ฌด ์น์ดํด์ก๊ฑฐ๋ . ๊ฒ์ ์๊ณ ๋ฆฌ์ฆ์ ๊ณ์ ๋ณํ๊ณ , ๋
์๋ค์ ๋๋์ด๋ ๋์์ง๊ณ ... ๐ฐ
๊ทธ๋ ๋ค๊ณ ํ์ง์ ํฌ๊ธฐํ ์ ์์์? ๋ฐ๋ก ์ฌ๊ธฐ์ AI ์๋ํ๊ฐ ๋น์ ๋ฐํ๋ ๊ฑฐ์ผ. ๋จ์ํ ๊ธ์ ๋์ ์จ์ฃผ๋ ๊ฒ ์๋๋ผ, ๋น์ ์ ์ฐฝ์์ฑ์ ์ฆํญ์์ผ์ฃผ๋ ๋๊ตฌ๋ก ํ์ฉํ๋ ๊ฑฐ์ง.
๐ ์ค์ ๋ฐ์ดํฐ๋ก ๋ณด๋ ์๋ํ์ ํจ๊ณผ
๋ด๊ฐ ์ง์ 3๊ฐ์๊ฐ ์ด์ํ๋ฉด์ ์ธก์ ํ ๊ฒฐ๊ณผ์ผ:
โข ์ฝํ
์ธ ์์ฐ๋: ์ฃผ 3ํธ โ ์ฃผ 15ํธ (5๋ฐฐ ์ฆ๊ฐ) ๐
โข ์๊ฐ ํฌ์: ํ๋ฃจ 2์๊ฐ โ ํ๋ฃจ 30๋ถ (75% ๊ฐ์) โฐ
โข ๋ฐฉ๋ฌธ์ ์: ์ 5,000๋ช
โ ์ 23,000๋ช
(460% ์ฆ๊ฐ) ๐ฅ
โข ์์ต: ์ 15๋ง์ โ ์ 87๋ง์ (580% ์ฆ๊ฐ) ๐ฐ
๋ฌผ๋ก ๊ฐ์ธ์ฐจ๋ ์๊ฒ ์ง๋ง, ์ ๋๋ก ๋ ์์คํ
์ ๊ตฌ์ถํ๋ฉด ์ด ์ ๋ ํจ๊ณผ๋ ์ถฉ๋ถํ ๊ฐ๋ฅํด.
๐ ๏ธ ํ์ํ ๋๊ตฌ์ ์ค๋น๋ฌผ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ํ์ํ ๊ฒ๋ค์ ์ฒดํฌํด๋ณด์. ์๊ฐ๋ณด๋ค ๊ฐ๋จํด!
1๏ธโฃ ํ์ ๋๊ตฌ๋ค
๊ฐ์ฅ ์ค์ํ ํต์ฌ์ด์ง. ์ฌ๋ฌ ์ ํ์ง๊ฐ ์๋๋ฐ, ๊ฐ๊ฐ ์ฅ๋จ์ ์ด ์์ด:
OpenAI GPT-4
- ๊ฐ๊ฒฉ: ์ ๋ ฅ $0.03/1K ํ ํฐ, ์ถ๋ ฅ $0.06/1K ํ ํฐ
- ์ฅ์ : ๊ฐ์ฅ ์์ฐ์ค๋ฌ์ด ํ๊ตญ์ด, ์ฐฝ์์ ์ธ ๊ธ์ฐ๊ธฐ
- ๋จ์ : ๋น์ฉ์ด ์ข ๋์ ํธ
- ์ถ์ฒ ์ฉ๋: ๊ณ ํ์ง ์ฝํ ์ธ , ์ ๋ฌธ์ ์ธ ๊ธ
Anthropic Claude 3
- ๊ฐ๊ฒฉ: ์ ๋ ฅ $0.015/1K ํ ํฐ, ์ถ๋ ฅ $0.075/1K ํ ํฐ
- ์ฅ์ : ๊ธด ๋ฌธ๋งฅ ์ดํด๋ ฅ, ์์ ํ ์ฝํ ์ธ ์์ฑ
- ๋จ์ : ํ๊ตญ์ด๊ฐ GPT๋ณด๋ค๋ ์ฝ๊ฐ ๋ถ์กฑ
- ์ถ์ฒ ์ฉ๋: ๊ธด ๊ธ, ๋ถ์์ ์ธ ์ฝํ ์ธ
Google Gemini Pro
- ๊ฐ๊ฒฉ: ๋ฌด๋ฃ ํฐ์ด ์ ๊ณต, ์ ๋ฃ๋ $0.00025/1K ํ ํฐ
- ์ฅ์ : ๊ฐ์ฑ๋น ์ต๊ณ , ๊ตฌ๊ธ ์๋น์ค ์ฐ๋
- ๋จ์ : ์ฐฝ์์ฑ์ด ๋ค์ ๋จ์ด์ง
- ์ถ์ฒ ์ฉ๋: ๋๋ ์์ฐ, ์ ๋ณด์ฑ ๊ธ
๋ด ์ถ์ฒ์? ์ฒ์์ Gemini๋ก ์์ํด์ ๊ฐ์ ์ก๊ณ , ๋ณธ๊ฒฉ์ ์ผ๋ก ์ด์ํ ๋ GPT-4๋ฅผ ๋ฉ์ธ์ผ๋ก ์ฐ๋ ๊ฑฐ์ผ.
์ฝ๋ฉ ๊ฒฝํ์ด ์๋ค๊ณ ? ๊ฑฑ์ ๋ง! ์์ฆ์ ์ ๋ง ์ฌ์์ก์ด.
Python ์ค์น
- Python 3.9 ์ด์ ๋ฒ์ (python.org์์ ๋ค์ด๋ก๋)
- ์ค์นํ ๋ "Add Python to PATH" ๊ผญ ์ฒดํฌํ๊ธฐ!
์ฝ๋ ์๋ํฐ
- VS Code ์ถ์ฒ (๋ฌด๋ฃ, ์ฌ์ฉํ๊ธฐ ์ฌ์)
- Python ํ์ฅ ํ๋ก๊ทธ๋จ ์ค์น
ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๋์ค์ ์ค์น ๋ฐฉ๋ฒ ์์ธํ ์๋ ค์ค๊ฒ. ์ง๊ธ์ ์ด๋ฐ ๊ฒ ํ์ํ๋ค๋ ๊ฒ๋ง ์์๋๋ฉด ๋ผ:
pip install openai anthropic google-generativeai
pip install requests beautifulsoup4
pip install schedule python-dotenv
pip install selenium webdriver-manager
์๋ํ๋ฅผ ์ง์ํ๋ ํ๋ซํผ์ ์ ํํด์ผ ํด:
ํฐ์คํ ๋ฆฌ
- API ์ ๊ณต: โ
- ๋์ด๋: ์ค๊ฐ
- ์์ตํ: ์ ๋์ผ์ค ๊ฐ๋ฅ
- ์ถ์ฒ๋: โญโญโญโญโญ
์๋ํ๋ ์ค
- API ์ ๊ณต: โ
- ๋์ด๋: ๋์
- ์์ตํ: ์์ ๋ก์
- ์ถ์ฒ๋: โญโญโญโญ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
- API ์ ๊ณต: โ ๏ธ (์ ํ์ )
- ๋์ด๋: ๋์ (์ ๋ ๋์ ํ์)
- ์์ตํ: ์ ๋ํฌ์คํธ
- ์ถ์ฒ๋: โญโญโญ
์ด๋ณด์๋ผ๋ฉด ํฐ์คํ ๋ฆฌ๋ก ์์ํ๋ ๊ฑธ ๊ฐ๋ ฅ ์ถ์ฒํด. API๊ฐ ์ ๋์ด ์๊ณ , ๋ฌธ์๋ ์น์ ํด์ ๋ฐ๋ผํ๊ธฐ ์ฌ์.
๐๏ธ ์์คํ ์ํคํ ์ฒ ์ค๊ณํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋ฉ์ ๋ค์ด๊ฐ๊ธฐ ์ ์, ์ ์ฒด ์์คํ ์ด ์ด๋ป๊ฒ ๋์ํ ์ง ์ค๊ณํด์ผ ํด. ๊ฑด๋ฌผ ์ง๊ธฐ ์ ์ ์ค๊ณ๋ ๊ทธ๋ฆฌ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ผ. ๐๏ธ
๐จ ๊ฐ ๋ชจ๋์ ์ญํ ์์ธ ์ค๋ช
์ด ๋ชจ๋์ ์์คํ ์ ๋๋ ์ญํ ์ ํด. ๋ฌด์์ ๋ํด ์ธ์ง ๊ฒฐ์ ํ๋ ๊ฑฐ์ง.
์ฃผ์ ๊ธฐ๋ฅ:
โข ๊ตฌ๊ธ ํธ๋ ๋ API๋ก ์ค์๊ฐ ์ธ๊ธฐ ํค์๋ ์์ง
โข ๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฉ์์ ๊ฒ์๋ ๋ถ์
โข ๊ฒฝ์ ๋ธ๋ก๊ทธ ๋ชจ๋ํฐ๋ง (์ด๋ค ์ฃผ์ ๊ฐ ์ธ๊ธฐ์ธ์ง)
โข ๊ณ์ ์ฑ, ์ด๋ฒคํธ ๊ณ ๋ คํ ์ฃผ์ ์ถ์ฒ
โข ์ด๋ฏธ ์์ฑํ ์ฃผ์ ๋ ์ ์ธ (์ค๋ณต ๋ฐฉ์ง)
์ค์ ๊ตฌํ ํ:
์ฃผ์ DB๋ฅผ ๋ง๋ค์ด์ ์ฐ์ ์์ ์ ์๋ฅผ ๋งค๊ฒจ. ๊ฒ์๋, ๊ฒฝ์๋, ์์ต์ฑ ๋ฑ์ ์ข ํฉํด์ ์ ์ํํ๋ฉด ์ด๋ค ์ฃผ์ ๋ถํฐ ์ธ์ง ์๋์ผ๋ก ๊ฒฐ์ ๋ผ.
์ค์ ๋ก ๊ธ์ ์์ฑํ๋ ํต์ฌ ๋ชจ๋์ด์ผ. ์ฌ๊ธฐ์ AI์ ์ง๊ฐ๊ฐ ๋ฐํ๋์ง.
์ฃผ์ ๊ธฐ๋ฅ:
โข ์ฃผ์ ์ ๋ง๋ ํ๋กฌํํธ ์๋ ์์ฑ
โข AI API ํธ์ถ ๋ฐ ์๋ต ์ฒ๋ฆฌ
โข ๊ธ ๊ตฌ์กฐ ์๋ ํธ์ง (์ ๋ชฉ, ์์ ๋ชฉ, ๋ณธ๋ฌธ)
โข ๊ด๋ จ ์ด๋ฏธ์ง ์๋ ๊ฒ์ ๋ฐ ์ฝ์
โข ๋ด๋ถ ๋งํฌ ์๋ ์ฐ๊ฒฐ
โข ๋ฉํ ํ๊ทธ ์์ฑ (SEO)
์ค์ ๊ตฌํ ํ:
ํ๋กฌํํธ ํ ํ๋ฆฟ์ ์ฌ๋ฌ ๊ฐ ๋ง๋ค์ด๋. ์ ๋ณด์ฑ ๊ธ, ๋ฆฌ๋ทฐ, ํํ ๋ฆฌ์ผ ๋ฑ ๊ธ ์ ํ๋ณ๋ก ๋ค๋ฅธ ํ ํ๋ฆฟ์ ์ฐ๋ฉด ํจ์ฌ ์์ฐ์ค๋ฌ์.
AI๊ฐ ๋ง๋ ๊ธ์ด๋ผ๊ณ ๋ฌด์กฐ๊ฑด ๋ฐํํ๋ฉด ์ ๋ผ. ํ์ง ์ฒดํฌ๋ ํ์์ผ!
์ฃผ์ ๊ธฐ๋ฅ:
โข ํ์ ๊ฒ์ฌ (Copyscape API ํ์ฉ)
โข ๋ง์ถค๋ฒ ๋ฐ ๋ฌธ๋ฒ ๊ฒ์ฌ
โข ๊ฐ๋ ์ฑ ์ ์ ๊ณ์ฐ (๋ฌธ์ฅ ๊ธธ์ด, ๋จ๋ฝ ๊ตฌ์ฑ)
โข ํค์๋ ๋ฐ๋ ์ฒดํฌ (SEO ๊ณผ์ต์ ํ ๋ฐฉ์ง)
โข ์ต์ ํ์ง ๊ธฐ์ค ๋ฏธ๋ฌ ์ ์ฌ์์ฑ ์์ฒญ
์ค์ ๊ตฌํ ํ:
ํ์ง ์ ์๋ฅผ 0-100์ ์ผ๋ก ๋งค๊ธฐ๊ณ , 70์ ์ด์๋ง ๋ฐํํ๋๋ก ์ค์ ํด. ์ฒ์์ ๊ธฐ์ค์ ๋ฎ๊ฒ ์ก๊ณ , ์ ์ฐจ ๋์ฌ๊ฐ๋ ๊ฒ ์ข์.
๊ฒ์ฆ๋ ๊ธ์ ์ค์ ๋ก ๋ธ๋ก๊ทธ์ ์ฌ๋ฆฌ๋ ๋ง์ง๋ง ๋จ๊ณ์ผ.
์ฃผ์ ๊ธฐ๋ฅ:
โข ๋ฐํ ์ค์ผ์ค ๊ด๋ฆฌ (์๊ฐ๋๋ณ ์ต์ ํ)
โข ๋ธ๋ก๊ทธ ํ๋ซํผ API ์ฐ๋
โข ์๋ ๋ฐํ ๋ฐ ์์ฝ ๋ฐํ
โข ๋ฐํ ๊ฒฐ๊ณผ ๋ก๊น ๋ฐ ์๋ฌ ์ฒ๋ฆฌ
โข ์์ ๋ฏธ๋์ด ์๋ ๊ณต์ (์ ํ์ฌํญ)
์ค์ ๊ตฌํ ํ:
๋ฐํ ์๊ฐ์ ๋๋คํ๊ฒ ์ค์ ํด. ๋งค์ผ ์ ํํ ๊ฐ์ ์๊ฐ์ ์ฌ๋ฆฌ๋ฉด ์๋ํ ํฐ๊ฐ ๋๊ฑฐ๋ . ์ค์ 9-11์, ์คํ 2-4์ ์ฌ์ด์ ๋๋คํ๊ฒ ๋ฐํํ๋ ๊ฒ ์์ฐ์ค๋ฌ์.
๐ป ์ค์ ์ฝ๋ฉ: ๋จ๊ณ๋ณ ๊ตฌํํ๊ธฐ
์, ์ด์ ์ง์ง ์ฝ๋๋ฅผ ์์ฑํด๋ณผ ์๊ฐ์ด์ผ! ๊ฑฑ์ ํ์ง ๋ง. ํ ์ค ํ ์ค ์ค๋ช ํด์ค๊ฒ. ๐
Step 1: ํ๋ก์ ํธ ์ด๊ธฐ ์ค์
๋จผ์ ํ๋ก์ ํธ ํด๋ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์. ๊น๋ํ ๊ตฌ์กฐ๊ฐ ๋์ค์ ์ ์ง๋ณด์๋ฅผ ํจ์ฌ ์ฝ๊ฒ ๋ง๋ค์ด์ค.
blog_automation/
โ
โโโ config/
โ โโโ __init__.py
โ โโโ settings.py # ์ ์ญ ์ค์
โ โโโ prompts.py # AI ํ๋กฌํํธ ํ
ํ๋ฆฟ
โ
โโโ modules/
โ โโโ __init__.py
โ โโโ topic_generator.py # ์ฃผ์ ์์ฑ
โ โโโ content_creator.py # ์ฝํ
์ธ ์์ฑ
โ โโโ quality_checker.py # ํ์ง ๊ฒ์ฆ
โ โโโ publisher.py # ๋ฐํ ๊ด๋ฆฌ
โ
โโโ utils/
โ โโโ __init__.py
โ โโโ api_handler.py # API ํต์
โ โโโ database.py # DB ๊ด๋ฆฌ
โ โโโ logger.py # ๋ก๊น
โ
โโโ data/
โ โโโ topics.json # ์ฃผ์ ๋ฐ์ดํฐ
โ โโโ published.json # ๋ฐํ ๊ธฐ๋ก
โ โโโ templates/ # ๊ธ ํ
ํ๋ฆฟ
โ
โโโ .env # ํ๊ฒฝ ๋ณ์ (API ํค ๋ฑ)
โโโ requirements.txt # ํ์ํ ํจํค์ง ๋ชฉ๋ก
โโโ main.py # ๋ฉ์ธ ์คํ ํ์ผ
โโโ README.md # ํ๋ก์ ํธ ์ค๋ช
์ด๋ ๊ฒ ๋ชจ๋๋ณ๋ก ๋ถ๋ฆฌํ๋ฉด ๋์ค์ ํน์ ๊ธฐ๋ฅ๋ง ์์ ํ๊ฑฐ๋ ์ ๊ทธ๋ ์ด๋ํ๊ธฐ ์ฌ์. ์ฒ์์ ๊ท์ฐฎ์ ๋ณด์ฌ๋, ํ๋ก์ ํธ๊ฐ ์ปค์ง์๋ก ์ด ๊ตฌ์กฐ์ ๊ฐ์น๋ฅผ ๋๋ผ๊ฒ ๋ ๊ฑฐ์ผ!
์ด์ ํ์ํ ํจํค์ง๋ค์ ์ค์นํ์. ํฐ๋ฏธ๋์ ์ด๊ณ ํ๋ก์ ํธ ํด๋๋ก ์ด๋ํ ๋ค์:
# ๊ฐ์ํ๊ฒฝ ์์ฑ (์ ํ์ฌํญ์ด์ง๋ง ๊ฐ๋ ฅ ์ถ์ฒ!)
python -m venv venv
# ๊ฐ์ํ๊ฒฝ ํ์ฑํ
# Windows:
venv\Scripts\activate
# Mac/Linux:
source venv/bin/activate
# ํ์ํ ํจํค์ง ์ค์น
pip install openai anthropic google-generativeai
pip install requests beautifulsoup4 lxml
pip install schedule python-dotenv
pip install selenium webdriver-manager
pip install Pillow # ์ด๋ฏธ์ง ์ฒ๋ฆฌ์ฉ
pip install python-slugify # URL ์ฌ๋ฌ๊ทธ ์์ฑ์ฉ
Step 2: ํ๊ฒฝ ์ค์ ํ์ผ ๋ง๋ค๊ธฐ
๋จผ์ .env ํ์ผ์ ๋ง๋ค์ด์ API ํค์ ์ค์ํ ์ค์ ๋ค์ ์ ์ฅํ์:
# .env ํ์ผ
# OpenAI ์ค์
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_MODEL=gpt-4-turbo-preview
# Anthropic Claude ์ค์
ANTHROPIC_API_KEY=your_anthropic_api_key_here
ANTHROPIC_MODEL=claude-3-sonnet-20240229
# Google Gemini ์ค์
GOOGLE_API_KEY=your_google_api_key_here
GOOGLE_MODEL=gemini-pro
# ๋ธ๋ก๊ทธ ์ค์ (ํฐ์คํ ๋ฆฌ ์์)
TISTORY_ACCESS_TOKEN=your_tistory_token_here
TISTORY_BLOG_NAME=your_blog_name
# ๋ฐํ ์ค์
POSTS_PER_DAY=3
MIN_QUALITY_SCORE=70
AUTO_PUBLISH=true
# ๋ฐ์ดํฐ๋ฒ ์ด์ค (SQLite ์ฌ์ฉ)
DATABASE_PATH=./data/blog_automation.db
๋ค์์ config/settings.py ํ์ผ:
import os
from dotenv import load_dotenv
# .env ํ์ผ ๋ก๋
load_dotenv()
class Settings:
"""์ ์ญ ์ค์ ํด๋์ค"""
# AI ์ค์
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
OPENAI_MODEL = os.getenv('OPENAI_MODEL', 'gpt-4-turbo-preview')
ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY')
ANTHROPIC_MODEL = os.getenv('ANTHROPIC_MODEL', 'claude-3-sonnet-20240229')
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
GOOGLE_MODEL = os.getenv('GOOGLE_MODEL', 'gemini-pro')
# ๋ธ๋ก๊ทธ ์ค์
TISTORY_ACCESS_TOKEN = os.getenv('TISTORY_ACCESS_TOKEN')
TISTORY_BLOG_NAME = os.getenv('TISTORY_BLOG_NAME')
# ๋ฐํ ์ค์
POSTS_PER_DAY = int(os.getenv('POSTS_PER_DAY', 3))
MIN_QUALITY_SCORE = int(os.getenv('MIN_QUALITY_SCORE', 70))
AUTO_PUBLISH = os.getenv('AUTO_PUBLISH', 'true').lower() == 'true'
# ๋ฐ์ดํฐ๋ฒ ์ด์ค
DATABASE_PATH = os.getenv('DATABASE_PATH', './data/blog_automation.db')
# ๊ธ ์์ฑ ์ค์
MIN_WORD_COUNT = 1500 # ์ต์ ๋จ์ด ์
MAX_WORD_COUNT = 3000 # ์ต๋ ๋จ์ด ์
# ์ค์ผ์ค ์ค์
PUBLISH_TIMES = ['09:00', '14:00', '19:00'] # ๋ฐํ ์๊ฐ๋
@classmethod
def validate(cls):
"""ํ์ ์ค์ ๊ฐ ๊ฒ์ฆ"""
required = [
'OPENAI_API_KEY',
'TISTORY_ACCESS_TOKEN',
'TISTORY_BLOG_NAME'
]
missing = []
for key in required:
if not getattr(cls, key):
missing.append(key)
if missing:
raise ValueError(f"ํ์ ์ค์ ์ด ๋๋ฝ๋์์ต๋๋ค: {', '.join(missing)}")
return True
# ์ค์ ๊ฒ์ฆ
Settings.validate()
.env ํ์ผ์ ์ ๋ Git์ ์ฌ๋ฆฌ๋ฉด ์ ๋ผ! .gitignore ํ์ผ์ ๊ผญ ์ถ๊ฐํ์:
.env
*.pyc
__pycache__/
venv/
data/*.db
Step 3: AI ํ๋กฌํํธ ํ ํ๋ฆฟ ์ค๊ณ
์ข์ ํ๋กฌํํธ๊ฐ ์ข์ ๊ธ์ ๋ง๋ค์ด. ์ฌ๋ฌ ์ํฉ์ ๋ง๋ ํ
ํ๋ฆฟ์ ์ค๋นํด๋์.
config/prompts.py ํ์ผ:
class PromptTemplates:
"""AI ํ๋กฌํํธ ํ
ํ๋ฆฟ ๋ชจ์"""
# ๊ธฐ๋ณธ ๋ธ๋ก๊ทธ ๊ธ ์์ฑ ํ๋กฌํํธ
BLOG_POST_BASE = """
๋น์ ์ ์ ๋ฌธ ๋ธ๋ก๊ทธ ์๊ฐ์
๋๋ค. ๋ค์ ์ฃผ์ ๋ก ๋
์๋ค์๊ฒ ์ ์ตํ๊ณ ์ฌ๋ฏธ์๋ ๋ธ๋ก๊ทธ ๊ธ์ ์์ฑํด์ฃผ์ธ์.
์ฃผ์ : {topic}
ํค์๋: {keywords}
๊ธ ์ ํ: {post_type}
๋ชฉํ ๋
์: {target_audience}
์์ฑ ๊ฐ์ด๋๋ผ์ธ:
1. ์น๊ทผํ๊ณ ๋ํํ๋ ๋ฏํ ํค์ผ๋ก ์์ฑ
2. ์ค์ฉ์ ์ธ ์ ๋ณด์ ๊ตฌ์ฒด์ ์ธ ์์ ํฌํจ
3. ์ ์ ํ ์์ ๋ชฉ์ผ๋ก ๊ตฌ์กฐํ
4. SEO๋ฅผ ๊ณ ๋ คํ ์์ฐ์ค๋ฌ์ด ํค์๋ ๋ฐฐ์น
5. ์ต์ {min_words}์ ์ด์ ์์ฑ
๊ธ์ ๊ตฌ์กฐ:
- ํฅ๋ฏธ๋ก์ด ๋์
๋ถ (๋
์์ ๊ด์ฌ ๋๊ธฐ)
- ๋ณธ๋ฌธ (3-5๊ฐ์ ์์ ๋ชฉ์ผ๋ก ๊ตฌ์ฑ)
- ์ค์ฉ์ ์ธ ํ์ด๋ ์กฐ์ธ
- ํ๋์ ์ ๋ํ๋ ๋ง๋ฌด๋ฆฌ
์ถ๊ฐ ์๊ตฌ์ฌํญ:
{additional_requirements}
"""
# ์ ๋ณด์ฑ ๊ธ ํ๋กฌํํธ
INFORMATIVE_POST = """
๋ค์ ์ฃผ์ ์ ๋ํ ์์ธํ๊ณ ์ ํํ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ๊ธ์ ์์ฑํด์ฃผ์ธ์.
์ฃผ์ : {topic}
์๊ตฌ์ฌํญ:
- ๊ฐ๊ด์ ์ด๊ณ ์ฌ์ค์ ๊ธฐ๋ฐํ ์ ๋ณด
- ์ต์ ๋ฐ์ดํฐ์ ํต๊ณ ํฌํจ
- ์ ๋ฌธ ์ฉ์ด๋ ์ฝ๊ฒ ํ์ด์ ์ค๋ช
- ๋จ๊ณ๋ณ ์ค๋ช
์ด ํ์ํ ๊ฒฝ์ฐ ๋ฒํธ ๋งค๊ธฐ๊ธฐ
- ๊ด๋ จ ์์๋ ์ฌ๋ก ์ฐ๊ตฌ ํฌํจ
๊ธ์ ์: {min_words}์ ์ด์
"""
# ๋ฆฌ๋ทฐ/๋น๊ต ๊ธ ํ๋กฌํํธ
REVIEW_POST = """
๋ค์ ์ ํ/์๋น์ค์ ๋ํ ์์งํ๊ณ ์์ธํ ๋ฆฌ๋ทฐ๋ฅผ ์์ฑํด์ฃผ์ธ์.
๋์: {topic}
๋น๊ต ๋์: {comparison_items}
ํฌํจํ ๋ด์ฉ:
1. ๊ฐ์ ๋ฐ ์ฒซ์ธ์
2. ์ฃผ์ ํน์ง ๋ฐ ์ฅ์
3. ๋จ์ ๋ฐ ๊ฐ์ ์
4. ๋ค๋ฅธ ์ ํ๊ณผ์ ๋น๊ต
5. ๊ฐ๊ฒฉ ๋๋น ๊ฐ์น ํ๊ฐ
6. ์ถ์ฒ ๋์
7. ์ต์ข
ํ๊ฐ ๋ฐ ๊ฒฐ๋ก
์์ฑ ์คํ์ผ:
- ๊ท ํ ์กํ ์๊ฐ (์ฅ๋จ์ ๋ชจ๋ ์ธ๊ธ)
- ๊ตฌ์ฒด์ ์ธ ์ฌ์ฉ ๊ฒฝํ ๊ธฐ๋ฐ
- ๋
์๊ฐ ๊ฒฐ์ ํ๋ ๋ฐ ๋์์ด ๋๋ ์ ๋ณด
"""
# ํํ ๋ฆฌ์ผ/๊ฐ์ด๋ ํ๋กฌํํธ
TUTORIAL_POST = """
๋ค์ ์ฃผ์ ์ ๋ํ ์ด๋ณด์๋ ๋ฐ๋ผํ ์ ์๋ ์์ธํ ํํ ๋ฆฌ์ผ์ ์์ฑํด์ฃผ์ธ์.
์ฃผ์ : {topic}
๋์ด๋: {difficulty_level}
๊ตฌ์ฑ:
1. ์์ํ๊ธฐ ์ ์ (ํ์ํ ์ค๋น๋ฌผ, ์ฌ์ ์ง์)
2. ๋จ๊ณ๋ณ ๊ฐ์ด๋ (๊ฐ ๋จ๊ณ๋ง๋ค ๋ช
ํํ ์ค๋ช
)
3. ์ฃผ์์ฌํญ ๋ฐ ํ
4. ๋ฌธ์ ํด๊ฒฐ (์์ฃผ ๋ฐ์ํ๋ ์ค๋ฅ์ ํด๊ฒฐ๋ฒ)
5. ๋ค์ ๋จ๊ณ (์ถ๊ฐ ํ์ต ์๋ฃ)
์์ฑ ์์น:
- ๊ฐ ๋จ๊ณ๋ ๋ช
ํํ๊ณ ๋ฐ๋ผํ๊ธฐ ์ฝ๊ฒ
- ์คํฌ๋ฆฐ์ท์ด๋ ์ฝ๋ ์์ ์์น ํ์
- ์ด๋ณด์ ๊ด์ ์์ ์ค๋ช
- ์ค์ํ๊ธฐ ์ฌ์ด ๋ถ๋ถ ๊ฐ์กฐ
"""
# SEO ์ต์ ํ ๋ฉํ ๋ฐ์ดํฐ ์์ฑ
META_GENERATION = """
๋ค์ ๋ธ๋ก๊ทธ ๊ธ์ ๋ํ SEO ์ต์ ํ๋ ๋ฉํ ๋ฐ์ดํฐ๋ฅผ ์์ฑํด์ฃผ์ธ์.
๊ธ ์ ๋ชฉ: {title}
์ฃผ์ ๋ด์ฉ: {content_summary}
ํ๊ฒ ํค์๋: {keywords}
์์ฑํ ํญ๋ชฉ:
1. ๋ฉํ ์ ๋ชฉ (50-60์, ํค์๋ ํฌํจ)
2. ๋ฉํ ์ค๋ช
(150-160์, ํด๋ฆญ ์ ๋)
3. URL ์ฌ๋ฌ๊ทธ (๊ฐ๊ฒฐํ๊ณ ํค์๋ ํฌํจ)
4. ํ๊ทธ 5-7๊ฐ (๊ด๋ จ์ฑ ๋์ ํค์๋)
5. ์นดํ
๊ณ ๋ฆฌ ์ถ์ฒ
JSON ํ์์ผ๋ก ์ถ๋ ฅํด์ฃผ์ธ์.
"""
@staticmethod
def get_prompt(template_type, **kwargs):
"""
ํ๋กฌํํธ ํ
ํ๋ฆฟ์ ๊ฐ์ ธ์์ ๋ณ์๋ฅผ ์ฑ์ ๋ฐํ
Args:
template_type: ํ
ํ๋ฆฟ ์ข
๋ฅ
**kwargs: ํ
ํ๋ฆฟ์ ๋ค์ด๊ฐ ๋ณ์๋ค
Returns:
์์ฑ๋ ํ๋กฌํํธ ๋ฌธ์์ด
"""
templates = {
'base': PromptTemplates.BLOG_POST_BASE,
'informative': PromptTemplates.INFORMATIVE_POST,
'review': PromptTemplates.REVIEW_POST,
'tutorial': PromptTemplates.TUTORIAL_POST,
'meta': PromptTemplates.META_GENERATION
}
template = templates.get(template_type)
if not template:
raise ValueError(f"์ ์ ์๋ ํ
ํ๋ฆฟ ํ์
: {template_type}")
try:
return template.format(**kwargs)
except KeyError as e:
raise ValueError(f"ํ์ ๋ณ์๊ฐ ๋๋ฝ๋์์ต๋๋ค: {e}")
@staticmethod
def get_system_prompt():
"""์์คํ
ํ๋กฌํํธ (AI์ ์ญํ ์ ์)"""
return """
๋น์ ์ 10๋
๊ฒฝ๋ ฅ์ ์ ๋ฌธ ๋ธ๋ก๊ทธ ์๊ฐ์
๋๋ค.
๋ค์ํ ์ฃผ์ ์ ๋ํด ๋
์๋ค์ด ์ดํดํ๊ธฐ ์ฝ๊ณ ์ค์ฉ์ ์ธ ๊ธ์ ์์ฑํ๋ ๋ฐ ํ์ํฉ๋๋ค.
๋น์ ์ ๊ธ์ ํญ์:
- ๋
์ ์นํ์ ์ด๊ณ ๋ํํ๋ ๋ฏํ ํค
- ์ ํํ ์ ๋ณด์ ๊ตฌ์ฒด์ ์ธ ์์
- ์ ์ ํ ๊ตฌ์กฐ์ ๊ฐ๋
์ฑ
- SEO ์ต์ ํ
- ๋
์์๊ฒ ์ค์ง์ ์ธ ๊ฐ์น ์ ๊ณต
ํ๊ตญ์ด๋ก ์์ฐ์ค๋ฝ๊ณ ์ ์ฐฝํ๊ฒ ์์ฑํ๋ฉฐ, ๋ฌธํ์ ๋งฅ๋ฝ์ ์ ์ดํดํฉ๋๋ค.
"""
ํ๋กฌํํธ๋ ๊ณ์ ๊ฐ์ ํด๋๊ฐ์ผ ํด. ์์ฑ๋ ๊ธ์ ํ์ง์ ๋ณด๋ฉด์ ํ๋กฌํํธ๋ฅผ ์กฐ๊ธ์ฉ ์์ ํ๋ค ๋ณด๋ฉด, ์ ์ ๋ ์ข์ ๊ฒฐ๊ณผ๋ฌผ์ด ๋์. ๋ด ๊ฒฝํ์ ํ๋กฌํํธ ํ์ง์ด ์ต์ข ๊ธ ํ์ง์ 70%๋ฅผ ๊ฒฐ์ ํด!
Step 4: ์ฃผ์ ์์ฑ๊ธฐ ๊ตฌํ
์ด์ ์ค์ ๋ก ๋ธ๋ก๊ทธ ์ฃผ์ ๋ฅผ ์๋์ผ๋ก ์์ฑํ๋ ๋ชจ๋์ ๋ง๋ค์ด๋ณด์.
modules/topic_generator.py:
import requests
import json
from datetime import datetime, timedelta
from typing import List, Dict
import random
class TopicGenerator:
"""๋ธ๋ก๊ทธ ์ฃผ์ ์๋ ์์ฑ ํด๋์ค"""
def __init__(self):
self.topics_db_path = './data/topics.json'
self.published_db_path = './data/published.json'
self.load_databases()
def load_databases(self):
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ผ ๋ก๋"""
try:
with open(self.topics_db_path, 'r', encoding='utf-8') as f:
self.topics_db = json.load(f)
except FileNotFoundError:
self.topics_db = {'topics': [], 'last_updated': None}
try:
with open(self.published_db_path, 'r', encoding='utf-8') as f:
self.published_db = json.load(f)
except FileNotFoundError:
self.published_db = {'posts': []}
def save_databases(self):
"""๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ผ ์ ์ฅ"""
with open(self.topics_db_path, 'w', encoding='utf-8') as f:
json.dump(self.topics_db, f, ensure_ascii=False, indent=2)
with open(self.published_db_path, 'w', encoding='utf-8') as f:
json.dump(self.published_db, f, ensure_ascii=False, indent=2)
def get_trending_keywords(self, category: str = None) -> List[str]:
"""
ํธ๋ ๋ฉ ํค์๋ ์์ง
์ค์ ๋ก๋ Google Trends API๋ ๋ค์ด๋ฒ ๋ฐ์ดํฐ๋ฉ API ์ฌ์ฉ
์ฌ๊ธฐ์๋ ์์๋ก ๊ฐ๋จํ ๊ตฌํ
"""
# ์ค์ ๊ตฌํ ์์๋ pytrends ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
# from pytrends.request import TrendReq
# ์์ ํค์๋ (์ค์ ๋ก๋ API์์ ๊ฐ์ ธ์ด)
trending_keywords = {
'tech': ['AI ์๋ํ', '์ฑGPT ํ์ฉ๋ฒ', '๋
ธ์ฝ๋ ๋๊ตฌ', '์์ฐ์ฑ ์ฑ'],
'lifestyle': ['๋ฏธ๋๋ฉ๋ฆฌ์ฆ', '์ฌํ
ํฌ', '์๊ธฐ๊ณ๋ฐ', '๊ฑด๊ฐ ๋ฃจํด'],
'business': ['์ฌ์ด๋ ํ๋ก์ ํธ', '์จ๋ผ์ธ ๋น์ฆ๋์ค', '๋ง์ผํ
์ ๋ต'],
}
if category and category in trending_keywords:
return trending_keywords[category]
# ๋ชจ๋ ์นดํ
๊ณ ๋ฆฌ์์ ๋๋ค ์ ํ
all_keywords = []
for keywords in trending_keywords.values():
all_keywords.extend(keywords)
return random.sample(all_keywords, min(5, len(all_keywords)))
def analyze_keyword_potential(self, keyword: str) -> Dict:
"""
ํค์๋์ ์ ์ฌ๋ ฅ ๋ถ์
- ๊ฒ์๋
- ๊ฒฝ์๋
- ์์ต์ฑ
"""
# ์ค์ ๋ก๋ ํค์๋ ๋๊ตฌ API ์ฌ์ฉ
# ์ฌ๊ธฐ์๋ ์์๋ก ๋๋ค ์ ์ ์์ฑ
return {
'keyword': keyword,
'search_volume': random.randint(1000, 50000),
'competition': random.uniform(0.3, 0.9),
'cpc': random.uniform(100, 2000), # ํด๋ฆญ๋น ๋น์ฉ (์)
'potential_score': random.uniform(60, 95)
}
def generate_topic_ideas(self, base_keyword: str, count: int = 10) -> List[Dict]:
"""
๊ธฐ๋ณธ ํค์๋๋ฅผ ๋ฐํ์ผ๋ก ๋ค์ํ ์ฃผ์ ์์ด๋์ด ์์ฑ
"""
# ์ฃผ์ ๋ณํ ํจํด
patterns = [
f"{base_keyword} ์๋ฒฝ ๊ฐ์ด๋",
f"{base_keyword} ์ด๋ณด์๋ฅผ ์ํ ํ",
f"{base_keyword}๋ก ์์ต ๋ด๋ ๋ฐฉ๋ฒ",
f"{base_keyword} ์ถ์ฒ TOP 10",
f"{base_keyword} ์ค์ ํ์ฉ๋ฒ",
f"{base_keyword} ์ฅ๋จ์ ๋น๊ต",
f"{base_keyword} ์์ํ๊ธฐ ์ ์ ์์์ผ ํ ๊ฒ๋ค",
f"2024๋
{base_keyword} ํธ๋ ๋",
f"{base_keyword} ์ฑ๊ณต ์ฌ๋ก ๋ถ์",
f"{base_keyword} ์์ฃผ ๋ฌป๋ ์ง๋ฌธ FAQ"
]
topics = []
for pattern in patterns[:count]:
topic = {
'title': pattern,
'keyword': base_keyword,
'type': self._determine_post_type(pattern),
'priority': random.uniform(0.5, 1.0),
'created_at': datetime.now().isoformat(),
'status': 'pending'
}
topics.append(topic)
return topics
def _determine_post_type(self, title: str) -> str:
"""์ ๋ชฉ์ ๋ณด๊ณ ๊ธ ์ ํ ํ๋จ"""
if '๊ฐ์ด๋' in title or '๋ฐฉ๋ฒ' in title:
return 'tutorial'
elif '๋น๊ต' in title or '์ถ์ฒ' in title:
return 'review'
elif 'FAQ' in title or '์ง๋ฌธ' in title:
return 'informative'
else:
return 'base'
def is_topic_already_published(self, topic_title: str) -> bool:
"""์ด๋ฏธ ๋ฐํ๋ ์ฃผ์ ์ธ์ง ํ์ธ"""
for post in self.published_db['posts']:
if post['title'] == topic_title:
return True
return False
def get_next_topic(self) -> Dict:
"""
๋ค์์ ์์ฑํ ์ฃผ์ ์ ํ
์ฐ์ ์์๊ฐ ๋๊ณ ์์ง ๋ฐํ๋์ง ์์ ์ฃผ์
"""
available_topics = [
topic for topic in self.topics_db['topics']
if topic['status'] == 'pending'
and not self.is_topic_already_published(topic['title'])
]
if not available_topics:
# ์๋ก์ด ์ฃผ์ ์์ฑ
keywords = self.get_trending_keywords()
for keyword in keywords:
new_topics = self.generate_topic_ideas(keyword, count=3)
self.topics_db['topics'].extend(new_topics)
self.save_databases()
available_topics = [t for t in self.topics_db['topics'] if t['status'] == 'pending']
# ์ฐ์ ์์ ์์ผ๋ก ์ ๋ ฌ
available_topics.sort(key=lambda x: x['priority'], reverse=True)
return available_topics[0] if available_topics else None
def mark_topic_as_used(self, topic_title: str):
"""์ฃผ์ ๋ฅผ ์ฌ์ฉ๋จ์ผ๋ก ํ์"""
for topic in self.topics_db['topics']:
if topic['title'] == topic_title:
topic['status'] = 'used'
topic['used_at'] = datetime.now().isoformat()
break
self.save_databases()
def update_topic_database(self):
"""
์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ
์ฃผ๊ธฐ์ ์ผ๋ก ์คํํ์ฌ ์๋ก์ด ํธ๋ ๋ ๋ฐ์
"""
print("๐ ์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ ์ค...")
# ํธ๋ ๋ฉ ํค์๋ ์์ง
categories = ['tech', 'lifestyle', 'business']
all_keywords = []
for category in categories:
keywords = self.get_trending_keywords(category)
all_keywords.extend(keywords)
# ๊ฐ ํค์๋์ ๋ํ ์ฃผ์ ์์ฑ
new_topics_count = 0
for keyword in all_keywords:
# ํค์๋ ์ ์ฌ๋ ฅ ๋ถ์
analysis = self.analyze_keyword_potential(keyword)
# ์ ์ฌ๋ ฅ์ด ๋์ ํค์๋๋ง ์ ํ
if analysis['potential_score'] > 70:
topics = self.generate_topic_ideas(keyword, count=5)
# ์ค๋ณต ์ฒดํฌ ํ ์ถ๊ฐ
for topic in topics:
if not any(t['title'] == topic['title'] for t in self.topics_db['topics']):
topic['analysis'] = analysis
self.topics_db['topics'].append(topic)
new_topics_count += 1
self.topics_db['last_updated'] = datetime.now().isoformat()
self.save_databases()
print(f"โ
{new_topics_count}๊ฐ์ ์๋ก์ด ์ฃผ์ ๊ฐ ์ถ๊ฐ๋์์ต๋๋ค!")
return new_topics_count
# ์ฌ์ฉ ์์
if __name__ == "__main__":
generator = TopicGenerator()
# ์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ
generator.update_topic_database()
# ๋ค์ ์ฃผ์ ๊ฐ์ ธ์ค๊ธฐ
next_topic = generator.get_next_topic()
print(f"\n๋ค์ ์์ฑ ์ฃผ์ : {next_topic['title']}")
print(f"ํค์๋: {next_topic['keyword']}")
print(f"๊ธ ์ ํ: {next_topic['type']}")
print(f"์ฐ์ ์์: {next_topic['priority']:.2f}")
์ฃผ์ ์์ฑ๊ธฐ๋ ์ผ์ฃผ์ผ์ ํ ๋ฒ ์ ๋ ์คํํด์ ์ฃผ์ ํ์ ์ฑ์๋๋ ๊ฒ ์ข์. ๊ทธ๋ฌ๋ฉด ๋งค์ผ ๊ธ์ ์ธ ๋๋ง๋ค ์๋ก ์ฃผ์ ๋ฅผ ์ฐพ์ ํ์๊ฐ ์์ด์ ํจ์จ์ ์ด์ผ. ๋๋ ๋งค์ฃผ ์ผ์์ผ ๋ฐค์ ์๋์ผ๋ก ์คํ๋๋๋ก ์ค์ผ์ค๋ฌ๋ฅผ ์ค์ ํด๋์ด.
Step 5: ์ฝํ ์ธ ์์ฑ๊ธฐ ๊ตฌํ
๋๋์ด ํต์ฌ! AI๋ฅผ ํ์ฉํด์ ์ค์ ๊ธ์ ์์ฑํ๋ ๋ชจ๋์ด์ผ.
modules/content_creator.py:
import openai
from anthropic import Anthropic
import google.generativeai as genai
from typing import Dict, Optional
import time
import json
from config.settings import Settings
from config.prompts import PromptTemplates
class ContentCreator:
"""AI๋ฅผ ํ์ฉํ ์ฝํ
์ธ ์์ฑ ํด๋์ค"""
def __init__(self, ai_provider: str = 'openai'):
"""
Args:
ai_provider: ์ฌ์ฉํ AI ์ ๊ณต์ ('openai', 'anthropic', 'google')
"""
self.ai_provider = ai_provider
self.setup_ai_client()
self.prompt_templates = PromptTemplates()
def setup_ai_client(self):
"""AI ํด๋ผ์ด์ธํธ ์ด๊ธฐํ"""
if self.ai_provider == 'openai':
openai.api_key = Settings.OPENAI_API_KEY
self.model = Settings.OPENAI_MODEL
elif self.ai_provider == 'anthropic':
self.client = Anthropic(api_key=Settings.ANTHROPIC_API_KEY)
self.model = Settings.ANTHROPIC_MODEL
elif self.ai_provider == 'google':
genai.configure(api_key=Settings.GOOGLE_API_KEY)
self.model = genai.GenerativeModel(Settings.GOOGLE_MODEL)
def generate_content(self, topic: Dict, retry_count: int = 3) -> Optional[Dict]:
"""
์ฃผ์ ๋ฅผ ๋ฐํ์ผ๋ก ๋ธ๋ก๊ทธ ์ฝํ
์ธ ์์ฑ
Args:
topic: ์ฃผ์ ์ ๋ณด ๋์
๋๋ฆฌ
retry_count: ์คํจ ์ ์ฌ์๋ ํ์
Returns:
์์ฑ๋ ์ฝํ
์ธ ๋์
๋๋ฆฌ ๋๋ None
"""
print(f"โ๏ธ '{topic['title']}' ์ฃผ์ ๋ก ๊ธ ์์ฑ ์ค...")
for attempt in range(retry_count):
try:
# ํ๋กฌํํธ ์์ฑ
prompt = self._create_prompt(topic)
# AI ํธ์ถ
content = self._call_ai(prompt)
if content:
# ๋ฉํ ๋ฐ์ดํฐ ์์ฑ
meta_data = self._generate_metadata(topic, content)
# ๊ฒฐ๊ณผ ๊ตฌ์ฑ
result = {
'title': topic['title'],
'content': content,
'meta': meta_data,
'topic': topic,
'created_at': time.strftime('%Y-%m-%d %H:%M:%S'),
'ai_provider': self.ai_provider,
'word_count': len(content)
}
print(f"โ
๊ธ ์์ฑ ์๋ฃ! (์ด {len(content)}์)")
return result
except Exception as e:
print(f"โ ๏ธ ์๋ {attempt + 1}/{retry_count} ์คํจ: {str(e)}")
if attempt < retry_count - 1:
time.sleep(2 ** attempt) # ์ง์ ๋ฐฑ์คํ
else:
print(f"โ ๊ธ ์์ฑ ์คํจ: {str(e)}")
return None
return None
def _create_prompt(self, topic: Dict) -> str:
"""์ฃผ์ ์ ๋ง๋ ํ๋กฌํํธ ์์ฑ"""
post_type = topic.get('type', 'base')
# ๊ธฐ๋ณธ ํ๋ผ๋ฏธํฐ
params = {
'topic': topic['title'],
'keywords': topic.get('keyword', ''),
'post_type': post_type,
'target_audience': '์ผ๋ฐ ๋
์',
'min_words': Settings.MIN_WORD_COUNT,
'additional_requirements': ''
}
# ๊ธ ์ ํ๋ณ ์ถ๊ฐ ์๊ตฌ์ฌํญ
if post_type == 'tutorial':
params['difficulty_level'] = '์ด๊ธ-์ค๊ธ'
return self.prompt_templates.get_prompt('tutorial', **params)
elif post_type == 'review':
params['comparison_items'] = '๊ด๋ จ ์ ํ/์๋น์ค'
return self.prompt_templates.get_prompt('review', **params)
elif post_type == 'informative':
return self.prompt_templates.get_prompt('informative', **params)
else:
return self.prompt_templates.get_prompt('base', **params)
def _call_ai(self, prompt: str) -> Optional[str]:
"""AI API ํธ์ถ"""
system_prompt = self.prompt_templates.get_system_prompt()
if self.ai_provider == 'openai':
response = openai.ChatCompletion.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=4000
)
return response.choices[0].message.content
elif self.ai_provider == 'anthropic':
response = self.client.messages.create(
model=self.model,
max_tokens=4000,
system=system_prompt,
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
elif self.ai_provider == 'google':
full_prompt = f"{system_prompt}\n\n{prompt}"
response = self.model.generate_content(full_prompt)
return response.text
return None
def _generate_metadata(self, topic: Dict, content: str) -> Dict:
"""SEO ๋ฉํ๋ฐ์ดํฐ ์์ฑ"""
# ๋ด์ฉ ์์ฝ (์ฒซ 200์)
content_summary = content[:200] + "..."
meta_prompt = self.prompt_templates.get_prompt(
'meta',
title=topic['title'],
content_summary=content_summary,
keywords=topic.get('keyword', '')
)
try:
meta_response = self._call_ai(meta_prompt)
# JSON ํ์ฑ
meta_data = json.loads(meta_response)
return meta_data
except:
# ์คํจ ์ ๊ธฐ๋ณธ ๋ฉํ๋ฐ์ดํฐ
return {
'meta_title': topic['title'],
'meta_description': content_summary,
'slug': topic['title'].replace(' ', '-').lower(),
'tags': [topic.get('keyword', '')],
'category': '์ผ๋ฐ'
}
def enhance_content(self, content: str) -> str:
"""
์ฝํ
์ธ ๊ฐ์ (์ด๋ฏธ์ง ์์น ํ์, ๋ด๋ถ ๋งํฌ ๋ฑ)
"""
enhanced = content
# ์ด๋ฏธ์ง ์ฝ์
์์น ํ์
paragraphs = enhanced.split('\n\n')
if len(paragraphs) > 3:
# ์ค๊ฐ ์ง์ ์ ์ด๋ฏธ์ง ํ์ ์ถ๊ฐ
mid_point = len(paragraphs) // 2
paragraphs.insert(mid_point, "[์ด๋ฏธ์ง ์ฝ์
์์น]")
enhanced = '\n\n'.join(paragraphs)
# ๋ด๋ถ ๋งํฌ ๊ธฐํ ์ฐพ๊ธฐ (๊ฐ๋จํ ์์)
# ์ค์ ๋ก๋ ๋ ์ ๊ตํ ๋ก์ง ํ์
return enhanced
def batch_generate(self, topics: list, max_concurrent: int = 3) -> list:
"""
์ฌ๋ฌ ์ฃผ์ ์ ๋ํด ๋ฐฐ์น๋ก ์ฝํ
์ธ ์์ฑ
Args:
topics: ์ฃผ์ ๋ฆฌ์คํธ
max_concurrent: ๋์ ์ฒ๋ฆฌ ๊ฐ์
Returns:
์์ฑ๋ ์ฝํ
์ธ ๋ฆฌ์คํธ
"""
results = []
for i, topic in enumerate(topics):
print(f"\n[{i+1}/{len(topics)}] ์ฒ๋ฆฌ ์ค...")
content = self.generate_content(topic)
if content:
results.append(content)
# API ๋ ์ดํธ ๋ฆฌ๋ฐ ๊ณ ๋ ค
if i < len(topics) - 1:
time.sleep(2)
return results
# ์ฌ์ฉ ์์
if __name__ == "__main__":
from modules.topic_generator import TopicGenerator
# ์ฃผ์ ๊ฐ์ ธ์ค๊ธฐ
topic_gen = TopicGenerator()
topic = topic_gen.get_next_topic()
# ์ฝํ
์ธ ์์ฑ
creator = ContentCreator(ai_provider='openai')
content = creator.generate_content(topic)
if content:
print(f"\n์์ฑ๋ ๊ธ:")
print(f"์ ๋ชฉ: {content['title']}")
print(f"๊ธ์ ์: {content['word_count']}")
print(f"๋ฉํ ์ ๋ชฉ: {content['meta']['meta_title']}")
print(f"\n๋ณธ๋ฌธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ:")
print(content['content'][:500] + "...")
AI API๋ ์ฌ์ฉ๋์ ๋ฐ๋ผ ๋น์ฉ์ด ๋ฐ์ํด. ํนํ GPT-4๋ ๋น์ผ ํธ์ด๋๊น ์ฃผ์ํด์ผ ํด:
โข ๊ฐ๋ฐ/ํ ์คํธ ๋จ๊ณ์์๋ Gemini ๋ฌด๋ฃ ํฐ์ด ํ์ฉ
โข ํ๋กฌํํธ๋ฅผ ์ต์ ํํด์ ํ ํฐ ์ ์ค์ด๊ธฐ
โข ์ผ์ผ ์์ฑ ๊ฐ์ ์ ํ ์ค์
โข ์๋ณ ์์ฐ ์๋ฆผ ์ค์
๋ด ๊ฒฝ์ฐ ํ๋ฃจ 3ํธ ๊ธฐ์ค์ผ๋ก GPT-4 ์ฌ์ฉ ์ ์ 5-7๋ง์ ์ ๋ ๋์. ๋ธ๋ก๊ทธ ์์ต์ด ์ด๋ณด๋ค ๋์ผ๋ฉด OK!
๐ ํ์ง ๊ฒ์ฆ ์์คํ ๊ตฌ์ถํ๊ธฐ
AI๊ฐ ๋ง๋ ๊ธ์ด๋ผ๊ณ ๋ฌด์กฐ๊ฑด ๋ฐํํ๋ฉด ์ ๋ผ. ํ์ง ์ฒดํฌ๋ ํ์์ผ! ๋ ์๋ค์ ๊ธ๋ฐฉ ์์์ฐจ๋ฆฌ๊ฑฐ๋ . ๐
ํ์ง ๊ฒ์ฆ ๋ชจ๋ ๊ตฌํ
modules/quality_checker.py:
import re
from typing import Dict, List, Tuple
import requests
from collections import Counter
class QualityChecker:
"""์ฝํ
์ธ ํ์ง ๊ฒ์ฆ ํด๋์ค"""
def __init__(self):
self.min_score = 70 # ์ต์ ํ์ง ์ ์
self.checks = {
'readability': 25, # ๊ฐ๋
์ฑ (25์ )
'structure': 20, # ๊ตฌ์กฐ (20์ )
'keyword_density': 15, # ํค์๋ ๋ฐ๋ (15์ )
'uniqueness': 20, # ๋
์ฐฝ์ฑ (20์ )
'grammar': 20 # ๋ฌธ๋ฒ (20์ )
}
def check_quality(self, content: Dict) -> Tuple[bool, Dict]:
"""
์ ์ฒด ํ์ง ๊ฒ์ฌ ์ํ
Returns:
(ํต๊ณผ ์ฌ๋ถ, ์์ธ ์ ์)
"""
print(f"๐ ํ์ง ๊ฒ์ฆ ์์: {content['title']}")
scores = {}
# 1. ๊ฐ๋
์ฑ ๊ฒ์ฌ
scores['readability'] = self._check_readability(content['content'])
# 2. ๊ตฌ์กฐ ๊ฒ์ฌ
scores['structure'] = self._check_structure(content['content'])
# 3. ํค์๋ ๋ฐ๋ ๊ฒ์ฌ
scores['keyword_density'] = self._check_keyword_density(
content['content'],
content['topic'].get('keyword', '')
)
# 4. ๋
์ฐฝ์ฑ ๊ฒ์ฌ
scores['uniqueness'] = self._check_uniqueness(content['content'])
# 5. ๋ฌธ๋ฒ ๊ฒ์ฌ
scores['grammar'] = self._check_grammar(content['content'])
# ์ด์ ๊ณ์ฐ
total_score = sum(scores.values())
# ๊ฒฐ๊ณผ ๊ตฌ์ฑ
result = {
'total_score': total_score,
'scores': scores,
'passed': total_score >= self.min_score,
'feedback': self._generate_feedback(scores)
}
print(f"๐ ์ด์ : {total_score}/100")
print(f"{'โ
ํต๊ณผ' if result['passed'] else 'โ ๋ถํฉ๊ฒฉ'}")
return result['passed'], result
def _check_readability(self, content: str) -> float:
"""๊ฐ๋
์ฑ ์ ์ ๊ณ์ฐ"""
score = 0
max_score = self.checks['readability']
# ๋ฌธ์ฅ ๋ถ์
sentences = re.split(r'[.!?]+', content)
sentences = [s.strip() for s in sentences if s.strip()]
if not sentences:
return 0
# ํ๊ท ๋ฌธ์ฅ ๊ธธ์ด (์ ์ : 15-30์)
avg_sentence_length = sum(len(s) for s in sentences) / len(sentences)
if 15 <= avg_sentence_length <= 30:
score += max_score * 0.4
elif 10 <= avg_sentence_length <= 40:
score += max_score * 0.2
# ๋จ๋ฝ ๊ฐ์ (์ ์ : 5๊ฐ ์ด์)
paragraphs = content.split('\n\n')
paragraphs = [p for p in paragraphs if p.strip()]
if len(paragraphs) >= 5:
score += max_score * 0.3
elif len(paragraphs) >= 3:
score += max_score * 0.15
# ์์ ๋ชฉ ์ฌ์ฉ (##, ###๋ก ์์ํ๋ ์ค)
subheadings = len(re.findall(r'^#{2,3}\s+.+$', content, re.MULTILINE))
if subheadings >= 3:
score += max_score * 0.3
elif subheadings >= 1:
score += max_score * 0.15
return min(score, max_score)
def _check_structure(self, content: str) -> float:
"""๊ธ ๊ตฌ์กฐ ์ ์ ๊ณ์ฐ"""
score = 0
max_score = self.checks['structure']
# ์ ๋ชฉ ๊ณ์ธต ๊ตฌ์กฐ ํ์ธ
h1_count = len(re.findall(r'^#\s+.+$', content, re.MULTILINE))
h2_count = len(re.findall(r'^##\s+.+$', content, re.MULTILINE))
h3_count = len(re.findall(r'^###\s+.+$', content, re.MULTILINE))
# H1์ 1๊ฐ๋ง
if h1_count == 1:
score += max_score * 0.3
# H2๋ 3๊ฐ ์ด์
if h2_count >= 3:
score += max_score * 0.4
elif h2_count >= 1:
score += max_score * 0.2
# H3 ์ ์ ํ ์ฌ์ฉ
if h3_count > 0:
score += max_score * 0.3
return min(score, max_score)
def _check_keyword_density(self, content: str, keyword: str) -> float:
"""ํค์๋ ๋ฐ๋ ์ ์ ๊ณ์ฐ"""
score = 0
max_score = self.checks['keyword_density']
if not keyword:
return max_score * 0.5 # ํค์๋ ์์ผ๋ฉด ์ค๊ฐ ์ ์
# ํค์๋ ์ถํ ํ์
keyword_count = content.lower().count(keyword.lower())
total_words = len(content.split())
if total_words == 0:
return 0
# ํค์๋ ๋ฐ๋ ๊ณ์ฐ (์ ์ : 1-3%)
density = (keyword_count / total_words) * 100
if 1 <= density <= 3:
score = max_score
elif 0.5 <= density <= 5:
score = max_score * 0.7
elif density > 0:
score = max_score * 0.4
return score
def _check_uniqueness(self, content: str) -> float:
"""๋
์ฐฝ์ฑ ์ ์ ๊ณ์ฐ (๊ฐ๋จํ ๋ฒ์ )"""
score = 0
max_score = self.checks['uniqueness']
# ์ค์ ๋ก๋ ํ์ ๊ฒ์ฌ API ์ฌ์ฉ (Copyscape ๋ฑ)
# ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ๋ฐ๋ณต ๋ฌธ๊ตฌ ์ฒดํฌ
sentences = re.split(r'[.!?]+', content)
sentences = [s.strip().lower() for s in sentences if s.strip()]
if not sentences:
return 0
# ์ค๋ณต ๋ฌธ์ฅ ๋น์จ
unique_sentences = len(set(sentences))
total_sentences = len(sentences)
uniqueness_ratio = unique_sentences / total_sentences
score = max_score * uniqueness_ratio
return score
def _check_grammar(self, content: str) -> float:
"""๋ฌธ๋ฒ ์ ์ ๊ณ์ฐ"""
score = 0
max_score = self.checks['grammar']
# ์ค์ ๋ก๋ ๋ง์ถค๋ฒ ๊ฒ์ฌ API ์ฌ์ฉ (๋ค์ด๋ฒ, ๋ถ์ฐ๋ ๋ฑ)
# ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ๊ท์น ๊ธฐ๋ฐ ์ฒดํฌ
issues = 0
# ๊ธฐ๋ณธ์ ์ธ ๋ฌธ๋ฒ ์ค๋ฅ ํจํด
patterns = [
r'[๊ฐ-ํฃ]+\s+[๊ฐ-ํฃ]+\s+[๊ฐ-ํฃ]+\s+[๊ฐ-ํฃ]+\s+[๊ฐ-ํฃ]+\s+[๊ฐ-ํฃ]+\s+[๊ฐ-ํฃ]+', # ๋๋ฌด ๊ธด ๋จ์ด ๋์ด
r'\.\.+', # ์ฐ์๋ ๋ง์นจํ
r'\?\?+', # ์ฐ์๋ ๋ฌผ์ํ
r'!!+', # ์ฐ์๋ ๋๋ํ
]
for pattern in patterns:
issues += len(re.findall(pattern, content))
# ์ด์๊ฐ ์ ์์๋ก ๋์ ์ ์
if issues == 0:
score = max_score
elif issues <= 3:
score = max_score * 0.8
elif issues <= 5:
score = max_score * 0.6
else:
score = max_score * 0.4
return score
def _generate_feedback(self, scores: Dict) -> List[str]:
"""์ ์ ๊ธฐ๋ฐ ํผ๋๋ฐฑ ์์ฑ"""
feedback = []
for category, score in scores.items():
max_score = self.checks[category]
percentage = (score / max_score) * 100
if percentage < 60:
if category == 'readability':
feedback.append("โ ๏ธ ๊ฐ๋
์ฑ ๊ฐ์ ํ์: ๋ฌธ์ฅ์ ๋ ์งง๊ฒ, ๋จ๋ฝ์ ๋ช
ํํ๊ฒ ๊ตฌ๋ถํ์ธ์.")
elif category == 'structure':
feedback.append("โ ๏ธ ๊ตฌ์กฐ ๊ฐ์ ํ์: ์์ ๋ชฉ์ ๋ ์ถ๊ฐํ๊ณ ๊ณ์ธต์ ๋ช
ํํ ํ์ธ์.")
elif category == 'keyword_density':
feedback.append("โ ๏ธ ํค์๋ ์ต์ ํ ํ์: ์์ฐ์ค๋ฝ๊ฒ ํค์๋๋ฅผ ๋ ํฌํจํ์ธ์.")
elif category == 'uniqueness':
feedback.append("โ ๏ธ ๋
์ฐฝ์ฑ ๋ถ์กฑ: ๋ฐ๋ณต๋๋ ๋ด์ฉ์ ์ค์ด๊ณ ์๋ก์ด ๊ด์ ์ ์ถ๊ฐํ์ธ์.")
elif category == 'grammar':
feedback.append("โ ๏ธ ๋ฌธ๋ฒ ๊ฒํ ํ์: ๋ง์ถค๋ฒ๊ณผ ๋ฌธ์ฅ ๊ตฌ์กฐ๋ฅผ ๋ค์ ํ์ธํ์ธ์.")
if not feedback:
feedback.append("โ
๋ชจ๋ ํญ๋ชฉ์ด ์ฐ์ํฉ๋๋ค!")
return feedback
def suggest_improvements(self, content: Dict, quality_result: Dict) -> Dict:
"""๊ฐ์ ์ ์ ์์ฑ"""
suggestions = {
'priority': 'high' if quality_result['total_score'] < 60 else 'medium' if quality_result['total_score'] < 80 else 'low',
'actions': quality_result['feedback'],
'estimated_time': '10-15๋ถ' if quality_result['total_score'] < 60 else '5-10๋ถ' if quality_result['total_score'] < 80 else '๊ฒํ ๋ง ํ์'
}
return suggestions
# ์ฌ์ฉ ์์
if __name__ == "__main__":
checker = QualityChecker()
# ํ
์คํธ ์ฝํ
์ธ
test_content = {
'title': 'AI ์๋ํ ๊ฐ์ด๋',
'content': """
# AI ์๋ํ ์๋ฒฝ ๊ฐ์ด๋
AI ์๋ํ๋ ํ๋ ๋น์ฆ๋์ค์ ํ์ ์์์
๋๋ค.
## AI ์๋ํ๋?
AI ์๋ํ๋ ์ธ๊ณต์ง๋ฅ์ ํ์ฉํ์ฌ ๋ฐ๋ณต์ ์ธ ์์
์ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ธฐ์ ์
๋๋ค.
์ด๋ฅผ ํตํด ์๊ฐ๊ณผ ๋น์ฉ์ ์ ์ฝํ ์ ์์ต๋๋ค.
## ์ฃผ์ ์ฅ์
1. ์์ฐ์ฑ ํฅ์
2. ๋น์ฉ ์ ๊ฐ
3. ์ ํ๋ ๊ฐ์
## ์ค์ ํ์ฉ๋ฒ
AI ์๋ํ๋ฅผ ์ค์ ๋ก ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ด
์๋ค.
### ์์ํ๊ธฐ
๋จผ์ ๋ชฉํ๋ฅผ ๋ช
ํํ ์ค์ ํด์ผ ํฉ๋๋ค.
### ๋๊ตฌ ์ ํ
์ ์ ํ AI ๋๊ตฌ๋ฅผ ์ ํํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
""",
'topic': {'keyword': 'AI ์๋ํ'}
}
passed, result = checker.check_quality(test_content)
print(f"\n์์ธ ์ ์:")
for category, score in result['scores'].items():
print(f" {category}: {score:.1f}/{checker.checks[category]}")
print(f"\nํผ๋๋ฐฑ:")
for fb in result['feedback']:
print(f" {fb}")
๐ ์๋ ๋ฐํ ์์คํ ๊ตฌ์ถ
์ด์ ๋ง์ง๋ง ๋จ๊ณ! ๊ฒ์ฆ๋ ๊ธ์ ์ค์ ๋ธ๋ก๊ทธ์ ์๋์ผ๋ก ์ฌ๋ฆฌ๋ ์์คํ ์ ๋ง๋ค์ด๋ณด์. ๐
๋ฐํ ๊ด๋ฆฌ์ ๊ตฌํ
modules/publisher.py:
import requests
import json
from datetime import datetime, timedelta
import time
from typing import Dict, Optional
import random
from config.settings import Settings
class Publisher:
"""๋ธ๋ก๊ทธ ์๋ ๋ฐํ ๊ด๋ฆฌ ํด๋์ค"""
def __init__(self, platform: str = 'tistory'):
"""
Args:
platform: ๋ธ๋ก๊ทธ ํ๋ซํผ ('tistory', 'wordpress', 'naver')
"""
self.platform = platform
self.published_log_path = './data/published.json'
self.load_published_log()
def load_published_log(self):
"""๋ฐํ ๊ธฐ๋ก ๋ก๋"""
try:
with open(self.published_log_path, 'r', encoding='utf-8') as f:
self.published_log = json.load(f)
except FileNotFoundError:
self.published_log = {'posts': []}
def save_published_log(self):
"""๋ฐํ ๊ธฐ๋ก ์ ์ฅ"""
with open(self.published_log_path, 'w', encoding='utf-8') as f:
json.dump(self.published_log, f, ensure_ascii=False, indent=2)
def publish(self, content: Dict, schedule_time: Optional[datetime] = None) -> bool:
"""
์ฝํ
์ธ ๋ฐํ
Args:
content: ๋ฐํํ ์ฝํ
์ธ
schedule_time: ์์ฝ ๋ฐํ ์๊ฐ (None์ด๋ฉด ์ฆ์ ๋ฐํ)
Returns:
๋ฐํ ์ฑ๊ณต ์ฌ๋ถ
"""
print(f"๐ค '{content['title']}' ๋ฐํ ์ค๋น ์ค...")
# ์์ฝ ๋ฐํ์ธ ๊ฒฝ์ฐ ๋๊ธฐ
if schedule_time:
wait_seconds = (schedule_time - datetime.now()).total_seconds()
if wait_seconds > 0:
print(f"โฐ {schedule_time.strftime('%Y-%m-%d %H:%M')}์ ๋ฐํ ์์ฝ๋จ")
time.sleep(wait_seconds)
# ํ๋ซํผ๋ณ ๋ฐํ
try:
if self.platform == 'tistory':
result = self._publish_to_tistory(content)
elif self.platform == 'wordpress':
result = self._publish_to_wordpress(content)
elif self.platform == 'naver':
result = self._publish_to_naver(content)
else:
raise ValueError(f"์ง์ํ์ง ์๋ ํ๋ซํผ: {self.platform}")
if result:
# ๋ฐํ ๊ธฐ๋ก ์ ์ฅ
self._log_publication(content, result)
print(f"โ
๋ฐํ ์๋ฃ!")
return True
else:
print(f"โ ๋ฐํ ์คํจ")
return False
except Exception as e:
print(f"โ ๋ฐํ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}")
return False
def _publish_to_tistory(self, content: Dict) -> Optional[Dict]:
"""ํฐ์คํ ๋ฆฌ์ ๋ฐํ"""
url = "https://www.tistory.com/apis/post/write"
# ์นดํ
๊ณ ๋ฆฌ ID ๊ฐ์ ธ์ค๊ธฐ (์ฌ์ ์ ์ค์ ํ์)
category_id = self._get_category_id(content['meta'].get('category', ''))
# ๋ฐํ ๋ฐ์ดํฐ ๊ตฌ์ฑ
data = {
'access_token': Settings.TISTORY_ACCESS_TOKEN,
'output': 'json',
'blogName': Settings.TISTORY_BLOG_NAME,
'title': content['title'],
'content': self._format_content_for_tistory(content['content']),
'visibility': '3', # 0: ๋น๊ณต๊ฐ, 1: ๋ณดํธ, 3: ๋ฐํ
'category': category_id,
'tag': ','.join(content['meta'].get('tags', []))
}
response = requests.post(url, data=data)
if response.status_code == 200:
result = response.json()
if result.get('tistory', {}).get('status') == '200':
return {
'post_id': result['tistory']['postId'],
'url': result['tistory']['url']
}
return None
def _publish_to_wordpress(self, content: Dict) -> Optional[Dict]:
"""์๋ํ๋ ์ค์ ๋ฐํ"""
# ์๋ํ๋ ์ค REST API ์ฌ์ฉ
site_url = "https://your-site.com"
api_url = f"{site_url}/wp-json/wp/v2/posts"
# ์ธ์ฆ (Application Password ์ฌ์ฉ)
auth = ('username', 'application_password')
# ๋ฐํ ๋ฐ์ดํฐ
data = {
'title': content['title'],
'content': content['content'],
'status': 'publish',
'categories': [1], # ์นดํ
๊ณ ๋ฆฌ ID
'tags': content['meta'].get('tags', [])
}
response = requests.post(api_url, json=data, auth=auth)
if response.status_code == 201:
result = response.json()
return {
'post_id': result['id'],
'url': result['link']
}
return None
def _publish_to_naver(self, content: Dict) -> Optional[Dict]:
"""๋ค์ด๋ฒ ๋ธ๋ก๊ทธ์ ๋ฐํ (์
๋ ๋์ ์ฌ์ฉ)"""
# ๋ค์ด๋ฒ๋ ๊ณต์ API๊ฐ ์ ํ์ ์ด๋ผ ์
๋ ๋์ ์ฌ์ฉ
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
try:
# ํฌ๋กฌ ๋๋ผ์ด๋ฒ ์ค์
options = webdriver.ChromeOptions()
options.add_argument('--headless') # ๋ฐฑ๊ทธ๋ผ์ด๋ ์คํ
driver = webdriver.Chrome(options=options)
# ๋ค์ด๋ฒ ๋ก๊ทธ์ธ (์ฟ ํค ์ฌ์ฉ ๊ถ์ฅ)
driver.get("https://blog.naver.com")
# ๋ก๊ทธ์ธ ๋ก์ง...
# ๊ธ์ฐ๊ธฐ ํ์ด์ง๋ก ์ด๋
driver.get("https://blog.naver.com/PostWriteForm.naver")
# ์ ๋ชฉ ์
๋ ฅ
title_input = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "subject"))
)
title_input.send_keys(content['title'])
# ๋ณธ๋ฌธ ์
๋ ฅ (iframe ์ ํ ํ์)
driver.switch_to.frame("mainFrame")
content_area = driver.find_element(By.CSS_SELECTOR, ".se-content")
content_area.send_keys(content['content'])
driver.switch_to.default_content()
# ๋ฐํ ๋ฒํผ ํด๋ฆญ
publish_btn = driver.find_element(By.ID, "publish")
publish_btn.click()
time.sleep(2)
# ๋ฐํ๋ URL ๊ฐ์ ธ์ค๊ธฐ
current_url = driver.current_url
driver.quit()
return {
'post_id': current_url.split('/')[-1],
'url': current_url
}
except Exception as e:
print(f"๋ค์ด๋ฒ ๋ฐํ ์ค๋ฅ: {str(e)}")
if 'driver' in locals():
driver.quit()
return None
def _format_content_for_tistory(self, content: str) -> str:
"""ํฐ์คํ ๋ฆฌ์ฉ HTML ํฌ๋งทํ
"""
# ๋งํฌ๋ค์ด์ HTML๋ก ๋ณํ (๊ฐ๋จํ ๋ฒ์ )
html = content
# ์ ๋ชฉ ๋ณํ
html = re.sub(r'^# (.+)$', r'<h1>\1</h1>', html, flags=re.MULTILINE)
html = re.sub(r'^## (.+)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
html = re.sub(r'^### (.+)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
# ๋จ๋ฝ ๋ณํ
paragraphs = html.split('\n\n')
html = ''.join(f'<p>{p}</p>' for p in paragraphs if p.strip())
return html
def _get_category_id(self, category_name: str) -> str:
"""์นดํ
๊ณ ๋ฆฌ ์ด๋ฆ์ผ๋ก ID ์ฐพ๊ธฐ"""
# ์ค์ ๋ก๋ ํฐ์คํ ๋ฆฌ API๋ก ์นดํ
๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํ
# ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ๋งคํ
category_map = {
'AI ์๋ํ': '123456',
'ํ๋ก๊ทธ๋๋ฐ': '123457',
'์ผ๋ฐ': '0'
}
return category_map.get(category_name, '0')
def _log_publication(self, content: Dict, result: Dict):
"""๋ฐํ ๊ธฐ๋ก ์ ์ฅ"""
log_entry = {
'title': content['title'],
'post_id': result['post_id'],
'url': result['url'],
'published_at': datetime.now().isoformat(),
'platform': self.platform,
'word_count': content['word_count'],
'ai_provider': content['ai_provider']
}
self.published_log['posts'].append(log_entry)
self.save_published_log()
def schedule_posts(self, contents: list, posts_per_day: int = 3) -> list:
"""
์ฌ๋ฌ ๊ธ์ ์ค์ผ์ค๋ง
Args:
contents: ๋ฐํํ ์ฝํ
์ธ ๋ฆฌ์คํธ
posts_per_day: ํ๋ฃจ ๋ฐํ ๊ฐ์
Returns:
์ค์ผ์ค ๋ฆฌ์คํธ
"""
schedule = []
# ๋ฐํ ์๊ฐ๋ (์ค์ ์์ ๊ฐ์ ธ์ด)
time_slots = Settings.PUBLISH_TIMES
current_date = datetime.now()
content_index = 0
while content_index < len(contents):
for time_slot in time_slots:
if content_index >= len(contents):
break
# ๋ฐํ ์๊ฐ ๊ณ์ฐ
hour, minute = map(int, time_slot.split(':'))
publish_time = current_date.replace(
hour=hour,
minute=minute,
second=0
)
# ์ฝ๊ฐ์ ๋๋ค์ฑ ์ถ๊ฐ (ยฑ15๋ถ)
random_offset = timedelta(minutes=random.randint(-15, 15))
publish_time += random_offset
schedule.append({
'content': contents[content_index],
'scheduled_time': publish_time
})
content_index += 1
# ๋ค์ ๋ ๋ก
current_date += timedelta(days=1)
return schedule
def auto_publish_scheduled(self, schedule: list):
"""์ค์ผ์ค์ ๋ฐ๋ผ ์๋ ๋ฐํ"""
print(f"๐
{len(schedule)}๊ฐ ๊ธ ์๋ ๋ฐํ ์์")
for item in schedule:
success = self.publish(
item['content'],
schedule_time=item['scheduled_time']
)
if success:
print(f"โ
{item['content']['title']} ๋ฐํ ์๋ฃ")
else:
print(f"โ {item['content']['title']} ๋ฐํ ์คํจ")
# ๋ค์ ๋ฐํ๊น์ง ๋๊ธฐ
time.sleep(5)
# ์ฌ์ฉ ์์
if __name__ == "__main__":
publisher = Publisher(platform='tistory')
# ํ
์คํธ ์ฝํ
์ธ
test_content = {
'title': 'AI ์๋ํ ํ
์คํธ ๊ธ',
'content': '<h1>ํ
์คํธ</h1><p>์ด๊ฒ์ ํ
์คํธ ๊ธ์
๋๋ค.</p>',
'meta': {
'category': 'AI ์๋ํ',
'tags': ['AI', '์๋ํ', 'ํ
์คํธ']
},
'word_count': 100,
'ai_provider': 'openai'
}
# ์ฆ์ ๋ฐํ
# publisher.publish(test_content)
# ์์ฝ ๋ฐํ
schedule_time = datetime.now() + timedelta(hours=1)
publisher.publish(test_content, schedule_time=schedule_time)
โข ๋๋ฌด ๋น ๋ฅธ ์๋๋ก ์ฐ์ ๋ฐํ ๊ธ์ง
โข ์ฌ๋์ฒ๋ผ ๋ณด์ด๋๋ก ๋๋ค ๋๋ ์ด ์ถ๊ฐ
โข User-Agent ์ค์
โข ์ฟ ํค ๊ธฐ๋ฐ ๋ก๊ทธ์ธ ์ฌ์ฉ
โข ํ๋ฃจ ๋ฐํ ๊ฐ์ ์ ํ (3-5๊ฐ ๊ถ์ฅ)
์์งํ ๋ค์ด๋ฒ๋ ์๋ํ๊ฐ ๊น๋ค๋ก์์, ํฐ์คํ ๋ฆฌ๋ ์๋ํ๋ ์ค๋ฅผ ์ถ์ฒํด.
๐ฎ ์ ์ฒด ์์คํ ํตํฉ ๋ฐ ์คํ
์ด์ ๋ชจ๋ ๋ชจ๋์ ํ๋๋ก ํฉ์ณ์ ์์ ์๋ํ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
๋ฉ์ธ ์คํ ํ์ผ
main.py:
import schedule
import time
from datetime import datetime
import logging
from modules.topic_generator import TopicGenerator
from modules.content_creator import ContentCreator
from modules.quality_checker import QualityChecker
from modules.publisher import Publisher
from config.settings import Settings
# ๋ก๊น
์ค์
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('./data/automation.log'),
logging.StreamHandler()
]
)
class BlogAutomationSystem:
"""๋ธ๋ก๊ทธ ์๋ํ ํตํฉ ์์คํ
"""
def __init__(self):
self.topic_generator = TopicGenerator()
self.content_creator = ContentCreator(ai_provider='openai')
self.quality_checker = QualityChecker()
self.publisher = Publisher(platform='tistory')
self.daily_post_count = 0
self.max_daily_posts = Settings.POSTS_PER_DAY
logging.info("๐ ๋ธ๋ก๊ทธ ์๋ํ ์์คํ
์ด๊ธฐํ ์๋ฃ")
def create_and_publish_post(self):
"""๊ธ ์์ฑ ๋ฐ ๋ฐํ ์ ์ฒด ํ๋ก์ธ์ค"""
try:
logging.info("=" * 50)
logging.info("๐ ์ ๊ธ ์์ฑ ํ๋ก์ธ์ค ์์")
# 1. ์ฃผ์ ์ ํ
topic = self.topic_generator.get_next_topic()
if not topic:
logging.warning("โ ๏ธ ์ฌ์ฉ ๊ฐ๋ฅํ ์ฃผ์ ๊ฐ ์์ต๋๋ค.")
return False
logging.info(f"๐ ์ ํ๋ ์ฃผ์ : {topic['title']}")
# 2. ์ฝํ
์ธ ์์ฑ
content = self.content_creator.generate_content(topic)
if not content:
logging.error("โ ์ฝํ
์ธ ์์ฑ ์คํจ")
return False
# 3. ํ์ง ๊ฒ์ฆ
passed, quality_result = self.quality_checker.check_quality(content)
if not passed:
logging.warning(f"โ ๏ธ ํ์ง ๊ธฐ์ค ๋ฏธ๋ฌ (์ ์: {quality_result['total_score']})")
# ์ฌ์์ฑ ์๋
if quality_result['total_score'] >= 60:
logging.info("๐ ์ฝํ
์ธ ๊ฐ์ ํ ์ฌ์๋...")
# ์ฌ๊ธฐ์ AI์๊ฒ ํผ๋๋ฐฑ์ ์ฃผ๊ณ ์ฌ์์ฑ ์์ฒญ ๊ฐ๋ฅ
# ๊ฐ๋จํ ํ๊ธฐ ์ํด ์๋ต
return False
logging.info(f"โ
ํ์ง ๊ฒ์ฆ ํต๊ณผ (์ ์: {quality_result['total_score']})")
# 4. ๋ฐํ
if Settings.AUTO_PUBLISH:
success = self.publisher.publish(content)
if success:
# ์ฃผ์ ๋ฅผ ์ฌ์ฉ๋จ์ผ๋ก ํ์
self.topic_generator.mark_topic_as_used(topic['title'])
self.daily_post_count += 1
logging.info(f"๐ ๋ฐํ ์๋ฃ! (์ค๋ {self.daily_post_count}/{self.max_daily_posts})")
return True
else:
logging.error("โ ๋ฐํ ์คํจ")
return False
else:
logging.info("๐พ ์๋ ๋ฐํ ๋นํ์ฑํ - ์ฝํ
์ธ ์ ์ฅ๋ง ์ํ")
# ์ฝํ
์ธ ๋ฅผ ํ์ผ๋ก ์ ์ฅ
self._save_content_to_file(content)
return True
except Exception as e:
logging.error(f"โ ์ค๋ฅ ๋ฐ์: {str(e)}", exc_info=True)
return False
def _save_content_to_file(self, content: Dict):
"""์ฝํ
์ธ ๋ฅผ ํ์ผ๋ก ์ ์ฅ"""
import json
from datetime import datetime
filename = f"./data/drafts/{datetime.now().strftime('%Y%m%d_%H%M%S')}_{content['title'][:20]}.json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(content, f, ensure_ascii=False, indent=2)
logging.info(f"๐พ ์ฝํ
์ธ ์ ์ฅ: {filename}")
def daily_routine(self):
"""์ผ์ผ ๋ฃจํด ์คํ"""
logging.info("๐
์ผ์ผ ๋ฃจํด ์์")
# ์ผ์ผ ์นด์ดํฐ ๋ฆฌ์
self.daily_post_count = 0
# ์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ (์ฃผ 1ํ)
if datetime.now().weekday() == 6: # ์ผ์์ผ
logging.info("๐ ์ฃผ๊ฐ ์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ")
self.topic_generator.update_topic_database()
# ์ค๋ ๋ฐํํ ๊ธ ๊ฐ์๋งํผ ์์ฑ
for i in range(self.max_daily_posts):
logging.info(f"\n[{i+1}/{self.max_daily_posts}] ๊ธ ์์ฑ ์์")
success = self.create_and_publish_post()
if success:
# ๋ค์ ๊ธ๊น์ง ๋๊ธฐ (1-2์๊ฐ)
wait_minutes = 60 + (i * 30)
logging.info(f"โฐ {wait_minutes}๋ถ ํ ๋ค์ ๊ธ ์์ฑ ์์ ")
time.sleep(wait_minutes * 60)
else:
logging.warning("โ ๏ธ ๊ธ ์์ฑ ์คํจ - 30๋ถ ํ ์ฌ์๋")
time.sleep(30 * 60)
logging.info("๐ ์ผ์ผ ๋ฃจํด ์๋ฃ")
def run_scheduler(self):
"""์ค์ผ์ค๋ฌ ์คํ"""
logging.info("โฐ ์ค์ผ์ค๋ฌ ์์")
# ๋งค์ผ ์ค์ 9์์ ์ผ์ผ ๋ฃจํด ์คํ
schedule.every().day.at("09:00").do(self.daily_routine)
# ๋งค์ฃผ ์ผ์์ผ ์์ ์ ์ฃผ์ ์
๋ฐ์ดํธ
schedule.every().sunday.at("00:00").do(
self.topic_generator.update_topic_database
)
logging.info("๐
์ค์ผ์ค ๋ฑ๋ก ์๋ฃ")
logging.info(" - ๋งค์ผ 09:00: ์ผ์ผ ๋ฃจํด")
logging.info(" - ๋งค์ฃผ ์ผ์์ผ 00:00: ์ฃผ์ ์
๋ฐ์ดํธ")
# ๋ฌดํ ๋ฃจํ๋ก ์ค์ผ์ค ์คํ
while True:
schedule.run_pending()
time.sleep(60) # 1๋ถ๋ง๋ค ์ฒดํฌ
def run_once(self):
"""ํ ๋ฒ๋ง ์คํ (ํ
์คํธ์ฉ)"""
logging.info("๐งช ํ
์คํธ ๋ชจ๋: 1ํ ์คํ")
self.create_and_publish_post()
def generate_report(self):
"""์ฑ๊ณผ ๋ฆฌํฌํธ ์์ฑ"""
import json
with open('./data/published.json', 'r', encoding='utf-8') as f:
published_data = json.load(f)
total_posts = len(published_data['posts'])
if total_posts == 0:
logging.info("๐ ์์ง ๋ฐํ๋ ๊ธ์ด ์์ต๋๋ค.")
return
# ์ต๊ทผ 30์ผ ํต๊ณ
from datetime import datetime, timedelta
thirty_days_ago = datetime.now() - timedelta(days=30)
recent_posts = [
p for p in published_data['posts']
if datetime.fromisoformat(p['published_at']) > thirty_days_ago
]
logging.info("=" * 50)
logging.info("๐ ๋ธ๋ก๊ทธ ์๋ํ ์ฑ๊ณผ ๋ฆฌํฌํธ")
logging.info("=" * 50)
logging.info(f"์ด ๋ฐํ ๊ธ ์: {total_posts}๊ฐ")
logging.info(f"์ต๊ทผ 30์ผ ๋ฐํ: {len(recent_posts)}๊ฐ")
logging.info(f"์ผํ๊ท ๋ฐํ: {len(recent_posts) / 30:.1f}๊ฐ")
# AI ์ ๊ณต์๋ณ ํต๊ณ
ai_stats = {}
for post in published_data['posts']:
provider = post.get('ai_provider', 'unknown')
ai_stats[provider] = ai_stats.get(provider, 0) + 1
logging.info("\nAI ์ ๊ณต์๋ณ ์ฌ์ฉ:")
for provider, count in ai_stats.items():
logging.info(f" {provider}: {count}๊ฐ ({count/total_posts*100:.1f}%)")
logging.info("=" * 50)
def main():
"""๋ฉ์ธ ํจ์"""
import sys
system = BlogAutomationSystem()
# ๋ช
๋ นํ ์ธ์ ์ฒ๋ฆฌ
if len(sys.argv) > 1:
command = sys.argv[1]
if command == 'once':
# 1ํ ์คํ
system.run_once()
elif command == 'daily':
# ์ผ์ผ ๋ฃจํด ์คํ
system.daily_routine()
elif command == 'report':
# ๋ฆฌํฌํธ ์์ฑ
system.generate_report()
elif command == 'update-topics':
# ์ฃผ์ ์
๋ฐ์ดํธ๋ง
system.topic_generator.update_topic_database()
else:
print("์ฌ์ฉ๋ฒ: python main.py [once|daily|report|update-topics]")
else:
# ๊ธฐ๋ณธ: ์ค์ผ์ค๋ฌ ์คํ
system.run_scheduler()
if __name__ == "__main__":
main()
1ํ ํ ์คํธ ์คํ:
python main.py once์ผ์ผ ๋ฃจํด ์คํ:
python main.py daily์ค์ผ์ค๋ฌ ์คํ (24/7 ์๋ํ):
python main.py์ฑ๊ณผ ๋ฆฌํฌํธ:
python main.py report
์์คํ ๋ฐฐํฌ ๋ฐ ์ด์
์ด์ ์์คํ ์ ์ค์ ๋ก ์ด์ํ๋ ค๋ฉด ์๋ฒ์ ๋ฐฐํฌํด์ผ ํด. ๋ช ๊ฐ์ง ์ต์ ์ด ์์ด:
๊ฐ์ฅ ๊ฐ๋จํ์ง๋ง ์ปดํจํฐ๋ฅผ ๊ณ์ ์ผ๋์ผ ํด.
์ฅ์ :
โข ๋ฌด๋ฃ
โข ์ค์ ๊ฐ๋จ
โข ๋๋ฒ๊น ์ฌ์
๋จ์ :
โข ์ ๊ธฐ์ธ
โข ์ปดํจํฐ ๊บผ์ง๋ฉด ์ค๋จ
โข ์ธ๋ถ ์ ๊ทผ ์ด๋ ค์
์ ๋ฌธ์ ์ธ ์ด์์ ์ ํฉํด.
์ถ์ฒ: AWS EC2 t2.micro (ํ๋ฆฌํฐ์ด)
โข 1๋ ๊ฐ ๋ฌด๋ฃ
โข 24์๊ฐ ์ด์ ๊ฐ๋ฅ
โข ์์ ์
์ค์ ๋ฐฉ๋ฒ:
1. EC2 ์ธ์คํด์ค ์์ฑ (Ubuntu 22.04)
2. SSH ์ ์
3. Python ๋ฐ ํ์ ํจํค์ง ์ค์น
4. ์ฝ๋ ์ ๋ก๋ (Git ์ฌ์ฉ)
5. systemd๋ก ์๋น์ค ๋ฑ๋ก
6. ์๋ ์์ ์ค์
Python ์ ์ฉ ํธ์คํ ์๋น์ค.
์ฅ์ :
โข ์ค์ ๋งค์ฐ ๊ฐ๋จ
โข ๋ฌด๋ฃ ํ๋ ์ ๊ณต
โข ์น ์ธํฐํ์ด์ค
๋จ์ :
โข ๋ฌด๋ฃ ํ๋์ ์ ํ์
โข ์ธ๋ถ API ํธ์ถ ์ ํ
๊ฐ๊ฒฉ:
โข ๋ฌด๋ฃ: ์ ํ์
โข $5/์: ์ถฉ๋ถํ ๋ฆฌ์์ค
์ ๋ ดํ ๋ฏธ๋ ์๋ฒ.
์ฅ์ :
โข ์ด๊ธฐ ๋น์ฉ๋ง (5-10๋ง์)
โข ์ ๊ธฐ์ธ ์ ๋ ด
โข ์์ ํ ์ ์ด
๋จ์ :
โข ์ด๊ธฐ ์ค์ ํ์
โข ์ฑ๋ฅ ์ ํ์
โข ๋คํธ์ํฌ ์ค์ ํ์
๋ด ์ถ์ฒ์? ์ฒ์์ ๋ก์ปฌ์์ ํ ์คํธํ๊ณ , ์์ ํ๋๋ฉด AWS ํ๋ฆฌํฐ์ด๋ PythonAnywhere๋ก ์ด์ ํ๋ ๊ฑฐ์ผ.
systemd ์๋น์ค ๋ฑ๋ก (Linux)
์๋ฒ์์ ์๋์ผ๋ก ์์๋๊ณ ์ฌ์์๋๋๋ก ์ค์ ํ์.
/etc/systemd/system/blog-automation.service:
[Unit]
Description=Blog Automation System
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/blog_automation
Environment="PATH=/home/ubuntu/blog_automation/venv/bin"
ExecStart=/home/ubuntu/blog_automation/venv/bin/python main.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
์๋น์ค ๋ฑ๋ก ๋ฐ ์์:
# ์๋น์ค ๋ฑ๋ก
sudo systemctl daemon-reload
sudo systemctl enable blog-automation
# ์๋น์ค ์์
sudo systemctl start blog-automation
# ์ํ ํ์ธ
sudo systemctl status blog-automation
# ๋ก๊ทธ ํ์ธ
sudo journalctl -u blog-automation -f
๐ ์ฑ๊ณผ ์ธก์ ๋ฐ ์ต์ ํ
์์คํ ์ ์ด์ํ๋ฉด์ ๊ณ์ ๊ฐ์ ํด๋๊ฐ์ผ ํด. ๋ฐ์ดํฐ ๊ธฐ๋ฐ์ผ๋ก ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
์ฑ๊ณผ ์งํ ์ถ์
์ถ์ ํด์ผ ํ ํต์ฌ ์งํ๋ค:
| ์งํ | ๋ชฉํ | ์ธก์ ๋ฐฉ๋ฒ |
|---|---|---|
| ์ผ์ผ ๋ฐํ ๊ธ ์ | 3-5๊ฐ | ์์คํ ๋ก๊ทธ |
| ํ๊ท ํ์ง ์ ์ | 80์ ์ด์ | ํ์ง ๊ฒ์ฆ ๋ก๊ทธ |
| ๊ธ๋น ํ๊ท ์กฐํ์ | 100+ (์ฒซ ๋ฌ) | ๊ตฌ๊ธ ์ ๋๋ฆฌํฑ์ค |
| ๊ฒ์ ์ ์ ๋ฅ | 60% ์ด์ | ๊ตฌ๊ธ ์์น ์ฝ์ |
| ํ๊ท ์ฒด๋ฅ ์๊ฐ | 2๋ถ ์ด์ | ๊ตฌ๊ธ ์ ๋๋ฆฌํฑ์ค |
| ์ ์์ต | API ๋น์ฉ ์ด์ | ์ ๋์ผ์ค/์ ํด |
A/B ํ ์คํธ ์ ๋ต
์ด๋ค ๋ฐฉ์์ด ๋ ํจ๊ณผ์ ์ธ์ง ํ ์คํธํด๋ณด์:
โข ํจํด A: "์๋ฒฝ ๊ฐ์ด๋" ์คํ์ผ
โข ํจํด B: "์ด๋ณด์๋ฅผ ์ํ" ์คํ์ผ
โข ํจํด C: ์ซ์ ํฌํจ "TOP 10" ์คํ์ผ
๊ฐ ํจํด์ผ๋ก 10๊ฐ์ฉ ๊ธ์ ์ฐ๊ณ ์กฐํ์ ๋น๊ตํด๋ด. ๋ด ๊ฒฝํ์ ์ซ์ ํฌํจ ์ ๋ชฉ์ด 20-30% ๋ ๋์ ํด๋ฆญ๋ฅ ์ ๋ณด์์ด.
โข ์ค์ 9-11์
โข ์คํ 2-4์
โข ์ ๋ 7-9์
๊ฐ ์๊ฐ๋๋ณ๋ก ๋ฐํํด๋ณด๊ณ ์ด๊ธฐ ์ ์ ์ ๋น๊ตํด. ๋ธ๋ก๊ทธ ์ฃผ์ ์ ๋ฐ๋ผ ์ต์ ์๊ฐ์ด ๋ค๋ฅผ ์ ์์ด.
โข GPT-4: ํ์ง ์ต๊ณ , ๋น์ฉ ๋์
โข Claude: ๊ท ํ์กํ
โข Gemini: ๋น์ฉ ์ต์
๊ฐ ๋ชจ๋ธ๋ก ์์ฑํ ๊ธ์ ์ฑ๊ณผ๋ฅผ ๋น๊ตํด์ ROI๊ฐ ๊ฐ์ฅ ๋์ ๋ชจ๋ธ์ ์ ํํด.
์๋ ์ต์ ํ ์์คํ
์ฑ๊ณผ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ์๋์ผ๋ก ๊ฐ์ ํ๋ ์์คํ ์ ์ถ๊ฐํ ์ ์์ด:
class PerformanceOptimizer:
"""์ฑ๊ณผ ๊ธฐ๋ฐ ์๋ ์ต์ ํ"""
def __init__(self):
self.performance_data = self.load_performance_data()
def analyze_best_practices(self):
"""์ฑ๊ณผ ์ข์ ๊ธ์ ํจํด ๋ถ์"""
top_posts = sorted(
self.performance_data,
key=lambda x: x['views'],
reverse=True
)[:20]
# ๊ณตํต ํจํด ์ถ์ถ
patterns = {
'avg_title_length': sum(len(p['title']) for p in top_posts) / len(top_posts),
'common_keywords': self._extract_common_keywords(top_posts),
'optimal_word_count': sum(p['word_count'] for p in top_posts) / len(top_posts),
'best_publish_hours': self._find_best_hours(top_posts)
}
return patterns
def adjust_settings(self, patterns):
"""์ค์ ์๋ ์กฐ์ """
# ์ต์ ๋จ์ด ์ ๋ฐ์
Settings.MIN_WORD_COUNT = int(patterns['optimal_word_count'] * 0.9)
Settings.MAX_WORD_COUNT = int(patterns['optimal_word_count'] * 1.1)
# ์ต์ ๋ฐํ ์๊ฐ ๋ฐ์
Settings.PUBLISH_TIMES = patterns['best_publish_hours']
logging.info("โ๏ธ ์ค์ ์๋ ์กฐ์ ์๋ฃ")
logging.info(f" ๋ชฉํ ๋จ์ด ์: {Settings.MIN_WORD_COUNT}-{Settings.MAX_WORD_COUNT}")
logging.info(f" ๋ฐํ ์๊ฐ: {Settings.PUBLISH_TIMES}")
์ต์ 2-4์ฃผ ์ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ชจ์ ํ์ ์ต์ ํ๋ฅผ ์์ํด. ๋๋ฌด ๋นจ๋ฆฌ ๋ณ๊ฒฝํ๋ฉด ์ ํํ ํจํด์ ํ์ ํ๊ธฐ ์ด๋ ค์. ๊ทธ๋ฆฌ๊ณ ํ ๋ฒ์ ํ๋์ฉ๋ง ๋ณ๊ฒฝํด์ ์ด๋ค ์์๊ฐ ํจ๊ณผ๊ฐ ์๋์ง ๋ช ํํ ํ์ ํ์!
โ ๏ธ ์ฃผ์์ฌํญ ๋ฐ ์ค๋ฆฌ์ ๊ณ ๋ ค
AI ์๋ํ๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ง๋ง, ์ฑ ์๊ฐ ์๊ฒ ์ฌ์ฉํด์ผ ํด. ๋ช ๊ฐ์ง ์ค์ํ ์ฃผ์์ฌํญ์ ์์๋ณด์. ๐ค
๋์ฑ :
โข Copyscape ๊ฐ์ ํ์ ๊ฒ์ฌ ๋๊ตฌ ์ฌ์ฉ
โข ๋ ์ฐฝ์ฑ ์ ์ 70% ์ด์ ์ ์ง
โข ์์ฌ์ค๋ฌ์ด ๋ถ๋ถ์ ์๋ ๊ฒํ
โข ์ถ์ฒ๊ฐ ๋ช ํํ ์ ๋ณด๋ ์ธ์ฉ ํ์
๋์ฑ :
โข ์ค์ํ ์ฌ์ค์ ์ ๋ขฐํ ์ ์๋ ์ถ์ฒ๋ก ๊ฒ์ฆ
โข ํต๊ณ ๋ฐ์ดํฐ๋ ์๋ณธ ํ์ธ
โข ์ต์ ์ ๋ณด๋ ๋ฐํ ์ ์ฌํ์ธ
โข ๋ถํ์คํ ๋ด์ฉ์ "~๋ก ์๋ ค์ ธ ์๋ค" ๊ฐ์ ํํ ์ฌ์ฉ
ํฐ์คํ ๋ฆฌ: API ์ฌ์ฉ ํ์ฉ, ๊ณผ๋ํ ๋ฐํ ์ฃผ์
๋ค์ด๋ฒ: ์๋ํ ๊ฐ์ง ์ ์ ์ฌ ๊ฐ๋ฅ, ์ ์คํ๊ฒ ์ฌ์ฉ
์๋ํ๋ ์ค: ์์ ๋ก์, ์คํธ๋ง ์ฃผ์
์์ ํ ์ด์ ์์น:
โข ํ๋ฃจ 5๊ฐ ์ดํ ๋ฐํ
โข ๋ฐํ ๊ฐ๊ฒฉ ๋๋คํ
โข ํ์ง ์ ์ง
โข ๋ ์ ๋๊ธ์ ์ง์ ์๋ต
๊ผญ ๋ฐํ ํ์๋ ์์ง๋ง:
โข ํ์ง์ด ์ฌ๋์ด ์ด ๊ฒ๊ณผ ๋๋ฑํ๊ฑฐ๋ ๋ ์ข์์ผ ํจ
โข ๋ ์์๊ฒ ๊ฐ์น๋ฅผ ์ ๊ณตํด์ผ ํจ
โข ์คํด์ ์์ง๊ฐ ์๋ ๋ด์ฉ์ ์์
โข ์ ๋ฌธ์ ์ธ ์กฐ์ธ์ด ํ์ํ ๋ถ์ผ(์๋ฃ, ๋ฒ๋ฅ ๋ฑ)๋ ์ ๋ฌธ๊ฐ ๊ฒํ ํ์
2. ์ ์ง์ฑ: ๊ณผ์ฅํ๊ฑฐ๋ ๊ฑฐ์ง ์ ๋ณด ๊ธ์ง
3. ์ฑ ์๊ฐ: ๋ฐํํ ์ฝํ ์ธ ์ ๋ํ ์ฑ ์์ ๋น์ ์๊ฒ ์์
4. ์ง์ ๊ฐ์ : ๋ ์ ํผ๋๋ฐฑ์ ๋ฐ์ํด์ ๊ณ์ ๊ฐ์
5. ์ธ๊ฐ๋ฏธ: ์์ ์๋ํ๋ณด๋ค๋ ์ฌ๋์ ์๊ธธ์ด ๋๊ปด์ง๋๋ก
๐ ์ค์ ํ์ฉ ์ฌ๋ก ๋ฐ ํ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํ ๊ฒ! ๋ด๊ฐ ์ง์ ๊ฒช์ผ๋ฉด์ ๋ฐฐ์ด ๊ฒ๋ค์ด์ผ. ๐ช
์ฌ๋ก 1: ๊ธฐ์ ๋ธ๋ก๊ทธ ์๋ํ
ํ๋ก๊ทธ๋๋ฐ ํํ ๋ฆฌ์ผ ๋ธ๋ก๊ทธ๋ฅผ ์ด์ํ๋๋ฐ, ๋งค์ผ ์๋ก์ด ๊ธฐ์ ์ด ๋์์ ๋ฐ๋ผ๊ฐ๊ธฐ ํ๋ค์์ด.
โข GitHub Trending API๋ก ์ธ๊ธฐ ํ๋ก์ ํธ ์๋ ์์ง
โข ๊ฐ ํ๋ก์ ํธ์ ๋ํ ์๊ฐ ๋ฐ ์ฌ์ฉ๋ฒ ์๋ ์์ฑ
โข ์ฝ๋ ์์ ๋ AI๊ฐ ์์ฑํ๋, ์ค์ ํ ์คํธ ํ ๋ฐํ
โข ์ฃผ 3ํ ๋ฐํ (์, ์, ๊ธ)
โข 3๊ฐ์ ๋ง์ ์ ๋ฐฉ๋ฌธ์ 5์ฒ โ 3๋ง
โข ๊ตฌ๊ธ ๊ฒ์ 1ํ์ด์ง ๋ ธ์ถ ํค์๋ 50๊ฐ ์ด์
โข ์ ์์ต 15๋ง์ โ 120๋ง์
โข ์๊ฐ ํฌ์: ์ฃผ 10์๊ฐ โ ์ฃผ 2์๊ฐ
๊ธฐ์ ๋ธ๋ก๊ทธ๋ ์ต์ ์ฑ์ด ์๋ช ์ด์ผ. ํธ๋ ๋๋ฅผ ๋น ๋ฅด๊ฒ ์บ์นํด์ ๋จ๋ค๋ณด๋ค ๋จผ์ ๊ธ์ ์ฌ๋ฆฌ๋ฉด ๊ฒ์ ์์์ ์ค๋ฅด๊ธฐ ์ฌ์. AI ์๋ํ๋ก ์๋๋ฅผ ํ๋ณดํ๋, ์ฝ๋๋ ๋ฐ๋์ ์ง์ ํ ์คํธํด์ผ ํด!
์ฌ๋ก 2: ์ ํ ๋ฆฌ๋ทฐ ๋ธ๋ก๊ทธ
์ ์์ ํ ๋ฆฌ๋ทฐ ๋ธ๋ก๊ทธ์ธ๋ฐ, ๋ชจ๋ ์ ํ์ ์ง์ ์จ๋ณผ ์ ์์์ด.
โข ์๋ง์กด, ์ฟ ํก ๋ฑ์์ ์ ํ ์ ๋ณด ํฌ๋กค๋ง
โข ์ฌ์ฉ์ ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ ์์ง ๋ฐ ๋ถ์
โข AI๋ก ์ข ํฉ ๋ฆฌ๋ทฐ ์์ฑ (์ฅ๋จ์ , ๋น๊ต ๋ฑ)
โข ์ค์ ์ฌ์ฉ ๊ฐ๋ฅํ ์ ํ์ ์ง์ ๋ฆฌ๋ทฐ ์ถ๊ฐ
โข ์ ํ ๋ฆฌ๋ทฐ ์: ์ 10๊ฐ โ ์ 50๊ฐ
โข ์ ํด ๋ง์ผํ ์์ต 300% ์ฆ๊ฐ
โข ๋กฑํ ์ผ ํค์๋ ๊ฒ์ ์ ์ ๋ํญ ์ฆ๊ฐ
๋ฆฌ๋ทฐ ๋ธ๋ก๊ทธ๋ ๊ฐ๊ด์ฑ๊ณผ ์์ธํจ์ด ์ค์ํด. AI๋ก ๊ธฐ๋ณธ ์ ๋ณด๋ฅผ ์ ๋ฆฌํ๋, ์ค์ ์ฌ์ฉ์ ๋ฆฌ๋ทฐ๋ฅผ ๋ง์ด ์ธ์ฉํ๊ณ , ๊ฐ๋ฅํ๋ฉด ์ง์ ์ฌ์ฉ ๊ฒฝํ์ ์ถ๊ฐํด. ๊ทธ๋ฆฌ๊ณ ๋น๊ตํ๋ฅผ ๋ง์ด ํ์ฉํ๋ฉด ๋ ์๋ค์ด ์ข์ํด!
์ฌ๋ก 3: ๋ผ์ดํ์คํ์ผ ๋ธ๋ก๊ทธ
์ฌํ ํฌ, ์๊ธฐ๊ณ๋ฐ ๋ฑ ๋ค์ํ ์ฃผ์ ๋ฅผ ๋ค๋ฃจ๋ ๋ธ๋ก๊ทธ์ธ๋ฐ ์์ด๋์ด๊ฐ ๊ณ ๊ฐ๋์ด.
โข ๋ค์ด๋ฒ ์ง์iN, ์ปค๋ฎค๋ํฐ์์ ์์ฃผ ๋ฌป๋ ์ง๋ฌธ ์์ง
โข ๊ฐ ์ง๋ฌธ์ ๋ํ ์์ธ ๋ต๋ณ ํ์์ผ๋ก ๊ธ ์์ฑ
โข ๊ฐ์ธ ๊ฒฝํ๋ด์ ๋ณ๋๋ก ์์ฑํด์ ์ถ๊ฐ
โข ๊ณ์ ๋ณ, ์ด๋ฒคํธ๋ณ ์ฃผ์ ์๋ ์์ฑ
โข ๊ฒ์ ์ ์ 80% ์ด์ (๋กฑํ ์ผ ํค์๋)
โข ํ๊ท ์ฒด๋ฅ ์๊ฐ 3๋ถ ์ด์
โข ์ฌ๋ฐฉ๋ฌธ์จ 40% (์ ๊ณ ํ๊ท 20%)
โข ์ ๋์ผ์ค ์์ต ์์ ์ ์ฆ๊ฐ
๋ผ์ดํ์คํ์ผ ๋ธ๋ก๊ทธ๋ ๊ณต๊ฐ๊ณผ ์ค์ฉ์ฑ์ด ํต์ฌ์ด์ผ. AI๊ฐ ์์ฑํ ๊ธ์ ๊ฐ์ธ์ ์ธ ๊ฒฝํ์ด๋ ๊ฐ์ ์ ์ถ๊ฐํ๋ฉด ํจ์ฌ ๋ ์ง์ ์ฑ ์๊ฒ ๋๊ปด์ ธ. ๊ทธ๋ฆฌ๊ณ "๋๋ ๊ทธ๋", "๋๋ง ๊ทธ๋ฐ ๊ฒ ์๋๊ตฌ๋" ๊ฐ์ ๊ณต๊ฐ์ ์ด๋์ด๋ด๋ ๊ฒ ์ค์ํด!
๐ง ๋ฌธ์ ํด๊ฒฐ ๊ฐ์ด๋
์๋ํ ์์คํ ์ ์ด์ํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์ด. ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ์ ํด๊ฒฐ๋ฒ์ ์ ๋ฆฌํ์ด! ๐ ๏ธ
์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค
์์ธ:
โข ํ๋กฌํํธ๊ฐ ๋๋ฌด ๊น
โข ๋ถํ์ํ ์ฌ์์ฑ์ด ๋ง์
โข ๋น์ผ ๋ชจ๋ธ ์ฌ์ฉ
ํด๊ฒฐ์ฑ :
โข ํ๋กฌํํธ ์ต์ ํ (ํต์ฌ๋ง ๊ฐ๊ฒฐํ๊ฒ)
โข ํ์ง ๊ธฐ์ค์ ์ ์ ํ ์กฐ์ (๋๋ฌด ๋์ผ๋ฉด ์ฌ์์ฑ ๋ง์)
โข ๊ฐ๋ฐ/ํ ์คํธ๋ Gemini, ์ค์ ๋ฐํ์ GPT-4
โข ์ผ์ผ ์์ฑ ๊ฐ์ ์ ํ ์ค์
โข ์บ์ฑ ํ์ฉ (๊ฐ์ ์ฃผ์ ๋ ์ฌ์ฌ์ฉ)
์์ธ:
โข ํ๋กฌํํธ๊ฐ ๋ชจํธํจ
โข AI ๋ชจ๋ธ์ temperature ์ค์ ๋ฌธ์
โข ์ฃผ์ ์ ํ ๊ธฐ์ค ๋ถ์กฑ
ํด๊ฒฐ์ฑ :
โข ํ๋กฌํํธ์ ๊ตฌ์ฒด์ ์ธ ์์ ํฌํจ
โข temperature๋ฅผ 0.7 ์ ๋๋ก ์ค์ (๋๋ฌด ๋์ผ๋ฉด ๋ถ์์ )
โข ์ฃผ์ ๋ณ๋ก ๋ค๋ฅธ ํ๋กฌํํธ ํ ํ๋ฆฟ ์ฌ์ฉ
โข ํ์ง ๊ฒ์ฆ ๊ธฐ์ค ๊ฐํ
โข ์ ํ์ง ๊ธ์ ๊ณผ๊ฐํ ํ๊ธฐํ๊ณ ์ฌ์์ฑ
์์ธ:
โข SEO ์ต์ ํ ๋ถ์กฑ
โข ํค์๋ ์ ํ ์๋ชป๋จ
โข ๊ฒฝ์์ด ๋๋ฌด ์ฌํ ํค์๋
ํด๊ฒฐ์ฑ :
โข ๋กฑํ ์ผ ํค์๋ ๊ณต๋ต (๊ฒฝ์ ๋ฎ์ ํค์๋)
โข ๋ฉํ ํ๊ทธ ์ต์ ํ (์ ๋ชฉ, ์ค๋ช , ํ๊ทธ)
โข ๋ด๋ถ ๋งํฌ ๊ตฌ์กฐ ๊ฐ์
โข ๊ตฌ๊ธ ์์น ์ฝ์์ ์ฌ์ดํธ๋งต ์ ์ถ
โข ์ต์ 3๊ฐ์์ ๊ธฐ๋ค๋ ค๋ด (SEO๋ ์๊ฐ ํ์)
์์ธ:
โข API ์๋ฌ ์ฒ๋ฆฌ ๋ถ์กฑ
โข ๋คํธ์ํฌ ๋ฌธ์
โข ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ
ํด๊ฒฐ์ฑ :
โข try-except๋ก ๋ชจ๋ API ํธ์ถ ๊ฐ์ธ๊ธฐ
โข ์ฌ์๋ ๋ก์ง ๊ตฌํ (exponential backoff)
โข ๋ก๊น ๊ฐํํด์ ์ด๋์ ๋ฉ์ถ๋์ง ํ์
โข systemd ๊ฐ์ ํ๋ก์ธ์ค ๊ด๋ฆฌ์ ์ฌ์ฉ
โข ์ ๊ธฐ์ ์ธ ์ฌ์์ ์ค์ผ์ค (์ฃผ 1ํ)
์์ธ:
โข ๋๋ฌด ๋น ๋ฅธ ์๋๋ก ๋ฐํ
โข ์ ํ์ง ์ฝํ ์ธ
โข ์คํธ์ผ๋ก ๊ฐ์ง๋จ
ํด๊ฒฐ์ฑ :
โข ๋ฐํ ์๋ ์ค์ด๊ธฐ (ํ๋ฃจ 3๊ฐ ์ดํ)
โข ํ์ง ๊ธฐ์ค ๋์ด๊ธฐ
โข ๋ฐํ ์๊ฐ ๋๋คํ
โข ํ๋ซํผ ์ ์ฑ ์ฌํ์ธ
โข ํ์์ ์๋ ๊ฒํ ์ถ๊ฐ
โข ๋ ์์ ์ํต (๋๊ธ ๋ต๋ณ ๋ฑ)
๐ ๊ณ ๊ธ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ
๊ธฐ๋ณธ ์์คํ ์ด ์์ ์ ์ผ๋ก ๋์๊ฐ๋ฉด, ์ด์ ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณผ ์ฐจ๋ก์ผ! ๐ฏ
1. ์ด๋ฏธ์ง ์๋ ์์ฑ ๋ฐ ์ฝ์
๊ธ๋ง ์์ผ๋ฉด ์ฌ์ฌํ์์? ์ด๋ฏธ์ง๋ฅผ ์๋์ผ๋ก ์์ฑํด์ ๋ฃ์ด๋ณด์!
from openai import OpenAI
import requests
from PIL import Image
from io import BytesIO
class ImageGenerator:
"""AI ์ด๋ฏธ์ง ์์ฑ ๋ฐ ๊ด๋ฆฌ"""
def __init__(self):
self.client = OpenAI(api_key=Settings.OPENAI_API_KEY)
def generate_featured_image(self, title: str, description: str) -> str:
"""
๋ํ ์ด๋ฏธ์ง ์์ฑ (DALL-E 3 ์ฌ์ฉ)
Returns:
์ด๋ฏธ์ง URL
"""
prompt = f"""
Create a professional blog featured image for:
Title: {title}
Description: {description}
Style: Modern, clean, professional
No text in image
"""
response = self.client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1792x1024",
quality="standard",
n=1
)
image_url = response.data[0].url
# ์ด๋ฏธ์ง ๋ค์ด๋ก๋ ๋ฐ ์ ์ฅ
local_path = self._download_and_save(image_url, title)
return local_path
def _download_and_save(self, url: str, title: str) -> str:
"""์ด๋ฏธ์ง ๋ค์ด๋ก๋ ๋ฐ ๋ก์ปฌ ์ ์ฅ"""
response = requests.get(url)
img = Image.open(BytesIO(response.content))
# ํ์ผ๋ช
์์ฑ
filename = f"./data/images/{slugify(title)}.png"
img.save(filename)
return filename
def find_stock_images(self, keyword: str, count: int = 3) -> list:
"""
๋ฌด๋ฃ ์คํก ์ด๋ฏธ์ง ๊ฒ์ (Unsplash API)
"""
url = "https://api.unsplash.com/search/photos"
params = {
'query': keyword,
'per_page': count,
'client_id': 'your_unsplash_access_key'
}
response = requests.get(url, params=params)
data = response.json()
images = []
for photo in data['results']:
images.append({
'url': photo['urls']['regular'],
'alt': photo['alt_description'],
'credit': photo['user']['name']
})
return images
DALL-E๋ ๋น์ฉ์ด ์ข ๋๊ฐ๋๊น (์ด๋ฏธ์ง๋น $0.04-0.08), ๋ํ ์ด๋ฏธ์ง๋ง ์์ฑํ๊ณ ๋ณธ๋ฌธ ์ด๋ฏธ์ง๋ Unsplash ๊ฐ์ ๋ฌด๋ฃ ์คํก ์ฌ์ดํธ๋ฅผ ํ์ฉํ๋ ๊ฒ ์ข์. ๊ทธ๋ฆฌ๊ณ ์ด๋ฏธ์ง์๋ ํญ์ alt ํ ์คํธ๋ฅผ ๋ฃ์ด์ SEO์๋ ๋์์ด ๋๋๋ก ํ์!
2. ์์ ๋ฏธ๋์ด ์๋ ๊ณต์
๊ธ์ ๋ฐํํ๋ฉด ์๋์ผ๋ก SNS์๋ ๊ณต์ ํ์!
import tweepy # Twitter
from facebook import GraphAPI # Facebook
class SocialMediaManager:
"""์์
๋ฏธ๋์ด ์๋ ๊ณต์ """
def __init__(self):
self.setup_twitter()
self.setup_facebook()
def setup_twitter(self):
"""ํธ์ํฐ API ์ค์ """
auth = tweepy.OAuthHandler(
'consumer_key',
'consumer_secret'
)
auth.set_access_token(
'access_token',
'access_token_secret'
)
self.twitter = tweepy.API(auth)
def setup_facebook(self):
"""ํ์ด์ค๋ถ API ์ค์ """
self.facebook = GraphAPI(access_token='your_access_token')
def share_to_twitter(self, post: Dict):
"""ํธ์ํฐ์ ๊ณต์ """
# ํธ์ ์์ฑ (280์ ์ ํ)
tweet_text = f"{post['title']}\n\n{post['meta']['meta_description'][:100]}...\n\n{post['url']}"
# ํด์ํ๊ทธ ์ถ๊ฐ
tags = ' '.join(f"#{tag}" for tag in post['meta']['tags'][:3])
tweet_text += f"\n\n{tags}"
try:
self.twitter.update_status(tweet_text)
print("โ
ํธ์ํฐ ๊ณต์ ์๋ฃ")
except Exception as e:
print(f"โ ํธ์ํฐ ๊ณต์ ์คํจ: {e}")
def share_to_facebook(self, post: Dict):
"""ํ์ด์ค๋ถ ํ์ด์ง์ ๊ณต์ """
try:
self.facebook.put_object(
parent_object='me',
connection_name='feed',
message=post['meta']['meta_description'],
link=post['url']
)
print("โ
ํ์ด์ค๋ถ ๊ณต์ ์๋ฃ")
except Exception as e:
print(f"โ ํ์ด์ค๋ถ ๊ณต์ ์คํจ: {e}")
3. ์ฑ๊ณผ ๋์๋ณด๋
์น ๋์๋ณด๋๋ฅผ ๋ง๋ค์ด์ ์ค์๊ฐ์ผ๋ก ์ฑ๊ณผ๋ฅผ ๋ชจ๋ํฐ๋งํ์!
from flask import Flask, render_template, jsonify
import json
from datetime import datetime, timedelta
app = Flask(__name__)
@app.route('/')
def dashboard():
"""๋ฉ์ธ ๋์๋ณด๋"""
return render_template('dashboard.html')
@app.route('/api/stats')
def get_stats():
"""ํต๊ณ ๋ฐ์ดํฐ API"""
with open('./data/published.json', 'r') as f:
data = json.load(f)
# ์ต๊ทผ 30์ผ ํต๊ณ
thirty_days_ago = datetime.now() - timedelta(days=30)
recent_posts = [
p for p in data['posts']
if datetime.fromisoformat(p['published_at']) > thirty_days_ago
]
stats = {
'total_posts': len(data['posts']),
'recent_posts': len(recent_posts),
'avg_daily': len(recent_posts) / 30,
'total_words': sum(p['word_count'] for p in data['posts']),
'ai_usage': {
'openai': sum(1 for p in data['posts'] if p['ai_provider'] == 'openai'),
'anthropic': sum(1 for p in data['posts'] if p['ai_provider'] == 'anthropic'),
'google': sum(1 for p in data['posts'] if p['ai_provider'] == 'google')
}
}
return jsonify(stats)
@app.route('/api/recent-posts')
def get_recent_posts():
"""์ต๊ทผ ๋ฐํ ๊ธ ๋ชฉ๋ก"""
with open('./data/published.json', 'r') as f:
data = json.load(f)
recent = sorted(
data['posts'],
key=lambda x: x['published_at'],
reverse=True
)[:10]
return jsonify(recent)
if __name__ == '__main__':
app.run(debug=True, port=5000)
๊ฐ๋จํ HTML ๋์๋ณด๋ (templates/dashboard.html):
<!DOCTYPE html>
<html>
<head>
<title>๋ธ๋ก๊ทธ ์๋ํ ๋์๋ณด๋</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: Arial; padding: 20px; }
.stat-box {
display: inline-block;
padding: 20px;
margin: 10px;
background: #f0f0f0;
border-radius: 8px;
}
.stat-value { font-size: 2em; font-weight: bold; }
</style>
</head>
<body>
<h1>๐ ๋ธ๋ก๊ทธ ์๋ํ ๋์๋ณด๋</h1>
<div id="stats"></div>
<h2>์ต๊ทผ ๋ฐํ ๊ธ</h2>
<div id="recent-posts"></div>
<script>
// ํต๊ณ ๋ก๋
fetch('/api/stats')
.then(r => r.json())
.then(data => {
document.getElementById('stats').innerHTML = `
<div class="stat-box">
<div>์ด ๋ฐํ ๊ธ</div>
<div class="stat-value">${data.total_posts}</div>
</div>
<div class="stat-box">
<div>์ต๊ทผ 30์ผ</div>
<div class="stat-value">${data.recent_posts}</div>
</div>
<div class="stat-box">
<div>์ผํ๊ท </div>
<div class="stat-value">${data.avg_daily.toFixed(1)}</div>
</div>
`;
});
// ์ต๊ทผ ๊ธ ๋ก๋
fetch('/api/recent-posts')
.then(r => r.json())
.then(posts => {
const html = posts.map(p => `
<div style="padding: 10px; border-bottom: 1px solid #ddd;">
<strong>${p.title}</strong><br>
<small>${p.published_at} | ${p.word_count}์ | ${p.ai_provider}</small>
</div>
`).join('');
document.getElementById('recent-posts').innerHTML = html;
});
</script>
</body>
</html>
4. ์ฌ๋ฅ๋ท ์ฐ๋
์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ ๋ธ๋ก๊ทธ ์๋ํ ์๋น์ค๋ฅผ ํ๋งคํ ์๋ ์์ด! ๋ด๊ฐ ๋ง๋ ์์คํ ์ ๋ค๋ฅธ ์ฌ๋๋ค์๊ฒ ์ ๊ณตํ๋ ๊ฑฐ์ง. ๐ผ
๊ธฐ๋ณธ ํจํค์ง (10๋ง์)
โข ์์คํ ์ค์น ๋ฐ ์ด๊ธฐ ์ค์
โข 1๊ฐ์ ๋ฌด๋ฃ ์ด์ ์ง์
โข ๊ธฐ๋ณธ ํ๋กฌํํธ ํ ํ๋ฆฟ ์ ๊ณต
ํ๋ฆฌ๋ฏธ์ ํจํค์ง (30๋ง์)
โข ๋ง์ถคํ ํ๋กฌํํธ ๊ฐ๋ฐ
โข 3๊ฐ์ ์ด์ ์ง์
โข ์ฑ๊ณผ ๋ถ์ ๋ฐ ์ต์ ํ
โข ๋์๋ณด๋ ํฌํจ
์ํฐํ๋ผ์ด์ฆ (ํ์)
โข ๋ค์ค ๋ธ๋ก๊ทธ ๊ด๋ฆฌ
โข ์ ์ฉ ์๋ฒ ๊ตฌ์ถ
โข ๋ฌด์ ํ ์ง์
์ฌ๋ฅ๋ท์์ ์๋น์ค๋ฅผ ํ๋งคํ ๋๋ ํฌํธํด๋ฆฌ์ค๊ฐ ์ค์ํด. ๋ด ๋ธ๋ก๊ทธ์ ์ฑ๊ณผ๋ฅผ ๊ตฌ์ฒด์ ์ธ ์์น๋ก ๋ณด์ฌ์ฃผ๊ณ , ์ค์ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ๊ณต์ ํ๋ฉด ์ ๋ขฐ๋๊ฐ ๋์์ ธ. ๊ทธ๋ฆฌ๊ณ ์ฒ์์ ๊ฐ๊ฒฉ์ ๋ฎ๊ฒ ์ฑ ์ ํด์ ๋ฆฌ๋ทฐ๋ฅผ ๋ง์ด ๋ฐ๋ ๊ฒ ์ข์!
๐ฐ ์์ตํ ์ ๋ต
์๋ํ ์์คํ ์ ๊ตฌ์ถํ์ผ๋ฉด ์ด์ ๋์ ๋ฒ์ด์ผ์ง! ์ฌ๋ฌ ์์ตํ ๋ฐฉ๋ฒ์ ์์๋ณด์. ๐ต
์์ตํ ๋ฐฉ๋ฒ ๋น๊ต
| ๋ฐฉ๋ฒ | ๋์ด๋ | ์์ต์ฑ | ์์ ์๊ธฐ |
|---|---|---|---|
| ๊ตฌ๊ธ ์ ๋์ผ์ค | ์ฌ์ | ์ค๊ฐ | ์ฆ์ |
| ์ ํด ๋ง์ผํ | ์ค๊ฐ | ๋์ | 1๊ฐ์ ํ |
| ๋์งํธ ์ํ ํ๋งค | ๋์ | ๋งค์ฐ ๋์ | 3๊ฐ์ ํ |
| ์ปจ์คํ /์๋น์ค | ์ค๊ฐ | ๋์ | 2๊ฐ์ ํ |
| ์คํฐ์ ์ฝํ ์ธ | ๋์ | ๋งค์ฐ ๋์ | 6๊ฐ์ ํ |
๋จ๊ณ๋ณ ์์ตํ ๋ก๋๋งต
๋ชฉํ: ์ ๋ฐฉ๋ฌธ์ 1๋ง๋ช
์ ๋ต:
โข SEO ์ต์ ํ์ ์ง์ค
โข ๋กฑํ ์ผ ํค์๋ ๊ณต๋ต
โข ์ผ๊ด๋ ๋ฐํ (์ฃผ 5-7ํ)
โข ๋ด๋ถ ๋งํฌ ๊ตฌ์กฐ ๊ฐํ
์์ต: ๊ฑฐ์ ์์ (ํฌ์ ๊ธฐ๊ฐ)
๋ชฉํ: ์ ๋ฐฉ๋ฌธ์ 3๋ง๋ช , ์ ์์ต 30-50๋ง์
์ ๋ต:
โข ๊ตฌ๊ธ ์ ๋์ผ์ค ์น์ธ ๋ฐ ์ต์ ํ
โข ์ฟ ํก ํํธ๋์ค, ์๋ง์กด ์ด์์์์ดํธ ๊ฐ์
โข ์ ํ ๋ฆฌ๋ทฐ ์ฝํ ์ธ ์ฆ๊ฐ
โข ์ด๋ฉ์ผ ๊ตฌ๋ ์ ๋ชจ์ผ๊ธฐ ์์
์์ต ๊ตฌ์ฑ:
โข ์ ๋์ผ์ค: 60%
โข ์ ํด ๋ง์ผํ : 40%
๋ชฉํ: ์ ๋ฐฉ๋ฌธ์ 10๋ง๋ช , ์ ์์ต 100-200๋ง์
์ ๋ต:
โข ๋์งํธ ์ํ ๊ฐ๋ฐ (์ ์์ฑ , ๊ฐ์, ํ ํ๋ฆฟ)
โข ํ๋ฆฌ๋ฏธ์ ์ฝํ ์ธ (์ ๋ฃ ๊ตฌ๋ )
โข ์ปจ์คํ ์๋น์ค ์์
โข ์ฌ๋ฅ๋ท์์ ์๋ํ ์๋น์ค ํ๋งค
์์ต ๊ตฌ์ฑ:
โข ์ ๋์ผ์ค: 30%
โข ์ ํด ๋ง์ผํ : 30%
โข ๋์งํธ ์ํ: 25%
โข ์๋น์ค: 15%
๋ชฉํ: ์ ๋ฐฉ๋ฌธ์ 30๋ง๋ช +, ์ ์์ต 300๋ง์+
์ ๋ต:
โข ๋ค์ค ๋ธ๋ก๊ทธ ์ด์
โข ์คํฐ์ ์ฝํ ์ธ ์ ์น
โข ์์ฒด ์ ํ/์๋น์ค ๋ฐ์นญ
โข ํ ๊ตฌ์ฑ (์ธ์ฃผ ํ์ฉ)
์์ต ๊ตฌ์ฑ:
โข ์คํฐ์: 40%
โข ์์ฒด ์ ํ: 30%
โข ์ ํด ๋ง์ผํ : 20%
โข ๊ธฐํ: 10%
2. ๋ค์ํ ์์ต์: ํ ๊ฐ์ง์๋ง ์์กดํ์ง ๋ง๊ณ ์ฌ๋ฌ ์ฑ๋ ํ๋ณด
3. ๊ฐ์น ์ ๊ณต ์ฐ์ : ๋ ์์๊ฒ ์ง์ง ๋์์ด ๋๋ฉด ์์ต์ ์์ฐ์ค๋ฝ๊ฒ ๋ฐ๋ผ์
4. ๋ฐ์ดํฐ ๊ธฐ๋ฐ ๊ฒฐ์ : ์ด๋ค ์ฝํ ์ธ ๊ฐ ์์ต์ด ๋์์ง ๋ถ์ํ๊ณ ์ง์ค
5. ์ฅ๊ธฐ์ ๊ด์ : ๋ธ๋ก๊ทธ๋ ๋ง๋ผํค์ด์ผ. ๊พธ์คํจ์ด ์น๋ฆฌ์ ์ด์ !
๐ฏ ๋ง๋ฌด๋ฆฌ ๋ฐ ๋ค์ ๋จ๊ณ
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ด์ ๋น์ ์ AI ๋ธ๋ก๊ทธ ์๋ํ ์์คํ ์ ๋ง๋ค ์ ์๋ ๋ชจ๋ ์ง์์ ๊ฐ์ท์ด.
ํต์ฌ ์์ฝ
1. ์์คํ ์ํคํ ์ฒ: ์ฃผ์ ์์ฑ โ ์ฝํ ์ธ ์์ฑ โ ํ์ง ๊ฒ์ฆ โ ์๋ ๋ฐํ
2. AI ํ์ฉ: GPT-4, Claude, Gemini๋ฅผ ์ํฉ์ ๋ง๊ฒ ์ ํ
3. ํ์ง ๊ด๋ฆฌ: ์๋ํํด๋ ํ์ง์ ํํํ์ง ์๊ธฐ
4. ์ค๋ฆฌ์ ์ด์: ๋ ์์๊ฒ ๊ฐ์น๋ฅผ ์ ๊ณตํ๋ ์ฝํ ์ธ
5. ์์ตํ: ๋จ๊ณ๋ณ๋ก ๋ค์ํ ์์ต์ ํ๋ณด
6. ์ต์ ํ: ๋ฐ์ดํฐ ๊ธฐ๋ฐ์ผ๋ก ์ง์์ ๊ฐ์
์์ํ๊ธฐ ์ํ ์ฒดํฌ๋ฆฌ์คํธ
๋ฐ๋ก ์์ํ ์ ์๋๋ก ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์์ด:
โ Python ๋ฐ ๊ฐ๋ฐ ํ๊ฒฝ ์ค์น
โ AI API ํค ๋ฐ๊ธ (OpenAI, Anthropic, Google)
โ ๋ธ๋ก๊ทธ ํ๋ซํผ ์ ํ ๋ฐ API ์ค์
โ ํ๋ก์ ํธ ํด๋ ๊ตฌ์กฐ ์์ฑ
โ ๊ธฐ๋ณธ ๋ชจ๋ ์ฝ๋ ์์ฑ
โ ์ฃผ์ ์์ฑ๊ธฐ ๊ตฌํ ๋ฐ ํ ์คํธ
โ ์ฝํ ์ธ ์์ฑ๊ธฐ ๊ตฌํ ๋ฐ ํ ์คํธ
โ ํ์ง ๊ฒ์ฆ๊ธฐ ๊ตฌํ
โ ๋ฐํ ๊ด๋ฆฌ์ ๊ตฌํ
โ ํตํฉ ํ ์คํธ (1ํ ์คํ)
โ ํ๋กฌํํธ ์ต์ ํ
โ ํ์ง ๊ธฐ์ค ์กฐ์
โ ์๋ฒ ๋ฐฐํฌ (๋๋ ๋ก์ปฌ 24์๊ฐ ์คํ)
โ ์ค์ผ์ค๋ฌ ์ค์
โ ๋ชจ๋ํฐ๋ง ์์คํ ๊ตฌ์ถ
โ ์ผ์ผ ์๋ ๋ฐํ ์์
โ ์ฑ๊ณผ ๋ชจ๋ํฐ๋ง
โ ๋ฌธ์ ๋ฐ์ ์ ์ฆ์ ๋์
โ ๋ ์ ํผ๋๋ฐฑ ์์ง
โ ์ฒซ ๋ฌ ๋ฆฌํฌํธ ์์ฑ
์ถ๊ฐ ํ์ต ์๋ฃ
๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด:
AI ๋ฐ ํ๋กฌํํธ ์์ง๋์ด๋ง:
โข OpenAI Cookbook (๊ณต์ ๋ฌธ์)
โข "The Art of Prompt Engineering" (์จ๋ผ์ธ ๊ฐ์)
โข Anthropic์ Claude ํ๋กฌํํธ ๊ฐ์ด๋
Python ์๋ํ:
โข "Automate the Boring Stuff with Python" (์ฑ )
โข Real Python ํํ ๋ฆฌ์ผ
โข Python Automation Cookbook
SEO ๋ฐ ๋ธ๋ก๊ทธ ์ด์:
โข Google Search Central (๊ณต์ ๊ฐ์ด๋)
โข Moz Beginner's Guide to SEO
โข Backlinko ๋ธ๋ก๊ทธ
์ปค๋ฎค๋ํฐ:
โข Reddit r/blogging
โข ์ฌ๋ฅ๋ท ์ปค๋ฎค๋ํฐ
โข ๋ค์ด๋ฒ ๋ธ๋ก๊ทธ ๋ง์ผํ ์นดํ
๋ง์ง๋ง ์กฐ์ธ
๋ธ๋ก๊ทธ ์๋ํ๋ ๋ง๋ฒ์ ์งํก์ด๊ฐ ์๋์ผ. ์์คํ ์ ๋ง๋๋ ๊ฒ๋ ์ค์ํ์ง๋ง, ๋ ์ค์ํ ๊ฑด ๊พธ์คํจ๊ณผ ๊ฐ์ ์ด์ผ. ๐ฑ
1. ์๋ฒฝ์ฃผ์ ๋ฒ๋ฆฌ๊ธฐ: ์ฒ์๋ถํฐ ์๋ฒฝํ ์ ์์ด. ์ผ๋จ ์์ํ๊ณ ๊ฐ์ ํด๋๊ฐ์
2. ์คํ ์ ์ : ๋ค์ํ ๋ฐฉ๋ฒ์ ์๋ํด๋ด. ์คํจํด๋ ๊ด์ฐฎ์, ๋ฐฐ์ฐ๋ ๊ฑฐ๋๊น
3. ๋ ์ ์ค์ฌ: ํญ์ ๋ ์์๊ฒ ๊ฐ์น๋ฅผ ์ ๊ณตํ๋ค๋ ๊ฑธ ์์ง ๋ง
4. ์ฅ๊ธฐ์ ๊ด์ : ๋น ๋ฅธ ์ฑ๊ณต์ ๊ธฐ๋ํ์ง ๋ง. 6๊ฐ์, 1๋ ํ๋ฅผ ๋ณด๊ณ ๊ฐ์
5. ์ง์์ ํ์ต: AI ๊ธฐ์ ์ ๋น ๋ฅด๊ฒ ๋ฐ์ ํด. ๊ณ์ ๊ณต๋ถํ๊ณ ์ ๋ฐ์ดํธํ์
6. ์ปค๋ฎค๋ํฐ ์ฐธ์ฌ: ํผ์ ํ์ง ๋ง๊ณ ๋ค๋ฅธ ์ฌ๋๋ค๊ณผ ๊ฒฝํ์ ๋๋์
7. ์ค๋ฆฌ์ ์ด์: ๋จ๊ธฐ ์์ต๋ณด๋ค ์ฅ๊ธฐ์ ์ ๋ขฐ๊ฐ ๋ ์ค์ํด
๋๋ ์ฒ์์ ๋ง๋งํ์ด. ์ฝ๋ ํ ์ค ์ฐ๋ ๊ฒ๋ ์ด๋ ค์ ๊ณ , AI๊ฐ ๋ง๋ ๊ธ์ด ์๋ง์ด์ด์ ์ข์ ํ ์ ๋ ๋ง์์ง. ํ์ง๋ง ํฌ๊ธฐํ์ง ์๊ณ ๊ณ์ ๊ฐ์ ํ๋ค ๋ณด๋ ์ง๊ธ์ ์์ ์ ์ผ๋ก ์ด์ํ๊ณ ์์ด. ๐
๋น์ ๋ ํ ์ ์์ด! ์ด ๊ธ์ด ๋์์ด ๋๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ๋๋ฅผ ์ฐพ์์ค. ๋ ์์ธํ ์๋ด์ด๋ ๋ง์ถคํ ๋์์ ์ ๊ณตํ ์ ์์ด. ํจ๊ป ์ฑ๊ณตํ์! ๐
๐ ์ถํํฉ๋๋ค!
์ด์ ๋น์ ์ AI ๋ธ๋ก๊ทธ ์๋ํ ์ ๋ฌธ๊ฐ์
๋๋ค!
์ด ์ง์์ ํ์ฉํด์ ๋ฉ์ง ๋ธ๋ก๊ทธ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์.
๊ถ๊ธํ ์ ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ฌ๋ฅ๋ท์์ ์ฐพ์์ฃผ์ธ์.
ํจ๊ป ์ฑ์ฅํ๋ ์ปค๋ฎค๋ํฐ๋ฅผ ๋ง๋ค์ด๊ฐ์๋ค! ๐ช
ํ์ดํ ! ์ฐ๋ฆฌ ๋ชจ๋ ์ฑ๊ณตํ์! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.