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-garden

3. 컴포넌트 책임 (Single Responsibility)

3.1 module_1_subject/ 패키지

파일역할입력출력
orchestrator.py5단계 흐름 조율시드, 프로필추천 리스트
home_feed_collector.pySelenium 홈판 50건 수집URLlist[HomeFeedItem]
csv_fallback_loader.pyvault CSV 백업 로드datelist[HomeFeedItem]
niche_generator.pyClaude 니치 후보 생성시드 5개후보 150개
scorer.py4가지 점수 + 종합니치 + 컨텍스트점수 dict
gate_keeper.py게이트 + 다양성점수 리스트필터링 결과
report_generator.pyMarkdown 리포트결과.md (vault)
prompts.py프롬프트 템플릿 상수-str
lookup_tables.py카테고리·수수료·CPM-dict
types.pydataclass 정의--

3.2 core/ 공통 인프라

파일역할
claude_client.pyAnthropic SDK 래퍼. prompt caching + 재시도 + 비용 로깅
db.pySQLite 연결, 마이그레이션, dataclass ↔ row 매핑
logger.pystructlog 기반 jsonl 구조화 로깅
config.pypydantic-settings, .env + settings.yaml 통합
vault_io.pyvault 경로 read/write 추상화 (위 2번 참조)
errors.pyHomeFeedCollectionError, ClaudeAPIError, GateRejectError

3.3 utils/ 유틸리티 (pure functions)

파일역할
selenium_driver.pyChrome 드라이버 팩토리 (stealth, UA 위장, headless)
keyword_extractor.py한국어 형태소 분석 → 키워드 (kiwipiepy)
category_inferrer.py제목/키워드 → 카테고리 (룰 + Claude fallback)
markdown_renderer.pyjinja2 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 final

7. 설정 (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: jsonl

7.2 .env (gitignore)

ANTHROPIC_API_KEY=sk-ant-...
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
VAULT_ROOT=~/projects/jyp-garden
ENVIRONMENT=development
DEBUG=true

8. 테스트 전략

8.1 Unit (빠른 피드백)

테스트대상모킹
test_scorer.py점수 공식 + 종합입력 dict 고정
test_gate_keeper.py3중 게이트 + 다양성fixtures
test_keyword_extractor.py한국어 키워드-
test_lookup_tables.py카테고리 룩업-

8.2 Integration

테스트대상모킹
test_orchestrator.py5단계 E2EClaude + Selenium mock
test_home_feed_collector.pySelenium 파싱로컬 HTML fixture
test_report_generator.pyMarkdown 출력snapshot 비교

8.3 Smoke (운영 검증)

uv run python scripts/run_module1_weekly.py --dry-run --week-id 2026-W19

9. 로깅 (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. 핵심 결정 사항 (실구현 반영)

  1. 코드 위치 ~/projects/naver-blog-automation/ — vault와 완전 분리
  2. vault 통합은 core/vault_io.py 단일 경로 — 양방향 결합 최소화
  3. Module 1 패키지화 — 단일 파일 대신 10개 컴포넌트 분리
  4. core/ 공통 인프라 — Module 2-7 재사용 보장
  5. utils/는 pure function — 의존성 없는 헬퍼만
  6. Claude Code CLI subprocess (claude -p) — Anthropic API 키 X, Pro 구독 사용
  7. 1회 통합 호출 — 니치 생성 + 점수화를 하나의 호출에 (호출당 ~$0.20 cache_creation 비용 우회)
  8. Selenium Manager 내장 사용 — webdriver-manager의 SSL 인증서 문제 우회
  9. PC www.naver.com 5개 탭 — 추천/패션뷰티/리빙푸드/지식/건강 (블로그 관련성 ↑)
  10. jsonl 구조화 로깅 — 분석 / 디버깅 용이
  11. 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일

관련 문서


작성: 2026-05-13 최종 수정: 2026-05-13 (Day 1~6 완료, MVP 완성) 상태: 🟢 MVP 완성 — launchd 등록 + 자동 운영 시작 다음 단계: Module 2 (제목 + 썸네일) — 2026-06 시작 예정