o2o-infinith-demo/supporters/scripts/export_template.mjs
Haewon Kam e60ef04503 feat(supporters): 영어 화면의 병원 고유 사실을 데이터에서 읽도록 바꾸고 템플릿 재생성
templates/supporters-astro 는 supporters/ 에서 만들어지는 파생물인데, 정본이 된
supporters/ 코드에 뷰 고유 사실이 문장으로 박혀 있었다. 그대로 내보내면 다른 병원
사이트에 틀린 사실이 나간다. 빈 데이터로 빌드해 보니 실제로
"Founded 확인 대기 · Sinnonhyeon Station Exit 3" 가 나왔다.

데이터로 바꾼 곳 (전부 factSheetEn.json 에 이미 있던 값이다)
- 히어로 이미지 alt: site.json 의 heroImageAltEn 을 읽고, 없으면 한국어 alt 를 쓴다.
- 히어로 칩: 설립연도와 위치를 데이터에 있는 것만 잇는다. 둘 다 없으면 칩을 내지 않는다.
- 방문 카드 요약: factSheetEn 의 airportShort 가 있을 때만 경로를 적는다.
- 병원 카드 요약: 설립연도와 factSheetEn 의 building 첫 문장을 데이터에서 잇는다.
- 공항 카드: factSheetEn 의 airport 가 있을 때만 카드를 낸다.
- 영문 플래너 병원명: factSheetEn 의 shortName 을 읽고, 영문 표기가 없는 병원은
  한국어 상호를 쓴다.
- 404 안내: 상호를 factSheet 에서 읽는다.

템플릿 빈 데이터셋에 factSheetEn.json 이 없어서 뷰의 영문 사실이 통째로 남고 있었다.
빈 factSheetEn.json 을 만들고 site.json 에 heroImageAltEn 자리를 넣었다.

export_template.mjs 의 누락 검사가 상호만 찾아 지명으로 적힌 사실을 놓쳤다.
지명·건물 표현을 패턴에 넣고, 문장을 그리는 코드(src/pages·layouts·components·lib)만
내보내기를 멈추게 하고 데이터·빌드 도구는 알림만 하도록 나눴다. 빌드 도구의 기본 인자
때문에 검사가 늘 실패해 신호가 되지 못하던 문제도 함께 풀린다.

검증
- 빈 데이터 템플릿 빌드 14페이지, 영어 홈에 병원 고유 사실 0건, 사실을 말하던 조각은
  생략되고 요약문만 일반 문장으로 남는다.
- 뷰 사이트 빌드 32페이지, 영어 홈의 칩·공항 카드·요약문이 모두 그대로 나온다.
- 게이트 41/41 통과, check.mjs 오류 0건(경고 23건은 기존 건).
- 루트 tsc --noEmit 0 에러, 내보내기 종료코드 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 14:27:57 +09:00

62 lines
4.3 KiB
JavaScript

// supporters/(뷰성형외과 샘플) → templates/supporters-astro/ 내보내기.
// 병원 고유 데이터(글·데이터 JSON·이미지·home_text·구조 기준선)를 빼고 빈 데이터 세트(scripts/template/data)를 넣는다.
// 멱등: 대상 폴더를 지우고 다시 만든다.
// node scripts/export_template.mjs [--dest ../templates/supporters-astro]
import { cpSync, rmSync, mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const SRC = fileURLToPath(new URL('../', import.meta.url)).replace(/\/$/, '');
const argDest = process.argv.indexOf('--dest');
const DEST = argDest > -1 ? process.argv[argDest + 1] : join(SRC, '..', 'templates', 'supporters-astro');
const TPL = join(SRC, 'scripts', 'template');
// 복사에서 뺄 경로 (SRC 기준 상대). 디렉터리는 하위 전부.
const EXCLUDE = [
'node_modules', 'dist', '.astro', '.vercel', 'docs',
'src/content/posts', 'public/img', 'scripts/home_text.txt', 'scripts/gate/layout-baseline.json', 'scripts/template', 'README.md',
'src/data/recoveryNotes.cache.json', // 시술 후 관리 안내 번역 캐시(병원별 산출물)
];
const excluded = (abs) => {
const rel = relative(SRC, abs).split(sep).join('/');
return EXCLUDE.some((e) => rel === e || rel.startsWith(e + '/'));
};
rmSync(DEST, { recursive: true, force: true });
mkdirSync(DEST, { recursive: true });
cpSync(SRC, DEST, { recursive: true, filter: (s) => !excluded(s) });
// 빈 데이터 세트
for (const f of readdirSync(join(TPL, 'data'))) cpSync(join(TPL, 'data', f), join(DEST, 'src', 'data', f));
mkdirSync(join(DEST, 'src', 'content', 'posts'), { recursive: true });
writeFileSync(join(DEST, 'src', 'content', 'posts', '.gitkeep'), '');
mkdirSync(join(DEST, 'public', 'img'), { recursive: true });
writeFileSync(join(DEST, 'public', 'img', '.gitkeep'), '');
writeFileSync(join(DEST, 'scripts', 'home_text.txt'), '');
cpSync(join(TPL, 'README.template.md'), join(DEST, 'README.md'));
// 배포 주소 자리표시자
const sub = (rel, from, to) => { const p = join(DEST, rel); const s = readFileSync(p, 'utf8'); if (!s.includes(from)) throw new Error(`${rel}: "${from}" 없음`); writeFileSync(p, s.split(from).join(to)); };
sub('astro.config.mjs', 'https://view-supporters-sample.vercel.app', 'https://supporters-__CLINIC_ID__.vercel.app');
sub('vercel.json', 'https://view-supporters-sample.vercel.app', 'https://supporters-__CLINIC_ID__.vercel.app');
const pkgPath = join(DEST, 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
pkg.name = 'supporters-template';
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
// 남은 병원 고유 문자열 검사
// 상호뿐 아니라 지명·건물 같은 병원 고유 사실도 잡는다. 상호만 찾으면
// "Sinnonhyeon Station Exit 3" 처럼 지명으로 적힌 사실이 그대로 템플릿에 남는다.
const CLINIC_RE = /뷰성형외과|viewclinic|ViewclinicKR|안성민|o2oteam|View Plastic|Sinnonhyeon|Bongeunsa|Gimpo|신논현|봉은사|19-storey/;
const leaks = [];
const walk = (d) => { for (const f of readdirSync(d, { withFileTypes: true })) { const p = join(d, f.name); if (f.isDirectory()) { if (f.name !== 'node_modules') walk(p); } else if (/\.(astro|ts|mjs|json|css)$/.test(f.name) && !p.endsWith('export_template.mjs') && !p.includes('/scripts/gate/fixtures/') && CLINIC_RE.test(readFileSync(p, 'utf8'))) leaks.push(relative(DEST, p)); } };
walk(DEST);
// 문장을 그리는 코드에 병원 고유 사실이 박히면 다른 병원 화면에 틀린 말이 나가므로 내보내기를 멈춘다.
// 데이터(src/data)는 워커가 병원마다 갈아끼우고 빌드 도구(scripts)는 고객이 보지 않으므로 알림만 한다.
const RENDER_RE = /^src\/(pages|layouts|components|lib)\//;
const blocking = leaks.filter((f) => RENDER_RE.test(f));
const notes = leaks.filter((f) => !RENDER_RE.test(f));
if (notes.length) console.log('참고 · 병원 이름이 남은 파일(고객 화면 아님):\n ' + notes.join('\n '));
if (blocking.length) { console.error('고객 화면 코드에 병원 고유 사실이 남았습니다. 데이터에서 읽도록 고치세요:\n ' + blocking.join('\n ')); process.exit(1); }
console.log(`템플릿 내보내기 완료: ${DEST}${existsSync(join(DEST, 'node_modules')) ? '' : ' (npm ci 필요)'}`);