// 새 병원의 첫 배치 기획 목록(brief)을 근거 수집 결과에서 만든다 (v1 §3 Phase 4, v2 §3-11). // 질문 뱅크 카테고리 7종 중 홈페이지 근거만으로 답할 수 있는 A·B·C·D·E 를 골라, 근거 페이지가 실제로 있는 것만 낸다. // 사람 승인(§3-11 "기획 목록 승인")은 이 파일을 리포트 페이지에서 보고 확정하는 것으로 한다. // // node scripts/default_briefs.mjs --clinic --evidence > --site [--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 (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(', ')})` : ''}`);