- 게이트: 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 <noreply@anthropic.com>
79 lines
5.9 KiB
JavaScript
79 lines
5.9 KiB
JavaScript
// 새 병원의 첫 배치 기획 목록(brief)을 근거 수집 결과에서 만든다 (v1 §3 Phase 4, v2 §3-11).
|
||
// 질문 뱅크 카테고리 7종 중 홈페이지 근거만으로 답할 수 있는 A·B·C·D·E 를 골라, 근거 페이지가 실제로 있는 것만 낸다.
|
||
// 사람 승인(§3-11 "기획 목록 승인")은 이 파일을 리포트 페이지에서 보고 확정하는 것으로 한다.
|
||
//
|
||
// node scripts/default_briefs.mjs --clinic <id> --evidence <evidence/<id>> --site <siteDir> [--out briefs.json] [--max 6]
|
||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||
import { join, resolve } from 'node:path';
|
||
|
||
const args = process.argv.slice(2);
|
||
const opt = (k, d) => { const i = args.indexOf(`--${k}`); return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : d; };
|
||
const clinic = opt('clinic'); const EV = resolve(opt('evidence', `../evidence/${clinic}`)); const SITE = resolve(opt('site', '.'));
|
||
const max = Number(opt('max', '6'));
|
||
if (!clinic || !existsSync(join(EV, 'index.json'))) { console.error('--clinic 과 --evidence <dir> (index.json 있는 폴더) 필요'); process.exit(2); }
|
||
|
||
const index = JSON.parse(readFileSync(join(EV, 'index.json'), 'utf8'));
|
||
const fact = JSON.parse(readFileSync(join(SITE, 'src/data/factSheet.json'), 'utf8'));
|
||
const authors = JSON.parse(readFileSync(join(SITE, 'src/data/authors.json'), 'utf8'));
|
||
const name = fact.shortName || fact.name || clinic;
|
||
const pages = index.pages.filter((p) => p.status === 200 && p.chars >= 200);
|
||
const byType = (t) => pages.filter((p) => p.type === t).sort((a, b) => b.chars - a.chars);
|
||
const urls = (...types) => types.flatMap((t) => byType(t).slice(0, 3).map((p) => p.url));
|
||
const physicianIds = Object.keys(authors.physicians ?? {});
|
||
const rep = physicianIds.find((id) => /대표/.test(authors.physicians[id].title ?? '')) ?? physicianIds[0];
|
||
|
||
const candidates = [
|
||
{
|
||
id: 'what-is-clinic', category: 'A', categoryLabel: '병원 소개', qbIds: ['A-02', 'A-05', 'A-07'],
|
||
title: `${name}은 어떤 병원인가요? 위치·진료 분야·의료진 구성`,
|
||
intent: '병원 소개·진료과·의료진 수·건물·위치를 사실만으로 정리한다. 수상·방송 이력은 홈페이지 텍스트에 있는 것만. 표 하나(진료 분야 × 담당 원장 수).',
|
||
need: ['about', 'doctor', 'direction'], pages: urls('about', 'doctor', 'facilities', 'direction'), reviewer: rep,
|
||
},
|
||
{
|
||
id: 'visit-guide', category: 'B', categoryLabel: '방문·예약', qbIds: ['B-01', 'B-03', 'B-05'],
|
||
title: `${name} 방문 안내: 위치·진료시간·주차·예약은 어떻게 하나요?`,
|
||
intent: '오시는길·진료시간·주차·예약 경로를 표로. 팩트 시트 값과 오시는길 페이지만 근거로. 확인되지 않은 시간은 "전화 확인".',
|
||
need: ['direction'], pages: urls('direction', 'reservation'), reviewer: null,
|
||
},
|
||
{
|
||
id: 'safety-system', category: 'E', categoryLabel: '안전·마취', qbIds: ['E-01', 'E-02', 'E-04'],
|
||
title: `${name}의 안전 시스템은 무엇으로 이루어져 있나요?`,
|
||
intent: '안전 시스템 페이지가 공개한 항목(마취 관리·응급 장비·검진·CCTV 등)을 항목 표로. 다른 병원·업계 평균과 비교하는 수치는 쓰지 않는다. 각 항목마다 상담에서 확인할 질문 하나.',
|
||
need: ['safety'], pages: urls('safety', 'checkup'), reviewer: physicianIds.find((id) => /마취/.test(authors.physicians[id].title ?? '')) ?? rep,
|
||
},
|
||
{
|
||
id: 'precautions-guide', category: 'D', categoryLabel: '시술 정보', qbIds: ['D-06', 'D-09'],
|
||
title: '수술 전·후 주의사항에서 꼭 지켜야 할 것은 무엇인가요?',
|
||
intent: '수술 전(약·금식·흡연)과 후(부기·샤워·운동·음주)의 시점을 부위 공통 항목 중심으로 표. 부위별로 다른 값은 부위를 명시. 개인차 조건 필수.',
|
||
need: ['precautions'], pages: urls('precautions', 'aftercare'), reviewer: rep,
|
||
},
|
||
{
|
||
id: 'consult-questions', category: 'C', categoryLabel: '선택 기준', qbIds: ['C-02', 'E-08'],
|
||
title: '상담에서 꼭 물어봐야 할 질문은 무엇인가요?',
|
||
intent: '안전·검진·애프터케어 페이지가 공개한 항목을 근거로 상담 질문 체크리스트 8~10개("- [ ]" 형식). 답을 적는 칸을 두고, 질문마다 왜 묻는지 한 줄.',
|
||
need: ['safety', 'aftercare'], pages: urls('safety', 'aftercare', 'checkup'), reviewer: rep,
|
||
},
|
||
{
|
||
id: 'aftercare-program', category: 'D', categoryLabel: '시술 정보', qbIds: ['D-08', 'E-06'],
|
||
title: '수술 후 관리 프로그램은 무엇이 포함되고 언제까지 이어지나요?',
|
||
intent: '애프터케어 페이지의 부위별 프로그램·기간을 표로. 견적 포함 여부는 "병원 확인 대기".',
|
||
need: ['aftercare'], pages: urls('aftercare', 'checkup'), reviewer: rep,
|
||
},
|
||
];
|
||
|
||
const posts = candidates
|
||
.filter((c) => c.need.every((t) => byType(t).length) && c.pages.length)
|
||
.slice(0, max)
|
||
.map(({ need, pages: pg, reviewer, ...c }) => ({
|
||
...c,
|
||
avoid: ['전후 사진', '가격 금액', '다른 병원·업계 평균 비교', '효과 보장', '환자 경험담'],
|
||
reviewerCandidate: reviewer ?? undefined,
|
||
evidence: { pages: [...new Set(pg)], shorts: [], news: [], regulation: [], platform: false },
|
||
videos: [], gallery: [],
|
||
}));
|
||
|
||
const out = { clinic, generatedAt: new Date().toISOString().slice(0, 10), note: '근거 수집 결과에서 자동 제안한 첫 배치. 사람이 확인·수정한 뒤 생성기에 넣는다(v2 §3-11).', posts };
|
||
const outPath = resolve(opt('out', join(SITE, 'briefs.json')));
|
||
writeFileSync(outPath, JSON.stringify(out, null, 2) + '\n');
|
||
console.log(`brief ${posts.length}편 → ${outPath}${posts.length < candidates.length ? ` (근거 부족으로 제외 ${candidates.length - posts.length}편: ${candidates.filter((c) => !posts.find((p) => p.id === c.id)).map((c) => c.id).join(', ')})` : ''}`);
|