/** * restructure-site-480111.js * ------------------------------------------------------------ * configs/site-480111_.../sitesettings/site-480111_latest.json を読み込み、 * SiteId 496626 への適用データ(site-480111_updated.json)を生成する。 * 480111自体(本番)は一切変更しない(このスクリプトはファイルの読み書きのみ、APIへは送信しない)。 * * 出力は SiteId 496626 の modify/{リクエストラベル}/ フォルダへ、496626の * 変更前スナップショット(before_site-496626_latest.json)と併せて保存する * (「修正依頼のたびに新しいフォルダを生成し、変更前後のconfigをまとめて保管する」規約)。 * * 反映内容: * フェーズB: 既存項目 Date031〜047系の連番を Date032〜048系へ1つずつ繰り下げ * フェーズC: 新規項目「解体紹介料申請」を空いた031として挿入(担当:営業) * フェーズD: 予定/実行の標準GridDesign(計38件)の末尾に備考(Class)を追記 * フェーズE: 事前申請チェックリストの見直し(Check009/010/011除外、その他追加) * フェーズF: 物件詳細1(DescriptionA)への入力ガイド追加 * フェーズG: 事前に編集済みのScripts/Styles本文を反映 * * 使い方: * node restructure-site-480111.js … modifyフォルダを自動作成 * node restructure-site-480111.js --request=既存のラベル … 既存のmodifyフォルダに書き足す * node restructure-site-480111.js [inputPath] [outputPath] … 手動でパスを指定(modify規約を使わない) * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const { findSiteDir, latestJsonPath, newModifyDir } = require("../../site-paths"); const { REPO_ROOT } = require("../../resolve-project"); const PROJECT_NAME = "新・着工要因システム"; const ENV = "production"; const SOURCE_SITE_ID = 480111; const TARGET_SITE_ID = 496626; const baseDir = path.join(REPO_ROOT, PROJECT_NAME); const configsDir = path.join(baseDir, "configs", ENV); const manualArgs = process.argv.slice(2).filter((a) => !a.startsWith("--")); const requestArg = process.argv.find((a) => a.startsWith("--request=")); const requestLabel = requestArg ? requestArg.split("=")[1] : null; const inputPath = manualArgs[0] || latestJsonPath(configsDir, SOURCE_SITE_ID) || path.join(configsDir, `site-${SOURCE_SITE_ID}_latest.json`); const siteDir = findSiteDir(configsDir, SOURCE_SITE_ID); if (!siteDir) { console.error(`[エラー] SiteId ${SOURCE_SITE_ID} のフォルダが見つかりません。先に node get-site-config.js を実行してください。`); process.exit(1); } let outputPath = manualArgs[1]; let modifyDir = null; if (!outputPath) { const targetSiteDir = findSiteDir(configsDir, TARGET_SITE_ID); if (!targetSiteDir) { console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。先に node get-site-config.js を実行してください。`); process.exit(1); } modifyDir = newModifyDir(targetSiteDir, requestLabel); outputPath = path.join(modifyDir, "site-480111_updated.json"); // 496626の変更前スナップショットも同じフォルダに保管する const beforePath = latestJsonPath(configsDir, TARGET_SITE_ID); if (beforePath && fs.existsSync(beforePath)) { fs.copyFileSync(beforePath, path.join(modifyDir, `before_site-${TARGET_SITE_ID}_latest.json`)); } } function fail(msg) { console.error(`[エラー] ${msg}`); process.exit(1); } function pad3(n) { return String(n).padStart(3, "0"); } if (!fs.existsSync(inputPath)) { fail(`入力ファイルが見つかりません: ${inputPath}\n先に node get-site-config.js を実行してください。`); } const json = JSON.parse(fs.readFileSync(inputPath, "utf-8")); const settings = json?.Response?.Data?.SiteSettings; if (!settings) fail("Response.Data.SiteSettings が見つかりません。"); const columns = settings.Columns; const editorColumnHash = settings.EditorColumnHash || {}; const sections = settings.Sections; const views = settings.Views || []; if (!Array.isArray(columns) || !Array.isArray(sections) || !Array.isArray(settings.GridColumns)) { fail("Columns / Sections / GridColumns の構造が想定と異なります。"); } const originalCounts = { columns: columns.length, sections: sections.length, gridColumns: settings.GridColumns.length, }; function columnExists(name) { return columns.some((c) => c.ColumnName === name); } // ============================================================ // フェーズA: 事前ガード // ============================================================ console.log("[フェーズA] 事前ガードを確認しています..."); if ( columnExists("Date048") || columnExists("Class048") || columnExists("Check148") || columnExists("Check018") || columnExists("ClassO") ) { fail( "Date048/Class048/Check148/Check018/ClassO のいずれかが既に存在します。" + "本スクリプトを二重実行していないか確認してください。" ); } if ((editorColumnHash["_Tab-1"] || []).includes("_Section-48")) { fail("_Section-48 が既に EditorColumnHash._Tab-1 に存在します。二重実行の可能性があります。"); } const class030Idx = columns.findIndex((c) => c.ColumnName === "Class030"); if (class030Idx === -1 || columns[class030Idx + 1]?.ColumnName !== "Date031") { fail("Class030 の直後が Date031 ではありません。想定と異なる構造のため中断します。"); } console.log(" OK"); // ============================================================ // フェーズB: 連番リネーム(降順 47→31、衝突回避) // ============================================================ console.log("[フェーズB] 連番リネームを実行しています(047→048 … 031→032)..."); for (let i = 47; i >= 31; i--) { const oldN = pad3(i); const newN = pad3(i + 1); const old100 = String(i + 100); const new100 = String(i + 101); const map = { ["Date" + oldN]: "Date" + newN, ["Date" + old100]: "Date" + new100, ["Check" + old100]: "Check" + new100, ["Class" + oldN]: "Class" + newN, ["_Section-" + i]: "_Section-" + (i + 1), }; // Columns[]: ColumnName本体 + 自己参照するGridDesign内のブラケットトークン for (const col of columns) { if (map[col.ColumnName]) col.ColumnName = map[col.ColumnName]; if (col.GridDesign) { for (const [oldTok, newTok] of Object.entries(map)) { col.GridDesign = col.GridDesign.split(`[${oldTok}]`).join(`[${newTok}]`); } } } // EditorColumnHash: 全キー(General, _Tab-1〜_Tab-5)を走査 for (const key of Object.keys(editorColumnHash)) { editorColumnHash[key] = editorColumnHash[key].map((tok) => map[tok] ?? tok); } // Sections[].Id for (const sec of sections) { if (sec.Id === i) sec.Id = i + 1; } // トップレベル GridColumns[] settings.GridColumns = settings.GridColumns.map((tok) => map[tok] ?? tok); // Views[]各要素の GridColumns[](存在する場合のみ) for (const view of views) { if (Array.isArray(view.GridColumns)) { view.GridColumns = view.GridColumns.map((tok) => map[tok] ?? tok); } } } console.log(" OK(17項目 × 4列 + セクション を繰り下げ)"); // ============================================================ // フェーズC: 新規項目「解体紹介料申請」をsuffix 031として挿入 // ============================================================ console.log("[フェーズC] 新規項目「解体紹介料申請」を挿入しています..."); const newQuad = [ { ColumnName: "Date031", LabelText: "予定", GridLabelText: "解体紹介料申請", Description: "営業", GridFormat: "Md", NoWrap: true, GridDesign: "[Date031]\n\n-----\n[Date131]", // フェーズDで標準パターンへ書き換えられる }, { ColumnName: "Date131", LabelText: "実行", GridFormat: "Md" }, { ColumnName: "Check131", LabelText: "無し" }, { ColumnName: "Class031", LabelText: "備考", FieldCss: "field-wide" }, ]; const insertColIdx = columns.findIndex((c) => c.ColumnName === "Class030") + 1; columns.splice(insertColIdx, 0, ...newQuad); const tab1 = editorColumnHash["_Tab-1"]; if (!tab1) fail("EditorColumnHash._Tab-1 が見つかりません。"); const tab1InsertIdx = tab1.indexOf("Class030") + 1; tab1.splice(tab1InsertIdx, 0, "_Section-31", "Date031", "Date131", "Check131", "Class031"); const sectionInsertIdx = sections.findIndex((s) => s.Id === 30) + 1; sections.splice(sectionInsertIdx, 0, { Id: 31, LabelText: "解体紹介料申請", AllowExpand: false, Expand: true, }); const gcInsertIdx = settings.GridColumns.indexOf("Date030") + 1; settings.GridColumns.splice(gcInsertIdx, 0, "Date031"); const salesView = views.find((v) => v.Id === 2 && Array.isArray(v.GridColumns)); if (!salesView) fail("営業タブに対応するView(Id:2, 営業担当項目)が見つかりません。"); const viewInsertIdx = salesView.GridColumns.indexOf("Date030") + 1; salesView.GridColumns.splice(viewInsertIdx, 0, "Date031"); if (typeof settings.SectionLatestId === "number") { settings.SectionLatestId = 49; } console.log(" OK(Columns 4列 / Section / GridColumns / View を追加)"); // ============================================================ // フェーズD: GridDesign一括書き換え(標準の予定/実行ペアのみ) // ============================================================ console.log("[フェーズD] GridDesignを一括書き換えしています..."); const STANDARD_RE = /^\[(Date\d{3})\]\n\n-----\n\[(Date\d{3})\]$/; let gridDesignRewritten = 0; for (const col of columns) { if (!col.GridDesign) continue; const m = col.GridDesign.match(STANDARD_RE); if (!m) continue; const [, dateTok, date100Tok] = m; const suffix = dateTok.slice(4); col.GridDesign = `[${dateTok}]\n-----\n[${date100Tok}]\n-----\n[Class${suffix}]`; gridDesignRewritten++; } console.log(` OK(${gridDesignRewritten}件を書き換え)`); // ============================================================ // フェーズE: 事前申請チェックリストの見直し // ============================================================ console.log("[フェーズE] 事前申請チェックリストを見直しています..."); const general = editorColumnHash["General"]; if (!general) fail("EditorColumnHash.General が見つかりません。"); for (const tok of ["Check009", "Check010", "Check011"]) { const idx = general.indexOf(tok); if (idx !== -1) general.splice(idx, 1); } const check017Idx = general.indexOf("Check017"); if (check017Idx === -1) fail("EditorColumnHash.General に Check017 が見つかりません。"); general.splice(check017Idx + 1, 0, "Check018", "ClassO"); const check017ColIdx = columns.findIndex((c) => c.ColumnName === "Check017"); if (check017ColIdx === -1) fail("Columns に Check017 が見つかりません。"); columns.splice(check017ColIdx + 1, 0, { ColumnName: "Check018", LabelText: "その他" }, { ColumnName: "ClassO", LabelText: "その他内容", FieldCss: "field-wide", Description: "上記に該当しない事前申請がある場合に入力してください", } ); console.log(" OK(Check009/010/011を除外、Check018/ClassOを追加。列定義自体は保持)"); // ============================================================ // フェーズF: 物件詳細1(DescriptionA)入力ガイド追加 // ============================================================ console.log("[フェーズF] DescriptionAへ入力ガイドを追加しています..."); const descA = columns.find((c) => c.ColumnName === "DescriptionA"); if (!descA) fail("Columns に DescriptionA が見つかりません。"); descA.Description = "手入力してください"; console.log(" OK"); // ============================================================ // フェーズG: Scripts/Stylesの反映(事前に編集済みの抽出ファイルを読み込む) // ============================================================ console.log("[フェーズG] 編集済みのScripts/Stylesを反映しています..."); const manifestPath = path.join(siteDir, "manifest.json"); if (!fs.existsSync(manifestPath)) fail(`manifest.jsonが見つかりません: ${manifestPath}`); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); function applyBodies(list, manifestEntries, dir, label) { let count = 0; for (const entry of manifestEntries || []) { const target = (list || []).find((x) => x.Id === entry.Id); if (!target) continue; const filePath = path.join(dir, entry.File); if (!fs.existsSync(filePath)) fail(`${label}のファイルが見つかりません: ${filePath}`); target.Body = fs.readFileSync(filePath, "utf-8"); count++; } return count; } const scriptsUpdated = applyBodies(settings.Scripts, manifest.Scripts, path.join(siteDir, "scripts"), "Scripts"); const stylesUpdated = applyBodies(settings.Styles, manifest.Styles, path.join(siteDir, "styles"), "Styles"); console.log(` OK(Scripts ${scriptsUpdated}件 / Styles ${stylesUpdated}件を反映)`); // ============================================================ // フェーズH: 書き込み前の整合性チェック // ============================================================ console.log("[フェーズH] 整合性チェックを実行しています..."); if (columns.length !== originalCounts.columns + 6) { fail(`Columns件数が想定と異なります(期待: +6, 実際: +${columns.length - originalCounts.columns})`); } if (sections.length !== originalCounts.sections + 1) { fail(`Sections件数が想定と異なります(期待: +1, 実際: +${sections.length - originalCounts.sections})`); } if (settings.GridColumns.length !== originalCounts.gridColumns + 1) { fail(`GridColumns件数が想定と異なります(期待: +1, 実際: +${settings.GridColumns.length - originalCounts.gridColumns})`); } const colNames = columns.map((c) => c.ColumnName); if (new Set(colNames).size !== colNames.length) fail("ColumnNameに重複があります。"); const sectionIds = sections.map((s) => s.Id); if (new Set(sectionIds).size !== sectionIds.length) fail("Section.Idに重複があります。"); for (const key of Object.keys(editorColumnHash)) { const arr = editorColumnHash[key]; if (new Set(arr).size !== arr.length) fail(`EditorColumnHash.${key} 内にトークンの重複があります。`); } const STANDARD_FINAL_RE = /^\[Date\d{3}\]\n-----\n\[Date\d{3}\]\n-----\n\[Class\d{3}\]$/; const finalCount = columns.filter((c) => c.GridDesign && STANDARD_FINAL_RE.test(c.GridDesign)).length; if (finalCount !== 38) { fail(`GridDesignの新パターン一致件数が38件ではありません(実際: ${finalCount}件)`); } console.log(" OK(全チェック通過)"); // ============================================================ // 書き出し // ============================================================ fs.writeFileSync(outputPath, JSON.stringify(json, null, 2), "utf-8"); console.log("\n========================================"); console.log(" 完了"); console.log("========================================"); console.log(`出力先: ${outputPath}`); console.log(` Columns : ${originalCounts.columns} → ${columns.length}`); console.log(` Sections : ${originalCounts.sections} → ${sections.length}`); console.log(` GridColumns : ${originalCounts.gridColumns} → ${settings.GridColumns.length}`); console.log(` GridDesign書き換え: ${gridDesignRewritten}件`); if (modifyDir) { console.log(`\nmodifyフォルダ: ${modifyDir}`); console.log(`次のステップ: node apply-to-site-496626.js --request=${path.basename(modifyDir)} で差分を確認してください。`); } else { console.log(`\n次のステップ: node apply-to-site-496626.js で差分を確認してください。`); }