From 7e515b70ae2349988a8645dff9d0ec12ca452d43 Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Mon, 7 Sep 2026 17:07:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(supporters):=20=EC=8A=B9=EC=9D=B8=20UI?= =?UTF-8?q?=C2=B7=EC=9E=85=EB=A0=A5=20=EB=B0=98=EC=98=81=C2=B7=EC=9B=8C?= =?UTF-8?q?=EC=BB=A4=20=E2=80=94=20=C2=A77-5=20ClinicInputsPanel=20+=20sup?= =?UTF-8?q?porter=5Fbuilds/inputs=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20+=20apply=5Finputs,=20=C2=A712-2=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20=EA=B3=A0=EC=A7=80=C2=B7=EA=B0=9C=EC=84=A4?= =?UTF-8?q?=EC=9E=90=20=EC=8A=B9=EC=9D=B8=C2=B7=EC=83=89=EC=9D=B8=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=ED=95=84=ED=84=B0,=20=C2=A77-6=20?= =?UTF-8?q?=EC=9B=8C=EC=BB=A4=20E2E(=EB=B7=B0=EC=84=B1=ED=98=95=EC=99=B8?= =?UTF-8?q?=EA=B3=BC=20URL=20=E2=86=92=205=ED=8E=B8=20=E2=86=92=20?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=ED=8A=B8=200=EC=98=A4=EB=A5=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게이트: DISCLOSURE_TOP_MISSING, APPROVED_*, UNAPPROVED_IN_INDEXABLE_BUILD, PRESS_VOICE 규칙화, 픽스처 추가 (16/16) - 생성기: --site 옵션, 병원 확인 답변(clinicAnswers) 근거 [A] - default_briefs.mjs 첫 배치 자동 기획, workers/supporters-build/run.mjs - 랜딩: SAMPLE REPORT 라벨 "실제 사례"로 (haewon 지시) - v2 §11-6·§11-7 기록, E2E 리포트 Co-Authored-By: Claude Fable 5.1 --- docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md | 21 +- ...clinic_Supporters_Worker_E2E_2026-09-07.md | 60 +++ .../default_briefs_viewclinic.json | 171 +++++++++ .../discovery/ClinicInputsPanel.tsx | 205 ++++++++++ src/components/discovery/SampleReport.tsx | 2 +- src/pages/DiscoveryReportPage.tsx | 4 + .../migrations/20260907_supporter_builds.sql | 42 ++ .../briefs/viewclinic/inputs.sample.json | 10 + supporters/scripts/apply_inputs.mjs | 141 +++++++ supporters/scripts/check.mjs | 10 + supporters/scripts/default_briefs.mjs | 78 ++++ .../gate/fixtures/approved-before-review.md | 12 + supporters/scripts/gate/rules.mjs | 24 +- supporters/scripts/gen/evidence.mjs | 9 +- supporters/scripts/gen/serialize.mjs | 5 - supporters/scripts/generate_posts.mjs | 9 +- .../scripts/template/data/clinicAnswers.json | 4 + supporters/scripts/template/data/site.json | 3 +- supporters/src/components/DisclosureTop.astro | 6 + supporters/src/content.config.ts | 2 + supporters/src/data/clinicAnswers.json | 4 + supporters/src/lib.ts | 5 + supporters/src/pages/authors/[id].astro | 4 +- supporters/src/pages/index.astro | 4 +- supporters/src/pages/posts/[id].astro | 7 +- supporters/src/pages/posts/index.astro | 4 +- supporters/src/pages/sitemap.xml.ts | 3 +- templates/supporters-astro/.gitignore | 3 + templates/supporters-astro/briefs/README.md | 54 +++ .../viewclinic/eye-double-eyelid-methods.json | 33 ++ .../briefs/viewclinic/inputs.sample.json | 10 + templates/supporters-astro/package-lock.json | 54 ++- templates/supporters-astro/package.json | 7 +- .../supporters-astro/scripts/apply_inputs.mjs | 141 +++++++ templates/supporters-astro/scripts/check.mjs | 14 +- .../scripts/collect_evidence.mjs | 296 +++++++++++++++ .../scripts/default_briefs.mjs | 78 ++++ .../gate/fixtures/approved-before-review.md | 12 + .../gate/fixtures/comparison-claim.html | 8 + .../supporters-astro/scripts/gate/rules.mjs | 42 +- .../supporters-astro/scripts/gate/test.mjs | 26 ++ .../supporters-astro/scripts/gen/evidence.mjs | 149 ++++++++ .../supporters-astro/scripts/gen/prompt.mjs | 151 ++++++++ .../scripts/gen/serialize.mjs | 64 ++++ .../scripts/generate_posts.mjs | 359 ++++++++++++++++++ .../src/components/DisclosureTop.astro | 6 + .../supporters-astro/src/content.config.ts | 2 + .../src/data/clinicAnswers.json | 4 + templates/supporters-astro/src/data/site.json | 3 +- templates/supporters-astro/src/lib.ts | 5 + .../src/pages/authors/[id].astro | 4 +- .../supporters-astro/src/pages/index.astro | 4 +- .../src/pages/posts/[id].astro | 7 +- .../src/pages/posts/index.astro | 4 +- .../supporters-astro/src/pages/sitemap.xml.ts | 3 +- workers/supporters-build/run.mjs | 179 +++++++++ 56 files changed, 2530 insertions(+), 41 deletions(-) create mode 100644 docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_Worker_E2E_2026-09-07.md create mode 100644 docs/reports/viewclinic/04_supporters/generated_samples/default_briefs_viewclinic.json create mode 100644 src/components/discovery/ClinicInputsPanel.tsx create mode 100644 supabase/migrations/20260907_supporter_builds.sql create mode 100644 supporters/briefs/viewclinic/inputs.sample.json create mode 100644 supporters/scripts/apply_inputs.mjs create mode 100644 supporters/scripts/default_briefs.mjs create mode 100644 supporters/scripts/gate/fixtures/approved-before-review.md create mode 100644 supporters/scripts/template/data/clinicAnswers.json create mode 100644 supporters/src/components/DisclosureTop.astro create mode 100644 supporters/src/data/clinicAnswers.json create mode 100644 templates/supporters-astro/briefs/README.md create mode 100644 templates/supporters-astro/briefs/viewclinic/eye-double-eyelid-methods.json create mode 100644 templates/supporters-astro/briefs/viewclinic/inputs.sample.json create mode 100644 templates/supporters-astro/scripts/apply_inputs.mjs create mode 100644 templates/supporters-astro/scripts/collect_evidence.mjs create mode 100644 templates/supporters-astro/scripts/default_briefs.mjs create mode 100644 templates/supporters-astro/scripts/gate/fixtures/approved-before-review.md create mode 100644 templates/supporters-astro/scripts/gate/fixtures/comparison-claim.html create mode 100644 templates/supporters-astro/scripts/gen/evidence.mjs create mode 100644 templates/supporters-astro/scripts/gen/prompt.mjs create mode 100644 templates/supporters-astro/scripts/gen/serialize.mjs create mode 100644 templates/supporters-astro/scripts/generate_posts.mjs create mode 100644 templates/supporters-astro/src/components/DisclosureTop.astro create mode 100644 templates/supporters-astro/src/data/clinicAnswers.json create mode 100644 workers/supporters-build/run.mjs diff --git a/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md b/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md index 74ff956..28a5920 100644 --- a/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md +++ b/docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md @@ -237,8 +237,8 @@ INFINITH 저장소(브랜치 feature/discovery-url-cta)에서 "AI Discovery 자 |---|---|---| | 3 근거 수집기 (`evidence//`) | **완료** (커밋 c92ecc8, §11-4) | 없음 | | 4 글 생성기 | **완료** (§11-5) | 편당 약 $0.09 실측. 단가 상한은 --max-cost-usd 로 걸 수 있고 값은 미정 | -| 5 승인 UI (§6 입력 폼) | 미착수 | 폼은 INFINITH 랜딩으로 결정(§12-1). 법적 주체는 §12-2 제안 확인 | -| 6 워커·배포·랜딩 연결 | 미착수 | §12-3 추천안 확인, `discovery_leads` 마이그레이션 미적용 | +| 5 승인 UI (§6 입력 폼) | **완료** (§11-6) | 마이그레이션 미적용(supabase db push). 상단 고지·개설자 승인은 §12-2 제안 확인 후 켠다 | +| 6 워커·배포·랜딩 연결 | **워커·랜딩 완료, 배포 미실행** (§11-7) | 워커 실행 위치(§12-3 Trigger.dev)와 첫 Vercel 프로젝트 생성은 haewon 확인 후 | | 7 두 번째 병원 회귀 | 미착수 | §9-4 병원 선정 | | 「채널 인기 전체」 탭 | 경고만 | §9-1. 제거로 결정되면 `check.mjs`의 `level: R.W`를 지우고 videos.astro 탭을 뺀다 | @@ -263,6 +263,21 @@ INFINITH 저장소(브랜치 feature/discovery-url-cta)에서 "AI Discovery 자 - 한계: 본문이 이미지 글자인 페이지(코 메인·가슴재수술·진료분야)는 근거 0자로 들어가 pending 에 쌓인다. 자막 없는 롱폼은 인용하지 않으므로 그 글은 정답지보다 얇다(보형물 표면 글 650~900자). 자막 파이프라인(v1 §6)이 붙으면 `evidence.transcripts` 로 넣는다. - 미결: 단가 상한값(§9-5). 실측 편당 $0.05~0.19, 첫 배치 10편이면 약 $1. +### 11-6. 승인 UI · 입력 반영 (§7-5, §12-2 코드 반영) · 2026-09-07 세션 3 +- 테이블: `supabase/migrations/20260907_supporter_builds.sql` — `supporter_builds`(빌드 실행 기록·단계·글 목록·미리보기 URL, 워커가 service_role 로 갱신) · `supporter_inputs`(§6 9항목 + 글별 post_review/post_approval, 익명 insert/select). **미적용** (discovery_leads 와 함께 `supabase db push`). +- 폼: `src/components/discovery/ClinicInputsPanel.tsx` → `/discovery/:id` 5절과 6절 사이. 빌드 상태·미리보기 링크, 9항목 입력(입력자 기록), 의료진 ID 표, 글별 검토(검토자 ID·검토일)와 게시 승인(개설자 이름·직함·승인일). 검토 전 승인 버튼은 비활성. tsc 통과. +- 반영: `supporters/scripts/apply_inputs.mjs --inputs file | --from-supabase`. 같은 (key, post_id) 최신 행만. author/editor → authors·corrections, sponsorship → site.sponsorNotice, specialty_doctors → physicians.specialty, domain → site, 자유 답변 4종 → `src/data/clinicAnswers.json`(생성기 근거 [A], `--site` 옵션으로 워커 작업 폴더에서도 동작), post_review → reviewer·reviewStatus=reviewed·reviewedAt·history, post_approval → approvedBy·approvedAt(검토 전이면 보류). 샘플 `supporters/briefs/viewclinic/inputs.sample.json` dry-run 9건 반영 확인. +- §12-2 코드 반영: (1) `DisclosureTop.astro` + 게이트 `DISCLOSURE_TOP_MISSING`(site.sponsorNotice 비면 warn, 채우면 error). 지금은 비어 있어 룩앤필 변화 없음. (2) frontmatter `approvedBy/approvedAt`, `lib.isPublishable`: PUBLIC_INDEXABLE=true 빌드는 승인 글만 낸다(글·목록·사이트맵·작성자 페이지). 검증: 색인 허용 빌드 → 승인 0편이라 글 0개, 게이트 오류 0. 게이트 `APPROVED_WITHOUT_BY/DATE`·`APPROVED_BEFORE_REVIEW`·`UNAPPROVED_IN_INDEXABLE_BUILD`. (3) `PRESS_VOICE` 경고를 rules.mjs 로. 규제 주장 검사에서 「게시 승인」은 제외. +- 랜딩 카피: SampleReport 의 "SAMPLE REPORT · 뷰성형외과 실측" → "SAMPLE REPORT · 실제 사례"(haewon 지시). 두 워크트리(이 브랜치, feature/discovery-url-cta)에 적용, **프로덕션 미배포**. + +### 11-7. 워커 (§7-6) · 2026-09-07 세션 3 +- `workers/supporters-build/run.mjs --clinic --url <홈> [--inputs|--from-supabase] [--brief] [--max-posts] [--max-cost-usd] [--deploy] [--build-id]`. 실행 환경 독립(로컬·GitHub Actions·Trigger.dev 태스크 어디서든 같은 명령). 단계 evidence → data → inputs → briefs → generate → build → deploy → done, 단계마다 `/status.json` + (build-id 있으면) supporter_builds PATCH. +- data 단계: 템플릿 복제 + `__CLINIC_ID__` 치환 + facts.draft 첫 후보로 팩트 시트(상호·전화·주소·사업자번호·대표, checkedAt/nextCheck) + doctors.json 으로 physicians(이름·직함·URL, 약력은 빈 배열) + 작성자 "확인 대기". +- briefs 단계: `supporters/scripts/default_briefs.mjs` 가 근거 유형(about·direction·safety·precautions·aftercare·checkup)이 실제로 있는 문항만 골라 첫 배치 최대 6편(A 병원 소개, B 방문, E 안전, D 주의사항, C 상담 질문, D 애프터케어). 비교 수치·가격·전후·경험담은 avoid 에 고정. +- **뷰성형외과 E2E 실측**: URL 하나 → 223페이지 수집 → 의료진 27명 → brief 5편(병원 소개는 about 페이지 본문 0자라 제외) → 5/5편 게이트 통과(수정 호출 0, $0.29, 편당 15초) → npm ci·astro build·check.mjs 오류 0(경고 2) → dist 생성. 사람 개입 0. 배포는 `--deploy` 를 주지 않아 건너뜀. 리포트 `docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_Worker_E2E_2026-09-07.md`, brief `generated_samples/default_briefs_viewclinic.json`. +- deploy 단계(미실행): `vercel link --yes --project supporters-` + `vercel deploy --prod --yes`. 첫 실행은 Vercel 프로젝트를 새로 만드는 외부 행위라 haewon 확인 후. 트리거(discovery_leads insert → Database Webhook → 워커)와 Trigger.dev 래핑은 §12-3 확인 후. +- 남은 것: discover-channels(유튜브·인스타 핸들)·유튜브 집계·뉴스룸(build_news.py)·감사 재실행(Phase 9)은 워커에 아직 안 붙었다. 이 세 개는 뷰성형외과 샘플에서 사람이 돌린 스크립트가 있으므로 다음 세션에 단계로 추가한다. + --- ## 12. haewon 결정 반영과 제안 (2026-09-07 세션 2) @@ -275,7 +290,7 @@ INFINITH 저장소(브랜치 feature/discovery-url-cta)에서 "AI Discovery 자 | 9-5 생성 모델 | **GPT-4 계열** (`OPENAI_API_KEY` 있음). 단가 상한은 첫 배치 실측 후 정함 | | 9-3 법적 주체 | 리서치 후 제안 → §12-2 | | 9-6 워커·배포 | 추천 → §12-3 | -| 9-1 탭 제거, 9-4 두 번째 병원 | 미결 | +| 9-1 탭 제거, 9-4 두 번째 병원, 9-5 단가 상한값, 첫 Vercel 프로젝트 생성 | 미결 | ### 12-2. 검토 승인의 법적 책임 주체 · 제안 근거 조문을 원문으로 확인한 결과다(법률 자문이 아니며, 계약 전 변호사 확인을 권한다). diff --git a/docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_Worker_E2E_2026-09-07.md b/docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_Worker_E2E_2026-09-07.md new file mode 100644 index 0000000..b9e66b4 --- /dev/null +++ b/docs/reports/viewclinic/04_supporters/Viewclinic_Supporters_Worker_E2E_2026-09-07.md @@ -0,0 +1,60 @@ +# 글 생성 리포트 · viewclinic · gpt-4.1 (대조 gpt-4.1) · 2026-09-07 + +통과 5 · 근거 플래그 남음 0 · 게이트 실패 0 · 오류 0 / 5 · 수정 호출 0편 · 총 $0.293 · 편당 $0.059 · 편당 15.34s +가격 기준 USD/1M tokens 입력 2 · 출력 8 (목록가, 실제 청구와 다를 수 있음) + +1차 출력 오류 분포(수정 전): 없음 +최종 경고 분포: NUMERIC_NO_QUALIFIER×2 +최종 오류 분포: 없음 + +상태: ok = 게이트·수치·근거 대조 모두 통과 / ok_needs_review = 게이트 통과, 모델 근거 대조 플래그 남음(사람 확인) / gate_failed = 배포 차단 + +| 글 | 분류 | 상태 | 오류 | 근거 플래그 | 경고 | 본문(한글) | 표 | FAQ | 확인 대기 | 호출 | 토큰 in/out | USD | 초 | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| visit-guide | B | ok | 0 | 0 | 0 | 824 | 2 | 8 | 3 | 2 | 11239/1462 | 0.034 | 12.8 | +| safety-system | E | ok | 0 | 0 | 0 | 1406 | 1 | 7 | 7 | 2 | 20919/1996 | 0.058 | 15.6 | +| precautions-guide | D | ok | 0 | 0 | 0 | 1023 | 2 | 8 | 5 | 2 | 26368/2261 | 0.071 | 16.3 | +| consult-questions | C | ok | 0 | 0 | 0 | 914 | 1 | 8 | 8 | 2 | 28580/1566 | 0.070 | 17.7 | +| aftercare-program | D | ok | 0 | 0 | 2 | 1035 | 1 | 7 | 3 | 2 | 22719/1823 | 0.060 | 14.3 | + +## 근거 대조 플래그 (사람이 확인할 것) + +(없음) + +## 병원 확인 대기 (모델이 근거에서 찾지 못한 것) + +### visit-guide +- 공휴일 및 일요일 진료 여부 +- 카카오톡 또는 기타 온라인 예약 경로 +- 주차장 만차 시 대체 주차 안내 + +### safety-system +- CCTV 영상의 실제 보관 기간 및 열람 절차 +- 마취통증의학과 전문의 상주 시간 및 직접 시술 여부 +- 응급상황 대응 시 담당자 및 실제 프로토콜 +- 수술 전/후 필수 검진 항목과 정기 검진 주기 +- 각 수술별로 실제 사용되는 의료장비 목록 +- 애프터케어 프로그램의 구체적 내용과 적용 대상 +- 집도의의 실제 참여 범위(상담~수술~경과) + +### precautions-guide +- 수술별(예: 눈, 코, 가슴 등) 세부 회복 일정의 최신 업데이트 +- 개인 건강 상태에 따른 약 복용 및 중단 시점의 예외 +- 특정 수술(예: 재수술, 복합수술)에서의 주의사항 변동 +- 수술 후 일상 복귀 가능 시점의 평균 및 범위 +- 애프터케어 프로그램 실제 적용 사례 및 상세 일정 + +### consult-questions +- 수술별 집도의 직접 수술 여부의 실제 적용 사례 +- 마취통증의학과 전문의 상주 시간 및 수술별 배정 기준 +- 애프터케어 프로그램의 실제 이용 가능 횟수와 비용 +- 정밀검진 항목별 비용 및 보험 적용 여부 +- 흉터 관리 서비스의 구체적 횟수와 추가 비용 +- 응급장비 점검 주기 및 실제 사용 사례 +- 호텔 연계 서비스의 구체적 제휴 호텔 목록 +- 외국어 통역 서비스의 예약 방법 및 이용 시간 + +### aftercare-program +- 관리 프로그램의 비용 포함 여부 +- 부위별 무제한 관리의 실제 적용 범위와 횟수 +- 홈케어 제품 제공 조건 및 추가 비용 여부 diff --git a/docs/reports/viewclinic/04_supporters/generated_samples/default_briefs_viewclinic.json b/docs/reports/viewclinic/04_supporters/generated_samples/default_briefs_viewclinic.json new file mode 100644 index 0000000..4c0e33f --- /dev/null +++ b/docs/reports/viewclinic/04_supporters/generated_samples/default_briefs_viewclinic.json @@ -0,0 +1,171 @@ +{ + "clinic": "viewclinic", + "generatedAt": "2026-09-07", + "note": "근거 수집 결과에서 자동 제안한 첫 배치. 사람이 확인·수정한 뒤 생성기에 넣는다(v2 §3-11).", + "posts": [ + { + "id": "visit-guide", + "category": "B", + "categoryLabel": "방문·예약", + "qbIds": [ + "B-01", + "B-03", + "B-05" + ], + "title": "뷰성형외과 방문 안내: 위치·진료시간·주차·예약은 어떻게 하나요?", + "intent": "오시는길·진료시간·주차·예약 경로를 표로. 팩트 시트 값과 오시는길 페이지만 근거로. 확인되지 않은 시간은 \"전화 확인\".", + "avoid": [ + "전후 사진", + "가격 금액", + "다른 병원·업계 평균 비교", + "효과 보장", + "환자 경험담" + ], + "evidence": { + "pages": [ + "https://www.viewclinic.com/viewis/direction3/", + "https://www.viewclinic.com/viewis/direction/", + "https://www.viewclinic.com/counsel/online-consultation/" + ], + "shorts": [], + "news": [], + "regulation": [], + "platform": false + }, + "videos": [], + "gallery": [] + }, + { + "id": "safety-system", + "category": "E", + "categoryLabel": "안전·마취", + "qbIds": [ + "E-01", + "E-02", + "E-04" + ], + "title": "뷰성형외과의 안전 시스템은 무엇으로 이루어져 있나요?", + "intent": "안전 시스템 페이지가 공개한 항목(마취 관리·응급 장비·검진·CCTV 등)을 항목 표로. 다른 병원·업계 평균과 비교하는 수치는 쓰지 않는다. 각 항목마다 상담에서 확인할 질문 하나.", + "avoid": [ + "전후 사진", + "가격 금액", + "다른 병원·업계 평균 비교", + "효과 보장", + "환자 경험담" + ], + "reviewerCandidate": "dr-songgyuyeong", + "evidence": { + "pages": [ + "https://www.viewclinic.com/system/safe-surgery/", + "https://www.viewclinic.com/breast-screening/breast-examination/", + "https://www.viewclinic.com/system/examination-center/", + "https://www.viewclinic.com/commonbottom-examination/" + ], + "shorts": [], + "news": [], + "regulation": [], + "platform": false + }, + "videos": [], + "gallery": [] + }, + { + "id": "precautions-guide", + "category": "D", + "categoryLabel": "시술 정보", + "qbIds": [ + "D-06", + "D-09" + ], + "title": "수술 전·후 주의사항에서 꼭 지켜야 할 것은 무엇인가요?", + "intent": "수술 전(약·금식·흡연)과 후(부기·샤워·운동·음주)의 시점을 부위 공통 항목 중심으로 표. 부위별로 다른 값은 부위를 명시. 개인차 조건 필수.", + "avoid": [ + "전후 사진", + "가격 금액", + "다른 병원·업계 평균 비교", + "효과 보장", + "환자 경험담" + ], + "reviewerCandidate": "dr-choisoonwoo", + "evidence": { + "pages": [ + "https://www.viewclinic.com/system/precautions/", + "https://www.viewclinic.com/system/after-care/" + ], + "shorts": [], + "news": [], + "regulation": [], + "platform": false + }, + "videos": [], + "gallery": [] + }, + { + "id": "consult-questions", + "category": "C", + "categoryLabel": "선택 기준", + "qbIds": [ + "C-02", + "E-08" + ], + "title": "상담에서 꼭 물어봐야 할 질문은 무엇인가요?", + "intent": "안전·검진·애프터케어 페이지가 공개한 항목을 근거로 상담 질문 체크리스트 8~10개(\"- [ ]\" 형식). 답을 적는 칸을 두고, 질문마다 왜 묻는지 한 줄.", + "avoid": [ + "전후 사진", + "가격 금액", + "다른 병원·업계 평균 비교", + "효과 보장", + "환자 경험담" + ], + "reviewerCandidate": "dr-choisoonwoo", + "evidence": { + "pages": [ + "https://www.viewclinic.com/system/safe-surgery/", + "https://www.viewclinic.com/system/after-care/", + "https://www.viewclinic.com/breast-screening/breast-examination/", + "https://www.viewclinic.com/system/examination-center/", + "https://www.viewclinic.com/commonbottom-examination/" + ], + "shorts": [], + "news": [], + "regulation": [], + "platform": false + }, + "videos": [], + "gallery": [] + }, + { + "id": "aftercare-program", + "category": "D", + "categoryLabel": "시술 정보", + "qbIds": [ + "D-08", + "E-06" + ], + "title": "수술 후 관리 프로그램은 무엇이 포함되고 언제까지 이어지나요?", + "intent": "애프터케어 페이지의 부위별 프로그램·기간을 표로. 견적 포함 여부는 \"병원 확인 대기\".", + "avoid": [ + "전후 사진", + "가격 금액", + "다른 병원·업계 평균 비교", + "효과 보장", + "환자 경험담" + ], + "reviewerCandidate": "dr-choisoonwoo", + "evidence": { + "pages": [ + "https://www.viewclinic.com/system/after-care/", + "https://www.viewclinic.com/breast-screening/breast-examination/", + "https://www.viewclinic.com/system/examination-center/", + "https://www.viewclinic.com/commonbottom-examination/" + ], + "shorts": [], + "news": [], + "regulation": [], + "platform": false + }, + "videos": [], + "gallery": [] + } + ] +} diff --git a/src/components/discovery/ClinicInputsPanel.tsx b/src/components/discovery/ClinicInputsPanel.tsx new file mode 100644 index 0000000..251ed99 --- /dev/null +++ b/src/components/discovery/ClinicInputsPanel.tsx @@ -0,0 +1,205 @@ +/** + * 병원 확인 항목 패널 (서포터즈 자동 빌드 v2 §6 · §7-5 · §12-3) + * - /discovery/:id 리포트 안에서, 자동 빌드 미리보기 상태와 병원이 입력해야 하는 값을 한 곳에 둔다. + * - 읽기: supporter_builds(최신 1건) · supporter_inputs(같은 clinic_id, 최신 행이 유효값) + * - 쓰기: supporter_inputs insert (anon RLS). 저장 후 워커가 apply_inputs → 재빌드한다. 여기서는 사이트를 직접 고치지 않는다. + * - 값이 없으면 사이트는 "확인 대기"로 발행된다. 이 폼은 그 자리를 채우는 곳이다. + * - 의학 검토·게시 승인은 사람 게이트다(§3-7). 후보만 보여주고 확정은 병원이 입력한다. + */ +import { useEffect, useMemo, useState } from 'react'; +import { supabase } from '../../lib/supabase'; + +type Build = { + id: string; + status: string; + phase: string | null; + preview_url: string | null; + posts: Array<{ id: string; title: string; category: string; status?: string; reviewStatus?: string; pending?: string[] }>; + report: { physicians?: Array<{ id: string; name: string; title: string }>; generator?: Record; totalCostUsd?: number } | null; + updated_at: string; +}; +type InputRow = { key: string; post_id: string | null; value: Record; input_by: string | null; created_at: string }; + +const STATUS_LABEL: Record = { + queued: '대기', running: '빌드 중', gate_failed: '발행 게이트 실패', failed: '실패', preview: '미리보기 준비됨', approved: '승인 완료', published: '색인 허용 발행', +}; + +// v2 §6 입력 항목. key 는 supporter_inputs.key 와 apply_inputs.mjs 가 같은 값을 쓴다. +const FIELDS: Array<{ key: string; title: string; hint: string; inputs: Array<{ name: string; label: string; type?: 'text' | 'textarea' | 'date' }> }> = [ + { key: 'author', title: '1. 작성자 실명·직함·공개 프로필', hint: '글 상단 "작성"과 Person 스키마에 들어갑니다.', inputs: [{ name: 'name', label: '이름' }, { name: 'role', label: '직함 (예: 편집 책임)' }, { name: 'sameAs', label: '공개 프로필 URL (인터뷰·기사, 쉼표 구분)' }, { name: 'email', label: '정정 요청 이메일' }] }, + { key: 'editor', title: '편집 책임자·정정 경로', hint: '정정 기록 페이지와 푸터에 표시됩니다.', inputs: [{ name: 'name', label: '편집 책임자 이름' }, { name: 'email', label: '정정 요청 이메일' }] }, + { key: 'sponsorship', title: '3. 지원 관계 문구', hint: '모든 글 첫 부분에 그대로 실립니다. 예: 이 글은 OO의원의 지원(광고비)을 받아 OO 서포터즈가 작성했습니다.', inputs: [{ name: 'notice', label: '고지 문장', type: 'textarea' }] }, + { key: 'quote_items', title: '4. 견적 기본 포함 항목과 결제 방식', hint: '금액은 받지 않습니다. 항목과 방식만 적어 주세요.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, + { key: 'recovery', title: '5. 회복 일정표의 병원 확정값', hint: '출근 기준, 걷기 시작 시점 등 공개 자료에 없는 값.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, + { key: 'revision_policy', title: '6. 재수술 정책', hint: '다른 병원 수술의 상담 가능 여부, 부위별 대기 기간.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, + { key: 'discrepancy', title: '7. 페이지 간 표기 불일치의 정답', hint: '예: 코 실밥 제거일이 코성형 페이지 7일, 애프터케어 3일로 다릅니다.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, + { key: 'specialty_doctors', title: '8. 진료 분야별 담당 원장', hint: '아래 의료진 목록의 ID 를 적습니다. 글별 검토자 후보가 됩니다.', inputs: [{ name: 'eye', label: '눈' }, { name: 'nose', label: '코' }, { name: 'contour', label: '윤곽' }, { name: 'lifting', label: '리프팅' }, { name: 'breast', label: '가슴' }, { name: 'anesthesia', label: '마취' }] }, + { key: 'domain', title: '9. 정식 도메인·색인 허용 시점·홈페이지 링크 위치', hint: '도메인이 정해지기 전에는 supporters-.vercel.app 미리보기(noindex)로 둡니다.', inputs: [{ name: 'domain', label: '도메인' }, { name: 'indexableFrom', label: '색인 허용 시점', type: 'date' }, { name: 'homepageLinkPosition', label: '홈페이지에서의 링크 위치' }] }, +]; + +export function ClinicInputsPanel({ clinicId }: { clinicId: string }) { + const [build, setBuild] = useState(null); + const [rows, setRows] = useState([]); + const [inputBy, setInputBy] = useState(''); + const [form, setForm] = useState>>({}); + const [postForm, setPostForm] = useState>({}); + const [busy, setBusy] = useState(null); + const [msg, setMsg] = useState(null); + const [loadError, setLoadError] = useState(null); + + useEffect(() => { + let alive = true; + (async () => { + const [b, i] = await Promise.all([ + supabase.from('supporter_builds').select('id,status,phase,preview_url,posts,report,updated_at').eq('clinic_id', clinicId).order('created_at', { ascending: false }).limit(1), + supabase.from('supporter_inputs').select('key,post_id,value,input_by,created_at').eq('clinic_id', clinicId).order('created_at', { ascending: true }), + ]); + if (!alive) return; + if (b.error || i.error) { setLoadError((b.error ?? i.error)?.message ?? '불러오기 실패'); return; } + setBuild((b.data?.[0] as Build | undefined) ?? null); + setRows((i.data as InputRow[]) ?? []); + })(); + return () => { alive = false; }; + }, [clinicId]); + + // 최신 행이 유효값 + const latest = useMemo(() => { + const m = new Map(); + for (const r of rows) m.set(`${r.key}|${r.post_id ?? ''}`, r); + return m; + }, [rows]); + const current = (key: string, postId = '') => latest.get(`${key}|${postId}`); + + async function save(key: string, value: Record, postId?: string) { + setBusy(`${key}|${postId ?? ''}`); setMsg(null); + const { error } = await supabase.from('supporter_inputs').insert({ clinic_id: clinicId, key, post_id: postId ?? null, value, input_by: inputBy || null }); + setBusy(null); + if (error) { setMsg(`저장 실패: ${error.message}`); return; } + setRows((prev) => [...prev, { key, post_id: postId ?? null, value, input_by: inputBy || null, created_at: new Date().toISOString() }]); + setMsg('저장했습니다. 다음 빌드에서 반영됩니다.'); + } + + const physicians = build?.report?.physicians ?? []; + const posts = build?.posts ?? []; + + return ( +
+
+
SUPPORTERS BUILD · 병원 확인 항목
+

서포터즈 사이트 미리보기와 병원이 확인해 주실 것

+

+ 병원 홈페이지 공개 자료만으로 만든 초안입니다. 아래 항목은 자료에 없어 병원만 답할 수 있는 값이며, 입력 전까지는 사이트에 "확인 대기"로 표시됩니다. 의학 검토와 게시 승인은 병원이 확정합니다. +

+ + {/* 빌드 상태 */} +
+ {loadError &&

불러오기 실패: {loadError}

} + {!loadError && !build &&

아직 자동 빌드 기록이 없습니다. URL 신청이 접수되면 여기에 진행 상태가 표시됩니다.

} + {build && ( +
+
+

상태

+

{STATUS_LABEL[build.status] ?? build.status}{build.phase ? 단계 {build.phase} : null}

+

갱신 {build.updated_at.slice(0, 16).replace('T', ' ')} · 글 {posts.length}편{typeof build.report?.totalCostUsd === 'number' ? ` · 생성 비용 $${build.report.totalCostUsd}` : ''}

+
+ {build.preview_url && ( + 미리보기 열기 (noindex) + )} +
+ )} +
+ +
+ {/* 9 항목 폼 */} +
+ + {FIELDS.map((f) => { + const cur = current(f.key)?.value as Record | undefined; + const draft = form[f.key] ?? {}; + return ( +
+
+

{f.title}

+ {cur ? `입력됨 ${String(current(f.key)?.created_at ?? '').slice(0, 10)}` : '확인 대기'} +
+

{f.hint}

+
+ {f.inputs.map((inp) => { + const value = draft[inp.name] ?? (cur ? String(Array.isArray(cur[inp.name]) ? (cur[inp.name] as string[]).join(', ') : cur[inp.name] ?? '') : ''); + const set = (v: string) => setForm((p) => ({ ...p, [f.key]: { ...(p[f.key] ?? {}), [inp.name]: v } })); + return ( +