- supporters/: Astro 사이트 소스 (pages·components·posts·factSheet/authors/videos 데이터, 뉴스룸 916건 빌더, 발행 게이트 check.mjs, vercel.json) - supporters/docs/: v1~v3 스크린샷 기록 - docs/INFINITH_Viewclinic_Supporters_Site_Design_v0.1.md: 서포터즈 사이트 설계 문서 - docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD.md: 다음 세션 자동 빌드 프롬프트 - docs/reports/Viewclinic_Supporters_vs_Official_AEO_Audit.xlsx + scripts/build_supporters_audit_xlsx.py: 서포터즈 vs 공식 사이트 AEO/GEO 감사 비교 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
188 lines
19 KiB
Python
188 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""공식 홈페이지 vs 서포터즈 사이트 AEO/GEO 비교 엑셀.
|
||
사용: python3 scripts/build_supporters_audit_xlsx.py <scratch/audit dir> <out.xlsx>
|
||
입력: official.json(공식 결과+항목명), supporters_prod.json(샘플 noindex 실측), supporters_idx.json(색인 허용 빌드 실측), overrides.json(수동 판정)"""
|
||
import json,sys,os
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font,PatternFill,Alignment,Border,Side
|
||
from openpyxl.utils import get_column_letter
|
||
A=sys.argv[1]; OUT=sys.argv[2]
|
||
off=json.load(open(f"{A}/official.json")); names=off["names"]
|
||
CATS={"A":("AI 크롤러 접근성",20),"B":("엔티티 식별·구조화",20),"C":("답변 인용 가능 콘텐츠",25),"D":("AI 인용 표면 확보",20),"E":("AI 가시성 측정 인프라",10),"F":("기술 위생",5)}
|
||
def load(fn):
|
||
d=json.load(open(f"{A}/{fn}")); res=d["results"] if isinstance(d,dict) and "results" in d else d
|
||
return {r["criterionId"]:r for r in res}
|
||
prod=load("supporters_prod.json"); idx=load("supporters_idx.json")
|
||
ov=json.load(open(f"{A}/overrides.json")) if os.path.exists(f"{A}/overrides.json") else {}
|
||
official={r["id"]:r for r in off["official"]}
|
||
def lvl(r): return r["level"] if r else "unverified"
|
||
# 색인 허용 열 = 로컬 색인 빌드 실측 + 로컬에서 판정 불가한 항목(A4·B6·A7)은 프로덕션 값 + 수동 판정 override
|
||
final={}
|
||
for cid in names:
|
||
r=idx.get(cid); src="로컬 색인 허용 빌드 실측"
|
||
if cid in ("A4","B6","A7"): r=prod.get(cid); src="프로덕션 실측 (로컬에서 판정 불가)"
|
||
ev=r["evidence"] if r else ""; L=lvl(r)
|
||
if cid in ov: L=ov[cid]["level"]; ev=ov[cid]["evidence"]; src="수동 판정 (서포터즈 실측)"
|
||
final[cid]={"level":L,"evidence":ev,"src":src}
|
||
W={c:names[c][1] for c in names}
|
||
def score(levels):
|
||
cat={}
|
||
for c in CATS:
|
||
e=p=n=0
|
||
for cid in names:
|
||
if cid[0]!=c: continue
|
||
L=levels.get(cid,"unverified")
|
||
if L=="unverified": continue
|
||
n+=1; p+=W[cid]; e+=W[cid]*L/3
|
||
cat[c]=(round(e,1),p,(round(e/p*100) if p else 0),n)
|
||
E=sum(v[0] for v in cat.values()); P=sum(v[1] for v in cat.values()); N=sum(v[3] for v in cat.values())
|
||
s=round(E/P*100) if P else 0; g="A" if s>=81 else "B" if s>=61 else "C" if s>=41 else "D"
|
||
return cat,round(E,1),P,s,g,N
|
||
lv_off={c:official[c]["level"] for c in names}; lv_prod={c:lvl(prod.get(c)) for c in names}; lv_fin={c:final[c]["level"] for c in names}
|
||
for c in ov:
|
||
if lv_prod.get(c)=="unverified": lv_prod[c]=ov[c]["level"]
|
||
S_off=score(lv_off); S_prod=score(lv_prod); S_fin=score(lv_fin)
|
||
|
||
NAVY="0A1128"; VIOLET="4F1DA1"; LIGHT="F4F6FB"; GOOD="F3F0FF"; WARN="FFF6ED"; CRIT="FFF0F0"
|
||
hf=Font(name="Pretendard",bold=True,color="FFFFFF",size=14); bf=Font(name="Pretendard",size=14); bb=Font(name="Pretendard",size=14,bold=True)
|
||
fill=lambda h:PatternFill("solid",fgColor=h); thin=Side(style="thin",color="E2E8F0"); bd=Border(left=thin,right=thin,top=thin,bottom=thin)
|
||
wrap=Alignment(wrap_text=True,vertical="top"); center=Alignment(horizontal="center",vertical="center",wrap_text=True)
|
||
wb=Workbook()
|
||
# ── 1. 요약
|
||
ws=wb.active; ws.title="요약"
|
||
ws["A1"]="뷰성형외과 공식 홈페이지 vs 뷰 서포터즈 사이트 · AEO/GEO 기준표 v1.0 비교"; ws["A1"].font=Font(name="Pretendard",bold=True,size=18,color=NAVY)
|
||
ws["A2"]="실측일 2026-09-04 · 도구 scripts/audit_aeo_geo.py(자동 21항목) + 수동 판정 · 미검증 항목은 0점이 아니라 분모 제외 · 등급 D≤40 / C 41~60 / B 61~80 / A 81+"; ws["A2"].font=Font(name="Pretendard",size=14,color="64748B")
|
||
hdr=["구분","공식 홈페이지 (viewclinic.com)","서포터즈 샘플 (현재·noindex)","서포터즈 색인 허용 빌드","비고"]
|
||
for i,h in enumerate(hdr,1):
|
||
c=ws.cell(row=4,column=i,value=h); c.font=hf; c.fill=fill(NAVY); c.alignment=center; c.border=bd
|
||
rows=[("총점 (100점 환산)",S_off[3],S_prod[3],S_fin[3],"검증 항목 가중 합 ÷ 검증 항목 가중치 합"),
|
||
("등급",S_off[4],S_prod[4],S_fin[4],""),
|
||
("획득 / 가능 가중치",f"{S_off[1]} / {S_off[2]}",f"{S_prod[1]} / {S_prod[2]}",f"{S_fin[1]} / {S_fin[2]}",""),
|
||
("검증 항목 수 (36 중)",S_off[5],S_prod[5],S_fin[5],"D·E 대부분은 오너 계정·수동 실측 필요")]
|
||
r=5
|
||
for row in rows:
|
||
for i,v in enumerate(row,1):
|
||
c=ws.cell(row=r,column=i,value=v); c.font=bb if i==1 else bf; c.border=bd; c.alignment=center if i>1 else wrap
|
||
r+=1
|
||
r+=1
|
||
for i,h in enumerate(["카테고리 (가중치)","공식 %","샘플 noindex %","색인 허용 %","차이 (색인 허용 − 공식)"],1):
|
||
c=ws.cell(row=r,column=i,value=h); c.font=hf; c.fill=fill(VIOLET); c.alignment=center; c.border=bd
|
||
r+=1
|
||
for cat,(nm,w) in CATS.items():
|
||
a=S_off[0][cat]; b=S_prod[0][cat]; f=S_fin[0][cat]
|
||
vals=[f"{cat}. {nm} ({w})",f"{a[2]}% ({a[3]}/{sum(1 for x in names if x[0]==cat)} 검증)",f"{b[2]}% ({b[3]} 검증)",f"{f[2]}% ({f[3]} 검증)",f"{f[2]-a[2]:+d}p"]
|
||
for i,v in enumerate(vals,1):
|
||
c=ws.cell(row=r,column=i,value=v); c.font=bf; c.border=bd; c.alignment=center if i>1 else wrap
|
||
if i==5: c.fill=fill(GOOD if f[2]-a[2]>0 else CRIT if f[2]-a[2]<0 else LIGHT)
|
||
r+=1
|
||
r+=1
|
||
notes=["읽는 법",
|
||
"· 샘플 noindex 열: 지금 배포된 그대로. robots.txt가 전면 차단이라 A1이 0점, sitemap 미선언으로 A5 1점. 의도된 상태입니다(발행 전).",
|
||
"· 색인 허용 빌드 열: PUBLIC_INDEXABLE=true로 빌드한 같은 코드. 발행 시 robots 전면 허용 + AI 봇 명시 + sitemap 선언. A4·B6·A7은 로컬에서 판정할 수 없어 프로덕션 실측값을 씁니다.",
|
||
"· D(표면)·E(측정)는 사이트가 아니라 병원 엔티티·오너 계정에 걸린 항목입니다. 서포터즈 사이트가 직접 바꿀 수 있는 항목은 A·B·C·F이며, E는 서포터즈 도메인의 서버 로그·GA4를 붙이면 채워집니다.",
|
||
"· 공식 홈페이지 수치는 2026-08-31 재채점 기준(50점/C)이며 discovery_viewclinic.ts와 같습니다.",
|
||
"· 점수는 '인용된다'는 약속이 아닙니다. 인용될 수 없는 이유를 제거했는지의 체크리스트입니다(INFINITH 정직성 규약)."]
|
||
for n in notes:
|
||
c=ws.cell(row=r,column=1,value=n); c.font=bb if n=="읽는 법" else bf; c.alignment=wrap; ws.merge_cells(start_row=r,start_column=1,end_row=r,end_column=5); ws.row_dimensions[r].height=44 if n!="읽는 법" else 24; r+=1
|
||
for i,w in enumerate([44,36,36,36,56],1): ws.column_dimensions[get_column_letter(i)].width=w
|
||
# ── 2. 항목별 비교
|
||
ws2=wb.create_sheet("항목별 비교 (36항목)")
|
||
hdr=["ID","카테고리","항목","가중치","공식 홈페이지 레벨","공식 근거","서포터즈 샘플 레벨 (noindex)","샘플 근거","서포터즈 색인 허용 레벨","색인 허용 근거","판정 출처","차이 (색인 허용 − 공식)"]
|
||
for i,h in enumerate(hdr,1):
|
||
c=ws2.cell(row=1,column=i,value=h); c.font=hf; c.fill=fill(NAVY); c.alignment=center; c.border=bd
|
||
r=2
|
||
def L2(v): return "미검증" if v=="unverified" else v
|
||
for cid in names:
|
||
o=official[cid]; p=prod.get(cid); f=final[cid]
|
||
pL=lv_prod[cid]; pE=(p["evidence"] if p else "")
|
||
if cid in ov and (not p or lvl(p)=="unverified"): pE=ov[cid]["evidence"]
|
||
diff="" if (o["level"]=="unverified" or f["level"]=="unverified") else f["level"]-o["level"]
|
||
vals=[cid,f"{cid[0]}. {CATS[cid[0]][0]}",names[cid][0],W[cid],L2(o["level"]),o["evidence"],L2(pL),pE,L2(f["level"]),f["evidence"],f["src"],(f"{diff:+d}" if diff!="" else "")]
|
||
for i,v in enumerate(vals,1):
|
||
c=ws2.cell(row=r,column=i,value=v); c.font=bf; c.border=bd; c.alignment=wrap if i in (3,6,8,10,11) else center
|
||
if i in (5,7,9) and v!="미검증": c.fill=fill(GOOD if v==3 else LIGHT if v==2 else WARN if v==1 else CRIT)
|
||
if i==12 and diff!="": c.fill=fill(GOOD if diff>0 else CRIT if diff<0 else LIGHT)
|
||
r+=1
|
||
for i,w in enumerate([8,24,38,9,11,60,12,60,12,60,24,12],1): ws2.column_dimensions[get_column_letter(i)].width=w
|
||
ws2.freeze_panes="D2"
|
||
# ── 3. 남은 과제
|
||
ws3=wb.create_sheet("서포터즈 남은 과제")
|
||
todo=[("A4","www 변형 미수렴","vercel.app 임시 도메인은 www 변형이 없음. 정식 도메인(별도 도메인 또는 서브도메인) 확정 후 http/www 4변형 301 통일","도메인 결정 후"),
|
||
("B4","주소 표기 2종","팩트 시트 단일 원본으로 통일 완료(2026-09-04 수정). 다음 실측에서 3점 예상","완료"),
|
||
("B5","sameAs 인식 1개","MedicalClinic 노드에 sameAs 5개가 있으나 채점기가 Organization 타입 노드를 먼저 읽음. 스키마 노드 순서·타입 보강","코드 수정"),
|
||
("B7","hreflang 없음","단일 언어 매체라 해당 없음. 영문판을 만들면 연결","해당 없음"),
|
||
("C4","구체 수치","시술 페이지에 절개 길이·수술 시간은 있으나 가격·회복 일수 표 없음. 가격 노출 범위는 경영진 결정 대기","결정 대기"),
|
||
("C5","표 1개","시술 글마다 비교표 1~2개로 확대","콘텐츠"),
|
||
("D1~D5","표면 항목","병원 엔티티 항목. 서포터즈 사이트가 아니라 병원 계정(강남언니·GBP·플레이스)에서 개선","병원"),
|
||
("E1~E4","측정 인프라","서포터즈 도메인에 Vercel 로그 드레인 + GA4 AI 채널그룹 + GSC·Bing 등록. 발행과 동시에 설치","발행 시"),
|
||
("F2","CMS 판정 불가","정적 빌드라 CMS·버전 노출 없음. 채점기 기준으로는 미검증이지만 실질 위험은 없음","해당 없음"),
|
||
("전제","noindex 해제","법무 확인·도메인 결정·서포터즈 실명·원장 감수 완료 후 PUBLIC_INDEXABLE=true로 발행","경영진")]
|
||
for i,h in enumerate(["항목","현재","조치","상태"],1):
|
||
c=ws3.cell(row=1,column=i,value=h); c.font=hf; c.fill=fill(VIOLET); c.alignment=center; c.border=bd
|
||
for r,row in enumerate(todo,2):
|
||
for i,v in enumerate(row,1):
|
||
c=ws3.cell(row=r,column=i,value=v); c.font=bf; c.border=bd; c.alignment=wrap
|
||
for i,w in enumerate([12,28,100,16],1): ws3.column_dimensions[get_column_letter(i)].width=w
|
||
|
||
# ── 4. E 측정 연결 가이드
|
||
ws4=wb.create_sheet("E 측정 인프라 연결 가이드")
|
||
ws4["A1"]="E. AI 가시성 측정 인프라 (가중치 10) · 무엇을 어디서 연결해야 채점되는가"; ws4["A1"].font=Font(name="Pretendard",bold=True,size=18,color=NAVY)
|
||
ws4["A2"]="원칙: 답변엔진 UI를 자동 조회하지 않는다(OpenAI·Perplexity 약관). 측정은 우리가 소유한 신호(서버 로그·GA4·GSC)와 정식 API 반복 측정으로만 한다. 현재 공식 홈페이지·서포터즈 모두 E 4항목이 미검증(분모 제외) 상태다."; ws4["A2"].font=Font(name="Pretendard",size=14,color="64748B"); ws4["A2"].alignment=wrap; ws4.merge_cells("A2:I2"); ws4.row_dimensions[2].height=48
|
||
hdr=["항목 (가중치)","레벨 정의 0 → 3","필요한 데이터","공식 홈페이지 (viewclinic.com, nginx 직접 호스팅)","서포터즈 사이트 (Vercel 정적)","연결 방법 (단계)","접근 권한 · 담당","주기 · 산출물","현재 → 목표"]
|
||
for i,h in enumerate(hdr,1):
|
||
c=ws4.cell(row=4,column=i,value=h); c.font=hf; c.fill=fill(NAVY); c.alignment=center; c.border=bd
|
||
E_ROWS=[
|
||
("E1 AI 크롤러 로그 · IP 검증 (3)",
|
||
"0 로그 접근 불가 → 1 로그 있음·미분석 → 2 UA 기준 집계 → 3 IP 검증 히트 집계",
|
||
"웹서버 접근 로그(요청 시각·URL·User-Agent·원격 IP·상태코드). AI 봇 UA: GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot, ClaudeBot, Claude-SearchBot, Google-Extended, Applebot-Extended, Bingbot, Amazonbot, CCBot, Bytespider",
|
||
"nginx access.log. 병원 호스팅 업체(또는 내부 IT)에서 로그 파일 또는 로그 전송(syslog/S3) 설정 필요. Cloudflare 미사용이라 AI Crawl Control 대시보드 없음",
|
||
"Vercel 프로젝트 → Settings → Log Drains(Pro 플랜 이상) 또는 Observability → Logs. 드레인 대상: Supabase Edge Function(수신 엔드포인트) → crawler_hits 테이블. 무료 플랜이면 Vercel 대시보드 로그 수동 내보내기로 시작",
|
||
"1) 로그 수집 경로 확보 2) UA로 1차 필터 3) 원격 IP를 공개 IP 대역과 대조해 위조 제거: OpenAI gptbot.json·searchbot.json, Google googlebot.json·special-crawlers.json, Perplexity perplexitybot.json, Bing bingbot.json, Anthropic 공개 IP 문서, 나머지는 역방향 DNS 4) 봇별·URL별 일별 히트 집계 5) 대시보드(/discovery 리포트 E 카드)에 표시. 참조 구현: ADO2 009. AEO-GEO/06_skill-snapshot/scripts/bot_ip_verify.py",
|
||
"공식: 병원 호스팅 관리자 계정 (병원 IT 또는 대행사). 서포터즈: INFINITH Vercel 팀(o2odev) + Supabase 서비스 키",
|
||
"일별 자동 집계, 주간 리포트. 산출물: crawler_hits 테이블, 봇별 방문 추이 차트, 첫 방문일(TTFC)",
|
||
"공식 미검증 → 2(UA 집계)까지 로그만 받으면 즉시, 3은 IP 검증 스크립트 연결 후. 서포터즈 미검증 → 발행과 동시에 3"),
|
||
("E2 GA4 AI 유입 채널 (3)",
|
||
"0 GA4 없음 → 1 GA4 있음·AI 채널 없음 → 2 기본 AI Assistant 채널 → 3 커스텀 채널(Perplexity 포함)",
|
||
"GA4 속성의 세션 소스/매체·리퍼러. 리퍼러 도메인: chatgpt.com, chat.openai.com, perplexity.ai, gemini.google.com, bard.google.com, copilot.microsoft.com, bing.com/chat, claude.ai, you.com, phind.com, 그리고 UTM(utm_source=chatgpt 등)",
|
||
"홈페이지에 GA4 태그가 있는지부터 확인(2026-08 실측 시 오너 접근 불가로 미검증). 있으면 관리 → 데이터 표시 → 채널 그룹에서 커스텀 채널 추가. 없으면 GTM으로 GA4 설치 후 동일",
|
||
"Astro 레이아웃에 GA4(gtag.js) 삽입 + 같은 커스텀 채널 그룹. 정적 사이트라 서버사이드 태깅 불필요. 서포터즈 GA4 속성은 병원 GA4 계정 안에 별도 속성으로 두어 병원이 소유",
|
||
"1) GA4 속성 생성 또는 접근 확보 2) 관리 → 채널 그룹 → 새 채널 그룹 생성 3) 채널 'AI Assistant' 규칙: 세션 소스가 정규식 (chatgpt|openai|perplexity|gemini|bard|copilot|bing\\.com/chat|claude|you\\.com|phind) 와 일치 4) 채널 우선순위를 Referral 위로 5) 탐색 보고서에 AI 채널 세션·랜딩 페이지·전환 추가 6) 서포터즈→병원 상담 예약 링크에 utm_source=viewsupporters 부착해 귀속",
|
||
"공식: 병원 GA4 관리자(편집자 권한). 서포터즈: INFINITH가 속성 생성 후 병원 계정에 관리자 권한 부여",
|
||
"실시간 수집, 월간 리포트. 산출물: AI 채널 세션 수·전환 수(하한값), 랜딩 페이지 순위. 주의: AI 유입의 35~70%가 Direct로 유실되므로 하한값으로만 보고",
|
||
"공식 미검증 → 3 (GA4 접근 확보 후 30분 작업). 서포터즈 미검증 → 발행 시 3"),
|
||
("E3 GSC AI Overviews 노출 (2)",
|
||
"0 GSC 미등록 → 1 등록만 → 2 데이터 확인 → 3 월간 추적",
|
||
"Google Search Console 실적 보고서의 검색 유형/검색 결과 모양 필터(AI 개요·AI 모드 노출·클릭). Bing Webmaster Tools(A6)와 함께 등록",
|
||
"search.google.com/search-console에 viewclinic.com 도메인 속성 등록. DNS TXT 인증은 도메인 관리자(병원)가 필요. 이미 등록돼 있을 수 있으니 오너 확인 먼저",
|
||
"서포터즈 도메인 확정 후 도메인 속성 등록(vercel.app 임시 도메인은 등록 불가 아님이나 발행 도메인으로 다시 등록해야 하므로 도메인 확정 후 진행). Vercel DNS면 TXT 추가 1분",
|
||
"1) GSC 도메인 속성 추가 → DNS TXT 인증 2) sitemap.xml 제출 3) 실적 보고서에서 '검색 결과 모양' 또는 '검색 유형' 필터로 AI 개요 노출 확인 4) 월 1회 스냅샷 저장(노출·클릭·평균 게재순위) 5) Bing 웹마스터 도구에 GSC 가져오기로 동시 등록(A6 해소) 6) IndexNow 키 설정",
|
||
"공식: 도메인 DNS 관리자(병원 또는 가비아 등 등록 대행). 서포터즈: INFINITH(Vercel DNS)",
|
||
"월 1회 수동 스냅샷. 산출물: AI 개요 노출 페이지 목록, 월별 추이표",
|
||
"공식 미검증 → 2는 등록 직후, 3은 3개월 누적 후. 서포터즈 미검증 → 도메인 확정 후 등록"),
|
||
("E4 기준선 스팟체크 (2)",
|
||
"0 없음 → 1 단발 조회만 → 2 반복 측정 1회 → 3 분기 반복 측정",
|
||
"질문 세트(QB 120문항 또는 핵심 10문항) × 엔진(OpenAI Responses API+web_search 서울 고정, Perplexity sonar) × 반복 5~7회 → 언급 여부·인용 URL·순위. 결과는 언급률 + 신뢰구간으로만 보고",
|
||
"이미 1회 실시(2026-09-01, 120문항 × 2엔진). 현재 레벨 2에 해당하나 리포트 원본이 INFINITH 측에 있어 병원 기준 미검증 처리. 분기 반복으로 3",
|
||
"동일 문항으로 발행 4주·8주 후 재측정. 서포터즈 도메인이 인용 URL에 등장하는지가 핵심 지표",
|
||
"1) scripts/run_question_bank_openai.py 로 OpenAI·Perplexity 반복 실행(JSONL 재개 가능) 2) scripts/merge_qb_results.py 로 통합표 3) scripts/export_qb_excel.py 로 엑셀 4) 브랜드/논브랜드 언급률·95% 신뢰구간 산출 5) 인용 도메인 빈도표에 viewsupporters 도메인 추가 6) 분기마다 반복해 추이 저장",
|
||
"INFINITH (OPENAI_API_KEY·PERPLEXITY_API_KEY 회사 계정). 병원 접근 불필요",
|
||
"분기 1회(발행 초기에는 4주·8주). 산출물: Viewclinic_QB_Results_*.xlsx, 언급률 추이, 인용 URL 목록",
|
||
"공식 미검증 → 2 즉시 인정 가능(리포트 첨부), 3은 다음 분기 재측정 후. 서포터즈 동일"),
|
||
]
|
||
for r,row in enumerate(E_ROWS,5):
|
||
for i,v in enumerate(row,1):
|
||
c=ws4.cell(row=r,column=i,value=v); c.font=bb if i==1 else bf; c.border=bd; c.alignment=wrap
|
||
ws4.row_dimensions[r].height=330
|
||
for i,w in enumerate([26,34,48,50,50,70,36,40,36],1): ws4.column_dimensions[get_column_letter(i)].width=w
|
||
ws4.freeze_panes="B5"
|
||
r=len(E_ROWS)+6
|
||
extra=[("공통 전제","서버 로그·GA4·GSC는 '오너가 소유한 신호'다. 병원이 계정 권한을 주지 않으면 공식 홈페이지 E는 계속 미검증이다. 서포터즈 사이트는 INFINITH가 인프라를 소유하므로 발행과 동시에 E 4항목을 모두 채울 수 있고, 이것이 서포터즈 사이트가 측정 장치 역할을 한다는 뜻이다."),
|
||
("IP 대역 출처","OpenAI: openai.com/gptbot.json, openai.com/searchbot.json, openai.com/chatgpt-user.json · Google: developers.google.com/search/apis/ipranges/googlebot.json, special-crawlers.json, user-triggered-fetchers.json · Perplexity: perplexity.com/perplexitybot.json · Bing: bing.com/toolbox/bingbot.json · Anthropic: 공식 문서의 공개 IP 목록 · 나머지 봇: 역방향 DNS + 정방향 재확인"),
|
||
("데이터 보관","crawler_hits(봇·IP·URL·시각·검증여부), ga4_ai_sessions(일·채널·랜딩·전환), gsc_ai_snapshot(월·페이지·노출·클릭), qb_runs(분기·문항·엔진·언급·인용URL). 모두 Supabase에 두고 /discovery/:id 리포트 E 카드가 읽는다."),
|
||
("보고 규칙","단발 조회 금지. 언급률은 신뢰구간과 함께. 크롤러 히트는 IP 검증분만. GA4 AI 유입은 하한값 표기. 스키마·콘텐츠 변경과 지표 변화의 인과를 단정하지 않는다.")]
|
||
for k,v in extra:
|
||
c=ws4.cell(row=r,column=1,value=k); c.font=bb; c.border=bd; c.alignment=wrap
|
||
c=ws4.cell(row=r,column=2,value=v); c.font=bf; c.border=bd; c.alignment=wrap; ws4.merge_cells(start_row=r,start_column=2,end_row=r,end_column=9); ws4.row_dimensions[r].height=64; r+=1
|
||
|
||
wb.save(OUT); print("saved",OUT, "official",S_off[3],S_off[4],"prod",S_prod[3],S_prod[4],"idx",S_fin[3],S_fin[4])
|