fix(supporters): 배포를 막던 게이트 두 건 · 대표 이미지 alt 의 효과 주장 제거
뷰 배포가 Vercel 빌드에서 멈췄다. 워커는 통과했는데 Vercel 이 막힌 이유는 --from deploy 가 빌드 게이트를 건너뛰고 바로 배포로 갔고, Vercel 은 자체 npm run build (astro build && check.mjs)를 돌리기 때문이다. 실패는 두 가지였다. 1. HOME_DUPLICATE: 검토자 표시줄의 원장 이력이 병원 홈페이지 문장과 40자 일치했다. 이 자리는 병원 의료진 페이지의 이력을 프로필 링크와 함께 그대로 옮기는 귀속 표시라 설계상 일치한다. 다른 말로 바꿔 쓰면 이력이 사실과 달라진다. /stay 의 인용 블록(#recovery-quote)을 이미 같은 이유로 빼고 있어서, 같은 규칙으로 .doctor-strip 을 QUOTED_BLOCKS 에 넣었다. 금칙어·운영자 어휘 검사는 그대로 적용된다. 2. 대표 이미지 alt 의 효과 주장: 배정된 alt 가 "손상된 피부 세포 재생과 콜라겐 생성으로 흉터와 색소침착 개선" 처럼 결과를 주장했다. 의료법 56조 2항 8호 대상이고 주제도 맞지 않았다. 이미지 자체는 문제가 없으므로 빼지 않고, 효과·결과 어휘가 있으면 출처만 밝히는 중립 문구로 바꾼다. 수집기의 분류를 믿지 않는다(일러스트가 시설로 들어온다)는 이유로 "시설 사진" 처럼 유형을 단정하지 않고 "홈페이지 이미지"로 쓴다. 검증: 세 사이트 모두 npm run build 종료코드 0(Vercel 이 돌리는 명령과 같다). 게이트 41/41, 샘플 check.mjs 오류 0건, tsc 0 에러, 내보내기 0. 연속 중복은 뷰 1·원진 1 이고, 오라클은 수집 이미지가 없어 그대로다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd754cf212
commit
f0857420e1
@ -80,6 +80,9 @@ const PREF = {
|
|||||||
G: ['clinic', 'procedure', 'equipment'],
|
G: ['clinic', 'procedure', 'equipment'],
|
||||||
};
|
};
|
||||||
const CATS = new Set(['procedure', 'equipment', 'clinic']);
|
const CATS = new Set(['procedure', 'equipment', 'clinic']);
|
||||||
|
// 수집한 alt 가 효과·결과를 주장하면 그 문장을 쓰지 않는다. 카드와 글 머리의 alt 로 나가면
|
||||||
|
// 의료법 56조 2항 8호(객관적 사실 과장) 대상이 된다. 이미지 자체는 문제가 없으므로
|
||||||
|
// 빼지 않고 무엇을 찍은 것인지만 밝히는 중립 문구로 바꾼다.
|
||||||
const pool = (manifest.items ?? [])
|
const pool = (manifest.items ?? [])
|
||||||
.filter((i) => CATS.has(i.category) && !shown.has(i.src) && !kept.has(i.src))
|
.filter((i) => CATS.has(i.category) && !shown.has(i.src) && !kept.has(i.src))
|
||||||
.map((i) => ({ ...i, inGallery: inGallery.has(i.src) }));
|
.map((i) => ({ ...i, inGallery: inGallery.has(i.src) }));
|
||||||
@ -88,12 +91,16 @@ if (!targets.length) { console.log(`글 ${posts.length}편 · 겹치는 대표
|
|||||||
if (!pool.length) { console.log(`대상 ${targets.length}편이지만 쓸 수 있는 이미지가 없음 (의료진 사진은 글 히어로로 쓰지 않는다)`); process.exit(0); }
|
if (!pool.length) { console.log(`대상 ${targets.length}편이지만 쓸 수 있는 이미지가 없음 (의료진 사진은 글 히어로로 쓰지 않는다)`); process.exit(0); }
|
||||||
|
|
||||||
// ---------- 문구 ----------
|
// ---------- 문구 ----------
|
||||||
|
const EFFICACY = /개선|완화|제거|재생|촉진|강화|효과|없애|줄여|늘려|맑아|밝아|탄력|안전하게|빠른\s*회복|통증\s*없/;
|
||||||
|
// 수집기의 분류를 그대로 믿지 않는다. 일러스트가 시설(clinic)로 들어오는 경우가 있어
|
||||||
|
// 유형을 단정하는 문구 대신 출처만 밝힌다.
|
||||||
function shortAlt(item) {
|
function shortAlt(item) {
|
||||||
const raw = String(item.alt || '').replace(/\s+/g, ' ').trim();
|
const raw = String(item.alt || '').replace(/\s+/g, ' ').trim();
|
||||||
if (!raw) return `${clinicName} 홈페이지 이미지`.trim();
|
const fallback = [clinicName, '홈페이지 이미지'].filter(Boolean).join(' ');
|
||||||
|
if (!raw || EFFICACY.test(raw)) return fallback;
|
||||||
let s = raw.split(/(?<=[.!?])\s|(?=\s\d\s)/)[0].replace(/\s*\d+\s*$/, '').trim();
|
let s = raw.split(/(?<=[.!?])\s|(?=\s\d\s)/)[0].replace(/\s*\d+\s*$/, '').trim();
|
||||||
if (s.length > 60) s = s.slice(0, 58).replace(/\s\S*$/, '') + '…';
|
if (s.length > 60) s = s.slice(0, 58).replace(/\s\S*$/, '') + '…';
|
||||||
return s || `${clinicName} 홈페이지 이미지`.trim();
|
return EFFICACY.test(s) ? fallback : (s || fallback);
|
||||||
}
|
}
|
||||||
function caption(item) {
|
function caption(item) {
|
||||||
const t = String(item.page?.title || '').replace(/\s+/g, ' ').trim();
|
const t = String(item.page?.title || '').replace(/\s+/g, ' ').trim();
|
||||||
|
|||||||
@ -36,9 +36,14 @@ function walk(dir, out = []) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withoutRecoveryQuote(html) {
|
// 홈페이지 40자 일치 검사에서 뺄 블록. 둘 다 출처를 밝히고 병원 원문을 그대로 옮기는 자리라 설계상 일치한다.
|
||||||
|
// #recovery-quote : /stay 의 "시술 후 관리 안내" 인용(출처 링크·확인일 표시)
|
||||||
|
// .doctor-strip : 글의 검토자 표시줄. 원장 이력을 병원 의료진 페이지에서 옮기고 프로필 링크를 단다.
|
||||||
|
// 다른 말로 바꿔 쓰면 이력이 사실과 달라지므로 원문 그대로 두는 것이 맞다.
|
||||||
|
const QUOTED_BLOCKS = ['#recovery-quote', '.doctor-strip'];
|
||||||
|
function withoutQuotedBlocks(html) {
|
||||||
const root = parseHtml(html);
|
const root = parseHtml(html);
|
||||||
root.querySelectorAll('#recovery-quote').forEach((el) => el.remove());
|
for (const sel of QUOTED_BLOCKS) root.querySelectorAll(sel).forEach((el) => el.remove());
|
||||||
return root.toString();
|
return root.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,9 +78,11 @@ for (const f of files) {
|
|||||||
const h1 = (html.match(/<h1\b/g) || []).length; if (h1 !== 1) add(rel, [{ level: R.E, code: 'H1_COUNT', msg: `H1 ${h1}개 (1개여야 함)` }]);
|
const h1 = (html.match(/<h1\b/g) || []).length; if (h1 !== 1) add(rel, [{ level: R.E, code: 'H1_COUNT', msg: `H1 ${h1}개 (1개여야 함)` }]);
|
||||||
// 금칙 표현·홈페이지 중복·운영자 어휘·제목 화법
|
// 금칙 표현·홈페이지 중복·운영자 어휘·제목 화법
|
||||||
add(rel, R.checkBannedBody(text));
|
add(rel, R.checkBannedBody(text));
|
||||||
// 홈페이지 40자 연속 일치(§3-11)는 글이 홈페이지 문장을 베끼는 것을 막는 규칙이다. /stay 의 "시술 후 관리 안내"(#recovery-quote)는
|
// 홈페이지 40자 연속 일치(§3-11)는 글이 홈페이지 문장을 베끼는 것을 막는 규칙이다.
|
||||||
// 출처 링크·확인일을 붙인 병원 원문 인용이라 설계상 일치하므로 그 블록만 빼고 본다. 금칙어·운영자 어휘 검사는 인용 블록에도 그대로 적용한다.
|
// 출처를 밝힌 인용 블록(QUOTED_BLOCKS)은 설계상 일치하므로 빼고 본다.
|
||||||
add(rel, R.checkHomeDuplicate(/id="recovery-quote"/.test(html) ? R.stripHtml(withoutRecoveryQuote(html)) : text, HOME_TEXT));
|
// 금칙어·운영자 어휘 검사는 인용 블록에도 그대로 적용한다.
|
||||||
|
const quoted = /id="recovery-quote"|class="[^"]*doctor-strip/.test(html);
|
||||||
|
add(rel, R.checkHomeDuplicate(quoted ? R.stripHtml(withoutQuotedBlocks(html)) : text, HOME_TEXT));
|
||||||
add(rel, R.checkOperatorVocab(text, rel));
|
add(rel, R.checkOperatorVocab(text, rel));
|
||||||
add(rel, R.checkHeadingEndings(html));
|
add(rel, R.checkHeadingEndings(html));
|
||||||
// §12-2 (1) 글 첫 부분 지원 고지. site.sponsorNotice 가 켜져 있으면 error, 아니면 warn
|
// §12-2 (1) 글 첫 부분 지원 고지. site.sponsorNotice 가 켜져 있으면 error, 아니면 warn
|
||||||
|
|||||||
@ -80,6 +80,9 @@ const PREF = {
|
|||||||
G: ['clinic', 'procedure', 'equipment'],
|
G: ['clinic', 'procedure', 'equipment'],
|
||||||
};
|
};
|
||||||
const CATS = new Set(['procedure', 'equipment', 'clinic']);
|
const CATS = new Set(['procedure', 'equipment', 'clinic']);
|
||||||
|
// 수집한 alt 가 효과·결과를 주장하면 그 문장을 쓰지 않는다. 카드와 글 머리의 alt 로 나가면
|
||||||
|
// 의료법 56조 2항 8호(객관적 사실 과장) 대상이 된다. 이미지 자체는 문제가 없으므로
|
||||||
|
// 빼지 않고 무엇을 찍은 것인지만 밝히는 중립 문구로 바꾼다.
|
||||||
const pool = (manifest.items ?? [])
|
const pool = (manifest.items ?? [])
|
||||||
.filter((i) => CATS.has(i.category) && !shown.has(i.src) && !kept.has(i.src))
|
.filter((i) => CATS.has(i.category) && !shown.has(i.src) && !kept.has(i.src))
|
||||||
.map((i) => ({ ...i, inGallery: inGallery.has(i.src) }));
|
.map((i) => ({ ...i, inGallery: inGallery.has(i.src) }));
|
||||||
@ -88,12 +91,16 @@ if (!targets.length) { console.log(`글 ${posts.length}편 · 겹치는 대표
|
|||||||
if (!pool.length) { console.log(`대상 ${targets.length}편이지만 쓸 수 있는 이미지가 없음 (의료진 사진은 글 히어로로 쓰지 않는다)`); process.exit(0); }
|
if (!pool.length) { console.log(`대상 ${targets.length}편이지만 쓸 수 있는 이미지가 없음 (의료진 사진은 글 히어로로 쓰지 않는다)`); process.exit(0); }
|
||||||
|
|
||||||
// ---------- 문구 ----------
|
// ---------- 문구 ----------
|
||||||
|
const EFFICACY = /개선|완화|제거|재생|촉진|강화|효과|없애|줄여|늘려|맑아|밝아|탄력|안전하게|빠른\s*회복|통증\s*없/;
|
||||||
|
// 수집기의 분류를 그대로 믿지 않는다. 일러스트가 시설(clinic)로 들어오는 경우가 있어
|
||||||
|
// 유형을 단정하는 문구 대신 출처만 밝힌다.
|
||||||
function shortAlt(item) {
|
function shortAlt(item) {
|
||||||
const raw = String(item.alt || '').replace(/\s+/g, ' ').trim();
|
const raw = String(item.alt || '').replace(/\s+/g, ' ').trim();
|
||||||
if (!raw) return `${clinicName} 홈페이지 이미지`.trim();
|
const fallback = [clinicName, '홈페이지 이미지'].filter(Boolean).join(' ');
|
||||||
|
if (!raw || EFFICACY.test(raw)) return fallback;
|
||||||
let s = raw.split(/(?<=[.!?])\s|(?=\s\d\s)/)[0].replace(/\s*\d+\s*$/, '').trim();
|
let s = raw.split(/(?<=[.!?])\s|(?=\s\d\s)/)[0].replace(/\s*\d+\s*$/, '').trim();
|
||||||
if (s.length > 60) s = s.slice(0, 58).replace(/\s\S*$/, '') + '…';
|
if (s.length > 60) s = s.slice(0, 58).replace(/\s\S*$/, '') + '…';
|
||||||
return s || `${clinicName} 홈페이지 이미지`.trim();
|
return EFFICACY.test(s) ? fallback : (s || fallback);
|
||||||
}
|
}
|
||||||
function caption(item) {
|
function caption(item) {
|
||||||
const t = String(item.page?.title || '').replace(/\s+/g, ' ').trim();
|
const t = String(item.page?.title || '').replace(/\s+/g, ' ').trim();
|
||||||
|
|||||||
@ -36,9 +36,14 @@ function walk(dir, out = []) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withoutRecoveryQuote(html) {
|
// 홈페이지 40자 일치 검사에서 뺄 블록. 둘 다 출처를 밝히고 병원 원문을 그대로 옮기는 자리라 설계상 일치한다.
|
||||||
|
// #recovery-quote : /stay 의 "시술 후 관리 안내" 인용(출처 링크·확인일 표시)
|
||||||
|
// .doctor-strip : 글의 검토자 표시줄. 원장 이력을 병원 의료진 페이지에서 옮기고 프로필 링크를 단다.
|
||||||
|
// 다른 말로 바꿔 쓰면 이력이 사실과 달라지므로 원문 그대로 두는 것이 맞다.
|
||||||
|
const QUOTED_BLOCKS = ['#recovery-quote', '.doctor-strip'];
|
||||||
|
function withoutQuotedBlocks(html) {
|
||||||
const root = parseHtml(html);
|
const root = parseHtml(html);
|
||||||
root.querySelectorAll('#recovery-quote').forEach((el) => el.remove());
|
for (const sel of QUOTED_BLOCKS) root.querySelectorAll(sel).forEach((el) => el.remove());
|
||||||
return root.toString();
|
return root.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,9 +78,11 @@ for (const f of files) {
|
|||||||
const h1 = (html.match(/<h1\b/g) || []).length; if (h1 !== 1) add(rel, [{ level: R.E, code: 'H1_COUNT', msg: `H1 ${h1}개 (1개여야 함)` }]);
|
const h1 = (html.match(/<h1\b/g) || []).length; if (h1 !== 1) add(rel, [{ level: R.E, code: 'H1_COUNT', msg: `H1 ${h1}개 (1개여야 함)` }]);
|
||||||
// 금칙 표현·홈페이지 중복·운영자 어휘·제목 화법
|
// 금칙 표현·홈페이지 중복·운영자 어휘·제목 화법
|
||||||
add(rel, R.checkBannedBody(text));
|
add(rel, R.checkBannedBody(text));
|
||||||
// 홈페이지 40자 연속 일치(§3-11)는 글이 홈페이지 문장을 베끼는 것을 막는 규칙이다. /stay 의 "시술 후 관리 안내"(#recovery-quote)는
|
// 홈페이지 40자 연속 일치(§3-11)는 글이 홈페이지 문장을 베끼는 것을 막는 규칙이다.
|
||||||
// 출처 링크·확인일을 붙인 병원 원문 인용이라 설계상 일치하므로 그 블록만 빼고 본다. 금칙어·운영자 어휘 검사는 인용 블록에도 그대로 적용한다.
|
// 출처를 밝힌 인용 블록(QUOTED_BLOCKS)은 설계상 일치하므로 빼고 본다.
|
||||||
add(rel, R.checkHomeDuplicate(/id="recovery-quote"/.test(html) ? R.stripHtml(withoutRecoveryQuote(html)) : text, HOME_TEXT));
|
// 금칙어·운영자 어휘 검사는 인용 블록에도 그대로 적용한다.
|
||||||
|
const quoted = /id="recovery-quote"|class="[^"]*doctor-strip/.test(html);
|
||||||
|
add(rel, R.checkHomeDuplicate(quoted ? R.stripHtml(withoutQuotedBlocks(html)) : text, HOME_TEXT));
|
||||||
add(rel, R.checkOperatorVocab(text, rel));
|
add(rel, R.checkOperatorVocab(text, rel));
|
||||||
add(rel, R.checkHeadingEndings(html));
|
add(rel, R.checkHeadingEndings(html));
|
||||||
// §12-2 (1) 글 첫 부분 지원 고지. site.sponsorNotice 가 켜져 있으면 error, 아니면 warn
|
// §12-2 (1) 글 첫 부분 지원 고지. site.sponsorNotice 가 켜져 있으면 error, 아니면 warn
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user