xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영. @xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외. Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
3.9 KiB
JavaScript
123 lines
3.9 KiB
JavaScript
#!/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();
|