577 lines
28 KiB
JavaScript
577 lines
28 KiB
JavaScript
const fs = require("fs");
|
||
const path = require("path");
|
||
const child_process = require("child_process");
|
||
|
||
const ROOT = __dirname;
|
||
const CONFIG_PATH = path.join(ROOT, "config.json");
|
||
const CONFIGS_DIR = path.join(ROOT, "configs");
|
||
const DOCS_DIR = path.join(ROOT, "docs");
|
||
const SITESETTING_JSON_DIR = path.join(ROOT, "siteSettingJsons");
|
||
|
||
function ensureDir(dir) {
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
|
||
function readJson(filePath) {
|
||
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||
}
|
||
|
||
function sanitizeName(raw) {
|
||
return String(raw || "").trim()
|
||
.replace(/[\\/:*?"<>|]/g, "_")
|
||
.replace(/\s+/g, " ")
|
||
.replace(/\.+$/, "")
|
||
.slice(0, 60)
|
||
.trim();
|
||
}
|
||
|
||
function parseSiteIds(rawSiteId) {
|
||
const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(",");
|
||
return list.map((id) => String(id).trim()).filter((id) => id.length > 0);
|
||
}
|
||
|
||
function resolveSiteData(json) {
|
||
if (json?.Response?.Data) return json.Response.Data;
|
||
if (json?.Response?.Site?.Data) return json.Response.Site.Data;
|
||
if (json?.Response?.Data?.SiteSettings) return json.Response.Data;
|
||
if (json?.Response?.SiteSettings) return json.Response;
|
||
throw new Error("getsiteレスポンスの想定構造が見つかりません。Response.Data を含むJSONを渡してください。");
|
||
}
|
||
|
||
function resolveTableCellValue(row, header) {
|
||
if (row[header] != null) return row[header];
|
||
|
||
const normalized = String(header)
|
||
.replace(/\s+/g, "")
|
||
.replace(/[()]/g, "")
|
||
.replace(///g, "/");
|
||
if (row[normalized] != null) return row[normalized];
|
||
|
||
const aliasMap = {
|
||
"順番": ["No", "Order", "Index"],
|
||
"列名": ["Column", "ColumnName", "Field", "Name"],
|
||
"列名(内部)": ["Column", "ColumnName", "Field", "Name"],
|
||
"表示ラベル": ["Label", "LabelText", "DisplayName", "Title"],
|
||
"説明/入力ガイド": ["Description", "DescriptionText", "ToolTip", "InputHelpText"],
|
||
"備考": ["Remarks", "Remark", "Notes"],
|
||
"種別": ["Type", "Kind"],
|
||
"対象": ["Target", "TargetId"],
|
||
"権限値": ["Permission", "PermissionValue", "Level", "Value"],
|
||
"タブ/キー": ["Tab", "Key", "TabKey"],
|
||
"項目一覧": ["Items", "ItemList"],
|
||
"項目": ["Field", "Column", "ColumnName"],
|
||
"権限設定": ["Permission", "PermissionSetting", "Value"],
|
||
"ファイル": ["File", "Path"],
|
||
"要約": ["Summary", "Description"],
|
||
"操作": ["Operation", "Name"],
|
||
"状態の変化": ["Change", "Transition"],
|
||
"説明": ["Description", "Detail"],
|
||
"起点列": ["From", "FromColumn", "FromField"],
|
||
"参照先SiteId": ["TargetSite", "TargetSiteId", "SiteId"],
|
||
"参照先の値→コピー先": ["To", "ToColumn", "ToField"],
|
||
"JsonFormat": ["JsonFormat", "Format"],
|
||
"ラベル": ["Label", "LabelText"],
|
||
"内容": ["Content", "Value", "Body"],
|
||
};
|
||
|
||
for (const alias of aliasMap[header] || []) {
|
||
if (row[alias] != null) return row[alias];
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
function formatMarkdownTable(header, rows) {
|
||
if (!rows || rows.length === 0) return "";
|
||
const heads = header.map((h) => `| ${h} `).join("") + "|";
|
||
const sep = header.map(() => "| --- ").join("") + "|";
|
||
const body = rows
|
||
.map((row) => {
|
||
return header
|
||
.map((cell) => {
|
||
const value = resolveTableCellValue(row, cell);
|
||
return `| ${String(value).replace(/\|/g, "\\|")} `;
|
||
})
|
||
.join("") + "|";
|
||
})
|
||
.join("\n");
|
||
return `${heads}\n${sep}\n${body}\n`;
|
||
}
|
||
|
||
function inferColumnType(column) {
|
||
const name = String(column.ColumnName || column.Name || "");
|
||
if (/^(Class[A-Z]|Class\d{3}|Class)/.test(name)) return "文字列";
|
||
if (/^Num/.test(name)) return "数値";
|
||
if (/^Date/.test(name)) return "日付";
|
||
if (/^Description/.test(name)) return "説明";
|
||
if (/^Check/.test(name)) return "チェック";
|
||
if (/^Attachments?/.test(name)) return "添付";
|
||
if (/^(Owner|Manager|Assignee|CreatedBy|UpdatedBy|Updator|Users?)/.test(name)) return "ユーザー";
|
||
if (/^Status$/i.test(name)) return "状態";
|
||
if (/Id$/.test(name) && !/^Title$/i.test(name)) return "ID";
|
||
if (/Title/i.test(name)) return "タイトル";
|
||
return column.Type || column.FieldType || "不明";
|
||
}
|
||
|
||
function getArray(value) {
|
||
if (!value) return [];
|
||
return Array.isArray(value) ? value : [value];
|
||
}
|
||
|
||
function formatValue(value) {
|
||
if (value === null || value === undefined) return "";
|
||
if (typeof value === "boolean") return value ? "true" : "false";
|
||
if (Array.isArray(value)) return value.join(", ");
|
||
return String(value);
|
||
}
|
||
|
||
function findExtractionDir(siteId) {
|
||
if (!fs.existsSync(CONFIGS_DIR)) return null;
|
||
const candidates = fs.readdirSync(CONFIGS_DIR).filter((name) => {
|
||
const full = path.join(CONFIGS_DIR, name);
|
||
return fs.statSync(full).isDirectory() && name.startsWith(`site-${siteId}`);
|
||
});
|
||
return candidates[0] ? path.join(CONFIGS_DIR, candidates[0]) : null;
|
||
}
|
||
|
||
function loadManifest(siteId) {
|
||
const dir = findExtractionDir(siteId);
|
||
if (!dir) return null;
|
||
const manifestPath = path.join(dir, "manifest.json");
|
||
if (!fs.existsSync(manifestPath)) return null;
|
||
return readJson(manifestPath);
|
||
}
|
||
|
||
function getSiteJsonPath(siteId) {
|
||
const searchDirs = [SITESETTING_JSON_DIR, CONFIGS_DIR];
|
||
for (const dir of searchDirs) {
|
||
if (!fs.existsSync(dir)) continue;
|
||
const candidate = path.join(dir, `site-${siteId}_latest.json`);
|
||
if (fs.existsSync(candidate)) return candidate;
|
||
const all = fs.readdirSync(dir).filter((name) => name.startsWith(`site-${siteId}_`) && name.endsWith(".json"));
|
||
if (all.length > 0) return path.join(dir, all[0]);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function buildBasicInfoTable(data, settings) {
|
||
const rows = [
|
||
{ Key: "TenantId", Value: formatValue(data.TenantId) },
|
||
{ Key: "Title", Value: formatValue(data.Title) },
|
||
{ Key: "ReferenceType", Value: formatValue(data.ReferenceType) },
|
||
{ Key: "ParentId", Value: formatValue(data.ParentId) },
|
||
{ Key: "InheritPermission", Value: formatValue(data.InheritPermission) },
|
||
{ Key: "Publish", Value: formatValue(data.Publish) },
|
||
{ Key: "DisableCrossSearch", Value: formatValue(data.DisableCrossSearch) },
|
||
{ Key: "Creator", Value: formatValue(data.Creator) },
|
||
{ Key: "Updator", Value: formatValue(data.Updator || data.UpdatedBy) },
|
||
{ Key: "CreatedTime", Value: formatValue(data.CreatedTime) },
|
||
{ Key: "UpdatedTime", Value: formatValue(data.UpdatedTime) },
|
||
{ Key: "SiteSettings.Version", Value: formatValue(settings?.Version) },
|
||
];
|
||
return rows.map((row) => `| ${row.Key} | ${row.Value} |`).join("\n") + "\n";
|
||
}
|
||
|
||
function inferLinkDescription(link) {
|
||
const from = link.From || link.FromColumn || link.FromField || "(不明)";
|
||
const to = link.To || link.ToColumn || link.ToField || "(不明)";
|
||
return `${from} → ${to}`;
|
||
}
|
||
|
||
function scriptSummary(filePath) {
|
||
if (!fs.existsSync(filePath)) return "";
|
||
const content = fs.readFileSync(filePath, "utf-8");
|
||
const line = content.split(/\r?\n/).find((l) => l.trim().length > 0);
|
||
return line ? line.slice(0, 120) : "";
|
||
}
|
||
|
||
function generateSpecMarkdown(siteId, data, settings, manifest) {
|
||
const title = data.Title || `Site ${siteId}`;
|
||
const specLines = [];
|
||
specLines.push(`# サイト仕様書:${title}(SiteId: ${siteId})\n`);
|
||
specLines.push(`- 取得元データ: [site-${siteId}_latest.json](./site-${siteId}_latest.json)`);
|
||
specLines.push(`- 取得日時: ${formatValue(data.UpdatedTime || data.CreatedTime || "不明")}`);
|
||
specLines.push(`- 設定バージョン: ${formatValue(settings?.Version || "不明")}\n`);
|
||
|
||
specLines.push(`## 1. サイト基本情報`);
|
||
specLines.push("| 項目 | 値 |\n| --- | --- |\n" + buildBasicInfoTable(data, settings));
|
||
|
||
const permissions = settings?.Permissions || data.Permissions || [];
|
||
if (permissions.length > 0) {
|
||
specLines.push(`## 2. アクセス権限(Permissions)`);
|
||
const permRows = permissions.map((perm) => ({
|
||
Type: formatValue(perm.Type || perm.PermissionType || perm.AuthorityType),
|
||
Target: formatValue(perm.TargetId || perm.Target || perm.RoleId || perm.Value),
|
||
Level: formatValue(perm.Permission || perm.Value || perm.Level),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["種別", "対象", "権限値"], permRows));
|
||
specLines.push(`※ 権限値は環境のロール定義に依存します。`);
|
||
}
|
||
|
||
specLines.push(`## 3. 画面構成`);
|
||
const gridColumns = settings?.GridColumns || [];
|
||
if (gridColumns.length > 0) {
|
||
specLines.push(`### 3.1 一覧画面(GridColumns)`);
|
||
const gridRows = gridColumns.map((col, index) => ({
|
||
No: index + 1,
|
||
Column: formatValue(col.ColumnName || col.Column || col.Name || col.Field),
|
||
Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["順番", "列名", "表示ラベル"], gridRows));
|
||
}
|
||
|
||
const editorHash = settings?.EditorColumnHash || {};
|
||
const sections = settings?.Sections || [];
|
||
if (Object.keys(editorHash).length > 0 || sections.length > 0) {
|
||
specLines.push(`### 3.2 編集画面レイアウト(EditorColumnHash / Sections)`);
|
||
if (Object.keys(editorHash).length > 0) {
|
||
const editorRows = Object.keys(editorHash).map((tabKey) => ({
|
||
Tab: tabKey,
|
||
Items: formatValue(getArray(editorHash[tabKey]).join(", ")),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["タブ/キー", "項目一覧"], editorRows));
|
||
}
|
||
if (sections.length > 0) {
|
||
const sectionRows = sections.map((section) => ({
|
||
Id: formatValue(section.Id),
|
||
Label: formatValue(section.LabelText || section.Name || ""),
|
||
AllowExpand: formatValue(section.AllowExpand),
|
||
Expand: formatValue(section.Expand),
|
||
}));
|
||
specLines.push(`\n#### Sections`);
|
||
specLines.push(formatMarkdownTable(["Id", "ラベル", "AllowExpand", "Expand"], sectionRows));
|
||
}
|
||
}
|
||
|
||
const titleColumns = settings?.TitleColumns || [];
|
||
if (titleColumns.length > 0) {
|
||
specLines.push(`### 3.3 タイトル表示(TitleColumns / TitleSeparator)`);
|
||
specLines.push(`- タイトル項目: ${formatValue(titleColumns.join(" + "))}`);
|
||
specLines.push(`- 区切り文字: ${formatValue(settings?.TitleSeparator || "")}`);
|
||
}
|
||
|
||
const columns = settings?.Columns || [];
|
||
if (columns.length > 0) {
|
||
specLines.push(`## 4. 項目定義(Columns)`);
|
||
const colRows = columns.map((col) => ({
|
||
Column: formatValue(col.ColumnName || col.Name || col.Field || col.Id),
|
||
Type: inferColumnType(col),
|
||
Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""),
|
||
Description: formatValue(col.Description || col.ToolTip || col.InputHelpText || ""),
|
||
Remarks: [col.NoWrap ? "NoWrap" : null, col.FieldCss ? "FieldCss" : null, col.ExtendedHtmlAfterControl ? "ExtendedHtmlAfterControl" : null].filter(Boolean).join(", "),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["列名(内部)", "種別", "表示ラベル", "説明/入力ガイド", "備考"], colRows));
|
||
|
||
const extendedItems = columns.filter((col) => col.ExtendedHtmlAfterControl);
|
||
if (extendedItems.length > 0) {
|
||
specLines.push(`## 4.1 項目ごとの補足HTML(ExtendedHtmlAfterControl)`);
|
||
const extRows = extendedItems.map((col) => ({
|
||
Column: formatValue(col.ColumnName || col.Name || col.Field),
|
||
Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""),
|
||
Content: formatValue(col.ExtendedHtmlAfterControl),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["列名", "ラベル", "内容"], extRows));
|
||
}
|
||
}
|
||
|
||
const createPerm = settings?.PermissionForCreating || [];
|
||
const updatePerm = settings?.PermissionForUpdating || [];
|
||
if (createPerm.length > 0 || updatePerm.length > 0) {
|
||
specLines.push(`## 5. 作成・更新権限(フィールド単位)`);
|
||
if (createPerm.length > 0) {
|
||
specLines.push(`### PermissionForCreating`);
|
||
const createRows = createPerm.map((item) => ({
|
||
Field: formatValue(item.ColumnName || item.Field || item.Column),
|
||
Permission: formatValue(item.Required || item.Permission || item.Value),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["項目", "権限設定"], createRows));
|
||
}
|
||
if (updatePerm.length > 0) {
|
||
specLines.push(`### PermissionForUpdating`);
|
||
const updateRows = updatePerm.map((item) => ({
|
||
Field: formatValue(item.ColumnName || item.Field || item.Column),
|
||
Permission: formatValue(item.Required || item.Permission || item.Value),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["項目", "権限設定"], updateRows));
|
||
}
|
||
}
|
||
|
||
const aggregations = settings?.Aggregations || [];
|
||
if (aggregations.length > 0) {
|
||
specLines.push(`## 6. 集計設定(Aggregations)`);
|
||
const aggRows = aggregations.map((item) => ({
|
||
Id: formatValue(item.Id),
|
||
GroupBy: formatValue(item.GroupBy),
|
||
Type: formatValue(item.Type),
|
||
Target: formatValue(item.Target),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["Id", "GroupBy", "Type", "Target"], aggRows));
|
||
}
|
||
|
||
const links = settings?.Links || [];
|
||
if (links.length > 0) {
|
||
specLines.push(`## 7. 他サイト連携(Links / ルックアップ)`);
|
||
const linkRows = links.map((link) => ({
|
||
From: formatValue(link.From || link.FromColumn || link.FromField),
|
||
TargetSite: formatValue(link.SiteId || link.TargetSiteId || link.ReferenceSiteId),
|
||
To: formatValue(link.To || link.ToColumn || link.ToField),
|
||
JsonFormat: formatValue(link.JsonFormat || link.Format),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["起点列", "参照先SiteId", "参照先の値→コピー先", "JsonFormat"], linkRows));
|
||
}
|
||
|
||
const processes = manifest ? readJson(path.join(findExtractionDir(siteId), "processes.json")) : settings?.Processes || [];
|
||
if (processes.length > 0) {
|
||
specLines.push(`## 8. プロセス設定(Processes)`);
|
||
specLines.push(`- 定義ファイル: [configs/${path.relative(ROOT, path.join(findExtractionDir(siteId), "processes.json")).replace(/\\/g, "/")}]`);
|
||
const procRows = processes.map((proc) => ({
|
||
Id: formatValue(proc.Id),
|
||
Name: formatValue(proc.Name),
|
||
DisplayName: formatValue(proc.DisplayName || proc.Name),
|
||
Condition: formatValue(`${proc.CurrentStatus || ""} → ${proc.ChangedStatus || ""}`),
|
||
Action: formatValue(proc.OnClick || proc.Action || proc.Name),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["Id", "Name", "DisplayName", "実行条件(Current→Changed)", "動作"], procRows));
|
||
}
|
||
|
||
if (manifest?.Styles?.length > 0) {
|
||
specLines.push(`## 9. スタイル(Styles)`);
|
||
const styleRows = manifest.Styles.map((style) => ({
|
||
Id: formatValue(style.Id),
|
||
Title: formatValue(style.Title),
|
||
File: `[${style.File}](./${path.posix.join(path.basename(findExtractionDir(siteId)), "styles", style.File)})`,
|
||
Remarks: formatValue(style.Disabled ? "Disabled" : ""),
|
||
}));
|
||
specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "備考"], styleRows));
|
||
}
|
||
|
||
const scripts = manifest?.Scripts || [];
|
||
const serverScripts = manifest?.ServerScripts || [];
|
||
if (scripts.length > 0 || serverScripts.length > 0) {
|
||
specLines.push(`## 10. スクリプト(Scripts)/サーバースクリプト(ServerScripts)`);
|
||
if (scripts.length > 0) {
|
||
const scriptRows = scripts.map((script) => ({
|
||
Id: formatValue(script.Id),
|
||
Title: formatValue(script.Title),
|
||
File: `[${script.File}](./${path.posix.join(path.basename(findExtractionDir(siteId)), "scripts", script.File)})`,
|
||
Summary: formatValue(scriptSummary(path.join(findExtractionDir(siteId), "scripts", script.File))),
|
||
}));
|
||
specLines.push(`### Scripts`);
|
||
specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "要約"], scriptRows));
|
||
}
|
||
if (serverScripts.length > 0) {
|
||
const serverRows = serverScripts.map((script) => ({
|
||
Id: formatValue(script.Id),
|
||
Title: formatValue(script.Title),
|
||
File: `[${script.File}](./${path.posix.join(path.basename(findExtractionDir(siteId)), "serverscripts", script.File)})`,
|
||
Summary: formatValue(scriptSummary(path.join(findExtractionDir(siteId), "serverscripts", script.File))),
|
||
}));
|
||
specLines.push(`### ServerScripts`);
|
||
specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "要約"], serverRows));
|
||
}
|
||
}
|
||
|
||
const otherSettings = { ...settings };
|
||
["Permissions", "GridColumns", "EditorColumnHash", "Sections", "TitleColumns", "TitleSeparator", "Columns", "PermissionForCreating", "PermissionForUpdating", "Aggregations", "Links", "Processes", "Scripts", "Styles", "ServerScripts"].forEach((k) => delete otherSettings[k]);
|
||
const otherEntries = Object.entries(otherSettings).filter(([key, value]) => value !== null && value !== undefined && value !== "" && !(Array.isArray(value) && value.length === 0) && !(typeof value === "object" && Object.keys(value).length === 0));
|
||
if (otherEntries.length > 0) {
|
||
specLines.push(`## 11. その他設定`);
|
||
specLines.push(`| 設定キー | 値 |\n| --- | --- |`);
|
||
otherEntries.forEach(([key, value]) => {
|
||
specLines.push(`| ${key} | ${formatValue(value)} |`);
|
||
});
|
||
}
|
||
|
||
const alertRows = [];
|
||
if (settings?.Scripts?.some((script) => /API_KEY|SECRET|Bearer\s+|token|webhook|password/i.test(script.Body || ""))) {
|
||
alertRows.push("抽出されたスクリプト内に API キーやシークレット、Webhook URL、Bearerトークンなどの機密情報が含まれている可能性があります。内容を目視で確認してください。");
|
||
}
|
||
if (alertRows.length > 0) {
|
||
specLines.push(`## 気になる点(レビュー観点)`);
|
||
alertRows.forEach((line) => specLines.push(`- ${line}`));
|
||
}
|
||
|
||
return specLines.join("\n\n");
|
||
}
|
||
|
||
function generateOverviewMarkdown(siteId, data, settings, manifest) {
|
||
const title = data.Title || `Site ${siteId}`;
|
||
const sanitizedTitle = sanitizeName(title);
|
||
const overviewLines = [];
|
||
const columns = getArray(settings?.Columns || []).filter((col) => col.LabelText || col.DisplayName || col.Title);
|
||
const processes = manifest ? readJson(path.join(findExtractionDir(siteId), "processes.json")) : settings?.Processes || [];
|
||
const links = getArray(settings?.Links || []);
|
||
const guideText = [data.GridGuide, data.EditorGuide, data.CalendarGuide, data.GanttGuide, data.CrosstabGuide, data.TimeSeriesGuide, data.BurnDownGuide, data.AnalyGuide, data.KambanGuide].filter(Boolean).join("\n");
|
||
|
||
overviewLines.push(`# 概要書:${title}`);
|
||
overviewLines.push(`- この資料は業務担当者向けに、システム上の構成をもとに「何を管理し、どのような流れで使うか」を整理したものです。`);
|
||
overviewLines.push(`- 技術仕様の詳細は [./site-${siteId}_${sanitizedTitle}_spec.md](./site-${siteId}_${sanitizedTitle}_spec.md) を参照してください。\n`);
|
||
|
||
overviewLines.push(`## 1. これはどんな画面か`);
|
||
overviewLines.push(`このサイトは「${title}」を管理するための業務画面です。${data.ReferenceType || "Results"}として扱われ、登録・照会・更新の基本的な業務フローに沿って利用されます。`);
|
||
if (guideText) {
|
||
overviewLines.push(`- 画面上の案内では、${guideText.replace(/\n/g, " ").slice(0, 240)}${guideText.length > 240 ? "..." : ""}`);
|
||
}
|
||
|
||
if (columns.length > 0) {
|
||
overviewLines.push(`## 2. 主に入力・参照する情報`);
|
||
const importantFields = columns.filter((col) => {
|
||
const name = String(col.ColumnName || col.Name || "");
|
||
const label = String(col.LabelText || col.DisplayName || col.Title || "");
|
||
return /Status|ClassA|ClassB|ClassC|ClassD|ClassE|ClassF|ClassG|ClassH|ClassI|ClassJ|ClassK|ClassM|ClassN|ClassO|ClassZ|Date|Description|Attachments|Title/i.test(name + label);
|
||
});
|
||
const sourceFields = importantFields.length > 0 ? importantFields : columns;
|
||
const items = sourceFields.slice(0, 12).map((col) => ({
|
||
Field: formatValue(col.LabelText || col.DisplayName || col.Title || col.ColumnName || col.Name),
|
||
Purpose: inferOverviewPurpose(col),
|
||
}));
|
||
overviewLines.push(formatMarkdownTable(["項目", "用途"], items));
|
||
}
|
||
|
||
if (processes.length > 0) {
|
||
overviewLines.push(`## 3. 業務でよく行う操作`);
|
||
const flowRows = processes.slice(0, 12).map((proc) => ({
|
||
Operation: formatValue(proc.Name || proc.DisplayName || "プロセス"),
|
||
Change: formatValue(proc.CurrentStatus || "") + (proc.ChangedStatus ? ` → ${proc.ChangedStatus}` : ""),
|
||
Description: inferProcessMeaning(proc),
|
||
}));
|
||
overviewLines.push(formatMarkdownTable(["操作", "状態の変化", "説明"], flowRows));
|
||
}
|
||
|
||
if (links.length > 0) {
|
||
overviewLines.push(`## 4. 他の管理表とのつながり`);
|
||
links.forEach((link) => {
|
||
const targetSite = formatValue(link.SiteId || link.TargetSiteId || link.ReferenceSiteId || "関連サイト");
|
||
const desc = inferLinkMeaning(link);
|
||
overviewLines.push(`- ${targetSite} の情報を参照・反映する連携です。${desc}`);
|
||
});
|
||
}
|
||
|
||
overviewLines.push(`## 5. 利用時のポイント`);
|
||
overviewLines.push(`- まずは画面上の入力項目を確認して、必要な情報を登録します。`);
|
||
overviewLines.push(`- 進捗や状態はステータス項目で管理し、必要に応じて他サイトの情報を参照します。`);
|
||
overviewLines.push(`- 重要な更新は履歴や添付情報と合わせて確認し、引き継ぎや問い合わせに備えます。`);
|
||
|
||
return { markdown: overviewLines.join("\n\n"), sanitizedTitle };
|
||
}
|
||
|
||
function inferOverviewPurpose(column) {
|
||
const name = String(column.ColumnName || column.Name || "");
|
||
const label = String(column.LabelText || column.DisplayName || column.Title || "");
|
||
const text = `${name}${label}`;
|
||
if (/Status/i.test(text)) return "登録状況や進捗を管理する項目です。";
|
||
if (/ClassA|管理番号|管理/i.test(text)) return "案件や物件を一意に識別するための項目です。";
|
||
if (/ClassB|登録者|担当|担当者/i.test(text)) return "誰が登録・担当しているかを管理する項目です。";
|
||
if (/Date|日時|日付/i.test(text)) return "予定や実施日などの時点を管理する項目です。";
|
||
if (/Description|備考|内容|コメント/i.test(text)) return "補足情報や業務メモを入力する項目です。";
|
||
if (/Attachments|添付/i.test(text)) return "関連書類や証跡を添付するための項目です。";
|
||
if (/Title/i.test(text)) return "件名や対象名を入力する項目です。";
|
||
if (/Class[0-9]{3}|Class[0-9]{2,3}/i.test(text)) return "業務上の分類コードを入力する項目です。";
|
||
return "業務上の情報を入力・保持するための項目です。";
|
||
}
|
||
|
||
function inferProcessMeaning(proc) {
|
||
const name = String(proc.Name || proc.DisplayName || "");
|
||
const current = String(proc.CurrentStatus || "");
|
||
const changed = String(proc.ChangedStatus || "");
|
||
const text = `${name}${current}${changed}`;
|
||
if (/承認|Approve|approval/i.test(text)) return "承認・確認の流れを進める操作です。";
|
||
if (/保留|Pending|hold/i.test(text)) return "一時停止や後続作業への待ち状態を作る操作です。";
|
||
if (/取消|Cancel|cancel/i.test(text)) return "登録内容を取り消す操作です。";
|
||
if (/終了|End|complete|完了/i.test(text)) return "業務を完了状態に遷移させる操作です。";
|
||
if (/登録|Create|new/i.test(text)) return "新規登録の初期状態を作る操作です。";
|
||
if (/チェック|Check/i.test(text)) return "入力内容や整合性を確認する操作です。";
|
||
return "業務状態を遷移させるための操作です。";
|
||
}
|
||
|
||
function inferLinkMeaning(link) {
|
||
const from = String(link.From || link.FromColumn || link.FromField || "");
|
||
const to = String(link.To || link.ToColumn || link.ToField || "");
|
||
if (from && to) return `${from} と ${to} の対応を取り、関連情報として参照します。`;
|
||
return "関連するマスタや別サイトの情報を参照する連携です。";
|
||
}
|
||
|
||
function resolveRequestedSiteIds(config) {
|
||
const cliEntries = process.argv.slice(2);
|
||
if (cliEntries.length > 0) {
|
||
return cliEntries.flatMap((entry) => parseSiteIds(entry));
|
||
}
|
||
return parseSiteIds(config.SiteId);
|
||
}
|
||
|
||
function generateAllDocs() {
|
||
if (!fs.existsSync(CONFIG_PATH)) {
|
||
console.error(`[エラー] 設定ファイルが見つかりません: ${CONFIG_PATH}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const config = readJson(CONFIG_PATH);
|
||
const siteIds = resolveRequestedSiteIds(config);
|
||
if (siteIds.length === 0) {
|
||
console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。");
|
||
process.exit(1);
|
||
}
|
||
|
||
ensureDir(CONFIGS_DIR);
|
||
ensureDir(DOCS_DIR);
|
||
|
||
console.log("[INFO] まず既存サイト情報を取得・抽出します...");
|
||
try {
|
||
child_process.execFileSync("node", [path.join(ROOT, "get-site-config.js")], {
|
||
stdio: "inherit",
|
||
cwd: ROOT,
|
||
});
|
||
} catch (err) {
|
||
console.error("[エラー] get-site-config.js の実行に失敗しました。取得処理を確認してください。");
|
||
process.exit(1);
|
||
}
|
||
|
||
for (const siteId of siteIds) {
|
||
console.log(`\n==== ドキュメント生成: SiteId=${siteId} ==== `);
|
||
const siteJsonPath = getSiteJsonPath(siteId);
|
||
if (!siteJsonPath) {
|
||
console.error(`[警告] site-${siteId}_latest.json が見つかりません。スキップします。`);
|
||
continue;
|
||
}
|
||
const json = readJson(siteJsonPath);
|
||
let data;
|
||
try {
|
||
data = resolveSiteData(json);
|
||
} catch (err) {
|
||
console.error(`[エラー] ${siteJsonPath} のデータ構造を解釈できませんでした: ${err.message}`);
|
||
continue;
|
||
}
|
||
const settings = data.SiteSettings || {};
|
||
const manifest = loadManifest(siteId);
|
||
|
||
const specMarkdown = generateSpecMarkdown(siteId, data, settings, manifest);
|
||
const titleToken = sanitizeName(data.Title || `site-${siteId}`);
|
||
const specPath = path.join(DOCS_DIR, `site-${siteId}_${titleToken}_spec.md`);
|
||
const legacySpecPath = path.join(CONFIGS_DIR, `site-${siteId}_spec.md`);
|
||
fs.writeFileSync(specPath, specMarkdown, "utf-8");
|
||
fs.writeFileSync(legacySpecPath, specMarkdown, "utf-8");
|
||
console.log(`[OK] 仕様書生成: ${specPath}`);
|
||
|
||
const { markdown: overviewMarkdown, sanitizedTitle } = generateOverviewMarkdown(siteId, data, settings, manifest);
|
||
const overviewName = `site-${siteId}_${sanitizedTitle}_overview.md`;
|
||
const overviewPath = path.join(DOCS_DIR, overviewName);
|
||
fs.writeFileSync(overviewPath, overviewMarkdown, "utf-8");
|
||
console.log(`[OK] 概要書生成: ${overviewPath}`);
|
||
|
||
try {
|
||
child_process.execFileSync("node", [path.join(ROOT, "md-to-pdf.js"), overviewPath], {
|
||
stdio: "inherit",
|
||
cwd: ROOT,
|
||
});
|
||
console.log(`[OK] PDF生成完了: ${overviewPath.replace(/\.md$/i, ".pdf")}`);
|
||
} catch (err) {
|
||
console.error(`[エラー] md-to-pdf.js による PDF 生成に失敗しました: ${overviewPath}`);
|
||
console.error(err.message || err);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (require.main === module) {
|
||
generateAllDocs();
|
||
}
|