'use strict'; // 반영 검증 리포트를 독립된 HTML 페이지로 렌더링한다(생성 화면과 분리, 사용자가 선택했을 때만 열람). function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); } const CAT = { cta: '행동 유도(CTA)', feature: '기능', page: '페이지', style: '디자인', copy: '문구·어조', facts: '사실정보', regulation: '규제 표현' }; const ST = { pass: ['통과', '#065f46', '#d1fae5'], partial: ['부분 반영', '#92400e', '#fef3c7'], fail: ['미반영', '#991b1b', '#fee2e2'] }; function render(site) { const plan = site.plan || {}; const v = plan.verification; const b = plan.brief || {}; const at = v ? new Date(v.at).toLocaleString('ko-KR') : ''; let body = ''; if (!v) body = '

아직 검증이 실행되지 않았습니다. 생성 화면에서 "재검증"을 누르면 리포트가 만들어집니다.

'; else { const r = v.rules; const s = r.summary; const chip = (t, bad) => `${esc(t)}`; body += `

요약

${chip(`페이지 파일 ${s.files}개`)}${chip(`누락 페이지 ${s.pagesMissing}`, s.pagesMissing > 0)}${chip(`[입력 필요] ${s.placeholders}곳`, s.placeholders > 0)}${chip(`Brief 에 없는 사실정보 ${s.extraneous}건`, s.extraneous > 0)}${chip(`규제 표현 ${s.regulated}건`, s.regulated > 0)}${v.ai && typeof v.ai.score === 'number' ? chip(`AI 대조 점수 ${v.ai.score}/100`, v.ai.score < 70) : ''}
${v.stale ? '

※ 마지막 검증 이후 페이지가 수정되었습니다. 최신 상태를 보려면 재검증하세요.

' : ''}
`; body += `

페이지 확인

${r.pages.map((p) => ``).join('')}
페이지파일상태
${esc(p.page)}${esc(p.file)}${p.exists ? '있음' : '없음'}
`; body += `

입력이 필요한 항목

${r.placeholders.length ? `

전화·주소·영업시간·가격·자격·후기 등은 입력한 값만 사용하며, 없는 값은 "[입력 필요]" 로 표시됩니다. 기본 사항의 사실정보를 입력한 뒤 수정 지시를 보내면 채워집니다.

` : '

없음 — 모든 사실정보가 채워져 있습니다.

'}
`; body += `

Brief 에 없는 사실정보 (제거 대상)

${r.extraneous.length ? `` : '

없음

'}
`; body += `

규제 표현 검사

${r.regulated.length ? `` : `

발견되지 않음${b.regulated && b.regulated.flag ? ` (적용 제약: ${esc((b.regulated.constraints || []).join(' / '))})` : ''}

`}
`; if (v.ai && v.ai.items) body += `

AI 대조 결과 — Brief ↔ 생성 결과

${v.ai.items.map((i) => { const st = ST[i.status] || ST.partial; return ``; }).join('')}
구분항목상태근거
${esc(CAT[i.category] || i.category)}${esc(i.name)}${st[0]}${esc(i.detail)}
${v.ai.fixes && v.ai.fixes.length ? `

제안된 수정

    ${v.ai.fixes.map((f) => `
  1. ${esc(f)}
  2. `).join('')}

생성 화면의 "검증 결과 자동 수정"으로 한 번에 반영할 수 있습니다.

` : ''}
`; // v1.3.16 다국어 품질 게이트 (번역 누락률·원문 잔존율·링크 대응·언어별 검수) if (v.i18n && v.i18n.languages) { const pct = (x) => `${(x * 100).toFixed(1)}%`; const th = v.i18n.thresholds || {}; body += `

다국어 품질 게이트 ${v.i18n.pass ? '통과' : '미통과'}

기준: 번역 누락률 ≤ ${pct(th.maxMissing || 0)} (시스템 문구 누락 0), 원문 잔존율 ≤ ${pct(th.maxResidue || 0)}, 사이트 안 링크가 기본 언어와 같은 짝, 언어별 검수 완료.

${Object.entries(v.i18n.languages).map(([c, r]) => r.source ? `` : ``).join('')}
언어누락률잔존율링크검수결과
${esc(c)} (기본)원문—
${esc(c)}${pct(r.missing.rate)}${r.runtimeMissing ? ` · 시스템 문구 ${r.runtimeMissing}` : ''}${pct(r.residue.rate)}${r.residue.samples.length ? `
${r.residue.samples.map(esc).join('
')}
` : ''}
${esc({ approved: '검수 완료', rejected: '수정 필요', pending: '검수 대기' }[r.review] || r.review)}${r.pass ? '통과' : '미통과'}
`; } if (v.ai && !v.ai.items && v.ai.error) body += `

AI 대조 결과

AI 대조를 완료하지 못했습니다. 재검증을 눌러 다시 시도하세요.

`; } return `검증 리포트 — ${esc(site.title)}

반영 검증 리포트

${esc(site.title)} · ${esc(site.url || '')}${at ? ` · 검증 시각 ${esc(at)}` : ''}
파일로 저장 인쇄
${body}
`; } module.exports = { render };