#!/usr/bin/env node /** * data/ai-success-stories.json 에는 없는데 data/ai-success-stories/*.md 만 남은 경우( Git pull로 json만 예전으로 돌아간 경우 등 ) * 본문 파일을 스캔해 메타 항목을 자동 추가합니다. * * 사용: 프로젝트 루트에서 * node scripts/merge-orphan-ai-success-stories.js * * 동작: json 백업(data/ai-success-stories.json.bak.<시간>) 후, 등록되지 않은 .md 마다 항목 추가. * 제목은 md 첫 줄의 # 제목을 쓰고, 없으면 슬러그에서 추정한 임시 제목. * pdfUrl은 비어 있음 → 관리자 /ai-cases/write 에서 해당 사례 편집 후 PDF 경로를 다시 넣으세요. */ const path = require("path"); const fs = require("fs"); const ROOT = path.join(__dirname, ".."); const DATA_DIR = path.join(ROOT, "data"); const META_PATH = path.join(DATA_DIR, "ai-success-stories.json"); const CONTENT_DIR = path.join(DATA_DIR, "ai-success-stories"); function parseSlugFromMdName(basename) { const noExt = basename.replace(/\.md$/i, ""); const m = noExt.match(/^(.+)-(\d{10,20})$/); if (m) return m[1].toLowerCase(); return noExt.toLowerCase().replace(/[^a-z0-9\-]/g, "") || "story"; } function firstHeadingOrTitle(md, slug) { const lines = md.split(/\r?\n/); for (const line of lines) { const t = line.trim(); if (t.startsWith("# ")) return t.slice(2).trim(); } return ( slug .split("-") .filter(Boolean) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" ") || "사례 (제목 확인)" ); } function excerptFromMd(md) { const lines = md.split(/\r?\n/).map((l) => l.trim()); const paras = []; for (const line of lines) { if (!line || line.startsWith("#")) continue; if (line.startsWith("```")) break; paras.push(line); if (paras.join(" ").length > 200) break; } const e = paras.join(" ").slice(0, 280); return e || ""; } function main() { if (!fs.existsSync(META_PATH)) { console.error("없음:", META_PATH); process.exit(1); } if (!fs.existsSync(CONTENT_DIR)) { console.error("없음:", CONTENT_DIR); process.exit(1); } const raw = fs.readFileSync(META_PATH, "utf8"); const meta = JSON.parse(raw); if (!Array.isArray(meta)) { console.error("ai-success-stories.json 형식이 배열이 아닙니다."); process.exit(1); } const used = new Set(meta.map((m) => (m.contentFile || "").split("/").pop()).filter(Boolean)); const files = fs.readdirSync(CONTENT_DIR).filter((f) => /\.md$/i.test(f)); const orphans = files.filter((f) => !used.has(f)); if (orphans.length === 0) { console.log("추가할 고아 .md 없음. 종료."); return; } const bak = `${META_PATH}.bak.${Date.now()}`; fs.copyFileSync(META_PATH, bak); console.log("백업:", bak); for (const file of orphans) { const full = path.join(CONTENT_DIR, file); const md = fs.readFileSync(full, "utf8"); const slug = parseSlugFromMdName(file); if (meta.some((m) => m.slug === slug)) { console.warn("슬러그 충돌 건너뜀 (이미 다른 항목에 동일 slug):", slug, file); continue; } const title = firstHeadingOrTitle(md, slug); const excerpt = excerptFromMd(md) || title.slice(0, 140); const tsMatch = file.match(/-(\d{10,20})\.md$/i); const id = tsMatch ? `story-${tsMatch[1]}` : `story-${Date.now()}`; const row = { id, slug, title, excerpt, author: "", department: "", publishedAt: new Date().toISOString().slice(0, 10), tags: [], contentFile: file, pdfUrl: "", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; meta.push(row); console.log("추가:", file, "→ slug:", slug); } fs.writeFileSync(META_PATH, JSON.stringify(meta, null, 2), "utf8"); console.log("저장 완료:", META_PATH, "총", meta.length, "건"); console.log("pdfUrl이 비어 있으면 관리자 화면에서 PDF 경로를 지정하세요."); } main();