경쟁사 PR 발굴기 (Competitor PR Finder)
제품 URL을 입력하세요. 경쟁사를 찾고, 모든 경쟁사가 사용한 PR 채널(뉴스, 팟캐스트, 커뮤니티)을 조사하여 여러 경쟁사에 걸쳐 나타나는 채널(검증된 타겟)을 발굴하고, 각 채널의 기자나 진행자를 찾아 제품에 맞춤화된 콜드 피치를 Tier 1 채널별로 작성합니다.
환각 금지 정책: 출력의 모든 채널, 기자 이름, 스토리 앵글, 피치 세부 정보는 특정 Tavily 검색 결과 또는 가져온 제품 페이지에서 추적 가능해야 합니다. 이는 다음에 적용됩니다:
- 경쟁사 이름: AI 훈련 지식이 아닌 Tavily 검색 결과에 나타난 이름이어야 함
- 채널 이름: 검색 결과에 URL이 있어야 함
- 기자/진행자 이름: Tavily 스니펫에 그대로 나타나야 함
- 스토리 앵글: 검색 결과의 기사/에피소드 제목에서만 추출
- 피치 초안: 검색 데이터 + 제품 분석의 구체적인 증거를 참조해야 함
일반적인 실수
| 에이전트가 하려는 행동 | 왜 틀렸는가 |
|---|---|
| 훈련 지식에서 기자 이름을 말하기 | 모든 기자 이름은 검색 결과 스니펫에서 추적되어야 합니다. 기억에서 "Sarah Perez가 TechCrunch에서 스타트업을 담당합니다"라고 쓰는 것은 환각입니다. |
| 증거 URL 없이 채널 나열하기 | 출력의 모든 채널은 경쟁사가 해당 채널에 소개되었음을 증명하는 PR 검색 결과의 URL이 하나 이상 있어야 합니다. |
| 경쟁사 확인 단계 건너뛰기 | 발견된 경쟁사를 항상 표시하고 사용자 확인을 기다리세요. 잘못된 경쟁사는 검색 낭비와 쓸모없는 출력을 초래합니다. |
| 일반적인 피치 생성("저희를 소개해 주세요") | 모든 피치는 증거의 특정 앵글과 제품 분석의 특정 차별점을 모두 참조해야 합니다. |
| 경쟁사 1개만 있는 채널을 Tier 1으로 표시 | Tier 1 = 경쟁사 3+개. Tier 2 = 정확히 2개. Tier 3 = 1개. 검증되지 않은 채널은 승격시키지 마십시오. |
| 출력에서 em 대시 사용 | 모든 em 대시(--)를 하이픈(-)으로 바꾸십시오. |
실행 전 참조 파일 읽기
cat references/pr-channel-types.md
cat references/pitch-guide.md
cat references/tier-scoring.md
1단계: 설정 확인
echo "TAVILY_API_KEY: ${TAVILY_API_KEY:+set}${TAVILY_API_KEY:-NOT SET -- required}"
echo "FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:+set}${FIRECRAWL_API_KEY:-not set, Tavily extract will be used as fallback}"
TAVILY_API_KEY가 없는 경우: 즉시 중단합니다. 사용자에게 말합니다: "경쟁사 조사 및 PR 취재 정보를 찾으려면 TAVILY_API_KEY가 필요합니다. 대체 방법이 없습니다. app.tavily.com에서 가져오세요. 무료 티어: 월 1000크레딧(약 23회 검색/실행 기준으로 약 43회 전체 실행 가능). .env 파일에 추가하세요."
FIRECRAWL_API_KEY만 없는 경우: 계속 진행합니다. URL 가져오기에는 Tavily 추출이 사용됩니다.
2단계: 입력 파싱
대화에서 수집:
product_url: 가져올 URL (필수, 사용자가 직접 설명을 붙여넣지 않는 한)product_name: 선택 사항, 제공되지 않으면 페이지에서 파생geography: 선택 사항 -- US / Europe / global. 기본값: US
사용자가 URL 없이 설명만 붙여넣는 경우: 3단계와 4단계를 건너뜁니다. 붙여넣은 텍스트를 product_content로 사용하여 4단계(제품 분석)로 바로 이동합니다. page_source를 user_description으로 설정하고 data_quality_flags에 기록합니다.
URL도 설명도 없는 경우: 묻습니다: "제품 또는 스타트업의 URL은 무엇인가요? 또는 짧은 설명을 붙여넣으세요: 무엇을 하는지, 누구를 위한 것인지, 경쟁사와 무엇이 다른지."
제품 슬러그 파생:
PRODUCT_SLUG=$(python3 -c "
from urllib.parse import urlparse
import sys
url = 'URL_HERE'
if url.startswith('http'):
host = urlparse(url).netloc.replace('www.', '')
print(host.split('.')[0])
else:
import re
print(re.sub(r'[^a-z0-9]', '-', url[:30].lower()).strip('-'))
")
echo "Product slug: $PRODUCT_SLUG"
3단계: 제품 페이지 가져오기
기본: Firecrawl (FIRECRAWL_API_KEY가 설정된 경우)
curl -s -X POST https://api.firecrawl.dev/v1/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "URL_HERE", "formats": ["markdown"], "onlyMainContent": true}' \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('data', {}).get('markdown', '') or d.get('markdown', '')
print(f'Fetched via Firecrawl: {len(content)} characters')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
대체: Tavily 추출 (FIRECRAWL_API_KEY가 설정되지 않은 경우)
curl -s -X POST https://api.tavily.com/extract \
-H "Content-Type: application/json" \
-d "{\"api_key\": \"$TAVILY_API_KEY\", \"urls\": [\"URL_HERE\"]}" \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('results', [{}])[0].get('raw_content', '')
print(f'Fetched via Tavily extract: {len(content)} characters')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
체크포인트:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()
if len(content) < 200:
print('ERROR: fewer than 200 characters fetched')
else:
print(f'Content OK: {len(content)} characters')
"
내용이 200자 미만인 경우: 가져오기를 중단합니다. 사용자에게 말합니다: "제품 페이지에서 읽을 수 있는 콘텐츠를 반환하지 않았습니다. 사이트가 JavaScript로 렌더링되어 가져오기가 차단된 것 같습니다. 설명을 직접 짧게 붙여넣어 주세요: 무엇을 하는지, 누구를 위한 것인지, 무엇이 다른지."
4단계: 제품 분석 (AI)
페이지 내용 출력:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()[:5000]
print('=== PRODUCT PAGE (first 5000 chars) ===')
print(content)
"
AI 지침: 위 제품 페이지를 분석하고 다음을 추출하세요:
product_name: 제품 또는 회사 이름one_line_description: 무엇을 하는지, 누구를 위한 것인지, 핵심 가치 제안. 20단어 미만. 마케팅 언어 사용 금지. 예시: "파이프라인을 자체 호스팅하는 개발자 팀을 위한 CI/CD 자동화"industry_taxonomy:l1(최상위: 예: developer tools / fintech / healthtech / consumer),l2(섹터: 예: devops / payments / telemedicine),l3(특정 틈새: 예: CI/CD automation / embedded payments / async video consultation). "기술"과 같은 모호한 라벨만으로는 허용되지 않습니다.differentiators: 이 제품을 일반 경쟁사와 구별하는 정확히 2-3가지 구체적인 사항. 이는 피치 초안에 직접 반영됩니다. 구체적으로 작성하세요. 예시: ["자체 호스팅 파이프라인 러너 -- 데이터가 인프라를 떠나지 않음", "동적 단계 생성을 통한 모노레포 기본 지원"]icp:buyer_persona(직책),company_type,company_sizegeography_bias: US / Europe / global / unclearpage_source: "live_page" 또는 "user_description"
/tmp/cprf-product-analysis.json에 쓰기:
python3 << 'PYEOF'
import json
analysis = {
# 위 분석 내용을 채우세요
"product_name": "",
"one_line_description": "",
"industry_taxonomy": {"l1": "", "l2": "", "l3": ""},
"differentiators": [],
"icp": {"buyer_persona": "", "company_type": "", "company_size": ""},
"geography_bias": "US",
"page_source": "live_page"
}
json.dump(analysis, open('/tmp/cprf-product-analysis.json', 'w'), indent=2)
print('Product analysis written.')
PYEOF
확인:
python3 -c "
import json
a = json.load(open('/tmp/cprf-product-analysis.json'))
print('Product:', a['product_name'])
print('Industry:', a['industry_taxonomy']['l1'], '>', a['industry_taxonomy']['l2'], '>', a['industry_taxonomy']['l3'])
print('Differentiators:')
for d in a['differentiators']:
print(f' - {d}')
"
4b단계: 1단계 -- 경쟁사 발견
ls scripts/research.py 2>/dev/null && echo "script found" || echo "ERROR: scripts/research.py not found -- cannot continue"
python3 scripts/research.py \
--phase discover \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-competitors-raw.json
AI 검토를 위해 결과 출력:
python3 -c "
import json
data = json.load(open('/tmp/cprf-competitors-raw.json'))
print(f'Searches run: {len(data[\"competitor_searches\"])}')
for s in data['competitor_searches']:
print(f'\nQuery: {s[\"query\"]}')
print(f'Answer: {s.get(\"answer\",\"\")[:400]}')
for r in s.get('results', [])[:5]:
print(f' - {r[\"title\"]} | {r[\"url\"]}')
print(f' {r.get(\"content\",\"\")[:200]}')
"
AI 지침: 위 검색 결과를 읽으십시오. 다음 조건을 충족하는 정확히 5개의 경쟁사 회사를 선택하십시오:
- 검색 결과 제목, 답변 또는 스니펫에 이름이 언급되어야 함
- 분석 중인 제품과 동일한 L3 틈새에 있어야 함
- 실제 경쟁 제품(에이전시, 컨설팅 회사, 목록 기사가 아님)이어야 함
- 서로 구별되어야 함(다른 이름의 동일 회사가 아님)
각 경쟁사에 대해 작성: name, url (해당 경쟁사가 나타난 검색 결과에서), description (스니펫의 한 문장), source_url (해당 경쟁사가 발견된 검색 결과 URL).
5단계: 경쟁사 확인
발견된 경쟁사를 사용자에게 표시:
python3 << 'PYEOF'
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
# 위 검색 결과에서 채울 5개의 경쟁사
candidates = [
# {"name": str, "url": str, "description": str, "source_url": str}
]
print(f"\nFound 5 competitors for {analysis['product_name']} in {analysis['industry_taxonomy']['l3']}:\n")
for i, c in enumerate(candidates, 1):
print(f" {i}. {c['name']} -- {c['description']}")
print(f" {c['url']}")
data = json.load(open('/tmp/cprf-competitors-raw.json'))
data['competitor_candidates'] = candidates
json.dump(data, open('/tmp/cprf-competitors-raw.json', 'w'), indent=2)
PYEOF
사용자에게 말합니다: "PR 취재 정보를 조사할 5개의 경쟁사입니다. 추가, 제거, 교체를 원하시면 알려주세요. 또는 '좋아요'라고 말씀해 주시면 계속 진행하겠습니다."
확인을 기다립니다. 사용자가 목록을 편집하는 경우(추가/제거/교체), 후보를 그에 따라 업데이트합니다. 그런 다음 확인된 목록을 작성합니다:
python3 << 'PYEOF'
import json
# 사용자 검토 후 확인된 경쟁사 목록 채우기
confirmed = [
# {"name": str, "url": str}
]
json.dump({"confirmed_competitors": confirmed}, open('/tmp/cprf-competitors-confirmed.json', 'w'), indent=2)
print(f"Confirmed {len(confirmed)} competitors for PR research.")
for c in confirmed:
print(f" - {c['name']} ({c['url']})")
PYEOF
6단계: 3-트랙 PR 조사 (2단계)
python3 scripts/research.py \
--phase pr-research \
--competitors /tmp/cprf-competitors-confirmed.json \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-pr-raw.json
이것은 경쟁사당 3개 검색을 실행합니다(총 15개):
- 트랙 A (미디어):
"[competiser]" featured press coverage TechCrunch Forbes Wired article interview - 트랙 B (팟캐스트):
"[competiser]" founder CEO podcast interview appeared on episode - 트랙 C (커뮤니티):
"[competiser]" site:reddit.com OR site:news.ycombinator.com OR site:producthunt.com
취재 요약 출력:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
print(f'Competitors researched: {data[\"competitors_researched\"]}')
print()
for r in data['results']:
print(f'{r[\"competitor\"]}:')
for track, tdata in r['tracks'].items():
n = len(tdata.get('results', []))
print(f' {track:12}: {n} results')
"
경쟁사에 대한 3개 트랙 모두 결과가 0인 경우: 이 경쟁사는 PR 취재가 매우 적습니다. data_quality_flags에 기록하고 진행합니다. 교차 경쟁사 패턴은 나머지 4개에서도 작동합니다.
7단계: 패턴 분석 (AI)
모든 원시 PR 결과 출력:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
for r in data['results']:
print(f'\n=== {r[\"competitor\"]} ===')
for track, tdata in r['tracks'].items():
print(f'\n--- Track {track.upper()} ---')
print(f'Query: {tdata[\"query\"]}')
print(f'Answer: {tdata.get(\"answer\",\"\")[:400]}')
for item in tdata.get('results', [])[:5]:
print(f' Title: {item[\"title\"]}')
print(f' URL: {item[\"url\"]}')
print(f' Snippet: {item.get(\"content\",\"\")[:200]}')
"
AI 지침: 위의 모든 검색 결과를 읽으십시오. 채널 빈도 맵을 구축하십시오.
1단계 -- URL을 루트 도메인으로 정규화: https://techcrunch.com/2023/06/article-title → techcrunch.com. https://open.spotify.com/episode/... → 팟캐스트로 식별(spotify 에피소드). https://www.reddit.com/r/devops/ → reddit.com/r/devops.
2단계 -- 발생 횟수 계산: 각 채널 루트의 결과에 몇 개의 다른 경쟁사가 나타났습니까? 경쟁사 A의 트랙 A와 경쟁사 B의 트랙 A 모두에 나타난 채널은 빈도 2로 계산됩니다.
3단계 -- 채널 계층화 (references/tier-scoring.md 참조):
- Tier 1: 3+개 경쟁사에 나타남
- Tier 2: 정확히 2개 경쟁사에 나타남
- Tier 3: 1개 경쟁사에 나타남
4단계 -- 스토리 앵글 추출 결과의 기사/에피소드 제목에서. 각각을 다음 중 하나로 분류: funding-announcement / product-launch / founder-story / trend-piece / category-creation / how-to / comparison / award. 추론하지 마십시오. 제목에 보이는 앵글만 분류하십시오.
5단계 -- 각각의 채널 유형 분류: editorial / podcast / community / newsletter.
/tmp/cprf-pr-patterns.json에 쓰기:
python3 << 'PYEOF'
import json
patterns = {
"tier_1_channels": [
# 채우기 -- 3+개 경쟁사에 나타난 채널
# 각 항목: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_2_channels": [
# 채우기 -- 정확히 2개 경쟁사에 나타난 채널
# 각 항목: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str]}
],
"tier_3_channels": [
# 채우기 -- 1개 경쟁사에 나타난 채널
# 각 항목: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str]}
],
"cross_pattern_insights": {
"dominant_channel_type": "",
"dominant_story_angle": "",
"niche_uniqueness": ""
},
"data_quality_flags": []
}
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('PR patterns written.')
PYEOF
8단계: 기자/진행자 찾기 (3단계)
Tier 1 채널의 경우 저널리스트/호스트 이름과 활동 분야를 찾으려면 추가 검색이 필요합니다.
# Tier 1 채널에 대해서만 실행
for channel_name in "TechCrunch" "The Verge"; do
echo "--- Looking up: $channel_name ---"
python3 scripts/research.py \
--phase lookup-journalist \
--channel "$channel_name" \
--niche "CATEGORY_L3" \
--tavily-key "$TAVILY_API_KEY" \
--output "/tmp/cprf-lookup-$(echo $channel_name | tr '[:upper:]' '[:lower:]' | tr ' ' '-').json"
done
조회 결과 출력 및 검토:
ls /tmp/cprf-lookup-*.json
python3 -c "
import json, glob
for f in sorted(glob.glob('/tmp/cprf-lookup-*.json')):
data = json.load(open(f))
channel = f.split('-',2)[-1].replace('.json','')
print(f'\n=== {channel} ===')
for r in data.get('results', [])[:4]:
print(f' Title: {r[\"title\"]}')
print(f' Snippet: {r.get(\"content\",\"\")[:200]}')
print(f' URL: {r[\"url\"]}')
"
AI 지침: 위 조회 결과를 읽습니다. 각 Tier 1 채널에 대해 journalist_name과 journalist_beat를 추출합니다. 베스트는: 결과 중 하나의 제목이나 스니펫에 나타난 정확한 이름. 두 번째 베스트는: 스니펫의 "기고자:" 라인. 저널리스트가 특정 틈새(예: "DevOps at The New Stack")를 다룬다는 증거가 스니펫에 없으면 journalist_beat를 "general tech"로 설정합니다. 이름이 전혀 없는 경우 빈 문자열로 남겨둡니다.
추출된 이름으로 /tmp/cprf-pr-patterns.json 업데이트:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
for channel in patterns['tier_1_channels']:
# 조회 결과에서 채운 journalist_name 및 journalist_beat
channel['journalist_name'] = ""
channel['journalist_beat'] = ""
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('Journalists added to patterns.')
PYEOF
9단계: 피치 작성 (AI)
references/pitch-guide.md의 지침에 따라 각 Tier 1 채널에 대한 개인화된 콜드 이메일을 작성합니다. 각 피치는 다음을 포함해야 합니다:
- 스니펫에 언급된 해당 채널에 소개된 경쟁사의 구체적인 출처
- 동일한 출처에서 추출한 스토리 앵글(예: "product launch for X")
/tmp/cprf-product-analysis.json의differentiators중 하나- 특정 워딩과 함께 제품을 해당 채널에 맞는 스토리로 포지셔닝하는 방법
출력을 /tmp/cprf-pitches.json에 저장:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
pitches = []
for channel in patterns['tier_1_channels']:
# 사용자 정의 피치 작성
pitch = {
"channel": channel['channel_name'],
"channel_url": channel['channel_url'],
"journalist_name": channel.get('journalist_name', 'Editor'),
"journalist_beat": channel.get('journalist_beat', 'General tech'),
"story_angle": channel['story_angles_used'][0] if channel.get('story_angles_used') else '',
"source_competitor": channel['found_in_competitors'][0],
"evidence_url": channel['evidence_urls'][0],
"pitch_draft": f"""Hi {{{{Journalist Name}}}},
I saw you covered {{{{Source Competitor}}}}'s {{story_angle}} in {{{{Channel Name}}}}: {{{{evidence_url}}}}
{{product_name}} is doing something similar but with a key difference: {{{{differentiator}}}}.
Would you be interested in {{product_name}}? We believe it would make a great story for your readers.
Best,
{{Your Name}}"""
}
pitches.append(pitch)
json.dump({"pitches": pitches}, open('/tmp/cprf-pitches.json', 'w'), indent=2, ensure_ascii=False)
print(f'{len(pitches)} pitches written to /tmp/cprf-pitches.json')
PYEOF
10단계: 최종 출력 생성
모든 데이터를 사람이 읽을 수 있는 형식으로 조합합니다:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
pitches = json.load(open('/tmp/cprf-pitches.json'))
output = f"""# 경쟁사 PR 분석 보고서: {analysis['product_name']}
## 개요
- **제품:** {analysis['product_name']}
- **설명:** {analysis['one_line_description']}
- **산업:** {analysis['industry_taxonomy']['l1']} > {analysis['industry_taxonomy']['l2']} > {analysis['industry_taxonomy']['l3']}
- **차별점:**
"""
for d in analysis['differentiators']:
output += f" - {d}\n"
output += f"\n## Tier 1 채널 (우선 순위 타겟)\n\n"
for c in patterns['tier_1_channels']:
output += f"### {c['channel_name']}\n"
output += f"- **유형:** {c['channel_type']}\n"
output += f"- **URL:** {c['channel_url']}\n"
output += f"- **발견된 경쟁사:** {', '.join(c['found_in_competitors'])}\n"
output += f"- **기자/호스트:** {c.get('journalist_name', '알 수 없음')} ({c.get('journalist_beat', '')})\n"
output += f"- **사용된 스토리 앵글:** {', '.join(c.get('story_angles_used', []))}\n"
output += f"- **증거 URL:**\n"
for url in c['evidence_urls'][:3]:
output += f" - {url}\n"
output += "\n"
output += f"## Tier 2 채널 (2차 타겟)\n\n"
for c in patterns['tier_2_channels']:
output += f"- {c['channel_name']} ({', '.join(c['found_in_competitors'])})\n"
output += f"\n## Tier 3 채널 (1회성 타겟)\n\n"
for c in patterns['tier_3_channels']:
output += f"- {c['channel_name']}\n"
output += f"\n## 교차 패턴 인사이트\n"
output += f"- **지배적인 채널 유형:** {patterns['cross_pattern_insights']['dominant_channel_type']}\n"
output += f"- **지배적인 스토리 앵글:** {patterns['cross_pattern_insights']['dominant_story_angle']}\n"
output += f"- **틈새 고유성:** {patterns['cross_pattern_insights']['niche_uniqueness']}\n"
if patterns.get('data_quality_flags'):
output += f"\n## 데이터 품질 플래그\n"
for flag in patterns['data_quality_flags']:
output += f"- ⚠️ {flag}\n"
output += "\n" + "="*50 + "\n"
print(output)
PYEOF
스크립트: research.py
/tmp/cprf-script.py에서 scripts/research.py를 찾거나 재생성합니다:
#!/usr/bin/env python3
"""경쟁사 PR 발굴기 (CPRF) - research.py
용법:
python3 research.py --phase discover --product-analysis <json> --tavily-key <key> --output <json>
python3 research.py --phase pr-research --competitors <json> --product-analysis <json> --tavily-key <key> --output <json>
python3 research.py --phase lookup-journalist --channel <str> --niche <str> --tavily-key <key> --output <json>
"""
import argparse
import json
import os
import sys
from urllib.parse import urlparse, quote
import requests
TAVILY_API_URL = "https://api.tavily.com/search"
def search_tavily(api_key: str, query: str, max_results: int = 10) -> dict:
"""Tavily 검색 API 호출"""
payload = {
"api_key": api_key,
"query": query,
"search_depth": "advanced",
"include_answer": True,
"max_results": max_results
}
resp = requests.post(TAVILY_API_URL, json=payload)
resp.raise_for_status()
return resp.json()
def phase_discover(product_analysis_path: str, tavily_key: str, output_path: str):
"""경쟁사 발견 단계: 제품 분석을 기반으로 검색을 실행하여 잠재적 경쟁사를 식별합니다."""
with open(product_analysis_path) as f:
analysis = json.load(f)
l3 = analysis['industry_taxonomy']['l3']
description = analysis['one_line_description']
queries = [
f"top competitors in {l3} {description}",
f"alternatives to {analysis['product_name']} {l3}",
f"best {l3} solutions {description}",
f"{l3} market leaders competitors",
f"who competes with {analysis['product_name']} in {l3}"
]
results = []
for q in queries:
try:
data = search_tavily(tavily_key, q, max_results=10)
results.append({
"query": q,
"answer": data.get("answer", ""),
"results": [
{"title": r["title"], "url": r["url"], "content": r["content"]}
for r in data.get("results", [])
]
})
except Exception as e:
results.append({"query": q, "error": str(e)})
output = {
"product_name": analysis['product_name'],
"competitor_searches": results,
"competitor_candidates": []
}
with open(output_path, 'w') as f:
json.dump(output, f, indent=2)
print(json.dumps({"message": f"Discovered {len(results)} searches written."}), file=sys.stderr)
def phase_pr_research(competitors_path: str, product_analysis_path: str, tavily_key: str, output_path: str):
"""PR 조사 단계: 각 경쟁사에 대해 3-트랙 검색을 실행합니다."""
with open(competitors_path) as f:
comp_data = json.load(f)
with open(product_analysis_path) as f:
analysis = json.load(f)
competitors = comp_data["confirmed_competitors"]
l3 = analysis['industry_taxonomy']['l3']
results = []
for comp in competitors:
name = comp["name"]
tracks = {}
# 트랙 A: 미디어
query_a = f'"{name}" featured press coverage TechCrunch Forbes Wired article interview'
try:
data_a = search_tavily(tavily_key, query_a, max_results=10)
tracks["editorial"] = {
"query": query_a,
"answer": data_a.get("answer", ""),
"results": [
{"title": r["title"], "url": r["url"], "content": r["content"]}
for r in data_a.get("results", [])
]
}
except Exception as e:
tracks["editorial"] = {"query": query_a, "error": str(e), "results": []}
# 트랙 B: 팟캐스트
query_b = f'"{name}" founder CEO podcast interview appeared on episode'
try:
data_b = search_tavily(tavily_key, query_b, max_results=10)
tracks["podcast"] = {
"query": query_b,
"answer": data_b.get("answer", ""),
"results": [
{"title": r["title"], "url": r["url"], "content": r["content"]}
for r in data_b.get("results", [])
]
}
except Exception as e:
tracks["podcast"] = {"query": query_b, "error": str(e), "results": []}
# 트랙 C: 커뮤니티
query_c = f'"{name}" site:reddit.com OR site:news.ycombinator.com OR site:producthunt.com'
try:
data_c = search_tavily(tavily_key, query_c, max_results=10)
tracks["community"] = {
"query": query_c,
"answer": data_c.get("answer", ""),
"results": [
{"title": r["title"], "url": r["url"], "content": r["content"]}
for r in data_c.get("results", [])
]
}
except Exception as e:
tracks["community"] = {"query": query_c, "error": str(e), "results": []}
results.append({"competitor": name, "tracks": tracks})
output = {
"competitors_researched": len(competitors),
"results": results
}
with open(output_path, 'w') as f:
json.dump(output, f, indent=2)
print(json.dumps({"message": f"PR research completed for {len(competitors)} competitors."}), file=sys.stderr)
def phase_lookup_journalist(channel: str, niche: str, tavily_key: str, output_path: str):
"""기자 조회 단계: 특정 채널의 기자/호스트를 찾습니다."""
queries = [
f"{channel} journalist who covers {niche}",
f"{channel} reporter {niche}",
f"{channel} editor {niche}",
f"{channel} author {niche}"
]
results = []
for q in queries:
try:
data = search_tavily(tavily_key, q, max_results=5)
results.append({
"query": q,
"results": [
{"title": r["title"], "url": r["url"], "content": r["content"]}
for r in data.get("results", [])
]
})
except Exception as e:
results.append({"query": q, "error": str(e)})
with open(output_path, 'w') as f:
json.dump({"channel": channel, "results": results}, f, indent=2)
print(json.dumps({"message": f"Journalist lookup completed for {channel}."}), file=sys.stderr)
def main():
parser = argparse.ArgumentParser(description="CPRF Research Script")
parser.add_argument("--phase", required=True, choices=["discover", "pr-research", "lookup-journalist"])
parser.add_argument("--product-analysis")
parser.add_argument("--competitors")
parser.add_argument("--channel")
parser.add_argument("--niche")
parser.add_argument("--tavily-key", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
if args.phase == "discover":
if not args.product_analysis:
print(json.dumps({"error": "--product-analysis required for discover phase"}), file=sys.stderr)
sys.exit(1)
phase_discover(args.product_analysis, args.tavily_key, args.output)
elif args.phase == "pr-research":
if not args.competitors or not args.product_analysis:
print(json.dumps({"error": "--competitors and --product-analysis required for pr-research phase"}), file=sys.stderr)
sys.exit(1)
phase_pr_research(args.competitors, args.product_analysis, args.tavily_key, args.output)
elif args.phase == "lookup-journalist":
if not args.channel or not args.niche:
print(json.dumps({"error": "--channel and --niche required for lookup-journalist phase"}), file=sys.stderr)
sys.exit(1)
phase_lookup_journalist(args.channel, args.niche, args.tavily_key, args.output)
if __name__ == "__main__":
main()
참조 파일
pr-channel-types.md
# PR 채널 유형
PR 채널은 일반적으로 세 가지 주요 유형으로 분류할 수 있습니다:
1. **미디어 (Editorial):** TechCrunch, Forbes, Wired, VentureBeat, The Verge 등 기술/비즈니스 뉴스 매체. 기사 형태의 보도.
2. **팟캐스트 (Podcast):** The Joe Rogan Experience, Lex Fridman Podcast, Software Engineering Daily 등. 인터뷰 또는 패널 토론 형식.
3. **커뮤니티 (Community):** Reddit (r/programming, r/devops), Hacker News, Product Hunt, Dev.to 등. 커뮤니티 중심의 발견 및 논의.
pitch-guide.md
# 피치 가이드
좋은 콜드 피치는 다음을 포함해야 합니다:
1. **맞춤형 인사말:** 기자/호스트의 이름을 사용합니다.
2. **관련성 증명:** 경쟁사가 그들의 채널에 소개된 기사/에피소드를 언급합니다.
3. **스토리 앵글:** 경쟁사가 어떤 각도로 소개되었는지 설명합니다.
4. **차별점:** 경쟁사와 달리 당신의 제품이 어떻게 다른지 명확히 합니다.
5. **명확한 CTA:** 기사, 인터뷰, 또는 데모를 요청합니다.
금지 사항:
- "우리는 당신의 채널을 좋아합니다"와 같은 일반적인 칭찬
- 요청 없이 긴 제품 기능 목록
- 증거 없이 "획기적인" 또는 "게임 체인저"와 같은 과장된 언어
- 모든 채널에 동일한 피치를 복사하여 붙여넣기
tier-scoring.md
# 계층 점수
채널은 여러 경쟁사에 걸친 출현 빈도에 따라 계층화됩니다:
- **Tier 1 (최우선):** 3개 이상의 경쟁사에 나타난 채널. 입증된 타겟.
- **Tier 2 (중간):** 정확히 2개의 경쟁사에 나타난 채널. 잠재적 타겟.
- **Tier 3 (낮음):** 1개의 경쟁사에만 나타난 채널. 탐색적 타겟.
계층이 높을수록 해당 채널이 이 틈새의 보도를 수용할 가능성이 높습니다.