o2o-infinith-demo/templates/supporters-astro/scripts/export_template.mjs
Haewon Kam 622441e6aa feat(supporters): 템플릿화 — 병원 고유값을 데이터로 분리하고 templates/supporters-astro 내보내기
- src/data/site.json 신설(매체 이름·로고·홈 큐레이션), factSheet에 kind·areaLabel·transitShort·transitNote·reservationUrl·doctorsUrl·youtube.title 추가
- lib.ts: PENDING('확인 대기')·v()·has()·surface()·editor() 도입, 스키마는 빈 값을 빼고 생성
- 페이지·컴포넌트 15개에서 뷰성형외과 하드코딩 제거, 빈 데이터 empty-state (글 0편·영상 0건·뉴스 0건·이미지 없음)
- 검증: 28페이지 표시 텍스트·구조 지문 이전 빌드와 동일(홈 title 마침표 1건만 정정), JSON-LD 차이는 빈 sameAs 제거뿐
- scripts/export_template.mjs + scripts/template/data(빈 데이터 세트) → templates/supporters-astro (빈 데이터로 10페이지 0오류 빌드, 게이트 통과)
- 게이트 픽스처를 자체 authors로 분리해 템플릿에서도 테스트 통과, 구조 기준선의 글 상세 페이지는 사전순 첫 글로 동적 선택

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 14:07:14 +09:00

53 lines
3.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',
];
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');
// 남은 병원 고유 문자열 검사
const CLINIC_RE = /뷰성형외과|viewclinic|ViewclinicKR|안성민|o2oteam/;
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);
if (leaks.length) { console.error('병원 고유 문자열이 남은 파일:\n ' + leaks.join('\n ')); process.exit(1); }
console.log(`템플릿 내보내기 완료: ${DEST}${existsSync(join(DEST, 'node_modules')) ? '' : ' (npm ci 필요)'}`);