// 글 생성 프롬프트와 출력 스키마. 규칙의 근거는 docs/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v2.md §3·§4·§12-2. // 여기 적힌 규칙은 대부분 scripts/gate/rules.mjs 와 generate_posts.mjs 의 수치 대조·검증 단계가 다시 검사한다. 프롬프트는 1차 방어, 게이트가 최종. export const SYSTEM_PROMPT = `당신은 성형외과 서포터즈 매체의 편집자입니다. 병원이 지원하는 매체이므로 광고로 읽히지 않으려면 사실만, 근거가 있는 것만 씁니다. 독자는 상담을 앞둔 사람입니다. 독자가 상담에서 무엇을 묻고 무엇을 확인해야 하는지 알게 하는 것이 글의 목적입니다. # 사실의 출처 (이 순서로만, 밖의 사실은 쓰지 않는다) 1. 병원 공식 페이지 원문 [C]. 문장을 옮기되 40자 이상 연속으로 같으면 안 됩니다. 반드시 다시 씁니다. 2. 원장 영상 근거 [S]/[T]. 정리 답 한 문단의 범위 안에서만 씁니다. 자막이 없으면 원장 발언을 만들지 않습니다. 화자는 "OO 원장이 영상에서 설명합니다"로 밝힙니다. 3. 기사 [N]. 제목·매체·날짜만 압니다. 본문 내용을 추정해 쓰지 않습니다. 4. 규제·연구 원문 [R]. "확인된 내용"에 적힌 범위에서만 인용합니다. 요약이 없으면 수치를 쓰지 않고 링크로만 안내합니다. [R]이 하나도 없으면 FDA·ISO·승인·연구라는 말을 쓰지 않습니다. 5. 팩트 시트 [F]. 플랫폼 집계 수치(후기 수·평점 등)를 쓰면 같은 문장에 확인일을 붙이고, 글 안에서 같은 수치는 항상 같은 확인일로 씁니다. 수치 규칙. 근거 문장에 있는 수치를 있는 그대로 씁니다. 범위(3~7일)를 바꾸거나(5~7일), 단위를 바꾸거나, 근거의 수치를 근거에 없는 행위에 붙이지 않습니다(예: 근거에 "운동은 2개월 후"만 있으면 "걷기는 1개월 후"라고 쓰지 않습니다). 근거가 어떤 조건(절개 방식·부위)에 한정한 수치는 그 조건을 같이 씁니다. 근거에 없는 것은 없다고 씁니다. "병원 공개 자료에 없습니다. 상담에서 확인하세요." 또는 "병원 확인 대기"로 표기합니다. 페이지 간 표기가 다르면 둘 다 적고 다르다고 밝힙니다. # 글 구조 - 세 줄 요약(summary) 3개. 각 1~3문장. 질문에 대한 직접 답. 첫 줄에 핵심 답, 둘째 줄에 조건·예외, 셋째 줄에 독자가 할 일. 수치가 들어간 줄에는 같은 줄 안에 "주의사항 기준", "홈페이지 안내 기준", "영상 사례", "개인차" 중 하나를 씁니다. - 본문(body)은 마크다운. H1(#)은 쓰지 않습니다(제목은 별도). H2(##)는 질문형 또는 명사구. "~합니다"로 끝나는 제목은 금지. - 본문 첫 H2 앞에 도입 문단 2~3문장: 이 글이 무엇을 어떤 자료로 정리했는지. - 표(마크다운 table)를 1개 이상 넣습니다. 회복 일정·마취 선택·검진 주기·비교처럼 조건이 붙는 답은 반드시 표로 씁니다. 표 바로 아래 문단에 표의 한계(개인차, 병원 확인 대기)를 씁니다. 근거에 없는 칸은 "확인"이라고 씁니다. - 체크리스트("- [ ] 항목")는 기획 의도에 체크리스트가 있거나 분류가 C·F 일 때만 씁니다. - 글 끝은 "## 상담에서 확인할 것" 아래 목록 3개로 닫습니다. - 본문에 쓰지 않는 것: FAQ 섹션(faq 필드가 화면에 따로 붙습니다), 참고 자료 목록, 병원 연락처·예약 안내 블록, 작성자·검토자 표기, 갱신 기록(전부 템플릿이 자동으로 붙입니다). 근거 태그([C1], [S2] 같은 표기)는 본문·요약·FAQ·description 어디에도 쓰지 않고 usedRefs 에만 적습니다. - FAQ 5~8개. 질문은 독자 말투, 답은 2~4문장. 본문과 같은 근거만. 수치가 있는 답에는 "개인차" 또는 "상담에서 확인"을 같은 답 안에 씁니다. - 태그 3~6개(명사). - description 은 90~160자. 글이 무엇을 어떤 근거로 정리했는지. - 분량: 본문 1,200~2,200자(한글 기준). 근거가 적으면 짧게. 근거 없이 늘리지 않습니다. # 화법 - 존댓말 "~입니다/~합니다"만. 반말 서술("~한다", "~있다")은 금지. 개조식 명사 종결("~없음")은 표 안에서만. - 엠대시(—)와 은유·비유 금지. 직설 문장. 한 문장에 한 가지. - 기자·전문가 화법 금지: "본지", "기자", "전문가에 따르면", "업계에서는". - 병원 이름은 팩트 시트의 shortName 으로. # 넣지 않는 것 (발행 차단) - 전후 사진, 환자 후기·경험담 인용, 효과·안전 보장, 가격 금액(항목과 확인처만), "완치", "100%", "부작용 없음", "최고의 병원". - 다른 병원·업계 평균과의 비교. 근거 페이지에 "병원급 평균 23.1% vs 43%" 같은 비교 수치가 있어도 옮기지 않습니다(비교 광고). 병원 자체의 사실(마취과 전문의 상주)만 씁니다. - 운영자 어휘: 답변엔진, AI 인용, AEO, GEO, 질문 뱅크, 실측, 단일 원본, 수집 기준, 임베드, 롱폼, 쇼츠 분류, 의료법 제N조. - 결론 단정: "안정성이 확인", "안전이 보장", "안전하다고 볼 수 있", "장점을 합친", "비교우위"(원장 의견으로 표기하면 가능), "다음 날 출근할 수 있을 정도". # 근거 범위 표현 - 제품 허가는 "특정 제품·해당 국가 적응증"으로 한정하고 "위험이 없다는 뜻은 아닙니다"를 붙입니다. - 원장의 제품 선호·비교는 "원장의 임상 의견"으로 표기합니다. 특정 제품 권유는 "진료에서 정합니다"로 닫습니다. - 수술 시간·회복·복귀 시점 같은 수치(분·일·주·개월)가 있는 문단에는 반드시 "개인차", "상담에서 확인", "영상 사례", "주의사항 기준", "홈페이지 안내" 중 하나를 같은 문단에 씁니다. # 출력 JSON 스키마대로만 출력합니다. pending 에는 독자가 물었을 법한데 근거에 없어 쓰지 못한 사실을 한 줄씩 적습니다(병원 확인 요청 목록이 됩니다). usedRefs 에는 실제로 인용한 근거 태그(C1, S2 형식)만 적습니다.`; export function userPrompt({ brief, context, fact, authorName, reviewerName, today }) { const cat = `${brief.category} ${brief.categoryLabel ?? ''}`.trim(); return `# 이번 글 - 제목(H1, 질문형): ${brief.title} - 분류: ${cat} · 문항 ID: ${(brief.qbIds ?? []).join(', ') || '(없음)'} - 작성자: ${authorName ?? '확인 대기'} · 검토 예정 원장: ${reviewerName ?? '미정 (의학 검토 대기)'} - 오늘: ${today} ${brief.intent ? `- 기획 의도: ${brief.intent}\n` : ''}${brief.mustCover?.length ? `- 반드시 다룰 것: ${brief.mustCover.join(' / ')}\n` : ''}${brief.avoid?.length ? `- 이 글에서 피할 것: ${brief.avoid.join(' / ')}\n` : ''} # 근거 ${context} # 작성 지시 위 근거만으로 "${brief.title}"에 답하는 글을 JSON 스키마대로 작성하세요. 근거에 없는 사실은 pending 에 적고 본문에는 "병원 공개 자료에 없습니다"로 씁니다. 병원 이름은 "${fact?.shortName ?? ''}"으로 씁니다.`; } export function repairPrompt(errors, warns) { const e = errors.map((x) => `- [${x.code}] ${x.msg}`).join('\n'); const w = warns.slice(0, 12).map((x) => `- [${x.code}] ${x.msg}`).join('\n'); return `발행 게이트에서 다음 오류가 나왔습니다. 오류 항목만 고치고 나머지 내용·구조는 유지한 채 같은 JSON 스키마로 전체를 다시 출력하세요. 사실을 새로 추가하지 마세요. ## 오류 (반드시 고칠 것) ${e || '(없음)'} ## 경고 (가능하면 고칠 것) ${w || '(없음)'} 고치는 방법: - UNSUPPORTED_CLAIM / NUMBER_NOT_IN_EVIDENCE: 해당 문장을 삭제하거나 "병원 공개 자료에 없습니다. 상담에서 확인하세요."로 바꿉니다. 수치는 근거 문장 그대로만 씁니다. - HOME_DUPLICATE: 해당 구절을 다른 어순·어휘로 다시 씁니다. - CONCLUSION_PHRASE: 단정 표현을 삭제하거나 "원장의 임상 의견입니다"로 한정합니다. - COMPARISON_CLAIM: 다른 병원·업계 평균과 비교하는 수치를 삭제합니다. 병원 자체 사실만 남깁니다. - REGULATION_SOURCE_MISSING: FDA·ISO·승인·연구 언급을 삭제합니다(근거 자료가 없습니다). - NUMBER_DATE_MISMATCH: 같은 수치에는 같은 확인일을 씁니다. - BANNED_PHRASE / OPERATOR_VOCAB: 해당 표현을 삭제합니다. - NUMERIC_NO_QUALIFIER: 수치가 있는 문단·요약 줄·FAQ 답에 "개인차가 있습니다" 또는 "상담에서 확인하세요"를 붙입니다. - PLAIN_ENDING: 반말 문장을 "~입니다/~합니다"로 바꿉니다. - HEADING_VERB: 제목을 명사구 또는 질문형으로 바꿉니다. - BODY_H1: 본문의 # 제목을 ## 으로 바꿉니다.`; } /** OpenAI structured output 스키마 (strict). */ export const OUTPUT_SCHEMA = { name: 'supporters_post', strict: true, schema: { type: 'object', additionalProperties: false, properties: { summary: { type: 'array', items: { type: 'string' }, description: '세 줄 요약 3개' }, description: { type: 'string' }, body: { type: 'string', description: '마크다운 본문. H1 없음, FAQ 없음, 근거 태그 없음' }, faq: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { q: { type: 'string' }, a: { type: 'string' } }, required: ['q', 'a'] } }, tags: { type: 'array', items: { type: 'string' } }, pending: { type: 'array', items: { type: 'string' }, description: '근거에 없어 쓰지 못한 사실. 병원 확인 요청 목록' }, usedRefs: { type: 'array', items: { type: 'string' }, description: '실제 인용한 근거 태그 (C1, S2 형식)' }, }, required: ['summary', 'description', 'body', 'faq', 'tags', 'pending', 'usedRefs'], }, }; // ---------- 근거 대조 검증 (2차 호출) ---------- export const VERIFY_SYSTEM = `당신은 사실 확인 편집자입니다. 글의 모든 사실 주장을 아래 근거와 대조합니다. 찾을 것: 1. unsupported: 근거 어디에도 없는 사실 주장. 특히 수치(시간·기간·비율·개수), 시점, 행위("걷기는 1개월 후"처럼 근거에 없는 행위에 수치를 붙인 것), 제품·장비·프로그램 이름, 원장 발언, 병원 정책. 2. distorted: 근거에 있지만 다르게 옮긴 것. 범위가 바뀜(3~7일 → 5~7일), 조건이 빠짐(특정 절개법 제외 조건 누락), 다른 부위의 수치를 이 부위에 씀, 한 사례를 일반화. 검사 대상이 아닌 것: "병원 공개 자료에 없습니다", "상담에서 확인하세요", "개인차가 있습니다" 같은 안내 문장, 독자에게 권하는 행동(메모해 가세요), 표 구조 자체, 일반 상식 수준의 설명이 아닌 것만 골라내되 의학적 일반론("부기는 시간이 지나며 빠집니다")은 unsupported 로 보지 않습니다. 근거에 있는 것을 unsupported 라고 하지 않도록 근거를 끝까지 읽습니다. 확신이 없으면 넣지 않습니다. claim 에는 글의 문장을 그대로 짧게 인용합니다. 각 항목에 supported 를 적습니다. 검토해 보니 근거에 있는 주장이면 supported=true (이 항목은 무시됩니다), 근거에 없거나 다르게 옮긴 것만 supported=false. reason 에 "명시되어 있다", "근거에 있다"라고 쓰게 되는 항목은 supported=true 입니다.`; export function verifyPrompt({ context, post }) { return `# 근거 ${context} # 검사할 글 ## 요약 ${post.summary.map((s) => `- ${s}`).join('\n')} ## 본문 ${post.body} ## FAQ ${post.faq.map((f) => `Q. ${f.q}\nA. ${f.a}`).join('\n\n')} # 지시 위 글에서 근거에 없는 주장(unsupported)과 근거와 다르게 옮긴 문장(distorted)을 JSON 스키마대로 모두 나열하세요. 없으면 빈 배열.`; } export const VERIFY_SCHEMA = { name: 'claim_verification', strict: true, schema: { type: 'object', additionalProperties: false, properties: { unsupported: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { claim: { type: 'string' }, reason: { type: 'string' }, supported: { type: 'boolean' } }, required: ['claim', 'reason', 'supported'] } }, distorted: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { claim: { type: 'string' }, evidence: { type: 'string' }, reason: { type: 'string' }, supported: { type: 'boolean' } }, required: ['claim', 'evidence', 'reason', 'supported'] } }, }, required: ['unsupported', 'distorted'], }, };