# InfoKeyword 기능 레지스트리
**버전**: v1.0
**최종 업데이트**: 2026-03-07
**코드 기준**: 694f382 (Initial commit: InfoKeyword - 정보성 키워드 자동 판별 서비스)

---

## 개요

- **한 줄 설명**: 네이버 블로그 검색 데이터를 7단계로 분석하여 키워드의 정보성 여부를 자동 판별하는 SaaS 도구
- **기술 스택**:
  - Backend (Worker): Python 3.12, FastAPI, uvicorn, httpx, BeautifulSoup4, Playwright, Pydantic v2, Claude CLI (subprocess)
  - Frontend: Next.js 15, React 19, TypeScript, Tailwind CSS, shadcn/ui, Firebase (Auth + Firestore)
  - 외부 저장소: Google Cloud Storage (리포트 PNG/HTML), Firebase Firestore (분석 이력, 사용자)
- **주요 경로**:
  - Worker API: `worker/main.py` (FastAPI, 포트 8100)
  - 분석 파이프라인: `worker/pipeline/analyzer.py`
  - 크롤러: `worker/crawler/`
  - 분석기: `worker/analyzer/`
  - 리포터: `worker/reporter/`
  - 프론트엔드: `src/app/` (Next.js App Router)
  - 공유 스키마: `shared/schemas/`

---

## 기능 목록

---

### 카테고리 1: Worker API 서버

#### 기능 1-1: FastAPI 애플리케이션 기동
- **파일**: `/home/jay/projects/InfoKeyword/worker/main.py`
- **설명**: FastAPI 기반 REST API 서버. CORS 미들웨어, 전역 예외 핸들러, 인메모리 분석 결과 저장소(`_analysis_store`)를 포함한다. uvicorn으로 0.0.0.0:8100에서 기동한다.
- **핵심 함수/클래스**: `app` (FastAPI 인스턴스), `global_exception_handler()`
- **입력**: 환경 변수 `API_HOST`, `API_PORT`, `CORS_ORIGINS`
- **출력**: HTTP 서버 기동
- **상태**: 구현완료
- **의존성**: `worker.config`, FastAPI, uvicorn

#### 기능 1-2: API 키 인증
- **파일**: `/home/jay/projects/InfoKeyword/worker/main.py`
- **설명**: 모든 보호 엔드포인트에 `X-API-Key` 헤더 검증 의존성을 주입한다. 키 불일치 시 HTTP 401을 반환한다.
- **핵심 함수/클래스**: `verify_api_key()`
- **입력**: `X-API-Key` HTTP 헤더, 환경 변수 `INFORKEYWORD_API_KEY`
- **출력**: None (통과) 또는 HTTPException 401
- **상태**: 구현완료
- **의존성**: `worker.config.API_KEY`

#### 기능 1-3: 헬스체크 엔드포인트
- **파일**: `/home/jay/projects/InfoKeyword/worker/main.py`
- **설명**: 인증 없이 서버 상태를 확인할 수 있는 GET 엔드포인트. 로드밸런서/모니터링 용도.
- **핵심 함수/클래스**: `health_check()` (`GET /health`)
- **입력**: 없음
- **출력**: `{"status": "ok", "service": "inforkeyword-worker"}`
- **상태**: 구현완료
- **의존성**: 없음

#### 기능 1-4: 키워드 생성 엔드포인트
- **파일**: `/home/jay/projects/InfoKeyword/worker/main.py`
- **설명**: 주제(topic)와 tier를 받아 Claude CLI로 키워드 30개를 동기 생성하여 반환한다.
- **핵심 함수/클래스**: `endpoint_generate()` (`POST /generate`)
- **입력**: `{"topic": str, "tier": int}` + `X-API-Key`
- **출력**: `{"keywords": [{"keyword": str, "tier": int}, ...]}`
- **상태**: 구현완료
- **의존성**: `worker.generator.keyword_generator.generate_keywords()`

#### 기능 1-5: 분석 요청 엔드포인트 (비동기)
- **파일**: `/home/jay/projects/InfoKeyword/worker/main.py`
- **설명**: 키워드 목록(최대 5개)을 받아 UUID analysis_id를 즉시 반환하고, 백그라운드 태스크(`asyncio.create_task`)로 7단계 분석을 실행한다.
- **핵심 함수/클래스**: `endpoint_analyze()` (`POST /analyze`), `_run_analysis()`
- **입력**: `{"keywords": ["kw1", ...]}` + `X-API-Key`
- **출력**: `{"analysis_id": str, "status": "processing"}`
- **상태**: 구현완료
- **의존성**: `worker.pipeline.analyzer.analyze_keywords()`

#### 기능 1-6: 분석 상태 조회 엔드포인트
- **파일**: `/home/jay/projects/InfoKeyword/worker/main.py`
- **설명**: analysis_id로 인메모리 저장소에서 분석 상태를 조회한다. 완료 시 결과와 report_url을 포함하여 반환한다.
- **핵심 함수/클래스**: `endpoint_status()` (`GET /status/{analysis_id}`)
- **입력**: `analysis_id` (path param) + `X-API-Key`
- **출력**: `{"analysis_id": str, "status": "processing"|"completed"|"failed", "results": [...] | null, "error": str | null}`
- **상태**: 구현완료
- **의존성**: `_analysis_store` (인메모리 dict)

---

### 카테고리 2: 키워드 생성 (generator)

#### 기능 2-1: Claude CLI 기반 키워드 자동 생성
- **파일**: `/home/jay/projects/InfoKeyword/worker/generator/keyword_generator.py`
- **설명**: 주제와 tier(단수)를 프롬프트로 Claude CLI를 subprocess 호출하여 키워드 30개를 JSON 배열로 생성한다. 파싱 실패 시 최대 2회 재시도한다. 입력 topic 자체를 결과 리스트 첫 번째에 배치한다.
- **핵심 함수/클래스**: `generate_keywords()`, `_build_prompt()`, `_extract_json_array()`, `_validate_keywords()`, `_call_claude()`
- **입력**: `topic: str`, `tier: int` (2 또는 3)
- **출력**: `[{"keyword": str, "tier": int}, ...]` (최대 30개)
- **상태**: 구현완료
- **의존성**: Claude CLI (`claude -p <prompt>`), `worker.config.CLAUDE_CLI_TIMEOUT`

---

### 카테고리 3: 7단계 분석 파이프라인 (pipeline)

#### 기능 3-1: 파이프라인 오케스트레이터
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 하나의 키워드에 대해 7단계 분석을 순서대로 실행하고 최종 판정(INFORMATIONAL / NOT_INFORMATIONAL)을 내린다. 2~4단계는 asyncio.gather로 병렬 실행한다. 스크린샷 캡처도 병렬로 수행한다.
- **핵심 함수/클래스**: `analyze_keyword()`, `analyze_keywords()`
- **입력**: `keyword: str`
- **출력**: `{"keyword": str, "steps": {...}, "steps_2_4_or": bool, "verdict": str, "passed_steps": int, "total_steps": 7, "report_url"?: str}`
- **상태**: 구현완료
- **의존성**: 모든 크롤러/분석기/리포터 모듈

#### 기능 3-2: Step 1 — 다단키워드 체크
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 키워드의 공백 기준 단어 수를 세어 2개 이상이면 통과.
- **핵심 함수/클래스**: `_step1_multiword()`
- **입력**: `keyword: str`
- **출력**: `{"pass": bool, "word_count": int}`
- **상태**: 구현완료
- **의존성**: 없음 (순수 Python)

#### 기능 3-3: Step 2 — 네이버 연관검색어 존재 여부
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 연관검색어가 1개 이상이면 통과.
- **핵심 함수/클래스**: `_step2_related()`
- **입력**: `keyword: str`
- **출력**: `{"pass": bool, "related_keywords": list[str]}`
- **상태**: 구현완료
- **의존성**: `crawler.related_keywords.get_related_keywords()`

#### 기능 3-4: Step 3 — 네이버 자동완성 결과 존재 여부
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 자동완성 제안어가 1개 이상이면 통과.
- **핵심 함수/클래스**: `_step3_autocomplete()`
- **입력**: `keyword: str`
- **출력**: `{"pass": bool, "suggestions": list[str]}`
- **상태**: 구현완료
- **의존성**: `crawler.autocomplete.get_autocomplete()`

#### 기능 3-5: Step 4 — 월간 검색량 임계값 확인
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 네이버 검색광고 API로 조회한 PC+모바일 합계 검색량이 SEARCH_VOLUME_THRESHOLD(기본 30) 이상이면 통과.
- **핵심 함수/클래스**: `_step4_search_volume()`
- **입력**: `keyword: str`
- **출력**: `{"pass": bool, "pc_volume": int, "mobile_volume": int, "total_volume": int}`
- **상태**: 구현완료
- **의존성**: `crawler.search_ad.get_search_volume()`

#### 기능 3-6: Step 5 — 블로그 TOP10 홍보성 비율 분석
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 네이버 블로그 검색 상위 10개를 크롤링하고, 광고 제외 후 비광고 네이버 블로그에 대해 전화번호/주소/외부링크/톡톡/플레이스/첨부파일/이미지 분석을 수행한다. 홍보성 비율이 PROMOTIONAL_THRESHOLD(기본 50%) 이하이면 통과. 세마포어로 동시 처리 수를 3으로 제한한다.
- **핵심 함수/클래스**: `_step5_promotional()`, `_analyze_single_blog()`
- **입력**: `blogs: list[dict]` (search_blogs 결과)
- **출력**: `{"pass": bool, "ratio": float, "promotional_count": int, "informational_count": int, "ad_excluded_count": int, "total_crawled": int, "total_analyzed": int, "informational_positions": list, "promotional_positions": list, "ad_excluded_positions": list, "details": list[dict]}`
- **상태**: 구현완료
- **의존성**: `crawler.blog_search`, `crawler.blog_content`, `analyzer.phone_address`, `analyzer.external_links`, `analyzer.attachment`, `analyzer.image_analysis`

#### 기능 3-7: Step 6 — 외부 블로그 부재 확인
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 블로그 검색 결과(광고 제외)에서 비네이버 블로그(티스토리, 브런치 등)가 0개이면 통과.
- **핵심 함수/클래스**: `_step6_external_blog()`
- **입력**: `blogs: list[dict]` (search_blogs 결과)
- **출력**: `{"pass": bool, "external_count": int, "total_base": int, "external_positions": list, "external_blogs": list}`
- **상태**: 구현완료
- **의존성**: 없음 (5단계 결과 재사용)

#### 기능 3-8: Step 7 — 카페탭 대표뱃지 수 확인
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: 네이버 카페탭 상위 10개에서 대표뱃지(대표카페) 수가 CAFE_BADGE_THRESHOLD(기본 5) 이하이면 통과.
- **핵심 함수/클래스**: `_step7_cafe_badge()`
- **입력**: `keyword: str`
- **출력**: `{"pass": bool, "representative_count": int, "total_results": int, "representative_positions": list, "details": list}`
- **상태**: 구현완료
- **의존성**: `crawler.cafe_search.count_cafe_badges()`

#### 기능 3-9: 최종 판정 로직
- **파일**: `/home/jay/projects/InfoKeyword/worker/pipeline/analyzer.py`
- **설명**: `step1 AND (step2 OR step3 OR step4) AND step5 AND step6 AND step7` 조건이 모두 충족되면 INFORMATIONAL, 그렇지 않으면 NOT_INFORMATIONAL로 판정한다.
- **핵심 함수/클래스**: `analyze_keyword()` 내 판정 블록
- **입력**: 7개 step 결과
- **출력**: `verdict: "INFORMATIONAL" | "NOT_INFORMATIONAL"`, `passed_steps: int`
- **상태**: 구현완료
- **의존성**: 없음

---

### 카테고리 4: 크롤러 (crawler)

#### 기능 4-1: 네이버 자동완성 키워드 크롤링
- **파일**: `/home/jay/projects/InfoKeyword/worker/crawler/autocomplete.py`
- **설명**: `mac.search.naver.com/mobile/ac` API를 호출하여 자동완성 제안 목록을 가져온다. 자기 자신 키워드는 제외한다.
- **핵심 함수/클래스**: `get_autocomplete()`
- **입력**: `keyword: str`
- **출력**: `list[str]` (제안어 목록, 오류 시 빈 리스트)
- **상태**: 구현완료
- **의존성**: httpx, `utils.rate_limiter`

#### 기능 4-2: 네이버 연관검색어 크롤링
- **파일**: `/home/jay/projects/InfoKeyword/worker/crawler/related_keywords.py`
- **설명**: 네이버 통합검색 HTML에서 `relatedKeywords` 데이터를 추출한다. 1차 전략: `<script>` 블록 내 URL 인코딩된 JSON 파싱. 2차 전략: HTML DOM의 `<a>` 태그 폴백 파싱.
- **핵심 함수/클래스**: `get_related_keywords()`, `_extract_from_script_blocks()`, `_extract_from_html_dom()`
- **입력**: `keyword: str`
- **출력**: `list[str]` (오류 시 빈 리스트)
- **상태**: 구현완료
- **의존성**: httpx, BeautifulSoup4, `utils.rate_limiter`

#### 기능 4-3: 네이버 검색광고 API — 월간 검색량 조회
- **파일**: `/home/jay/projects/InfoKeyword/worker/crawler/search_ad.py`
- **설명**: HMAC-SHA256 서명 인증으로 `api.searchad.naver.com/keywordstool`을 호출하여 PC/모바일 월간 검색량을 반환한다. API 키 미설정 시 스텁(0) 값을 반환한다. `< 10` 형식의 저량 값도 파싱한다.
- **핵심 함수/클래스**: `get_search_volume()`, `_fetch_search_volume()`, `_build_signature()`, `_build_headers()`, `_parse_count()`
- **입력**: `keyword: str`
- **출력**: `{"pc": int, "mobile": int, "total": int}`
- **상태**: 구현완료
- **의존성**: httpx, HMAC-SHA256, `worker.config` (NAVER_SEARCHAD_API_KEY 등)

#### 기능 4-4: 네이버 블로그 검색 결과 스크래핑
- **파일**: `/home/jay/projects/InfoKeyword/worker/crawler/blog_search.py`
- **설명**: 네이버 블로그탭 검색 결과에서 상위 N개(기본 10)를 파싱한다. 광고 항목(파워링크)을 다중 전략으로 감지하여 `is_ad=True`로 태그한다. 블로그 URL에서 네이버/외부 구분, 도메인, 작성자명을 추출한다.
- **핵심 함수/클래스**: `search_blogs()`, `_is_ad()`, `_parse_blog_item()`, `_is_naver_blog()`, `_parse_source_domain()`
- **입력**: `keyword: str`, `top_n: int` (기본 10)
- **출력**: `list[dict]` — 각 항목: `{title, url, blog_name, is_naver_blog, source_domain, rank, is_ad}`
- **상태**: 구현완료
- **의존성**: httpx, BeautifulSoup4, `utils.rate_limiter`

#### 기능 4-5: 네이버 블로그 포스트 본문 수집
- **파일**: `/home/jay/projects/InfoKeyword/worker/crawler/blog_content.py`
- **설명**: `blog.naver.com/PostView.naver` 포맷으로 블로그 포스트 전문을 수집한다. SmartEditor 3(SE3) 및 SmartEditor 2 구조를 모두 지원한다. 본문 텍스트, 이미지 URL, 외부 링크, 첨부파일 유무, 네이버 톡톡/플레이스 링크를 추출한다. 애드포스트 광고 컨테이너 링크는 제외한다.
- **핵심 함수/클래스**: `get_blog_content()`, `_parse_blog_id_and_log_no()`, `_extract_text()`, `_extract_images()`, `_extract_external_links()`, `_detect_naver_special_links()`, `_has_attachment()`
- **입력**: `blog_url: str` (blog.naver.com 또는 m.blog.naver.com)
- **출력**: `{"text": str, "image_urls": list, "external_links": list, "has_attachment": bool, "has_talktalk": bool, "has_place": bool, "raw_html": str}`
- **상태**: 구현완료
- **의존성**: httpx, BeautifulSoup4, `utils.rate_limiter`

#### 기능 4-6: 네이버 카페탭 대표뱃지 수 집계
- **파일**: `/home/jay/projects/InfoKeyword/worker/crawler/cafe_search.py`
- **설명**: 네이버 카페탭 검색 결과에서 광고를 제외한 상위 N개(기본 10) 항목을 파싱하고, 대표카페 뱃지(`i.spnew.api_ico_total` 또는 텍스트 "대표") 수를 집계한다.
- **핵심 함수/클래스**: `count_cafe_badges()`, `_has_representative_badge()`, `_is_ad_item()`, `_parse_cafe_item()`
- **입력**: `keyword: str`, `top_n: int` (기본 10)
- **출력**: `{"representative_count": int, "total_results": int, "representative_positions": list, "details": list[dict]}`
- **상태**: 구현완료
- **의존성**: httpx, BeautifulSoup4, `utils.rate_limiter`

---

### 카테고리 5: 분석기 (analyzer)

#### 기능 5-1: 전화번호 텍스트 감지
- **파일**: `/home/jay/projects/InfoKeyword/worker/analyzer/phone_address.py`
- **설명**: 정규식으로 블로그 본문에서 유선전화(02/031~099), 휴대폰(010/011/016/017/018/019), 대표번호(1588/1644 등 15XX~19XX)를 감지한다. 중복 제거 후 발견 순서 유지.
- **핵심 함수/클래스**: `detect_phone_numbers()`
- **입력**: `text: str`
- **출력**: `list[str]` (감지된 전화번호)
- **상태**: 구현완료
- **의존성**: 없음 (순수 Python re)

#### 기능 5-2: 주소 텍스트 감지
- **파일**: `/home/jay/projects/InfoKeyword/worker/analyzer/phone_address.py`
- **설명**: 정규식으로 시도(서울/경기 등 17개)+ 시군구 + 로/길/동/읍/면 패턴의 한국 주소를 감지한다.
- **핵심 함수/클래스**: `detect_addresses()`
- **입력**: `text: str`
- **출력**: `list[str]` (감지된 주소)
- **상태**: 구현완료
- **의존성**: 없음 (순수 Python re)

#### 기능 5-3: 외부 링크 감지
- **파일**: `/home/jay/projects/InfoKeyword/worker/analyzer/external_links.py`
- **설명**: URL 목록에서 네이버 외부 도메인으로의 링크를 감지한다. 예외: `*.naver.com`, `*.naver.net`, `blog.naver.com`, `expert.naver.com`, `*.go.kr`, `*.or.kr`. 앵커/javascript/mailto/tel 링크는 무시한다.
- **핵심 함수/클래스**: `detect_external_links()`, `detect_talktalk()`, `detect_place()`
- **입력**: `links: list[str]`
- **출력**: `list[str]` (외부 링크 목록), `bool` (톡톡/플레이스 여부)
- **상태**: 구현완료
- **의존성**: 없음 (순수 Python urllib)

#### 기능 5-4: 첨부파일 감지
- **파일**: `/home/jay/projects/InfoKeyword/worker/analyzer/attachment.py`
- **설명**: 블로그 본문 HTML에서 SmartEditor 첨부파일 컨테이너(`.se-module-file`)를 감지한다.
- **핵심 함수/클래스**: `detect_attachment()`
- **입력**: `html: str`
- **출력**: `bool`
- **상태**: 구현완료
- **의존성**: BeautifulSoup4

#### 기능 5-5: 이미지 내 전화번호/주소 감지 (Playwright + Claude 멀티모달)
- **파일**: `/home/jay/projects/InfoKeyword/worker/analyzer/image_analysis.py`
- **설명**: Playwright로 블로그 포스트 전체 페이지 스크린샷(1280x900, 최대 4096px 높이)을 촬영하고, Claude CLI 멀티모달로 이미지에 포함된 전화번호/주소를 판별한다. 네이버 카페 홍보 이미지(아프니까사장이다 등)는 분석에서 제외한다. 스크린샷 임시 파일은 분석 후 즉시 삭제한다.
- **핵심 함수/클래스**: `analyze_blog_images()`, `_take_screenshot()`, `_resize_screenshot()`, `_call_claude()`, `_normalize_result()`
- **입력**: `blog_url: str`
- **출력**: `{"has_phone_in_image": bool, "has_address_in_image": bool, "detected_phones": list, "detected_addresses": list}`
- **상태**: 구현완료
- **의존성**: Playwright (Chromium), Claude CLI, Pillow, `worker.config.CHROMIUM_PATH`

#### 기능 5-6: LLM 기반 홍보성 글 판별 (미사용 모듈)
- **파일**: `/home/jay/projects/InfoKeyword/worker/analyzer/llm_promotional.py`
- **설명**: 블로그 본문 텍스트(앞 1500자 + 뒤 500자 발췌)를 Claude CLI로 분석하여 홍보성 여부와 confidence를 반환한다. confidence 0.7 이상일 때만 홍보로 판정한다. 최대 2회 재시도한다. **현재 파이프라인에서 직접 호출되지 않음** — 규칙 기반 분석(5-1~5-4)이 대신 사용된다.
- **핵심 함수/클래스**: `judge_promotional()`, `_truncate_text()`, `_call_claude()`, `_normalize_result()`
- **입력**: `text: str`
- **출력**: `{"is_promotional": bool, "confidence": float, "reason": str}`
- **상태**: 부분구현 (코드 존재, 파이프라인 미연동)
- **의존성**: Claude CLI, `worker.config.CLAUDE_CLI_TIMEOUT`

---

### 카테고리 6: 유틸리티

#### 기능 6-1: 비동기 Rate Limiter
- **파일**: `/home/jay/projects/InfoKeyword/worker/utils/rate_limiter.py`
- **설명**: 모든 크롤러가 공유하는 모듈 레벨 싱글턴 Rate Limiter. asyncio.Semaphore로 최대 동시 요청 수(기본 3)를 제한하고, 요청마다 0.3~1.0초 무작위 지연을 적용한다. async context manager로 사용한다.
- **핵심 함수/클래스**: `RateLimiter`, `rate_limiter` (모듈 레벨 싱글턴)
- **입력**: `max_concurrent=3`, `delay_min=0.3`, `delay_max=1.0`
- **출력**: 세마포어 취득 + 지연 후 컨텍스트 진입
- **상태**: 구현완료
- **의존성**: asyncio

---

### 카테고리 7: 리포터 (reporter)

#### 기능 7-1: Playwright 리포트 스크린샷 캡처
- **파일**: `/home/jay/projects/InfoKeyword/worker/reporter/screenshot.py`
- **설명**: 분석 리포트에 첨부할 스크린샷을 촬영한다. 네이버 통합검색(web), 블로그탭(blog), 카페탭(cafe) URL을 각각 캡처하는 함수와 블로그 포스트 URL, 임의 URL 캡처 함수를 제공한다. 최대 높이 4096px 초과 시 자동 잘라냄.
- **핵심 함수/클래스**: `capture_naver_search()`, `capture_blog_post()`, `capture_url()`, `_take_screenshot_of_url()`, `_resize_if_needed()`
- **입력**: `keyword: str`, `tab: str` ("web"/"blog"/"cafe") / `url: str`
- **출력**: 스크린샷 파일 경로(`str`) 또는 `None`
- **상태**: 구현완료
- **의존성**: Playwright (Chromium), Pillow, `worker.config.CHROMIUM_PATH`, `REPORT_SCREENSHOT_DIR`

#### 기능 7-2: HTML 리포트 생성
- **파일**: `/home/jay/projects/InfoKeyword/worker/reporter/report_generator.py`
- **설명**: 분석 결과 dict와 스크린샷 GCS URL을 받아 발표형 HTML 리포트를 생성한다. 7단계 결과를 각각 카드 형태로 렌더링하며 통과/미통과 뱃지, 검색량 데이터 카드, 블로그 상세 정보를 포함한다.
- **핵심 함수/클래스**: `generate_html_report()`, `save_html_report()`, `_step_card()`, `_verdict_badge()`, `_screenshot_section()`
- **입력**: `keyword_result: dict` (analyze_keyword 반환), `screenshot_urls: dict[str, str]`
- **출력**: HTML 문자열 / 저장된 파일 경로
- **상태**: 구현완료
- **의존성**: `worker.config.REPORT_SCREENSHOT_DIR`

#### 기능 7-3: Google Cloud Storage 업로드
- **파일**: `/home/jay/projects/InfoKeyword/worker/reporter/drive_uploader.py`
- **설명**: GCS 서비스 계정 인증 후 스크린샷 PNG와 HTML 리포트를 `reports/{analysis_id}/` 경로에 업로드하고 공개 URL을 반환한다. 7일(REPORT_RETENTION_DAYS) 경과된 오래된 blob을 삭제하는 기능도 포함한다.
- **핵심 함수/클래스**: `DriveUploader`, `upload_file()`, `upload_report()`, `delete_old_folders()`
- **입력**: 로컬 파일 경로, GCS 경로 접두사, MIME 타입
- **출력**: 공개 URL (`https://storage.googleapis.com/{bucket}/{blob_path}`)
- **상태**: 구현완료
- **의존성**: `google-cloud-storage`, `google-auth`, `worker.config.GCS_BUCKET_NAME`

---

### 카테고리 8: 데이터 모델 및 스키마

#### 기능 8-1: Pydantic 분석 결과 모델
- **파일**: `/home/jay/projects/InfoKeyword/worker/models.py`, `/home/jay/projects/InfoKeyword/worker/_step_models.py`
- **설명**: 분석 결과 전체 구조를 Pydantic v2로 타입화한다. `KeywordAnalysisResult`가 최상위 모델이며, 7개 step 모델을 `AnalysisSteps`로 묶는다. `pass` 예약어 충돌을 `Field(alias="pass")`로 처리한다.
- **핵심 함수/클래스**: `KeywordAnalysisResult`, `AnalysisSteps`, `Step1MultiWord`, `Step2Related`, `Step3Autocomplete`, `Step4SearchVolume`, `Step5Promotional`, `Step6ExternalBlog`, `Step7CafeBadge`, `BlogDetail`, `BlogInfo`
- **입력**: 분석 결과 dict
- **출력**: 타입 검증된 Pydantic 모델 인스턴스
- **상태**: 구현완료
- **의존성**: Pydantic v2

#### 기능 8-2: JSON Schema 자동 내보내기
- **파일**: `/home/jay/projects/InfoKeyword/scripts/export_schema.py`
- **설명**: Pydantic 모델에서 `keyword-analysis.schema.json` (JSON Schema draft-07)을 자동 생성하여 `shared/schemas/`에 저장한다. `make export-schema`로 실행한다.
- **핵심 함수/클래스**: `main()`, `KeywordAnalysisResult.model_json_schema()`
- **입력**: 없음 (Pydantic 모델 자동 추출)
- **출력**: `/home/jay/projects/InfoKeyword/shared/schemas/keyword-analysis.schema.json`
- **상태**: 구현완료
- **의존성**: `worker.models`

---

### 카테고리 9: 프론트엔드 — 인증

#### 기능 9-1: Firebase Google 소셜 로그인
- **파일**: `/home/jay/projects/InfoKeyword/src/app/login/page.tsx`, `/home/jay/projects/InfoKeyword/src/lib/auth-context.tsx`
- **설명**: Firebase Authentication의 Google 팝업 로그인을 제공한다. 로그인 성공 시 `ik_users` Firestore 컬렉션에 사용자를 upsert하고 승인 상태를 확인한다. 승인됨 → 대시보드, 미승인 → pending 페이지로 분기한다.
- **핵심 함수/클래스**: `LoginPage`, `signInWithGoogle()`, `ensureIkUser()`, `AuthProvider`, `useAuth()`
- **입력**: Google OAuth 팝업
- **출력**: Firebase User, IkUser (Firestore 문서)
- **상태**: 구현완료
- **의존성**: Firebase Auth, Firebase Firestore

#### 기능 9-2: 승인 유효성 검사 (시간 기반)
- **파일**: `/home/jay/projects/InfoKeyword/src/lib/auth-context.tsx`
- **설명**: `approvalDuration` 분 이내이면 유효한 승인으로 인정한다. 0이면 무제한. 만료 시 `isApproved=false`가 되어 접근이 차단된다.
- **핵심 함수/클래스**: `isApprovalValid()`
- **입력**: `IkUser.approvedAt`, `IkUser.approvalDuration`
- **출력**: `bool`
- **상태**: 구현완료
- **의존성**: Firebase Firestore

#### 기능 9-3: 어드민 자동 승인
- **파일**: `/home/jay/projects/InfoKeyword/src/lib/auth-context.tsx`
- **설명**: 로그인 이메일이 `jonghyuk.jeon@gmail.com`인 경우 자동으로 `approved=true`, `isAdmin=true`, `approvalDuration=0`(무제한)으로 설정한다.
- **핵심 함수/클래스**: `ensureIkUser()` 내 `isAdmin` 판정 로직
- **입력**: Firebase User 이메일
- **출력**: Firestore ik_users 문서 갱신
- **상태**: 구현완료
- **의존성**: Firebase Firestore

#### 기능 9-4: 클라이언트 사이드 AuthGuard
- **파일**: `/home/jay/projects/InfoKeyword/src/components/auth-guard.tsx`
- **설명**: 미승인 사용자를 보호된 페이지에서 차단하는 래퍼 컴포넌트. 인증 상태 로딩 중에는 스피너를 표시한다.
- **핵심 함수/클래스**: `AuthGuard`
- **입력**: `children: React.ReactNode`
- **출력**: children 렌더링 또는 login/pending 리다이렉트
- **상태**: 구현완료
- **의존성**: `useAuth()`

#### 기능 9-5: 승인 대기 페이지
- **파일**: `/home/jay/projects/InfoKeyword/src/app/pending/page.tsx`
- **설명**: 로그인했지만 관리자 승인을 기다리는 사용자에게 안내 메시지를 표시한다. 승인 완료 시 자동으로 대시보드로 이동한다.
- **핵심 함수/클래스**: `PendingPage`
- **입력**: `useAuth()` 상태
- **출력**: 대기 안내 UI + 로그아웃 버튼
- **상태**: 구현완료
- **의존성**: `useAuth()`

---

### 카테고리 10: 프론트엔드 — 분석 플로우 (3단계)

#### 기능 10-1: STEP 1/3 — 주제 입력 및 키워드 생성
- **파일**: `/home/jay/projects/InfoKeyword/src/app/analyze/topic/page.tsx`
- **설명**: 사용자가 분석 주제와 tier(2단/3단)를 입력하면 Worker API를 통해 Claude로 키워드 30개를 생성한다. 생성된 키워드를 sessionStorage에 저장하고 선택 페이지로 이동한다.
- **핵심 함수/클래스**: `TopicContent`, `handleGenerate()`
- **입력**: 주제 텍스트, tier 선택 (2 또는 3)
- **출력**: 키워드 30개 (sessionStorage: `ik_topic`, `ik_tier`, `ik_keywords`)
- **상태**: 구현완료
- **의존성**: `src/lib/api.ts generateKeywords()`

#### 기능 10-2: STEP 2/3 — 키워드 선택
- **파일**: `/home/jay/projects/InfoKeyword/src/app/analyze/select/page.tsx`
- **설명**: 생성된 30개 키워드를 그리드로 표시하고 최대 5개를 선택할 수 있다. 선택 후 분석 시작 버튼으로 Worker API에 분석을 요청하고 Firestore에 분석 기록을 저장한다.
- **핵심 함수/클래스**: `SelectContent`, `handleToggle()`, `handleStartAnalysis()`
- **입력**: 생성된 키워드 목록, 사용자 선택 (최대 5개)
- **출력**: analysis_id, Firestore 문서 ID (sessionStorage: `ik_analysis_id`, `ik_doc_id`)
- **상태**: 구현완료
- **의존성**: `api.ts startAnalysis()`, `api.ts saveAnalysis()`

#### 기능 10-3: STEP 3/3 — 분석 진행 상황 모니터링
- **파일**: `/home/jay/projects/InfoKeyword/src/app/analyze/progress/page.tsx`
- **설명**: 3초 간격으로 Worker API를 폴링하여 분석 상태를 확인한다. 완료 시 결과를 Firestore에 저장하고 리포트 페이지로 이동한다. 프로그레스 바는 완료 전까지 10%씩 증가한다.
- **핵심 함수/클래스**: `ProgressContent`, `poll()`, `handleCompleted()`
- **입력**: sessionStorage의 `ik_analysis_id`, `ik_doc_id`
- **출력**: Firestore 결과 업데이트 → `/report/{docId}` 이동
- **상태**: 구현완료
- **의존성**: `api.ts checkAnalysisStatus()`, `api.ts updateAnalysisResults()`

---

### 카테고리 11: 프론트엔드 — 대시보드 및 리포트

#### 기능 11-1: 분석 대시보드
- **파일**: `/home/jay/projects/InfoKeyword/src/app/dashboard/page.tsx`
- **설명**: 현재 사용자의 분석 이력을 Firestore에서 조회하여 최신순으로 표시한다. 각 카드에 주제, 분석 날짜, 키워드 목록, 정보성/비정보성 집계를 보여준다. 카드 클릭 시 리포트 페이지로 이동한다.
- **핵심 함수/클래스**: `DashboardContent`, `StatusBadge`
- **입력**: `user.uid` (Firebase Auth)
- **출력**: 분석 이력 카드 목록
- **상태**: 구현완료
- **의존성**: `api.ts getUserAnalyses()`

#### 기능 11-2: 분석 결과 리포트 페이지
- **파일**: `/home/jay/projects/InfoKeyword/src/app/report/[id]/page.tsx`
- **설명**: Firestore에서 분석 결과를 조회하여 키워드별 상세 결과를 표시한다. AnalysisSummary로 전체 요약을 표시하고, KeywordResultCard로 각 키워드의 7단계 결과를 확장/축소 가능하게 렌더링한다. Step 2~4는 OR 그룹으로 시각화한다. GCS report_url이 있으면 "근거 리포트 보기" 버튼을 표시한다.
- **핵심 함수/클래스**: `ReportContent`, `KeywordResultCard`, `renderStepDetail()`
- **입력**: Firestore 분석 ID (URL param)
- **출력**: 7단계 분석 결과 UI
- **상태**: 구현완료
- **의존성**: `api.ts getAnalysisById()`

#### 기능 11-3: 근거 리포트 페이지 (스크린샷 임베드)
- **파일**: `/home/jay/projects/InfoKeyword/src/app/report/[id]/evidence/page.tsx`
- **설명**: 각 키워드의 GCS report_url(HTML)을 iframe으로 임베드하여 스크린샷과 단계별 근거를 표시한다. report_url이 없는 키워드는 빈 상태 UI를 표시한다.
- **핵심 함수/클래스**: `EvidenceContent`, `EvidenceCard`
- **입력**: Firestore 분석 ID (URL param)
- **출력**: 키워드별 GCS HTML 리포트 iframe 임베드
- **상태**: 구현완료
- **의존성**: `api.ts getAnalysisById()`

---

### 카테고리 12: 프론트엔드 — 어드민

#### 기능 12-1: 사용자 관리 어드민 페이지
- **파일**: `/home/jay/projects/InfoKeyword/src/app/admin/page.tsx`
- **설명**: `ik_users` Firestore 컬렉션의 전체 사용자를 목록으로 표시한다. 비어드민 사용자에 대해 승인/승인취소 버튼을 제공한다. 승인 시 30분 유효기간이 설정된다.
- **핵심 함수/클래스**: `AdminContent`, `ApprovalBadge`, `handleApprove()`, `handleRevoke()`
- **입력**: 어드민 권한 확인
- **출력**: 사용자 목록 + 승인/취소 액션
- **상태**: 구현완료
- **의존성**: `api.ts getAllUsers()`, `api.ts approveUser()`, `api.ts revokeUser()`

---

### 카테고리 13: 프론트엔드 — Next.js API 라우트 (BFF)

#### 기능 13-1: 키워드 생성 프록시
- **파일**: `/home/jay/projects/InfoKeyword/src/app/api/keyword/generate/route.ts`
- **설명**: 프론트엔드의 키워드 생성 요청을 Worker API(`POST /generate`)로 프록시한다. WORKER_URL 및 WORKER_API_KEY 환경 변수로 Worker 엔드포인트와 인증을 관리한다.
- **핵심 함수/클래스**: `POST` handler
- **입력**: `{"topic": str, "tier": int}`
- **출력**: Worker 응답 그대로 전달
- **상태**: 구현완료
- **의존성**: 환경 변수 `WORKER_URL`, `WORKER_API_KEY`

#### 기능 13-2: 분석 시작 프록시
- **파일**: `/home/jay/projects/InfoKeyword/src/app/api/analyze/route.ts`
- **설명**: 프론트엔드의 분석 시작 요청을 Worker API(`POST /analyze`)로 프록시한다.
- **핵심 함수/클래스**: `POST` handler
- **입력**: `{"keywords": list[str]}`
- **출력**: `{"analysis_id": str, "status": "processing"}`
- **상태**: 구현완료
- **의존성**: 환경 변수 `WORKER_URL`, `WORKER_API_KEY`

#### 기능 13-3: 분석 상태 조회 프록시
- **파일**: `/home/jay/projects/InfoKeyword/src/app/api/analyze/[id]/route.ts`
- **설명**: 분석 상태 조회 요청을 Worker API(`GET /status/{id}`)로 프록시한다.
- **핵심 함수/클래스**: `GET` handler
- **입력**: `id` (URL path param)
- **출력**: Worker 상태 응답
- **상태**: 구현완료
- **의존성**: 환경 변수 `WORKER_URL`, `WORKER_API_KEY`

---

### 카테고리 14: 프론트엔드 — 공통 컴포넌트

#### 기능 14-1: 분석 요약 컴포넌트
- **파일**: `/home/jay/projects/InfoKeyword/src/components/analysis-summary.tsx`
- **설명**: 전체 분석 결과의 정보성/비정보성 집계를 요약 카드로 표시한다.
- **핵심 함수/클래스**: `AnalysisSummary`
- **입력**: `results: KeywordResult[]`
- **출력**: 요약 집계 카드 UI
- **상태**: 구현완료

#### 기능 14-2: 키워드 카드 컴포넌트
- **파일**: `/home/jay/projects/InfoKeyword/src/components/keyword-card.tsx`
- **설명**: 선택 페이지에서 키워드별 카드를 표시하고 선택/비선택 토글을 제공한다. disabled 상태(최대 5개 초과)도 처리한다.
- **핵심 함수/클래스**: `KeywordCard`
- **입력**: `keyword: str`, `tier: int`, `selected: bool`, `onToggle: func`, `disabled?: bool`
- **출력**: 토글 가능한 키워드 카드
- **상태**: 구현완료

#### 기능 14-3: 단계 뱃지 컴포넌트
- **파일**: `/home/jay/projects/InfoKeyword/src/components/step-badge.tsx`
- **설명**: 분석 단계(1~7)의 통과/실패/대기 상태를 색상 뱃지로 표시한다.
- **핵심 함수/클래스**: `StepBadge`
- **입력**: `stepNumber: int`, `label: str`, `status: "pass" | "fail" | "pending"`
- **출력**: 색상 코딩된 단계 뱃지
- **상태**: 구현완료

#### 기능 14-4: 헤더 컴포넌트
- **파일**: `/home/jay/projects/InfoKeyword/src/components/header.tsx`
- **설명**: 전역 상단 헤더. 로고, 사용자 정보, 로그아웃 버튼을 포함한다.
- **핵심 함수/클래스**: `Header`
- **상태**: 구현완료

---

### 카테고리 15: 유지보수 스크립트

#### 기능 15-1: Google Drive 오래된 리포트 정리
- **파일**: `/home/jay/projects/InfoKeyword/scripts/cleanup_drive.py`
- **설명**: Google Drive API로 `InfoKeyword-Reports` 루트 폴더의 하위 폴더 중 지정 일수(기본 7일) 이상 경과된 것을 삭제한다. `--dry-run` 옵션으로 실제 삭제 없이 대상만 확인할 수 있다. cron으로 매일 실행하도록 설계되었다.
- **핵심 함수/클래스**: `cleanup_old_reports()`, `authenticate_drive()`, `find_root_folder()`, `delete_folder()`
- **입력**: `--days int`, `--dry-run flag`
- **출력**: 삭제 결과 요약 (total_found, deleted, errors)
- **상태**: 구현완료
- **의존성**: `google-api-python-client`, Google Drive API v3

---

## 외부 연동

| 서비스 | 용도 | 환경 변수 | 설정 위치 |
|---|---|---|---|
| Claude CLI | 키워드 생성, 이미지 멀티모달 분석 | `CLAUDE_CLI_TIMEOUT` | `worker/config.py` |
| 네이버 검색광고 API | 월간 검색량 조회 (Step 4) | `NAVER_SEARCHAD_API_KEY`, `NAVER_SEARCHAD_SECRET_KEY`, `NAVER_SEARCHAD_CUSTOMER_ID` | `worker/config.py` |
| Firebase Authentication | Google 소셜 로그인 | `NEXT_PUBLIC_FIREBASE_*` (6개) | `src/lib/firebase.ts` |
| Firebase Firestore | 분석 이력 저장 (`ik_analyses`), 사용자 관리 (`ik_users`) | Firebase 프로젝트 설정 | `src/lib/firebase.ts` |
| Google Cloud Storage | 스크린샷/HTML 리포트 저장 (버킷: `infokeyword-j2h-reports`) | `GCS_BUCKET_NAME`, `GOOGLE_DRIVE_CREDENTIALS_PATH` | `worker/config.py` |
| Google Drive API | 오래된 리포트 폴더 정리 (cleanup 스크립트 전용) | `GOOGLE_DRIVE_CREDENTIALS_PATH` | `scripts/cleanup_drive.py` |
| Playwright (Chromium) | 블로그/검색 페이지 스크린샷 | `CHROMIUM_PATH` (기본: `~/.cache/ms-playwright/...`) | `worker/config.py` |

---

## 설정 파일

### `worker/config.py`

| 설정 키 | 기본값 | 설명 |
|---|---|---|
| `CLAUDE_CLI_TIMEOUT` | 60초 | Claude CLI subprocess 타임아웃 |
| `NAVER_SEARCHAD_API_KEY` | "" | 네이버 검색광고 API 키 |
| `NAVER_SEARCHAD_SECRET_KEY` | "" | 네이버 검색광고 시크릿 키 |
| `NAVER_SEARCHAD_CUSTOMER_ID` | "" | 네이버 검색광고 고객 ID |
| `CRAWL_DELAY_MIN` | 0.3초 | Rate limiter 최소 지연 |
| `CRAWL_DELAY_MAX` | 1.0초 | Rate limiter 최대 지연 |
| `USER_AGENT` | Chrome 131 UA | 크롤링 User-Agent |
| `CHROMIUM_PATH` | `~/.cache/ms-playwright/chromium-1208/...` | Playwright Chromium 경로 |
| `SCREENSHOT_DIR` | `/tmp/inforkeyword-screenshots` | 이미지 분석용 임시 스크린샷 디렉토리 |
| `BLOG_TOP_N` | 10 | 블로그 상위 N개 크롤링 |
| `CAFE_TOP_N` | 10 | 카페 상위 N개 크롤링 |
| `PROMOTIONAL_THRESHOLD` | 0.5 (50%) | 홍보성 통과 기준 비율 |
| `CAFE_BADGE_THRESHOLD` | 5 | 카페 대표뱃지 통과 기준 수 |
| `SEARCH_VOLUME_THRESHOLD` | 30 | 검색량 통과 기준값 |
| `API_HOST` | "0.0.0.0" | Worker API 바인딩 호스트 |
| `API_PORT` | 8100 | Worker API 포트 |
| `API_KEY` | "" | Worker API 인증 키 (`INFORKEYWORD_API_KEY`) |
| `CORS_ORIGINS` | "http://localhost:3000" | 허용 CORS 오리진 (콤마 구분) |
| `GOOGLE_DRIVE_CREDENTIALS_PATH` | "" | GCS/Drive 서비스 계정 JSON 경로 |
| `GOOGLE_DRIVE_SHARED_FOLDER_ID` | "1MBjfdNL2woyX47866UgfrLRyWZGHcSHP" | (레거시) Google Drive 폴더 ID |
| `GCS_BUCKET_NAME` | "infokeyword-j2h-reports" | GCS 버킷명 |
| `REPORT_SCREENSHOT_DIR` | `/tmp/infokeyword-report-screenshots` | 리포트용 스크린샷 임시 디렉토리 |
| `REPORT_RETENTION_DAYS` | 7 | GCS 리포트 보존 일수 |

### `src/lib/firebase.ts`
Firebase 설정은 `NEXT_PUBLIC_FIREBASE_*` 환경 변수 6개로 관리: `API_KEY`, `AUTH_DOMAIN`, `PROJECT_ID`, `STORAGE_BUCKET`, `MESSAGING_SENDER_ID`, `APP_ID`.

### `.env` / `.env.keys`
Worker 루트에 `.env`와 `.env.keys`를 사용한다. `.env.keys`가 존재하면 override=True로 우선 적용된다.

### `shared/schemas/`
- `keyword-analysis.schema.json` — Pydantic 모델로부터 자동 생성된 JSON Schema (draft-07)
- `keyword-analysis.sample.normal.json` — 정상 케이스 샘플 데이터
- `keyword-analysis.sample.edge.json` — 경계값/최소값 케이스 샘플 데이터

---

## 테스트 커버리지

| 테스트 파일 | 대상 | 주요 내용 |
|---|---|---|
| `tests/test_pipeline_steps.py` | `_step1_multiword()`, `_step6_external_blog()` | 단어 수 체크, 외부 블로그 판정 로직 (네트워크 없는 유닛 테스트) |
| `worker/tests/test_contract.py` | `KeywordAnalysisResult`, JSON Schema | Pydantic 검증, JSON Schema 검증, 모델-스키마 동기화, 데이터 무결성 |
| `tests/test_attachment.py` | `analyzer.attachment` | 첨부파일 감지 로직 |
| `tests/test_external_links.py` | `analyzer.external_links` | 외부 링크/톡톡/플레이스 감지 |
| `tests/test_phone_address.py` | `analyzer.phone_address` | 전화번호/주소 정규식 감지 |
| `tests/test_api.py` | Worker API 엔드포인트 | HTTP 요청/응답 테스트 |
| `tests/test_claude_integration.py` | Claude CLI 연동 | LLM 호출 통합 테스트 |
| `tests/test_integration.py` | 전체 파이프라인 | 엔드투엔드 통합 테스트 |
| `src/__tests__/api/routes.test.ts` | Next.js API 라우트 | `/api/analyze`, `/api/keyword/generate` |
| `src/__tests__/components/` | 프론트엔드 컴포넌트 | `AnalysisSummary`, `KeywordCard`, `StepBadge` |
| `src/__tests__/integration/worker-error.test.ts` | Worker 에러 처리 | Worker 오류 시나리오 |

---

## 미구현 / 계획 중 기능

### 코드 내 명시적 미연동
1. **LLM 기반 홍보성 판별 (`llm_promotional.py`)**: `judge_promotional()` 함수가 구현되어 있으나 파이프라인(`analyzer.py`)에서 호출되지 않는다. 현재는 규칙 기반(전화번호/주소/외부링크/첨부파일/이미지) 분석이 대신 사용된다.

### 코드 주석 기반
2. **Google Drive 공유 폴더 업로드 (`config.py` 주석)**: `GOOGLE_DRIVE_SHARED_FOLDER_ID` 설정에 "향후 사용 가능" 주석이 있다. 현재는 GCS 직접 업로드만 사용한다.
3. **근거 리포트 표시 (`evidence/page.tsx` 주석)**: "Phase 2 완료 후 표시됩니다"라는 안내 문구가 UI에 존재한다. GCS 연동이 설정되지 않은 환경에서는 근거 리포트가 생성되지 않는다.
4. **승인 만료 자동 체크**: 어드민 페이지에서 만료 상태를 표시하지만 Firestore에 `approved=false` 자동 갱신 로직은 서버 사이드에 없다 (클라이언트 사이드 시간 계산만 존재).

### 타입 정의 기반 미연동
5. **`AnalysisRecord.reportUrl` 필드**: `src/types/index.ts`에 `reportUrl?: string` 필드가 정의되어 있으나 실제로 Firestore에 저장하는 로직이 `src/lib/api.ts`에 없다. Worker의 `result["report_url"]`은 각 KeywordResult 내부에만 저장된다.
