Files
ai_platform/scripts/merge-orphan-ai-success-stories.js
dsyoon 073a8343dd feat: xavis ai_platform 기능 이전 및 ncue 환경 전환
xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영.
@xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:27:48 +09:00

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();