Module 1: 코드 구조 설계 (v1.0)
Module 1 (스마트블록 기반 주제 선정)의 디렉토리·파일 구조와 컴포넌트 책임 정의.
0. 코드와 vault 분리 원칙
| 위치 | 역할 | 형식 |
|---|---|---|
~/projects/naver-blog-automation/ | 실행 코드 (Python 프로젝트) | Git 리포, venv |
| vault (이 문서) | 설계·운영 기록 | Obsidian Markdown |
vault 00-Inbox/blog/ | 수동 CSV 백업 | csv (코드가 읽음) |
vault 20-Personal/logs/ | 주간 리포트 출력 | Markdown (코드가 씀) |
코드 → vault 쓰기는 출력물(리포트)만. 코드 → vault 읽기는 CSV 백업·사용자 프로필만. 양방향 결합을 최소화해 코드 리포의 독립성 확보.
1. 디렉토리 구조 (~/projects/naver-blog-automation/)
~/projects/naver-blog-automation/
│
├── README.md 프로젝트 개요
├── pyproject.toml 의존성 + 빌드 (uv / poetry)
├── requirements.txt pip 호환
├── .env.example / .env API 키 (gitignore)
├── .gitignore
├── settings.yaml 설정 (가중치·임계값 등)
│
├── src/
│ ├── __init__.py
│ ├── main.py CLI 엔트리포인트 (argparse)
│ │
│ ├── modules/
│ │ ├── __init__.py
│ │ │
│ │ └── module_1_subject/ ⭐ Module 1 전체
│ │ ├── __init__.py 공개 API: run_module_1()
│ │ ├── orchestrator.py 5단계 흐름 조율
│ │ ├── home_feed_collector.py Selenium 홈판 수집
│ │ ├── csv_fallback_loader.py 수동 CSV 백업 로더
│ │ ├── niche_generator.py Claude 니치 150개 생성
│ │ ├── scorer.py 4가지 점수 + 종합 계산
│ │ ├── gate_keeper.py 3중 게이트 + 카테고리 다양성
│ │ ├── report_generator.py Vault Markdown 리포트
│ │ ├── prompts.py Claude 프롬프트 템플릿
│ │ ├── lookup_tables.py 카테고리·수수료·CPM 룩업
│ │ └── types.py dataclass 정의
│ │
│ ├── core/ 공통 인프라 (Module 2-7 재사용)
│ │ ├── __init__.py
│ │ ├── claude_client.py Anthropic SDK 래퍼 (prompt caching)
│ │ ├── db.py SQLite 연결·스키마 관리
│ │ ├── logger.py 구조화 로깅 (jsonl)
│ │ ├── config.py pydantic-settings 통합 로더
│ │ ├── vault_io.py ⭐ vault 경로 read/write 추상화
│ │ └── errors.py 커스텀 예외
│ │
│ └── utils/ pure functions
│ ├── __init__.py
│ ├── selenium_driver.py Chrome 드라이버 팩토리
│ ├── keyword_extractor.py 제목→키워드 추출 (kiwipiepy)
│ ├── category_inferrer.py 카테고리 분류 (룰 + Claude)
│ └── markdown_renderer.py jinja2 템플릿 렌더링
│
├── data/ 로컬 데이터 (gitignore)
│ ├── cache/ API 응답 캐시
│ ├── db/
│ │ └── blog.sqlite 메인 DB
│ └── seeds/
│ └── user_profile.yaml 사용자 프로필
│
├── scripts/ 일회성 / 스케줄 진입점
│ ├── init_db.py SQLite 초기화
│ ├── collect_home_feed_daily.py 매일 08:10 cron
│ ├── run_module1_weekly.py 매주 금 14:00 cron
│ └── tune_weights.py Phase 4 가중치 회귀
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py pytest fixture
│ ├── unit/
│ │ ├── test_scorer.py
│ │ ├── test_gate_keeper.py
│ │ ├── test_keyword_extractor.py
│ │ └── test_lookup_tables.py
│ ├── integration/
│ │ ├── test_orchestrator.py E2E (mock)
│ │ └── test_home_feed_collector.py
│ └── fixtures/
│ ├── sample_home_feed.json
│ ├── sample_user_profile.yaml
│ └── sample_naver_home.html Selenium mock용
│
├── logs/ 런타임 로그 (gitignore)
│ ├── home_feed.jsonl
│ ├── module1.jsonl
│ └── errors.jsonl
│
└── docs/
├── ARCHITECTURE.md
├── API_SETUP.md
└── RUNBOOK.md 운영 매뉴얼 (DOM 깨짐 등)
2. vault 통합 지점 (core/vault_io.py)
코드가 vault에 접근하는 유일한 추상화 레이어. 경로 하드코딩 금지.
# src/core/vault_io.py
from pathlib import Path
from .config import settings
class VaultIO:
def __init__(self, vault_root: Path | None = None):
self.root = vault_root or Path(settings.VAULT_ROOT).expanduser()
# 기본: ~/projects/jyp-garden
# === 읽기 ===
def read_user_profile(self) -> dict:
""" data/seeds/user_profile.yaml 또는
vault의 20-Personal/projects/.../user_profile.yaml 읽기"""
def read_csv_fallback(self, date_str: str) -> Path:
""" 00-Inbox/blog/home-feed-YYYY-MM-DD.csv 경로 반환"""
return self.root / "00-Inbox" / "blog" / f"home-feed-{date_str}.csv"
# === 쓰기 ===
def write_weekly_report(self, week_id: str, markdown: str) -> Path:
""" 20-Personal/logs/module1-{week_id}.md 에 저장"""
path = self.root / "20-Personal" / "logs" / f"module1-{week_id}.md"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(markdown, encoding="utf-8")
return path
def write_error_log(self, message: str) -> Path:
""" system/logs/module1-errors.log 에 append"""vault 경로 설정 (.env):
VAULT_ROOT=~/projects/jyp-garden3. 컴포넌트 책임 (Single Responsibility)
3.1 module_1_subject/ 패키지
| 파일 | 역할 | 입력 | 출력 |
|---|---|---|---|
orchestrator.py | 5단계 흐름 조율 | 시드, 프로필 | 추천 리스트 |
home_feed_collector.py | Selenium 홈판 50건 수집 | URL | list[HomeFeedItem] |
csv_fallback_loader.py | vault CSV 백업 로드 | date | list[HomeFeedItem] |
niche_generator.py | Claude 니치 후보 생성 | 시드 5개 | 후보 150개 |
scorer.py | 4가지 점수 + 종합 | 니치 + 컨텍스트 | 점수 dict |
gate_keeper.py | 게이트 + 다양성 | 점수 리스트 | 필터링 결과 |
report_generator.py | Markdown 리포트 | 결과 | .md (vault) |
prompts.py | 프롬프트 템플릿 상수 | - | str |
lookup_tables.py | 카테고리·수수료·CPM | - | dict |
types.py | dataclass 정의 | - | - |
3.2 core/ 공통 인프라
| 파일 | 역할 |
|---|---|
claude_client.py | Anthropic SDK 래퍼. prompt caching + 재시도 + 비용 로깅 |
db.py | SQLite 연결, 마이그레이션, dataclass ↔ row 매핑 |
logger.py | structlog 기반 jsonl 구조화 로깅 |
config.py | pydantic-settings, .env + settings.yaml 통합 |
vault_io.py | vault 경로 read/write 추상화 (위 2번 참조) |
errors.py | HomeFeedCollectionError, ClaudeAPIError, GateRejectError 등 |
3.3 utils/ 유틸리티 (pure functions)
| 파일 | 역할 |
|---|---|
selenium_driver.py | Chrome 드라이버 팩토리 (stealth, UA 위장, headless) |
keyword_extractor.py | 한국어 형태소 분석 → 키워드 (kiwipiepy) |
category_inferrer.py | 제목/키워드 → 카테고리 (룰 + Claude fallback) |
markdown_renderer.py | jinja2 Markdown 빌더 |
4. 핵심 인터페이스 (Type Hints)
4.1 데이터 클래스 (types.py)
from dataclasses import dataclass
from datetime import datetime
from typing import Literal
Tier = Literal["A", "B", "C", "REJECTED"]
Section = Literal["home", "blog_top", "datalab"]
@dataclass
class HomeFeedItem:
rank: int
title: str
blog_id: str
category: str
keywords: list[str]
thumbnail_url: str | None
collected_at: datetime
section: Section
@dataclass
class NicheCandidate:
niche: str # "제주도 + 아이 + 카페"
seed_keyword: str
category: str
@dataclass
class ScoredNiche:
niche: str
seed_keyword: str
category: str
popularity: float; popularity_reason: str
affinity: float; affinity_reason: str
monetization: float; monetization_reason: str
competition: float; competition_reason: str
total_score: float
tier: Tier
reject_reason: str | None
week_id: str # 2026-W19
@dataclass
class UserProfile:
bio: str
top_categories: list[str]
recent_posts: list[str]
self_rating: dict[str, int]4.2 공개 API
# src/modules/module_1_subject/__init__.py
from .orchestrator import run_module_1
from .types import HomeFeedItem, NicheCandidate, ScoredNiche, UserProfile
__all__ = ["run_module_1", "HomeFeedItem", "NicheCandidate",
"ScoredNiche", "UserProfile"]4.3 메인 진입점
# orchestrator.py
def run_module_1(
seed_keywords: list[str],
user_profile: UserProfile,
*,
candidate_count: int = 150,
cutoff_score: float = 70.0,
use_selenium: bool = True,
csv_fallback_date: str | None = None,
week_id: str | None = None,
) -> list[ScoredNiche]:
"""
1. 홈판 데이터 수집 (Selenium → CSV fallback)
2. 니치 후보 생성 (Claude)
3. 4가지 점수화
4. 게이트 + 다양성
5. SQLite 저장 + vault Markdown 리포트
"""5. 의존성 그래프
orchestrator
│
┌───────────────┼────────────────┐
▼ ▼ ▼
home_feed_collector niche_generator scorer
│ │ │
│ │ ├─→ lookup_tables
│ │ │
▼ ▼ ▼
selenium_driver claude_client claude_client
csv_fallback_loader
│ gate_keeper
│ │
└─→ vault_io ▼
report_generator
│
├─→ markdown_renderer
└─→ vault_io
공통 인프라 (모두 사용): core/{config, db, logger, claude_client}
원칙:
- 상위 → 하위 단방향
utils/는 외부 라이브러리만 의존core/는 외부 SDK +utils/만 의존- vault 접근은 반드시
core/vault_io.py경유
6. 실행 플로우 (E2E Sequence)
6.1 cron 진입점
# scripts/run_module1_weekly.py (매주 금 14:00)
from src.modules.module_1_subject import run_module_1, UserProfile
from src.core.vault_io import VaultIO
from src.core.config import settings
def main():
vault = VaultIO()
profile_data = vault.read_user_profile()
profile = UserProfile(**profile_data)
results = run_module_1(
seed_keywords=["초등 육아", "정리정돈", "다이소", "워킹맘", "5월 가정의달"],
user_profile=profile,
candidate_count=150,
cutoff_score=70.0,
use_selenium=True,
csv_fallback_date="2026-05-13",
week_id="2026-W19",
)
print(f"✅ {len(results)} 니치 선정 완료 (A티어 {sum(1 for r in results if r.tier=='A')}개)")
if __name__ == "__main__":
main()6.2 Orchestrator 의사 코드
def run_module_1(seed_keywords, user_profile, **opts):
log = get_logger("module1")
vault = VaultIO()
# 1️⃣ 홈판 수집 (Selenium → CSV fallback)
try:
home_items = home_feed_collector.collect()
except HomeFeedCollectionError as e:
log.warn("selenium_failed", err=str(e))
csv_path = vault.read_csv_fallback(opts["csv_fallback_date"])
home_items = csv_fallback_loader.load(csv_path)
db.save_home_feed(home_items)
# 2️⃣ 니치 후보 생성
candidates = niche_generator.generate(seed_keywords, count=opts["candidate_count"])
# 3️⃣ 점수화 (배치 Claude 호출)
scored = scorer.score_batch(
candidates=candidates, user_profile=user_profile, home_items=home_items
)
# 4️⃣ 게이트 + 다양성
filtered = gate_keeper.apply_gates(scored)
final = gate_keeper.diversify(filtered, max_per_category=10)
# 5️⃣ 저장 + 리포트
db.save_niches(final, week_id=opts["week_id"])
markdown = report_generator.render(final, home_items, opts["week_id"])
report_path = vault.write_weekly_report(opts["week_id"], markdown)
log.info("completed", selected=len(final), report=str(report_path))
return final7. 설정 (settings.yaml + .env)
7.1 settings.yaml (체크인)
module_1:
weights:
popularity: 0.30
affinity: 0.30
monetization: 0.25
competition_inverse: 0.15
popularity_sub:
home_feed_match: 0.35
search_trend: 0.25
seasonality: 0.20
trend_direction: 0.20
cutoff_score: 70
candidate_count: 150
max_per_category: 10
gate_rules:
competition_max: 90
affinity_min: 30
selenium:
headless: true
user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 ...)"
retry_count: 3
retry_delay_seconds: 5
rate_limit_per_day: 1
claude:
model: "claude-sonnet-4-6"
temperature: 0.0
max_tokens_per_request: 8192
enable_prompt_caching: true
logging:
level: INFO
format: jsonl7.2 .env (gitignore)
ANTHROPIC_API_KEY=sk-ant-...
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
VAULT_ROOT=~/projects/jyp-garden
ENVIRONMENT=development
DEBUG=true8. 테스트 전략
8.1 Unit (빠른 피드백)
| 테스트 | 대상 | 모킹 |
|---|---|---|
test_scorer.py | 점수 공식 + 종합 | 입력 dict 고정 |
test_gate_keeper.py | 3중 게이트 + 다양성 | fixtures |
test_keyword_extractor.py | 한국어 키워드 | - |
test_lookup_tables.py | 카테고리 룩업 | - |
8.2 Integration
| 테스트 | 대상 | 모킹 |
|---|---|---|
test_orchestrator.py | 5단계 E2E | Claude + Selenium mock |
test_home_feed_collector.py | Selenium 파싱 | 로컬 HTML fixture |
test_report_generator.py | Markdown 출력 | snapshot 비교 |
8.3 Smoke (운영 검증)
uv run python scripts/run_module1_weekly.py --dry-run --week-id 2026-W199. 로깅 (logs/*.jsonl)
{"ts":"2026-05-13T06:30:00","level":"INFO","module":"home_feed_collector","msg":"수집 시작","section":"home"}
{"ts":"2026-05-13T06:30:45","level":"INFO","module":"home_feed_collector","msg":"수집 완료","items":50}
{"ts":"2026-05-13T06:31:00","level":"INFO","module":"niche_generator","msg":"Claude 호출","tokens_in":3200,"tokens_out":4500,"cost_usd":0.018}
{"ts":"2026-05-13T06:32:30","level":"WARN","module":"scorer","msg":"단일 항목 100점","niche":"X"}
{"ts":"2026-05-13T06:33:00","level":"INFO","module":"orchestrator","msg":"완료","selected":38,"a_tier":12,"total_cost_usd":0.082}10. 의존성 (pyproject.toml 발췌)
[project]
name = "naver-blog-automation"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"anthropic>=0.40.0",
"selenium>=4.20.0",
"webdriver-manager>=4.0.0",
"kiwipiepy>=0.17.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"pyyaml>=6.0",
"python-dotenv>=1.0.0",
"sqlite-utils>=3.36",
"jinja2>=3.1.0",
"structlog>=24.1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-mock>=3.12.0",
"ruff>=0.5.0",
"mypy>=1.10.0",
]11. 확장성 (Module 2-7 대응)
src/modules/
├── module_1_subject/ ⭐ 본 문서
├── module_2_title/ 제목/썸네일 (CTR)
├── module_3_content/ 본문 (체류시간)
├── module_4_image/ 이미지 URL
├── module_5_cta/ CTA/해시태그
├── module_6_branding/ 브랜딩/제휴
└── module_7_quality/ 품질 관리
공유 인프라 (core/)는 모든 모듈이 동일하게 사용. 모듈 간 데이터 전달은 data/db/blog.sqlite을 통한 영속화.
12. MVP 구현 진행 (실측 일정 반영)
Day 1 (05-13): 기반 ✅
✅ ~/projects/naver-blog-automation/ git init (commit 5e6b102)
✅ pyproject.toml + venv + .env.example + settings.yaml
✅ core/{config, db, logger, vault_io, claude_client, errors} 구현
✅ scripts/init_db.py + test_claude_cli.py 작동
✅ Claude Code CLI 검증 ($0.20/호출, 32K 캐시)
Day 2 (05-13): 수집 ✅
✅ utils/selenium_driver.py (Selenium Manager 사용, SSL 우회)
✅ utils/keyword_extractor.py + 8/8 단위 테스트 통과
✅ module_1/home_feed_collector.py — www.naver.com PC 5개 탭
✅ module_1/csv_fallback_loader.py (vault CSV)
✅ module_1/types.py — HomeFeedItem(tab/domain) + NicheCandidate/ScoredNiche/UserProfile
✅ scripts/collect_home_feed_daily.py — 74건 수집 검증
Day 3 (05-13): Claude 통합 + 점수화 ✅
✅ module_1/lookup_tables.py — 카테고리 수수료/CPM/시즌
✅ module_1/prompts.py — SYSTEM_PROMPT + build_user_prompt
✅ module_1/niche_generator.py — 1회 통합 호출
✅ module_1/scorer.py — 종합 점수 + 티어 (A/B/C)
✅ data/seeds/user_profile.yaml (차박/캠핑 위주)
✅ scripts/test_niche_generator.py — 50 후보 / $0.40 검증
Day 4 (예정): 게이트 + 리포트
□ module_1/gate_keeper.py (3중 게이트 + 카테고리 다양성)
□ module_1/report_generator.py + jinja2 템플릿
□ utils/markdown_renderer.py
□ tests/unit/test_gate_keeper.py + test_scorer.py
Day 5 (예정): 오케스트레이션 + cron
□ module_1/orchestrator.py — 5단계 통합
□ scripts/run_module1_weekly.py — cron 진입점
□ macOS launchd 또는 cron 등록
Day 6 (예정): E2E 검증
□ 실 홈판 + 실 Claude → vault 리포트 생성
□ 150 후보 실호출 ($1 미만 예상)
□ docs/RUNBOOK.md 작성
□ MVP 완료 보고
13. 핵심 결정 사항 (실구현 반영)
- 코드 위치
~/projects/naver-blog-automation/— vault와 완전 분리 - vault 통합은
core/vault_io.py단일 경로 — 양방향 결합 최소화 - Module 1 패키지화 — 단일 파일 대신 10개 컴포넌트 분리
core/공통 인프라 — Module 2-7 재사용 보장utils/는 pure function — 의존성 없는 헬퍼만- ⭐ Claude Code CLI subprocess (
claude -p) — Anthropic API 키 X, Pro 구독 사용 - ⭐ 1회 통합 호출 — 니치 생성 + 점수화를 하나의 호출에 (호출당 ~$0.20 cache_creation 비용 우회)
- ⭐ Selenium Manager 내장 사용 — webdriver-manager의 SSL 인증서 문제 우회
- ⭐ PC www.naver.com 5개 탭 — 추천/패션뷰티/리빙푸드/지식/건강 (블로그 관련성 ↑)
- jsonl 구조화 로깅 — 분석 / 디버깅 용이
- SQLite 단일 DB — 모듈 간 데이터 공유 + Claude 비용 자동 추적
14. 다음 단계
| 단계 | 내용 | 소요 |
|---|---|---|
| 즉시 | ~/projects/naver-blog-automation/ git init + 골격 생성 | 30분 |
| 단기 | core/ 인프라 4개 구현 | 1일 |
| 중기 | Module 1 컴포넌트 10개 구현 | 5일 |
| 검증 | E2E 실데이터 dry-run + vault 리포트 검수 | 1일 |
| 자동화 | cron 등록 + RUNBOOK 문서화 | 1일 |
관련 문서
- 부모: 블로그-홈판-노출-수익화-자동화-프로젝트
- 자매: module1-점수화-엔진-설계
- 알고리즘: 네이버-홈판-알고리즘
- 코드 리포:
~/projects/naver-blog-automation/(별도 git)
작성: 2026-05-13 최종 수정: 2026-05-13 (Day 1~6 완료, MVP 완성) 상태: 🟢 MVP 완성 — launchd 등록 + 자동 운영 시작 다음 단계: Module 2 (제목 + 썸네일) — 2026-06 시작 예정