haewon 지적: 목록 화면에 같은 사진이 나란히 선다. 원인은 PostCard 의 대체 순서다. thumbnail·hero·영상이 모두 없으면 건물 사진 하나로 수렴한다. 뷰는 5편 전부, 오라클은 8편 전부가 그랬고, 원진은 6편 중 3편이 같은 소개 커버를 쓰고 있었다. heroes 단계를 워커에 넣었다(generate 다음, build 앞). scripts/assign_post_heroes.mjs 가 images.json 에서 글마다 다른 사진을 고른다. - 글 분류별로 어울리는 유형을 먼저 쓴다. 병원 소개·방문은 시설, 시술 정보는 시술 설명 일러스트, 안전은 장비 순이다. 방문 안내에 시술 일러스트가 붙는 것을 막는다. - 의료진 사진은 쓰지 않는다. 저자·검토자 표시가 지정 용도이고, 글 대표로 쓰면 그 원장이 쓴 글로 읽힌다. - 홈 히어로·건물 사진과 겹치는 글도 다시 배정한다. - 수집 원문 alt 를 그대로 쓰지 않는다. 원문은 술기 설명 문단이라 고객 화면에 낼 수 없어 첫 구절만 줄여 쓴다. 캡션의 출처도 페이지 제목이 슬로건일 때 상호로 바꾼다. - 이미지가 글보다 적으면 남는 글은 비워 둔다. 다시 쓰면 처음 문제로 돌아간다. 작업 중 더 큰 문제를 찾았다. 뷰의 수집 이미지 중 다섯 장이 의료광고 금지 유형인데 전부 시설(clinic)로 분류돼 홈과 /clinic 에 나가고 있었다. - img02: 밑선 절개 흉터의 1개월·6개월 경과를 나란히 놓은 전후 사진 - stemcell_about_01: 팔에 주사·드레싱을 하는 시술 행위 사진 - view-abdominoplasty 2장, view-motiva-preserve-premium-standard 2장: "이미지 광고 모델" 표기가 있는 모델 신체 사진 - stemcell_count_01_mo: 장비 제조사의 성능 주장 그래픽 파일명과 alt 에 아무 신호가 없어 규칙 문서가 예고한 파일명·alt 검사로는 잡히지 않는다. briefs/viewclinic/clinic.json 의 excludeImages 로 뺐고, 홈 히어로가 가슴 라인 일러스트였던 것과 건물 사진 alt 가 "가슴확대 시설 사진"이던 것도 함께 고정했다. 육안 검토용 scripts/build_image_review.mjs 를 만들었다. 세 병원 수집 이미지를 한 화면에 모아 분류·쓰이는 곳·의심 표시를 보여준다. 91장 중 파일명·alt 로 걸러진 것은 0장이었다. 이 유형은 사람이 봐야 갈린다는 뜻이다. 결과: 홈·목록의 연속 중복이 뷰 1, 원진 1 로 내려갔다(이전 뷰 6, 원진 3). 오라클은 수집 이미지가 0장이라 배정할 것이 없다. 근거의 이미지 alt 가 EUC-KR 에서 깨져(????) 분류기가 하나도 고르지 못하는 것이 원인으로 좁혀졌으나, 병원 사이트가 응답하지 않아 재수집·검증을 하지 못했다. 검증: 게이트 41/41, check.mjs 오류 0건, tsc 0 에러, 내보내기 종료코드 0. 모두 미배포. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| launchd | ||
| poll.mjs | ||
| README.md | ||
| run.mjs | ||
서포터즈 자동 빌드 워커
병원 홈페이지 URL 하나로 근거 수집 → 팩트 시트·의료진 초안 → 첫 배치 기획 → 글 생성(GPT-4.1, 게이트·근거 대조) → Astro 빌드·발행 게이트 → Vercel 미리보기(noindex) 까지 돈다. 사람 게이트(의학 검토·게시 승인)는 자동화하지 않는다.
node workers/supporters-build/run.mjs --clinic <id> --url <https://...> [--name 상호] [--logo file.svg] [--youtube 채널] [--deploy] [--build-id <uuid>] [--max-cost-usd 10] [--no-tourism] [--no-recovery]
# 단계: evidence → ocr(이미지 글자) → data → discover(채널 발견·엔티티 검증) → youtube(로고·파비콘·채널 집계) → news(뉴스룸: 네이버 뉴스 + 언론보도 게시판) → images(의료법 기준 이미지) → tourism(주변 안내: /stay·/en/stay 데이터) → recovery(시술 후 관리 안내: 병원 시술 페이지 원문 인용·번역) → inputs → briefs → generate → build → deploy
# 주변 안내만 다시: node workers/supporters-build/run.mjs --clinic <id> --url <https://...> --from tourism --skip-generate
# 시술 후 관리 안내만 다시: node workers/supporters-build/run.mjs --clinic <id> --url <https://...> --from recovery --skip-generate
node workers/supporters-build/poll.mjs [--loop 300] [--no-deploy] # supporter_builds 큐(status=queued)를 집어 run.mjs 실행
흐름
- 랜딩
/discovery의 URL 신청(DiscoveryLeadModal) →discovery_leads+supporter_builds(queued) 행. poll.mjs가 queued 행을 집어(running 으로 조건부 갱신)run.mjs를 돌린다. 단계마다supporter_builds갱신.- 완료되면
preview_url이 기록되고,/discovery/:id또는/supporters/:id의 "병원 확인 항목" 패널에 미리보기 링크와 입력 폼이 뜬다. - 병원이 입력하면
supporter_inputs에 쌓인다. 재빌드:run.mjs --from data --from-supabase --deploy --build-id <id>(apply_inputs 가 데이터·frontmatter 에 반영).
실행 위치 (권장)
| 단계 | 어디서 | haewon 이 할 일 |
|---|---|---|
| 지금 (파일럿, 월 수 건) | haewon 의 Mac 에서 poll.mjs 를 launchd 로 5분마다. launchd/ 폴더의 plist 를 설치하면 된다 |
설치 명령 한 번. Mac 이 켜져 있어야 돈다 |
| 정착 (월 수십 건, 무인) | Trigger.dev 클라우드. poll.mjs 와 같은 코드를 태스크로 올린다. 타임아웃 없음, 재시도·로그 내장, 무료 구간(월 1,500 run)이면 충분 |
가입(GitHub 계정), 프로젝트 만들고 API 키 1개 전달. 코드·배포는 Claude 가 한다 |
VPS(서버 직접 운영)와 Vercel Functions(최대 800초)는 권하지 않는다. 전자는 관리 부담, 후자는 빌드가 시간 안에 끝나지 않는다.
필요한 것
.env:VITE_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,OPENAI_API_KEY,YOUTUBE_API_KEY,NAVER_CLIENT_ID/NAVER_CLIENT_SECRET(뉴스룸, 주변 안내의 주소→좌표),TOUR_API_KEY/TOUR_API_BASE(주변 안내, 한국관광공사 TourAPI. 없으면 tourism 단계를 건너뛰고 페이지는 확인 대기로 발행)python3(tourism 단계가scripts/fetch_medical_tourism.py를 부른다.certifi가 있으면 SSL 인증서 검증에 쓴다)vercelCLI 로그인 (배포 시). 병원당 프로젝트supporters-<id>를 만든다templates/supporters-astro/(supporters 에서npm run export:template)
주변 안내 (tourism 단계)
/stay(국문)·/en/stay(영문)의 식사·숙박·휴식·관광·문화·쇼핑·축제 목록은 병원 좌표를 기준으로 한국관광공사 TourAPI 에서 모은다. 기준 좌표는 팩트 시트 주소(address.full)와 상호를 네이버 지역검색에 넣어 결과의 도로명·번지가 같을 때만 받는다. 주소만으로는 지역검색 결과가 없으므로 상호가 필요하다.
- 좌표를 못 정하면 스크립트는 종료 코드 1 로 멈추고, 워커는
status.json의 해당 단계를warning으로 적고 빌드를 계속한다. 페이지는 템플릿의 빈 데이터로 "확인 대기"를 그린다. meta.origin.coordSource에 좌표 출처와 날짜가 남는다(예:네이버 지역검색 '뷰성형외과의원' (...) 2026-09-10).- 결과 요약은
status.json의report.tourism에 있다(기준 좌표, 언어별 장소·축제 수). - 네이버 이미지 검색 폴백은 제3자 저작물이다. 공개 배포 빌드는
--no-naver-images를 붙인다. - 수동 실행:
python3 scripts/fetch_medical_tourism.py --clinic <id> --name "<상호>" --address "<주소>" --label "<상호> (<도로명>)" --out-en <site>/src/data/medicalTourism.json --out-ko <site>/src/data/medicalTourismKo.json
시술 후 관리 안내 (recovery 단계)
/stay·/en/stay 의 "하루 동선" 아래 "시술 후 관리 안내" 블록. scripts/collect_recovery.mjs 가 근거 페이지(type procedure·aftercare·precautions, OCR 텍스트 포함)의 문장 중 시점 표현과 행동·주의 표현이 함께 있는 것만 고른다. 효과·홍보 문장, 병원 연락처 문장, 수술 전 준비 문장, 표 칸이 뒤섞인 OCR 문장, 발행 게이트 규칙(금칙어·효과 보장·금액·운영자 어휘)에 걸리는 문장은 뺀다. 페이지당 최대 5문장, 최대 30페이지.
- 영문은
OPENAI_API_KEY로 gpt-4.1-mini 배치 번역(temperature 0, 상한 $0.5). 번역문의 숫자가 원문과 다르면 그 문장은 영문을 비우고(enFailed) 영문 화면에 한글 원문 + "Korean" 표시로 나간다. 같은 문장은<site>/src/data/recoveryNotes.cache.json에서 다시 쓴다. - 결과
<site>/src/data/recoveryNotes.json. items 가 비면 페이지는 블록을 그리지 않는다. 요약은status.json의report.recovery. - 병원 확정 안내문(
site.recoveryStagesKo/En)과 역할이 다르다. 확정값은 "병원 안내" 문단, 이 블록은 출처 링크·확인일이 붙은 원문 인용이다. 확정값이 없으면 "확인 대기" 문단은 그대로 남는다. - 발행 게이트의 홈페이지 40자 일치 검사(§3-11)는 이 인용 블록(
#recovery-quote)만 빼고 본다. 금칙어·운영자 어휘 검사는 인용 블록에도 적용된다.
단가
글 생성 상한은 실행당 $10 (--max-cost-usd, haewon 결정 2026-09-07). 실측: 편당 $0.05~0.19, 첫 배치 5~6편이면 $0.3~1.
두 번째 병원에서 배운 것 (원진성형외과, 2026-09-07)
- 비브라우저 User-Agent 를 403 으로 막는다 → 브라우저 UA + 식별 문자열.
- 경로 끝 슬래시를 붙이면 404 (뷰성형외과는 반대로 슬래시로 301) → 슬래시를 강제하지 않고 404 면 반대 형태 재시도.
- 페이지 유형 분류가 URL 경로 어휘(system/precautions 등)에 기대면 다른 병원에서 못 잡는다 → 제목·본문 키워드 분류를 보강할 것.