chore: 作業中の変更を整理しコミット(複数プロジェクト分)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kenichiro NOGI 2026-08-02 11:09:50 +09:00
parent 1c1d47e3fa
commit ed33892f08
265 changed files with 173828 additions and 1180 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# 秘密鍵ファイル(LINE WORKS Service Account等)
*.key

View File

@ -0,0 +1,245 @@
/**
* apply-9item-phase1-496626.js
* ------------------------------------------------------------
* site-496626着工要因(東京)v29項目改修のうちitem1,2,3,4,5,6,7,9
* SiteSettings構造変更Sections/Columns/EditorColumnHash/GridColumns/
* FilterColumns/Aggregations/Links/Summaries/Notificationsをまとめて反映する
* item8引越し削除+20工程番号リネームは影響範囲が大きく別スクリプトで扱う
*
* 現在のサイト全体getsiteを取得しin-memoryで変換した上でupdatesite全体更新
* で送信するScripts/Styles/ServerScripts等この改修で対象外のフィールドは
* 取得した現在値をそのまま使うため変更されない
*
* 使い方:
* node apply-9item-phase1-496626.js 差分表示のみ送信なし
* node apply-9item-phase1-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
function transform(ss) {
const log = [];
// ---- item1: ゼロエミ承認 → ゼロエミ交付申請 ----
{
const sec = ss.Sections.find((s) => s.Id === 20);
sec.LabelText = "ゼロエミ交付申請";
const col = ss.Columns.find((c) => c.ColumnName === "Date020");
col.GridLabelText = "ゼロエミ交付申請";
log.push("item1: ゼロエミ承認 -> ゼロエミ交付申請");
}
// ---- item2: 取出有無業者連絡 → 取出業者手配 ----
{
const sec = ss.Sections.find((s) => s.Id === 15);
sec.LabelText = "取出業者手配";
const col = ss.Columns.find((c) => c.ColumnName === "Date015");
col.GridLabelText = "取出業者手配";
log.push("item2: 取出有無業者連絡 -> 取出業者手配");
}
// ---- item5: 物件詳細2 → 補助金項目 ----
{
const col = ss.Columns.find((c) => c.ColumnName === "DescriptionB");
col.LabelText = "補助金項目";
log.push("item5: 物件詳細2 -> 補助金項目");
}
// ---- item6: 一覧からDateD列を非表示 ----
{
const removeDateD = (arr) => {
const idx = arr.indexOf("DateD");
if (idx >= 0) arr.splice(idx, 1);
};
removeDateD(ss.GridColumns);
ss.Views.forEach((v) => {
if (v.GridColumns) removeDateD(v.GridColumns);
});
const colDateD = ss.Columns.find((c) => c.ColumnName === "DateD");
colDateD.GridDesign = "[DateD]\n着工可能:[NumE]\n完成予定:[NumG]";
log.push("item6: DateD列をGridColumns(4箇所)から削除。GridDesignをNumG参照に更新");
}
// ---- item9: 変更・注意点等コメント(DescriptionC)を一覧列から削除 ----
{
const removeDescC = (arr) => {
const idx = arr.indexOf("DescriptionC");
if (idx >= 0) arr.splice(idx, 1);
};
removeDescC(ss.GridColumns);
ss.Views.forEach((v) => {
if (v.GridColumns) removeDescC(v.GridColumns);
});
log.push("item9: DescriptionC列をGridColumns(4箇所)から削除");
}
// ---- item7: NumE/NumF/NumG 再定義 ----
{
const numFIdx = ss.Columns.findIndex((c) => c.ColumnName === "NumF");
ss.Columns[numFIdx].LabelText = "着工可能から完成予定日数";
const numG = { ColumnName: "NumG", LabelText: "契約から完成予定", DefaultInput: "0", Unit: "日" };
ss.Columns.splice(numFIdx + 1, 0, numG);
const genIdx = ss.EditorColumnHash.General.indexOf("NumF");
ss.EditorColumnHash.General.splice(genIdx + 1, 0, "NumG");
log.push("item7: NumF LabelText変更、NumG新規追加(Columns+EditorColumnHash)");
}
// ---- item3: 着工可能日セクション(Id=6)廃止、DateAへ一本化 ----
{
// Columns: DateY, DateZ, ClassY, ClassZ を削除
ss.Columns = ss.Columns.filter(
(c) => !["DateY", "DateZ", "ClassY", "ClassZ"].includes(c.ColumnName)
);
// Sections: Id=6 を削除
ss.Sections = ss.Sections.filter((s) => s.Id !== 6);
// EditorColumnHash.General: _Section-6 と DateY/DateZ/ClassZ/ClassY を削除
// Comments/Title/Class200/Description200 はitem4の処理で一括してSection5末尾へ移設する
ss.EditorColumnHash.General = ss.EditorColumnHash.General.filter(
(t) => !["_Section-6", "DateY", "DateZ", "ClassZ", "ClassY"].includes(t)
);
// DateA を編集可能化読取専用CSSを除去
const colDateA = ss.Columns.find((c) => c.ColumnName === "DateA");
delete colDateA.ExtendedFieldCss;
// FilterColumns / Aggregations / Links / Summaries から ClassZ 関連を削除
ss.FilterColumns = ss.FilterColumns.filter((c) => c !== "ClassZ");
ss.Aggregations = ss.Aggregations.filter((a) => a.GroupBy !== "ClassZ");
ss.Links = ss.Links.filter((l) => l.ColumnName !== "ClassZ");
ss.Summaries = ss.Summaries.filter((s) => s.LinkColumn !== "ClassZ");
// Notifications Id:1着工日更新通知の監視列を DateZ -> DateA に付け替え
// DateZ削除により監視対象が消滅し、通知が永久に発火しなくなるための対応
const notif1 = ss.Notifications.find((n) => n.Id === 1);
if (notif1) {
notif1.MonitorChangesColumns = notif1.MonitorChangesColumns.map((c) =>
c === "DateZ" ? "DateA" : c
);
}
log.push(
"item3: DateY/DateZ/ClassY/ClassZ削除、Section6削除、DateA編集可能化、FilterColumns/Aggregations/Links/Summaries整理、Notification1監視列をDateA化"
);
}
// ---- item4: リードタイム関連情報セクション(Id=7)廃止、Class190-192は存続 ----
{
// Columns: Class113〜Class119 のみ削除Class190〜192は残す
const readtimeCols = ["Class113", "Class114", "Class115", "Class116", "Class117", "Class118", "Class119"];
ss.Columns = ss.Columns.filter((c) => !readtimeCols.includes(c.ColumnName));
// Sections: Id=7 を削除
ss.Sections = ss.Sections.filter((s) => s.Id !== 7);
// EditorColumnHash.General: _Section-7 と Class113〜119 を削除
ss.EditorColumnHash.General = ss.EditorColumnHash.General.filter(
(t) => !["_Section-7", ...readtimeCols].includes(t)
);
// Class190/191/192 を現在の位置から削除し、Section5末尾(DescriptionDの直後)へ移設
ss.EditorColumnHash.General = ss.EditorColumnHash.General.filter(
(t) => !["Class190", "Class191", "Class192"].includes(t)
);
const descDIdx = ss.EditorColumnHash.General.indexOf("DescriptionD");
ss.EditorColumnHash.General.splice(descDIdx + 1, 0, "Class190", "Class191", "Class192");
log.push(
"item4: Class113-119削除、Section7削除、Class190-192をSection5末尾へ移設Notifications3件は宛先存続のため変更なし"
);
}
return log;
}
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` 9項目改修 Phase1item1,2,3,4,5,6,7,9SiteSettings構造変更updatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
const log = transform(siteSettings);
log.forEach((l) => console.log(" " + l));
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "9item-revision");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_phase1_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-9item-phase1-496626.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_phase1_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,190 @@
/**
* apply-9item-phase2-496626.js
* ------------------------------------------------------------
* site-496626着工要因(東京)v29項目改修のitem8
* 引越し工程削除後続20工程の内部カラム番号一括リネーム
* SiteSettings構造変更Sections/Columns/EditorColumnHash/GridColumnsを反映する
*
* 対象: Date028(引越し)を削除しDate02904820工程 Date028047
* 対応する実績列(Date1xx)備考列(Class0xx)無しチェック列(Check1xx)Section Idも
* 全て1つずつ前へリネームする工程名LabelText/GridLabelText自体は変更しない
*
* このスクリプトはSiteSettings構造のみを変更する既存レコードのデータ
* Date029等の実際の値はこの反映だけでは移行されないデータ移行は
* migrate-process028-rename-496626.js を別途実行すること構造変更を先に
* 反映しその直後にデータ移行を実行する運用を想定している
*
* 使い方:
* node apply-9item-phase2-496626.js 差分表示のみ送信なし
* node apply-9item-phase2-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
// ---- リネームマッピング構築 ----
const REMOVE_COLUMNS = new Set(["Date028", "Date128", "Class028", "Check128"]);
const RENAME_MAP = {};
const SECTION_ID_MAP = {};
for (let n = 29; n <= 48; n++) {
const newN = n - 1;
const pad = (x) => String(x).padStart(3, "0");
RENAME_MAP["Date" + pad(n)] = "Date" + pad(newN);
RENAME_MAP["Date" + (n + 100)] = "Date" + (newN + 100);
RENAME_MAP["Class" + pad(n)] = "Class" + pad(newN);
RENAME_MAP["Check" + (n + 100)] = "Check" + (newN + 100);
SECTION_ID_MAP[n] = newN;
}
function renameGridDesign(text) {
if (!text) return text;
return text.replace(/\[([A-Za-z]+\d+)\]/g, (m, colName) => {
return RENAME_MAP[colName] ? `[${RENAME_MAP[colName]}]` : m;
});
}
function renameTokenArray(arr) {
return arr
.filter((t) => t !== "_Section-28" && !REMOVE_COLUMNS.has(t))
.map((t) => {
if (t.startsWith("_Section-")) {
const id = parseInt(t.replace("_Section-", ""), 10);
return SECTION_ID_MAP[id] ? `_Section-${SECTION_ID_MAP[id]}` : t;
}
return RENAME_MAP[t] || t;
});
}
function transform(ss) {
const log = [];
// ---- Columns ----
ss.Columns = ss.Columns.filter((c) => !REMOVE_COLUMNS.has(c.ColumnName));
ss.Columns.forEach((c) => {
if (RENAME_MAP[c.ColumnName]) {
c.ColumnName = RENAME_MAP[c.ColumnName];
}
if (c.GridDesign) {
c.GridDesign = renameGridDesign(c.GridDesign);
}
});
log.push("Columns: Date028系4項目削除、Date029〜048系80項目をリネーム(GridDesign内参照含む)");
// ---- Sections ----
ss.Sections = ss.Sections.filter((s) => s.Id !== 28);
ss.Sections.forEach((s) => {
if (SECTION_ID_MAP[s.Id]) {
s.Id = SECTION_ID_MAP[s.Id];
}
});
log.push("Sections: Id28削除、Id29〜48をId28〜47へリネーム(LabelTextは変更なし)");
// ---- EditorColumnHash5タブ全て ----
["_Tab-1", "_Tab-2", "_Tab-3", "_Tab-4", "_Tab-5"].forEach((tab) => {
if (ss.EditorColumnHash[tab]) {
const before = ss.EditorColumnHash[tab].length;
ss.EditorColumnHash[tab] = renameTokenArray(ss.EditorColumnHash[tab]);
log.push(`EditorColumnHash.${tab}: ${before}トークン -> ${ss.EditorColumnHash[tab].length}トークン`);
}
});
// ---- GridColumnsメイン + View2/5/6 ----
ss.GridColumns = renameTokenArray(ss.GridColumns);
ss.Views.forEach((v) => {
if (v.GridColumns) {
v.GridColumns = renameTokenArray(v.GridColumns);
}
});
log.push("GridColumns: メイン・View2・View5・View6の4箇所をリネーム");
return log;
}
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` 9項目改修 Phase2item8引越し削除+20工程番号リネームupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
const log = transform(siteSettings);
log.forEach((l) => console.log(" " + l));
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "9item-revision");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_phase2_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-9item-phase2-496626.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_phase2_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,127 @@
/**
* apply-desired-config-full.js
* ------------------------------------------------------------
* この環境では updatesitesettings部分更新がHTTP 200を返しても実際には
* 反映されないことを確認済みのためREADME.md注意事項参照
* Scripts/Styles/ServerScripts/Processesの反映には常にこちらを使う
*
* build-desired-config.js が生成した desired_config.json
* Scripts/Styles/ServerScripts/Processes 現在のサイト全体getsite
* 上書きマージしupdatesite全体更新で送信する
* Columns/GridColumns/Permissions等それ以外のSiteSettingsTitle等は
* 現状のまま一切変更しない
*
* 使い方:
* node build-desired-config.js site-{SiteId}_{サイト名} 先にこちらを実行
* node apply-desired-config-full.js 差分表示のみ送信なし
* node apply-desired-config-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const baseDir = path.join(__dirname, "..", "..");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffList(curList, desList, key) {
(curList || []).forEach((c) => {
const d = (desList || []).find((x) => x.Id === c.Id);
const same = d && JSON.stringify(d) === JSON.stringify(c);
console.log(` ${key} Id:${c.Id} (${c.Title || c.Name || ""}) -> 変更${!same}`);
});
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
const SITE_ID = desired.SiteId;
const siteDir = findSiteDir(path.join(baseDir, "configs"), SITE_ID);
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` Scripts/Styles/ServerScripts/Processes 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log("--- Scripts ---");
diffList(siteSettings.Scripts, desired.SiteSettings.Scripts, "Script");
console.log("--- Styles ---");
diffList(siteSettings.Styles, desired.SiteSettings.Styles, "Style");
console.log("--- ServerScripts ---");
diffList(siteSettings.ServerScripts, desired.SiteSettings.ServerScripts, "ServerScript");
console.log("他のSiteSettingsColumns/GridColumns等・Permissions・Title等は一切変更しません。");
siteSettings.Scripts = desired.SiteSettings.Scripts;
siteSettings.Styles = desired.SiteSettings.Styles;
siteSettings.ServerScripts = desired.SiteSettings.ServerScripts;
siteSettings.Processes = desired.SiteSettings.Processes;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "desired-config-full-update");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-desired-config-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,146 @@
/**
* apply-jisseki-label-fix-496626.js
* ------------------------------------------------------------
* site-496626のColumns定義のうちDate111Date148実績日37項目
* LabelTextが実行のままになっているものだけを実績に書き換える専用スクリプト
*
* 現在のサイト全体getsiteを取得し対象37項目のLabelTextだけを書き換えて
* updatesite全体更新で送信するColumns内の対象37項目以外のフィールド
* ColumnName/GridLabelText/Type/各種設定等GridColumns他のSiteSettings
* PermissionsTitle等は一切変更しない
*
* 使い方:
* node apply-jisseki-label-fix-496626.js 差分表示のみ送信なし
* node apply-jisseki-label-fix-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
// 対象: Date111〜Date148
const TARGET_COLUMNS = new Set(
Array.from({ length: 38 }, (_, i) => i + 111).map((n) => `Date${n}`)
);
const OLD_LABEL = "実行";
const NEW_LABEL = "実績";
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` Columns LabelText「実行」→「実績」書き換えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
const changed = [];
const alreadyOk = [];
const missing = [];
TARGET_COLUMNS.forEach((columnName) => {
const column = siteSettings.Columns.find((c) => c.ColumnName === columnName);
if (!column) {
missing.push(columnName);
return;
}
if (column.LabelText === OLD_LABEL) {
column.LabelText = NEW_LABEL;
changed.push(`${columnName} (${column.GridLabelText || ""})`);
} else if (column.LabelText === NEW_LABEL) {
alreadyOk.push(columnName);
} else {
console.log(` [注意] ${columnName} のLabelTextは想定外の値です: "${column.LabelText}"(変更しません)`);
}
});
console.log(`変更対象: ${changed.length}`);
changed.forEach((c) => console.log(` ${c}: 実行 -> 実績`));
if (alreadyOk.length > 0) {
console.log(`既に「実績」: ${alreadyOk.length}`);
}
if (missing.length > 0) {
console.log(`[警告] Columnsに見つからなかった項目: ${missing.join(", ")}`);
}
console.log("他のSiteSettingsColumns内の対象外フィールド・GridColumns・Scripts/Styles/ServerScripts等・Permissions・Title等は一切変更しません。");
if (changed.length === 0) {
console.log("\n変更対象がないため、送信を行いません。");
process.exit(0);
}
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "jisseki-labeltext-fix");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_labeltext_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-jisseki-label-fix-496626.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_labeltext_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,227 @@
/**
* apply-permission-grant.js
* ------------------------------------------------------------
* SiteId 496626 の権限継承先サイトInheritPermissionに対し
* UserId 579 と同じ権限をユーザー名福島 成佳に追加する
* 480111側の継承元426945には適用しない
*
* Permissionsは SiteSettings 配下ではなくサイトデータ直下の項目であり
* updatesitesettings部分更新API はPermissionsを更新対象に含まない
* 公式マニュアルで確認済みそのため本スクリプトは updatesite全体更新を使い
* 継承先サイトの現状の Title/ReferenceType/ParentId/InheritPermission/SiteSettings
* 一切変更せずそのまま再送信しPermissionsのみ新規エントリを追加した配列を送信する
*
* 注意: 公式マニュアルには updatesite のリクエストボディでPermissionsを
* 指定できるという明記が無い記載が省略されている可能性はある
* 実行前に必ずプレビュー出力を確認すること
*
* 差分プレビュー送信結果は権限継承先サイトの modify/{リクエストラベル}/ フォルダへ保存する
* 修正依頼のたびに新しいフォルダを生成する規約省略時はタイムスタンプを自動採番
*
* 使い方:
* node apply-permission-grant.js 差分表示アーティファクト生成のみ送信なし
* node apply-permission-grant.js --execute 上記に加えて実際に送信する
* node apply-permission-grant.js --request=ラベル modifyフォルダ名を明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
const { findSiteDir, latestJsonPath, newModifyDir, latestModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 496626;
const MIRROR_FROM_USER_ID = 579;
const GRANT_TO_USER_NAME = "福島 成佳";
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const sourceSitePath =
latestJsonPath(configsDir, SOURCE_SITE_ID) ||
path.join(configsDir, `site-${SOURCE_SITE_ID}_latest.json`);
const sourceSiteJson = loadJson(sourceSitePath);
const inheritPermissionId = sourceSiteJson?.Response?.Data?.InheritPermission;
if (!inheritPermissionId) {
console.error(`[エラー] ${sourceSitePath} からInheritPermissionを取得できませんでした。`);
process.exit(1);
}
const targetPath =
latestJsonPath(configsDir, inheritPermissionId) ||
path.join(configsDir, `site-${inheritPermissionId}_latest.json`);
const targetJson = loadJson(targetPath, false);
if (!targetJson) {
console.error(`[エラー] ${targetPath} が見つかりません。先に node get-site-config.js を実行してください`);
console.error(`SiteId ${SOURCE_SITE_ID} 取得時にInheritPermission追跡ロジックで自動取得されるはずです`);
process.exit(1);
}
const targetData = targetJson.Response.Data;
const currentPermissions = targetData.Permissions || [];
const targetSiteDir = findSiteDir(configsDir, inheritPermissionId);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${inheritPermissionId} のフォルダが見つかりません。`);
process.exit(1);
}
// --request指定があればそれを使う。無ければ直近のmodifyフォルダを再利用し、
// まだ無ければ新規作成する(プレビュー→実行の一連の流れを同じフォルダにまとめるため)。
const modifyDir = requestLabel
? newModifyDir(targetSiteDir, requestLabel)
: latestModifyDir(targetSiteDir) || newModifyDir(targetSiteDir);
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
console.log("========================================");
console.log(` 権限追加${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象サイト: SiteId ${inheritPermissionId}"${targetData.Title}"、SiteId ${SOURCE_SITE_ID} の権限継承先)`);
console.log("");
// UserId 579 と同じ権限エントリを抽出複数PermissionTypeを持つ場合は全て対象
const sourceEntries = currentPermissions.filter((entry) => {
const [type, id] = String(entry).split(",");
return type === "User" && Number(id) === MIRROR_FROM_USER_ID;
});
if (sourceEntries.length === 0) {
console.error(`[エラー] UserId ${MIRROR_FROM_USER_ID} のPermissionsエントリが見つかりません。`);
console.error(`現在のPermissions: ${JSON.stringify(currentPermissions)}`);
process.exit(1);
}
// 福島 成佳 のUserIdを解決
const master = loadMasterData(baseDir);
const candidateIds = resolveId(master, { Type: "User", Name: GRANT_TO_USER_NAME });
if (candidateIds.length === 0) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」が見つかりません。configs/master_users.jsonを確認してください。`);
process.exit(1);
}
if (candidateIds.length > 1) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」に一致する候補が複数あります: ${candidateIds.join(", ")}`);
process.exit(1);
}
const grantToUserId = candidateIds[0];
// 579のエントリをコピーし、ユーザーIDだけ差し替えた新規エントリを作成
const newEntries = sourceEntries.map((entry) => {
const parts = String(entry).split(",");
parts[1] = String(grantToUserId);
return parts.join(",");
});
// 既に同一エントリが存在する場合は追加しない
const entriesToAdd = newEntries.filter((e) => !currentPermissions.includes(e));
console.log(`コピー元: UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`);
console.log(` 既存エントリ: ${JSON.stringify(sourceEntries)}`);
console.log(`追加先: UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`);
console.log(` 追加予定エントリ: ${JSON.stringify(newEntries)}`);
if (entriesToAdd.length === 0) {
console.log("\n[INFO] 追加予定のエントリは既に全て存在しています。変更の必要はありません。");
process.exit(0);
}
if (entriesToAdd.length !== newEntries.length) {
console.log(`[INFO] 一部エントリは既に存在するため、実際に追加するのは ${entriesToAdd.length} 件です: ${JSON.stringify(entriesToAdd)}`);
}
const updatedPermissions = [...currentPermissions, ...entriesToAdd];
console.log("\n★注意: updatesite APIでPermissionsを更新できるという公式記載は確認できていません。");
console.log(" 実行前に必ず内容を確認し、実行後は管理画面での反映結果も確認してください。");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${inheritPermissionId}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: inheritPermissionId,
Title: targetData.Title,
ReferenceType: targetData.ReferenceType,
ParentId: targetData.ParentId,
InheritPermission: targetData.InheritPermission,
Permissions: updatedPermissions,
SiteSettings: targetData.SiteSettings, // 変更しない(現状のまま再送信)
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(変更なし。Columns ${(targetData.SiteSettings?.Columns || []).length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${inheritPermissionId}_permission_diff_${ts}.txt`);
const diffText = [
`SiteId ${inheritPermissionId} 権限追加プレビュー (${ts})`,
`URL: ${url}`,
`対象: SiteId ${SOURCE_SITE_ID} の権限継承先`,
"",
`コピー元 UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`,
` 既存エントリ: ${JSON.stringify(sourceEntries)}`,
`追加先 UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`,
` 追加エントリ: ${JSON.stringify(entriesToAdd)}`,
"",
`変更前Permissions: ${JSON.stringify(currentPermissions)}`,
`変更後Permissions: ${JSON.stringify(updatedPermissions)}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-permission-grant.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(baseDir, "configs", `site-${inheritPermissionId}_permission_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面で反映結果を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,125 @@
/**
* apply-remark-fix-496626-full.js
* ------------------------------------------------------------
* updatesitesettings部分更新で送った Script Id:4 / Style Id:2 / ServerScript Id:1
* 修正がHTTP 200成功を返すにもかかわらず実際にはDBに反映されていない
* 直後のgetsiteで確認しても内容もVer/UpdatedTimeも変化しないことが分かったため
* 確実に反映される updatesite全体更新で同じ変更を送り直す
*
* 現在のサイト全体getsiteを取得しTitle/ReferenceType/ParentId/InheritPermission/
* Permissionsはそのまま再送信SiteSettingsもScripts[Id:4]/Styles[Id:2]/
* ServerScripts[Id:1]のBodyだけをローカルファイルの内容に差し替え他は一切変更しない
*
* 使い方:
* node apply-remark-fix-496626-full.js 差分表示のみ送信なし
* node apply-remark-fix-496626-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
const scriptBody = fs.readFileSync(path.join(siteDir, "scripts", "4_new03_一覧画面自動処理.js"), "utf-8");
const styleBody = fs.readFileSync(path.join(siteDir, "styles", "2_一覧スタイル調整.css"), "utf-8");
const serverScriptBody = fs.readFileSync(path.join(siteDir, "serverscripts", "1_一覧画面 物件詳細.js"), "utf-8");
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
const script4 = siteSettings.Scripts.find((s) => s.Id === 4);
const style2 = siteSettings.Styles.find((s) => s.Id === 2);
const serverScript1 = siteSettings.ServerScripts.find((s) => s.Id === 1);
if (!script4 || !style2 || !serverScript1) {
console.error("[エラー] Script Id:4 / Style Id:2 / ServerScript Id:1 のいずれかが見つかりません。");
process.exit(1);
}
console.log("========================================");
console.log(` Script/Style/ServerScript 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`Script Id:4 現状${script4.Body.length}文字 → 変更後${scriptBody.length}文字(変更${script4.Body !== scriptBody}`);
console.log(`Style Id:2 現状${style2.Body.length}文字 → 変更後${styleBody.length}文字(変更${style2.Body !== styleBody}`);
console.log(`ServerScript Id:1 現状${serverScript1.Body.length}文字 → 変更後${serverScriptBody.length}文字(変更${serverScript1.Body !== serverScriptBody}`);
console.log("他のSiteSettingsColumns/GridColumns/Processes等・Permissions・Title等は一切変更しません。");
script4.Body = scriptBody;
style2.Body = styleBody;
serverScript1.Body = serverScriptBody;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "serverscript-remark-fix");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,154 @@
/**
* apply-remark-fix-496626.js
* ------------------------------------------------------------
* SiteId 496626 の一覧画面スクリプトId:4 new03_一覧画面自動処理
* 備考の3文字省略独自ポップアップ表示に対応した修正版へ差し替える
*
* 対象は desired_config.jsonbuild-desired-config.js
* configs/site-496626_着工要因(東京)v2/ 配下から再構成したもの
* SiteSettings.Scripts/Styles/ServerScripts/Processes をそのまま送信する
* 事前に確認した通り実際に内容が変わるのは Scripts Id:4 のみ
* 他はキー順序が変わるだけでBodyは現状と完全一致
*
* ユーザーがその場で直接適用してよいと明示的に許可した場合に限り実行する
* 一回限りの専用スクリプト.claude/skills/pleasanter-site-spec/SKILL.md
* クロスサイト構造変更節の運用に準拠
*
* 使い方:
* node apply-remark-fix-496626.js 差分表示のみ送信なし
* node apply-remark-fix-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { latestJsonPath, newModifyDir, latestModifyDir, findSiteDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
if (desired.SiteId !== SITE_ID) {
console.error(`[エラー] desired_config.json の SiteId (${desired.SiteId}) が ${SITE_ID} ではありません。`);
console.error(`先に node build-desired-config.js site-${SITE_ID}_新・着工要因(東京)v2 を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, SITE_ID);
const current = loadJson(currentPath, false);
const currentScripts = current?.Response?.Data?.SiteSettings?.Scripts || [];
const desiredScripts = desired.SiteSettings.Scripts || [];
const curScript4 = currentScripts.find((s) => s.Id === 4);
const newScript4 = desiredScripts.find((s) => s.Id === 4);
console.log("========================================");
console.log(` 一覧画面スクリプト(Id:4)の差し替え${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象: SiteId ${SITE_ID}"${current?.Response?.Data?.Title || ""}"`);
console.log("");
console.log(`Script Id:4 現状の文字数: ${curScript4 ? curScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 変更後の文字数: ${newScript4 ? newScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 以外のScripts/Styles/ServerScripts/Processesは内容変更なしキー順序のみ変わる場合あり`);
const siteDir = findSiteDir(configsDir, SITE_ID);
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "remark-truncate-fix");
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_diff_${ts}.txt`);
fs.writeFileSync(
diffOutPath,
[
`SiteId ${SITE_ID} 一覧画面スクリプト(Id:4)差し替えプレビュー (${ts})`,
"",
"--- 現状のBody ---",
curScript4 ? curScript4.Body : "(取得できず)",
"",
"--- 変更後のBody ---",
newScript4 ? newScript4.Body : "(取得できず)",
].join("\n"),
"utf-8"
);
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesitesettings`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
SiteSettings: desired.SiteSettings,
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示 ---");
console.log(
JSON.stringify(
{
...maskedBody,
SiteSettings: {
Scripts: `${desired.SiteSettings.Scripts.length}Id:4のみ内容変更`,
Styles: `${desired.SiteSettings.Styles.length}件(変更なし)`,
ServerScripts: `${desired.SiteSettings.ServerScripts.length}件(変更なし)`,
Processes: `${desired.SiteSettings.Processes.length}件(変更なし)`,
},
},
null,
2
)
);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面・一覧画面で表示を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -36,6 +36,7 @@
const fs = require("fs");
const path = require("path");
const { latestJsonPath } = require("./site-paths");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
@ -86,7 +87,7 @@ function maskApiKey(key) {
}
async function main() {
const baseDir = __dirname;
const baseDir = path.join(__dirname, "..", "..");
const configPath = path.join(baseDir, "config.json");
const desiredPath = process.argv[2] || path.join(baseDir, "desired_config.json");
@ -98,7 +99,9 @@ async function main() {
const endpoint = mode === "full" ? "updatesite" : "updatesitesettings";
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/${endpoint}`;
const latestConfigPath = path.join(baseDir, "configs", `site-${siteId}_latest.json`);
const latestConfigPath =
latestJsonPath(path.join(baseDir, "configs"), siteId) ||
path.join(baseDir, "configs", `site-${siteId}_latest.json`);
const current = loadJson(latestConfigPath, false);
const currentSiteSettings =
current?.Response?.Data?.SiteSettings ||

View File

@ -0,0 +1,211 @@
/**
* apply-to-site-496626.js
* ------------------------------------------------------------
* configs/site-480111_updated.json のSiteSettingsをSiteId 496626
* まるごと上書き適用するupdatesite / Mode:"full"
*
* 通常このプロジェクトの apply-site-config.js はドライラン専用実送信しないだが
* 本スクリプトは今回のタスクに限りユーザーが明示的に許可した専用の直接実行スクリプト
* であり--execute 指定時のみ実際にAPIへPOSTするcurlは使わない
*
* Permissionsは送信ボディに含めない496626は権限をInheritPermissionで
* 継承しているため直接の書き込み対象ではない権限変更は apply-permission-grant.js
* を参照Title/ReferenceType/ParentId/InheritPermissionは496626自身の
* 現状値configs/site-496626_latest.jsonから取得しSiteSettingsのみ
* 480111由来の変更後内容で置き換える
*
* 差分プレビュー送信結果はSiteId 496626 modify/{リクエストラベル}/ フォルダへ保存する
* restructure-site-480111.js が作成したフォルダを --request= で指定するか省略時は
* 最新のmodifyフォルダを自動的に使う
*
* 使い方:
* node apply-to-site-496626.js 差分表示アーティファクト生成のみ送信なし
* node apply-to-site-496626.js --execute 上記に加えて実際に送信する
* node apply-to-site-496626.js --request=ラベル 対象のmodifyフォルダを明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, latestJsonPath, newModifyDir } = require("./site-paths");
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffTopLevel(before, after) {
const lines = [];
const afterKeys = Object.keys(after || {});
let changed = false;
for (const key of afterKeys) {
const b = JSON.stringify(before?.[key]);
const a = JSON.stringify(after?.[key]);
if (b !== a) {
changed = true;
const bLen = Array.isArray(before?.[key]) ? `(${before[key].length}件)` : "";
const aLen = Array.isArray(after?.[key]) ? `(${after[key].length}件)` : "";
lines.push(` [変更] SiteSettings.${key} ${bLen}${aLen}`);
}
}
if (!changed) lines.push(" 差分なし");
return lines;
}
// 対象のmodifyフォルダを解決する--request指定があればそれ、無ければ最新のものを使う
const targetSiteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。先に node get-site-config.js を実行してください。`);
process.exit(1);
}
function resolveModifyDir() {
if (requestLabel) return newModifyDir(targetSiteDir, requestLabel);
const modifyRoot = path.join(targetSiteDir, "modify");
if (!fs.existsSync(modifyRoot)) {
console.error(`[エラー] modifyフォルダがまだありません。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const dirs = fs.readdirSync(modifyRoot).filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory());
if (dirs.length === 0) {
console.error(`[エラー] modifyフォルダの中身が空です: ${modifyRoot}`);
process.exit(1);
}
dirs.sort();
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
const modifyDir = resolveModifyDir();
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
const config = loadJson(path.join(baseDir, "config.json"));
const updatedPath = path.join(modifyDir, "site-480111_updated.json");
const updatedJson = loadJson(updatedPath);
const modifiedSiteSettings = updatedJson?.Response?.Data?.SiteSettings;
if (!modifiedSiteSettings) {
console.error(`[エラー] ${updatedPath} からSiteSettingsを取得できませんでした。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, TARGET_SITE_ID);
const currentJson = loadJson(currentPath, false);
const currentData = currentJson?.Response?.Data;
if (!currentData) {
console.error(`[エラー] ${currentPath} が見つかりません。先に node get-site-config.jsSiteIdに${TARGET_SITE_ID}を含めて)を実行してください。`);
process.exit(1);
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: TARGET_SITE_ID,
Title: currentData.Title,
ReferenceType: currentData.ReferenceType,
ParentId: currentData.ParentId,
InheritPermission: currentData.InheritPermission,
SiteSettings: modifiedSiteSettings,
};
console.log("========================================");
console.log(` SiteId ${TARGET_SITE_ID} への適用${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`URL : ${url}`);
console.log(`Title : ${currentData.Title}`);
console.log(`InheritPermission: ${currentData.InheritPermission}(権限は今回変更しません)`);
console.log("");
console.log("--- SiteSettings 差分496626現状 → 480111由来の変更後 ---");
const diffLines = diffTopLevel(currentData.SiteSettings, modifiedSiteSettings);
diffLines.forEach((l) => console.log(l));
console.log("");
const newItem = (modifiedSiteSettings.Columns || []).find(
(c) => c.ColumnName === "Date031" && c.GridLabelText === "解体紹介料申請"
);
console.log(`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`);
console.log(`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`);
console.log(`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`);
console.log("");
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(省略: Columns ${modifiedSiteSettings.Columns.length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_diff_${ts}.txt`);
const diffText = [
`SiteId ${TARGET_SITE_ID} 適用差分プレビュー (${ts})`,
`URL: ${url}`,
`Title: ${currentData.Title}`,
`InheritPermission: ${currentData.InheritPermission}(今回は権限変更なし)`,
"",
"--- SiteSettings差分 ---",
...diffLines,
"",
`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`,
`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`,
`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-to-site-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -87,7 +87,7 @@ function rebuildList(manifestEntries, dir, bodyKey = "Body") {
}
function main() {
const baseDir = __dirname;
const baseDir = path.join(__dirname, "..", "..");
const arg = process.argv[2];
if (!arg) {
console.error("[エラー] 対象フォルダを指定してください(例: node build-desired-config.js site-214812");

View File

@ -29,15 +29,7 @@
const fs = require("fs");
const path = require("path");
function sanitize(name) {
return (
String(name || "untitled")
.replace(/[\\/:*?"<>|]/g, "_")
.trim()
.slice(0, 60) || "untitled"
);
}
const { sanitize, ensureSiteDir, latestJsonPath } = require("./site-paths");
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@ -72,7 +64,7 @@ const GUIDE_FIELDS = [
// get-site-config.js からも呼び出せるよう関数化。
// CLI実行時直接 `node extract-site-config.js`)は末尾のブロックが引数解決を行う。
function extractSiteConfig(latestPath) {
const baseDir = __dirname;
const baseDir = path.join(__dirname, "..", "..");
if (!fs.existsSync(latestPath)) {
console.error(`[エラー] ファイルが見つかりません: ${latestPath}`);
@ -89,9 +81,8 @@ function extractSiteConfig(latestPath) {
const siteId = data.SiteId;
const settings = data.SiteSettings || {};
const dirName = `site-${siteId}_${sanitize(data.Title)}`;
const outRoot = path.join(baseDir, "configs", dirName);
ensureDir(outRoot);
const configsDir = path.join(baseDir, "configs");
const outRoot = ensureSiteDir(configsDir, siteId, data.Title);
const manifest = {
SiteId: siteId,
@ -136,14 +127,14 @@ function extractSiteConfig(latestPath) {
console.log(` Html(ガイド) : ${manifest.Html.length}`);
console.log(` Processes : ${manifest.ProcessesCount}件 → processes.json`);
console.log("\n次のステップ: 各ファイルを直接編集するか、Claudeに修正内容を指示してください。");
console.log(`修正後は次のコマンドで反映用JSONを作成できます: node build-desired-config.js ${dirName}`);
console.log(`修正後は次のコマンドで反映用JSONを作成できます: node build-desired-config.js ${path.basename(outRoot)}`);
return manifest;
}
// CLIとして直接実行された場合のみ、引数からlatestPathを解決して実行する
if (require.main === module) {
const baseDir = __dirname;
const baseDir = path.join(__dirname, "..", "..");
let latestPath = process.argv[2];
if (!latestPath) {
@ -153,7 +144,8 @@ if (require.main === module) {
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
latestPath = path.join(baseDir, "configs", `site-${config.SiteId}_latest.json`);
const resolved = latestJsonPath(path.join(baseDir, "configs"), config.SiteId);
latestPath = resolved || path.join(baseDir, "configs", `site-${config.SiteId}_latest.json`);
}
extractSiteConfig(latestPath);

View File

@ -0,0 +1,142 @@
/**
* get-master-data.js
* ------------------------------------------------------------
* Pleasanterのユーザー/組織(部署)/グループ一覧を取得し
* ./configs/ 配下に保存しますresolve-names.js が読み込む
*
* 使い方:
* node get-master-data.js
* node get-master-data.js ./config.json 設定ファイルを明示指定する場合
*
* 参照した公式マニュアル:
* - 開発者向け機能APIユーザ操作ユーザ取得全て
* https://pleasanter.org/ja/manual/api-user-get-all
* - 開発者向け機能API組織操作組織取得
* https://pleasanter.org/ja/manual/api-dept-get
* - 開発者向け機能APIグループ操作グループ取得
* https://pleasanter.org/ja/manual/api-group-get
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const TARGETS = [
{ key: "users", endpoint: "users/get", outFile: "master_users.json", label: "ユーザー", idField: "UserId" },
{ key: "depts", endpoint: "depts/get", outFile: "master_depts.json", label: "組織", idField: "DeptId" },
{ key: "groups", endpoint: "groups/get", outFile: "master_groups.json", label: "グループ", idField: "GroupId" },
];
// 一部API/api/users/get 等はページング応答TotalCount > 1回あたりのData件数を返すため、
// 全件取得できるまで Offset をずらして繰り返し取得し、Data配列を結合する。
async function fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/${target.endpoint}`;
let allData = [];
let offset = 0;
let totalCount = null;
let firstJson = null;
while (true) {
const body = {
ApiVersion: ApiVersion || "1.1",
ApiKey: ApiKey,
Offset: offset,
};
console.log(`[INFO] 取得先: ${url} (Offset: ${offset})`);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (err) {
console.error(`[エラー] ${target.label}の取得に失敗しました:`, err.message);
return false;
}
const text = await response.text();
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return false;
}
let json;
try {
json = JSON.parse(text);
} catch (err) {
console.error(`[エラー] ${target.label}のレスポンスがJSONとして解析できませんでした。`);
console.error(text);
return false;
}
if (!firstJson) firstJson = json;
const pageData = json?.Response?.Data || [];
allData = allData.concat(pageData);
totalCount = json?.Response?.TotalCount ?? allData.length;
if (pageData.length === 0 || allData.length >= totalCount) break;
offset = allData.length;
}
// ページ間でOffset起点がずれて重複が混ざる場合があるため、idFieldで重複排除する
const seen = new Set();
allData = allData.filter((item) => {
const id = item[target.idField];
if (seen.has(id)) return false;
seen.add(id);
return true;
});
// 結合済みのData配列で1件目のレスポンスを上書きし、全件入りのJSONとして保存する
const outJson = { ...firstJson, Response: { ...firstJson.Response, Data: allData } };
const outFile = path.join(outDir, target.outFile);
fs.writeFileSync(outFile, JSON.stringify(outJson, null, 2), "utf-8");
console.log(`[OK] ${target.label}: ${allData.length}/${totalCount}件 保存しました: ${outFile}`);
return true;
}
async function main() {
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
console.error("config.example.json をコピーして config.json を作成してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const { BaseUrl, ApiKey, ApiVersion } = config;
if (!BaseUrl || !ApiKey) {
console.error("[エラー] config.json に BaseUrl / ApiKey を設定してください。");
process.exit(1);
}
const outDir = path.join(path.join(__dirname, "..", ".."), "configs", "master");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const results = [];
for (const target of TARGETS) {
const ok = await fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir);
results.push({ target: target.label, ok });
}
console.log("\n==== 取得結果まとめ ====");
for (const { target, ok } of results) {
console.log(` ${target}: ${ok ? "OK" : "失敗"}`);
}
if (results.some((r) => !r.ok)) {
process.exit(1);
}
}
main();

View File

@ -32,6 +32,7 @@
const fs = require("fs");
const path = require("path");
const { extractSiteConfig } = require("./extract-site-config");
const { ensureSiteDir, sitesettingsDir } = require("./site-paths");
// config.json の SiteId数値 / カンマ区切り文字列 / 配列)を
// 個々のSiteId文字列の配列に正規化する。
@ -63,7 +64,7 @@ async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
} catch (err) {
console.error("[エラー] リクエスト送信に失敗しました:", err.message);
console.error("BaseUrlの疎通・ネットワーク設定を確認してください。");
return false;
return { ok: false };
}
const text = await response.text();
@ -71,7 +72,7 @@ async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return false;
return { ok: false };
}
let json;
@ -80,7 +81,7 @@ async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
} catch (err) {
console.error("[エラー] レスポンスがJSONとして解析できませんでした。");
console.error(text);
return false;
return { ok: false };
}
// レスポンス構造の確認用ログ(初回はここを見て構造を把握してください)
@ -89,9 +90,11 @@ async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
console.log("[INFO] Response配下のキー:", Object.keys(json.Response));
}
// 保存
const outDir = path.join(__dirname, "configs");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
// 保存: configs/site-{siteId}_{サイト名}/sitesettings/ 配下へ
const configsDir = path.join(path.join(__dirname, "..", ".."), "configs");
const title = json?.Response?.Data?.Title;
const siteDir = ensureSiteDir(configsDir, siteId, title);
const outDir = sitesettingsDir(siteDir);
const timestamp = new Date()
.toISOString()
@ -111,11 +114,15 @@ async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
console.log("\n[INFO] 続けて設定の個別ファイル抽出を実行します...");
extractSiteConfig(latestFile);
return true;
// 権限継承先の追跡: InheritPermission が自サイト以外を指している場合、
// 実際の権限はそちらのサイトにあるため、そのサイトも同様に取得する。
const inheritPermission = json?.Response?.Data?.InheritPermission;
return { ok: true, inheritPermission };
}
async function main() {
const configPath = process.argv[2] || path.join(__dirname, "config.json");
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
@ -137,22 +144,37 @@ async function main() {
process.exit(1);
}
const multiple = siteIds.length > 1;
// 権限継承先の追跡: 指定サイトの取得後、InheritPermission が自サイト以外を
// 指していれば、そのサイトも同じ要領でキューに追加して取得する
// (権限の実体は継承元サイド側にあるため)。
const queue = [...siteIds];
const visited = new Set();
const results = [];
for (const [index, siteId] of siteIds.entries()) {
if (multiple) {
console.log(`\n==== [${index + 1}/${siteIds.length}] SiteId: ${siteId} ====`);
}
const ok = await fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion });
while (queue.length > 0) {
const siteId = String(queue.shift());
if (visited.has(siteId)) continue;
visited.add(siteId);
console.log(`\n==== [${results.length + 1}] SiteId: ${siteId} ====`);
const { ok, inheritPermission } = await fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion });
results.push({ siteId, ok });
if (
ok &&
inheritPermission != null &&
String(inheritPermission) !== "0" &&
String(inheritPermission) !== siteId &&
!visited.has(String(inheritPermission))
) {
console.log(`[INFO] SiteId ${siteId} は権限をSiteId ${inheritPermission} から継承しています。継承元も取得します。`);
queue.push(String(inheritPermission));
}
}
if (multiple) {
console.log("\n==== 取得結果まとめ ====");
for (const { siteId, ok } of results) {
console.log(` SiteId ${siteId}: ${ok ? "OK" : "失敗"}`);
}
console.log("\n==== 取得結果まとめ ====");
for (const { siteId, ok } of results) {
console.log(` SiteId ${siteId}: ${ok ? "OK" : "失敗"}`);
}
console.log("\n次のステップ: latest.jsonをClaudeのチャットにアップロードして仕様書化を依頼するか、");

View File

@ -0,0 +1,185 @@
/**
* migrate-data-480111-to-496626.js
* ------------------------------------------------------------
* SiteId 480111 の全レコードを取得しSiteSettings側で行った連番リネーム
* Date031047 Date032048Date131147 Date132148
* Check131147 Check132148Class031047 Class032048
* 同じマッピングでフィールド値を付け替えた上でSiteId 496626
* bulkupsert新規作成でインポートする
*
* 除外するもの:
* - ResultId / SiteId / Ver / CreatedTime / UpdatedTime / Locked / ItemTitle
* 新サイト側で自動採番自動計算されるため
* - Comments変更履歴コメントテキスト量が多くCreatorが480111時点のユーザーIDのため
* そのまま再現すると誤解を招く可能性がある除外し必要なら別途手動で移行する
* - AttachmentsHash添付ファイルはサイトをまたいだAPI経由の単純コピーができないため対象外
* 添付がある場合はプレビュー時に警告する
*
* 使い方:
* node migrate-data-480111-to-496626.js 件数サンプル警告の表示のみ送信なし
* node migrate-data-480111-to-496626.js --execute 上記に加えて実際にbulkupsertで送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const BATCH_SIZE = 20;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function pad3(n) {
return String(n).padStart(3, "0");
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
// SiteSettings側の連番リネームと同じマッピングを構築する
const classMap = {};
const dateMap = {};
const checkMap = {};
for (let i = 31; i <= 47; i++) {
classMap[`Class${pad3(i)}`] = `Class${pad3(i + 1)}`;
dateMap[`Date${pad3(i)}`] = `Date${pad3(i + 1)}`;
dateMap[`Date${i + 100}`] = `Date${i + 101}`;
checkMap[`Check${i + 100}`] = `Check${i + 101}`;
}
function remapHash(hash, map) {
const result = {};
for (const [key, value] of Object.entries(hash || {})) {
const newKey = map[key] || key;
result[newKey] = value;
}
return result;
}
async function fetchAllRecords(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/get`;
let all = [];
let offset = 0;
let totalCount = null;
while (true) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: ApiVersion || "1.1", ApiKey, Offset: offset }),
});
const json = await res.json();
if (json.StatusCode !== 200) {
console.error(`[エラー] レコード取得に失敗しましたOffset:${offset}:`, JSON.stringify(json).slice(0, 500));
process.exit(1);
}
const page = json.Response.Data || [];
all = all.concat(page);
totalCount = json.Response.TotalCount ?? all.length;
if (page.length === 0 || all.length >= totalCount) break;
offset = all.length;
}
return all;
}
function buildMigratedRecord(record) {
return {
Body: record.Body,
Status: record.Status,
Manager: record.Manager,
Owner: record.Owner,
ClassHash: remapHash(record.ClassHash, classMap),
NumHash: record.NumHash || {},
DateHash: remapHash(record.DateHash, dateMap),
DescriptionHash: record.DescriptionHash || {},
CheckHash: remapHash(record.CheckHash, checkMap),
};
}
async function main() {
const config = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
console.log(`[INFO] SiteId ${SOURCE_SITE_ID} の全レコードを取得しています...`);
const sourceRecords = await fetchAllRecords(SOURCE_SITE_ID, config);
console.log(`[OK] ${sourceRecords.length}件取得しました。`);
const attachmentRecords = sourceRecords.filter(
(r) => r.AttachmentsHash && Object.values(r.AttachmentsHash).some((a) => Array.isArray(a) && a.length > 0)
);
const migrated = sourceRecords.map(buildMigratedRecord);
console.log("\n========================================");
console.log(` データ移行 (480111 → 496626)${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`移行対象件数: ${migrated.length}`);
if (attachmentRecords.length > 0) {
console.log(`\n⚠️ 添付ファイルを含むレコードが${attachmentRecords.length}件ありますResultId: ${attachmentRecords.map((r) => r.ResultId).join(", ")})。`);
console.log(" 添付ファイルは本スクリプトの移行対象外ですAPI経由でのサイト間コピーは非対応のため。");
console.log(" 必要な場合は管理画面から手動での再アップロードを検討してください。");
} else {
console.log("添付ファイルを含むレコードはありません。");
}
console.log("\nComments変更履歴コメントは移行対象外です。");
console.log("\n--- サンプル1件目、移行後の姿 ---");
console.log(JSON.stringify(migrated[0], null, 2));
const siteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!siteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。`);
process.exit(1);
}
// requestLabel未指定時は固定ラベルを使い、プレビュー→実行を同じフォルダにまとめる
const modifyDir = newModifyDir(siteDir, requestLabel || "data-migration");
const previewPath = path.join(modifyDir, "data_migration_preview.json");
fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8");
console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node migrate-data-480111-to-496626.js --execute");
return;
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/bulkupsert`;
const results = [];
for (let i = 0; i < migrated.length; i += BATCH_SIZE) {
const batch = migrated.slice(i, i + BATCH_SIZE);
console.log(`\n実際に送信します(${i + 1}${i + batch.length}件目 / 全${migrated.length}件)...`);
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SsCache: true,
Data: batch,
}),
});
const text = await res.text();
console.log(`HTTP ${res.status} ${res.statusText}`);
let json;
try {
json = JSON.parse(text);
console.log(json.StatusCode === 200 ? "[OK] このバッチは成功しました。" : "[エラー] このバッチは失敗した可能性があります。");
} catch {
console.log(text.slice(0, 1000));
}
results.push({ batchStart: i, status: res.status, body: text });
}
const resultPath = path.join(modifyDir, `data_migration_result_${timestamp()}.json`);
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8");
console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`);
}
main();

View File

@ -0,0 +1,208 @@
/**
* migrate-process028-rename-496626.js
* ------------------------------------------------------------
* site-496626着工要因(東京)v2item8対応
* SiteSettings側で行った番号リネームDate029048 Date028047
* Date129148 Date128147Class029048 Class028047
* Check129148 Check128147と同じマッピングで既存全レコードの
* フィールド値を同一サイト内で付け替えるupdate
*
* 旧Date028引越しの値は破棄するリネームで空くスロット
* 新Date048/Date148/Class048/Check148は明示的にクリアする
* 日付のNULL化はPleasanter仕様により "1899/12/31" を送る
* 参照: https://pleasanter.org/ja/manual/faq-api-set-date-to-null
*
* 読み取りは常に移行前の元レコードから行い書き込みは新しいハッシュ
* オブジェクトへ行うため処理順序による上書き事故は発生しない
*
* 使い方:
* node migrate-process028-rename-496626.js 件数サンプル表示のみ送信なし
* node migrate-process028-rename-496626.js --execute 上記に加えて実際にupdateで送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, latestModifyDir, newModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const BATCH_SIZE = 20;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function pad3(n) {
return String(n).padStart(3, "0");
}
// Pleasanterの「未入力」は 1899-12-30 という番兵値で返るため、年が1899以下なら未入力扱いにする
function isEmptyDate(v) {
if (!v) return true;
const d = new Date(v);
return isNaN(d.getTime()) || d.getFullYear() <= 1899;
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
// SiteSettings側のリネームと同じマッピングを構築する29〜48 → 28〜47
const dateMap = {};
const classMap = {};
const checkMap = {};
for (let n = 29; n <= 48; n++) {
const newN = n - 1;
dateMap[`Date${pad3(n)}`] = `Date${pad3(newN)}`;
dateMap[`Date${n + 100}`] = `Date${newN + 100}`;
classMap[`Class${pad3(n)}`] = `Class${pad3(newN)}`;
checkMap[`Check${n + 100}`] = `Check${newN + 100}`;
}
async function fetchAllRecords(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/get`;
let all = [];
let offset = 0;
let totalCount = null;
while (true) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: ApiVersion || "1.1", ApiKey, Offset: offset }),
});
const json = await res.json();
if (json.StatusCode !== 200) {
console.error(`[エラー] レコード取得に失敗しましたOffset:${offset}:`, JSON.stringify(json).slice(0, 500));
process.exit(1);
}
const page = json.Response.Data || [];
all = all.concat(page);
totalCount = json.Response.TotalCount ?? all.length;
if (page.length === 0 || all.length >= totalCount) break;
offset = all.length;
}
return all;
}
// 読み取りは常に元のrecordから、書き込みは新しいhashへ行う
function buildMigratedRecord(record) {
const oldDate = record.DateHash || {};
const oldClass = record.ClassHash || {};
const oldCheck = record.CheckHash || {};
const newDate = {};
const newClass = {};
const newCheck = {};
for (const [oldKey, newKey] of Object.entries(dateMap)) {
newDate[newKey] = oldDate[oldKey] || "1899/12/31";
}
for (const [oldKey, newKey] of Object.entries(classMap)) {
newClass[newKey] = oldClass[oldKey] || "";
}
for (const [oldKey, newKey] of Object.entries(checkMap)) {
newCheck[newKey] = oldCheck[oldKey] || false;
}
// 空くスロット新Date048/Date148/Class048/Check148を明示的にクリア
newDate["Date048"] = "1899/12/31";
newDate["Date148"] = "1899/12/31";
newClass["Class048"] = "";
newCheck["Check148"] = false;
return {
ResultId: record.ResultId,
DateHash: newDate,
ClassHash: newClass,
CheckHash: newCheck,
};
}
async function main() {
const config = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
console.log(`[INFO] SiteId ${SITE_ID} の全レコードを取得しています...`);
const records = await fetchAllRecords(SITE_ID, config);
console.log(`[OK] ${records.length}件取得しました。`);
const migrated = records.map(buildMigratedRecord);
const withProcess028Data = records.filter(
(r) => r.DateHash && !isEmptyDate(r.DateHash.Date028)
);
console.log("\n========================================");
console.log(` データ移行: 引越し(Date028)削除+後続20工程リネーム${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`移行対象件数: ${migrated.length}`);
if (withProcess028Data.length > 0) {
console.log(
`\n[注意] 旧「引越し」(Date028)に実績日が入っていたレコードが${withProcess028Data.length}件ありますResultId: ${withProcess028Data
.map((r) => r.ResultId)
.join(", ")}この値は破棄されます`
);
} else {
console.log("旧「引越し」(Date028)にデータが入っているレコードはありません。");
}
console.log("\n--- サンプル1件目、移行後の姿 ---");
console.log(JSON.stringify(migrated[0], null, 2));
const siteDir = findSiteDir(configsDir, SITE_ID);
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "9item-revision");
const previewPath = path.join(modifyDir, "data_migration_process028_preview.json");
fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8");
console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`);
// 突合用に移行前の生データも保存しておく
const beforePath = path.join(modifyDir, "data_migration_process028_before.json");
fs.writeFileSync(beforePath, JSON.stringify(records, null, 2), "utf-8");
console.log(`[OK] 移行前の生データを保存しました: ${beforePath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node migrate-process028-rename-496626.js --execute");
return;
}
// bulkupsertはResultIdを指定しても新規追加として扱われる事象を確認したため、
// 確実に既存レコードを更新する単体update API/api/items/{ResultId}/update
// レコードごとに個別に呼び出す方式にする。
const results = [];
let okCount = 0;
let ngCount = 0;
for (const rec of migrated) {
const { ResultId, ...body } = rec;
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${ResultId}/update`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
...body,
}),
});
const text = await res.text();
let json;
let ok = false;
try {
json = JSON.parse(text);
ok = json.StatusCode === 200;
} catch {
ok = false;
}
ok ? okCount++ : ngCount++;
console.log(`ResultId ${ResultId}: HTTP ${res.status} ${ok ? "[OK]" : "[エラー] " + text.slice(0, 300)}`);
results.push({ resultId: ResultId, status: res.status, body: text });
}
console.log(`\n成功: ${okCount}件 / 失敗: ${ngCount}`);
const resultPath = path.join(modifyDir, `data_migration_process028_result_${timestamp()}.json`);
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8");
console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`);
}
main();

View File

@ -0,0 +1,83 @@
/**
* resolve-names.js
* ------------------------------------------------------------
* get-master-data.js が保存した configs/master_users.json /
* master_depts.json / master_groups.json を読み込み
* Permissions等に登場する {Type, Id} 形式の値とユーザー/組織/グループ名との
* 相互変換を行うユーティリティ
*
* 使い方他スクリプトから:
* const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
* const master = loadMasterData();
* resolveName(master, { Type: "User", Id: 579 }); // => "福島 成佳" 等
* resolveId(master, { Type: "User", Name: "福島 成佳" }); // => [579] 一致するIdの配列
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const MASTER_FILES = {
User: "master_users.json",
Dept: "master_depts.json",
Group: "master_groups.json",
};
function readJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
console.error("先に node get-master-data.js を実行してください。");
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// configs/master/master_*.json を読み込み、Type別に { idField, nameField, list } を返す
function loadMasterData(baseDir = path.join(__dirname, "..", "..")) {
const masterDir = path.join(baseDir, "configs", "master");
const usersJson = readJson(path.join(masterDir, MASTER_FILES.User));
const deptsJson = readJson(path.join(masterDir, MASTER_FILES.Dept));
const groupsJson = readJson(path.join(masterDir, MASTER_FILES.Group));
return {
User: {
idField: "UserId",
nameField: "Name",
list: usersJson?.Response?.Data || [],
},
Dept: {
idField: "DeptId",
nameField: "DeptName",
list: deptsJson?.Response?.Data || [],
},
Group: {
idField: "GroupId",
nameField: "GroupName",
list: groupsJson?.Response?.Data || [],
},
};
}
// { Type: "User"|"Dept"|"Group", Id } -> 名前文字列(見つからなければ "(不明:Type#Id)"
function resolveName(master, { Type, Id }) {
const bucket = master[Type];
if (!bucket) return `(不明な種別:${Type})`;
const item = bucket.list.find((x) => String(x[bucket.idField]) === String(Id));
if (!item) return `(不明:${Type}#${Id})`;
return item[bucket.nameField];
}
// { Type: "User"|"Dept"|"Group", Name } -> 一致するIdの配列完全一致、複数該当もありうる
function resolveId(master, { Type, Name }) {
const bucket = master[Type];
if (!bucket) return [];
return bucket.list
.filter((x) => x[bucket.nameField] === Name)
.map((x) => x[bucket.idField]);
}
module.exports = { loadMasterData, resolveName, resolveId };

View File

@ -0,0 +1,365 @@
/**
* 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: 既存項目 Date031047系の連番を Date032048系へ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 SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
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(" OK17項目 × 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("営業タブに対応するViewId:2, 営業担当項目)が見つかりません。");
const viewInsertIdx = salesView.GridColumns.indexOf("Date030") + 1;
salesView.GridColumns.splice(viewInsertIdx, 0, "Date031");
if (typeof settings.SectionLatestId === "number") {
settings.SectionLatestId = 49;
}
console.log(" OKColumns 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(" OKCheck009/010/011を除外、Check018/ClassOを追加。列定義自体は保持");
// ============================================================
// フェーズF: 物件詳細1DescriptionA入力ガイド追加
// ============================================================
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(` OKScripts ${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 で差分を確認してください。`);
}

View File

@ -0,0 +1,105 @@
/**
* site-paths.js
* ------------------------------------------------------------
* configs/ 配下のフォルダ構成を一箇所で管理する共有ヘルパー
*
* 構成:
* configs/
* master/ ユーザー/組織/グループのマスターデータget-master-data.js
* site-{SiteId}_{サイト名}/
* sitesettings/ getsiteの生JSONsite-{SiteId}_latest.json / _{timestamp}.json
* scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
* extract-site-config.js による分割済みファイル従来通り
* docs/ 仕様書(.md)や改善要望プラン等sitesettings以外の関連ドキュメント
* modify/{request}/ 修正依頼ごとに新規作成変更前後のconfigファイルや差分結果を保管
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function sanitize(name) {
return (
String(name || "untitled")
.replace(/[\\/:*?"<>|]/g, "_")
.trim()
.slice(0, 60) || "untitled"
);
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return dir;
}
// configs/ 配下から "site-{siteId}_*"(前方一致)または "site-{siteId}"(完全一致)の
// フォルダを探す。見つからなければ null。
function findSiteDir(configsDir, siteId) {
if (!fs.existsSync(configsDir)) return null;
const bare = `site-${siteId}`;
const prefix = `site-${siteId}_`;
const candidates = fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return fs.statSync(full).isDirectory() && (name === bare || name.startsWith(prefix));
});
const suffixed = candidates.filter((n) => n !== bare);
if (suffixed.length >= 1) return path.join(configsDir, suffixed[0]);
if (candidates.includes(bare)) return path.join(configsDir, bare);
return null;
}
// 既存フォルダがあればそれを使い、無ければ site-{siteId}_{sanitize(title)} を新規作成する
function ensureSiteDir(configsDir, siteId, title) {
const existing = findSiteDir(configsDir, siteId);
if (existing) return existing;
const dir = path.join(configsDir, `site-${siteId}_${sanitize(title)}`);
return ensureDir(dir);
}
function sitesettingsDir(siteDir) {
return ensureDir(path.join(siteDir, "sitesettings"));
}
function docsDir(siteDir) {
return ensureDir(path.join(siteDir, "docs"));
}
// 修正依頼用の新規フォルダを作成するlabelを省略した場合はタイムスタンプを使う
function newModifyDir(siteDir, label) {
const name = label || new Date().toISOString().replace(/[:.]/g, "-");
return ensureDir(path.join(siteDir, "modify", name));
}
// 既存のmodifyフォルダの中で最も新しいものフォルダ名の降順ソートを返す。無ければnull。
function latestModifyDir(siteDir) {
const modifyRoot = path.join(siteDir, "modify");
if (!fs.existsSync(modifyRoot)) return null;
const dirs = fs
.readdirSync(modifyRoot)
.filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory())
.sort();
if (dirs.length === 0) return null;
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
// サイトの最新getsiteダンプsite-{siteId}_latest.jsonのパスを返す。
// フォルダ自体が無い場合は null。
function latestJsonPath(configsDir, siteId) {
const siteDir = findSiteDir(configsDir, siteId);
if (!siteDir) return null;
return path.join(siteDir, "sitesettings", `site-${siteId}_latest.json`);
}
module.exports = {
sanitize,
ensureDir,
findSiteDir,
ensureSiteDir,
sitesettingsDir,
docsDir,
newModifyDir,
latestModifyDir,
latestJsonPath,
};

View File

@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"Bash(node -c get-site-config.js)",
"Bash(node get-master-data.js)",
"Bash(node get-site-config.js)",
"WebFetch(domain:pleasanter.org)",
"Bash(node -e ' *)",
"Bash(node restructure-site-480111.js)",
"Bash(ls \"configs/site-496626_新・着工要因\\(東京\\)v2/scripts\" | grep -i apply 2>/dev/null; find \"configs/site-496626_新・着工要因\\(東京\\)v2/..\" -maxdepth 2 -iname \"*apply-desired*\" 2>/dev/null)",
"Read(//c/Users/k.nogi/#GitHub/ken_nogi/ClaudePleasanter/新・着工要因システム/configs/**)"
]
}
}

View File

@ -0,0 +1,256 @@
---
name: pleasanter-site-spec
description: Pleasanterのサイト設定JSONget-site-config.js / api-site-get の getsite レスポンスから、SiteSettingsを読み解いたMarkdown仕様書を生成し、Scripts/Styles/ServerScripts/Processesの抽出・編集・反映extract-site-config.js / build-desired-config.js / apply-site-config.jsを行う。また、ユーザー/組織/グループのマスターデータ取得get-master-data.js、別サイトへのSiteSettings丸ごと適用や項目の追加・連番リネームといった構造変更restructure-site-*.js / apply-to-site-*.js、権限Permissionsの名前指定での追加apply-permission-grant.jsにも対応する。「仕様書を作って」「サイト仕様書」「SiteSettingsをドキュメント化」「スクリプトを抜き出して」「プロセス設定を直して反映して」「別サイトに丸ごと適用して」「項目を追加して連番をズラして」「この人と同じ権限を追加して」「site-spec」等の依頼で使う。
---
# Pleasanter サイト仕様書生成・構成管理
`configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json``getsite` APIレスポンス、`Response.Data` 配下にサイト情報)を読み解き、
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` として日本語のMarkdown仕様書を出力する。
## フォルダ構成configs/ 配下)
```
configs/
master/ … ユーザー/組織/グループのマスターデータget-master-data.js
master_users.json
master_depts.json
master_groups.json
site-{SiteId}_{サイト名}/
sitesettings/ … getsiteの生JSONget-site-config.jsが保存。latest.json + タイムスタンプ付き履歴)
site-{SiteId}_latest.json
site-{SiteId}_{timestamp}.json
scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
… extract-site-config.js による分割済みファイル(従来通り)
docs/ … 仕様書(.md)や改善要望プラン等、sitesettings以外の関連ドキュメント
modify/{リクエストラベル}/ … 修正依頼のたびに新規作成。変更前後のconfigファイル・
差分プレビュー・送信結果をひとまとめに保管する
```
パス解決は `site-paths.js` に共通化されている(`findSiteDir` / `ensureSiteDir` / `sitesettingsDir` / `docsDir` /
`newModifyDir` / `latestModifyDir` / `latestJsonPath`)。新しいスクリプトを書くときはここを再利用し、
パス組み立てを重複させない。
## 入力の特定
1. ユーザーがJSONファイルパスやSiteIdを指定した場合はそれを使う。
2. 指定がなければ `configs/site-*/sitesettings/site-*_latest.json` を探す。複数ある場合はユーザーに確認する。
3. 対象サイトの `sitesettings/site-{SiteId}_latest.json` が無ければ、先に `node get-site-config.js` の実行を提案する
(エンドポイントは `/api/items/{SiteId}/getsite` を使うこと。`/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物なので注意
## 出力ファイル
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md`(既存があれば上書きしてよい)。
## 仕様書の構成(この順番・見出しレベルを踏襲する)
```markdown
# サイト仕様書:{Title}SiteId: {SiteId}
- 取得元データ: [site-{SiteId}_latest.json](../sitesettings/site-{SiteId}_latest.json)
- 取得日時: {UpdatedTime}(サイト更新日時)/設定バージョン `Ver: {Ver}`
## 1. サイト基本情報
TenantId, Title, ReferenceType, ParentId, InheritPermission, Publish,
DisableCrossSearch, Creator/Updator, CreatedTime, SiteSettings.Version を表形式で)
## 2. アクセス権限Permissions
Permissions配列を 種別/対象ID/権限レベル の表に分解。可能であれば `resolve-names.js`
`resolveName` でユーザー名・組織名・グループ名も併記する。
InheritPermissionが自サイト以外を指している場合は、権限の実体はそちらのサイトにある旨を明記し、
継承元サイトのPermissionsも取得できていれば併せて記載する。
「権限値はビットフラグの加算方式のため正確な意味は環境のロール定義に依存する」旨を注記)
## 3. 画面構成
### 3.1 一覧画面GridColumns
GridColumns配列を 順番/列名/表示ラベル の表に。ラベルはColumns配列から解決、
無ければ「項目種別の判定ルール」のデフォルト名を使う)
### 3.2 編集画面レイアウトEditorColumnHash / Sections
EditorColumnHashの各タブ配列を 順番/項目名(内部名)/ラベル名 の表に。
`_Section-N` はラベル名欄に「―(セクション「{LabelText}」開始)」と記載。
続けてSections配列を Id/ラベル/AllowExpand/Expand の表で示す)
### 3.3 タイトル表示TitleColumns / TitleSeparator
TitleColumns配列をTitleSeparatorで連結した式を明記
## 4. 項目定義Columns
Columns配列を 列名(内部)/種別/表示ラベル/説明・入力ガイド/備考 の表に。
種別は下記「項目種別の判定ルール」で推定。
備考にはNoWrap, FieldCss, ExtendedHtmlAfterControlの有無などを記載
### 4.1 項目ごとの補足HTMLExtendedHtmlAfterControl
(該当項目があれば 列名/色styleやclassから読み取れれば/内容 の表。無ければこの節は省略)
## 5. 作成・更新権限(フィールド単位)
PermissionForCreating / PermissionForUpdating を表で。無ければ「設定なし」と明記)
## 6. 集計設定Aggregations
存在する場合のみ。Id/GroupBy/Type/Target を表で)
## 7. 他サイト連携Links / ルックアップ)
(存在する場合のみ。起点列/参照先SiteId/参照先の値→自コピー先/JsonFormat を表で。
「参照先サイトの{From}列の値を、このサイトの{To}列にコピーする」という関係性を文章でも補足)
## 8. プロセス設定Processes
(存在する場合のみ。定義ファイルへのリンク ../processes.json を明記した上で、
Id/Name・DisplayName/実行条件(CurrentStatus→ChangedStatus)/動作(OnClick等)/成功メッセージ を表で)
## 9. スタイルStyles
存在する場合のみ。Id/Title/適用範囲(New/Edit/Index等のフラグ)/ファイルへのリンクを表で。
CSS本体はMarkdownに貼らず、抽出済みファイルへのリンクのみとする
## 10. スクリプトScriptsサーバースクリプトServerScripts
存在する場合のみ。Id/Title/適用画面/ファイルへのリンクを表で。
スクリプト本体はMarkdownに貼らず、抽出済みファイルへのリンクのみとする。
概要は「何をするスクリプトか」を1〜2行で要約。
本文中にAPIキー・Webhook URL・パスワード等の秘密情報らしき文字列`API_KEY`, `SECRET`, `Bearer `,
URLに埋め込まれたトークン等を見つけた場合は、値そのものをMarkdownに転記せず、
「気になる点」に必ずセキュリティ上の指摘として記載する)
## 11. その他設定
NoDisplayIfReadOnly, HideLink, SectionLatestId, 各種ガイド文の設定有無, Commentsの使用有無など
---
## 気になる点(レビュー観点)
(プレースホルダーらしき文言、権限設定の偏り、項目単位の権限の抜け、
未設定のガイド文など、気づいた点を箇条書きで)
```
## 項目種別の判定ルールColumns内 ColumnName のプレフィックスから推定)
| プレフィックス/名前 | 種別 |
|---|---|
| ClassA〜Z | 文字列(分類) |
| Class001〜040等3桁連番 | 文字列(拡張分類)。`Hide: true`の場合が多く、他項目のルックアップ先の値を裏で保持する用途が多い |
| NumA〜Z | 数値 |
| DateA〜Z | 日付 |
| DescriptionA〜Z | 説明(複数行文字列) |
| CheckA〜Z | チェックボックス |
| AttachmentsA〜Z | 添付ファイル |
| Owner, Manager, Assignee等 | ユーザー選択 |
| Status | 状態 |
| {対象}IdResultId, IssueId等 | 自動採番ID |
| Title | タイトル |
| Comments | コメント欄 |
「予定/実行/無し/備考」の4列1組パターン`DateNNN`/`Date(NNN+100)`/`Check(NNN+100)`/`ClassNNN`、NNNは3桁ゼロ埋め連番
繰り返し登場するサイトもある。この場合 `DateNNN.Description` が入力担当(部署名等)、`DateNNN.GridLabelText` が
項目の表示名を表す。詳細は下記「クロスサイト構造変更(新項目追加・連番リネーム)」を参照。
## 注意事項
- SiteSettingsに存在しない項目・空の設定例: `ClassHash`, `NumHash` などのラベル定義ハッシュが `{}`)は
「標準ラベルのまま」として仕様書に明記し、存在しないかのように省略しない。
- 表形式を基本とし、箇条書きより表を優先する(今回のレビューで「編集画面レイアウトは項目名とラベル名を併記」との
指摘を受けた経緯があるため、内部名とラベル名は必ず併記する)。
- 出力言語は日本語。
## 関連ツールScripts/Styles/ServerScripts/Processesの抽出・編集・反映
仕様書化とは別に、「スクリプトを個別ファイルに分けて」「プロセス設定を直して反映して」等の依頼では、
以下のツールチェーンを使う(詳細は `README.md` 参照)。
1. `node get-site-config.js``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` を取得し、
続けて自動的に抽出も行う(`extract-site-config.js` が内部から呼ばれ、`configs/site-{SiteId}_{サイト名}/` に以下を分割保存する。未取得なら先にこれを実行)
- `scripts/{Id}_{Title}.js`(クライアントスクリプト本体)
- `styles/{Id}_{Title}.css`(スタイル本体)
- `serverscripts/{Id}_{Title}.js`(サーバースクリプト本体)
- `html/{GuideName}.html`GridGuide等のガイドHTML。SiteSettingsではなくサイトデータ直下の項目
- `processes.json``SiteSettings.Processes` の定義ファイル、1ファイルにまとめる。分割しない
- `manifest.json`上記ファイルとId/Titleの対応表。再構成に必須
- `config.json``SiteId` はカンマ区切りで複数指定可能。また、取得したサイトの `InheritPermission`
自サイト以外を指している場合、権限の実体を持つそのサイトも自動的に追いかけて取得する(後述)。
- 抽出だけをやり直したい場合(`latest.json`は既に取得済み)は `node extract-site-config.js` を単独実行してもよい
- **注意**: `get-site-config.js` を再実行すると、その時点のサイトの生の状態で `scripts/` `styles/` 等が
上書きされる。あるサイトの内容を「別サイトへ適用するための下書き」として編集している最中に、
同じサイトへ `get-site-config.js` を再実行すると編集内容が失われるので、クロスサイト作業中は
むやみに元サイトを再取得しない(詳しくは後述の「クロスサイト構造変更」参照)。
2. ユーザーの指示に従い、`scripts/*.js` 等の中身を直接編集する(`manifest.json` のId/メタ情報は変更しない)。
新規追加の場合は `manifest.json` に対応エントリ(`Id`は既存と重複しない値、`File`名)を追加してからファイルを作成する。
3. `node build-desired-config.js site-{SiteId}` … 分割ファイルを再構成し `desired_config.json` を生成(送信なし)。
フォルダ名はサイト名まで含む正式名(`site-{SiteId}_{サイト名}`)だが、`site-{SiteId}` の前方一致指定でも
`configs/`配下から自動解決される
4. `node apply-site-config.js` … ドライラン確認差分・送信Body・curlコマンド表示のみ、送信なし
5. 内容をユーザーに確認してもらい、問題なければ表示されたcurlコマンドをユーザー自身が手動実行する
## 関連ツール:ユーザー/組織/グループのマスターデータ取得
権限Permissionsやスクリプト内の担当者振り分けなどをID表記ではなく名前で扱いたい場合に使う。
1. `node get-master-data.js``configs/master/` へ以下を取得・保存する(ページング自動対応。
`/api/users/get` はレスポンスの `TotalCount` が1回あたりの件数を超えることがあるため、
トップレベルの `Offset` パラメータで全件を取得しきるまでループする)。
- `master_users.json``UserId` / `LoginId` / `Name` / `DeptId` 等)
- `master_depts.json``DeptId` / `DeptCode` / `DeptName`
- `master_groups.json``GroupId` / `GroupName` / `GroupMembers[]` / `GroupChildren[]`
- 参照した公式マニュアル: [ユーザ取得(全て)](https://pleasanter.org/ja/manual/api-user-get-all) /
[組織取得](https://pleasanter.org/ja/manual/api-dept-get) / [グループ取得](https://pleasanter.org/ja/manual/api-group-get)
2. `resolve-names.js` をスクリプトから `require` して使う:
- `loadMasterData(baseDir)` … 3つの `master_*.json` を読み込む
- `resolveName(master, { Type: "User"|"Dept"|"Group", Id })` … IDから名前を引く
- `resolveId(master, { Type, Name })` … 名前から一致するIDの配列を引く完全一致。0件/複数件はエラーとして扱う)
## 関連ツール:クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用)
「Aサイトを土台に項目を追加してBサイトへ適用して」「この項目の後ろに新しい項目を追加して、以降の連番を
ズラして」のような依頼で使うパターン。2026-07-16に SiteId 480111 → 496626 への適用(「解体紹介料申請」項目の
追加・連番リネーム・GridDesign一括改修・改善要望3件の反映で実際に使った構成を一般化したもの。
`restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` は、その際の実例
(そのまま次回動かせるテンプレートとして残っている)。**次回別の依頼が来たら、これらをコピーして
対象サイトIDやフェーズの中身を書き換えて使う**(汎用パラメータ化はしていない。要望ごとに列の増減や
チェックリストの中身が違うため、都度スクリプトとして書き下ろす方が確実)。
### 全体の流れ
1. **マスターデータ+サイト情報の取得**: `node get-master-data.js``node get-site-config.js`
`config.json` の `SiteId` を一時的に `"元サイトId,対象サイトId"` にして実行し、終わったら単一IDへ戻す
`InheritPermission` が自サイト以外を指していれば、その継承元サイトも自動的に追いかけて取得される。
2. **抽出済みScripts/Stylesの編集**: 元サイトの `configs/site-{元SiteId}_.../scripts/*.js` 等を直接編集する。
⚠️ ここは元サイト自身の抽出フォルダなので、元サイトを `get-site-config.js` で再取得すると
編集内容が消える。編集後は速やかに restructure スクリプト(次項)を走らせて `modify/` フォルダへ
本文を取り込んでおくこと。
3. **`restructure-site-{元SiteId}.js`(新規作成)**: 元サイトの `sitesettings/latest.json` を読み込み、
構造変更列の挿入・連番リネーム・GridDesign書き換え等を行い、`manifest.json` 経由で
編集済みのScripts/Styles本文を組み込んだ上で、対象サイトの `modify/{リクエストラベル}/` フォルダへ
`site-{元SiteId}_updated.json` として書き出す(`site-paths.js` の `findSiteDir` / `newModifyDir` を使う)。
同じフォルダへ対象サイトの変更前スナップショット(`before_site-{対象SiteId}_latest.json`)も
コピーしておく。書き込み前に、想定した件数増分・重複の有無などの整合性チェックを必ず入れる
今回の実装では「フェーズA:事前ガード」「フェーズH:整合性チェック」として実施)。
4. **`apply-to-site-{対象SiteId}.js`(新規作成)**: `modify/` フォルダの `site-{元SiteId}_updated.json`
`updatesite``Mode:"full"`)で対象サイトへ送る。`Title`/`ReferenceType`/`ParentId`/`InheritPermission`は
**対象サイト自身の現状値**から取得し、`SiteSettings`のみ元サイト由来の内容で置き換える。
`Permissions`は送信ボディに含めない(対象サイトが権限を継承している場合、直接の書き込み対象ではないため。
継承有無は `InheritPermission` で判定する)。デフォルトは差分プレビュー+アーティファクト保存のみ、
`--execute` 指定時のみ実際に送信する。差分プレビュー・送信結果は同じ `modify/` フォルダへ保存する。
5. **権限を名前指定で追加する場合**: `apply-permission-grant.js` のパターンで、対象サイトの
`InheritPermission` が指す継承元サイトに対して行う(対象サイト自身の `Permissions` は空配列=継承の
ままにしておき、直接は変更しない)。`Permissions` はサイトデータ直下の項目で `"Type,Id,PermissionType"`
形式の文字列配列(例: `"User,579,15"`)。`updatesitesettings`部分更新APIはPermissionsを更新対象に
含まないことを公式マニュアルで確認済みのため、`updatesite`(全体更新)を使い、継承元サイトの
`SiteSettings` 等は一切変更せずそのまま再送信しつつ `Permissions` のみ新規エントリを追加する。
⚠️ 公式マニュアルには `updatesite` のリクエストボディで `Permissions` を指定できるという明記が
無いため、実行前に必ずプレビュー出力をユーザーに確認してもらうこと。
6. **内容確認 → 実行**: 生成された `modify/` フォルダの中身(`before_*.json` / `site-*_updated.json` /
`*_diff_*.txt`)をユーザーに提示し、レビューを受けてから `--execute` を実行する。
### 厳守事項(安全設計、クロスサイト構造変更にも適用)
- **`apply-site-config.js` は常にドライラン。Claude自身がAPIへ実送信することはしない**
`ENABLE_ACTUAL_SEND` を有効化する改修や、curlコマンドの代理実行も行わない。これは変更しない。
- 一方で `apply-to-site-{対象SiteId}.js` / `apply-permission-grant.js` のような**専用の直接実行スクリプト**は、
ユーザーがその場で明示的に「今回は直接実行してよい」と許可した場合に限り作成・実行してよい
2026-07-16の実例。これは **その依頼一回限りの例外** であり、標準運用(ドライラン+手動反映)を
恒久的に変更するものではない。次回以降も、実送信を伴うスクリプトを作る/`--execute` を走らせる前には、
改めてユーザーに差分プレビューを提示し、明示的な許可を得ること。
- `build-desired-config.js` が作る `desired_config.json``Mode:"partial"``updatesitesettings`)専用。
Scripts/Styles/ServerScripts/Processes以外のサイト設定には触れない。Columns/EditorColumnHash/Sections/
GridColumns/Permissions等の構造変更は、この節で説明した `restructure-site-*.js` パターンで行う。
- ガイドHTML`html/*.html`)は `updatesitesettings` では反映できない(サイト直下の項目のため)。
反映が必要なら `Mode:"full"``updatesite`用の別JSONが要ることをユーザーに伝える。自動生成はしない。
- 抽出したスクリプト・サーバースクリプトの中に **APIキーやWebhook URL等の秘密情報がハードコードされている場合がある**
(実例あり)。抽出・編集作業でこれらの値を見つけたら、チャット上にそのまま貼り付けず、ユーザーに直接ファイルを確認してもらう。
また `configs/` フォルダと `config.json``.gitignore` 済みだが、誤ってコミットしないよう注意喚起する。
- 権限変更・別サイトへの丸ごと上書きはブラスト半径が大きい。実行前に必ず差分プレビューを提示し、
対象サイトIDや継承関係`InheritPermission`)の理解が誤っていないか、実行のたびに確認する。

View File

@ -0,0 +1,9 @@
# Pleasanter API接続情報実APIキーを含む
config.json
# サイト構成の取得結果・抽出結果(サイトのスクリプト等に秘密情報が
# ハードコードされている場合があるため、フォルダごと除外)
configs/
# 反映用に生成される希望構成(機密を含むスクリプト等を含み得る)
desired_config.json

View File

@ -0,0 +1,3 @@
{
"markdown-pdf.executablePath": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
}

View File

@ -0,0 +1,115 @@
# プリザンター サイト構成 取得・レビュー・反映ツール
## 全体の流れ
```
① node get-site-config.js
→ configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json に構成を保存
→ 続けて自動的に extract-site-config.js が実行され、
Scripts / Styles / ServerScripts / Processes / ガイドHTML が
configs/site-{SiteId}_{サイト名}/ 配下に個別ファイルとして分割保存される
→ config.json の SiteId をカンマ区切りにすると、複数サイトをまとめて①の処理でループ取得する
→ 取得したサイトの InheritPermission が自サイト以外を指していれば、権限の実体を持つ
そのサイトも自動的に追いかけて取得する
② その latest.json をClaudeのチャットにアップロード
→ Claudeが構成をレビュー・仕様書化configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md
③ configs/site-{SiteId}_{サイト名}/ 配下のファイルを直接編集、
またはClaudeに「このスクリプトを◯◯に直して」等と修正を指示
④ node build-desired-config.js site-{SiteId}
→ 分割ファイルを再構成し、desired_config.json を生成(送信は行わない)
⑤ node apply-site-config.js
→ 実際には送信せず、差分・送信予定Body・curlコマンドのみ表示常にドライラン
⑥ 内容を確認し、問題なければ表示されたcurlコマンドを手動実行
```
`extract-site-config.js``node get-site-config.js` の中から自動実行されるため、通常は個別に実行する必要はありません
(取得済みの `latest.json` から抽出をやり直したい場合のみ、単独で `node extract-site-config.js` を実行してください)。
上記はScripts/Styles/ServerScripts/Processesの反映フロー単一サイト内で完結です。項目の追加や連番リネーム、
別サイトへの丸ごと適用、権限の名前指定追加など、より大きな構造変更は
`.claude/skills/pleasanter-site-spec/SKILL.md` の「クロスサイト構造変更」の節を参照してください。
## 初回セットアップ
1. `config.example.json``config.json` にコピー
2. `BaseUrl` / `SiteId` / `ApiKey` を実環境の値に書き換え
- `SiteId` は単一サイトなら数値(例: `12345`)、複数サイトをまとめて取得したい場合は
カンマ区切りの文字列(例: `"12345,23456,34567"`)で指定できます
- APIキーの発行方法: https://pleasanter.org/manual/api-key
- APIキー発行にはテナント管理者権限が必要ですサイト設定の更新系APIを使う場合
3. Node.js 18以降がインストールされていることを確認組み込みfetchを使用、追加パッケージ不要
## ファイル構成
| ファイル | 役割 |
|---|---|
| `config.json` | 接続情報BaseUrl/SiteId/ApiKey※Git管理・共有厳禁 |
| `config.example.json` | 上記のテンプレート |
| `site-paths.js` | `configs/` 配下のフォルダ構成(後述)を解決する共通ヘルパー。他のスクリプトから`require`して使う |
| `get-site-config.js` | サイト構成取得(`getsite``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` に保存し、続けて `extract-site-config.js` を自動実行。`SiteId` をカンマ区切りにすると全サイトを1回の実行で順次取得。`InheritPermission` が自サイト以外を指していれば継承元サイトも自動追跡 |
| `extract-site-config.js` | `latest.json` から Scripts/Styles/ServerScripts/Processes/ガイドHTML を `configs/site-{SiteId}_{サイト名}/` に個別ファイル分割(`get-site-config.js` から自動呼び出し、または単独実行も可) |
| `build-desired-config.js` | 分割ファイルを再構成し `desired_config.json` を生成(**送信は行わない** |
| `apply-site-config.js` | 希望構成JSONとの差分表示・送信内容確認**送信は行わない** |
| `apply-desired-config-full.js` | `desired_config.json`のScripts/Styles/ServerScripts/Processesを、現状サイト全体getsiteに上書きマージし`updatesite`(全体更新)で反映する専用実行スクリプト。`updatesitesettings`部分更新がこの環境で反映されない問題下記「注意事項」参照を踏まえた標準の反映手段。Columns/GridColumns/Permissions等は現状のまま変更しない。デフォルトは差分プレビューのみ、`--execute`指定時のみ実際に送信する |
| `get-master-data.js` | ユーザー/組織/グループのマスターデータを取得し `configs/master/` へ保存(ページング自動対応) |
| `resolve-names.js` | `configs/master/` のマスターデータを使い、ID⇔名前を相互変換するユーティリティ`require`して使う) |
| `restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` | クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用・権限の名前指定追加)の実例。次回同様の依頼が来たらコピーして書き換える。詳細は `.claude/skills/pleasanter-site-spec/SKILL.md` 参照 |
| `desired_config.json` | Claude、または `build-desired-config.js` が生成する「希望構成」 |
| `configs/master/master_{users,depts,groups}.json` | ユーザー/組織/グループのマスターデータ |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` | 直近取得した現状構成Claudeへのアップロード対象 |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_{timestamp}.json` | 取得時点ごとの履歴(タイムスタンプ付き) |
| `configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` | SiteSettingsを読み解いたMarkdown仕様書。改善要望プラン等その他ドキュメントもここに置く |
| `configs/site-{SiteId}_{サイト名}/manifest.json` | 分割ファイルの一覧Id/Title/ファイル名の対応表) |
| `configs/site-{SiteId}_{サイト名}/scripts/*.js` | クライアントスクリプト本体1ファイル=1スクリプト |
| `configs/site-{SiteId}_{サイト名}/styles/*.css` | スタイル本体 |
| `configs/site-{SiteId}_{サイト名}/serverscripts/*.js` | サーバースクリプト本体 |
| `configs/site-{SiteId}_{サイト名}/html/*.html` | ガイドHTMLGridGuide等。値が空でないもののみ |
| `configs/site-{SiteId}_{サイト名}/processes.json` | プロセス設定(`SiteSettings.Processes` の定義ファイル) |
| `configs/site-{SiteId}_{サイト名}/modify/{リクエストラベル}/` | 修正依頼ごとに新規作成。変更前後のconfig・差分プレビュー・送信結果をまとめて保管 |
## Scripts/Styles/ServerScripts/Processes の反映について
- `build-desired-config.js` が生成する `desired_config.json``Mode:"partial"``updatesitesettings`)です。
Scripts / Styles / ServerScripts / Processes の4項目のみを対象とし、それ以外のサイト設定GridColumns等には触れません。
- ガイドHTML`html/*.html`はSiteSettingsではなく**サイトデータ直下**の項目のため、`updatesitesettings` では反映できません。
反映したい場合は `Mode:"full"``updatesite`)用の別の `desired_config.json` を用意し、`Title`/`ReferenceType` 等の必須項目とあわせて指定してください。
- 抽出(`extract-site-config.js`)はその時点のスナップショットです。抽出後に元サイトが他の人・画面から変更されている可能性がある場合は、
反映前に `node get-site-config.js``node extract-site-config.js` を再実行し、最新状態を取り直してから編集してください。
## 参照した公式API仕様
- サイト情報取得: `POST {BaseUrl}/api/items/{SiteId}/getsite`
https://pleasanter.org/ja/manual/api-site-get
(※ `/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物
- サイト全体更新: `POST {BaseUrl}/api/items/{SiteId}/updatesite`
https://pleasanter.org/manual/api-site-update
- サイト設定部分更新: `POST {BaseUrl}/api/items/{SiteId}/updatesitesettings`
https://pleasanter.org/ja/manual/api-update-sitesettings
- ユーザ取得(全て): `POST {BaseUrl}/api/users/get`
https://pleasanter.org/ja/manual/api-user-get-all
(※ ページングはトップレベルの `Offset` パラメータで行う。`View.Offset` ではない点に注意)
- 組織取得: `POST {BaseUrl}/api/depts/get`
https://pleasanter.org/ja/manual/api-dept-get
- グループ取得: `POST {BaseUrl}/api/groups/get`
https://pleasanter.org/ja/manual/api-group-get
## 安全設計について
- `apply-site-config.js`**常にドライラン** です。デフォルトでは実際のAPI呼び出しコード自体に到達しないようガード`ENABLE_ACTUAL_SEND = false` 固定)しています。
- 反映する際は、表示されたcurlコマンドをご自身で実行するか、内容を確認の上でスクリプトを改修してくださいClaudeが安全策なしに自動実行することはありません
- APIキーは `config.json` にのみ保持し、Claudeにアップロードする `latest.json` には含まれませんget時のレスポンスにAPIキーは含まれないため。念のためアップロード前に目視確認することをおすすめします。
- `apply-to-site-*.js` / `apply-permission-grant.js` のような、ユーザーがその場で明示的に許可した場合に限って
作成される「専用の直接実行スクリプト」は例外です(`--execute` 指定時のみ実送信)。これは依頼ごとの一回限りの
許可であり、標準運用(ドライラン+手動反映)を恒久的に変更するものではありません。
## 注意事項
- プリザンターのバージョンにより `get` レスポンスの構造(`Response.Site.SiteSettings` の位置など)が異なる場合があります。初回実行時にコンソールへ出力されるキー構造を確認してください。
- サイト設定の更新系APIは **テナント管理者権限のAPIキー** が必要です一般ユーザーのAPIキーでは403等になります
- ★重要: この環境では `updatesitesettings`(部分更新)が **HTTP 200・成功メッセージを返すにもかかわらず、実際にはScripts/Styles/ServerScriptsの内容がDBに反映されないことを確認済み**です2026-07-16、SiteId 496626で発生。直後に`getsite`し直しても内容・Ver・UpdatedTimeが変化しなかった
Scripts/Styles/ServerScriptsを実際に反映する専用スクリプトを書く場合は、`updatesitesettings`ではなく`updatesite`全体更新。Title/ReferenceType/ParentId/InheritPermission/Permissionsは現状のまま再送信し、SiteSettingsは現状を取得した上でScripts/Styles/ServerScriptsだけ差し替えるを使うこと。反映後は必ず`getsite`し直し、`UpdatedTime`が更新されているか・目的の文字列が含まれているかを確認する。

View File

@ -0,0 +1,6 @@
{
"BaseUrl": "http://your-pleasanter-server/",
"SiteId": "12345,23456",
"ApiKey": "ここに発行したAPIキーを貼り付け",
"ApiVersion": "1.1"
}

View File

@ -0,0 +1,127 @@
/**
* apply-desired-config-full.js
* ------------------------------------------------------------
* この環境では updatesitesettings部分更新がHTTP 200を返しても実際には
* 反映されないことを確認済みのためREADME.md注意事項参照
* Scripts/Styles/ServerScripts/Processesの反映には常にこちらを使う
*
* build-desired-config.js が生成した desired_config.json
* Scripts/Styles/ServerScripts/Processes 現在のサイト全体getsite
* 上書きマージしupdatesite全体更新で送信する
* Columns/GridColumns/Permissions等それ以外のSiteSettingsTitle等は
* 現状のまま一切変更しない
*
* 使い方:
* node build-desired-config.js site-{SiteId}_{サイト名} 先にこちらを実行
* node apply-desired-config-full.js 差分表示のみ送信なし
* node apply-desired-config-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const baseDir = path.join(__dirname, "..", "..");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffList(curList, desList, key) {
(curList || []).forEach((c) => {
const d = (desList || []).find((x) => x.Id === c.Id);
const same = d && JSON.stringify(d) === JSON.stringify(c);
console.log(` ${key} Id:${c.Id} (${c.Title || c.Name || ""}) -> 変更${!same}`);
});
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
const SITE_ID = desired.SiteId;
const siteDir = findSiteDir(path.join(baseDir, "configs"), SITE_ID);
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` Scripts/Styles/ServerScripts/Processes 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log("--- Scripts ---");
diffList(siteSettings.Scripts, desired.SiteSettings.Scripts, "Script");
console.log("--- Styles ---");
diffList(siteSettings.Styles, desired.SiteSettings.Styles, "Style");
console.log("--- ServerScripts ---");
diffList(siteSettings.ServerScripts, desired.SiteSettings.ServerScripts, "ServerScript");
console.log("他のSiteSettingsColumns/GridColumns等・Permissions・Title等は一切変更しません。");
siteSettings.Scripts = desired.SiteSettings.Scripts;
siteSettings.Styles = desired.SiteSettings.Styles;
siteSettings.ServerScripts = desired.SiteSettings.ServerScripts;
siteSettings.Processes = desired.SiteSettings.Processes;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "desired-config-full-update");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-desired-config-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,227 @@
/**
* apply-permission-grant.js
* ------------------------------------------------------------
* SiteId 496626 の権限継承先サイトInheritPermissionに対し
* UserId 579 と同じ権限をユーザー名福島 成佳に追加する
* 480111側の継承元426945には適用しない
*
* Permissionsは SiteSettings 配下ではなくサイトデータ直下の項目であり
* updatesitesettings部分更新API はPermissionsを更新対象に含まない
* 公式マニュアルで確認済みそのため本スクリプトは updatesite全体更新を使い
* 継承先サイトの現状の Title/ReferenceType/ParentId/InheritPermission/SiteSettings
* 一切変更せずそのまま再送信しPermissionsのみ新規エントリを追加した配列を送信する
*
* 注意: 公式マニュアルには updatesite のリクエストボディでPermissionsを
* 指定できるという明記が無い記載が省略されている可能性はある
* 実行前に必ずプレビュー出力を確認すること
*
* 差分プレビュー送信結果は権限継承先サイトの modify/{リクエストラベル}/ フォルダへ保存する
* 修正依頼のたびに新しいフォルダを生成する規約省略時はタイムスタンプを自動採番
*
* 使い方:
* node apply-permission-grant.js 差分表示アーティファクト生成のみ送信なし
* node apply-permission-grant.js --execute 上記に加えて実際に送信する
* node apply-permission-grant.js --request=ラベル modifyフォルダ名を明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
const { findSiteDir, latestJsonPath, newModifyDir, latestModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 496626;
const MIRROR_FROM_USER_ID = 579;
const GRANT_TO_USER_NAME = "福島 成佳";
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const sourceSitePath =
latestJsonPath(configsDir, SOURCE_SITE_ID) ||
path.join(configsDir, `site-${SOURCE_SITE_ID}_latest.json`);
const sourceSiteJson = loadJson(sourceSitePath);
const inheritPermissionId = sourceSiteJson?.Response?.Data?.InheritPermission;
if (!inheritPermissionId) {
console.error(`[エラー] ${sourceSitePath} からInheritPermissionを取得できませんでした。`);
process.exit(1);
}
const targetPath =
latestJsonPath(configsDir, inheritPermissionId) ||
path.join(configsDir, `site-${inheritPermissionId}_latest.json`);
const targetJson = loadJson(targetPath, false);
if (!targetJson) {
console.error(`[エラー] ${targetPath} が見つかりません。先に node get-site-config.js を実行してください`);
console.error(`SiteId ${SOURCE_SITE_ID} 取得時にInheritPermission追跡ロジックで自動取得されるはずです`);
process.exit(1);
}
const targetData = targetJson.Response.Data;
const currentPermissions = targetData.Permissions || [];
const targetSiteDir = findSiteDir(configsDir, inheritPermissionId);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${inheritPermissionId} のフォルダが見つかりません。`);
process.exit(1);
}
// --request指定があればそれを使う。無ければ直近のmodifyフォルダを再利用し、
// まだ無ければ新規作成する(プレビュー→実行の一連の流れを同じフォルダにまとめるため)。
const modifyDir = requestLabel
? newModifyDir(targetSiteDir, requestLabel)
: latestModifyDir(targetSiteDir) || newModifyDir(targetSiteDir);
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
console.log("========================================");
console.log(` 権限追加${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象サイト: SiteId ${inheritPermissionId}"${targetData.Title}"、SiteId ${SOURCE_SITE_ID} の権限継承先)`);
console.log("");
// UserId 579 と同じ権限エントリを抽出複数PermissionTypeを持つ場合は全て対象
const sourceEntries = currentPermissions.filter((entry) => {
const [type, id] = String(entry).split(",");
return type === "User" && Number(id) === MIRROR_FROM_USER_ID;
});
if (sourceEntries.length === 0) {
console.error(`[エラー] UserId ${MIRROR_FROM_USER_ID} のPermissionsエントリが見つかりません。`);
console.error(`現在のPermissions: ${JSON.stringify(currentPermissions)}`);
process.exit(1);
}
// 福島 成佳 のUserIdを解決
const master = loadMasterData(baseDir);
const candidateIds = resolveId(master, { Type: "User", Name: GRANT_TO_USER_NAME });
if (candidateIds.length === 0) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」が見つかりません。configs/master_users.jsonを確認してください。`);
process.exit(1);
}
if (candidateIds.length > 1) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」に一致する候補が複数あります: ${candidateIds.join(", ")}`);
process.exit(1);
}
const grantToUserId = candidateIds[0];
// 579のエントリをコピーし、ユーザーIDだけ差し替えた新規エントリを作成
const newEntries = sourceEntries.map((entry) => {
const parts = String(entry).split(",");
parts[1] = String(grantToUserId);
return parts.join(",");
});
// 既に同一エントリが存在する場合は追加しない
const entriesToAdd = newEntries.filter((e) => !currentPermissions.includes(e));
console.log(`コピー元: UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`);
console.log(` 既存エントリ: ${JSON.stringify(sourceEntries)}`);
console.log(`追加先: UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`);
console.log(` 追加予定エントリ: ${JSON.stringify(newEntries)}`);
if (entriesToAdd.length === 0) {
console.log("\n[INFO] 追加予定のエントリは既に全て存在しています。変更の必要はありません。");
process.exit(0);
}
if (entriesToAdd.length !== newEntries.length) {
console.log(`[INFO] 一部エントリは既に存在するため、実際に追加するのは ${entriesToAdd.length} 件です: ${JSON.stringify(entriesToAdd)}`);
}
const updatedPermissions = [...currentPermissions, ...entriesToAdd];
console.log("\n★注意: updatesite APIでPermissionsを更新できるという公式記載は確認できていません。");
console.log(" 実行前に必ず内容を確認し、実行後は管理画面での反映結果も確認してください。");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${inheritPermissionId}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: inheritPermissionId,
Title: targetData.Title,
ReferenceType: targetData.ReferenceType,
ParentId: targetData.ParentId,
InheritPermission: targetData.InheritPermission,
Permissions: updatedPermissions,
SiteSettings: targetData.SiteSettings, // 変更しない(現状のまま再送信)
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(変更なし。Columns ${(targetData.SiteSettings?.Columns || []).length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${inheritPermissionId}_permission_diff_${ts}.txt`);
const diffText = [
`SiteId ${inheritPermissionId} 権限追加プレビュー (${ts})`,
`URL: ${url}`,
`対象: SiteId ${SOURCE_SITE_ID} の権限継承先`,
"",
`コピー元 UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`,
` 既存エントリ: ${JSON.stringify(sourceEntries)}`,
`追加先 UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`,
` 追加エントリ: ${JSON.stringify(entriesToAdd)}`,
"",
`変更前Permissions: ${JSON.stringify(currentPermissions)}`,
`変更後Permissions: ${JSON.stringify(updatedPermissions)}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-permission-grant.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(baseDir, "configs", `site-${inheritPermissionId}_permission_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面で反映結果を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,125 @@
/**
* apply-remark-fix-496626-full.js
* ------------------------------------------------------------
* updatesitesettings部分更新で送った Script Id:4 / Style Id:2 / ServerScript Id:1
* 修正がHTTP 200成功を返すにもかかわらず実際にはDBに反映されていない
* 直後のgetsiteで確認しても内容もVer/UpdatedTimeも変化しないことが分かったため
* 確実に反映される updatesite全体更新で同じ変更を送り直す
*
* 現在のサイト全体getsiteを取得しTitle/ReferenceType/ParentId/InheritPermission/
* Permissionsはそのまま再送信SiteSettingsもScripts[Id:4]/Styles[Id:2]/
* ServerScripts[Id:1]のBodyだけをローカルファイルの内容に差し替え他は一切変更しない
*
* 使い方:
* node apply-remark-fix-496626-full.js 差分表示のみ送信なし
* node apply-remark-fix-496626-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
const scriptBody = fs.readFileSync(path.join(siteDir, "scripts", "4_new03_一覧画面自動処理.js"), "utf-8");
const styleBody = fs.readFileSync(path.join(siteDir, "styles", "2_一覧スタイル調整.css"), "utf-8");
const serverScriptBody = fs.readFileSync(path.join(siteDir, "serverscripts", "1_一覧画面 物件詳細.js"), "utf-8");
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
const script4 = siteSettings.Scripts.find((s) => s.Id === 4);
const style2 = siteSettings.Styles.find((s) => s.Id === 2);
const serverScript1 = siteSettings.ServerScripts.find((s) => s.Id === 1);
if (!script4 || !style2 || !serverScript1) {
console.error("[エラー] Script Id:4 / Style Id:2 / ServerScript Id:1 のいずれかが見つかりません。");
process.exit(1);
}
console.log("========================================");
console.log(` Script/Style/ServerScript 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`Script Id:4 現状${script4.Body.length}文字 → 変更後${scriptBody.length}文字(変更${script4.Body !== scriptBody}`);
console.log(`Style Id:2 現状${style2.Body.length}文字 → 変更後${styleBody.length}文字(変更${style2.Body !== styleBody}`);
console.log(`ServerScript Id:1 現状${serverScript1.Body.length}文字 → 変更後${serverScriptBody.length}文字(変更${serverScript1.Body !== serverScriptBody}`);
console.log("他のSiteSettingsColumns/GridColumns/Processes等・Permissions・Title等は一切変更しません。");
script4.Body = scriptBody;
style2.Body = styleBody;
serverScript1.Body = serverScriptBody;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "serverscript-remark-fix");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,154 @@
/**
* apply-remark-fix-496626.js
* ------------------------------------------------------------
* SiteId 496626 の一覧画面スクリプトId:4 new03_一覧画面自動処理
* 備考の3文字省略独自ポップアップ表示に対応した修正版へ差し替える
*
* 対象は desired_config.jsonbuild-desired-config.js
* configs/site-496626_着工要因(東京)v2/ 配下から再構成したもの
* SiteSettings.Scripts/Styles/ServerScripts/Processes をそのまま送信する
* 事前に確認した通り実際に内容が変わるのは Scripts Id:4 のみ
* 他はキー順序が変わるだけでBodyは現状と完全一致
*
* ユーザーがその場で直接適用してよいと明示的に許可した場合に限り実行する
* 一回限りの専用スクリプト.claude/skills/pleasanter-site-spec/SKILL.md
* クロスサイト構造変更節の運用に準拠
*
* 使い方:
* node apply-remark-fix-496626.js 差分表示のみ送信なし
* node apply-remark-fix-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { latestJsonPath, newModifyDir, latestModifyDir, findSiteDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
if (desired.SiteId !== SITE_ID) {
console.error(`[エラー] desired_config.json の SiteId (${desired.SiteId}) が ${SITE_ID} ではありません。`);
console.error(`先に node build-desired-config.js site-${SITE_ID}_新・着工要因(東京)v2 を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, SITE_ID);
const current = loadJson(currentPath, false);
const currentScripts = current?.Response?.Data?.SiteSettings?.Scripts || [];
const desiredScripts = desired.SiteSettings.Scripts || [];
const curScript4 = currentScripts.find((s) => s.Id === 4);
const newScript4 = desiredScripts.find((s) => s.Id === 4);
console.log("========================================");
console.log(` 一覧画面スクリプト(Id:4)の差し替え${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象: SiteId ${SITE_ID}"${current?.Response?.Data?.Title || ""}"`);
console.log("");
console.log(`Script Id:4 現状の文字数: ${curScript4 ? curScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 変更後の文字数: ${newScript4 ? newScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 以外のScripts/Styles/ServerScripts/Processesは内容変更なしキー順序のみ変わる場合あり`);
const siteDir = findSiteDir(configsDir, SITE_ID);
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "remark-truncate-fix");
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_diff_${ts}.txt`);
fs.writeFileSync(
diffOutPath,
[
`SiteId ${SITE_ID} 一覧画面スクリプト(Id:4)差し替えプレビュー (${ts})`,
"",
"--- 現状のBody ---",
curScript4 ? curScript4.Body : "(取得できず)",
"",
"--- 変更後のBody ---",
newScript4 ? newScript4.Body : "(取得できず)",
].join("\n"),
"utf-8"
);
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesitesettings`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
SiteSettings: desired.SiteSettings,
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示 ---");
console.log(
JSON.stringify(
{
...maskedBody,
SiteSettings: {
Scripts: `${desired.SiteSettings.Scripts.length}Id:4のみ内容変更`,
Styles: `${desired.SiteSettings.Styles.length}件(変更なし)`,
ServerScripts: `${desired.SiteSettings.ServerScripts.length}件(変更なし)`,
Processes: `${desired.SiteSettings.Processes.length}件(変更なし)`,
},
},
null,
2
)
);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面・一覧画面で表示を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,159 @@
/**
* apply-site-config.js
* ------------------------------------------------------------
* このスクリプトは常にドライランです実際のAPI送信は一切行いません
*
* Claudeが生成した desired_config.json希望構成を読み込み
* 1. 現状構成configs/site-{SiteId}_latest.jsonとの差分表示
* 2. 実際に送信されるはずの HTTPリクエストURL / Bodyの表示
* 3. そのまま使えるcurlコマンドの出力
* のみを行います実行fetch送信は行いません
*
* 内容を確認した上で問題なければ
* - 表示されたcurlコマンドを手動で実行する
* - またはこのファイル末尾の "実送信ブロック" のコメントを
* 自分の判断で外して実行する
* のいずれかで反映してください
*
* desired_config.json の形式:
* {
* "Mode": "partial", // "partial" = updatesitesettings部分更新 / "full" = updatesite全体更新
* "SiteId": 12345, // 省略時は config.json の値を使用
* "SiteSettings": { ... }, // 変更したい項目のみpartialの場合/ 全体fullの場合
* "Title": "...", // full更新時のみ必要な場合あり
* "ReferenceType": "...",
* "ParentId": ...,
* "InheritPermission": ...
* }
*
* 参照した公式マニュアル:
* - サイト更新全体: https://pleasanter.org/manual/api-site-update
* URL: POST {BaseUrl}/api/items/{SiteId}/updatesite
* - サイト設定の部分更新: https://pleasanter.org/ja/manual/api-update-sitesettings
* URL: POST {BaseUrl}/api/items/{SiteId}/updatesitesettings
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { latestJsonPath } = require("./site-paths");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// ざっくりした差分表示キー単位・トップ1階層+SiteSettings1階層まで
// partialモードupdatesitesettingsは desired 側にあるキーだけがサーバーに送られ、
// 記載のないキーは現状のまま変わらない。そのため比較対象は afterKeys のみに限定する
// beforeにしかないキーを「削除される」ように見せるのは誤り
// fullモードupdatesiteはSiteSettings全体を丸ごと置き換えるため、beforeKeysも含めて比較する。
function diffTopLevel(before, after, label, mode = "partial") {
if (!before) {
console.log(` (現状データなし。get-site-config.jsで先に取得しておくと差分表示できます)`);
return;
}
const afterKeys = new Set(Object.keys(after || {}));
const allKeys = mode === "full" ? new Set([...Object.keys(before || {}), ...afterKeys]) : afterKeys;
let changed = false;
for (const key of allKeys) {
const b = JSON.stringify(before?.[key]);
const a = JSON.stringify(after?.[key]);
if (b !== a) {
changed = true;
console.log(` [変更] ${label}.${key}`);
console.log(` - 現状: ${truncate(b)}`);
console.log(` + 希望: ${truncate(a)}`);
}
}
if (!changed) console.log(" 差分なし");
}
function truncate(str, max = 200) {
if (str === undefined) return "(undefined)";
return str.length > max ? str.slice(0, max) + "...(略)" : str;
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
async function main() {
const baseDir = path.join(__dirname, "..", "..");
const configPath = path.join(baseDir, "config.json");
const desiredPath = process.argv[2] || path.join(baseDir, "desired_config.json");
const config = loadJson(configPath);
const desired = loadJson(desiredPath);
const siteId = desired.SiteId || config.SiteId;
const mode = desired.Mode || "partial";
const endpoint = mode === "full" ? "updatesite" : "updatesitesettings";
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/${endpoint}`;
const latestConfigPath =
latestJsonPath(path.join(baseDir, "configs"), siteId) ||
path.join(baseDir, "configs", `site-${siteId}_latest.json`);
const current = loadJson(latestConfigPath, false);
const currentSiteSettings =
current?.Response?.Data?.SiteSettings ||
current?.Response?.Site?.SiteSettings ||
current?.Response?.SiteSettings ||
null;
console.log("========================================");
console.log(" ドライラン結果(実際の送信は行いません)");
console.log("========================================");
console.log(`Mode : ${mode} (${mode === "full" ? "全体更新" : "部分更新"})`);
console.log(`URL : ${url}`);
console.log(`SiteId : ${siteId}`);
console.log("");
console.log("--- SiteSettings 差分(現状 → 希望) ---");
diffTopLevel(currentSiteSettings, desired.SiteSettings, "SiteSettings", mode);
console.log("");
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
...desired,
};
delete body.Mode; // Mode はエンドポイント選択にのみ使用し、送信Bodyには含めない
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("--- 送信されるはずのBodyAPIキーはマスク表示 ---");
console.log(JSON.stringify(maskedBody, null, 2));
console.log("");
console.log("--- 手動実行用 curl コマンド(内容確認の上、必要ならご自身で実行してください) ---");
console.log(
`curl -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(body)}'`
);
console.log("");
console.log("[注意] このスクリプトはAPIへの送信を一切行っていませんドライラン専用。");
console.log("内容に問題がなければ、上記curlコマンドを手動実行するか、");
console.log("スクリプト内の「実送信ブロック」を有効化する改修をご自身の判断で行ってください。");
// ------------------------------------------------------------
// 実送信ブロック(デフォルトでは絶対に到達しないようにガードしています)
// 意図的に反映を自動化したい場合のみ、下のガードを外して使ってください。
// ------------------------------------------------------------
const ENABLE_ACTUAL_SEND = false; // ← 安全のため常にfalse固定。変更は自己責任で。
if (ENABLE_ACTUAL_SEND) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
console.log(await res.text());
}
}
main();

View File

@ -0,0 +1,211 @@
/**
* apply-to-site-496626.js
* ------------------------------------------------------------
* configs/site-480111_updated.json のSiteSettingsをSiteId 496626
* まるごと上書き適用するupdatesite / Mode:"full"
*
* 通常このプロジェクトの apply-site-config.js はドライラン専用実送信しないだが
* 本スクリプトは今回のタスクに限りユーザーが明示的に許可した専用の直接実行スクリプト
* であり--execute 指定時のみ実際にAPIへPOSTするcurlは使わない
*
* Permissionsは送信ボディに含めない496626は権限をInheritPermissionで
* 継承しているため直接の書き込み対象ではない権限変更は apply-permission-grant.js
* を参照Title/ReferenceType/ParentId/InheritPermissionは496626自身の
* 現状値configs/site-496626_latest.jsonから取得しSiteSettingsのみ
* 480111由来の変更後内容で置き換える
*
* 差分プレビュー送信結果はSiteId 496626 modify/{リクエストラベル}/ フォルダへ保存する
* restructure-site-480111.js が作成したフォルダを --request= で指定するか省略時は
* 最新のmodifyフォルダを自動的に使う
*
* 使い方:
* node apply-to-site-496626.js 差分表示アーティファクト生成のみ送信なし
* node apply-to-site-496626.js --execute 上記に加えて実際に送信する
* node apply-to-site-496626.js --request=ラベル 対象のmodifyフォルダを明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, latestJsonPath, newModifyDir } = require("./site-paths");
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffTopLevel(before, after) {
const lines = [];
const afterKeys = Object.keys(after || {});
let changed = false;
for (const key of afterKeys) {
const b = JSON.stringify(before?.[key]);
const a = JSON.stringify(after?.[key]);
if (b !== a) {
changed = true;
const bLen = Array.isArray(before?.[key]) ? `(${before[key].length}件)` : "";
const aLen = Array.isArray(after?.[key]) ? `(${after[key].length}件)` : "";
lines.push(` [変更] SiteSettings.${key} ${bLen}${aLen}`);
}
}
if (!changed) lines.push(" 差分なし");
return lines;
}
// 対象のmodifyフォルダを解決する--request指定があればそれ、無ければ最新のものを使う
const targetSiteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。先に node get-site-config.js を実行してください。`);
process.exit(1);
}
function resolveModifyDir() {
if (requestLabel) return newModifyDir(targetSiteDir, requestLabel);
const modifyRoot = path.join(targetSiteDir, "modify");
if (!fs.existsSync(modifyRoot)) {
console.error(`[エラー] modifyフォルダがまだありません。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const dirs = fs.readdirSync(modifyRoot).filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory());
if (dirs.length === 0) {
console.error(`[エラー] modifyフォルダの中身が空です: ${modifyRoot}`);
process.exit(1);
}
dirs.sort();
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
const modifyDir = resolveModifyDir();
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
const config = loadJson(path.join(baseDir, "config.json"));
const updatedPath = path.join(modifyDir, "site-480111_updated.json");
const updatedJson = loadJson(updatedPath);
const modifiedSiteSettings = updatedJson?.Response?.Data?.SiteSettings;
if (!modifiedSiteSettings) {
console.error(`[エラー] ${updatedPath} からSiteSettingsを取得できませんでした。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, TARGET_SITE_ID);
const currentJson = loadJson(currentPath, false);
const currentData = currentJson?.Response?.Data;
if (!currentData) {
console.error(`[エラー] ${currentPath} が見つかりません。先に node get-site-config.jsSiteIdに${TARGET_SITE_ID}を含めて)を実行してください。`);
process.exit(1);
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: TARGET_SITE_ID,
Title: currentData.Title,
ReferenceType: currentData.ReferenceType,
ParentId: currentData.ParentId,
InheritPermission: currentData.InheritPermission,
SiteSettings: modifiedSiteSettings,
};
console.log("========================================");
console.log(` SiteId ${TARGET_SITE_ID} への適用${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`URL : ${url}`);
console.log(`Title : ${currentData.Title}`);
console.log(`InheritPermission: ${currentData.InheritPermission}(権限は今回変更しません)`);
console.log("");
console.log("--- SiteSettings 差分496626現状 → 480111由来の変更後 ---");
const diffLines = diffTopLevel(currentData.SiteSettings, modifiedSiteSettings);
diffLines.forEach((l) => console.log(l));
console.log("");
const newItem = (modifiedSiteSettings.Columns || []).find(
(c) => c.ColumnName === "Date031" && c.GridLabelText === "解体紹介料申請"
);
console.log(`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`);
console.log(`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`);
console.log(`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`);
console.log("");
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(省略: Columns ${modifiedSiteSettings.Columns.length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_diff_${ts}.txt`);
const diffText = [
`SiteId ${TARGET_SITE_ID} 適用差分プレビュー (${ts})`,
`URL: ${url}`,
`Title: ${currentData.Title}`,
`InheritPermission: ${currentData.InheritPermission}(今回は権限変更なし)`,
"",
"--- SiteSettings差分 ---",
...diffLines,
"",
`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`,
`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`,
`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-to-site-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,139 @@
/**
* build-desired-config.js
* ------------------------------------------------------------
* extract-site-config.js で分割した configs/site-{SiteId}/ 配下のファイル
* scripts/*.js, styles/*.css, serverscripts/*.js, processes.json
* manifest.json を手がかりに再構成しapply-site-config.js が読み込む
* desired_config.json を生成します
*
* このスクリプトもAPIへは一切送信しませんファイル生成のみ
* 反映は既存の運用どおり
* 1. node apply-site-config.js でドライラン確認差分送信Bodycurl表示
* 2. 内容を確認し問題なければ表示されたcurlコマンドを手動実行
* の流れに乗せてください
*
* 使い方:
* node build-desired-config.js site-214812
* フォルダ名は "site-{SiteId}_{サイト名}" 形式だが"site-{SiteId}" だけの
* 前方一致指定でも configs/ 配下から自動的に該当フォルダを探して解決する
* node build-desired-config.js ./configs/site-214812_物件基本情報
*
* 出力: desired_config.jsonこのスクリプトと同じ階層既存ファイルは上書き
*
* 対応範囲:
* Scripts / Styles / ServerScripts / Processes
* Mode:"partial"updatesitesettingsで反映可能なためそのまま組み込みます
* HtmlGridGuide等のガイド
* SiteSettingsではなくサイトデータ直下の項目のため
* 部分更新updatesitesettingsには含められません
* 反映したい場合は Mode:"full"updatesite用の別JSONを用意し
* Title/ReferenceType等の必須項目とあわせて指定してください
* このスクリプトはhtml/配下に抽出物がある場合警告のみ表示します
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function readJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// フォルダ名は "site-{SiteId}_{サイト名}" 形式。"site-{SiteId}" のみの指定でも
// configs/ 配下から前方一致で該当フォルダを探して解決する。
// 新形式(サイト名付き)を優先し、裸の "site-{SiteId}" 完全一致(リネーム前の古い抽出結果の可能性)は
// 他に候補が無い場合のみ後方互換フォールバックとして使う。
function resolveSiteDir(baseDir, arg) {
const configsDir = path.join(baseDir, "configs");
const direct = path.isAbsolute(arg) ? arg : path.join(configsDir, path.basename(arg));
const baseName = path.basename(arg);
const candidates = fs.existsSync(configsDir)
? fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return (
fs.statSync(full).isDirectory() && (name === baseName || name.startsWith(`${baseName}_`))
);
})
: [];
const suffixed = candidates.filter((name) => name !== baseName);
if (suffixed.length === 1) return path.join(configsDir, suffixed[0]);
if (suffixed.length > 1) {
console.error(`[エラー] "${baseName}" に一致するフォルダが複数見つかりました: ${suffixed.join(", ")}`);
console.error("フォルダ名をフルで指定してください。");
process.exit(1);
}
if (candidates.includes(baseName)) return path.join(configsDir, baseName);
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) return direct;
console.error(`[エラー] フォルダが見つかりません: ${direct}`);
console.error("先に node extract-site-config.jsまたはnode get-site-config.jsを実行してください。");
process.exit(1);
}
function rebuildList(manifestEntries, dir, bodyKey = "Body") {
return (manifestEntries || []).map((entry) => {
const { File, ...meta } = entry;
const filePath = path.join(dir, File);
if (!fs.existsSync(filePath)) {
console.error(`[エラー] manifest.jsonに記載のファイルが見つかりません: ${filePath}`);
process.exit(1);
}
const body = fs.readFileSync(filePath, "utf-8");
return { ...meta, [bodyKey]: body };
});
}
function main() {
const baseDir = path.join(__dirname, "..", "..");
const arg = process.argv[2];
if (!arg) {
console.error("[エラー] 対象フォルダを指定してください(例: node build-desired-config.js site-214812");
process.exit(1);
}
const siteDir = resolveSiteDir(baseDir, arg);
const manifestPath = path.join(siteDir, "manifest.json");
if (!fs.existsSync(manifestPath)) {
console.error(`[エラー] manifest.jsonが見つかりません: ${manifestPath}`);
console.error("先に node extract-site-config.js を実行してください。");
process.exit(1);
}
const manifest = readJson(manifestPath);
const siteSettings = {};
siteSettings.Scripts = rebuildList(manifest.Scripts, path.join(siteDir, "scripts"));
siteSettings.Styles = rebuildList(manifest.Styles, path.join(siteDir, "styles"));
siteSettings.ServerScripts = rebuildList(manifest.ServerScripts, path.join(siteDir, "serverscripts"));
const processesPath = path.join(siteDir, manifest.ProcessesFile || "processes.json");
siteSettings.Processes = fs.existsSync(processesPath) ? readJson(processesPath) : [];
const desired = {
Mode: "partial",
SiteId: manifest.SiteId,
SiteSettings: siteSettings,
};
const outPath = path.join(baseDir, "desired_config.json");
fs.writeFileSync(outPath, JSON.stringify(desired, null, 2), "utf-8");
console.log(`[OK] 生成しました: ${outPath}`);
console.log(` Scripts : ${siteSettings.Scripts.length}`);
console.log(` Styles : ${siteSettings.Styles.length}`);
console.log(` ServerScripts : ${siteSettings.ServerScripts.length}`);
console.log(` Processes : ${siteSettings.Processes.length}`);
if (manifest.Html && manifest.Html.length > 0) {
console.log("\n[注意] html/ 配下のガイドHTMLは、この desired_config.json には含まれていません。");
console.log(" GridGuide等はSiteSettingsではなくサイト直下の項目のため、部分更新(updatesitesettings)では送れません。");
console.log(' 反映するには Mode:"full"updatesite用の別JSONを用意し、Title等の必須項目とあわせて指定してください。');
}
console.log("\n次のステップ: node apply-site-config.js でドライラン確認 → 内容確認の上、表示されたcurlコマンドを手動実行してください。");
}
main();

View File

@ -0,0 +1,154 @@
/**
* extract-site-config.js
* ------------------------------------------------------------
* 取得済みのサイト構成 (configs/site-{SiteId}_latest.json) から
* Scripts / Styles / ServerScripts / Processes / ガイドHTML
* 個別ファイルとして configs/site-{SiteId}/ 配下に書き出します
*
* 使い方:
* node extract-site-config.js
* node extract-site-config.js ./configs/site-214812_latest.json
*
* 出力:
* configs/site-{SiteId}/
* manifest.json 抽出内容の一覧build-desired-config.jsでの再構成に使用
* scripts/{Id}_{Title}.js
* styles/{Id}_{Title}.css
* serverscripts/{Id}_{Title}.js
* html/{GuideName}.html GridGuide等ガイドHTML値が空でないもののみ
* processes.json プロセス設定SiteSettings.Processes をそのまま保存
*
* 注意:
* ガイドHTMLGridGuide等はSiteSettingsではなくサイトデータ直下の項目です
* そのため反映時はサイト設定の部分更新updatesitesettingsではなく
* サイト全体更新updatesite / Mode:"full"が必要になります
* Scripts/Styles/ServerScripts/Processesは部分更新Mode:"partial"で反映できます
* 詳しくは build-desired-config.js の実行結果メッセージを参照してください
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { sanitize, ensureSiteDir, latestJsonPath } = require("./site-paths");
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function extractList(items, outDir, ext, bodyKey = "Body") {
const manifestEntries = [];
if (!items || items.length === 0) return manifestEntries;
ensureDir(outDir);
for (const item of items) {
const fileName = `${item.Id}_${sanitize(item.Title || item.Name)}${ext}`;
fs.writeFileSync(path.join(outDir, fileName), item[bodyKey] || "", "utf-8");
const { [bodyKey]: _omit, ...meta } = item;
manifestEntries.push({ ...meta, File: fileName });
}
return manifestEntries;
}
const GUIDE_FIELDS = [
"GridGuide",
"EditorGuide",
"CalendarGuide",
"CrosstabGuide",
"GanttGuide",
"BurnDownGuide",
"TimeSeriesGuide",
"AnalyGuide",
"KambanGuide",
"ImageLibGuide",
];
// get-site-config.js からも呼び出せるよう関数化。
// CLI実行時直接 `node extract-site-config.js`)は末尾のブロックが引数解決を行う。
function extractSiteConfig(latestPath) {
const baseDir = path.join(__dirname, "..", "..");
if (!fs.existsSync(latestPath)) {
console.error(`[エラー] ファイルが見つかりません: ${latestPath}`);
console.error("先に node get-site-config.js を実行してください。");
process.exit(1);
}
const json = JSON.parse(fs.readFileSync(latestPath, "utf-8"));
const data = json?.Response?.Data;
if (!data) {
console.error("[エラー] Response.Data が見つかりません。getsiteのレスポンス形式を確認してください。");
process.exit(1);
}
const siteId = data.SiteId;
const settings = data.SiteSettings || {};
const configsDir = path.join(baseDir, "configs");
const outRoot = ensureSiteDir(configsDir, siteId, data.Title);
const manifest = {
SiteId: siteId,
Title: data.Title,
ExtractedAt: new Date().toISOString(),
SourceFile: path.relative(baseDir, latestPath).replace(/\\/g, "/"),
};
manifest.Scripts = extractList(settings.Scripts, path.join(outRoot, "scripts"), ".js");
manifest.Styles = extractList(settings.Styles, path.join(outRoot, "styles"), ".css");
manifest.ServerScripts = extractList(settings.ServerScripts, path.join(outRoot, "serverscripts"), ".js");
// ガイドHTMLサイトデータ直下。SiteSettingsではない点に注意
const htmlDir = path.join(outRoot, "html");
const htmlEntries = [];
for (const field of GUIDE_FIELDS) {
const value = data[field];
if (value) {
ensureDir(htmlDir);
const fileName = `${field}.html`;
fs.writeFileSync(path.join(htmlDir, fileName), value, "utf-8");
htmlEntries.push({ Field: field, File: fileName, TopLevel: true });
}
}
manifest.Html = htmlEntries;
// プロセス設定 … 個別分割せず1つの定義ファイルとして保存
fs.writeFileSync(
path.join(outRoot, "processes.json"),
JSON.stringify(settings.Processes || [], null, 2),
"utf-8"
);
manifest.ProcessesFile = "processes.json";
manifest.ProcessesCount = (settings.Processes || []).length;
fs.writeFileSync(path.join(outRoot, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
console.log(`[OK] 抽出完了: ${outRoot}`);
console.log(` Scripts : ${manifest.Scripts.length}`);
console.log(` Styles : ${manifest.Styles.length}`);
console.log(` ServerScripts : ${manifest.ServerScripts.length}`);
console.log(` Html(ガイド) : ${manifest.Html.length}`);
console.log(` Processes : ${manifest.ProcessesCount}件 → processes.json`);
console.log("\n次のステップ: 各ファイルを直接編集するか、Claudeに修正内容を指示してください。");
console.log(`修正後は次のコマンドで反映用JSONを作成できます: node build-desired-config.js ${path.basename(outRoot)}`);
return manifest;
}
// CLIとして直接実行された場合のみ、引数からlatestPathを解決して実行する
if (require.main === module) {
const baseDir = path.join(__dirname, "..", "..");
let latestPath = process.argv[2];
if (!latestPath) {
const configPath = path.join(baseDir, "config.json");
if (!fs.existsSync(configPath)) {
console.error("[エラー] config.jsonが見つかりません。抽出対象のJSONパスを引数で指定してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const resolved = latestJsonPath(path.join(baseDir, "configs"), config.SiteId);
latestPath = resolved || path.join(baseDir, "configs", `site-${config.SiteId}_latest.json`);
}
extractSiteConfig(latestPath);
}
module.exports = { extractSiteConfig };

View File

@ -0,0 +1,142 @@
/**
* get-master-data.js
* ------------------------------------------------------------
* Pleasanterのユーザー/組織(部署)/グループ一覧を取得し
* ./configs/ 配下に保存しますresolve-names.js が読み込む
*
* 使い方:
* node get-master-data.js
* node get-master-data.js ./config.json 設定ファイルを明示指定する場合
*
* 参照した公式マニュアル:
* - 開発者向け機能APIユーザ操作ユーザ取得全て
* https://pleasanter.org/ja/manual/api-user-get-all
* - 開発者向け機能API組織操作組織取得
* https://pleasanter.org/ja/manual/api-dept-get
* - 開発者向け機能APIグループ操作グループ取得
* https://pleasanter.org/ja/manual/api-group-get
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const TARGETS = [
{ key: "users", endpoint: "users/get", outFile: "master_users.json", label: "ユーザー", idField: "UserId" },
{ key: "depts", endpoint: "depts/get", outFile: "master_depts.json", label: "組織", idField: "DeptId" },
{ key: "groups", endpoint: "groups/get", outFile: "master_groups.json", label: "グループ", idField: "GroupId" },
];
// 一部API/api/users/get 等はページング応答TotalCount > 1回あたりのData件数を返すため、
// 全件取得できるまで Offset をずらして繰り返し取得し、Data配列を結合する。
async function fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/${target.endpoint}`;
let allData = [];
let offset = 0;
let totalCount = null;
let firstJson = null;
while (true) {
const body = {
ApiVersion: ApiVersion || "1.1",
ApiKey: ApiKey,
Offset: offset,
};
console.log(`[INFO] 取得先: ${url} (Offset: ${offset})`);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (err) {
console.error(`[エラー] ${target.label}の取得に失敗しました:`, err.message);
return false;
}
const text = await response.text();
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return false;
}
let json;
try {
json = JSON.parse(text);
} catch (err) {
console.error(`[エラー] ${target.label}のレスポンスがJSONとして解析できませんでした。`);
console.error(text);
return false;
}
if (!firstJson) firstJson = json;
const pageData = json?.Response?.Data || [];
allData = allData.concat(pageData);
totalCount = json?.Response?.TotalCount ?? allData.length;
if (pageData.length === 0 || allData.length >= totalCount) break;
offset = allData.length;
}
// ページ間でOffset起点がずれて重複が混ざる場合があるため、idFieldで重複排除する
const seen = new Set();
allData = allData.filter((item) => {
const id = item[target.idField];
if (seen.has(id)) return false;
seen.add(id);
return true;
});
// 結合済みのData配列で1件目のレスポンスを上書きし、全件入りのJSONとして保存する
const outJson = { ...firstJson, Response: { ...firstJson.Response, Data: allData } };
const outFile = path.join(outDir, target.outFile);
fs.writeFileSync(outFile, JSON.stringify(outJson, null, 2), "utf-8");
console.log(`[OK] ${target.label}: ${allData.length}/${totalCount}件 保存しました: ${outFile}`);
return true;
}
async function main() {
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
console.error("config.example.json をコピーして config.json を作成してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const { BaseUrl, ApiKey, ApiVersion } = config;
if (!BaseUrl || !ApiKey) {
console.error("[エラー] config.json に BaseUrl / ApiKey を設定してください。");
process.exit(1);
}
const outDir = path.join(path.join(__dirname, "..", ".."), "configs", "master");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const results = [];
for (const target of TARGETS) {
const ok = await fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir);
results.push({ target: target.label, ok });
}
console.log("\n==== 取得結果まとめ ====");
for (const { target, ok } of results) {
console.log(` ${target}: ${ok ? "OK" : "失敗"}`);
}
if (results.some((r) => !r.ok)) {
process.exit(1);
}
}
main();

View File

@ -0,0 +1,188 @@
/**
* get-site-config.js
* ------------------------------------------------------------
* プリザンターの指定サイトの構成情報SiteSettings等を取得し
* ./configs/ 配下にタイムスタンプ付きJSONとして保存します
*
* 使い方:
* node get-site-config.js
* node get-site-config.js ./config.json 設定ファイルを明示指定する場合
*
* 事前準備:
* 1. config.example.json config.json にコピーし
* BaseUrl / SiteId / ApiKey を実環境の値に書き換えてください
* SiteId はカンマ区切りで複数指定可能です: "480111,438166,96235"
* 1件のみの場合は従来通り数値: 480111でも構いません
* 2. Node.js 18以降組み込みfetchを使用
*
* 参照した公式マニュアル:
* - 開発者向け機能APIサイト操作サイト更新
* https://pleasanter.org/manual/api-site-update
* - 開発者向け機能APIサイト操作サイト設定の更新部分追加/更新/削除
* https://pleasanter.org/ja/manual/api-update-sitesettings
*
* 注意:
* プリザンターのバージョンにより get のレスポンス構造
* SiteSettingsが Response.Site 配下にあるか等が異なる場合があります
* 初回実行時はコンソールに出力されるレスポンスのトップレベルキーを確認し
* 想定通りの構造か確認してください
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { extractSiteConfig } = require("./extract-site-config");
const { ensureSiteDir, sitesettingsDir } = require("./site-paths");
// config.json の SiteId数値 / カンマ区切り文字列 / 配列)を
// 個々のSiteId文字列の配列に正規化する。
function parseSiteIds(rawSiteId) {
const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(",");
return list
.map((id) => String(id).trim())
.filter((id) => id.length > 0);
}
// 1サイト分の取得・保存・抽出処理
async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/getsite`;
const body = {
ApiVersion: ApiVersion || "1.1",
ApiKey: ApiKey,
};
console.log(`[INFO] 取得先: ${url}`);
console.log(`[INFO] SiteId: ${siteId}`);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (err) {
console.error("[エラー] リクエスト送信に失敗しました:", err.message);
console.error("BaseUrlの疎通・ネットワーク設定を確認してください。");
return { ok: false };
}
const text = await response.text();
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return { ok: false };
}
let json;
try {
json = JSON.parse(text);
} catch (err) {
console.error("[エラー] レスポンスがJSONとして解析できませんでした。");
console.error(text);
return { ok: false };
}
// レスポンス構造の確認用ログ(初回はここを見て構造を把握してください)
console.log("[INFO] レスポンス トップレベルキー:", Object.keys(json));
if (json.Response) {
console.log("[INFO] Response配下のキー:", Object.keys(json.Response));
}
// 保存: configs/site-{siteId}_{サイト名}/sitesettings/ 配下へ
const configsDir = path.join(path.join(__dirname, "..", ".."), "configs");
const title = json?.Response?.Data?.Title;
const siteDir = ensureSiteDir(configsDir, siteId, title);
const outDir = sitesettingsDir(siteDir);
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-");
const outFile = path.join(outDir, `site-${siteId}_${timestamp}.json`);
fs.writeFileSync(outFile, JSON.stringify(json, null, 2), "utf-8");
// 常に「最新」ファイルも別名で保持Claudeへのアップロード用に固定名があると便利
const latestFile = path.join(outDir, `site-${siteId}_latest.json`);
fs.writeFileSync(latestFile, JSON.stringify(json, null, 2), "utf-8");
console.log(`[OK] 保存しました: ${outFile}`);
console.log(`[OK] 最新版としても保存しました: ${latestFile}`);
// 取得と同時に Scripts/Styles/ServerScripts/Processes/ガイドHTML も抽出する
console.log("\n[INFO] 続けて設定の個別ファイル抽出を実行します...");
extractSiteConfig(latestFile);
// 権限継承先の追跡: InheritPermission が自サイト以外を指している場合、
// 実際の権限はそちらのサイトにあるため、そのサイトも同様に取得する。
const inheritPermission = json?.Response?.Data?.InheritPermission;
return { ok: true, inheritPermission };
}
async function main() {
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
console.error("config.example.json をコピーして config.json を作成してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const { BaseUrl, SiteId, ApiKey, ApiVersion } = config;
if (!BaseUrl || !SiteId || !ApiKey) {
console.error("[エラー] config.json に BaseUrl / SiteId / ApiKey を設定してください。");
process.exit(1);
}
const siteIds = parseSiteIds(SiteId);
if (siteIds.length === 0) {
console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。");
process.exit(1);
}
// 権限継承先の追跡: 指定サイトの取得後、InheritPermission が自サイト以外を
// 指していれば、そのサイトも同じ要領でキューに追加して取得する
// (権限の実体は継承元サイド側にあるため)。
const queue = [...siteIds];
const visited = new Set();
const results = [];
while (queue.length > 0) {
const siteId = String(queue.shift());
if (visited.has(siteId)) continue;
visited.add(siteId);
console.log(`\n==== [${results.length + 1}] SiteId: ${siteId} ====`);
const { ok, inheritPermission } = await fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion });
results.push({ siteId, ok });
if (
ok &&
inheritPermission != null &&
String(inheritPermission) !== "0" &&
String(inheritPermission) !== siteId &&
!visited.has(String(inheritPermission))
) {
console.log(`[INFO] SiteId ${siteId} は権限をSiteId ${inheritPermission} から継承しています。継承元も取得します。`);
queue.push(String(inheritPermission));
}
}
console.log("\n==== 取得結果まとめ ====");
for (const { siteId, ok } of results) {
console.log(` SiteId ${siteId}: ${ok ? "OK" : "失敗"}`);
}
console.log("\n次のステップ: latest.jsonをClaudeのチャットにアップロードして仕様書化を依頼するか、");
console.log("configs/site-{SiteId}/ 配下のファイルを直接編集・指示して修正してください。");
if (results.some((r) => !r.ok)) {
process.exit(1);
}
}
main();

View File

@ -0,0 +1,185 @@
/**
* migrate-data-480111-to-496626.js
* ------------------------------------------------------------
* SiteId 480111 の全レコードを取得しSiteSettings側で行った連番リネーム
* Date031047 Date032048Date131147 Date132148
* Check131147 Check132148Class031047 Class032048
* 同じマッピングでフィールド値を付け替えた上でSiteId 496626
* bulkupsert新規作成でインポートする
*
* 除外するもの:
* - ResultId / SiteId / Ver / CreatedTime / UpdatedTime / Locked / ItemTitle
* 新サイト側で自動採番自動計算されるため
* - Comments変更履歴コメントテキスト量が多くCreatorが480111時点のユーザーIDのため
* そのまま再現すると誤解を招く可能性がある除外し必要なら別途手動で移行する
* - AttachmentsHash添付ファイルはサイトをまたいだAPI経由の単純コピーができないため対象外
* 添付がある場合はプレビュー時に警告する
*
* 使い方:
* node migrate-data-480111-to-496626.js 件数サンプル警告の表示のみ送信なし
* node migrate-data-480111-to-496626.js --execute 上記に加えて実際にbulkupsertで送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const BATCH_SIZE = 20;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function pad3(n) {
return String(n).padStart(3, "0");
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
// SiteSettings側の連番リネームと同じマッピングを構築する
const classMap = {};
const dateMap = {};
const checkMap = {};
for (let i = 31; i <= 47; i++) {
classMap[`Class${pad3(i)}`] = `Class${pad3(i + 1)}`;
dateMap[`Date${pad3(i)}`] = `Date${pad3(i + 1)}`;
dateMap[`Date${i + 100}`] = `Date${i + 101}`;
checkMap[`Check${i + 100}`] = `Check${i + 101}`;
}
function remapHash(hash, map) {
const result = {};
for (const [key, value] of Object.entries(hash || {})) {
const newKey = map[key] || key;
result[newKey] = value;
}
return result;
}
async function fetchAllRecords(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/get`;
let all = [];
let offset = 0;
let totalCount = null;
while (true) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: ApiVersion || "1.1", ApiKey, Offset: offset }),
});
const json = await res.json();
if (json.StatusCode !== 200) {
console.error(`[エラー] レコード取得に失敗しましたOffset:${offset}:`, JSON.stringify(json).slice(0, 500));
process.exit(1);
}
const page = json.Response.Data || [];
all = all.concat(page);
totalCount = json.Response.TotalCount ?? all.length;
if (page.length === 0 || all.length >= totalCount) break;
offset = all.length;
}
return all;
}
function buildMigratedRecord(record) {
return {
Body: record.Body,
Status: record.Status,
Manager: record.Manager,
Owner: record.Owner,
ClassHash: remapHash(record.ClassHash, classMap),
NumHash: record.NumHash || {},
DateHash: remapHash(record.DateHash, dateMap),
DescriptionHash: record.DescriptionHash || {},
CheckHash: remapHash(record.CheckHash, checkMap),
};
}
async function main() {
const config = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
console.log(`[INFO] SiteId ${SOURCE_SITE_ID} の全レコードを取得しています...`);
const sourceRecords = await fetchAllRecords(SOURCE_SITE_ID, config);
console.log(`[OK] ${sourceRecords.length}件取得しました。`);
const attachmentRecords = sourceRecords.filter(
(r) => r.AttachmentsHash && Object.values(r.AttachmentsHash).some((a) => Array.isArray(a) && a.length > 0)
);
const migrated = sourceRecords.map(buildMigratedRecord);
console.log("\n========================================");
console.log(` データ移行 (480111 → 496626)${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`移行対象件数: ${migrated.length}`);
if (attachmentRecords.length > 0) {
console.log(`\n⚠️ 添付ファイルを含むレコードが${attachmentRecords.length}件ありますResultId: ${attachmentRecords.map((r) => r.ResultId).join(", ")})。`);
console.log(" 添付ファイルは本スクリプトの移行対象外ですAPI経由でのサイト間コピーは非対応のため。");
console.log(" 必要な場合は管理画面から手動での再アップロードを検討してください。");
} else {
console.log("添付ファイルを含むレコードはありません。");
}
console.log("\nComments変更履歴コメントは移行対象外です。");
console.log("\n--- サンプル1件目、移行後の姿 ---");
console.log(JSON.stringify(migrated[0], null, 2));
const siteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!siteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。`);
process.exit(1);
}
// requestLabel未指定時は固定ラベルを使い、プレビュー→実行を同じフォルダにまとめる
const modifyDir = newModifyDir(siteDir, requestLabel || "data-migration");
const previewPath = path.join(modifyDir, "data_migration_preview.json");
fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8");
console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node migrate-data-480111-to-496626.js --execute");
return;
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/bulkupsert`;
const results = [];
for (let i = 0; i < migrated.length; i += BATCH_SIZE) {
const batch = migrated.slice(i, i + BATCH_SIZE);
console.log(`\n実際に送信します(${i + 1}${i + batch.length}件目 / 全${migrated.length}件)...`);
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SsCache: true,
Data: batch,
}),
});
const text = await res.text();
console.log(`HTTP ${res.status} ${res.statusText}`);
let json;
try {
json = JSON.parse(text);
console.log(json.StatusCode === 200 ? "[OK] このバッチは成功しました。" : "[エラー] このバッチは失敗した可能性があります。");
} catch {
console.log(text.slice(0, 1000));
}
results.push({ batchStart: i, status: res.status, body: text });
}
const resultPath = path.join(modifyDir, `data_migration_result_${timestamp()}.json`);
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8");
console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`);
}
main();

View File

@ -0,0 +1,83 @@
/**
* resolve-names.js
* ------------------------------------------------------------
* get-master-data.js が保存した configs/master_users.json /
* master_depts.json / master_groups.json を読み込み
* Permissions等に登場する {Type, Id} 形式の値とユーザー/組織/グループ名との
* 相互変換を行うユーティリティ
*
* 使い方他スクリプトから:
* const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
* const master = loadMasterData();
* resolveName(master, { Type: "User", Id: 579 }); // => "福島 成佳" 等
* resolveId(master, { Type: "User", Name: "福島 成佳" }); // => [579] 一致するIdの配列
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const MASTER_FILES = {
User: "master_users.json",
Dept: "master_depts.json",
Group: "master_groups.json",
};
function readJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
console.error("先に node get-master-data.js を実行してください。");
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// configs/master/master_*.json を読み込み、Type別に { idField, nameField, list } を返す
function loadMasterData(baseDir = path.join(__dirname, "..", "..")) {
const masterDir = path.join(baseDir, "configs", "master");
const usersJson = readJson(path.join(masterDir, MASTER_FILES.User));
const deptsJson = readJson(path.join(masterDir, MASTER_FILES.Dept));
const groupsJson = readJson(path.join(masterDir, MASTER_FILES.Group));
return {
User: {
idField: "UserId",
nameField: "Name",
list: usersJson?.Response?.Data || [],
},
Dept: {
idField: "DeptId",
nameField: "DeptName",
list: deptsJson?.Response?.Data || [],
},
Group: {
idField: "GroupId",
nameField: "GroupName",
list: groupsJson?.Response?.Data || [],
},
};
}
// { Type: "User"|"Dept"|"Group", Id } -> 名前文字列(見つからなければ "(不明:Type#Id)"
function resolveName(master, { Type, Id }) {
const bucket = master[Type];
if (!bucket) return `(不明な種別:${Type})`;
const item = bucket.list.find((x) => String(x[bucket.idField]) === String(Id));
if (!item) return `(不明:${Type}#${Id})`;
return item[bucket.nameField];
}
// { Type: "User"|"Dept"|"Group", Name } -> 一致するIdの配列完全一致、複数該当もありうる
function resolveId(master, { Type, Name }) {
const bucket = master[Type];
if (!bucket) return [];
return bucket.list
.filter((x) => x[bucket.nameField] === Name)
.map((x) => x[bucket.idField]);
}
module.exports = { loadMasterData, resolveName, resolveId };

View File

@ -0,0 +1,365 @@
/**
* 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: 既存項目 Date031047系の連番を Date032048系へ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 SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
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(" OK17項目 × 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("営業タブに対応するViewId:2, 営業担当項目)が見つかりません。");
const viewInsertIdx = salesView.GridColumns.indexOf("Date030") + 1;
salesView.GridColumns.splice(viewInsertIdx, 0, "Date031");
if (typeof settings.SectionLatestId === "number") {
settings.SectionLatestId = 49;
}
console.log(" OKColumns 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(" OKCheck009/010/011を除外、Check018/ClassOを追加。列定義自体は保持");
// ============================================================
// フェーズF: 物件詳細1DescriptionA入力ガイド追加
// ============================================================
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(` OKScripts ${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 で差分を確認してください。`);
}

View File

@ -0,0 +1,105 @@
/**
* site-paths.js
* ------------------------------------------------------------
* configs/ 配下のフォルダ構成を一箇所で管理する共有ヘルパー
*
* 構成:
* configs/
* master/ ユーザー/組織/グループのマスターデータget-master-data.js
* site-{SiteId}_{サイト名}/
* sitesettings/ getsiteの生JSONsite-{SiteId}_latest.json / _{timestamp}.json
* scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
* extract-site-config.js による分割済みファイル従来通り
* docs/ 仕様書(.md)や改善要望プラン等sitesettings以外の関連ドキュメント
* modify/{request}/ 修正依頼ごとに新規作成変更前後のconfigファイルや差分結果を保管
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function sanitize(name) {
return (
String(name || "untitled")
.replace(/[\\/:*?"<>|]/g, "_")
.trim()
.slice(0, 60) || "untitled"
);
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return dir;
}
// configs/ 配下から "site-{siteId}_*"(前方一致)または "site-{siteId}"(完全一致)の
// フォルダを探す。見つからなければ null。
function findSiteDir(configsDir, siteId) {
if (!fs.existsSync(configsDir)) return null;
const bare = `site-${siteId}`;
const prefix = `site-${siteId}_`;
const candidates = fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return fs.statSync(full).isDirectory() && (name === bare || name.startsWith(prefix));
});
const suffixed = candidates.filter((n) => n !== bare);
if (suffixed.length >= 1) return path.join(configsDir, suffixed[0]);
if (candidates.includes(bare)) return path.join(configsDir, bare);
return null;
}
// 既存フォルダがあればそれを使い、無ければ site-{siteId}_{sanitize(title)} を新規作成する
function ensureSiteDir(configsDir, siteId, title) {
const existing = findSiteDir(configsDir, siteId);
if (existing) return existing;
const dir = path.join(configsDir, `site-${siteId}_${sanitize(title)}`);
return ensureDir(dir);
}
function sitesettingsDir(siteDir) {
return ensureDir(path.join(siteDir, "sitesettings"));
}
function docsDir(siteDir) {
return ensureDir(path.join(siteDir, "docs"));
}
// 修正依頼用の新規フォルダを作成するlabelを省略した場合はタイムスタンプを使う
function newModifyDir(siteDir, label) {
const name = label || new Date().toISOString().replace(/[:.]/g, "-");
return ensureDir(path.join(siteDir, "modify", name));
}
// 既存のmodifyフォルダの中で最も新しいものフォルダ名の降順ソートを返す。無ければnull。
function latestModifyDir(siteDir) {
const modifyRoot = path.join(siteDir, "modify");
if (!fs.existsSync(modifyRoot)) return null;
const dirs = fs
.readdirSync(modifyRoot)
.filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory())
.sort();
if (dirs.length === 0) return null;
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
// サイトの最新getsiteダンプsite-{siteId}_latest.jsonのパスを返す。
// フォルダ自体が無い場合は null。
function latestJsonPath(configsDir, siteId) {
const siteDir = findSiteDir(configsDir, siteId);
if (!siteDir) return null;
return path.join(siteDir, "sitesettings", `site-${siteId}_latest.json`);
}
module.exports = {
sanitize,
ensureDir,
findSiteDir,
ensureSiteDir,
sitesettingsDir,
docsDir,
newModifyDir,
latestModifyDir,
latestJsonPath,
};

View File

@ -2,10 +2,11 @@
"permissions": {
"allow": [
"Bash(node -c get-site-config.js)",
"Bash(node get-site-config.js \"C:\\\\Users\\\\KA5AC~1.NOG\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-k-nogi--GitHub-ken-nogi-ClaudePleasanter-MSS-------\\\\4612346a-d27e-4254-ab27-863e0d44e52a\\\\scratchpad\\\\config_492045.json\")",
"Bash(echo \"exit:$?\")",
"Bash(node get-site-config.js \"C:\\\\Users\\\\KA5AC~1.NOG\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-k-nogi--GitHub-ken-nogi-ClaudePleasanter-MSS-------\\\\4612346a-d27e-4254-ab27-863e0d44e52a\\\\scratchpad\\\\config_475384.json\")",
"Bash(node get-site-config.js \"C:\\\\Users\\\\KA5AC~1.NOG\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-k-nogi--GitHub-ken-nogi-ClaudePleasanter-MSS-------\\\\4612346a-d27e-4254-ab27-863e0d44e52a\\\\scratchpad\\\\config_492169.json\")"
"Bash(node get-master-data.js)",
"Bash(node get-site-config.js)",
"WebFetch(domain:pleasanter.org)",
"Bash(node -e ' *)",
"Bash(node restructure-site-480111.js)"
]
}
}

View File

@ -1,30 +1,63 @@
---
name: pleasanter-site-spec
description: Pleasanterのサイト設定JSONget-site-config.js / api-site-get の getsite レスポンスから、SiteSettingsを読み解いたMarkdown仕様書を生成し、Scripts/Styles/ServerScripts/Processesの抽出・編集・反映extract-site-config.js / build-desired-config.js / apply-site-config.jsを行う。また、技術仕様書とは別に業務担当者向けの概要書docs/site-{SiteId}_overview.mdを作成し、md-to-pdf.jsでPDF化する。「仕様書を作って」「サイト仕様書」「SiteSettingsをドキュメント化」「スクリプトを抜き出して」「プロセス設定を直して反映して」「概要書を作って」「PDFにして」「site-spec」等の依頼で使う。
description: Pleasanterのサイト設定JSONget-site-config.js / api-site-get の getsite レスポンスから、SiteSettingsを読み解いたMarkdown仕様書を生成し、Scripts/Styles/ServerScripts/Processesの抽出・編集・反映extract-site-config.js / build-desired-config.js / apply-site-config.jsを行う。また、ユーザー/組織/グループのマスターデータ取得get-master-data.js、別サイトへのSiteSettings丸ごと適用や項目の追加・連番リネームといった構造変更restructure-site-*.js / apply-to-site-*.js、権限Permissionsの名前指定での追加apply-permission-grant.jsにも対応する。「仕様書を作って」「サイト仕様書」「SiteSettingsをドキュメント化」「スクリプトを抜き出して」「プロセス設定を直して反映して」「別サイトに丸ごと適用して」「項目を追加して連番をズラして」「この人と同じ権限を追加して」「site-spec」等の依頼で使う。
---
# Pleasanter サイト仕様書生成
# Pleasanter サイト仕様書生成・構成管理
`./configs/site-{SiteId}_latest.json``getsite` APIレスポンス、`Response.Data` 配下にサイト情報)を読み解き、
`./configs/site-{SiteId}_spec.md` として日本語のMarkdown仕様書を出力する。
`configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json``getsite` APIレスポンス、`Response.Data` 配下にサイト情報)を読み解き、
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` として日本語のMarkdown仕様書を出力する。
## フォルダ構成configs/ 配下)
```
configs/
master/ … ユーザー/組織/グループのマスターデータget-master-data.js
master_users.json
master_depts.json
master_groups.json
site-{SiteId}_{サイト名}/
sitesettings/ … getsiteの生JSONget-site-config.jsが保存。latest.json + タイムスタンプ付き履歴)
site-{SiteId}_latest.json
site-{SiteId}_{timestamp}.json
scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
… extract-site-config.js による分割済みファイル(従来通り)
docs/ … 仕様書(.md)や改善要望プラン等、sitesettings以外の関連ドキュメント
modify/{リクエストラベル}/ … 修正依頼のたびに新規作成。変更前後のconfigファイル・
差分プレビュー・送信結果に加え、summary.md後述
必ずひとまとめに保管する
```
修正を1件でも反映した場合、`modify/{リクエストラベル}/` フォルダに機械可読な生JSON差分プレビュー・送信結果だけでなく、
人間が読めるサマリー `summary_{timestamp}.md` を必ず作成・保存する(ユーザーの明示指示が無くても毎回行う)。
`{timestamp}` は同じ反映で生成される送信結果ファイル(`site-{SiteId}_full_update_result_{timestamp}.json` 等)と
揃える。`summary.md` のような固定名で毎回上書きせず、反映のたびに別ファイルとして残す。記載内容の目安:
- 対象サイト・反映日時・反映方法実行したコマンド・反映結果HTTPステータス・メッセージ
- 依頼内容ごとの変更点(変更前 → 変更後を表や箇条書きで)
- 保留・見送りにした事項(あれば)
`.md`ファイルのため、内容を原始人口調で書くか通常の日本語で書くかは、他のテキストファイル生成と同様に一度だけユーザーに確認する。
パス解決は `site-paths.js` に共通化されている(`findSiteDir` / `ensureSiteDir` / `sitesettingsDir` / `docsDir` /
`newModifyDir` / `latestModifyDir` / `latestJsonPath`)。新しいスクリプトを書くときはここを再利用し、
パス組み立てを重複させない。
## 入力の特定
1. ユーザーがJSONファイルパスやSiteIdを指定した場合はそれを使う。
2. 指定がなければ `./configs/site-*_latest.json` を探す。複数ある場合はユーザーに確認する。
3. `./configs/site-{SiteId}_latest.json` が無ければ、先に `node get-site-config.js` の実行を提案する
2. 指定がなければ `configs/site-*/sitesettings/site-*_latest.json` を探す。複数ある場合はユーザーに確認する。
3. 対象サイトの `sitesettings/site-{SiteId}_latest.json` が無ければ、先に `node get-site-config.js` の実行を提案する
(エンドポイントは `/api/items/{SiteId}/getsite` を使うこと。`/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物なので注意
## 出力ファイル
`./configs/site-{SiteId}_spec.md`(既存があれば上書きしてよい)。
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md`(既存があれば上書きしてよい)。
## 仕様書の構成(この順番・見出しレベルを踏襲する)
```markdown
# サイト仕様書:{Title}SiteId: {SiteId}
- 取得元データ: [site-{SiteId}_latest.json](./site-{SiteId}_latest.json)
- 取得元データ: [site-{SiteId}_latest.json](../sitesettings/site-{SiteId}_latest.json)
- 取得日時: {UpdatedTime}(サイト更新日時)/設定バージョン `Ver: {Ver}`
## 1. サイト基本情報
@ -32,7 +65,10 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
DisableCrossSearch, Creator/Updator, CreatedTime, SiteSettings.Version を表形式で)
## 2. アクセス権限Permissions
Permissions配列を 種別/対象ID/権限レベル の表に分解。
Permissions配列を 種別/対象ID/権限レベル の表に分解。可能であれば `resolve-names.js`
`resolveName` でユーザー名・組織名・グループ名も併記する。
InheritPermissionが自サイト以外を指している場合は、権限の実体はそちらのサイトにある旨を明記し、
継承元サイトのPermissionsも取得できていれば併せて記載する。
「権限値はビットフラグの加算方式のため正確な意味は環境のロール定義に依存する」旨を注記)
## 3. 画面構成
@ -67,7 +103,7 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
「参照先サイトの{From}列の値を、このサイトの{To}列にコピーする」という関係性を文章でも補足)
## 8. プロセス設定Processes
(存在する場合のみ。定義ファイルへのリンク configs/site-{SiteId}_{サイト名}/processes.json を明記した上で、
(存在する場合のみ。定義ファイルへのリンク ../processes.json を明記した上で、
Id/Name・DisplayName/実行条件(CurrentStatus→ChangedStatus)/動作(OnClick等)/成功メッセージ を表で)
## 9. スタイルStyles
@ -109,6 +145,10 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
| Title | タイトル |
| Comments | コメント欄 |
「予定/実行/無し/備考」の4列1組パターン`DateNNN`/`Date(NNN+100)`/`Check(NNN+100)`/`ClassNNN`、NNNは3桁ゼロ埋め連番
繰り返し登場するサイトもある。この場合 `DateNNN.Description` が入力担当(部署名等)、`DateNNN.GridLabelText` が
項目の表示名を表す。詳細は下記「クロスサイト構造変更(新項目追加・連番リネーム)」を参照。
## 注意事項
- SiteSettingsに存在しない項目・空の設定例: `ClassHash`, `NumHash` などのラベル定義ハッシュが `{}`)は
@ -116,91 +156,33 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
- 表形式を基本とし、箇条書きより表を優先する(今回のレビューで「編集画面レイアウトは項目名とラベル名を併記」との
指摘を受けた経緯があるため、内部名とラベル名は必ず併記する)。
- 出力言語は日本語。
- 複数サイトが関連する仕様書(「関連するテーブルと合わせて」等の依頼)を作る場合は、対象サイトの`Links`設定だけでなく、
ServerScripts内で`items.Get(masterTableId)`のように直接SiteIdをハードコードして参照している「正式なLinks設定に現れない
暗黙的なテーブル参照」も洗い出し、別セクションとして明記する。あわせて`ParentId`を辿ってサイトツリー上の位置関係も
簡潔に図示するとレビューしやすい。
## 概要書(業務担当者向け機能説明)の生成
技術仕様書spec.mdとは別に、「概要書を作って」「機能的な説明がほしい」等の依頼があった場合は、
`ColumnName`ClassA, DateB, Class001等の内部コードやJSON設定値・スクリプト実装といった技術的要素を一切排除し、
ラベル名と業務フローのみで構成した概要書を作成する。
### 出力ファイル
`./docs/site-{SiteId}_overview.md``configs/`ではなく`docs/`フォルダに置く。`configs/`は秘密情報を含み得るため
`.gitignore`済みだが、`docs/`配下の概要書・PDFは共有物としてGit管理対象にする想定
`docs/`フォルダが無ければ作成する。
### 概要書の構成
```markdown
# 概要書:{Title}
- 技術仕様(項目のシステム上の名称・設定値等)は [{spec.mdへのリンク}](../configs/site-{SiteId}_spec.md) を参照。
本書は業務担当者向けに、機能面のみを平易に説明するものです。
## 1. これは何か
業務的にこのサイトが何のためのものかを2〜4文で
## 2. 登場する情報(入力項目)
(分類/項目(ラベル名のみ)/内容 の表。ColumnNameは記載しない。Hide項目でも
業務上意味のあるもの(自動反映される裏項目等)は「内容」欄で触れる)
## 3. 業務の流れ(ボタン操作)
(操作/状態の変化(ラベル値同士、例:「新規作成→図確回覧」)/説明 の表。
Processesの内部処理・スクリプト名には触れず、「何が起きるか」を業務言葉で説明する
## 4. 通知される場面
Notificationsを「◯◯時に誰に何が届くか」という箇条書きに変換。メール本文・件名の技術的書式は転記しない
## 5. 関連する仕組み(マスタ連携)
Links・暗黙的テーブル参照を「◯◯マスタ何を自動反映するか」という業務目線の箇条書きに変換。
SiteIdやColumnNameは書かず、サイト名Titleとその役割のみ記載。
兄弟サイト同じ設計パターンの別サイトがあれば、違いを1文で触れる
```
### 作成時の注意
- 表内の「項目」列は必ずラベル名LabelTextのみ。ラベル未設定の項目は概要書には載せない業務上意味を説明できないため
- Statusの遷移は内部コード100/300/900等を書かず、ラベル値新規作成完了 等)同士の矢印で表現する。
- スクリプト内で見つけた秘密情報APIキー等は概要書には一切転記しないそもそも概要書はスクリプト実装に触れない
- 複数サイト分をまとめて依頼された場合、共通する業務フローは重複して書いてよいが、各サイト固有の違い(段階数の違い、
追加の管理項目など)を各概要書の末尾で明示し、他サイトとの差分が一目で分かるようにする。
## PDF化
概要書または他のMarkdownをPDF化する依頼があった場合は、プロジェクトルートの `md-to-pdf.js` を使う。
```
node md-to-pdf.js ./docs/site-{SiteId}_overview.md
```
- 追加npmパッケージ非依存本プロジェクトに`package.json`が無いことに合わせた設計。Markdown→HTML変換は
スクリプト内の簡易パーサ(見出し/表/太字/リンク/箇条書き/水平線/段落で行い、PDF化はOS標準のMicrosoft Edge
無ければGoogle Chromeのヘッドレス印刷機能を使う。
- 出力は入力と同じフォルダに`{同名}.pdf`中間HTMLは一時フォルダに作成し、変換後に自動削除
- 複数ファイルを一度に渡せる(`node md-to-pdf.js ./docs/*.md`)。
- **既知の落とし穴**: フォルダパスに`#`を含む場合(例: `#GitHub`)、素朴に`file://` + パス文字列を連結すると
`#`以降がURLフラグメントと誤認識され、無関係なページが開かれてPDFの中身が空別物になる。
`md-to-pdf.js`は`url.pathToFileURL()`で正しくエンコードしているため通常は問題にならないが、
同種のブラウザ自動化コードを新たに書く場合は同じ罠に注意する。
## 関連ツールScripts/Styles/ServerScripts/Processesの抽出・編集・反映
仕様書化とは別に、「スクリプトを個別ファイルに分けて」「プロセス設定を直して反映して」等の依頼では、
以下のツールチェーンを使う(詳細は `README.md` 参照)。
1. `node get-site-config.js``configs/site-{SiteId}_latest.json` を取得し、続けて自動的に抽出も行う
`extract-site-config.js` が内部から呼ばれ、`configs/site-{SiteId}_{サイト名}/` に以下を分割保存する。未取得なら先にこれを実行)
1. `node get-site-config.js``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` を取得し、
続けて自動的に抽出も行う(`extract-site-config.js` が内部から呼ばれ、`configs/site-{SiteId}_{サイト名}/` に以下を分割保存する。未取得なら先にこれを実行)
- `scripts/{Id}_{Title}.js`(クライアントスクリプト本体)
- `styles/{Id}_{Title}.css`(スタイル本体)
- `serverscripts/{Id}_{Title}.js`(サーバースクリプト本体)
- `html/{GuideName}.html`GridGuide等のガイドHTML。SiteSettingsではなくサイトデータ直下の項目
- `processes.json``SiteSettings.Processes` の定義ファイル、1ファイルにまとめる。分割しない
- `manifest.json`上記ファイルとId/Titleの対応表。再構成に必須
- `config.json``SiteId` はカンマ区切りで複数指定可能。また、取得したサイトの `InheritPermission`
自サイト以外を指している場合、権限の実体を持つそのサイトも自動的に追いかけて取得する(後述)。
- 抽出だけをやり直したい場合(`latest.json`は既に取得済み)は `node extract-site-config.js` を単独実行してもよい
- **注意**: `get-site-config.js` を再実行すると、その時点のサイトの生の状態で `scripts/` `styles/` 等が
上書きされる。あるサイトの内容を「別サイトへ適用するための下書き」として編集している最中に、
同じサイトへ `get-site-config.js` を再実行すると編集内容が失われるので、クロスサイト作業中は
むやみに元サイトを再取得しない(詳しくは後述の「クロスサイト構造変更」参照)。
- **下書きファイルの整理**: `scripts/` `styles/` `serverscripts/` 配下に `_修正` 等のサフィックス付きで
`manifest.json` 未登録の下書きファイルが残っていることがある(過去に本文を直接編集した際の作業ファイル)。
再取得後は、この下書きファイルと対応する正規ファイル(`manifest.json` の `File` に記載された方)を
diffで比較する。内容が完全一致していれば、下書きの編集内容は既にサイトへ反映済みということなので、
その下書きファイルは削除してよい。内容が異なる場合は未反映の編集が残っている可能性があるため、
削除せずユーザーに確認する。
2. ユーザーの指示に従い、`scripts/*.js` 等の中身を直接編集する(`manifest.json` のId/メタ情報は変更しない)。
新規追加の場合は `manifest.json` に対応エントリ(`Id`は既存と重複しない値、`File`名)を追加してからファイルを作成する。
3. `node build-desired-config.js site-{SiteId}` … 分割ファイルを再構成し `desired_config.json` を生成(送信なし)。
@ -209,15 +191,82 @@ node md-to-pdf.js ./docs/site-{SiteId}_overview.md
4. `node apply-site-config.js` … ドライラン確認差分・送信Body・curlコマンド表示のみ、送信なし
5. 内容をユーザーに確認してもらい、問題なければ表示されたcurlコマンドをユーザー自身が手動実行する
### 厳守事項(安全設計)
## 関連ツール:ユーザー/組織/グループのマスターデータ取得
権限Permissionsやスクリプト内の担当者振り分けなどをID表記ではなく名前で扱いたい場合に使う。
1. `node get-master-data.js``configs/master/` へ以下を取得・保存する(ページング自動対応。
`/api/users/get` はレスポンスの `TotalCount` が1回あたりの件数を超えることがあるため、
トップレベルの `Offset` パラメータで全件を取得しきるまでループする)。
- `master_users.json``UserId` / `LoginId` / `Name` / `DeptId` 等)
- `master_depts.json``DeptId` / `DeptCode` / `DeptName`
- `master_groups.json``GroupId` / `GroupName` / `GroupMembers[]` / `GroupChildren[]`
- 参照した公式マニュアル: [ユーザ取得(全て)](https://pleasanter.org/ja/manual/api-user-get-all) /
[組織取得](https://pleasanter.org/ja/manual/api-dept-get) / [グループ取得](https://pleasanter.org/ja/manual/api-group-get)
2. `resolve-names.js` をスクリプトから `require` して使う:
- `loadMasterData(baseDir)` … 3つの `master_*.json` を読み込む
- `resolveName(master, { Type: "User"|"Dept"|"Group", Id })` … IDから名前を引く
- `resolveId(master, { Type, Name })` … 名前から一致するIDの配列を引く完全一致。0件/複数件はエラーとして扱う)
## 関連ツール:クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用)
「Aサイトを土台に項目を追加してBサイトへ適用して」「この項目の後ろに新しい項目を追加して、以降の連番を
ズラして」のような依頼で使うパターン。2026-07-16に SiteId 480111 → 496626 への適用(「解体紹介料申請」項目の
追加・連番リネーム・GridDesign一括改修・改善要望3件の反映で実際に使った構成を一般化したもの。
`restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` は、その際の実例
(そのまま次回動かせるテンプレートとして残っている)。**次回別の依頼が来たら、これらをコピーして
対象サイトIDやフェーズの中身を書き換えて使う**(汎用パラメータ化はしていない。要望ごとに列の増減や
チェックリストの中身が違うため、都度スクリプトとして書き下ろす方が確実)。
### 全体の流れ
1. **マスターデータ+サイト情報の取得**: `node get-master-data.js``node get-site-config.js`
`config.json` の `SiteId` を一時的に `"元サイトId,対象サイトId"` にして実行し、終わったら単一IDへ戻す
`InheritPermission` が自サイト以外を指していれば、その継承元サイトも自動的に追いかけて取得される。
2. **抽出済みScripts/Stylesの編集**: 元サイトの `configs/site-{元SiteId}_.../scripts/*.js` 等を直接編集する。
⚠️ ここは元サイト自身の抽出フォルダなので、元サイトを `get-site-config.js` で再取得すると
編集内容が消える。編集後は速やかに restructure スクリプト(次項)を走らせて `modify/` フォルダへ
本文を取り込んでおくこと。
3. **`restructure-site-{元SiteId}.js`(新規作成)**: 元サイトの `sitesettings/latest.json` を読み込み、
構造変更列の挿入・連番リネーム・GridDesign書き換え等を行い、`manifest.json` 経由で
編集済みのScripts/Styles本文を組み込んだ上で、対象サイトの `modify/{リクエストラベル}/` フォルダへ
`site-{元SiteId}_updated.json` として書き出す(`site-paths.js` の `findSiteDir` / `newModifyDir` を使う)。
同じフォルダへ対象サイトの変更前スナップショット(`before_site-{対象SiteId}_latest.json`)も
コピーしておく。書き込み前に、想定した件数増分・重複の有無などの整合性チェックを必ず入れる
今回の実装では「フェーズA:事前ガード」「フェーズH:整合性チェック」として実施)。
4. **`apply-to-site-{対象SiteId}.js`(新規作成)**: `modify/` フォルダの `site-{元SiteId}_updated.json`
`updatesite``Mode:"full"`)で対象サイトへ送る。`Title`/`ReferenceType`/`ParentId`/`InheritPermission`は
**対象サイト自身の現状値**から取得し、`SiteSettings`のみ元サイト由来の内容で置き換える。
`Permissions`は送信ボディに含めない(対象サイトが権限を継承している場合、直接の書き込み対象ではないため。
継承有無は `InheritPermission` で判定する)。デフォルトは差分プレビュー+アーティファクト保存のみ、
`--execute` 指定時のみ実際に送信する。差分プレビュー・送信結果は同じ `modify/` フォルダへ保存する。
5. **権限を名前指定で追加する場合**: `apply-permission-grant.js` のパターンで、対象サイトの
`InheritPermission` が指す継承元サイトに対して行う(対象サイト自身の `Permissions` は空配列=継承の
ままにしておき、直接は変更しない)。`Permissions` はサイトデータ直下の項目で `"Type,Id,PermissionType"`
形式の文字列配列(例: `"User,579,15"`)。`updatesitesettings`部分更新APIはPermissionsを更新対象に
含まないことを公式マニュアルで確認済みのため、`updatesite`(全体更新)を使い、継承元サイトの
`SiteSettings` 等は一切変更せずそのまま再送信しつつ `Permissions` のみ新規エントリを追加する。
⚠️ 公式マニュアルには `updatesite` のリクエストボディで `Permissions` を指定できるという明記が
無いため、実行前に必ずプレビュー出力をユーザーに確認してもらうこと。
6. **内容確認 → 実行**: 生成された `modify/` フォルダの中身(`before_*.json` / `site-*_updated.json` /
`*_diff_*.txt`)をユーザーに提示し、レビューを受けてから `--execute` を実行する。
### 厳守事項(安全設計、クロスサイト構造変更にも適用)
- **`apply-site-config.js` は常にドライラン。Claude自身がAPIへ実送信することはしない**
`ENABLE_ACTUAL_SEND` を有効化する改修や、curlコマンドの代理実行も行わない
反映の最終実行はユーザー自身に委ねる。
`ENABLE_ACTUAL_SEND` を有効化する改修や、curlコマンドの代理実行も行わない。これは変更しない。
- 一方で `apply-to-site-{対象SiteId}.js` / `apply-permission-grant.js` のような**専用の直接実行スクリプト**は、
ユーザーがその場で明示的に「今回は直接実行してよい」と許可した場合に限り作成・実行してよい
2026-07-16の実例。これは **その依頼一回限りの例外** であり、標準運用(ドライラン+手動反映)を
恒久的に変更するものではない。次回以降も、実送信を伴うスクリプトを作る/`--execute` を走らせる前には、
改めてユーザーに差分プレビューを提示し、明示的な許可を得ること。
- `build-desired-config.js` が作る `desired_config.json``Mode:"partial"``updatesitesettings`)専用。
Scripts/Styles/ServerScripts/Processes以外のサイト設定には触れない。
Scripts/Styles/ServerScripts/Processes以外のサイト設定には触れない。Columns/EditorColumnHash/Sections/
GridColumns/Permissions等の構造変更は、この節で説明した `restructure-site-*.js` パターンで行う。
- ガイドHTML`html/*.html`)は `updatesitesettings` では反映できない(サイト直下の項目のため)。
反映が必要なら `Mode:"full"``updatesite`用の別JSONが要ることをユーザーに伝える。自動生成はしない。
- 抽出したスクリプト・サーバースクリプトの中に **APIキーやWebhook URL等の秘密情報がハードコードされている場合がある**
(実例あり)。抽出・編集作業でこれらの値を見つけたら、チャット上にそのまま貼り付けず、ユーザーに直接ファイルを確認してもらう。
また `configs/` フォルダと `config.json``.gitignore` 済みだが、誤ってコミットしないよう注意喚起する。
- 権限変更・別サイトへの丸ごと上書きはブラスト半径が大きい。実行前に必ず差分プレビューを提示し、
対象サイトIDや継承関係`InheritPermission`)の理解が誤っていないか、実行のたびに確認する。

View File

@ -4,14 +4,16 @@
```
① node get-site-config.js
→ configs/site-{SiteId}_latest.json に構成を保存
→ configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json に構成を保存
→ 続けて自動的に extract-site-config.js が実行され、
Scripts / Styles / ServerScripts / Processes / ガイドHTML が
configs/site-{SiteId}_{サイト名}/ 配下に個別ファイルとして分割保存される
→ config.json の SiteId をカンマ区切りにすると、複数サイトをまとめて①の処理でループ取得する
→ 取得したサイトの InheritPermission が自サイト以外を指していれば、権限の実体を持つ
そのサイトも自動的に追いかけて取得する
② その latest.json をClaudeのチャットにアップロード
→ Claudeが構成をレビュー・仕様書化configs/site-{SiteId}_spec.md
→ Claudeが構成をレビュー・仕様書化configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md
③ configs/site-{SiteId}_{サイト名}/ 配下のファイルを直接編集、
またはClaudeに「このスクリプトを◯◯に直して」等と修正を指示
@ -23,20 +25,15 @@
→ 実際には送信せず、差分・送信予定Body・curlコマンドのみ表示常にドライラン
⑥ 内容を確認し、問題なければ表示されたcurlコマンドを手動実行
--- (任意:非エンジニア向けドキュメントが必要な場合) ---
⑦ Claudeに「概要書を作って」と依頼
→ 技術仕様書とは別に、ColumnName等の内部コードを含まない業務向け概要書を
docs/site-{SiteId}_overview.md として生成configs/とは異なりGit管理対象
⑧ node md-to-pdf.js ./docs/site-{SiteId}_overview.md
→ 同名の docs/site-{SiteId}_overview.pdf を生成
```
`extract-site-config.js``node get-site-config.js` の中から自動実行されるため、通常は個別に実行する必要はありません
(取得済みの `latest.json` から抽出をやり直したい場合のみ、単独で `node extract-site-config.js` を実行してください)。
上記はScripts/Styles/ServerScripts/Processesの反映フロー単一サイト内で完結です。項目の追加や連番リネーム、
別サイトへの丸ごと適用、権限の名前指定追加など、より大きな構造変更は
`.claude/skills/pleasanter-site-spec/SKILL.md` の「クロスサイト構造変更」の節を参照してください。
## 初回セットアップ
1. `config.example.json``config.json` にコピー
@ -53,22 +50,27 @@
|---|---|
| `config.json` | 接続情報BaseUrl/SiteId/ApiKey※Git管理・共有厳禁 |
| `config.example.json` | 上記のテンプレート |
| `get-site-config.js` | サイト構成取得(`getsite``configs/site-{SiteId}_latest.json` に保存し、続けて `extract-site-config.js` を自動実行。`SiteId` をカンマ区切りにすると全サイトを1回の実行で順次取得 |
| `site-paths.js` | `configs/` 配下のフォルダ構成(後述)を解決する共通ヘルパー。他のスクリプトから`require`して使う |
| `get-site-config.js` | サイト構成取得(`getsite``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` に保存し、続けて `extract-site-config.js` を自動実行。`SiteId` をカンマ区切りにすると全サイトを1回の実行で順次取得。`InheritPermission` が自サイト以外を指していれば継承元サイトも自動追跡 |
| `extract-site-config.js` | `latest.json` から Scripts/Styles/ServerScripts/Processes/ガイドHTML を `configs/site-{SiteId}_{サイト名}/` に個別ファイル分割(`get-site-config.js` から自動呼び出し、または単独実行も可) |
| `build-desired-config.js` | 分割ファイルを再構成し `desired_config.json` を生成(**送信は行わない** |
| `apply-site-config.js` | 希望構成JSONとの差分表示・送信内容確認**送信は行わない** |
| `apply-desired-config-full.js` | `desired_config.json`のScripts/Styles/ServerScripts/Processesを、現状サイト全体getsiteに上書きマージし`updatesite`(全体更新)で反映する専用実行スクリプト。`updatesitesettings`部分更新がこの環境で反映されない問題下記「注意事項」参照を踏まえた標準の反映手段。Columns/GridColumns/Permissions等は現状のまま変更しない。デフォルトは差分プレビューのみ、`--execute`指定時のみ実際に送信する |
| `get-master-data.js` | ユーザー/組織/グループのマスターデータを取得し `configs/master/` へ保存(ページング自動対応) |
| `resolve-names.js` | `configs/master/` のマスターデータを使い、ID⇔名前を相互変換するユーティリティ`require`して使う) |
| `restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` | クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用・権限の名前指定追加)の実例。次回同様の依頼が来たらコピーして書き換える。詳細は `.claude/skills/pleasanter-site-spec/SKILL.md` 参照 |
| `desired_config.json` | Claude、または `build-desired-config.js` が生成する「希望構成」 |
| `configs/site-{SiteId}_latest.json` | 直近取得した現状構成Claudeへのアップロード対象 |
| `configs/site-{SiteId}_spec.md` | SiteSettingsを読み解いたMarkdown仕様書 |
| `configs/master/master_{users,depts,groups}.json` | ユーザー/組織/グループのマスターデータ |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` | 直近取得した現状構成Claudeへのアップロード対象 |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_{timestamp}.json` | 取得時点ごとの履歴(タイムスタンプ付き) |
| `configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` | SiteSettingsを読み解いたMarkdown仕様書。改善要望プラン等その他ドキュメントもここに置く |
| `configs/site-{SiteId}_{サイト名}/manifest.json` | 分割ファイルの一覧Id/Title/ファイル名の対応表) |
| `configs/site-{SiteId}_{サイト名}/scripts/*.js` | クライアントスクリプト本体1ファイル=1スクリプト |
| `configs/site-{SiteId}_{サイト名}/styles/*.css` | スタイル本体 |
| `configs/site-{SiteId}_{サイト名}/serverscripts/*.js` | サーバースクリプト本体 |
| `configs/site-{SiteId}_{サイト名}/html/*.html` | ガイドHTMLGridGuide等。値が空でないもののみ |
| `configs/site-{SiteId}_{サイト名}/processes.json` | プロセス設定(`SiteSettings.Processes` の定義ファイル) |
| `md-to-pdf.js` | Markdownファイルを同名の`.pdf`に変換追加npmパッケージ非依存、OS標準のEdge/Chromeのヘッドレス印刷を利用 |
| `docs/site-{SiteId}_overview.md` | 業務担当者向けの概要書ColumnName等の内部コードを含まない機能説明。`configs/`と異なりGit管理対象 |
| `docs/site-{SiteId}_overview.pdf` | 上記概要書のPDF版 |
| `configs/site-{SiteId}_{サイト名}/modify/{リクエストラベル}/` | 修正依頼ごとに新規作成。変更前後のconfig・差分プレビュー・送信結果をまとめて保管 |
## Scripts/Styles/ServerScripts/Processes の反映について
@ -88,14 +90,26 @@
https://pleasanter.org/manual/api-site-update
- サイト設定部分更新: `POST {BaseUrl}/api/items/{SiteId}/updatesitesettings`
https://pleasanter.org/ja/manual/api-update-sitesettings
- ユーザ取得(全て): `POST {BaseUrl}/api/users/get`
https://pleasanter.org/ja/manual/api-user-get-all
(※ ページングはトップレベルの `Offset` パラメータで行う。`View.Offset` ではない点に注意)
- 組織取得: `POST {BaseUrl}/api/depts/get`
https://pleasanter.org/ja/manual/api-dept-get
- グループ取得: `POST {BaseUrl}/api/groups/get`
https://pleasanter.org/ja/manual/api-group-get
## 安全設計について
- `apply-site-config.js`**常にドライラン** です。デフォルトでは実際のAPI呼び出しコード自体に到達しないようガード`ENABLE_ACTUAL_SEND = false` 固定)しています。
- 反映する際は、表示されたcurlコマンドをご自身で実行するか、内容を確認の上でスクリプトを改修してくださいClaudeが安全策なしに自動実行することはありません
- APIキーは `config.json` にのみ保持し、Claudeにアップロードする `latest.json` には含まれませんget時のレスポンスにAPIキーは含まれないため。念のためアップロード前に目視確認することをおすすめします。
- `apply-to-site-*.js` / `apply-permission-grant.js` のような、ユーザーがその場で明示的に許可した場合に限って
作成される「専用の直接実行スクリプト」は例外です(`--execute` 指定時のみ実送信)。これは依頼ごとの一回限りの
許可であり、標準運用(ドライラン+手動反映)を恒久的に変更するものではありません。
## 注意事項
- プリザンターのバージョンにより `get` レスポンスの構造(`Response.Site.SiteSettings` の位置など)が異なる場合があります。初回実行時にコンソールへ出力されるキー構造を確認してください。
- サイト設定の更新系APIは **テナント管理者権限のAPIキー** が必要です一般ユーザーのAPIキーでは403等になります
- ★重要: この環境では `updatesitesettings`(部分更新)が **HTTP 200・成功メッセージを返すにもかかわらず、実際にはScripts/Styles/ServerScriptsの内容がDBに反映されないことを確認済み**です2026-07-16、SiteId 496626で発生。直後に`getsite`し直しても内容・Ver・UpdatedTimeが変化しなかった
Scripts/Styles/ServerScriptsを実際に反映する専用スクリプトを書く場合は、`updatesitesettings`ではなく`updatesite`全体更新。Title/ReferenceType/ParentId/InheritPermission/Permissionsは現状のまま再送信し、SiteSettingsは現状を取得した上でScripts/Styles/ServerScriptsだけ差し替えるを使うこと。反映後は必ず`getsite`し直し、`UpdatedTime`が更新されているか・目的の文字列が含まれているかを確認する。

View File

@ -0,0 +1,101 @@
# プリザンター サイト構成 取得・レビュー・反映ツール
## 全体の流れ
```
① node get-site-config.js
→ configs/site-{SiteId}_latest.json に構成を保存
→ 続けて自動的に extract-site-config.js が実行され、
Scripts / Styles / ServerScripts / Processes / ガイドHTML が
configs/site-{SiteId}_{サイト名}/ 配下に個別ファイルとして分割保存される
→ config.json の SiteId をカンマ区切りにすると、複数サイトをまとめて①の処理でループ取得する
② その latest.json をClaudeのチャットにアップロード
→ Claudeが構成をレビュー・仕様書化configs/site-{SiteId}_spec.md
③ configs/site-{SiteId}_{サイト名}/ 配下のファイルを直接編集、
またはClaudeに「このスクリプトを◯◯に直して」等と修正を指示
④ node build-desired-config.js site-{SiteId}
→ 分割ファイルを再構成し、desired_config.json を生成(送信は行わない)
⑤ node apply-site-config.js
→ 実際には送信せず、差分・送信予定Body・curlコマンドのみ表示常にドライラン
⑥ 内容を確認し、問題なければ表示されたcurlコマンドを手動実行
--- (任意:非エンジニア向けドキュメントが必要な場合) ---
⑦ Claudeに「概要書を作って」と依頼
→ 技術仕様書とは別に、ColumnName等の内部コードを含まない業務向け概要書を
docs/site-{SiteId}_overview.md として生成configs/とは異なりGit管理対象
⑧ node md-to-pdf.js ./docs/site-{SiteId}_overview.md
→ 同名の docs/site-{SiteId}_overview.pdf を生成
```
`extract-site-config.js``node get-site-config.js` の中から自動実行されるため、通常は個別に実行する必要はありません
(取得済みの `latest.json` から抽出をやり直したい場合のみ、単独で `node extract-site-config.js` を実行してください)。
## 初回セットアップ
1. `config.example.json``config.json` にコピー
2. `BaseUrl` / `SiteId` / `ApiKey` を実環境の値に書き換え
- `SiteId` は単一サイトなら数値(例: `12345`)、複数サイトをまとめて取得したい場合は
カンマ区切りの文字列(例: `"12345,23456,34567"`)で指定できます
- APIキーの発行方法: https://pleasanter.org/manual/api-key
- APIキー発行にはテナント管理者権限が必要ですサイト設定の更新系APIを使う場合
3. Node.js 18以降がインストールされていることを確認組み込みfetchを使用、追加パッケージ不要
## ファイル構成
| ファイル | 役割 |
|---|---|
| `config.json` | 接続情報BaseUrl/SiteId/ApiKey※Git管理・共有厳禁 |
| `config.example.json` | 上記のテンプレート |
| `get-site-config.js` | サイト構成取得(`getsite``configs/site-{SiteId}_latest.json` に保存し、続けて `extract-site-config.js` を自動実行。`SiteId` をカンマ区切りにすると全サイトを1回の実行で順次取得 |
| `extract-site-config.js` | `latest.json` から Scripts/Styles/ServerScripts/Processes/ガイドHTML を `configs/site-{SiteId}_{サイト名}/` に個別ファイル分割(`get-site-config.js` から自動呼び出し、または単独実行も可) |
| `build-desired-config.js` | 分割ファイルを再構成し `desired_config.json` を生成(**送信は行わない** |
| `apply-site-config.js` | 希望構成JSONとの差分表示・送信内容確認**送信は行わない** |
| `desired_config.json` | Claude、または `build-desired-config.js` が生成する「希望構成」 |
| `configs/site-{SiteId}_latest.json` | 直近取得した現状構成Claudeへのアップロード対象 |
| `configs/site-{SiteId}_spec.md` | SiteSettingsを読み解いたMarkdown仕様書 |
| `configs/site-{SiteId}_{サイト名}/manifest.json` | 分割ファイルの一覧Id/Title/ファイル名の対応表) |
| `configs/site-{SiteId}_{サイト名}/scripts/*.js` | クライアントスクリプト本体1ファイル=1スクリプト |
| `configs/site-{SiteId}_{サイト名}/styles/*.css` | スタイル本体 |
| `configs/site-{SiteId}_{サイト名}/serverscripts/*.js` | サーバースクリプト本体 |
| `configs/site-{SiteId}_{サイト名}/html/*.html` | ガイドHTMLGridGuide等。値が空でないもののみ |
| `configs/site-{SiteId}_{サイト名}/processes.json` | プロセス設定(`SiteSettings.Processes` の定義ファイル) |
| `md-to-pdf.js` | Markdownファイルを同名の`.pdf`に変換追加npmパッケージ非依存、OS標準のEdge/Chromeのヘッドレス印刷を利用 |
| `docs/site-{SiteId}_overview.md` | 業務担当者向けの概要書ColumnName等の内部コードを含まない機能説明。`configs/`と異なりGit管理対象 |
| `docs/site-{SiteId}_overview.pdf` | 上記概要書のPDF版 |
## Scripts/Styles/ServerScripts/Processes の反映について
- `build-desired-config.js` が生成する `desired_config.json``Mode:"partial"``updatesitesettings`)です。
Scripts / Styles / ServerScripts / Processes の4項目のみを対象とし、それ以外のサイト設定GridColumns等には触れません。
- ガイドHTML`html/*.html`はSiteSettingsではなく**サイトデータ直下**の項目のため、`updatesitesettings` では反映できません。
反映したい場合は `Mode:"full"``updatesite`)用の別の `desired_config.json` を用意し、`Title`/`ReferenceType` 等の必須項目とあわせて指定してください。
- 抽出(`extract-site-config.js`)はその時点のスナップショットです。抽出後に元サイトが他の人・画面から変更されている可能性がある場合は、
反映前に `node get-site-config.js``node extract-site-config.js` を再実行し、最新状態を取り直してから編集してください。
## 参照した公式API仕様
- サイト情報取得: `POST {BaseUrl}/api/items/{SiteId}/getsite`
https://pleasanter.org/ja/manual/api-site-get
(※ `/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物
- サイト全体更新: `POST {BaseUrl}/api/items/{SiteId}/updatesite`
https://pleasanter.org/manual/api-site-update
- サイト設定部分更新: `POST {BaseUrl}/api/items/{SiteId}/updatesitesettings`
https://pleasanter.org/ja/manual/api-update-sitesettings
## 安全設計について
- `apply-site-config.js`**常にドライラン** です。デフォルトでは実際のAPI呼び出しコード自体に到達しないようガード`ENABLE_ACTUAL_SEND = false` 固定)しています。
- 反映する際は、表示されたcurlコマンドをご自身で実行するか、内容を確認の上でスクリプトを改修してくださいClaudeが安全策なしに自動実行することはありません
- APIキーは `config.json` にのみ保持し、Claudeにアップロードする `latest.json` には含まれませんget時のレスポンスにAPIキーは含まれないため。念のためアップロード前に目視確認することをおすすめします。
## 注意事項
- プリザンターのバージョンにより `get` レスポンスの構造(`Response.Site.SiteSettings` の位置など)が異なる場合があります。初回実行時にコンソールへ出力されるキー構造を確認してください。
- サイト設定の更新系APIは **テナント管理者権限のAPIキー** が必要です一般ユーザーのAPIキーでは403等になります

View File

@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(node -c get-site-config.js)",
"Bash(node get-site-config.js \"C:\\\\Users\\\\KA5AC~1.NOG\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-k-nogi--GitHub-ken-nogi-ClaudePleasanter-MSS-------\\\\4612346a-d27e-4254-ab27-863e0d44e52a\\\\scratchpad\\\\config_492045.json\")",
"Bash(echo \"exit:$?\")",
"Bash(node get-site-config.js \"C:\\\\Users\\\\KA5AC~1.NOG\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-k-nogi--GitHub-ken-nogi-ClaudePleasanter-MSS-------\\\\4612346a-d27e-4254-ab27-863e0d44e52a\\\\scratchpad\\\\config_475384.json\")",
"Bash(node get-site-config.js \"C:\\\\Users\\\\KA5AC~1.NOG\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-k-nogi--GitHub-ken-nogi-ClaudePleasanter-MSS-------\\\\4612346a-d27e-4254-ab27-863e0d44e52a\\\\scratchpad\\\\config_492169.json\")"
]
}
}

View File

@ -0,0 +1,223 @@
---
name: pleasanter-site-spec
description: Pleasanterのサイト設定JSONget-site-config.js / api-site-get の getsite レスポンスから、SiteSettingsを読み解いたMarkdown仕様書を生成し、Scripts/Styles/ServerScripts/Processesの抽出・編集・反映extract-site-config.js / build-desired-config.js / apply-site-config.jsを行う。また、技術仕様書とは別に業務担当者向けの概要書docs/site-{SiteId}_overview.mdを作成し、md-to-pdf.jsでPDF化する。「仕様書を作って」「サイト仕様書」「SiteSettingsをドキュメント化」「スクリプトを抜き出して」「プロセス設定を直して反映して」「概要書を作って」「PDFにして」「site-spec」等の依頼で使う。
---
# Pleasanter サイト仕様書生成
`./configs/site-{SiteId}_latest.json``getsite` APIレスポンス、`Response.Data` 配下にサイト情報)を読み解き、
`./configs/site-{SiteId}_spec.md` として日本語のMarkdown仕様書を出力する。
## 入力の特定
1. ユーザーがJSONファイルパスやSiteIdを指定した場合はそれを使う。
2. 指定がなければ `./configs/site-*_latest.json` を探す。複数ある場合はユーザーに確認する。
3. `./configs/site-{SiteId}_latest.json` が無ければ、先に `node get-site-config.js` の実行を提案する
(エンドポイントは `/api/items/{SiteId}/getsite` を使うこと。`/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物なので注意
## 出力ファイル
`./configs/site-{SiteId}_spec.md`(既存があれば上書きしてよい)。
## 仕様書の構成(この順番・見出しレベルを踏襲する)
```markdown
# サイト仕様書:{Title}SiteId: {SiteId}
- 取得元データ: [site-{SiteId}_latest.json](./site-{SiteId}_latest.json)
- 取得日時: {UpdatedTime}(サイト更新日時)/設定バージョン `Ver: {Ver}`
## 1. サイト基本情報
TenantId, Title, ReferenceType, ParentId, InheritPermission, Publish,
DisableCrossSearch, Creator/Updator, CreatedTime, SiteSettings.Version を表形式で)
## 2. アクセス権限Permissions
Permissions配列を 種別/対象ID/権限レベル の表に分解。
「権限値はビットフラグの加算方式のため正確な意味は環境のロール定義に依存する」旨を注記)
## 3. 画面構成
### 3.1 一覧画面GridColumns
GridColumns配列を 順番/列名/表示ラベル の表に。ラベルはColumns配列から解決、
無ければ「項目種別の判定ルール」のデフォルト名を使う)
### 3.2 編集画面レイアウトEditorColumnHash / Sections
EditorColumnHashの各タブ配列を 順番/項目名(内部名)/ラベル名 の表に。
`_Section-N` はラベル名欄に「―(セクション「{LabelText}」開始)」と記載。
続けてSections配列を Id/ラベル/AllowExpand/Expand の表で示す)
### 3.3 タイトル表示TitleColumns / TitleSeparator
TitleColumns配列をTitleSeparatorで連結した式を明記
## 4. 項目定義Columns
Columns配列を 列名(内部)/種別/表示ラベル/説明・入力ガイド/備考 の表に。
種別は下記「項目種別の判定ルール」で推定。
備考にはNoWrap, FieldCss, ExtendedHtmlAfterControlの有無などを記載
### 4.1 項目ごとの補足HTMLExtendedHtmlAfterControl
(該当項目があれば 列名/色styleやclassから読み取れれば/内容 の表。無ければこの節は省略)
## 5. 作成・更新権限(フィールド単位)
PermissionForCreating / PermissionForUpdating を表で。無ければ「設定なし」と明記)
## 6. 集計設定Aggregations
存在する場合のみ。Id/GroupBy/Type/Target を表で)
## 7. 他サイト連携Links / ルックアップ)
(存在する場合のみ。起点列/参照先SiteId/参照先の値→自コピー先/JsonFormat を表で。
「参照先サイトの{From}列の値を、このサイトの{To}列にコピーする」という関係性を文章でも補足)
## 8. プロセス設定Processes
(存在する場合のみ。定義ファイルへのリンク configs/site-{SiteId}_{サイト名}/processes.json を明記した上で、
Id/Name・DisplayName/実行条件(CurrentStatus→ChangedStatus)/動作(OnClick等)/成功メッセージ を表で)
## 9. スタイルStyles
存在する場合のみ。Id/Title/適用範囲(New/Edit/Index等のフラグ)/ファイルへのリンクを表で。
CSS本体はMarkdownに貼らず、抽出済みファイルへのリンクのみとする
## 10. スクリプトScriptsサーバースクリプトServerScripts
存在する場合のみ。Id/Title/適用画面/ファイルへのリンクを表で。
スクリプト本体はMarkdownに貼らず、抽出済みファイルへのリンクのみとする。
概要は「何をするスクリプトか」を1〜2行で要約。
本文中にAPIキー・Webhook URL・パスワード等の秘密情報らしき文字列`API_KEY`, `SECRET`, `Bearer `,
URLに埋め込まれたトークン等を見つけた場合は、値そのものをMarkdownに転記せず、
「気になる点」に必ずセキュリティ上の指摘として記載する)
## 11. その他設定
NoDisplayIfReadOnly, HideLink, SectionLatestId, 各種ガイド文の設定有無, Commentsの使用有無など
---
## 気になる点(レビュー観点)
(プレースホルダーらしき文言、権限設定の偏り、項目単位の権限の抜け、
未設定のガイド文など、気づいた点を箇条書きで)
```
## 項目種別の判定ルールColumns内 ColumnName のプレフィックスから推定)
| プレフィックス/名前 | 種別 |
|---|---|
| ClassA〜Z | 文字列(分類) |
| Class001〜040等3桁連番 | 文字列(拡張分類)。`Hide: true`の場合が多く、他項目のルックアップ先の値を裏で保持する用途が多い |
| NumA〜Z | 数値 |
| DateA〜Z | 日付 |
| DescriptionA〜Z | 説明(複数行文字列) |
| CheckA〜Z | チェックボックス |
| AttachmentsA〜Z | 添付ファイル |
| Owner, Manager, Assignee等 | ユーザー選択 |
| Status | 状態 |
| {対象}IdResultId, IssueId等 | 自動採番ID |
| Title | タイトル |
| Comments | コメント欄 |
## 注意事項
- SiteSettingsに存在しない項目・空の設定例: `ClassHash`, `NumHash` などのラベル定義ハッシュが `{}`)は
「標準ラベルのまま」として仕様書に明記し、存在しないかのように省略しない。
- 表形式を基本とし、箇条書きより表を優先する(今回のレビューで「編集画面レイアウトは項目名とラベル名を併記」との
指摘を受けた経緯があるため、内部名とラベル名は必ず併記する)。
- 出力言語は日本語。
- 複数サイトが関連する仕様書(「関連するテーブルと合わせて」等の依頼)を作る場合は、対象サイトの`Links`設定だけでなく、
ServerScripts内で`items.Get(masterTableId)`のように直接SiteIdをハードコードして参照している「正式なLinks設定に現れない
暗黙的なテーブル参照」も洗い出し、別セクションとして明記する。あわせて`ParentId`を辿ってサイトツリー上の位置関係も
簡潔に図示するとレビューしやすい。
## 概要書(業務担当者向け機能説明)の生成
技術仕様書spec.mdとは別に、「概要書を作って」「機能的な説明がほしい」等の依頼があった場合は、
`ColumnName`ClassA, DateB, Class001等の内部コードやJSON設定値・スクリプト実装といった技術的要素を一切排除し、
ラベル名と業務フローのみで構成した概要書を作成する。
### 出力ファイル
`./docs/site-{SiteId}_overview.md``configs/`ではなく`docs/`フォルダに置く。`configs/`は秘密情報を含み得るため
`.gitignore`済みだが、`docs/`配下の概要書・PDFは共有物としてGit管理対象にする想定
`docs/`フォルダが無ければ作成する。
### 概要書の構成
```markdown
# 概要書:{Title}
- 技術仕様(項目のシステム上の名称・設定値等)は [{spec.mdへのリンク}](../configs/site-{SiteId}_spec.md) を参照。
本書は業務担当者向けに、機能面のみを平易に説明するものです。
## 1. これは何か
業務的にこのサイトが何のためのものかを2〜4文で
## 2. 登場する情報(入力項目)
(分類/項目(ラベル名のみ)/内容 の表。ColumnNameは記載しない。Hide項目でも
業務上意味のあるもの(自動反映される裏項目等)は「内容」欄で触れる)
## 3. 業務の流れ(ボタン操作)
(操作/状態の変化(ラベル値同士、例:「新規作成→図確回覧」)/説明 の表。
Processesの内部処理・スクリプト名には触れず、「何が起きるか」を業務言葉で説明する
## 4. 通知される場面
Notificationsを「◯◯時に誰に何が届くか」という箇条書きに変換。メール本文・件名の技術的書式は転記しない
## 5. 関連する仕組み(マスタ連携)
Links・暗黙的テーブル参照を「◯◯マスタ何を自動反映するか」という業務目線の箇条書きに変換。
SiteIdやColumnNameは書かず、サイト名Titleとその役割のみ記載。
兄弟サイト同じ設計パターンの別サイトがあれば、違いを1文で触れる
```
### 作成時の注意
- 表内の「項目」列は必ずラベル名LabelTextのみ。ラベル未設定の項目は概要書には載せない業務上意味を説明できないため
- Statusの遷移は内部コード100/300/900等を書かず、ラベル値新規作成完了 等)同士の矢印で表現する。
- スクリプト内で見つけた秘密情報APIキー等は概要書には一切転記しないそもそも概要書はスクリプト実装に触れない
- 複数サイト分をまとめて依頼された場合、共通する業務フローは重複して書いてよいが、各サイト固有の違い(段階数の違い、
追加の管理項目など)を各概要書の末尾で明示し、他サイトとの差分が一目で分かるようにする。
## PDF化
概要書または他のMarkdownをPDF化する依頼があった場合は、プロジェクトルートの `md-to-pdf.js` を使う。
```
node md-to-pdf.js ./docs/site-{SiteId}_overview.md
```
- 追加npmパッケージ非依存本プロジェクトに`package.json`が無いことに合わせた設計。Markdown→HTML変換は
スクリプト内の簡易パーサ(見出し/表/太字/リンク/箇条書き/水平線/段落で行い、PDF化はOS標準のMicrosoft Edge
無ければGoogle Chromeのヘッドレス印刷機能を使う。
- 出力は入力と同じフォルダに`{同名}.pdf`中間HTMLは一時フォルダに作成し、変換後に自動削除
- 複数ファイルを一度に渡せる(`node md-to-pdf.js ./docs/*.md`)。
- **既知の落とし穴**: フォルダパスに`#`を含む場合(例: `#GitHub`)、素朴に`file://` + パス文字列を連結すると
`#`以降がURLフラグメントと誤認識され、無関係なページが開かれてPDFの中身が空別物になる。
`md-to-pdf.js`は`url.pathToFileURL()`で正しくエンコードしているため通常は問題にならないが、
同種のブラウザ自動化コードを新たに書く場合は同じ罠に注意する。
## 関連ツールScripts/Styles/ServerScripts/Processesの抽出・編集・反映
仕様書化とは別に、「スクリプトを個別ファイルに分けて」「プロセス設定を直して反映して」等の依頼では、
以下のツールチェーンを使う(詳細は `README.md` 参照)。
1. `node get-site-config.js``configs/site-{SiteId}_latest.json` を取得し、続けて自動的に抽出も行う
`extract-site-config.js` が内部から呼ばれ、`configs/site-{SiteId}_{サイト名}/` に以下を分割保存する。未取得なら先にこれを実行)
- `scripts/{Id}_{Title}.js`(クライアントスクリプト本体)
- `styles/{Id}_{Title}.css`(スタイル本体)
- `serverscripts/{Id}_{Title}.js`(サーバースクリプト本体)
- `html/{GuideName}.html`GridGuide等のガイドHTML。SiteSettingsではなくサイトデータ直下の項目
- `processes.json``SiteSettings.Processes` の定義ファイル、1ファイルにまとめる。分割しない
- `manifest.json`上記ファイルとId/Titleの対応表。再構成に必須
- 抽出だけをやり直したい場合(`latest.json`は既に取得済み)は `node extract-site-config.js` を単独実行してもよい
2. ユーザーの指示に従い、`scripts/*.js` 等の中身を直接編集する(`manifest.json` のId/メタ情報は変更しない)。
新規追加の場合は `manifest.json` に対応エントリ(`Id`は既存と重複しない値、`File`名)を追加してからファイルを作成する。
3. `node build-desired-config.js site-{SiteId}` … 分割ファイルを再構成し `desired_config.json` を生成(送信なし)。
フォルダ名はサイト名まで含む正式名(`site-{SiteId}_{サイト名}`)だが、`site-{SiteId}` の前方一致指定でも
`configs/`配下から自動解決される
4. `node apply-site-config.js` … ドライラン確認差分・送信Body・curlコマンド表示のみ、送信なし
5. 内容をユーザーに確認してもらい、問題なければ表示されたcurlコマンドをユーザー自身が手動実行する
### 厳守事項(安全設計)
- **`apply-site-config.js` は常にドライラン。Claude自身がAPIへ実送信することはしない**
`ENABLE_ACTUAL_SEND` を有効化する改修や、curlコマンドの代理実行も行わない
反映の最終実行はユーザー自身に委ねる。
- `build-desired-config.js` が作る `desired_config.json``Mode:"partial"``updatesitesettings`)専用。
Scripts/Styles/ServerScripts/Processes以外のサイト設定には触れない。
- ガイドHTML`html/*.html`)は `updatesitesettings` では反映できない(サイト直下の項目のため)。
反映が必要なら `Mode:"full"``updatesite`用の別JSONが要ることをユーザーに伝える。自動生成はしない。
- 抽出したスクリプト・サーバースクリプトの中に **APIキーやWebhook URL等の秘密情報がハードコードされている場合がある**
(実例あり)。抽出・編集作業でこれらの値を見つけたら、チャット上にそのまま貼り付けず、ユーザーに直接ファイルを確認してもらう。
また `configs/` フォルダと `config.json``.gitignore` 済みだが、誤ってコミットしないよう注意喚起する。

View File

@ -0,0 +1,245 @@
/**
* apply-9item-phase1-496626.js
* ------------------------------------------------------------
* site-496626着工要因(東京)v29項目改修のうちitem1,2,3,4,5,6,7,9
* SiteSettings構造変更Sections/Columns/EditorColumnHash/GridColumns/
* FilterColumns/Aggregations/Links/Summaries/Notificationsをまとめて反映する
* item8引越し削除+20工程番号リネームは影響範囲が大きく別スクリプトで扱う
*
* 現在のサイト全体getsiteを取得しin-memoryで変換した上でupdatesite全体更新
* で送信するScripts/Styles/ServerScripts等この改修で対象外のフィールドは
* 取得した現在値をそのまま使うため変更されない
*
* 使い方:
* node apply-9item-phase1-496626.js 差分表示のみ送信なし
* node apply-9item-phase1-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
function transform(ss) {
const log = [];
// ---- item1: ゼロエミ承認 → ゼロエミ交付申請 ----
{
const sec = ss.Sections.find((s) => s.Id === 20);
sec.LabelText = "ゼロエミ交付申請";
const col = ss.Columns.find((c) => c.ColumnName === "Date020");
col.GridLabelText = "ゼロエミ交付申請";
log.push("item1: ゼロエミ承認 -> ゼロエミ交付申請");
}
// ---- item2: 取出有無業者連絡 → 取出業者手配 ----
{
const sec = ss.Sections.find((s) => s.Id === 15);
sec.LabelText = "取出業者手配";
const col = ss.Columns.find((c) => c.ColumnName === "Date015");
col.GridLabelText = "取出業者手配";
log.push("item2: 取出有無業者連絡 -> 取出業者手配");
}
// ---- item5: 物件詳細2 → 補助金項目 ----
{
const col = ss.Columns.find((c) => c.ColumnName === "DescriptionB");
col.LabelText = "補助金項目";
log.push("item5: 物件詳細2 -> 補助金項目");
}
// ---- item6: 一覧からDateD列を非表示 ----
{
const removeDateD = (arr) => {
const idx = arr.indexOf("DateD");
if (idx >= 0) arr.splice(idx, 1);
};
removeDateD(ss.GridColumns);
ss.Views.forEach((v) => {
if (v.GridColumns) removeDateD(v.GridColumns);
});
const colDateD = ss.Columns.find((c) => c.ColumnName === "DateD");
colDateD.GridDesign = "[DateD]\n着工可能:[NumE]\n完成予定:[NumG]";
log.push("item6: DateD列をGridColumns(4箇所)から削除。GridDesignをNumG参照に更新");
}
// ---- item9: 変更・注意点等コメント(DescriptionC)を一覧列から削除 ----
{
const removeDescC = (arr) => {
const idx = arr.indexOf("DescriptionC");
if (idx >= 0) arr.splice(idx, 1);
};
removeDescC(ss.GridColumns);
ss.Views.forEach((v) => {
if (v.GridColumns) removeDescC(v.GridColumns);
});
log.push("item9: DescriptionC列をGridColumns(4箇所)から削除");
}
// ---- item7: NumE/NumF/NumG 再定義 ----
{
const numFIdx = ss.Columns.findIndex((c) => c.ColumnName === "NumF");
ss.Columns[numFIdx].LabelText = "着工可能から完成予定日数";
const numG = { ColumnName: "NumG", LabelText: "契約から完成予定", DefaultInput: "0", Unit: "日" };
ss.Columns.splice(numFIdx + 1, 0, numG);
const genIdx = ss.EditorColumnHash.General.indexOf("NumF");
ss.EditorColumnHash.General.splice(genIdx + 1, 0, "NumG");
log.push("item7: NumF LabelText変更、NumG新規追加(Columns+EditorColumnHash)");
}
// ---- item3: 着工可能日セクション(Id=6)廃止、DateAへ一本化 ----
{
// Columns: DateY, DateZ, ClassY, ClassZ を削除
ss.Columns = ss.Columns.filter(
(c) => !["DateY", "DateZ", "ClassY", "ClassZ"].includes(c.ColumnName)
);
// Sections: Id=6 を削除
ss.Sections = ss.Sections.filter((s) => s.Id !== 6);
// EditorColumnHash.General: _Section-6 と DateY/DateZ/ClassZ/ClassY を削除
// Comments/Title/Class200/Description200 はitem4の処理で一括してSection5末尾へ移設する
ss.EditorColumnHash.General = ss.EditorColumnHash.General.filter(
(t) => !["_Section-6", "DateY", "DateZ", "ClassZ", "ClassY"].includes(t)
);
// DateA を編集可能化読取専用CSSを除去
const colDateA = ss.Columns.find((c) => c.ColumnName === "DateA");
delete colDateA.ExtendedFieldCss;
// FilterColumns / Aggregations / Links / Summaries から ClassZ 関連を削除
ss.FilterColumns = ss.FilterColumns.filter((c) => c !== "ClassZ");
ss.Aggregations = ss.Aggregations.filter((a) => a.GroupBy !== "ClassZ");
ss.Links = ss.Links.filter((l) => l.ColumnName !== "ClassZ");
ss.Summaries = ss.Summaries.filter((s) => s.LinkColumn !== "ClassZ");
// Notifications Id:1着工日更新通知の監視列を DateZ -> DateA に付け替え
// DateZ削除により監視対象が消滅し、通知が永久に発火しなくなるための対応
const notif1 = ss.Notifications.find((n) => n.Id === 1);
if (notif1) {
notif1.MonitorChangesColumns = notif1.MonitorChangesColumns.map((c) =>
c === "DateZ" ? "DateA" : c
);
}
log.push(
"item3: DateY/DateZ/ClassY/ClassZ削除、Section6削除、DateA編集可能化、FilterColumns/Aggregations/Links/Summaries整理、Notification1監視列をDateA化"
);
}
// ---- item4: リードタイム関連情報セクション(Id=7)廃止、Class190-192は存続 ----
{
// Columns: Class113〜Class119 のみ削除Class190〜192は残す
const readtimeCols = ["Class113", "Class114", "Class115", "Class116", "Class117", "Class118", "Class119"];
ss.Columns = ss.Columns.filter((c) => !readtimeCols.includes(c.ColumnName));
// Sections: Id=7 を削除
ss.Sections = ss.Sections.filter((s) => s.Id !== 7);
// EditorColumnHash.General: _Section-7 と Class113〜119 を削除
ss.EditorColumnHash.General = ss.EditorColumnHash.General.filter(
(t) => !["_Section-7", ...readtimeCols].includes(t)
);
// Class190/191/192 を現在の位置から削除し、Section5末尾(DescriptionDの直後)へ移設
ss.EditorColumnHash.General = ss.EditorColumnHash.General.filter(
(t) => !["Class190", "Class191", "Class192"].includes(t)
);
const descDIdx = ss.EditorColumnHash.General.indexOf("DescriptionD");
ss.EditorColumnHash.General.splice(descDIdx + 1, 0, "Class190", "Class191", "Class192");
log.push(
"item4: Class113-119削除、Section7削除、Class190-192をSection5末尾へ移設Notifications3件は宛先存続のため変更なし"
);
}
return log;
}
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` 9項目改修 Phase1item1,2,3,4,5,6,7,9SiteSettings構造変更updatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
const log = transform(siteSettings);
log.forEach((l) => console.log(" " + l));
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "9item-revision");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_phase1_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-9item-phase1-496626.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_phase1_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,190 @@
/**
* apply-9item-phase2-496626.js
* ------------------------------------------------------------
* site-496626着工要因(東京)v29項目改修のitem8
* 引越し工程削除後続20工程の内部カラム番号一括リネーム
* SiteSettings構造変更Sections/Columns/EditorColumnHash/GridColumnsを反映する
*
* 対象: Date028(引越し)を削除しDate02904820工程 Date028047
* 対応する実績列(Date1xx)備考列(Class0xx)無しチェック列(Check1xx)Section Idも
* 全て1つずつ前へリネームする工程名LabelText/GridLabelText自体は変更しない
*
* このスクリプトはSiteSettings構造のみを変更する既存レコードのデータ
* Date029等の実際の値はこの反映だけでは移行されないデータ移行は
* migrate-process028-rename-496626.js を別途実行すること構造変更を先に
* 反映しその直後にデータ移行を実行する運用を想定している
*
* 使い方:
* node apply-9item-phase2-496626.js 差分表示のみ送信なし
* node apply-9item-phase2-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
// ---- リネームマッピング構築 ----
const REMOVE_COLUMNS = new Set(["Date028", "Date128", "Class028", "Check128"]);
const RENAME_MAP = {};
const SECTION_ID_MAP = {};
for (let n = 29; n <= 48; n++) {
const newN = n - 1;
const pad = (x) => String(x).padStart(3, "0");
RENAME_MAP["Date" + pad(n)] = "Date" + pad(newN);
RENAME_MAP["Date" + (n + 100)] = "Date" + (newN + 100);
RENAME_MAP["Class" + pad(n)] = "Class" + pad(newN);
RENAME_MAP["Check" + (n + 100)] = "Check" + (newN + 100);
SECTION_ID_MAP[n] = newN;
}
function renameGridDesign(text) {
if (!text) return text;
return text.replace(/\[([A-Za-z]+\d+)\]/g, (m, colName) => {
return RENAME_MAP[colName] ? `[${RENAME_MAP[colName]}]` : m;
});
}
function renameTokenArray(arr) {
return arr
.filter((t) => t !== "_Section-28" && !REMOVE_COLUMNS.has(t))
.map((t) => {
if (t.startsWith("_Section-")) {
const id = parseInt(t.replace("_Section-", ""), 10);
return SECTION_ID_MAP[id] ? `_Section-${SECTION_ID_MAP[id]}` : t;
}
return RENAME_MAP[t] || t;
});
}
function transform(ss) {
const log = [];
// ---- Columns ----
ss.Columns = ss.Columns.filter((c) => !REMOVE_COLUMNS.has(c.ColumnName));
ss.Columns.forEach((c) => {
if (RENAME_MAP[c.ColumnName]) {
c.ColumnName = RENAME_MAP[c.ColumnName];
}
if (c.GridDesign) {
c.GridDesign = renameGridDesign(c.GridDesign);
}
});
log.push("Columns: Date028系4項目削除、Date029〜048系80項目をリネーム(GridDesign内参照含む)");
// ---- Sections ----
ss.Sections = ss.Sections.filter((s) => s.Id !== 28);
ss.Sections.forEach((s) => {
if (SECTION_ID_MAP[s.Id]) {
s.Id = SECTION_ID_MAP[s.Id];
}
});
log.push("Sections: Id28削除、Id29〜48をId28〜47へリネーム(LabelTextは変更なし)");
// ---- EditorColumnHash5タブ全て ----
["_Tab-1", "_Tab-2", "_Tab-3", "_Tab-4", "_Tab-5"].forEach((tab) => {
if (ss.EditorColumnHash[tab]) {
const before = ss.EditorColumnHash[tab].length;
ss.EditorColumnHash[tab] = renameTokenArray(ss.EditorColumnHash[tab]);
log.push(`EditorColumnHash.${tab}: ${before}トークン -> ${ss.EditorColumnHash[tab].length}トークン`);
}
});
// ---- GridColumnsメイン + View2/5/6 ----
ss.GridColumns = renameTokenArray(ss.GridColumns);
ss.Views.forEach((v) => {
if (v.GridColumns) {
v.GridColumns = renameTokenArray(v.GridColumns);
}
});
log.push("GridColumns: メイン・View2・View5・View6の4箇所をリネーム");
return log;
}
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` 9項目改修 Phase2item8引越し削除+20工程番号リネームupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
const log = transform(siteSettings);
log.forEach((l) => console.log(" " + l));
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "9item-revision");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_phase2_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-9item-phase2-496626.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_phase2_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,127 @@
/**
* apply-desired-config-full.js
* ------------------------------------------------------------
* この環境では updatesitesettings部分更新がHTTP 200を返しても実際には
* 反映されないことを確認済みのためREADME.md注意事項参照
* Scripts/Styles/ServerScripts/Processesの反映には常にこちらを使う
*
* build-desired-config.js が生成した desired_config.json
* Scripts/Styles/ServerScripts/Processes 現在のサイト全体getsite
* 上書きマージしupdatesite全体更新で送信する
* Columns/GridColumns/Permissions等それ以外のSiteSettingsTitle等は
* 現状のまま一切変更しない
*
* 使い方:
* node build-desired-config.js site-{SiteId}_{サイト名} 先にこちらを実行
* node apply-desired-config-full.js 差分表示のみ送信なし
* node apply-desired-config-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const baseDir = path.join(__dirname, "..", "..");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffList(curList, desList, key) {
(curList || []).forEach((c) => {
const d = (desList || []).find((x) => x.Id === c.Id);
const same = d && JSON.stringify(d) === JSON.stringify(c);
console.log(` ${key} Id:${c.Id} (${c.Title || c.Name || ""}) -> 変更${!same}`);
});
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
const SITE_ID = desired.SiteId;
const siteDir = findSiteDir(path.join(baseDir, "configs"), SITE_ID);
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` Scripts/Styles/ServerScripts/Processes 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log("--- Scripts ---");
diffList(siteSettings.Scripts, desired.SiteSettings.Scripts, "Script");
console.log("--- Styles ---");
diffList(siteSettings.Styles, desired.SiteSettings.Styles, "Style");
console.log("--- ServerScripts ---");
diffList(siteSettings.ServerScripts, desired.SiteSettings.ServerScripts, "ServerScript");
console.log("他のSiteSettingsColumns/GridColumns等・Permissions・Title等は一切変更しません。");
siteSettings.Scripts = desired.SiteSettings.Scripts;
siteSettings.Styles = desired.SiteSettings.Styles;
siteSettings.ServerScripts = desired.SiteSettings.ServerScripts;
siteSettings.Processes = desired.SiteSettings.Processes;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "desired-config-full-update");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-desired-config-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,146 @@
/**
* apply-jisseki-label-fix-496626.js
* ------------------------------------------------------------
* site-496626のColumns定義のうちDate111Date148実績日37項目
* LabelTextが実行のままになっているものだけを実績に書き換える専用スクリプト
*
* 現在のサイト全体getsiteを取得し対象37項目のLabelTextだけを書き換えて
* updatesite全体更新で送信するColumns内の対象37項目以外のフィールド
* ColumnName/GridLabelText/Type/各種設定等GridColumns他のSiteSettings
* PermissionsTitle等は一切変更しない
*
* 使い方:
* node apply-jisseki-label-fix-496626.js 差分表示のみ送信なし
* node apply-jisseki-label-fix-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
// 対象: Date111〜Date148
const TARGET_COLUMNS = new Set(
Array.from({ length: 38 }, (_, i) => i + 111).map((n) => `Date${n}`)
);
const OLD_LABEL = "実行";
const NEW_LABEL = "実績";
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` Columns LabelText「実行」→「実績」書き換えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
const changed = [];
const alreadyOk = [];
const missing = [];
TARGET_COLUMNS.forEach((columnName) => {
const column = siteSettings.Columns.find((c) => c.ColumnName === columnName);
if (!column) {
missing.push(columnName);
return;
}
if (column.LabelText === OLD_LABEL) {
column.LabelText = NEW_LABEL;
changed.push(`${columnName} (${column.GridLabelText || ""})`);
} else if (column.LabelText === NEW_LABEL) {
alreadyOk.push(columnName);
} else {
console.log(` [注意] ${columnName} のLabelTextは想定外の値です: "${column.LabelText}"(変更しません)`);
}
});
console.log(`変更対象: ${changed.length}`);
changed.forEach((c) => console.log(` ${c}: 実行 -> 実績`));
if (alreadyOk.length > 0) {
console.log(`既に「実績」: ${alreadyOk.length}`);
}
if (missing.length > 0) {
console.log(`[警告] Columnsに見つからなかった項目: ${missing.join(", ")}`);
}
console.log("他のSiteSettingsColumns内の対象外フィールド・GridColumns・Scripts/Styles/ServerScripts等・Permissions・Title等は一切変更しません。");
if (changed.length === 0) {
console.log("\n変更対象がないため、送信を行いません。");
process.exit(0);
}
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "jisseki-labeltext-fix");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_labeltext_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-jisseki-label-fix-496626.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_labeltext_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,227 @@
/**
* apply-permission-grant.js
* ------------------------------------------------------------
* SiteId 496626 の権限継承先サイトInheritPermissionに対し
* UserId 579 と同じ権限をユーザー名福島 成佳に追加する
* 480111側の継承元426945には適用しない
*
* Permissionsは SiteSettings 配下ではなくサイトデータ直下の項目であり
* updatesitesettings部分更新API はPermissionsを更新対象に含まない
* 公式マニュアルで確認済みそのため本スクリプトは updatesite全体更新を使い
* 継承先サイトの現状の Title/ReferenceType/ParentId/InheritPermission/SiteSettings
* 一切変更せずそのまま再送信しPermissionsのみ新規エントリを追加した配列を送信する
*
* 注意: 公式マニュアルには updatesite のリクエストボディでPermissionsを
* 指定できるという明記が無い記載が省略されている可能性はある
* 実行前に必ずプレビュー出力を確認すること
*
* 差分プレビュー送信結果は権限継承先サイトの modify/{リクエストラベル}/ フォルダへ保存する
* 修正依頼のたびに新しいフォルダを生成する規約省略時はタイムスタンプを自動採番
*
* 使い方:
* node apply-permission-grant.js 差分表示アーティファクト生成のみ送信なし
* node apply-permission-grant.js --execute 上記に加えて実際に送信する
* node apply-permission-grant.js --request=ラベル modifyフォルダ名を明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
const { findSiteDir, latestJsonPath, newModifyDir, latestModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 496626;
const MIRROR_FROM_USER_ID = 579;
const GRANT_TO_USER_NAME = "福島 成佳";
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const sourceSitePath =
latestJsonPath(configsDir, SOURCE_SITE_ID) ||
path.join(configsDir, `site-${SOURCE_SITE_ID}_latest.json`);
const sourceSiteJson = loadJson(sourceSitePath);
const inheritPermissionId = sourceSiteJson?.Response?.Data?.InheritPermission;
if (!inheritPermissionId) {
console.error(`[エラー] ${sourceSitePath} からInheritPermissionを取得できませんでした。`);
process.exit(1);
}
const targetPath =
latestJsonPath(configsDir, inheritPermissionId) ||
path.join(configsDir, `site-${inheritPermissionId}_latest.json`);
const targetJson = loadJson(targetPath, false);
if (!targetJson) {
console.error(`[エラー] ${targetPath} が見つかりません。先に node get-site-config.js を実行してください`);
console.error(`SiteId ${SOURCE_SITE_ID} 取得時にInheritPermission追跡ロジックで自動取得されるはずです`);
process.exit(1);
}
const targetData = targetJson.Response.Data;
const currentPermissions = targetData.Permissions || [];
const targetSiteDir = findSiteDir(configsDir, inheritPermissionId);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${inheritPermissionId} のフォルダが見つかりません。`);
process.exit(1);
}
// --request指定があればそれを使う。無ければ直近のmodifyフォルダを再利用し、
// まだ無ければ新規作成する(プレビュー→実行の一連の流れを同じフォルダにまとめるため)。
const modifyDir = requestLabel
? newModifyDir(targetSiteDir, requestLabel)
: latestModifyDir(targetSiteDir) || newModifyDir(targetSiteDir);
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
console.log("========================================");
console.log(` 権限追加${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象サイト: SiteId ${inheritPermissionId}"${targetData.Title}"、SiteId ${SOURCE_SITE_ID} の権限継承先)`);
console.log("");
// UserId 579 と同じ権限エントリを抽出複数PermissionTypeを持つ場合は全て対象
const sourceEntries = currentPermissions.filter((entry) => {
const [type, id] = String(entry).split(",");
return type === "User" && Number(id) === MIRROR_FROM_USER_ID;
});
if (sourceEntries.length === 0) {
console.error(`[エラー] UserId ${MIRROR_FROM_USER_ID} のPermissionsエントリが見つかりません。`);
console.error(`現在のPermissions: ${JSON.stringify(currentPermissions)}`);
process.exit(1);
}
// 福島 成佳 のUserIdを解決
const master = loadMasterData(baseDir);
const candidateIds = resolveId(master, { Type: "User", Name: GRANT_TO_USER_NAME });
if (candidateIds.length === 0) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」が見つかりません。configs/master_users.jsonを確認してください。`);
process.exit(1);
}
if (candidateIds.length > 1) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」に一致する候補が複数あります: ${candidateIds.join(", ")}`);
process.exit(1);
}
const grantToUserId = candidateIds[0];
// 579のエントリをコピーし、ユーザーIDだけ差し替えた新規エントリを作成
const newEntries = sourceEntries.map((entry) => {
const parts = String(entry).split(",");
parts[1] = String(grantToUserId);
return parts.join(",");
});
// 既に同一エントリが存在する場合は追加しない
const entriesToAdd = newEntries.filter((e) => !currentPermissions.includes(e));
console.log(`コピー元: UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`);
console.log(` 既存エントリ: ${JSON.stringify(sourceEntries)}`);
console.log(`追加先: UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`);
console.log(` 追加予定エントリ: ${JSON.stringify(newEntries)}`);
if (entriesToAdd.length === 0) {
console.log("\n[INFO] 追加予定のエントリは既に全て存在しています。変更の必要はありません。");
process.exit(0);
}
if (entriesToAdd.length !== newEntries.length) {
console.log(`[INFO] 一部エントリは既に存在するため、実際に追加するのは ${entriesToAdd.length} 件です: ${JSON.stringify(entriesToAdd)}`);
}
const updatedPermissions = [...currentPermissions, ...entriesToAdd];
console.log("\n★注意: updatesite APIでPermissionsを更新できるという公式記載は確認できていません。");
console.log(" 実行前に必ず内容を確認し、実行後は管理画面での反映結果も確認してください。");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${inheritPermissionId}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: inheritPermissionId,
Title: targetData.Title,
ReferenceType: targetData.ReferenceType,
ParentId: targetData.ParentId,
InheritPermission: targetData.InheritPermission,
Permissions: updatedPermissions,
SiteSettings: targetData.SiteSettings, // 変更しない(現状のまま再送信)
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(変更なし。Columns ${(targetData.SiteSettings?.Columns || []).length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${inheritPermissionId}_permission_diff_${ts}.txt`);
const diffText = [
`SiteId ${inheritPermissionId} 権限追加プレビュー (${ts})`,
`URL: ${url}`,
`対象: SiteId ${SOURCE_SITE_ID} の権限継承先`,
"",
`コピー元 UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`,
` 既存エントリ: ${JSON.stringify(sourceEntries)}`,
`追加先 UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`,
` 追加エントリ: ${JSON.stringify(entriesToAdd)}`,
"",
`変更前Permissions: ${JSON.stringify(currentPermissions)}`,
`変更後Permissions: ${JSON.stringify(updatedPermissions)}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-permission-grant.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(baseDir, "configs", `site-${inheritPermissionId}_permission_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面で反映結果を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,125 @@
/**
* apply-remark-fix-496626-full.js
* ------------------------------------------------------------
* updatesitesettings部分更新で送った Script Id:4 / Style Id:2 / ServerScript Id:1
* 修正がHTTP 200成功を返すにもかかわらず実際にはDBに反映されていない
* 直後のgetsiteで確認しても内容もVer/UpdatedTimeも変化しないことが分かったため
* 確実に反映される updatesite全体更新で同じ変更を送り直す
*
* 現在のサイト全体getsiteを取得しTitle/ReferenceType/ParentId/InheritPermission/
* Permissionsはそのまま再送信SiteSettingsもScripts[Id:4]/Styles[Id:2]/
* ServerScripts[Id:1]のBodyだけをローカルファイルの内容に差し替え他は一切変更しない
*
* 使い方:
* node apply-remark-fix-496626-full.js 差分表示のみ送信なし
* node apply-remark-fix-496626-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
const scriptBody = fs.readFileSync(path.join(siteDir, "scripts", "4_new03_一覧画面自動処理.js"), "utf-8");
const styleBody = fs.readFileSync(path.join(siteDir, "styles", "2_一覧スタイル調整.css"), "utf-8");
const serverScriptBody = fs.readFileSync(path.join(siteDir, "serverscripts", "1_一覧画面 物件詳細.js"), "utf-8");
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
const script4 = siteSettings.Scripts.find((s) => s.Id === 4);
const style2 = siteSettings.Styles.find((s) => s.Id === 2);
const serverScript1 = siteSettings.ServerScripts.find((s) => s.Id === 1);
if (!script4 || !style2 || !serverScript1) {
console.error("[エラー] Script Id:4 / Style Id:2 / ServerScript Id:1 のいずれかが見つかりません。");
process.exit(1);
}
console.log("========================================");
console.log(` Script/Style/ServerScript 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`Script Id:4 現状${script4.Body.length}文字 → 変更後${scriptBody.length}文字(変更${script4.Body !== scriptBody}`);
console.log(`Style Id:2 現状${style2.Body.length}文字 → 変更後${styleBody.length}文字(変更${style2.Body !== styleBody}`);
console.log(`ServerScript Id:1 現状${serverScript1.Body.length}文字 → 変更後${serverScriptBody.length}文字(変更${serverScript1.Body !== serverScriptBody}`);
console.log("他のSiteSettingsColumns/GridColumns/Processes等・Permissions・Title等は一切変更しません。");
script4.Body = scriptBody;
style2.Body = styleBody;
serverScript1.Body = serverScriptBody;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "serverscript-remark-fix");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,154 @@
/**
* apply-remark-fix-496626.js
* ------------------------------------------------------------
* SiteId 496626 の一覧画面スクリプトId:4 new03_一覧画面自動処理
* 備考の3文字省略独自ポップアップ表示に対応した修正版へ差し替える
*
* 対象は desired_config.jsonbuild-desired-config.js
* configs/site-496626_着工要因(東京)v2/ 配下から再構成したもの
* SiteSettings.Scripts/Styles/ServerScripts/Processes をそのまま送信する
* 事前に確認した通り実際に内容が変わるのは Scripts Id:4 のみ
* 他はキー順序が変わるだけでBodyは現状と完全一致
*
* ユーザーがその場で直接適用してよいと明示的に許可した場合に限り実行する
* 一回限りの専用スクリプト.claude/skills/pleasanter-site-spec/SKILL.md
* クロスサイト構造変更節の運用に準拠
*
* 使い方:
* node apply-remark-fix-496626.js 差分表示のみ送信なし
* node apply-remark-fix-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { latestJsonPath, newModifyDir, latestModifyDir, findSiteDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
if (desired.SiteId !== SITE_ID) {
console.error(`[エラー] desired_config.json の SiteId (${desired.SiteId}) が ${SITE_ID} ではありません。`);
console.error(`先に node build-desired-config.js site-${SITE_ID}_新・着工要因(東京)v2 を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, SITE_ID);
const current = loadJson(currentPath, false);
const currentScripts = current?.Response?.Data?.SiteSettings?.Scripts || [];
const desiredScripts = desired.SiteSettings.Scripts || [];
const curScript4 = currentScripts.find((s) => s.Id === 4);
const newScript4 = desiredScripts.find((s) => s.Id === 4);
console.log("========================================");
console.log(` 一覧画面スクリプト(Id:4)の差し替え${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象: SiteId ${SITE_ID}"${current?.Response?.Data?.Title || ""}"`);
console.log("");
console.log(`Script Id:4 現状の文字数: ${curScript4 ? curScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 変更後の文字数: ${newScript4 ? newScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 以外のScripts/Styles/ServerScripts/Processesは内容変更なしキー順序のみ変わる場合あり`);
const siteDir = findSiteDir(configsDir, SITE_ID);
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "remark-truncate-fix");
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_diff_${ts}.txt`);
fs.writeFileSync(
diffOutPath,
[
`SiteId ${SITE_ID} 一覧画面スクリプト(Id:4)差し替えプレビュー (${ts})`,
"",
"--- 現状のBody ---",
curScript4 ? curScript4.Body : "(取得できず)",
"",
"--- 変更後のBody ---",
newScript4 ? newScript4.Body : "(取得できず)",
].join("\n"),
"utf-8"
);
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesitesettings`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
SiteSettings: desired.SiteSettings,
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示 ---");
console.log(
JSON.stringify(
{
...maskedBody,
SiteSettings: {
Scripts: `${desired.SiteSettings.Scripts.length}Id:4のみ内容変更`,
Styles: `${desired.SiteSettings.Styles.length}件(変更なし)`,
ServerScripts: `${desired.SiteSettings.ServerScripts.length}件(変更なし)`,
Processes: `${desired.SiteSettings.Processes.length}件(変更なし)`,
},
},
null,
2
)
);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面・一覧画面で表示を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,159 @@
/**
* apply-site-config.js
* ------------------------------------------------------------
* このスクリプトは常にドライランです実際のAPI送信は一切行いません
*
* Claudeが生成した desired_config.json希望構成を読み込み
* 1. 現状構成configs/site-{SiteId}_latest.jsonとの差分表示
* 2. 実際に送信されるはずの HTTPリクエストURL / Bodyの表示
* 3. そのまま使えるcurlコマンドの出力
* のみを行います実行fetch送信は行いません
*
* 内容を確認した上で問題なければ
* - 表示されたcurlコマンドを手動で実行する
* - またはこのファイル末尾の "実送信ブロック" のコメントを
* 自分の判断で外して実行する
* のいずれかで反映してください
*
* desired_config.json の形式:
* {
* "Mode": "partial", // "partial" = updatesitesettings部分更新 / "full" = updatesite全体更新
* "SiteId": 12345, // 省略時は config.json の値を使用
* "SiteSettings": { ... }, // 変更したい項目のみpartialの場合/ 全体fullの場合
* "Title": "...", // full更新時のみ必要な場合あり
* "ReferenceType": "...",
* "ParentId": ...,
* "InheritPermission": ...
* }
*
* 参照した公式マニュアル:
* - サイト更新全体: https://pleasanter.org/manual/api-site-update
* URL: POST {BaseUrl}/api/items/{SiteId}/updatesite
* - サイト設定の部分更新: https://pleasanter.org/ja/manual/api-update-sitesettings
* URL: POST {BaseUrl}/api/items/{SiteId}/updatesitesettings
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { latestJsonPath } = require("./site-paths");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// ざっくりした差分表示キー単位・トップ1階層+SiteSettings1階層まで
// partialモードupdatesitesettingsは desired 側にあるキーだけがサーバーに送られ、
// 記載のないキーは現状のまま変わらない。そのため比較対象は afterKeys のみに限定する
// beforeにしかないキーを「削除される」ように見せるのは誤り
// fullモードupdatesiteはSiteSettings全体を丸ごと置き換えるため、beforeKeysも含めて比較する。
function diffTopLevel(before, after, label, mode = "partial") {
if (!before) {
console.log(` (現状データなし。get-site-config.jsで先に取得しておくと差分表示できます)`);
return;
}
const afterKeys = new Set(Object.keys(after || {}));
const allKeys = mode === "full" ? new Set([...Object.keys(before || {}), ...afterKeys]) : afterKeys;
let changed = false;
for (const key of allKeys) {
const b = JSON.stringify(before?.[key]);
const a = JSON.stringify(after?.[key]);
if (b !== a) {
changed = true;
console.log(` [変更] ${label}.${key}`);
console.log(` - 現状: ${truncate(b)}`);
console.log(` + 希望: ${truncate(a)}`);
}
}
if (!changed) console.log(" 差分なし");
}
function truncate(str, max = 200) {
if (str === undefined) return "(undefined)";
return str.length > max ? str.slice(0, max) + "...(略)" : str;
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
async function main() {
const baseDir = path.join(__dirname, "..", "..");
const configPath = path.join(baseDir, "config.json");
const desiredPath = process.argv[2] || path.join(baseDir, "desired_config.json");
const config = loadJson(configPath);
const desired = loadJson(desiredPath);
const siteId = desired.SiteId || config.SiteId;
const mode = desired.Mode || "partial";
const endpoint = mode === "full" ? "updatesite" : "updatesitesettings";
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/${endpoint}`;
const latestConfigPath =
latestJsonPath(path.join(baseDir, "configs"), siteId) ||
path.join(baseDir, "configs", `site-${siteId}_latest.json`);
const current = loadJson(latestConfigPath, false);
const currentSiteSettings =
current?.Response?.Data?.SiteSettings ||
current?.Response?.Site?.SiteSettings ||
current?.Response?.SiteSettings ||
null;
console.log("========================================");
console.log(" ドライラン結果(実際の送信は行いません)");
console.log("========================================");
console.log(`Mode : ${mode} (${mode === "full" ? "全体更新" : "部分更新"})`);
console.log(`URL : ${url}`);
console.log(`SiteId : ${siteId}`);
console.log("");
console.log("--- SiteSettings 差分(現状 → 希望) ---");
diffTopLevel(currentSiteSettings, desired.SiteSettings, "SiteSettings", mode);
console.log("");
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
...desired,
};
delete body.Mode; // Mode はエンドポイント選択にのみ使用し、送信Bodyには含めない
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("--- 送信されるはずのBodyAPIキーはマスク表示 ---");
console.log(JSON.stringify(maskedBody, null, 2));
console.log("");
console.log("--- 手動実行用 curl コマンド(内容確認の上、必要ならご自身で実行してください) ---");
console.log(
`curl -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(body)}'`
);
console.log("");
console.log("[注意] このスクリプトはAPIへの送信を一切行っていませんドライラン専用。");
console.log("内容に問題がなければ、上記curlコマンドを手動実行するか、");
console.log("スクリプト内の「実送信ブロック」を有効化する改修をご自身の判断で行ってください。");
// ------------------------------------------------------------
// 実送信ブロック(デフォルトでは絶対に到達しないようにガードしています)
// 意図的に反映を自動化したい場合のみ、下のガードを外して使ってください。
// ------------------------------------------------------------
const ENABLE_ACTUAL_SEND = false; // ← 安全のため常にfalse固定。変更は自己責任で。
if (ENABLE_ACTUAL_SEND) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
console.log(await res.text());
}
}
main();

View File

@ -0,0 +1,211 @@
/**
* apply-to-site-496626.js
* ------------------------------------------------------------
* configs/site-480111_updated.json のSiteSettingsをSiteId 496626
* まるごと上書き適用するupdatesite / Mode:"full"
*
* 通常このプロジェクトの apply-site-config.js はドライラン専用実送信しないだが
* 本スクリプトは今回のタスクに限りユーザーが明示的に許可した専用の直接実行スクリプト
* であり--execute 指定時のみ実際にAPIへPOSTするcurlは使わない
*
* Permissionsは送信ボディに含めない496626は権限をInheritPermissionで
* 継承しているため直接の書き込み対象ではない権限変更は apply-permission-grant.js
* を参照Title/ReferenceType/ParentId/InheritPermissionは496626自身の
* 現状値configs/site-496626_latest.jsonから取得しSiteSettingsのみ
* 480111由来の変更後内容で置き換える
*
* 差分プレビュー送信結果はSiteId 496626 modify/{リクエストラベル}/ フォルダへ保存する
* restructure-site-480111.js が作成したフォルダを --request= で指定するか省略時は
* 最新のmodifyフォルダを自動的に使う
*
* 使い方:
* node apply-to-site-496626.js 差分表示アーティファクト生成のみ送信なし
* node apply-to-site-496626.js --execute 上記に加えて実際に送信する
* node apply-to-site-496626.js --request=ラベル 対象のmodifyフォルダを明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, latestJsonPath, newModifyDir } = require("./site-paths");
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffTopLevel(before, after) {
const lines = [];
const afterKeys = Object.keys(after || {});
let changed = false;
for (const key of afterKeys) {
const b = JSON.stringify(before?.[key]);
const a = JSON.stringify(after?.[key]);
if (b !== a) {
changed = true;
const bLen = Array.isArray(before?.[key]) ? `(${before[key].length}件)` : "";
const aLen = Array.isArray(after?.[key]) ? `(${after[key].length}件)` : "";
lines.push(` [変更] SiteSettings.${key} ${bLen}${aLen}`);
}
}
if (!changed) lines.push(" 差分なし");
return lines;
}
// 対象のmodifyフォルダを解決する--request指定があればそれ、無ければ最新のものを使う
const targetSiteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。先に node get-site-config.js を実行してください。`);
process.exit(1);
}
function resolveModifyDir() {
if (requestLabel) return newModifyDir(targetSiteDir, requestLabel);
const modifyRoot = path.join(targetSiteDir, "modify");
if (!fs.existsSync(modifyRoot)) {
console.error(`[エラー] modifyフォルダがまだありません。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const dirs = fs.readdirSync(modifyRoot).filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory());
if (dirs.length === 0) {
console.error(`[エラー] modifyフォルダの中身が空です: ${modifyRoot}`);
process.exit(1);
}
dirs.sort();
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
const modifyDir = resolveModifyDir();
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
const config = loadJson(path.join(baseDir, "config.json"));
const updatedPath = path.join(modifyDir, "site-480111_updated.json");
const updatedJson = loadJson(updatedPath);
const modifiedSiteSettings = updatedJson?.Response?.Data?.SiteSettings;
if (!modifiedSiteSettings) {
console.error(`[エラー] ${updatedPath} からSiteSettingsを取得できませんでした。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, TARGET_SITE_ID);
const currentJson = loadJson(currentPath, false);
const currentData = currentJson?.Response?.Data;
if (!currentData) {
console.error(`[エラー] ${currentPath} が見つかりません。先に node get-site-config.jsSiteIdに${TARGET_SITE_ID}を含めて)を実行してください。`);
process.exit(1);
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: TARGET_SITE_ID,
Title: currentData.Title,
ReferenceType: currentData.ReferenceType,
ParentId: currentData.ParentId,
InheritPermission: currentData.InheritPermission,
SiteSettings: modifiedSiteSettings,
};
console.log("========================================");
console.log(` SiteId ${TARGET_SITE_ID} への適用${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`URL : ${url}`);
console.log(`Title : ${currentData.Title}`);
console.log(`InheritPermission: ${currentData.InheritPermission}(権限は今回変更しません)`);
console.log("");
console.log("--- SiteSettings 差分496626現状 → 480111由来の変更後 ---");
const diffLines = diffTopLevel(currentData.SiteSettings, modifiedSiteSettings);
diffLines.forEach((l) => console.log(l));
console.log("");
const newItem = (modifiedSiteSettings.Columns || []).find(
(c) => c.ColumnName === "Date031" && c.GridLabelText === "解体紹介料申請"
);
console.log(`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`);
console.log(`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`);
console.log(`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`);
console.log("");
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(省略: Columns ${modifiedSiteSettings.Columns.length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_diff_${ts}.txt`);
const diffText = [
`SiteId ${TARGET_SITE_ID} 適用差分プレビュー (${ts})`,
`URL: ${url}`,
`Title: ${currentData.Title}`,
`InheritPermission: ${currentData.InheritPermission}(今回は権限変更なし)`,
"",
"--- SiteSettings差分 ---",
...diffLines,
"",
`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`,
`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`,
`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-to-site-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,139 @@
/**
* build-desired-config.js
* ------------------------------------------------------------
* extract-site-config.js で分割した configs/site-{SiteId}/ 配下のファイル
* scripts/*.js, styles/*.css, serverscripts/*.js, processes.json
* manifest.json を手がかりに再構成しapply-site-config.js が読み込む
* desired_config.json を生成します
*
* このスクリプトもAPIへは一切送信しませんファイル生成のみ
* 反映は既存の運用どおり
* 1. node apply-site-config.js でドライラン確認差分送信Bodycurl表示
* 2. 内容を確認し問題なければ表示されたcurlコマンドを手動実行
* の流れに乗せてください
*
* 使い方:
* node build-desired-config.js site-214812
* フォルダ名は "site-{SiteId}_{サイト名}" 形式だが"site-{SiteId}" だけの
* 前方一致指定でも configs/ 配下から自動的に該当フォルダを探して解決する
* node build-desired-config.js ./configs/site-214812_物件基本情報
*
* 出力: desired_config.jsonこのスクリプトと同じ階層既存ファイルは上書き
*
* 対応範囲:
* Scripts / Styles / ServerScripts / Processes
* Mode:"partial"updatesitesettingsで反映可能なためそのまま組み込みます
* HtmlGridGuide等のガイド
* SiteSettingsではなくサイトデータ直下の項目のため
* 部分更新updatesitesettingsには含められません
* 反映したい場合は Mode:"full"updatesite用の別JSONを用意し
* Title/ReferenceType等の必須項目とあわせて指定してください
* このスクリプトはhtml/配下に抽出物がある場合警告のみ表示します
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function readJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// フォルダ名は "site-{SiteId}_{サイト名}" 形式。"site-{SiteId}" のみの指定でも
// configs/ 配下から前方一致で該当フォルダを探して解決する。
// 新形式(サイト名付き)を優先し、裸の "site-{SiteId}" 完全一致(リネーム前の古い抽出結果の可能性)は
// 他に候補が無い場合のみ後方互換フォールバックとして使う。
function resolveSiteDir(baseDir, arg) {
const configsDir = path.join(baseDir, "configs");
const direct = path.isAbsolute(arg) ? arg : path.join(configsDir, path.basename(arg));
const baseName = path.basename(arg);
const candidates = fs.existsSync(configsDir)
? fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return (
fs.statSync(full).isDirectory() && (name === baseName || name.startsWith(`${baseName}_`))
);
})
: [];
const suffixed = candidates.filter((name) => name !== baseName);
if (suffixed.length === 1) return path.join(configsDir, suffixed[0]);
if (suffixed.length > 1) {
console.error(`[エラー] "${baseName}" に一致するフォルダが複数見つかりました: ${suffixed.join(", ")}`);
console.error("フォルダ名をフルで指定してください。");
process.exit(1);
}
if (candidates.includes(baseName)) return path.join(configsDir, baseName);
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) return direct;
console.error(`[エラー] フォルダが見つかりません: ${direct}`);
console.error("先に node extract-site-config.jsまたはnode get-site-config.jsを実行してください。");
process.exit(1);
}
function rebuildList(manifestEntries, dir, bodyKey = "Body") {
return (manifestEntries || []).map((entry) => {
const { File, ...meta } = entry;
const filePath = path.join(dir, File);
if (!fs.existsSync(filePath)) {
console.error(`[エラー] manifest.jsonに記載のファイルが見つかりません: ${filePath}`);
process.exit(1);
}
const body = fs.readFileSync(filePath, "utf-8");
return { ...meta, [bodyKey]: body };
});
}
function main() {
const baseDir = path.join(__dirname, "..", "..");
const arg = process.argv[2];
if (!arg) {
console.error("[エラー] 対象フォルダを指定してください(例: node build-desired-config.js site-214812");
process.exit(1);
}
const siteDir = resolveSiteDir(baseDir, arg);
const manifestPath = path.join(siteDir, "manifest.json");
if (!fs.existsSync(manifestPath)) {
console.error(`[エラー] manifest.jsonが見つかりません: ${manifestPath}`);
console.error("先に node extract-site-config.js を実行してください。");
process.exit(1);
}
const manifest = readJson(manifestPath);
const siteSettings = {};
siteSettings.Scripts = rebuildList(manifest.Scripts, path.join(siteDir, "scripts"));
siteSettings.Styles = rebuildList(manifest.Styles, path.join(siteDir, "styles"));
siteSettings.ServerScripts = rebuildList(manifest.ServerScripts, path.join(siteDir, "serverscripts"));
const processesPath = path.join(siteDir, manifest.ProcessesFile || "processes.json");
siteSettings.Processes = fs.existsSync(processesPath) ? readJson(processesPath) : [];
const desired = {
Mode: "partial",
SiteId: manifest.SiteId,
SiteSettings: siteSettings,
};
const outPath = path.join(baseDir, "desired_config.json");
fs.writeFileSync(outPath, JSON.stringify(desired, null, 2), "utf-8");
console.log(`[OK] 生成しました: ${outPath}`);
console.log(` Scripts : ${siteSettings.Scripts.length}`);
console.log(` Styles : ${siteSettings.Styles.length}`);
console.log(` ServerScripts : ${siteSettings.ServerScripts.length}`);
console.log(` Processes : ${siteSettings.Processes.length}`);
if (manifest.Html && manifest.Html.length > 0) {
console.log("\n[注意] html/ 配下のガイドHTMLは、この desired_config.json には含まれていません。");
console.log(" GridGuide等はSiteSettingsではなくサイト直下の項目のため、部分更新(updatesitesettings)では送れません。");
console.log(' 反映するには Mode:"full"updatesite用の別JSONを用意し、Title等の必須項目とあわせて指定してください。');
}
console.log("\n次のステップ: node apply-site-config.js でドライラン確認 → 内容確認の上、表示されたcurlコマンドを手動実行してください。");
}
main();

View File

@ -0,0 +1,154 @@
/**
* extract-site-config.js
* ------------------------------------------------------------
* 取得済みのサイト構成 (configs/site-{SiteId}_latest.json) から
* Scripts / Styles / ServerScripts / Processes / ガイドHTML
* 個別ファイルとして configs/site-{SiteId}/ 配下に書き出します
*
* 使い方:
* node extract-site-config.js
* node extract-site-config.js ./configs/site-214812_latest.json
*
* 出力:
* configs/site-{SiteId}/
* manifest.json 抽出内容の一覧build-desired-config.jsでの再構成に使用
* scripts/{Id}_{Title}.js
* styles/{Id}_{Title}.css
* serverscripts/{Id}_{Title}.js
* html/{GuideName}.html GridGuide等ガイドHTML値が空でないもののみ
* processes.json プロセス設定SiteSettings.Processes をそのまま保存
*
* 注意:
* ガイドHTMLGridGuide等はSiteSettingsではなくサイトデータ直下の項目です
* そのため反映時はサイト設定の部分更新updatesitesettingsではなく
* サイト全体更新updatesite / Mode:"full"が必要になります
* Scripts/Styles/ServerScripts/Processesは部分更新Mode:"partial"で反映できます
* 詳しくは build-desired-config.js の実行結果メッセージを参照してください
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { sanitize, ensureSiteDir, latestJsonPath } = require("./site-paths");
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function extractList(items, outDir, ext, bodyKey = "Body") {
const manifestEntries = [];
if (!items || items.length === 0) return manifestEntries;
ensureDir(outDir);
for (const item of items) {
const fileName = `${item.Id}_${sanitize(item.Title || item.Name)}${ext}`;
fs.writeFileSync(path.join(outDir, fileName), item[bodyKey] || "", "utf-8");
const { [bodyKey]: _omit, ...meta } = item;
manifestEntries.push({ ...meta, File: fileName });
}
return manifestEntries;
}
const GUIDE_FIELDS = [
"GridGuide",
"EditorGuide",
"CalendarGuide",
"CrosstabGuide",
"GanttGuide",
"BurnDownGuide",
"TimeSeriesGuide",
"AnalyGuide",
"KambanGuide",
"ImageLibGuide",
];
// get-site-config.js からも呼び出せるよう関数化。
// CLI実行時直接 `node extract-site-config.js`)は末尾のブロックが引数解決を行う。
function extractSiteConfig(latestPath) {
const baseDir = path.join(__dirname, "..", "..");
if (!fs.existsSync(latestPath)) {
console.error(`[エラー] ファイルが見つかりません: ${latestPath}`);
console.error("先に node get-site-config.js を実行してください。");
process.exit(1);
}
const json = JSON.parse(fs.readFileSync(latestPath, "utf-8"));
const data = json?.Response?.Data;
if (!data) {
console.error("[エラー] Response.Data が見つかりません。getsiteのレスポンス形式を確認してください。");
process.exit(1);
}
const siteId = data.SiteId;
const settings = data.SiteSettings || {};
const configsDir = path.join(baseDir, "configs");
const outRoot = ensureSiteDir(configsDir, siteId, data.Title);
const manifest = {
SiteId: siteId,
Title: data.Title,
ExtractedAt: new Date().toISOString(),
SourceFile: path.relative(baseDir, latestPath).replace(/\\/g, "/"),
};
manifest.Scripts = extractList(settings.Scripts, path.join(outRoot, "scripts"), ".js");
manifest.Styles = extractList(settings.Styles, path.join(outRoot, "styles"), ".css");
manifest.ServerScripts = extractList(settings.ServerScripts, path.join(outRoot, "serverscripts"), ".js");
// ガイドHTMLサイトデータ直下。SiteSettingsではない点に注意
const htmlDir = path.join(outRoot, "html");
const htmlEntries = [];
for (const field of GUIDE_FIELDS) {
const value = data[field];
if (value) {
ensureDir(htmlDir);
const fileName = `${field}.html`;
fs.writeFileSync(path.join(htmlDir, fileName), value, "utf-8");
htmlEntries.push({ Field: field, File: fileName, TopLevel: true });
}
}
manifest.Html = htmlEntries;
// プロセス設定 … 個別分割せず1つの定義ファイルとして保存
fs.writeFileSync(
path.join(outRoot, "processes.json"),
JSON.stringify(settings.Processes || [], null, 2),
"utf-8"
);
manifest.ProcessesFile = "processes.json";
manifest.ProcessesCount = (settings.Processes || []).length;
fs.writeFileSync(path.join(outRoot, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
console.log(`[OK] 抽出完了: ${outRoot}`);
console.log(` Scripts : ${manifest.Scripts.length}`);
console.log(` Styles : ${manifest.Styles.length}`);
console.log(` ServerScripts : ${manifest.ServerScripts.length}`);
console.log(` Html(ガイド) : ${manifest.Html.length}`);
console.log(` Processes : ${manifest.ProcessesCount}件 → processes.json`);
console.log("\n次のステップ: 各ファイルを直接編集するか、Claudeに修正内容を指示してください。");
console.log(`修正後は次のコマンドで反映用JSONを作成できます: node build-desired-config.js ${path.basename(outRoot)}`);
return manifest;
}
// CLIとして直接実行された場合のみ、引数からlatestPathを解決して実行する
if (require.main === module) {
const baseDir = path.join(__dirname, "..", "..");
let latestPath = process.argv[2];
if (!latestPath) {
const configPath = path.join(baseDir, "config.json");
if (!fs.existsSync(configPath)) {
console.error("[エラー] config.jsonが見つかりません。抽出対象のJSONパスを引数で指定してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const resolved = latestJsonPath(path.join(baseDir, "configs"), config.SiteId);
latestPath = resolved || path.join(baseDir, "configs", `site-${config.SiteId}_latest.json`);
}
extractSiteConfig(latestPath);
}
module.exports = { extractSiteConfig };

View File

@ -0,0 +1,142 @@
/**
* get-master-data.js
* ------------------------------------------------------------
* Pleasanterのユーザー/組織(部署)/グループ一覧を取得し
* ./configs/ 配下に保存しますresolve-names.js が読み込む
*
* 使い方:
* node get-master-data.js
* node get-master-data.js ./config.json 設定ファイルを明示指定する場合
*
* 参照した公式マニュアル:
* - 開発者向け機能APIユーザ操作ユーザ取得全て
* https://pleasanter.org/ja/manual/api-user-get-all
* - 開発者向け機能API組織操作組織取得
* https://pleasanter.org/ja/manual/api-dept-get
* - 開発者向け機能APIグループ操作グループ取得
* https://pleasanter.org/ja/manual/api-group-get
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const TARGETS = [
{ key: "users", endpoint: "users/get", outFile: "master_users.json", label: "ユーザー", idField: "UserId" },
{ key: "depts", endpoint: "depts/get", outFile: "master_depts.json", label: "組織", idField: "DeptId" },
{ key: "groups", endpoint: "groups/get", outFile: "master_groups.json", label: "グループ", idField: "GroupId" },
];
// 一部API/api/users/get 等はページング応答TotalCount > 1回あたりのData件数を返すため、
// 全件取得できるまで Offset をずらして繰り返し取得し、Data配列を結合する。
async function fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/${target.endpoint}`;
let allData = [];
let offset = 0;
let totalCount = null;
let firstJson = null;
while (true) {
const body = {
ApiVersion: ApiVersion || "1.1",
ApiKey: ApiKey,
Offset: offset,
};
console.log(`[INFO] 取得先: ${url} (Offset: ${offset})`);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (err) {
console.error(`[エラー] ${target.label}の取得に失敗しました:`, err.message);
return false;
}
const text = await response.text();
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return false;
}
let json;
try {
json = JSON.parse(text);
} catch (err) {
console.error(`[エラー] ${target.label}のレスポンスがJSONとして解析できませんでした。`);
console.error(text);
return false;
}
if (!firstJson) firstJson = json;
const pageData = json?.Response?.Data || [];
allData = allData.concat(pageData);
totalCount = json?.Response?.TotalCount ?? allData.length;
if (pageData.length === 0 || allData.length >= totalCount) break;
offset = allData.length;
}
// ページ間でOffset起点がずれて重複が混ざる場合があるため、idFieldで重複排除する
const seen = new Set();
allData = allData.filter((item) => {
const id = item[target.idField];
if (seen.has(id)) return false;
seen.add(id);
return true;
});
// 結合済みのData配列で1件目のレスポンスを上書きし、全件入りのJSONとして保存する
const outJson = { ...firstJson, Response: { ...firstJson.Response, Data: allData } };
const outFile = path.join(outDir, target.outFile);
fs.writeFileSync(outFile, JSON.stringify(outJson, null, 2), "utf-8");
console.log(`[OK] ${target.label}: ${allData.length}/${totalCount}件 保存しました: ${outFile}`);
return true;
}
async function main() {
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
console.error("config.example.json をコピーして config.json を作成してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const { BaseUrl, ApiKey, ApiVersion } = config;
if (!BaseUrl || !ApiKey) {
console.error("[エラー] config.json に BaseUrl / ApiKey を設定してください。");
process.exit(1);
}
const outDir = path.join(path.join(__dirname, "..", ".."), "configs", "master");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const results = [];
for (const target of TARGETS) {
const ok = await fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir);
results.push({ target: target.label, ok });
}
console.log("\n==== 取得結果まとめ ====");
for (const { target, ok } of results) {
console.log(` ${target}: ${ok ? "OK" : "失敗"}`);
}
if (results.some((r) => !r.ok)) {
process.exit(1);
}
}
main();

View File

@ -0,0 +1,188 @@
/**
* get-site-config.js
* ------------------------------------------------------------
* プリザンターの指定サイトの構成情報SiteSettings等を取得し
* ./configs/ 配下にタイムスタンプ付きJSONとして保存します
*
* 使い方:
* node get-site-config.js
* node get-site-config.js ./config.json 設定ファイルを明示指定する場合
*
* 事前準備:
* 1. config.example.json config.json にコピーし
* BaseUrl / SiteId / ApiKey を実環境の値に書き換えてください
* SiteId はカンマ区切りで複数指定可能です: "480111,438166,96235"
* 1件のみの場合は従来通り数値: 480111でも構いません
* 2. Node.js 18以降組み込みfetchを使用
*
* 参照した公式マニュアル:
* - 開発者向け機能APIサイト操作サイト更新
* https://pleasanter.org/manual/api-site-update
* - 開発者向け機能APIサイト操作サイト設定の更新部分追加/更新/削除
* https://pleasanter.org/ja/manual/api-update-sitesettings
*
* 注意:
* プリザンターのバージョンにより get のレスポンス構造
* SiteSettingsが Response.Site 配下にあるか等が異なる場合があります
* 初回実行時はコンソールに出力されるレスポンスのトップレベルキーを確認し
* 想定通りの構造か確認してください
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { extractSiteConfig } = require("./extract-site-config");
const { ensureSiteDir, sitesettingsDir } = require("./site-paths");
// config.json の SiteId数値 / カンマ区切り文字列 / 配列)を
// 個々のSiteId文字列の配列に正規化する。
function parseSiteIds(rawSiteId) {
const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(",");
return list
.map((id) => String(id).trim())
.filter((id) => id.length > 0);
}
// 1サイト分の取得・保存・抽出処理
async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/getsite`;
const body = {
ApiVersion: ApiVersion || "1.1",
ApiKey: ApiKey,
};
console.log(`[INFO] 取得先: ${url}`);
console.log(`[INFO] SiteId: ${siteId}`);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (err) {
console.error("[エラー] リクエスト送信に失敗しました:", err.message);
console.error("BaseUrlの疎通・ネットワーク設定を確認してください。");
return { ok: false };
}
const text = await response.text();
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return { ok: false };
}
let json;
try {
json = JSON.parse(text);
} catch (err) {
console.error("[エラー] レスポンスがJSONとして解析できませんでした。");
console.error(text);
return { ok: false };
}
// レスポンス構造の確認用ログ(初回はここを見て構造を把握してください)
console.log("[INFO] レスポンス トップレベルキー:", Object.keys(json));
if (json.Response) {
console.log("[INFO] Response配下のキー:", Object.keys(json.Response));
}
// 保存: configs/site-{siteId}_{サイト名}/sitesettings/ 配下へ
const configsDir = path.join(path.join(__dirname, "..", ".."), "configs");
const title = json?.Response?.Data?.Title;
const siteDir = ensureSiteDir(configsDir, siteId, title);
const outDir = sitesettingsDir(siteDir);
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-");
const outFile = path.join(outDir, `site-${siteId}_${timestamp}.json`);
fs.writeFileSync(outFile, JSON.stringify(json, null, 2), "utf-8");
// 常に「最新」ファイルも別名で保持Claudeへのアップロード用に固定名があると便利
const latestFile = path.join(outDir, `site-${siteId}_latest.json`);
fs.writeFileSync(latestFile, JSON.stringify(json, null, 2), "utf-8");
console.log(`[OK] 保存しました: ${outFile}`);
console.log(`[OK] 最新版としても保存しました: ${latestFile}`);
// 取得と同時に Scripts/Styles/ServerScripts/Processes/ガイドHTML も抽出する
console.log("\n[INFO] 続けて設定の個別ファイル抽出を実行します...");
extractSiteConfig(latestFile);
// 権限継承先の追跡: InheritPermission が自サイト以外を指している場合、
// 実際の権限はそちらのサイトにあるため、そのサイトも同様に取得する。
const inheritPermission = json?.Response?.Data?.InheritPermission;
return { ok: true, inheritPermission };
}
async function main() {
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
console.error("config.example.json をコピーして config.json を作成してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const { BaseUrl, SiteId, ApiKey, ApiVersion } = config;
if (!BaseUrl || !SiteId || !ApiKey) {
console.error("[エラー] config.json に BaseUrl / SiteId / ApiKey を設定してください。");
process.exit(1);
}
const siteIds = parseSiteIds(SiteId);
if (siteIds.length === 0) {
console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。");
process.exit(1);
}
// 権限継承先の追跡: 指定サイトの取得後、InheritPermission が自サイト以外を
// 指していれば、そのサイトも同じ要領でキューに追加して取得する
// (権限の実体は継承元サイド側にあるため)。
const queue = [...siteIds];
const visited = new Set();
const results = [];
while (queue.length > 0) {
const siteId = String(queue.shift());
if (visited.has(siteId)) continue;
visited.add(siteId);
console.log(`\n==== [${results.length + 1}] SiteId: ${siteId} ====`);
const { ok, inheritPermission } = await fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion });
results.push({ siteId, ok });
if (
ok &&
inheritPermission != null &&
String(inheritPermission) !== "0" &&
String(inheritPermission) !== siteId &&
!visited.has(String(inheritPermission))
) {
console.log(`[INFO] SiteId ${siteId} は権限をSiteId ${inheritPermission} から継承しています。継承元も取得します。`);
queue.push(String(inheritPermission));
}
}
console.log("\n==== 取得結果まとめ ====");
for (const { siteId, ok } of results) {
console.log(` SiteId ${siteId}: ${ok ? "OK" : "失敗"}`);
}
console.log("\n次のステップ: latest.jsonをClaudeのチャットにアップロードして仕様書化を依頼するか、");
console.log("configs/site-{SiteId}/ 配下のファイルを直接編集・指示して修正してください。");
if (results.some((r) => !r.ok)) {
process.exit(1);
}
}
main();

View File

@ -0,0 +1,185 @@
/**
* migrate-data-480111-to-496626.js
* ------------------------------------------------------------
* SiteId 480111 の全レコードを取得しSiteSettings側で行った連番リネーム
* Date031047 Date032048Date131147 Date132148
* Check131147 Check132148Class031047 Class032048
* 同じマッピングでフィールド値を付け替えた上でSiteId 496626
* bulkupsert新規作成でインポートする
*
* 除外するもの:
* - ResultId / SiteId / Ver / CreatedTime / UpdatedTime / Locked / ItemTitle
* 新サイト側で自動採番自動計算されるため
* - Comments変更履歴コメントテキスト量が多くCreatorが480111時点のユーザーIDのため
* そのまま再現すると誤解を招く可能性がある除外し必要なら別途手動で移行する
* - AttachmentsHash添付ファイルはサイトをまたいだAPI経由の単純コピーができないため対象外
* 添付がある場合はプレビュー時に警告する
*
* 使い方:
* node migrate-data-480111-to-496626.js 件数サンプル警告の表示のみ送信なし
* node migrate-data-480111-to-496626.js --execute 上記に加えて実際にbulkupsertで送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const BATCH_SIZE = 20;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function pad3(n) {
return String(n).padStart(3, "0");
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
// SiteSettings側の連番リネームと同じマッピングを構築する
const classMap = {};
const dateMap = {};
const checkMap = {};
for (let i = 31; i <= 47; i++) {
classMap[`Class${pad3(i)}`] = `Class${pad3(i + 1)}`;
dateMap[`Date${pad3(i)}`] = `Date${pad3(i + 1)}`;
dateMap[`Date${i + 100}`] = `Date${i + 101}`;
checkMap[`Check${i + 100}`] = `Check${i + 101}`;
}
function remapHash(hash, map) {
const result = {};
for (const [key, value] of Object.entries(hash || {})) {
const newKey = map[key] || key;
result[newKey] = value;
}
return result;
}
async function fetchAllRecords(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/get`;
let all = [];
let offset = 0;
let totalCount = null;
while (true) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: ApiVersion || "1.1", ApiKey, Offset: offset }),
});
const json = await res.json();
if (json.StatusCode !== 200) {
console.error(`[エラー] レコード取得に失敗しましたOffset:${offset}:`, JSON.stringify(json).slice(0, 500));
process.exit(1);
}
const page = json.Response.Data || [];
all = all.concat(page);
totalCount = json.Response.TotalCount ?? all.length;
if (page.length === 0 || all.length >= totalCount) break;
offset = all.length;
}
return all;
}
function buildMigratedRecord(record) {
return {
Body: record.Body,
Status: record.Status,
Manager: record.Manager,
Owner: record.Owner,
ClassHash: remapHash(record.ClassHash, classMap),
NumHash: record.NumHash || {},
DateHash: remapHash(record.DateHash, dateMap),
DescriptionHash: record.DescriptionHash || {},
CheckHash: remapHash(record.CheckHash, checkMap),
};
}
async function main() {
const config = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
console.log(`[INFO] SiteId ${SOURCE_SITE_ID} の全レコードを取得しています...`);
const sourceRecords = await fetchAllRecords(SOURCE_SITE_ID, config);
console.log(`[OK] ${sourceRecords.length}件取得しました。`);
const attachmentRecords = sourceRecords.filter(
(r) => r.AttachmentsHash && Object.values(r.AttachmentsHash).some((a) => Array.isArray(a) && a.length > 0)
);
const migrated = sourceRecords.map(buildMigratedRecord);
console.log("\n========================================");
console.log(` データ移行 (480111 → 496626)${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`移行対象件数: ${migrated.length}`);
if (attachmentRecords.length > 0) {
console.log(`\n⚠️ 添付ファイルを含むレコードが${attachmentRecords.length}件ありますResultId: ${attachmentRecords.map((r) => r.ResultId).join(", ")})。`);
console.log(" 添付ファイルは本スクリプトの移行対象外ですAPI経由でのサイト間コピーは非対応のため。");
console.log(" 必要な場合は管理画面から手動での再アップロードを検討してください。");
} else {
console.log("添付ファイルを含むレコードはありません。");
}
console.log("\nComments変更履歴コメントは移行対象外です。");
console.log("\n--- サンプル1件目、移行後の姿 ---");
console.log(JSON.stringify(migrated[0], null, 2));
const siteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!siteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。`);
process.exit(1);
}
// requestLabel未指定時は固定ラベルを使い、プレビュー→実行を同じフォルダにまとめる
const modifyDir = newModifyDir(siteDir, requestLabel || "data-migration");
const previewPath = path.join(modifyDir, "data_migration_preview.json");
fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8");
console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node migrate-data-480111-to-496626.js --execute");
return;
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/bulkupsert`;
const results = [];
for (let i = 0; i < migrated.length; i += BATCH_SIZE) {
const batch = migrated.slice(i, i + BATCH_SIZE);
console.log(`\n実際に送信します(${i + 1}${i + batch.length}件目 / 全${migrated.length}件)...`);
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SsCache: true,
Data: batch,
}),
});
const text = await res.text();
console.log(`HTTP ${res.status} ${res.statusText}`);
let json;
try {
json = JSON.parse(text);
console.log(json.StatusCode === 200 ? "[OK] このバッチは成功しました。" : "[エラー] このバッチは失敗した可能性があります。");
} catch {
console.log(text.slice(0, 1000));
}
results.push({ batchStart: i, status: res.status, body: text });
}
const resultPath = path.join(modifyDir, `data_migration_result_${timestamp()}.json`);
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8");
console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`);
}
main();

View File

@ -0,0 +1,208 @@
/**
* migrate-process028-rename-496626.js
* ------------------------------------------------------------
* site-496626着工要因(東京)v2item8対応
* SiteSettings側で行った番号リネームDate029048 Date028047
* Date129148 Date128147Class029048 Class028047
* Check129148 Check128147と同じマッピングで既存全レコードの
* フィールド値を同一サイト内で付け替えるupdate
*
* 旧Date028引越しの値は破棄するリネームで空くスロット
* 新Date048/Date148/Class048/Check148は明示的にクリアする
* 日付のNULL化はPleasanter仕様により "1899/12/31" を送る
* 参照: https://pleasanter.org/ja/manual/faq-api-set-date-to-null
*
* 読み取りは常に移行前の元レコードから行い書き込みは新しいハッシュ
* オブジェクトへ行うため処理順序による上書き事故は発生しない
*
* 使い方:
* node migrate-process028-rename-496626.js 件数サンプル表示のみ送信なし
* node migrate-process028-rename-496626.js --execute 上記に加えて実際にupdateで送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, latestModifyDir, newModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const BATCH_SIZE = 20;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function pad3(n) {
return String(n).padStart(3, "0");
}
// Pleasanterの「未入力」は 1899-12-30 という番兵値で返るため、年が1899以下なら未入力扱いにする
function isEmptyDate(v) {
if (!v) return true;
const d = new Date(v);
return isNaN(d.getTime()) || d.getFullYear() <= 1899;
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
// SiteSettings側のリネームと同じマッピングを構築する29〜48 → 28〜47
const dateMap = {};
const classMap = {};
const checkMap = {};
for (let n = 29; n <= 48; n++) {
const newN = n - 1;
dateMap[`Date${pad3(n)}`] = `Date${pad3(newN)}`;
dateMap[`Date${n + 100}`] = `Date${newN + 100}`;
classMap[`Class${pad3(n)}`] = `Class${pad3(newN)}`;
checkMap[`Check${n + 100}`] = `Check${newN + 100}`;
}
async function fetchAllRecords(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/get`;
let all = [];
let offset = 0;
let totalCount = null;
while (true) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: ApiVersion || "1.1", ApiKey, Offset: offset }),
});
const json = await res.json();
if (json.StatusCode !== 200) {
console.error(`[エラー] レコード取得に失敗しましたOffset:${offset}:`, JSON.stringify(json).slice(0, 500));
process.exit(1);
}
const page = json.Response.Data || [];
all = all.concat(page);
totalCount = json.Response.TotalCount ?? all.length;
if (page.length === 0 || all.length >= totalCount) break;
offset = all.length;
}
return all;
}
// 読み取りは常に元のrecordから、書き込みは新しいhashへ行う
function buildMigratedRecord(record) {
const oldDate = record.DateHash || {};
const oldClass = record.ClassHash || {};
const oldCheck = record.CheckHash || {};
const newDate = {};
const newClass = {};
const newCheck = {};
for (const [oldKey, newKey] of Object.entries(dateMap)) {
newDate[newKey] = oldDate[oldKey] || "1899/12/31";
}
for (const [oldKey, newKey] of Object.entries(classMap)) {
newClass[newKey] = oldClass[oldKey] || "";
}
for (const [oldKey, newKey] of Object.entries(checkMap)) {
newCheck[newKey] = oldCheck[oldKey] || false;
}
// 空くスロット新Date048/Date148/Class048/Check148を明示的にクリア
newDate["Date048"] = "1899/12/31";
newDate["Date148"] = "1899/12/31";
newClass["Class048"] = "";
newCheck["Check148"] = false;
return {
ResultId: record.ResultId,
DateHash: newDate,
ClassHash: newClass,
CheckHash: newCheck,
};
}
async function main() {
const config = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
console.log(`[INFO] SiteId ${SITE_ID} の全レコードを取得しています...`);
const records = await fetchAllRecords(SITE_ID, config);
console.log(`[OK] ${records.length}件取得しました。`);
const migrated = records.map(buildMigratedRecord);
const withProcess028Data = records.filter(
(r) => r.DateHash && !isEmptyDate(r.DateHash.Date028)
);
console.log("\n========================================");
console.log(` データ移行: 引越し(Date028)削除+後続20工程リネーム${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`移行対象件数: ${migrated.length}`);
if (withProcess028Data.length > 0) {
console.log(
`\n[注意] 旧「引越し」(Date028)に実績日が入っていたレコードが${withProcess028Data.length}件ありますResultId: ${withProcess028Data
.map((r) => r.ResultId)
.join(", ")}この値は破棄されます`
);
} else {
console.log("旧「引越し」(Date028)にデータが入っているレコードはありません。");
}
console.log("\n--- サンプル1件目、移行後の姿 ---");
console.log(JSON.stringify(migrated[0], null, 2));
const siteDir = findSiteDir(configsDir, SITE_ID);
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "9item-revision");
const previewPath = path.join(modifyDir, "data_migration_process028_preview.json");
fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8");
console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`);
// 突合用に移行前の生データも保存しておく
const beforePath = path.join(modifyDir, "data_migration_process028_before.json");
fs.writeFileSync(beforePath, JSON.stringify(records, null, 2), "utf-8");
console.log(`[OK] 移行前の生データを保存しました: ${beforePath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node migrate-process028-rename-496626.js --execute");
return;
}
// bulkupsertはResultIdを指定しても新規追加として扱われる事象を確認したため、
// 確実に既存レコードを更新する単体update API/api/items/{ResultId}/update
// レコードごとに個別に呼び出す方式にする。
const results = [];
let okCount = 0;
let ngCount = 0;
for (const rec of migrated) {
const { ResultId, ...body } = rec;
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${ResultId}/update`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
...body,
}),
});
const text = await res.text();
let json;
let ok = false;
try {
json = JSON.parse(text);
ok = json.StatusCode === 200;
} catch {
ok = false;
}
ok ? okCount++ : ngCount++;
console.log(`ResultId ${ResultId}: HTTP ${res.status} ${ok ? "[OK]" : "[エラー] " + text.slice(0, 300)}`);
results.push({ resultId: ResultId, status: res.status, body: text });
}
console.log(`\n成功: ${okCount}件 / 失敗: ${ngCount}`);
const resultPath = path.join(modifyDir, `data_migration_process028_result_${timestamp()}.json`);
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8");
console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`);
}
main();

View File

@ -0,0 +1,83 @@
/**
* resolve-names.js
* ------------------------------------------------------------
* get-master-data.js が保存した configs/master_users.json /
* master_depts.json / master_groups.json を読み込み
* Permissions等に登場する {Type, Id} 形式の値とユーザー/組織/グループ名との
* 相互変換を行うユーティリティ
*
* 使い方他スクリプトから:
* const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
* const master = loadMasterData();
* resolveName(master, { Type: "User", Id: 579 }); // => "福島 成佳" 等
* resolveId(master, { Type: "User", Name: "福島 成佳" }); // => [579] 一致するIdの配列
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const MASTER_FILES = {
User: "master_users.json",
Dept: "master_depts.json",
Group: "master_groups.json",
};
function readJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
console.error("先に node get-master-data.js を実行してください。");
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// configs/master/master_*.json を読み込み、Type別に { idField, nameField, list } を返す
function loadMasterData(baseDir = path.join(__dirname, "..", "..")) {
const masterDir = path.join(baseDir, "configs", "master");
const usersJson = readJson(path.join(masterDir, MASTER_FILES.User));
const deptsJson = readJson(path.join(masterDir, MASTER_FILES.Dept));
const groupsJson = readJson(path.join(masterDir, MASTER_FILES.Group));
return {
User: {
idField: "UserId",
nameField: "Name",
list: usersJson?.Response?.Data || [],
},
Dept: {
idField: "DeptId",
nameField: "DeptName",
list: deptsJson?.Response?.Data || [],
},
Group: {
idField: "GroupId",
nameField: "GroupName",
list: groupsJson?.Response?.Data || [],
},
};
}
// { Type: "User"|"Dept"|"Group", Id } -> 名前文字列(見つからなければ "(不明:Type#Id)"
function resolveName(master, { Type, Id }) {
const bucket = master[Type];
if (!bucket) return `(不明な種別:${Type})`;
const item = bucket.list.find((x) => String(x[bucket.idField]) === String(Id));
if (!item) return `(不明:${Type}#${Id})`;
return item[bucket.nameField];
}
// { Type: "User"|"Dept"|"Group", Name } -> 一致するIdの配列完全一致、複数該当もありうる
function resolveId(master, { Type, Name }) {
const bucket = master[Type];
if (!bucket) return [];
return bucket.list
.filter((x) => x[bucket.nameField] === Name)
.map((x) => x[bucket.idField]);
}
module.exports = { loadMasterData, resolveName, resolveId };

View File

@ -0,0 +1,365 @@
/**
* 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: 既存項目 Date031047系の連番を Date032048系へ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 SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
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(" OK17項目 × 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("営業タブに対応するViewId:2, 営業担当項目)が見つかりません。");
const viewInsertIdx = salesView.GridColumns.indexOf("Date030") + 1;
salesView.GridColumns.splice(viewInsertIdx, 0, "Date031");
if (typeof settings.SectionLatestId === "number") {
settings.SectionLatestId = 49;
}
console.log(" OKColumns 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(" OKCheck009/010/011を除外、Check018/ClassOを追加。列定義自体は保持");
// ============================================================
// フェーズF: 物件詳細1DescriptionA入力ガイド追加
// ============================================================
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(` OKScripts ${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 で差分を確認してください。`);
}

View File

@ -0,0 +1,105 @@
/**
* site-paths.js
* ------------------------------------------------------------
* configs/ 配下のフォルダ構成を一箇所で管理する共有ヘルパー
*
* 構成:
* configs/
* master/ ユーザー/組織/グループのマスターデータget-master-data.js
* site-{SiteId}_{サイト名}/
* sitesettings/ getsiteの生JSONsite-{SiteId}_latest.json / _{timestamp}.json
* scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
* extract-site-config.js による分割済みファイル従来通り
* docs/ 仕様書(.md)や改善要望プラン等sitesettings以外の関連ドキュメント
* modify/{request}/ 修正依頼ごとに新規作成変更前後のconfigファイルや差分結果を保管
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function sanitize(name) {
return (
String(name || "untitled")
.replace(/[\\/:*?"<>|]/g, "_")
.trim()
.slice(0, 60) || "untitled"
);
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return dir;
}
// configs/ 配下から "site-{siteId}_*"(前方一致)または "site-{siteId}"(完全一致)の
// フォルダを探す。見つからなければ null。
function findSiteDir(configsDir, siteId) {
if (!fs.existsSync(configsDir)) return null;
const bare = `site-${siteId}`;
const prefix = `site-${siteId}_`;
const candidates = fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return fs.statSync(full).isDirectory() && (name === bare || name.startsWith(prefix));
});
const suffixed = candidates.filter((n) => n !== bare);
if (suffixed.length >= 1) return path.join(configsDir, suffixed[0]);
if (candidates.includes(bare)) return path.join(configsDir, bare);
return null;
}
// 既存フォルダがあればそれを使い、無ければ site-{siteId}_{sanitize(title)} を新規作成する
function ensureSiteDir(configsDir, siteId, title) {
const existing = findSiteDir(configsDir, siteId);
if (existing) return existing;
const dir = path.join(configsDir, `site-${siteId}_${sanitize(title)}`);
return ensureDir(dir);
}
function sitesettingsDir(siteDir) {
return ensureDir(path.join(siteDir, "sitesettings"));
}
function docsDir(siteDir) {
return ensureDir(path.join(siteDir, "docs"));
}
// 修正依頼用の新規フォルダを作成するlabelを省略した場合はタイムスタンプを使う
function newModifyDir(siteDir, label) {
const name = label || new Date().toISOString().replace(/[:.]/g, "-");
return ensureDir(path.join(siteDir, "modify", name));
}
// 既存のmodifyフォルダの中で最も新しいものフォルダ名の降順ソートを返す。無ければnull。
function latestModifyDir(siteDir) {
const modifyRoot = path.join(siteDir, "modify");
if (!fs.existsSync(modifyRoot)) return null;
const dirs = fs
.readdirSync(modifyRoot)
.filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory())
.sort();
if (dirs.length === 0) return null;
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
// サイトの最新getsiteダンプsite-{siteId}_latest.jsonのパスを返す。
// フォルダ自体が無い場合は null。
function latestJsonPath(configsDir, siteId) {
const siteDir = findSiteDir(configsDir, siteId);
if (!siteDir) return null;
return path.join(siteDir, "sitesettings", `site-${siteId}_latest.json`);
}
module.exports = {
sanitize,
ensureDir,
findSiteDir,
ensureSiteDir,
sitesettingsDir,
docsDir,
newModifyDir,
latestModifyDir,
latestJsonPath,
};

View File

@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"Bash(node -c get-site-config.js)",
"Bash(node get-master-data.js)",
"Bash(node get-site-config.js)",
"WebFetch(domain:pleasanter.org)",
"Bash(node -e ' *)",
"Bash(node restructure-site-480111.js)",
"Bash(ls \"configs/site-496626_新・着工要因\\(東京\\)v2/scripts\" | grep -i apply 2>/dev/null; find \"configs/site-496626_新・着工要因\\(東京\\)v2/..\" -maxdepth 2 -iname \"*apply-desired*\" 2>/dev/null)",
"Read(//c/Users/k.nogi/#GitHub/ken_nogi/ClaudePleasanter/新・着工要因システム/configs/**)"
]
}
}

View File

@ -0,0 +1,256 @@
---
name: pleasanter-site-spec
description: Pleasanterのサイト設定JSONget-site-config.js / api-site-get の getsite レスポンスから、SiteSettingsを読み解いたMarkdown仕様書を生成し、Scripts/Styles/ServerScripts/Processesの抽出・編集・反映extract-site-config.js / build-desired-config.js / apply-site-config.jsを行う。また、ユーザー/組織/グループのマスターデータ取得get-master-data.js、別サイトへのSiteSettings丸ごと適用や項目の追加・連番リネームといった構造変更restructure-site-*.js / apply-to-site-*.js、権限Permissionsの名前指定での追加apply-permission-grant.jsにも対応する。「仕様書を作って」「サイト仕様書」「SiteSettingsをドキュメント化」「スクリプトを抜き出して」「プロセス設定を直して反映して」「別サイトに丸ごと適用して」「項目を追加して連番をズラして」「この人と同じ権限を追加して」「site-spec」等の依頼で使う。
---
# Pleasanter サイト仕様書生成・構成管理
`configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json``getsite` APIレスポンス、`Response.Data` 配下にサイト情報)を読み解き、
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` として日本語のMarkdown仕様書を出力する。
## フォルダ構成configs/ 配下)
```
configs/
master/ … ユーザー/組織/グループのマスターデータget-master-data.js
master_users.json
master_depts.json
master_groups.json
site-{SiteId}_{サイト名}/
sitesettings/ … getsiteの生JSONget-site-config.jsが保存。latest.json + タイムスタンプ付き履歴)
site-{SiteId}_latest.json
site-{SiteId}_{timestamp}.json
scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
… extract-site-config.js による分割済みファイル(従来通り)
docs/ … 仕様書(.md)や改善要望プラン等、sitesettings以外の関連ドキュメント
modify/{リクエストラベル}/ … 修正依頼のたびに新規作成。変更前後のconfigファイル・
差分プレビュー・送信結果をひとまとめに保管する
```
パス解決は `site-paths.js` に共通化されている(`findSiteDir` / `ensureSiteDir` / `sitesettingsDir` / `docsDir` /
`newModifyDir` / `latestModifyDir` / `latestJsonPath`)。新しいスクリプトを書くときはここを再利用し、
パス組み立てを重複させない。
## 入力の特定
1. ユーザーがJSONファイルパスやSiteIdを指定した場合はそれを使う。
2. 指定がなければ `configs/site-*/sitesettings/site-*_latest.json` を探す。複数ある場合はユーザーに確認する。
3. 対象サイトの `sitesettings/site-{SiteId}_latest.json` が無ければ、先に `node get-site-config.js` の実行を提案する
(エンドポイントは `/api/items/{SiteId}/getsite` を使うこと。`/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物なので注意
## 出力ファイル
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md`(既存があれば上書きしてよい)。
## 仕様書の構成(この順番・見出しレベルを踏襲する)
```markdown
# サイト仕様書:{Title}SiteId: {SiteId}
- 取得元データ: [site-{SiteId}_latest.json](../sitesettings/site-{SiteId}_latest.json)
- 取得日時: {UpdatedTime}(サイト更新日時)/設定バージョン `Ver: {Ver}`
## 1. サイト基本情報
TenantId, Title, ReferenceType, ParentId, InheritPermission, Publish,
DisableCrossSearch, Creator/Updator, CreatedTime, SiteSettings.Version を表形式で)
## 2. アクセス権限Permissions
Permissions配列を 種別/対象ID/権限レベル の表に分解。可能であれば `resolve-names.js`
`resolveName` でユーザー名・組織名・グループ名も併記する。
InheritPermissionが自サイト以外を指している場合は、権限の実体はそちらのサイトにある旨を明記し、
継承元サイトのPermissionsも取得できていれば併せて記載する。
「権限値はビットフラグの加算方式のため正確な意味は環境のロール定義に依存する」旨を注記)
## 3. 画面構成
### 3.1 一覧画面GridColumns
GridColumns配列を 順番/列名/表示ラベル の表に。ラベルはColumns配列から解決、
無ければ「項目種別の判定ルール」のデフォルト名を使う)
### 3.2 編集画面レイアウトEditorColumnHash / Sections
EditorColumnHashの各タブ配列を 順番/項目名(内部名)/ラベル名 の表に。
`_Section-N` はラベル名欄に「―(セクション「{LabelText}」開始)」と記載。
続けてSections配列を Id/ラベル/AllowExpand/Expand の表で示す)
### 3.3 タイトル表示TitleColumns / TitleSeparator
TitleColumns配列をTitleSeparatorで連結した式を明記
## 4. 項目定義Columns
Columns配列を 列名(内部)/種別/表示ラベル/説明・入力ガイド/備考 の表に。
種別は下記「項目種別の判定ルール」で推定。
備考にはNoWrap, FieldCss, ExtendedHtmlAfterControlの有無などを記載
### 4.1 項目ごとの補足HTMLExtendedHtmlAfterControl
(該当項目があれば 列名/色styleやclassから読み取れれば/内容 の表。無ければこの節は省略)
## 5. 作成・更新権限(フィールド単位)
PermissionForCreating / PermissionForUpdating を表で。無ければ「設定なし」と明記)
## 6. 集計設定Aggregations
存在する場合のみ。Id/GroupBy/Type/Target を表で)
## 7. 他サイト連携Links / ルックアップ)
(存在する場合のみ。起点列/参照先SiteId/参照先の値→自コピー先/JsonFormat を表で。
「参照先サイトの{From}列の値を、このサイトの{To}列にコピーする」という関係性を文章でも補足)
## 8. プロセス設定Processes
(存在する場合のみ。定義ファイルへのリンク ../processes.json を明記した上で、
Id/Name・DisplayName/実行条件(CurrentStatus→ChangedStatus)/動作(OnClick等)/成功メッセージ を表で)
## 9. スタイルStyles
存在する場合のみ。Id/Title/適用範囲(New/Edit/Index等のフラグ)/ファイルへのリンクを表で。
CSS本体はMarkdownに貼らず、抽出済みファイルへのリンクのみとする
## 10. スクリプトScriptsサーバースクリプトServerScripts
存在する場合のみ。Id/Title/適用画面/ファイルへのリンクを表で。
スクリプト本体はMarkdownに貼らず、抽出済みファイルへのリンクのみとする。
概要は「何をするスクリプトか」を1〜2行で要約。
本文中にAPIキー・Webhook URL・パスワード等の秘密情報らしき文字列`API_KEY`, `SECRET`, `Bearer `,
URLに埋め込まれたトークン等を見つけた場合は、値そのものをMarkdownに転記せず、
「気になる点」に必ずセキュリティ上の指摘として記載する)
## 11. その他設定
NoDisplayIfReadOnly, HideLink, SectionLatestId, 各種ガイド文の設定有無, Commentsの使用有無など
---
## 気になる点(レビュー観点)
(プレースホルダーらしき文言、権限設定の偏り、項目単位の権限の抜け、
未設定のガイド文など、気づいた点を箇条書きで)
```
## 項目種別の判定ルールColumns内 ColumnName のプレフィックスから推定)
| プレフィックス/名前 | 種別 |
|---|---|
| ClassA〜Z | 文字列(分類) |
| Class001〜040等3桁連番 | 文字列(拡張分類)。`Hide: true`の場合が多く、他項目のルックアップ先の値を裏で保持する用途が多い |
| NumA〜Z | 数値 |
| DateA〜Z | 日付 |
| DescriptionA〜Z | 説明(複数行文字列) |
| CheckA〜Z | チェックボックス |
| AttachmentsA〜Z | 添付ファイル |
| Owner, Manager, Assignee等 | ユーザー選択 |
| Status | 状態 |
| {対象}IdResultId, IssueId等 | 自動採番ID |
| Title | タイトル |
| Comments | コメント欄 |
「予定/実行/無し/備考」の4列1組パターン`DateNNN`/`Date(NNN+100)`/`Check(NNN+100)`/`ClassNNN`、NNNは3桁ゼロ埋め連番
繰り返し登場するサイトもある。この場合 `DateNNN.Description` が入力担当(部署名等)、`DateNNN.GridLabelText` が
項目の表示名を表す。詳細は下記「クロスサイト構造変更(新項目追加・連番リネーム)」を参照。
## 注意事項
- SiteSettingsに存在しない項目・空の設定例: `ClassHash`, `NumHash` などのラベル定義ハッシュが `{}`)は
「標準ラベルのまま」として仕様書に明記し、存在しないかのように省略しない。
- 表形式を基本とし、箇条書きより表を優先する(今回のレビューで「編集画面レイアウトは項目名とラベル名を併記」との
指摘を受けた経緯があるため、内部名とラベル名は必ず併記する)。
- 出力言語は日本語。
## 関連ツールScripts/Styles/ServerScripts/Processesの抽出・編集・反映
仕様書化とは別に、「スクリプトを個別ファイルに分けて」「プロセス設定を直して反映して」等の依頼では、
以下のツールチェーンを使う(詳細は `README.md` 参照)。
1. `node get-site-config.js``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` を取得し、
続けて自動的に抽出も行う(`extract-site-config.js` が内部から呼ばれ、`configs/site-{SiteId}_{サイト名}/` に以下を分割保存する。未取得なら先にこれを実行)
- `scripts/{Id}_{Title}.js`(クライアントスクリプト本体)
- `styles/{Id}_{Title}.css`(スタイル本体)
- `serverscripts/{Id}_{Title}.js`(サーバースクリプト本体)
- `html/{GuideName}.html`GridGuide等のガイドHTML。SiteSettingsではなくサイトデータ直下の項目
- `processes.json``SiteSettings.Processes` の定義ファイル、1ファイルにまとめる。分割しない
- `manifest.json`上記ファイルとId/Titleの対応表。再構成に必須
- `config.json``SiteId` はカンマ区切りで複数指定可能。また、取得したサイトの `InheritPermission`
自サイト以外を指している場合、権限の実体を持つそのサイトも自動的に追いかけて取得する(後述)。
- 抽出だけをやり直したい場合(`latest.json`は既に取得済み)は `node extract-site-config.js` を単独実行してもよい
- **注意**: `get-site-config.js` を再実行すると、その時点のサイトの生の状態で `scripts/` `styles/` 等が
上書きされる。あるサイトの内容を「別サイトへ適用するための下書き」として編集している最中に、
同じサイトへ `get-site-config.js` を再実行すると編集内容が失われるので、クロスサイト作業中は
むやみに元サイトを再取得しない(詳しくは後述の「クロスサイト構造変更」参照)。
2. ユーザーの指示に従い、`scripts/*.js` 等の中身を直接編集する(`manifest.json` のId/メタ情報は変更しない)。
新規追加の場合は `manifest.json` に対応エントリ(`Id`は既存と重複しない値、`File`名)を追加してからファイルを作成する。
3. `node build-desired-config.js site-{SiteId}` … 分割ファイルを再構成し `desired_config.json` を生成(送信なし)。
フォルダ名はサイト名まで含む正式名(`site-{SiteId}_{サイト名}`)だが、`site-{SiteId}` の前方一致指定でも
`configs/`配下から自動解決される
4. `node apply-site-config.js` … ドライラン確認差分・送信Body・curlコマンド表示のみ、送信なし
5. 内容をユーザーに確認してもらい、問題なければ表示されたcurlコマンドをユーザー自身が手動実行する
## 関連ツール:ユーザー/組織/グループのマスターデータ取得
権限Permissionsやスクリプト内の担当者振り分けなどをID表記ではなく名前で扱いたい場合に使う。
1. `node get-master-data.js``configs/master/` へ以下を取得・保存する(ページング自動対応。
`/api/users/get` はレスポンスの `TotalCount` が1回あたりの件数を超えることがあるため、
トップレベルの `Offset` パラメータで全件を取得しきるまでループする)。
- `master_users.json``UserId` / `LoginId` / `Name` / `DeptId` 等)
- `master_depts.json``DeptId` / `DeptCode` / `DeptName`
- `master_groups.json``GroupId` / `GroupName` / `GroupMembers[]` / `GroupChildren[]`
- 参照した公式マニュアル: [ユーザ取得(全て)](https://pleasanter.org/ja/manual/api-user-get-all) /
[組織取得](https://pleasanter.org/ja/manual/api-dept-get) / [グループ取得](https://pleasanter.org/ja/manual/api-group-get)
2. `resolve-names.js` をスクリプトから `require` して使う:
- `loadMasterData(baseDir)` … 3つの `master_*.json` を読み込む
- `resolveName(master, { Type: "User"|"Dept"|"Group", Id })` … IDから名前を引く
- `resolveId(master, { Type, Name })` … 名前から一致するIDの配列を引く完全一致。0件/複数件はエラーとして扱う)
## 関連ツール:クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用)
「Aサイトを土台に項目を追加してBサイトへ適用して」「この項目の後ろに新しい項目を追加して、以降の連番を
ズラして」のような依頼で使うパターン。2026-07-16に SiteId 480111 → 496626 への適用(「解体紹介料申請」項目の
追加・連番リネーム・GridDesign一括改修・改善要望3件の反映で実際に使った構成を一般化したもの。
`restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` は、その際の実例
(そのまま次回動かせるテンプレートとして残っている)。**次回別の依頼が来たら、これらをコピーして
対象サイトIDやフェーズの中身を書き換えて使う**(汎用パラメータ化はしていない。要望ごとに列の増減や
チェックリストの中身が違うため、都度スクリプトとして書き下ろす方が確実)。
### 全体の流れ
1. **マスターデータ+サイト情報の取得**: `node get-master-data.js``node get-site-config.js`
`config.json` の `SiteId` を一時的に `"元サイトId,対象サイトId"` にして実行し、終わったら単一IDへ戻す
`InheritPermission` が自サイト以外を指していれば、その継承元サイトも自動的に追いかけて取得される。
2. **抽出済みScripts/Stylesの編集**: 元サイトの `configs/site-{元SiteId}_.../scripts/*.js` 等を直接編集する。
⚠️ ここは元サイト自身の抽出フォルダなので、元サイトを `get-site-config.js` で再取得すると
編集内容が消える。編集後は速やかに restructure スクリプト(次項)を走らせて `modify/` フォルダへ
本文を取り込んでおくこと。
3. **`restructure-site-{元SiteId}.js`(新規作成)**: 元サイトの `sitesettings/latest.json` を読み込み、
構造変更列の挿入・連番リネーム・GridDesign書き換え等を行い、`manifest.json` 経由で
編集済みのScripts/Styles本文を組み込んだ上で、対象サイトの `modify/{リクエストラベル}/` フォルダへ
`site-{元SiteId}_updated.json` として書き出す(`site-paths.js` の `findSiteDir` / `newModifyDir` を使う)。
同じフォルダへ対象サイトの変更前スナップショット(`before_site-{対象SiteId}_latest.json`)も
コピーしておく。書き込み前に、想定した件数増分・重複の有無などの整合性チェックを必ず入れる
今回の実装では「フェーズA:事前ガード」「フェーズH:整合性チェック」として実施)。
4. **`apply-to-site-{対象SiteId}.js`(新規作成)**: `modify/` フォルダの `site-{元SiteId}_updated.json`
`updatesite``Mode:"full"`)で対象サイトへ送る。`Title`/`ReferenceType`/`ParentId`/`InheritPermission`は
**対象サイト自身の現状値**から取得し、`SiteSettings`のみ元サイト由来の内容で置き換える。
`Permissions`は送信ボディに含めない(対象サイトが権限を継承している場合、直接の書き込み対象ではないため。
継承有無は `InheritPermission` で判定する)。デフォルトは差分プレビュー+アーティファクト保存のみ、
`--execute` 指定時のみ実際に送信する。差分プレビュー・送信結果は同じ `modify/` フォルダへ保存する。
5. **権限を名前指定で追加する場合**: `apply-permission-grant.js` のパターンで、対象サイトの
`InheritPermission` が指す継承元サイトに対して行う(対象サイト自身の `Permissions` は空配列=継承の
ままにしておき、直接は変更しない)。`Permissions` はサイトデータ直下の項目で `"Type,Id,PermissionType"`
形式の文字列配列(例: `"User,579,15"`)。`updatesitesettings`部分更新APIはPermissionsを更新対象に
含まないことを公式マニュアルで確認済みのため、`updatesite`(全体更新)を使い、継承元サイトの
`SiteSettings` 等は一切変更せずそのまま再送信しつつ `Permissions` のみ新規エントリを追加する。
⚠️ 公式マニュアルには `updatesite` のリクエストボディで `Permissions` を指定できるという明記が
無いため、実行前に必ずプレビュー出力をユーザーに確認してもらうこと。
6. **内容確認 → 実行**: 生成された `modify/` フォルダの中身(`before_*.json` / `site-*_updated.json` /
`*_diff_*.txt`)をユーザーに提示し、レビューを受けてから `--execute` を実行する。
### 厳守事項(安全設計、クロスサイト構造変更にも適用)
- **`apply-site-config.js` は常にドライラン。Claude自身がAPIへ実送信することはしない**
`ENABLE_ACTUAL_SEND` を有効化する改修や、curlコマンドの代理実行も行わない。これは変更しない。
- 一方で `apply-to-site-{対象SiteId}.js` / `apply-permission-grant.js` のような**専用の直接実行スクリプト**は、
ユーザーがその場で明示的に「今回は直接実行してよい」と許可した場合に限り作成・実行してよい
2026-07-16の実例。これは **その依頼一回限りの例外** であり、標準運用(ドライラン+手動反映)を
恒久的に変更するものではない。次回以降も、実送信を伴うスクリプトを作る/`--execute` を走らせる前には、
改めてユーザーに差分プレビューを提示し、明示的な許可を得ること。
- `build-desired-config.js` が作る `desired_config.json``Mode:"partial"``updatesitesettings`)専用。
Scripts/Styles/ServerScripts/Processes以外のサイト設定には触れない。Columns/EditorColumnHash/Sections/
GridColumns/Permissions等の構造変更は、この節で説明した `restructure-site-*.js` パターンで行う。
- ガイドHTML`html/*.html`)は `updatesitesettings` では反映できない(サイト直下の項目のため)。
反映が必要なら `Mode:"full"``updatesite`用の別JSONが要ることをユーザーに伝える。自動生成はしない。
- 抽出したスクリプト・サーバースクリプトの中に **APIキーやWebhook URL等の秘密情報がハードコードされている場合がある**
(実例あり)。抽出・編集作業でこれらの値を見つけたら、チャット上にそのまま貼り付けず、ユーザーに直接ファイルを確認してもらう。
また `configs/` フォルダと `config.json``.gitignore` 済みだが、誤ってコミットしないよう注意喚起する。
- 権限変更・別サイトへの丸ごと上書きはブラスト半径が大きい。実行前に必ず差分プレビューを提示し、
対象サイトIDや継承関係`InheritPermission`)の理解が誤っていないか、実行のたびに確認する。

View File

@ -0,0 +1,9 @@
# Pleasanter API接続情報実APIキーを含む
config.json
# サイト構成の取得結果・抽出結果(サイトのスクリプト等に秘密情報が
# ハードコードされている場合があるため、フォルダごと除外)
configs/
# 反映用に生成される希望構成(機密を含むスクリプト等を含み得る)
desired_config.json

View File

@ -0,0 +1,3 @@
{
"markdown-pdf.executablePath": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
}

View File

@ -0,0 +1,115 @@
# プリザンター サイト構成 取得・レビュー・反映ツール
## 全体の流れ
```
① node get-site-config.js
→ configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json に構成を保存
→ 続けて自動的に extract-site-config.js が実行され、
Scripts / Styles / ServerScripts / Processes / ガイドHTML が
configs/site-{SiteId}_{サイト名}/ 配下に個別ファイルとして分割保存される
→ config.json の SiteId をカンマ区切りにすると、複数サイトをまとめて①の処理でループ取得する
→ 取得したサイトの InheritPermission が自サイト以外を指していれば、権限の実体を持つ
そのサイトも自動的に追いかけて取得する
② その latest.json をClaudeのチャットにアップロード
→ Claudeが構成をレビュー・仕様書化configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md
③ configs/site-{SiteId}_{サイト名}/ 配下のファイルを直接編集、
またはClaudeに「このスクリプトを◯◯に直して」等と修正を指示
④ node build-desired-config.js site-{SiteId}
→ 分割ファイルを再構成し、desired_config.json を生成(送信は行わない)
⑤ node apply-site-config.js
→ 実際には送信せず、差分・送信予定Body・curlコマンドのみ表示常にドライラン
⑥ 内容を確認し、問題なければ表示されたcurlコマンドを手動実行
```
`extract-site-config.js``node get-site-config.js` の中から自動実行されるため、通常は個別に実行する必要はありません
(取得済みの `latest.json` から抽出をやり直したい場合のみ、単独で `node extract-site-config.js` を実行してください)。
上記はScripts/Styles/ServerScripts/Processesの反映フロー単一サイト内で完結です。項目の追加や連番リネーム、
別サイトへの丸ごと適用、権限の名前指定追加など、より大きな構造変更は
`.claude/skills/pleasanter-site-spec/SKILL.md` の「クロスサイト構造変更」の節を参照してください。
## 初回セットアップ
1. `config.example.json``config.json` にコピー
2. `BaseUrl` / `SiteId` / `ApiKey` を実環境の値に書き換え
- `SiteId` は単一サイトなら数値(例: `12345`)、複数サイトをまとめて取得したい場合は
カンマ区切りの文字列(例: `"12345,23456,34567"`)で指定できます
- APIキーの発行方法: https://pleasanter.org/manual/api-key
- APIキー発行にはテナント管理者権限が必要ですサイト設定の更新系APIを使う場合
3. Node.js 18以降がインストールされていることを確認組み込みfetchを使用、追加パッケージ不要
## ファイル構成
| ファイル | 役割 |
|---|---|
| `config.json` | 接続情報BaseUrl/SiteId/ApiKey※Git管理・共有厳禁 |
| `config.example.json` | 上記のテンプレート |
| `site-paths.js` | `configs/` 配下のフォルダ構成(後述)を解決する共通ヘルパー。他のスクリプトから`require`して使う |
| `get-site-config.js` | サイト構成取得(`getsite``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` に保存し、続けて `extract-site-config.js` を自動実行。`SiteId` をカンマ区切りにすると全サイトを1回の実行で順次取得。`InheritPermission` が自サイト以外を指していれば継承元サイトも自動追跡 |
| `extract-site-config.js` | `latest.json` から Scripts/Styles/ServerScripts/Processes/ガイドHTML を `configs/site-{SiteId}_{サイト名}/` に個別ファイル分割(`get-site-config.js` から自動呼び出し、または単独実行も可) |
| `build-desired-config.js` | 分割ファイルを再構成し `desired_config.json` を生成(**送信は行わない** |
| `apply-site-config.js` | 希望構成JSONとの差分表示・送信内容確認**送信は行わない** |
| `apply-desired-config-full.js` | `desired_config.json`のScripts/Styles/ServerScripts/Processesを、現状サイト全体getsiteに上書きマージし`updatesite`(全体更新)で反映する専用実行スクリプト。`updatesitesettings`部分更新がこの環境で反映されない問題下記「注意事項」参照を踏まえた標準の反映手段。Columns/GridColumns/Permissions等は現状のまま変更しない。デフォルトは差分プレビューのみ、`--execute`指定時のみ実際に送信する |
| `get-master-data.js` | ユーザー/組織/グループのマスターデータを取得し `configs/master/` へ保存(ページング自動対応) |
| `resolve-names.js` | `configs/master/` のマスターデータを使い、ID⇔名前を相互変換するユーティリティ`require`して使う) |
| `restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` | クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用・権限の名前指定追加)の実例。次回同様の依頼が来たらコピーして書き換える。詳細は `.claude/skills/pleasanter-site-spec/SKILL.md` 参照 |
| `desired_config.json` | Claude、または `build-desired-config.js` が生成する「希望構成」 |
| `configs/master/master_{users,depts,groups}.json` | ユーザー/組織/グループのマスターデータ |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` | 直近取得した現状構成Claudeへのアップロード対象 |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_{timestamp}.json` | 取得時点ごとの履歴(タイムスタンプ付き) |
| `configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` | SiteSettingsを読み解いたMarkdown仕様書。改善要望プラン等その他ドキュメントもここに置く |
| `configs/site-{SiteId}_{サイト名}/manifest.json` | 分割ファイルの一覧Id/Title/ファイル名の対応表) |
| `configs/site-{SiteId}_{サイト名}/scripts/*.js` | クライアントスクリプト本体1ファイル=1スクリプト |
| `configs/site-{SiteId}_{サイト名}/styles/*.css` | スタイル本体 |
| `configs/site-{SiteId}_{サイト名}/serverscripts/*.js` | サーバースクリプト本体 |
| `configs/site-{SiteId}_{サイト名}/html/*.html` | ガイドHTMLGridGuide等。値が空でないもののみ |
| `configs/site-{SiteId}_{サイト名}/processes.json` | プロセス設定(`SiteSettings.Processes` の定義ファイル) |
| `configs/site-{SiteId}_{サイト名}/modify/{リクエストラベル}/` | 修正依頼ごとに新規作成。変更前後のconfig・差分プレビュー・送信結果をまとめて保管 |
## Scripts/Styles/ServerScripts/Processes の反映について
- `build-desired-config.js` が生成する `desired_config.json``Mode:"partial"``updatesitesettings`)です。
Scripts / Styles / ServerScripts / Processes の4項目のみを対象とし、それ以外のサイト設定GridColumns等には触れません。
- ガイドHTML`html/*.html`はSiteSettingsではなく**サイトデータ直下**の項目のため、`updatesitesettings` では反映できません。
反映したい場合は `Mode:"full"``updatesite`)用の別の `desired_config.json` を用意し、`Title`/`ReferenceType` 等の必須項目とあわせて指定してください。
- 抽出(`extract-site-config.js`)はその時点のスナップショットです。抽出後に元サイトが他の人・画面から変更されている可能性がある場合は、
反映前に `node get-site-config.js``node extract-site-config.js` を再実行し、最新状態を取り直してから編集してください。
## 参照した公式API仕様
- サイト情報取得: `POST {BaseUrl}/api/items/{SiteId}/getsite`
https://pleasanter.org/ja/manual/api-site-get
(※ `/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物
- サイト全体更新: `POST {BaseUrl}/api/items/{SiteId}/updatesite`
https://pleasanter.org/manual/api-site-update
- サイト設定部分更新: `POST {BaseUrl}/api/items/{SiteId}/updatesitesettings`
https://pleasanter.org/ja/manual/api-update-sitesettings
- ユーザ取得(全て): `POST {BaseUrl}/api/users/get`
https://pleasanter.org/ja/manual/api-user-get-all
(※ ページングはトップレベルの `Offset` パラメータで行う。`View.Offset` ではない点に注意)
- 組織取得: `POST {BaseUrl}/api/depts/get`
https://pleasanter.org/ja/manual/api-dept-get
- グループ取得: `POST {BaseUrl}/api/groups/get`
https://pleasanter.org/ja/manual/api-group-get
## 安全設計について
- `apply-site-config.js`**常にドライラン** です。デフォルトでは実際のAPI呼び出しコード自体に到達しないようガード`ENABLE_ACTUAL_SEND = false` 固定)しています。
- 反映する際は、表示されたcurlコマンドをご自身で実行するか、内容を確認の上でスクリプトを改修してくださいClaudeが安全策なしに自動実行することはありません
- APIキーは `config.json` にのみ保持し、Claudeにアップロードする `latest.json` には含まれませんget時のレスポンスにAPIキーは含まれないため。念のためアップロード前に目視確認することをおすすめします。
- `apply-to-site-*.js` / `apply-permission-grant.js` のような、ユーザーがその場で明示的に許可した場合に限って
作成される「専用の直接実行スクリプト」は例外です(`--execute` 指定時のみ実送信)。これは依頼ごとの一回限りの
許可であり、標準運用(ドライラン+手動反映)を恒久的に変更するものではありません。
## 注意事項
- プリザンターのバージョンにより `get` レスポンスの構造(`Response.Site.SiteSettings` の位置など)が異なる場合があります。初回実行時にコンソールへ出力されるキー構造を確認してください。
- サイト設定の更新系APIは **テナント管理者権限のAPIキー** が必要です一般ユーザーのAPIキーでは403等になります
- ★重要: この環境では `updatesitesettings`(部分更新)が **HTTP 200・成功メッセージを返すにもかかわらず、実際にはScripts/Styles/ServerScriptsの内容がDBに反映されないことを確認済み**です2026-07-16、SiteId 496626で発生。直後に`getsite`し直しても内容・Ver・UpdatedTimeが変化しなかった
Scripts/Styles/ServerScriptsを実際に反映する専用スクリプトを書く場合は、`updatesitesettings`ではなく`updatesite`全体更新。Title/ReferenceType/ParentId/InheritPermission/Permissionsは現状のまま再送信し、SiteSettingsは現状を取得した上でScripts/Styles/ServerScriptsだけ差し替えるを使うこと。反映後は必ず`getsite`し直し、`UpdatedTime`が更新されているか・目的の文字列が含まれているかを確認する。

View File

@ -0,0 +1,127 @@
/**
* apply-desired-config-full.js
* ------------------------------------------------------------
* この環境では updatesitesettings部分更新がHTTP 200を返しても実際には
* 反映されないことを確認済みのためREADME.md注意事項参照
* Scripts/Styles/ServerScripts/Processesの反映には常にこちらを使う
*
* build-desired-config.js が生成した desired_config.json
* Scripts/Styles/ServerScripts/Processes 現在のサイト全体getsite
* 上書きマージしupdatesite全体更新で送信する
* Columns/GridColumns/Permissions等それ以外のSiteSettingsTitle等は
* 現状のまま一切変更しない
*
* 使い方:
* node build-desired-config.js site-{SiteId}_{サイト名} 先にこちらを実行
* node apply-desired-config-full.js 差分表示のみ送信なし
* node apply-desired-config-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const baseDir = path.join(__dirname, "..", "..");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffList(curList, desList, key) {
(curList || []).forEach((c) => {
const d = (desList || []).find((x) => x.Id === c.Id);
const same = d && JSON.stringify(d) === JSON.stringify(c);
console.log(` ${key} Id:${c.Id} (${c.Title || c.Name || ""}) -> 変更${!same}`);
});
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
const SITE_ID = desired.SiteId;
const siteDir = findSiteDir(path.join(baseDir, "configs"), SITE_ID);
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
console.log("========================================");
console.log(` Scripts/Styles/ServerScripts/Processes 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log("--- Scripts ---");
diffList(siteSettings.Scripts, desired.SiteSettings.Scripts, "Script");
console.log("--- Styles ---");
diffList(siteSettings.Styles, desired.SiteSettings.Styles, "Style");
console.log("--- ServerScripts ---");
diffList(siteSettings.ServerScripts, desired.SiteSettings.ServerScripts, "ServerScript");
console.log("他のSiteSettingsColumns/GridColumns等・Permissions・Title等は一切変更しません。");
siteSettings.Scripts = desired.SiteSettings.Scripts;
siteSettings.Styles = desired.SiteSettings.Styles;
siteSettings.ServerScripts = desired.SiteSettings.ServerScripts;
siteSettings.Processes = desired.SiteSettings.Processes;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "desired-config-full-update");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-desired-config-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,227 @@
/**
* apply-permission-grant.js
* ------------------------------------------------------------
* SiteId 496626 の権限継承先サイトInheritPermissionに対し
* UserId 579 と同じ権限をユーザー名福島 成佳に追加する
* 480111側の継承元426945には適用しない
*
* Permissionsは SiteSettings 配下ではなくサイトデータ直下の項目であり
* updatesitesettings部分更新API はPermissionsを更新対象に含まない
* 公式マニュアルで確認済みそのため本スクリプトは updatesite全体更新を使い
* 継承先サイトの現状の Title/ReferenceType/ParentId/InheritPermission/SiteSettings
* 一切変更せずそのまま再送信しPermissionsのみ新規エントリを追加した配列を送信する
*
* 注意: 公式マニュアルには updatesite のリクエストボディでPermissionsを
* 指定できるという明記が無い記載が省略されている可能性はある
* 実行前に必ずプレビュー出力を確認すること
*
* 差分プレビュー送信結果は権限継承先サイトの modify/{リクエストラベル}/ フォルダへ保存する
* 修正依頼のたびに新しいフォルダを生成する規約省略時はタイムスタンプを自動採番
*
* 使い方:
* node apply-permission-grant.js 差分表示アーティファクト生成のみ送信なし
* node apply-permission-grant.js --execute 上記に加えて実際に送信する
* node apply-permission-grant.js --request=ラベル modifyフォルダ名を明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
const { findSiteDir, latestJsonPath, newModifyDir, latestModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 496626;
const MIRROR_FROM_USER_ID = 579;
const GRANT_TO_USER_NAME = "福島 成佳";
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const sourceSitePath =
latestJsonPath(configsDir, SOURCE_SITE_ID) ||
path.join(configsDir, `site-${SOURCE_SITE_ID}_latest.json`);
const sourceSiteJson = loadJson(sourceSitePath);
const inheritPermissionId = sourceSiteJson?.Response?.Data?.InheritPermission;
if (!inheritPermissionId) {
console.error(`[エラー] ${sourceSitePath} からInheritPermissionを取得できませんでした。`);
process.exit(1);
}
const targetPath =
latestJsonPath(configsDir, inheritPermissionId) ||
path.join(configsDir, `site-${inheritPermissionId}_latest.json`);
const targetJson = loadJson(targetPath, false);
if (!targetJson) {
console.error(`[エラー] ${targetPath} が見つかりません。先に node get-site-config.js を実行してください`);
console.error(`SiteId ${SOURCE_SITE_ID} 取得時にInheritPermission追跡ロジックで自動取得されるはずです`);
process.exit(1);
}
const targetData = targetJson.Response.Data;
const currentPermissions = targetData.Permissions || [];
const targetSiteDir = findSiteDir(configsDir, inheritPermissionId);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${inheritPermissionId} のフォルダが見つかりません。`);
process.exit(1);
}
// --request指定があればそれを使う。無ければ直近のmodifyフォルダを再利用し、
// まだ無ければ新規作成する(プレビュー→実行の一連の流れを同じフォルダにまとめるため)。
const modifyDir = requestLabel
? newModifyDir(targetSiteDir, requestLabel)
: latestModifyDir(targetSiteDir) || newModifyDir(targetSiteDir);
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
console.log("========================================");
console.log(` 権限追加${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象サイト: SiteId ${inheritPermissionId}"${targetData.Title}"、SiteId ${SOURCE_SITE_ID} の権限継承先)`);
console.log("");
// UserId 579 と同じ権限エントリを抽出複数PermissionTypeを持つ場合は全て対象
const sourceEntries = currentPermissions.filter((entry) => {
const [type, id] = String(entry).split(",");
return type === "User" && Number(id) === MIRROR_FROM_USER_ID;
});
if (sourceEntries.length === 0) {
console.error(`[エラー] UserId ${MIRROR_FROM_USER_ID} のPermissionsエントリが見つかりません。`);
console.error(`現在のPermissions: ${JSON.stringify(currentPermissions)}`);
process.exit(1);
}
// 福島 成佳 のUserIdを解決
const master = loadMasterData(baseDir);
const candidateIds = resolveId(master, { Type: "User", Name: GRANT_TO_USER_NAME });
if (candidateIds.length === 0) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」が見つかりません。configs/master_users.jsonを確認してください。`);
process.exit(1);
}
if (candidateIds.length > 1) {
console.error(`[エラー] ユーザー名「${GRANT_TO_USER_NAME}」に一致する候補が複数あります: ${candidateIds.join(", ")}`);
process.exit(1);
}
const grantToUserId = candidateIds[0];
// 579のエントリをコピーし、ユーザーIDだけ差し替えた新規エントリを作成
const newEntries = sourceEntries.map((entry) => {
const parts = String(entry).split(",");
parts[1] = String(grantToUserId);
return parts.join(",");
});
// 既に同一エントリが存在する場合は追加しない
const entriesToAdd = newEntries.filter((e) => !currentPermissions.includes(e));
console.log(`コピー元: UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`);
console.log(` 既存エントリ: ${JSON.stringify(sourceEntries)}`);
console.log(`追加先: UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`);
console.log(` 追加予定エントリ: ${JSON.stringify(newEntries)}`);
if (entriesToAdd.length === 0) {
console.log("\n[INFO] 追加予定のエントリは既に全て存在しています。変更の必要はありません。");
process.exit(0);
}
if (entriesToAdd.length !== newEntries.length) {
console.log(`[INFO] 一部エントリは既に存在するため、実際に追加するのは ${entriesToAdd.length} 件です: ${JSON.stringify(entriesToAdd)}`);
}
const updatedPermissions = [...currentPermissions, ...entriesToAdd];
console.log("\n★注意: updatesite APIでPermissionsを更新できるという公式記載は確認できていません。");
console.log(" 実行前に必ず内容を確認し、実行後は管理画面での反映結果も確認してください。");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${inheritPermissionId}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: inheritPermissionId,
Title: targetData.Title,
ReferenceType: targetData.ReferenceType,
ParentId: targetData.ParentId,
InheritPermission: targetData.InheritPermission,
Permissions: updatedPermissions,
SiteSettings: targetData.SiteSettings, // 変更しない(現状のまま再送信)
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(変更なし。Columns ${(targetData.SiteSettings?.Columns || []).length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${inheritPermissionId}_permission_diff_${ts}.txt`);
const diffText = [
`SiteId ${inheritPermissionId} 権限追加プレビュー (${ts})`,
`URL: ${url}`,
`対象: SiteId ${SOURCE_SITE_ID} の権限継承先`,
"",
`コピー元 UserId ${MIRROR_FROM_USER_ID} (${resolveName(master, { Type: "User", Id: MIRROR_FROM_USER_ID })})`,
` 既存エントリ: ${JSON.stringify(sourceEntries)}`,
`追加先 UserId ${grantToUserId} (${GRANT_TO_USER_NAME})`,
` 追加エントリ: ${JSON.stringify(entriesToAdd)}`,
"",
`変更前Permissions: ${JSON.stringify(currentPermissions)}`,
`変更後Permissions: ${JSON.stringify(updatedPermissions)}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-permission-grant.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(baseDir, "configs", `site-${inheritPermissionId}_permission_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面で反映結果を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,125 @@
/**
* apply-remark-fix-496626-full.js
* ------------------------------------------------------------
* updatesitesettings部分更新で送った Script Id:4 / Style Id:2 / ServerScript Id:1
* 修正がHTTP 200成功を返すにもかかわらず実際にはDBに反映されていない
* 直後のgetsiteで確認しても内容もVer/UpdatedTimeも変化しないことが分かったため
* 確実に反映される updatesite全体更新で同じ変更を送り直す
*
* 現在のサイト全体getsiteを取得しTitle/ReferenceType/ParentId/InheritPermission/
* Permissionsはそのまま再送信SiteSettingsもScripts[Id:4]/Styles[Id:2]/
* ServerScripts[Id:1]のBodyだけをローカルファイルの内容に差し替え他は一切変更しない
*
* 使い方:
* node apply-remark-fix-496626-full.js 差分表示のみ送信なし
* node apply-remark-fix-496626-full.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir, latestModifyDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const siteDir = findSiteDir(configsDir, SITE_ID);
const scriptBody = fs.readFileSync(path.join(siteDir, "scripts", "4_new03_一覧画面自動処理.js"), "utf-8");
const styleBody = fs.readFileSync(path.join(siteDir, "styles", "2_一覧スタイル調整.css"), "utf-8");
const serverScriptBody = fs.readFileSync(path.join(siteDir, "serverscripts", "1_一覧画面 物件詳細.js"), "utf-8");
(async () => {
console.log("[INFO] 現在のサイト情報を取得しています...");
const getUrl = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/getsite`;
const getRes = await fetch(getUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey }),
});
const getJson = await getRes.json();
if (getJson.StatusCode !== 200) {
console.error("[エラー] getsiteに失敗しました:", JSON.stringify(getJson).slice(0, 500));
process.exit(1);
}
const data = getJson.Response.Data;
console.log(`[OK] 取得しました現在のVer: ${data.Ver}, UpdatedTime: ${data.UpdatedTime}`);
const siteSettings = JSON.parse(JSON.stringify(data.SiteSettings));
const script4 = siteSettings.Scripts.find((s) => s.Id === 4);
const style2 = siteSettings.Styles.find((s) => s.Id === 2);
const serverScript1 = siteSettings.ServerScripts.find((s) => s.Id === 1);
if (!script4 || !style2 || !serverScript1) {
console.error("[エラー] Script Id:4 / Style Id:2 / ServerScript Id:1 のいずれかが見つかりません。");
process.exit(1);
}
console.log("========================================");
console.log(` Script/Style/ServerScript 差し替えupdatesite全体更新${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`Script Id:4 現状${script4.Body.length}文字 → 変更後${scriptBody.length}文字(変更${script4.Body !== scriptBody}`);
console.log(`Style Id:2 現状${style2.Body.length}文字 → 変更後${styleBody.length}文字(変更${style2.Body !== styleBody}`);
console.log(`ServerScript Id:1 現状${serverScript1.Body.length}文字 → 変更後${serverScriptBody.length}文字(変更${serverScript1.Body !== serverScriptBody}`);
console.log("他のSiteSettingsColumns/GridColumns/Processes等・Permissions・Title等は一切変更しません。");
script4.Body = scriptBody;
style2.Body = styleBody;
serverScript1.Body = serverScriptBody;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
Title: data.Title,
ReferenceType: data.ReferenceType,
ParentId: data.ParentId,
InheritPermission: data.InheritPermission,
Permissions: data.Permissions,
SiteSettings: siteSettings,
};
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "serverscript-remark-fix");
const ts = timestamp();
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const previewPath = path.join(modifyDir, `site-${SITE_ID}_full_update_preview_${ts}.json`);
fs.writeFileSync(previewPath, JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`\n[OK] 送信予定内容を保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626-full.js --execute");
process.exit(0);
}
console.log("\n実際に送信しますupdatesite...");
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesite`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_full_update_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
console.log(text);
})();

View File

@ -0,0 +1,154 @@
/**
* apply-remark-fix-496626.js
* ------------------------------------------------------------
* SiteId 496626 の一覧画面スクリプトId:4 new03_一覧画面自動処理
* 備考の3文字省略独自ポップアップ表示に対応した修正版へ差し替える
*
* 対象は desired_config.jsonbuild-desired-config.js
* configs/site-496626_着工要因(東京)v2/ 配下から再構成したもの
* SiteSettings.Scripts/Styles/ServerScripts/Processes をそのまま送信する
* 事前に確認した通り実際に内容が変わるのは Scripts Id:4 のみ
* 他はキー順序が変わるだけでBodyは現状と完全一致
*
* ユーザーがその場で直接適用してよいと明示的に許可した場合に限り実行する
* 一回限りの専用スクリプト.claude/skills/pleasanter-site-spec/SKILL.md
* クロスサイト構造変更節の運用に準拠
*
* 使い方:
* node apply-remark-fix-496626.js 差分表示のみ送信なし
* node apply-remark-fix-496626.js --execute 上記に加えて実際に送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { latestJsonPath, newModifyDir, latestModifyDir, findSiteDir } = require("./site-paths");
const SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
const config = loadJson(path.join(baseDir, "config.json"));
const desired = loadJson(path.join(baseDir, "desired_config.json"));
if (desired.SiteId !== SITE_ID) {
console.error(`[エラー] desired_config.json の SiteId (${desired.SiteId}) が ${SITE_ID} ではありません。`);
console.error(`先に node build-desired-config.js site-${SITE_ID}_新・着工要因(東京)v2 を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, SITE_ID);
const current = loadJson(currentPath, false);
const currentScripts = current?.Response?.Data?.SiteSettings?.Scripts || [];
const desiredScripts = desired.SiteSettings.Scripts || [];
const curScript4 = currentScripts.find((s) => s.Id === 4);
const newScript4 = desiredScripts.find((s) => s.Id === 4);
console.log("========================================");
console.log(` 一覧画面スクリプト(Id:4)の差し替え${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`対象: SiteId ${SITE_ID}"${current?.Response?.Data?.Title || ""}"`);
console.log("");
console.log(`Script Id:4 現状の文字数: ${curScript4 ? curScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 変更後の文字数: ${newScript4 ? newScript4.Body.length : "(取得できず)"}`);
console.log(`Script Id:4 以外のScripts/Styles/ServerScripts/Processesは内容変更なしキー順序のみ変わる場合あり`);
const siteDir = findSiteDir(configsDir, SITE_ID);
const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "remark-truncate-fix");
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_diff_${ts}.txt`);
fs.writeFileSync(
diffOutPath,
[
`SiteId ${SITE_ID} 一覧画面スクリプト(Id:4)差し替えプレビュー (${ts})`,
"",
"--- 現状のBody ---",
curScript4 ? curScript4.Body : "(取得できず)",
"",
"--- 変更後のBody ---",
newScript4 ? newScript4.Body : "(取得できず)",
].join("\n"),
"utf-8"
);
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${SITE_ID}/updatesitesettings`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: SITE_ID,
SiteSettings: desired.SiteSettings,
};
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("\n--- 送信されるBodyAPIキーはマスク表示 ---");
console.log(
JSON.stringify(
{
...maskedBody,
SiteSettings: {
Scripts: `${desired.SiteSettings.Scripts.length}Id:4のみ内容変更`,
Styles: `${desired.SiteSettings.Styles.length}件(変更なし)`,
ServerScripts: `${desired.SiteSettings.ServerScripts.length}件(変更なし)`,
Processes: `${desired.SiteSettings.Processes.length}件(変更なし)`,
},
},
null,
2
)
);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-remark-fix-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${SITE_ID}_remark_fix_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。管理画面・一覧画面で表示を確認してください。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,159 @@
/**
* apply-site-config.js
* ------------------------------------------------------------
* このスクリプトは常にドライランです実際のAPI送信は一切行いません
*
* Claudeが生成した desired_config.json希望構成を読み込み
* 1. 現状構成configs/site-{SiteId}_latest.jsonとの差分表示
* 2. 実際に送信されるはずの HTTPリクエストURL / Bodyの表示
* 3. そのまま使えるcurlコマンドの出力
* のみを行います実行fetch送信は行いません
*
* 内容を確認した上で問題なければ
* - 表示されたcurlコマンドを手動で実行する
* - またはこのファイル末尾の "実送信ブロック" のコメントを
* 自分の判断で外して実行する
* のいずれかで反映してください
*
* desired_config.json の形式:
* {
* "Mode": "partial", // "partial" = updatesitesettings部分更新 / "full" = updatesite全体更新
* "SiteId": 12345, // 省略時は config.json の値を使用
* "SiteSettings": { ... }, // 変更したい項目のみpartialの場合/ 全体fullの場合
* "Title": "...", // full更新時のみ必要な場合あり
* "ReferenceType": "...",
* "ParentId": ...,
* "InheritPermission": ...
* }
*
* 参照した公式マニュアル:
* - サイト更新全体: https://pleasanter.org/manual/api-site-update
* URL: POST {BaseUrl}/api/items/{SiteId}/updatesite
* - サイト設定の部分更新: https://pleasanter.org/ja/manual/api-update-sitesettings
* URL: POST {BaseUrl}/api/items/{SiteId}/updatesitesettings
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { latestJsonPath } = require("./site-paths");
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// ざっくりした差分表示キー単位・トップ1階層+SiteSettings1階層まで
// partialモードupdatesitesettingsは desired 側にあるキーだけがサーバーに送られ、
// 記載のないキーは現状のまま変わらない。そのため比較対象は afterKeys のみに限定する
// beforeにしかないキーを「削除される」ように見せるのは誤り
// fullモードupdatesiteはSiteSettings全体を丸ごと置き換えるため、beforeKeysも含めて比較する。
function diffTopLevel(before, after, label, mode = "partial") {
if (!before) {
console.log(` (現状データなし。get-site-config.jsで先に取得しておくと差分表示できます)`);
return;
}
const afterKeys = new Set(Object.keys(after || {}));
const allKeys = mode === "full" ? new Set([...Object.keys(before || {}), ...afterKeys]) : afterKeys;
let changed = false;
for (const key of allKeys) {
const b = JSON.stringify(before?.[key]);
const a = JSON.stringify(after?.[key]);
if (b !== a) {
changed = true;
console.log(` [変更] ${label}.${key}`);
console.log(` - 現状: ${truncate(b)}`);
console.log(` + 希望: ${truncate(a)}`);
}
}
if (!changed) console.log(" 差分なし");
}
function truncate(str, max = 200) {
if (str === undefined) return "(undefined)";
return str.length > max ? str.slice(0, max) + "...(略)" : str;
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
async function main() {
const baseDir = path.join(__dirname, "..", "..");
const configPath = path.join(baseDir, "config.json");
const desiredPath = process.argv[2] || path.join(baseDir, "desired_config.json");
const config = loadJson(configPath);
const desired = loadJson(desiredPath);
const siteId = desired.SiteId || config.SiteId;
const mode = desired.Mode || "partial";
const endpoint = mode === "full" ? "updatesite" : "updatesitesettings";
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/${endpoint}`;
const latestConfigPath =
latestJsonPath(path.join(baseDir, "configs"), siteId) ||
path.join(baseDir, "configs", `site-${siteId}_latest.json`);
const current = loadJson(latestConfigPath, false);
const currentSiteSettings =
current?.Response?.Data?.SiteSettings ||
current?.Response?.Site?.SiteSettings ||
current?.Response?.SiteSettings ||
null;
console.log("========================================");
console.log(" ドライラン結果(実際の送信は行いません)");
console.log("========================================");
console.log(`Mode : ${mode} (${mode === "full" ? "全体更新" : "部分更新"})`);
console.log(`URL : ${url}`);
console.log(`SiteId : ${siteId}`);
console.log("");
console.log("--- SiteSettings 差分(現状 → 希望) ---");
diffTopLevel(currentSiteSettings, desired.SiteSettings, "SiteSettings", mode);
console.log("");
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
...desired,
};
delete body.Mode; // Mode はエンドポイント選択にのみ使用し、送信Bodyには含めない
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("--- 送信されるはずのBodyAPIキーはマスク表示 ---");
console.log(JSON.stringify(maskedBody, null, 2));
console.log("");
console.log("--- 手動実行用 curl コマンド(内容確認の上、必要ならご自身で実行してください) ---");
console.log(
`curl -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(body)}'`
);
console.log("");
console.log("[注意] このスクリプトはAPIへの送信を一切行っていませんドライラン専用。");
console.log("内容に問題がなければ、上記curlコマンドを手動実行するか、");
console.log("スクリプト内の「実送信ブロック」を有効化する改修をご自身の判断で行ってください。");
// ------------------------------------------------------------
// 実送信ブロック(デフォルトでは絶対に到達しないようにガードしています)
// 意図的に反映を自動化したい場合のみ、下のガードを外して使ってください。
// ------------------------------------------------------------
const ENABLE_ACTUAL_SEND = false; // ← 安全のため常にfalse固定。変更は自己責任で。
if (ENABLE_ACTUAL_SEND) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
console.log(await res.text());
}
}
main();

View File

@ -0,0 +1,211 @@
/**
* apply-to-site-496626.js
* ------------------------------------------------------------
* configs/site-480111_updated.json のSiteSettingsをSiteId 496626
* まるごと上書き適用するupdatesite / Mode:"full"
*
* 通常このプロジェクトの apply-site-config.js はドライラン専用実送信しないだが
* 本スクリプトは今回のタスクに限りユーザーが明示的に許可した専用の直接実行スクリプト
* であり--execute 指定時のみ実際にAPIへPOSTするcurlは使わない
*
* Permissionsは送信ボディに含めない496626は権限をInheritPermissionで
* 継承しているため直接の書き込み対象ではない権限変更は apply-permission-grant.js
* を参照Title/ReferenceType/ParentId/InheritPermissionは496626自身の
* 現状値configs/site-496626_latest.jsonから取得しSiteSettingsのみ
* 480111由来の変更後内容で置き換える
*
* 差分プレビュー送信結果はSiteId 496626 modify/{リクエストラベル}/ フォルダへ保存する
* restructure-site-480111.js が作成したフォルダを --request= で指定するか省略時は
* 最新のmodifyフォルダを自動的に使う
*
* 使い方:
* node apply-to-site-496626.js 差分表示アーティファクト生成のみ送信なし
* node apply-to-site-496626.js --execute 上記に加えて実際に送信する
* node apply-to-site-496626.js --request=ラベル 対象のmodifyフォルダを明示指定
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, latestJsonPath, newModifyDir } = require("./site-paths");
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function loadJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
function maskApiKey(key) {
if (!key) return key;
return key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function diffTopLevel(before, after) {
const lines = [];
const afterKeys = Object.keys(after || {});
let changed = false;
for (const key of afterKeys) {
const b = JSON.stringify(before?.[key]);
const a = JSON.stringify(after?.[key]);
if (b !== a) {
changed = true;
const bLen = Array.isArray(before?.[key]) ? `(${before[key].length}件)` : "";
const aLen = Array.isArray(after?.[key]) ? `(${after[key].length}件)` : "";
lines.push(` [変更] SiteSettings.${key} ${bLen}${aLen}`);
}
}
if (!changed) lines.push(" 差分なし");
return lines;
}
// 対象のmodifyフォルダを解決する--request指定があればそれ、無ければ最新のものを使う
const targetSiteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!targetSiteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。先に node get-site-config.js を実行してください。`);
process.exit(1);
}
function resolveModifyDir() {
if (requestLabel) return newModifyDir(targetSiteDir, requestLabel);
const modifyRoot = path.join(targetSiteDir, "modify");
if (!fs.existsSync(modifyRoot)) {
console.error(`[エラー] modifyフォルダがまだありません。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const dirs = fs.readdirSync(modifyRoot).filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory());
if (dirs.length === 0) {
console.error(`[エラー] modifyフォルダの中身が空です: ${modifyRoot}`);
process.exit(1);
}
dirs.sort();
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
const modifyDir = resolveModifyDir();
console.log(`[INFO] modifyフォルダ: ${modifyDir}`);
const config = loadJson(path.join(baseDir, "config.json"));
const updatedPath = path.join(modifyDir, "site-480111_updated.json");
const updatedJson = loadJson(updatedPath);
const modifiedSiteSettings = updatedJson?.Response?.Data?.SiteSettings;
if (!modifiedSiteSettings) {
console.error(`[エラー] ${updatedPath} からSiteSettingsを取得できませんでした。先に node restructure-site-480111.js を実行してください。`);
process.exit(1);
}
const currentPath = latestJsonPath(configsDir, TARGET_SITE_ID);
const currentJson = loadJson(currentPath, false);
const currentData = currentJson?.Response?.Data;
if (!currentData) {
console.error(`[エラー] ${currentPath} が見つかりません。先に node get-site-config.jsSiteIdに${TARGET_SITE_ID}を含めて)を実行してください。`);
process.exit(1);
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/updatesite`;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: TARGET_SITE_ID,
Title: currentData.Title,
ReferenceType: currentData.ReferenceType,
ParentId: currentData.ParentId,
InheritPermission: currentData.InheritPermission,
SiteSettings: modifiedSiteSettings,
};
console.log("========================================");
console.log(` SiteId ${TARGET_SITE_ID} への適用${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`URL : ${url}`);
console.log(`Title : ${currentData.Title}`);
console.log(`InheritPermission: ${currentData.InheritPermission}(権限は今回変更しません)`);
console.log("");
console.log("--- SiteSettings 差分496626現状 → 480111由来の変更後 ---");
const diffLines = diffTopLevel(currentData.SiteSettings, modifiedSiteSettings);
diffLines.forEach((l) => console.log(l));
console.log("");
const newItem = (modifiedSiteSettings.Columns || []).find(
(c) => c.ColumnName === "Date031" && c.GridLabelText === "解体紹介料申請"
);
console.log(`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`);
console.log(`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`);
console.log(`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`);
console.log("");
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
console.log("--- 送信されるBodyAPIキーはマスク表示、SiteSettingsは件数のみ表示 ---");
console.log(
JSON.stringify(
{ ...maskedBody, SiteSettings: `(省略: Columns ${modifiedSiteSettings.Columns.length}件 等)` },
null,
2
)
);
const ts = timestamp();
const diffOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_diff_${ts}.txt`);
const diffText = [
`SiteId ${TARGET_SITE_ID} 適用差分プレビュー (${ts})`,
`URL: ${url}`,
`Title: ${currentData.Title}`,
`InheritPermission: ${currentData.InheritPermission}(今回は権限変更なし)`,
"",
"--- SiteSettings差分 ---",
...diffLines,
"",
`新規追加項目: ${newItem ? "解体紹介料申請 (Date031, 担当:" + newItem.Description + ")" : "(見つかりません)"}`,
`Columns件数 : ${(currentData.SiteSettings?.Columns || []).length}${modifiedSiteSettings.Columns.length}`,
`Sections件数: ${(currentData.SiteSettings?.Sections || []).length}${modifiedSiteSettings.Sections.length}`,
].join("\n");
fs.writeFileSync(diffOutPath, diffText, "utf-8");
console.log(`\n[OK] 差分プレビューを保存しました: ${diffOutPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node apply-to-site-496626.js --execute");
process.exit(0);
}
(async () => {
console.log("\n実際に送信します...");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
const resultOutPath = path.join(modifyDir, `site-${TARGET_SITE_ID}_apply_result_${ts}.json`);
fs.writeFileSync(resultOutPath, text, "utf-8");
console.log(`HTTP ${res.status} ${res.statusText}`);
console.log(`[OK] 応答を保存しました: ${resultOutPath}`);
try {
const json = JSON.parse(text);
if (json.StatusCode === 200) {
console.log("[OK] 適用に成功しました。");
} else {
console.error("[エラー] 適用に失敗した可能性があります。応答内容を確認してください。");
}
} catch {
console.log(text);
}
})();

View File

@ -0,0 +1,139 @@
/**
* build-desired-config.js
* ------------------------------------------------------------
* extract-site-config.js で分割した configs/site-{SiteId}/ 配下のファイル
* scripts/*.js, styles/*.css, serverscripts/*.js, processes.json
* manifest.json を手がかりに再構成しapply-site-config.js が読み込む
* desired_config.json を生成します
*
* このスクリプトもAPIへは一切送信しませんファイル生成のみ
* 反映は既存の運用どおり
* 1. node apply-site-config.js でドライラン確認差分送信Bodycurl表示
* 2. 内容を確認し問題なければ表示されたcurlコマンドを手動実行
* の流れに乗せてください
*
* 使い方:
* node build-desired-config.js site-214812
* フォルダ名は "site-{SiteId}_{サイト名}" 形式だが"site-{SiteId}" だけの
* 前方一致指定でも configs/ 配下から自動的に該当フォルダを探して解決する
* node build-desired-config.js ./configs/site-214812_物件基本情報
*
* 出力: desired_config.jsonこのスクリプトと同じ階層既存ファイルは上書き
*
* 対応範囲:
* Scripts / Styles / ServerScripts / Processes
* Mode:"partial"updatesitesettingsで反映可能なためそのまま組み込みます
* HtmlGridGuide等のガイド
* SiteSettingsではなくサイトデータ直下の項目のため
* 部分更新updatesitesettingsには含められません
* 反映したい場合は Mode:"full"updatesite用の別JSONを用意し
* Title/ReferenceType等の必須項目とあわせて指定してください
* このスクリプトはhtml/配下に抽出物がある場合警告のみ表示します
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function readJson(p) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// フォルダ名は "site-{SiteId}_{サイト名}" 形式。"site-{SiteId}" のみの指定でも
// configs/ 配下から前方一致で該当フォルダを探して解決する。
// 新形式(サイト名付き)を優先し、裸の "site-{SiteId}" 完全一致(リネーム前の古い抽出結果の可能性)は
// 他に候補が無い場合のみ後方互換フォールバックとして使う。
function resolveSiteDir(baseDir, arg) {
const configsDir = path.join(baseDir, "configs");
const direct = path.isAbsolute(arg) ? arg : path.join(configsDir, path.basename(arg));
const baseName = path.basename(arg);
const candidates = fs.existsSync(configsDir)
? fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return (
fs.statSync(full).isDirectory() && (name === baseName || name.startsWith(`${baseName}_`))
);
})
: [];
const suffixed = candidates.filter((name) => name !== baseName);
if (suffixed.length === 1) return path.join(configsDir, suffixed[0]);
if (suffixed.length > 1) {
console.error(`[エラー] "${baseName}" に一致するフォルダが複数見つかりました: ${suffixed.join(", ")}`);
console.error("フォルダ名をフルで指定してください。");
process.exit(1);
}
if (candidates.includes(baseName)) return path.join(configsDir, baseName);
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) return direct;
console.error(`[エラー] フォルダが見つかりません: ${direct}`);
console.error("先に node extract-site-config.jsまたはnode get-site-config.jsを実行してください。");
process.exit(1);
}
function rebuildList(manifestEntries, dir, bodyKey = "Body") {
return (manifestEntries || []).map((entry) => {
const { File, ...meta } = entry;
const filePath = path.join(dir, File);
if (!fs.existsSync(filePath)) {
console.error(`[エラー] manifest.jsonに記載のファイルが見つかりません: ${filePath}`);
process.exit(1);
}
const body = fs.readFileSync(filePath, "utf-8");
return { ...meta, [bodyKey]: body };
});
}
function main() {
const baseDir = path.join(__dirname, "..", "..");
const arg = process.argv[2];
if (!arg) {
console.error("[エラー] 対象フォルダを指定してください(例: node build-desired-config.js site-214812");
process.exit(1);
}
const siteDir = resolveSiteDir(baseDir, arg);
const manifestPath = path.join(siteDir, "manifest.json");
if (!fs.existsSync(manifestPath)) {
console.error(`[エラー] manifest.jsonが見つかりません: ${manifestPath}`);
console.error("先に node extract-site-config.js を実行してください。");
process.exit(1);
}
const manifest = readJson(manifestPath);
const siteSettings = {};
siteSettings.Scripts = rebuildList(manifest.Scripts, path.join(siteDir, "scripts"));
siteSettings.Styles = rebuildList(manifest.Styles, path.join(siteDir, "styles"));
siteSettings.ServerScripts = rebuildList(manifest.ServerScripts, path.join(siteDir, "serverscripts"));
const processesPath = path.join(siteDir, manifest.ProcessesFile || "processes.json");
siteSettings.Processes = fs.existsSync(processesPath) ? readJson(processesPath) : [];
const desired = {
Mode: "partial",
SiteId: manifest.SiteId,
SiteSettings: siteSettings,
};
const outPath = path.join(baseDir, "desired_config.json");
fs.writeFileSync(outPath, JSON.stringify(desired, null, 2), "utf-8");
console.log(`[OK] 生成しました: ${outPath}`);
console.log(` Scripts : ${siteSettings.Scripts.length}`);
console.log(` Styles : ${siteSettings.Styles.length}`);
console.log(` ServerScripts : ${siteSettings.ServerScripts.length}`);
console.log(` Processes : ${siteSettings.Processes.length}`);
if (manifest.Html && manifest.Html.length > 0) {
console.log("\n[注意] html/ 配下のガイドHTMLは、この desired_config.json には含まれていません。");
console.log(" GridGuide等はSiteSettingsではなくサイト直下の項目のため、部分更新(updatesitesettings)では送れません。");
console.log(' 反映するには Mode:"full"updatesite用の別JSONを用意し、Title等の必須項目とあわせて指定してください。');
}
console.log("\n次のステップ: node apply-site-config.js でドライラン確認 → 内容確認の上、表示されたcurlコマンドを手動実行してください。");
}
main();

View File

@ -0,0 +1,154 @@
/**
* extract-site-config.js
* ------------------------------------------------------------
* 取得済みのサイト構成 (configs/site-{SiteId}_latest.json) から
* Scripts / Styles / ServerScripts / Processes / ガイドHTML
* 個別ファイルとして configs/site-{SiteId}/ 配下に書き出します
*
* 使い方:
* node extract-site-config.js
* node extract-site-config.js ./configs/site-214812_latest.json
*
* 出力:
* configs/site-{SiteId}/
* manifest.json 抽出内容の一覧build-desired-config.jsでの再構成に使用
* scripts/{Id}_{Title}.js
* styles/{Id}_{Title}.css
* serverscripts/{Id}_{Title}.js
* html/{GuideName}.html GridGuide等ガイドHTML値が空でないもののみ
* processes.json プロセス設定SiteSettings.Processes をそのまま保存
*
* 注意:
* ガイドHTMLGridGuide等はSiteSettingsではなくサイトデータ直下の項目です
* そのため反映時はサイト設定の部分更新updatesitesettingsではなく
* サイト全体更新updatesite / Mode:"full"が必要になります
* Scripts/Styles/ServerScripts/Processesは部分更新Mode:"partial"で反映できます
* 詳しくは build-desired-config.js の実行結果メッセージを参照してください
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { sanitize, ensureSiteDir, latestJsonPath } = require("./site-paths");
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function extractList(items, outDir, ext, bodyKey = "Body") {
const manifestEntries = [];
if (!items || items.length === 0) return manifestEntries;
ensureDir(outDir);
for (const item of items) {
const fileName = `${item.Id}_${sanitize(item.Title || item.Name)}${ext}`;
fs.writeFileSync(path.join(outDir, fileName), item[bodyKey] || "", "utf-8");
const { [bodyKey]: _omit, ...meta } = item;
manifestEntries.push({ ...meta, File: fileName });
}
return manifestEntries;
}
const GUIDE_FIELDS = [
"GridGuide",
"EditorGuide",
"CalendarGuide",
"CrosstabGuide",
"GanttGuide",
"BurnDownGuide",
"TimeSeriesGuide",
"AnalyGuide",
"KambanGuide",
"ImageLibGuide",
];
// get-site-config.js からも呼び出せるよう関数化。
// CLI実行時直接 `node extract-site-config.js`)は末尾のブロックが引数解決を行う。
function extractSiteConfig(latestPath) {
const baseDir = path.join(__dirname, "..", "..");
if (!fs.existsSync(latestPath)) {
console.error(`[エラー] ファイルが見つかりません: ${latestPath}`);
console.error("先に node get-site-config.js を実行してください。");
process.exit(1);
}
const json = JSON.parse(fs.readFileSync(latestPath, "utf-8"));
const data = json?.Response?.Data;
if (!data) {
console.error("[エラー] Response.Data が見つかりません。getsiteのレスポンス形式を確認してください。");
process.exit(1);
}
const siteId = data.SiteId;
const settings = data.SiteSettings || {};
const configsDir = path.join(baseDir, "configs");
const outRoot = ensureSiteDir(configsDir, siteId, data.Title);
const manifest = {
SiteId: siteId,
Title: data.Title,
ExtractedAt: new Date().toISOString(),
SourceFile: path.relative(baseDir, latestPath).replace(/\\/g, "/"),
};
manifest.Scripts = extractList(settings.Scripts, path.join(outRoot, "scripts"), ".js");
manifest.Styles = extractList(settings.Styles, path.join(outRoot, "styles"), ".css");
manifest.ServerScripts = extractList(settings.ServerScripts, path.join(outRoot, "serverscripts"), ".js");
// ガイドHTMLサイトデータ直下。SiteSettingsではない点に注意
const htmlDir = path.join(outRoot, "html");
const htmlEntries = [];
for (const field of GUIDE_FIELDS) {
const value = data[field];
if (value) {
ensureDir(htmlDir);
const fileName = `${field}.html`;
fs.writeFileSync(path.join(htmlDir, fileName), value, "utf-8");
htmlEntries.push({ Field: field, File: fileName, TopLevel: true });
}
}
manifest.Html = htmlEntries;
// プロセス設定 … 個別分割せず1つの定義ファイルとして保存
fs.writeFileSync(
path.join(outRoot, "processes.json"),
JSON.stringify(settings.Processes || [], null, 2),
"utf-8"
);
manifest.ProcessesFile = "processes.json";
manifest.ProcessesCount = (settings.Processes || []).length;
fs.writeFileSync(path.join(outRoot, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
console.log(`[OK] 抽出完了: ${outRoot}`);
console.log(` Scripts : ${manifest.Scripts.length}`);
console.log(` Styles : ${manifest.Styles.length}`);
console.log(` ServerScripts : ${manifest.ServerScripts.length}`);
console.log(` Html(ガイド) : ${manifest.Html.length}`);
console.log(` Processes : ${manifest.ProcessesCount}件 → processes.json`);
console.log("\n次のステップ: 各ファイルを直接編集するか、Claudeに修正内容を指示してください。");
console.log(`修正後は次のコマンドで反映用JSONを作成できます: node build-desired-config.js ${path.basename(outRoot)}`);
return manifest;
}
// CLIとして直接実行された場合のみ、引数からlatestPathを解決して実行する
if (require.main === module) {
const baseDir = path.join(__dirname, "..", "..");
let latestPath = process.argv[2];
if (!latestPath) {
const configPath = path.join(baseDir, "config.json");
if (!fs.existsSync(configPath)) {
console.error("[エラー] config.jsonが見つかりません。抽出対象のJSONパスを引数で指定してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const resolved = latestJsonPath(path.join(baseDir, "configs"), config.SiteId);
latestPath = resolved || path.join(baseDir, "configs", `site-${config.SiteId}_latest.json`);
}
extractSiteConfig(latestPath);
}
module.exports = { extractSiteConfig };

View File

@ -0,0 +1,142 @@
/**
* get-master-data.js
* ------------------------------------------------------------
* Pleasanterのユーザー/組織(部署)/グループ一覧を取得し
* ./configs/ 配下に保存しますresolve-names.js が読み込む
*
* 使い方:
* node get-master-data.js
* node get-master-data.js ./config.json 設定ファイルを明示指定する場合
*
* 参照した公式マニュアル:
* - 開発者向け機能APIユーザ操作ユーザ取得全て
* https://pleasanter.org/ja/manual/api-user-get-all
* - 開発者向け機能API組織操作組織取得
* https://pleasanter.org/ja/manual/api-dept-get
* - 開発者向け機能APIグループ操作グループ取得
* https://pleasanter.org/ja/manual/api-group-get
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const TARGETS = [
{ key: "users", endpoint: "users/get", outFile: "master_users.json", label: "ユーザー", idField: "UserId" },
{ key: "depts", endpoint: "depts/get", outFile: "master_depts.json", label: "組織", idField: "DeptId" },
{ key: "groups", endpoint: "groups/get", outFile: "master_groups.json", label: "グループ", idField: "GroupId" },
];
// 一部API/api/users/get 等はページング応答TotalCount > 1回あたりのData件数を返すため、
// 全件取得できるまで Offset をずらして繰り返し取得し、Data配列を結合する。
async function fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/${target.endpoint}`;
let allData = [];
let offset = 0;
let totalCount = null;
let firstJson = null;
while (true) {
const body = {
ApiVersion: ApiVersion || "1.1",
ApiKey: ApiKey,
Offset: offset,
};
console.log(`[INFO] 取得先: ${url} (Offset: ${offset})`);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (err) {
console.error(`[エラー] ${target.label}の取得に失敗しました:`, err.message);
return false;
}
const text = await response.text();
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return false;
}
let json;
try {
json = JSON.parse(text);
} catch (err) {
console.error(`[エラー] ${target.label}のレスポンスがJSONとして解析できませんでした。`);
console.error(text);
return false;
}
if (!firstJson) firstJson = json;
const pageData = json?.Response?.Data || [];
allData = allData.concat(pageData);
totalCount = json?.Response?.TotalCount ?? allData.length;
if (pageData.length === 0 || allData.length >= totalCount) break;
offset = allData.length;
}
// ページ間でOffset起点がずれて重複が混ざる場合があるため、idFieldで重複排除する
const seen = new Set();
allData = allData.filter((item) => {
const id = item[target.idField];
if (seen.has(id)) return false;
seen.add(id);
return true;
});
// 結合済みのData配列で1件目のレスポンスを上書きし、全件入りのJSONとして保存する
const outJson = { ...firstJson, Response: { ...firstJson.Response, Data: allData } };
const outFile = path.join(outDir, target.outFile);
fs.writeFileSync(outFile, JSON.stringify(outJson, null, 2), "utf-8");
console.log(`[OK] ${target.label}: ${allData.length}/${totalCount}件 保存しました: ${outFile}`);
return true;
}
async function main() {
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
console.error("config.example.json をコピーして config.json を作成してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const { BaseUrl, ApiKey, ApiVersion } = config;
if (!BaseUrl || !ApiKey) {
console.error("[エラー] config.json に BaseUrl / ApiKey を設定してください。");
process.exit(1);
}
const outDir = path.join(path.join(__dirname, "..", ".."), "configs", "master");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const results = [];
for (const target of TARGETS) {
const ok = await fetchAndSave(target, { BaseUrl, ApiKey, ApiVersion }, outDir);
results.push({ target: target.label, ok });
}
console.log("\n==== 取得結果まとめ ====");
for (const { target, ok } of results) {
console.log(` ${target}: ${ok ? "OK" : "失敗"}`);
}
if (results.some((r) => !r.ok)) {
process.exit(1);
}
}
main();

View File

@ -0,0 +1,188 @@
/**
* get-site-config.js
* ------------------------------------------------------------
* プリザンターの指定サイトの構成情報SiteSettings等を取得し
* ./configs/ 配下にタイムスタンプ付きJSONとして保存します
*
* 使い方:
* node get-site-config.js
* node get-site-config.js ./config.json 設定ファイルを明示指定する場合
*
* 事前準備:
* 1. config.example.json config.json にコピーし
* BaseUrl / SiteId / ApiKey を実環境の値に書き換えてください
* SiteId はカンマ区切りで複数指定可能です: "480111,438166,96235"
* 1件のみの場合は従来通り数値: 480111でも構いません
* 2. Node.js 18以降組み込みfetchを使用
*
* 参照した公式マニュアル:
* - 開発者向け機能APIサイト操作サイト更新
* https://pleasanter.org/manual/api-site-update
* - 開発者向け機能APIサイト操作サイト設定の更新部分追加/更新/削除
* https://pleasanter.org/ja/manual/api-update-sitesettings
*
* 注意:
* プリザンターのバージョンにより get のレスポンス構造
* SiteSettingsが Response.Site 配下にあるか等が異なる場合があります
* 初回実行時はコンソールに出力されるレスポンスのトップレベルキーを確認し
* 想定通りの構造か確認してください
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { extractSiteConfig } = require("./extract-site-config");
const { ensureSiteDir, sitesettingsDir } = require("./site-paths");
// config.json の SiteId数値 / カンマ区切り文字列 / 配列)を
// 個々のSiteId文字列の配列に正規化する。
function parseSiteIds(rawSiteId) {
const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(",");
return list
.map((id) => String(id).trim())
.filter((id) => id.length > 0);
}
// 1サイト分の取得・保存・抽出処理
async function fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/getsite`;
const body = {
ApiVersion: ApiVersion || "1.1",
ApiKey: ApiKey,
};
console.log(`[INFO] 取得先: ${url}`);
console.log(`[INFO] SiteId: ${siteId}`);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (err) {
console.error("[エラー] リクエスト送信に失敗しました:", err.message);
console.error("BaseUrlの疎通・ネットワーク設定を確認してください。");
return { ok: false };
}
const text = await response.text();
if (!response.ok) {
console.error(`[エラー] HTTP ${response.status} ${response.statusText}`);
console.error(text);
return { ok: false };
}
let json;
try {
json = JSON.parse(text);
} catch (err) {
console.error("[エラー] レスポンスがJSONとして解析できませんでした。");
console.error(text);
return { ok: false };
}
// レスポンス構造の確認用ログ(初回はここを見て構造を把握してください)
console.log("[INFO] レスポンス トップレベルキー:", Object.keys(json));
if (json.Response) {
console.log("[INFO] Response配下のキー:", Object.keys(json.Response));
}
// 保存: configs/site-{siteId}_{サイト名}/sitesettings/ 配下へ
const configsDir = path.join(path.join(__dirname, "..", ".."), "configs");
const title = json?.Response?.Data?.Title;
const siteDir = ensureSiteDir(configsDir, siteId, title);
const outDir = sitesettingsDir(siteDir);
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-");
const outFile = path.join(outDir, `site-${siteId}_${timestamp}.json`);
fs.writeFileSync(outFile, JSON.stringify(json, null, 2), "utf-8");
// 常に「最新」ファイルも別名で保持Claudeへのアップロード用に固定名があると便利
const latestFile = path.join(outDir, `site-${siteId}_latest.json`);
fs.writeFileSync(latestFile, JSON.stringify(json, null, 2), "utf-8");
console.log(`[OK] 保存しました: ${outFile}`);
console.log(`[OK] 最新版としても保存しました: ${latestFile}`);
// 取得と同時に Scripts/Styles/ServerScripts/Processes/ガイドHTML も抽出する
console.log("\n[INFO] 続けて設定の個別ファイル抽出を実行します...");
extractSiteConfig(latestFile);
// 権限継承先の追跡: InheritPermission が自サイト以外を指している場合、
// 実際の権限はそちらのサイトにあるため、そのサイトも同様に取得する。
const inheritPermission = json?.Response?.Data?.InheritPermission;
return { ok: true, inheritPermission };
}
async function main() {
const configPath = process.argv[2] || path.join(path.join(__dirname, "..", ".."), "config.json");
if (!fs.existsSync(configPath)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${configPath}`);
console.error("config.example.json をコピーして config.json を作成してください。");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const { BaseUrl, SiteId, ApiKey, ApiVersion } = config;
if (!BaseUrl || !SiteId || !ApiKey) {
console.error("[エラー] config.json に BaseUrl / SiteId / ApiKey を設定してください。");
process.exit(1);
}
const siteIds = parseSiteIds(SiteId);
if (siteIds.length === 0) {
console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。");
process.exit(1);
}
// 権限継承先の追跡: 指定サイトの取得後、InheritPermission が自サイト以外を
// 指していれば、そのサイトも同じ要領でキューに追加して取得する
// (権限の実体は継承元サイド側にあるため)。
const queue = [...siteIds];
const visited = new Set();
const results = [];
while (queue.length > 0) {
const siteId = String(queue.shift());
if (visited.has(siteId)) continue;
visited.add(siteId);
console.log(`\n==== [${results.length + 1}] SiteId: ${siteId} ====`);
const { ok, inheritPermission } = await fetchAndSaveSite(siteId, { BaseUrl, ApiKey, ApiVersion });
results.push({ siteId, ok });
if (
ok &&
inheritPermission != null &&
String(inheritPermission) !== "0" &&
String(inheritPermission) !== siteId &&
!visited.has(String(inheritPermission))
) {
console.log(`[INFO] SiteId ${siteId} は権限をSiteId ${inheritPermission} から継承しています。継承元も取得します。`);
queue.push(String(inheritPermission));
}
}
console.log("\n==== 取得結果まとめ ====");
for (const { siteId, ok } of results) {
console.log(` SiteId ${siteId}: ${ok ? "OK" : "失敗"}`);
}
console.log("\n次のステップ: latest.jsonをClaudeのチャットにアップロードして仕様書化を依頼するか、");
console.log("configs/site-{SiteId}/ 配下のファイルを直接編集・指示して修正してください。");
if (results.some((r) => !r.ok)) {
process.exit(1);
}
}
main();

View File

@ -0,0 +1,185 @@
/**
* migrate-data-480111-to-496626.js
* ------------------------------------------------------------
* SiteId 480111 の全レコードを取得しSiteSettings側で行った連番リネーム
* Date031047 Date032048Date131147 Date132148
* Check131147 Check132148Class031047 Class032048
* 同じマッピングでフィールド値を付け替えた上でSiteId 496626
* bulkupsert新規作成でインポートする
*
* 除外するもの:
* - ResultId / SiteId / Ver / CreatedTime / UpdatedTime / Locked / ItemTitle
* 新サイト側で自動採番自動計算されるため
* - Comments変更履歴コメントテキスト量が多くCreatorが480111時点のユーザーIDのため
* そのまま再現すると誤解を招く可能性がある除外し必要なら別途手動で移行する
* - AttachmentsHash添付ファイルはサイトをまたいだAPI経由の単純コピーができないため対象外
* 添付がある場合はプレビュー時に警告する
*
* 使い方:
* node migrate-data-480111-to-496626.js 件数サンプル警告の表示のみ送信なし
* node migrate-data-480111-to-496626.js --execute 上記に加えて実際にbulkupsertで送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const { findSiteDir, newModifyDir } = require("./site-paths");
const SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const BATCH_SIZE = 20;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
const execute = process.argv.includes("--execute");
const requestArg = process.argv.find((a) => a.startsWith("--request="));
const requestLabel = requestArg ? requestArg.split("=")[1] : null;
function pad3(n) {
return String(n).padStart(3, "0");
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
// SiteSettings側の連番リネームと同じマッピングを構築する
const classMap = {};
const dateMap = {};
const checkMap = {};
for (let i = 31; i <= 47; i++) {
classMap[`Class${pad3(i)}`] = `Class${pad3(i + 1)}`;
dateMap[`Date${pad3(i)}`] = `Date${pad3(i + 1)}`;
dateMap[`Date${i + 100}`] = `Date${i + 101}`;
checkMap[`Check${i + 100}`] = `Check${i + 101}`;
}
function remapHash(hash, map) {
const result = {};
for (const [key, value] of Object.entries(hash || {})) {
const newKey = map[key] || key;
result[newKey] = value;
}
return result;
}
async function fetchAllRecords(siteId, { BaseUrl, ApiKey, ApiVersion }) {
const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/get`;
let all = [];
let offset = 0;
let totalCount = null;
while (true) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ApiVersion: ApiVersion || "1.1", ApiKey, Offset: offset }),
});
const json = await res.json();
if (json.StatusCode !== 200) {
console.error(`[エラー] レコード取得に失敗しましたOffset:${offset}:`, JSON.stringify(json).slice(0, 500));
process.exit(1);
}
const page = json.Response.Data || [];
all = all.concat(page);
totalCount = json.Response.TotalCount ?? all.length;
if (page.length === 0 || all.length >= totalCount) break;
offset = all.length;
}
return all;
}
function buildMigratedRecord(record) {
return {
Body: record.Body,
Status: record.Status,
Manager: record.Manager,
Owner: record.Owner,
ClassHash: remapHash(record.ClassHash, classMap),
NumHash: record.NumHash || {},
DateHash: remapHash(record.DateHash, dateMap),
DescriptionHash: record.DescriptionHash || {},
CheckHash: remapHash(record.CheckHash, checkMap),
};
}
async function main() {
const config = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
console.log(`[INFO] SiteId ${SOURCE_SITE_ID} の全レコードを取得しています...`);
const sourceRecords = await fetchAllRecords(SOURCE_SITE_ID, config);
console.log(`[OK] ${sourceRecords.length}件取得しました。`);
const attachmentRecords = sourceRecords.filter(
(r) => r.AttachmentsHash && Object.values(r.AttachmentsHash).some((a) => Array.isArray(a) && a.length > 0)
);
const migrated = sourceRecords.map(buildMigratedRecord);
console.log("\n========================================");
console.log(` データ移行 (480111 → 496626)${execute ? "(実行)" : "(プレビューのみ)"}`);
console.log("========================================");
console.log(`移行対象件数: ${migrated.length}`);
if (attachmentRecords.length > 0) {
console.log(`\n⚠️ 添付ファイルを含むレコードが${attachmentRecords.length}件ありますResultId: ${attachmentRecords.map((r) => r.ResultId).join(", ")})。`);
console.log(" 添付ファイルは本スクリプトの移行対象外ですAPI経由でのサイト間コピーは非対応のため。");
console.log(" 必要な場合は管理画面から手動での再アップロードを検討してください。");
} else {
console.log("添付ファイルを含むレコードはありません。");
}
console.log("\nComments変更履歴コメントは移行対象外です。");
console.log("\n--- サンプル1件目、移行後の姿 ---");
console.log(JSON.stringify(migrated[0], null, 2));
const siteDir = findSiteDir(configsDir, TARGET_SITE_ID);
if (!siteDir) {
console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。`);
process.exit(1);
}
// requestLabel未指定時は固定ラベルを使い、プレビュー→実行を同じフォルダにまとめる
const modifyDir = newModifyDir(siteDir, requestLabel || "data-migration");
const previewPath = path.join(modifyDir, "data_migration_preview.json");
fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8");
console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`);
if (!execute) {
console.log("\n[注意] --execute が指定されていないため、送信は行っていません。");
console.log("内容を確認の上、問題なければ次を実行してください: node migrate-data-480111-to-496626.js --execute");
return;
}
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/bulkupsert`;
const results = [];
for (let i = 0; i < migrated.length; i += BATCH_SIZE) {
const batch = migrated.slice(i, i + BATCH_SIZE);
console.log(`\n実際に送信します(${i + 1}${i + batch.length}件目 / 全${migrated.length}件)...`);
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SsCache: true,
Data: batch,
}),
});
const text = await res.text();
console.log(`HTTP ${res.status} ${res.statusText}`);
let json;
try {
json = JSON.parse(text);
console.log(json.StatusCode === 200 ? "[OK] このバッチは成功しました。" : "[エラー] このバッチは失敗した可能性があります。");
} catch {
console.log(text.slice(0, 1000));
}
results.push({ batchStart: i, status: res.status, body: text });
}
const resultPath = path.join(modifyDir, `data_migration_result_${timestamp()}.json`);
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8");
console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`);
}
main();

View File

@ -0,0 +1,83 @@
/**
* resolve-names.js
* ------------------------------------------------------------
* get-master-data.js が保存した configs/master_users.json /
* master_depts.json / master_groups.json を読み込み
* Permissions等に登場する {Type, Id} 形式の値とユーザー/組織/グループ名との
* 相互変換を行うユーティリティ
*
* 使い方他スクリプトから:
* const { loadMasterData, resolveName, resolveId } = require("./resolve-names");
* const master = loadMasterData();
* resolveName(master, { Type: "User", Id: 579 }); // => "福島 成佳" 等
* resolveId(master, { Type: "User", Name: "福島 成佳" }); // => [579] 一致するIdの配列
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const MASTER_FILES = {
User: "master_users.json",
Dept: "master_depts.json",
Group: "master_groups.json",
};
function readJson(p, required = true) {
if (!fs.existsSync(p)) {
if (required) {
console.error(`[エラー] ファイルが見つかりません: ${p}`);
console.error("先に node get-master-data.js を実行してください。");
process.exit(1);
}
return null;
}
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
// configs/master/master_*.json を読み込み、Type別に { idField, nameField, list } を返す
function loadMasterData(baseDir = path.join(__dirname, "..", "..")) {
const masterDir = path.join(baseDir, "configs", "master");
const usersJson = readJson(path.join(masterDir, MASTER_FILES.User));
const deptsJson = readJson(path.join(masterDir, MASTER_FILES.Dept));
const groupsJson = readJson(path.join(masterDir, MASTER_FILES.Group));
return {
User: {
idField: "UserId",
nameField: "Name",
list: usersJson?.Response?.Data || [],
},
Dept: {
idField: "DeptId",
nameField: "DeptName",
list: deptsJson?.Response?.Data || [],
},
Group: {
idField: "GroupId",
nameField: "GroupName",
list: groupsJson?.Response?.Data || [],
},
};
}
// { Type: "User"|"Dept"|"Group", Id } -> 名前文字列(見つからなければ "(不明:Type#Id)"
function resolveName(master, { Type, Id }) {
const bucket = master[Type];
if (!bucket) return `(不明な種別:${Type})`;
const item = bucket.list.find((x) => String(x[bucket.idField]) === String(Id));
if (!item) return `(不明:${Type}#${Id})`;
return item[bucket.nameField];
}
// { Type: "User"|"Dept"|"Group", Name } -> 一致するIdの配列完全一致、複数該当もありうる
function resolveId(master, { Type, Name }) {
const bucket = master[Type];
if (!bucket) return [];
return bucket.list
.filter((x) => x[bucket.nameField] === Name)
.map((x) => x[bucket.idField]);
}
module.exports = { loadMasterData, resolveName, resolveId };

View File

@ -0,0 +1,365 @@
/**
* 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: 既存項目 Date031047系の連番を Date032048系へ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 SOURCE_SITE_ID = 480111;
const TARGET_SITE_ID = 496626;
const baseDir = path.join(__dirname, "..", "..");
const configsDir = path.join(baseDir, "configs");
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(" OK17項目 × 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("営業タブに対応するViewId:2, 営業担当項目)が見つかりません。");
const viewInsertIdx = salesView.GridColumns.indexOf("Date030") + 1;
salesView.GridColumns.splice(viewInsertIdx, 0, "Date031");
if (typeof settings.SectionLatestId === "number") {
settings.SectionLatestId = 49;
}
console.log(" OKColumns 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(" OKCheck009/010/011を除外、Check018/ClassOを追加。列定義自体は保持");
// ============================================================
// フェーズF: 物件詳細1DescriptionA入力ガイド追加
// ============================================================
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(` OKScripts ${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 で差分を確認してください。`);
}

View File

@ -0,0 +1,105 @@
/**
* site-paths.js
* ------------------------------------------------------------
* configs/ 配下のフォルダ構成を一箇所で管理する共有ヘルパー
*
* 構成:
* configs/
* master/ ユーザー/組織/グループのマスターデータget-master-data.js
* site-{SiteId}_{サイト名}/
* sitesettings/ getsiteの生JSONsite-{SiteId}_latest.json / _{timestamp}.json
* scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
* extract-site-config.js による分割済みファイル従来通り
* docs/ 仕様書(.md)や改善要望プラン等sitesettings以外の関連ドキュメント
* modify/{request}/ 修正依頼ごとに新規作成変更前後のconfigファイルや差分結果を保管
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
function sanitize(name) {
return (
String(name || "untitled")
.replace(/[\\/:*?"<>|]/g, "_")
.trim()
.slice(0, 60) || "untitled"
);
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return dir;
}
// configs/ 配下から "site-{siteId}_*"(前方一致)または "site-{siteId}"(完全一致)の
// フォルダを探す。見つからなければ null。
function findSiteDir(configsDir, siteId) {
if (!fs.existsSync(configsDir)) return null;
const bare = `site-${siteId}`;
const prefix = `site-${siteId}_`;
const candidates = fs.readdirSync(configsDir).filter((name) => {
const full = path.join(configsDir, name);
return fs.statSync(full).isDirectory() && (name === bare || name.startsWith(prefix));
});
const suffixed = candidates.filter((n) => n !== bare);
if (suffixed.length >= 1) return path.join(configsDir, suffixed[0]);
if (candidates.includes(bare)) return path.join(configsDir, bare);
return null;
}
// 既存フォルダがあればそれを使い、無ければ site-{siteId}_{sanitize(title)} を新規作成する
function ensureSiteDir(configsDir, siteId, title) {
const existing = findSiteDir(configsDir, siteId);
if (existing) return existing;
const dir = path.join(configsDir, `site-${siteId}_${sanitize(title)}`);
return ensureDir(dir);
}
function sitesettingsDir(siteDir) {
return ensureDir(path.join(siteDir, "sitesettings"));
}
function docsDir(siteDir) {
return ensureDir(path.join(siteDir, "docs"));
}
// 修正依頼用の新規フォルダを作成するlabelを省略した場合はタイムスタンプを使う
function newModifyDir(siteDir, label) {
const name = label || new Date().toISOString().replace(/[:.]/g, "-");
return ensureDir(path.join(siteDir, "modify", name));
}
// 既存のmodifyフォルダの中で最も新しいものフォルダ名の降順ソートを返す。無ければnull。
function latestModifyDir(siteDir) {
const modifyRoot = path.join(siteDir, "modify");
if (!fs.existsSync(modifyRoot)) return null;
const dirs = fs
.readdirSync(modifyRoot)
.filter((n) => fs.statSync(path.join(modifyRoot, n)).isDirectory())
.sort();
if (dirs.length === 0) return null;
return path.join(modifyRoot, dirs[dirs.length - 1]);
}
// サイトの最新getsiteダンプsite-{siteId}_latest.jsonのパスを返す。
// フォルダ自体が無い場合は null。
function latestJsonPath(configsDir, siteId) {
const siteDir = findSiteDir(configsDir, siteId);
if (!siteDir) return null;
return path.join(siteDir, "sitesettings", `site-${siteId}_latest.json`);
}
module.exports = {
sanitize,
ensureDir,
findSiteDir,
ensureSiteDir,
sitesettingsDir,
docsDir,
newModifyDir,
latestModifyDir,
latestJsonPath,
};

View File

@ -2,7 +2,11 @@
"permissions": {
"allow": [
"Bash(node -c get-site-config.js)",
"Edit(/.claude/skills/pleasanter-site-spec/**)"
"Bash(node get-master-data.js)",
"Bash(node get-site-config.js)",
"WebFetch(domain:pleasanter.org)",
"Bash(node -e ' *)",
"Bash(node restructure-site-480111.js)"
]
}
}

View File

@ -1,36 +1,63 @@
---
name: pleasanter-site-spec
description: Pleasanterのサイト設定JSONget-site-config.js / api-site-get の getsite レスポンスから、SiteSettingsを読み解いたMarkdown仕様書を生成し、Scripts/Styles/ServerScripts/Processesの抽出・編集・反映extract-site-config.js / build-desired-config.js / apply-site-config.jsを行う。また、技術仕様書とは別に業務担当者向けの概要書docs/site-{SiteId}_{サイト名}_overview.mdを作成し、md-to-pdf.jsでPDF化する。「仕様書を作って」「サイト仕様書」「SiteSettingsをドキュメント化」「スクリプトを抜き出して」「プロセス設定を直して反映して」「概要書を作って」「PDFにして」「site-spec」等の依頼で使う。
description: Pleasanterのサイト設定JSONget-site-config.js / api-site-get の getsite レスポンスから、SiteSettingsを読み解いたMarkdown仕様書を生成し、Scripts/Styles/ServerScripts/Processesの抽出・編集・反映extract-site-config.js / build-desired-config.js / apply-site-config.jsを行う。また、ユーザー/組織/グループのマスターデータ取得get-master-data.js、別サイトへのSiteSettings丸ごと適用や項目の追加・連番リネームといった構造変更restructure-site-*.js / apply-to-site-*.js、権限Permissionsの名前指定での追加apply-permission-grant.jsにも対応する。また、プロセス設定ProcessesのStatus遷移を横向きMermaidフローチャートとして可視化するgenerate-process-flowchart.js。「仕様書を作って」「サイト仕様書」「SiteSettingsをドキュメント化」「スクリプトを抜き出して」「プロセス設定を直して反映して」「別サイトに丸ごと適用して」「項目を追加して連番をズラして」「この人と同じ権限を追加して」「フローチャートを作って」「フローチャートを更新」「site-spec」等の依頼で使う。
---
# Pleasanter サイト仕様書生成
# Pleasanter サイト仕様書生成・構成管理
`./siteSettingJsons/site-{SiteId}_latest.json``getsite` APIレスポンス、`Response.Data` 配下にサイト情報)と、
`./configs/site-{SiteId}_{サイト名}/``extract-site-config.js` による抽出済みScripts/Styles/ServerScripts/Processes
読み解き、`./docs/site-{SiteId}_{サイト名}_spec.md` として日本語のMarkdown仕様書を出力する。
`configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json``getsite` APIレスポンス、`Response.Data` 配下にサイト情報)を読み解き、
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` として日本語のMarkdown仕様書を出力する。
この仕様書はClaudeが対話的に読み解いて執筆するものであり、機械的なテンプレート処理で自動生成するものではない
`generate-site-documentation.js` は過去に試みられた機械生成版で、サイト間の暗黙的な関連や業務的な類推まではできず
品質面の問題があったため現在のワークフローでは使用しない。削除はされていないが参照しないこと)。
## フォルダ構成configs/ 配下)
```
configs/
master/ … ユーザー/組織/グループのマスターデータget-master-data.js
master_users.json
master_depts.json
master_groups.json
site-{SiteId}_{サイト名}/
sitesettings/ … getsiteの生JSONget-site-config.jsが保存。latest.json + タイムスタンプ付き履歴)
site-{SiteId}_latest.json
site-{SiteId}_{timestamp}.json
scripts/ styles/ serverscripts/ html/ processes.json / manifest.json
… extract-site-config.js による分割済みファイル(従来通り)
docs/ … 仕様書(.md)や改善要望プラン等、sitesettings以外の関連ドキュメント
modify/{リクエストラベル}/ … 修正依頼のたびに新規作成。変更前後のconfigファイル・
差分プレビュー・送信結果に加え、summary.md後述
必ずひとまとめに保管する
```
修正を1件でも反映した場合、`modify/{リクエストラベル}/` フォルダに機械可読な生JSON差分プレビュー・送信結果だけでなく、
人間が読めるサマリー `summary_{timestamp}.md` を必ず作成・保存する(ユーザーの明示指示が無くても毎回行う)。
`{timestamp}` は同じ反映で生成される送信結果ファイル(`site-{SiteId}_full_update_result_{timestamp}.json` 等)と
揃える。`summary.md` のような固定名で毎回上書きせず、反映のたびに別ファイルとして残す。記載内容の目安:
- 対象サイト・反映日時・反映方法実行したコマンド・反映結果HTTPステータス・メッセージ
- 依頼内容ごとの変更点(変更前 → 変更後を表や箇条書きで)
- 保留・見送りにした事項(あれば)
`.md`ファイルのため、内容を原始人口調で書くか通常の日本語で書くかは、他のテキストファイル生成と同様に一度だけユーザーに確認する。
パス解決は `site-paths.js` に共通化されている(`findSiteDir` / `ensureSiteDir` / `sitesettingsDir` / `docsDir` /
`newModifyDir` / `latestModifyDir` / `latestJsonPath`)。新しいスクリプトを書くときはここを再利用し、
パス組み立てを重複させない。
## 入力の特定
1. ユーザーがJSONファイルパスやSiteIdを指定した場合はそれを使う。
2. 指定がなければ `./siteSettingJsons/site-*_latest.json` を探す。複数ある場合はユーザーに確認する。
3. `./siteSettingJsons/site-{SiteId}_latest.json` が無ければ、先に `node get-site-config.js` の実行を提案する
2. 指定がなければ `configs/site-*/sitesettings/site-*_latest.json` を探す。複数ある場合はユーザーに確認する。
3. 対象サイトの `sitesettings/site-{SiteId}_latest.json` が無ければ、先に `node get-site-config.js` の実行を提案する
(エンドポイントは `/api/items/{SiteId}/getsite` を使うこと。`/api/items/{SiteId}/get` はアイテム一覧取得APIであり別物なので注意
## 出力ファイル
`./docs/site-{SiteId}_{サイト名}_spec.md`(サイト名は `configs/site-{SiteId}_{サイト名}/` のフォルダ名と揃える。既存があれば上書きしてよい)。
`docs/`フォルダが無ければ作成する。
`configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md`(既存があれば上書きしてよい)。
## 仕様書の構成(この順番・見出しレベルを踏襲する)
```markdown
# サイト仕様書:{Title}SiteId: {SiteId}
- 取得元データ: [site-{SiteId}_latest.json](../siteSettingJsons/site-{SiteId}_latest.json)
- 取得元データ: [site-{SiteId}_latest.json](../sitesettings/site-{SiteId}_latest.json)
- 取得日時: {UpdatedTime}(サイト更新日時)/設定バージョン `Ver: {Ver}`
## 1. サイト基本情報
@ -38,7 +65,10 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
DisableCrossSearch, Creator/Updator, CreatedTime, SiteSettings.Version を表形式で)
## 2. アクセス権限Permissions
Permissions配列を 種別/対象ID/権限レベル の表に分解。
Permissions配列を 種別/対象ID/権限レベル の表に分解。可能であれば `resolve-names.js`
`resolveName` でユーザー名・組織名・グループ名も併記する。
InheritPermissionが自サイト以外を指している場合は、権限の実体はそちらのサイトにある旨を明記し、
継承元サイトのPermissionsも取得できていれば併せて記載する。
「権限値はビットフラグの加算方式のため正確な意味は環境のロール定義に依存する」旨を注記)
## 3. 画面構成
@ -73,8 +103,9 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
「参照先サイトの{From}列の値を、このサイトの{To}列にコピーする」という関係性を文章でも補足)
## 8. プロセス設定Processes
(存在する場合のみ。定義ファイルへのリンク configs/site-{SiteId}_{サイト名}/processes.json を明記した上で、
Id/Name・DisplayName/実行条件(CurrentStatus→ChangedStatus)/動作(OnClick等)/成功メッセージ を表で)
(存在する場合のみ。定義ファイルへのリンク ../processes.json を明記した上で、
Id/Name・DisplayName/実行条件(CurrentStatus→ChangedStatus)/動作(OnClick等)/成功メッセージ を表で。
加えて、generate-process-flowchart.jsで生成したMermaid横向きフローチャートStatus遷移図を表の後に掲載する
## 9. スタイルStyles
存在する場合のみ。Id/Title/適用範囲(New/Edit/Index等のフラグ)/ファイルへのリンクを表で。
@ -115,6 +146,10 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
| Title | タイトル |
| Comments | コメント欄 |
「予定/実行/無し/備考」の4列1組パターン`DateNNN`/`Date(NNN+100)`/`Check(NNN+100)`/`ClassNNN`、NNNは3桁ゼロ埋め連番
繰り返し登場するサイトもある。この場合 `DateNNN.Description` が入力担当(部署名等)、`DateNNN.GridLabelText` が
項目の表示名を表す。詳細は下記「クロスサイト構造変更(新項目追加・連番リネーム)」を参照。
## 注意事項
- SiteSettingsに存在しない項目・空の設定例: `ClassHash`, `NumHash` などのラベル定義ハッシュが `{}`)は
@ -122,128 +157,33 @@ description: Pleasanterのサイト設定JSONget-site-config.js / api-site-ge
- 表形式を基本とし、箇条書きより表を優先する(今回のレビューで「編集画面レイアウトは項目名とラベル名を併記」との
指摘を受けた経緯があるため、内部名とラベル名は必ず併記する)。
- 出力言語は日本語。
- 複数サイトが関連する仕様書(「関連するテーブルと合わせて」等の依頼)を作る場合は、対象サイトの`Links`設定だけでなく、
ServerScripts内で`items.Get(masterTableId)`のように直接SiteIdをハードコードして参照している「正式なLinks設定に現れない
暗黙的なテーブル参照」も洗い出し、別セクションとして明記する。あわせて`ParentId`を辿ってサイトツリー上の位置関係も
簡潔に図示するとレビューしやすい。
## 概要書(業務担当者向け機能説明)の生成
技術仕様書spec.mdとは別に、「概要書を作って」「機能的な説明がほしい」等の依頼があった場合は、
`ColumnName`ClassA, DateB, Class001等の内部コードやJSON設定値・スクリプト実装といった技術的要素を一切排除し、
ラベル名と業務フローのみで構成した概要書を作成する。
### 出力ファイル
`./docs/site-{SiteId}_{サイト名}_overview.md``siteSettingJsons/`・`configs/`ではなく`docs/`フォルダに置く。
これらは秘密情報を含み得るため`.gitignore`済みだが、`docs/`配下のspec.md・概要書・PDFは共有物としてGit管理対象にする想定
`docs/`フォルダが無ければ作成する。
### 概要書の構成
```markdown
# 概要書:{Title}
- 技術仕様(項目のシステム上の名称・設定値等)は [{spec.mdへのリンク}](./site-{SiteId}_{サイト名}_spec.md) を参照。
本書は業務担当者向けに、機能面のみを平易に説明するものです。
## 1. これは何か
業務的にこのサイトが何のためのものかを2〜4文で
## 2. 登場する情報(入力項目)
(分類/項目(ラベル名のみ)/内容 の表。ColumnNameは記載しない。Hide項目でも
業務上意味のあるもの(自動反映される裏項目等)は「内容」欄で触れる)
## 3. 業務の流れ(ボタン操作)
(操作/状態の変化(ラベル値同士、例:「新規作成→図確回覧」)/説明 の表。
Processesの内部処理・スクリプト名には触れず、「何が起きるか」を業務言葉で説明する
## 4. 通知される場面
Notificationsを「◯◯時に誰に何が届くか」という箇条書きに変換。メール本文・件名の技術的書式は転記しない
## 5. 関連する仕組み(マスタ連携)
Links・暗黙的テーブル参照を「◯◯マスタ何を自動反映するか」という業務目線の箇条書きに変換。
SiteIdやColumnNameは書かず、サイト名Titleとその役割のみ記載。
兄弟サイト同じ設計パターンの別サイトがあれば、違いを1文で触れる
```
### 作成時の注意
- 表内の「項目」列は必ずラベル名LabelTextのみ。ラベル未設定の項目は概要書には載せない業務上意味を説明できないため
- Statusの遷移は内部コード100/300/900等を書かず、ラベル値新規作成完了 等)同士の矢印で表現する。
- スクリプト内で見つけた秘密情報APIキー等は概要書には一切転記しないそもそも概要書はスクリプト実装に触れない
- 複数サイト分をまとめて依頼された場合、共通する業務フローは重複して書いてよいが、各サイト固有の違い(段階数の違い、
追加の管理項目など)を各概要書の末尾で明示し、他サイトとの差分が一目で分かるようにする。
## 複数サイトの全体関連図site-relations_overview.md
`config.json``SiteId` に複数サイトが列挙されている場合など、「全体の関連図を作って」「サイト間の連携をまとめて」
等の依頼があれば、個々のサイト仕様書とは別に `./docs/site-relations_overview.md` を作成する。対象は
`config.json``SiteId` に列挙された全サイト(または依頼で指定された範囲)。
### 出力ファイルと構成
`./docs/site-relations_overview.md`(同名`.pdf`も作成対象。以下を1枚で俯瞰できるようにまとめる。
```markdown
# サイト関連図:{システム名など}
## 1. サイトツリーParentId
`ParentId` を辿った親子関係を、簡易的なツリー表記かMermaid `graph` で図示。
ードにSiteIdとサイト名Titleを併記
## 2. サイト一覧
SiteId/サイト名/役割1行の表。概要書へのリンクも併記
## 3. 明示的な連携Links
(各サイトの`Links`設定を 起点サイト/起点列/参照先サイト/参照先の値→コピー先 の表にまとめる。
Idではなくサイト名で表現し、業務的に何を自動反映しているかも一言添える
## 4. 暗黙的な連携ServerScriptsのハードコード参照
ServerScripts内で`items.Get(SiteId)`等により直接SiteIdを指定して他サイトを参照している箇所を洗い出し、
起点サイト/参照先サイト/該当スクリプトファイル/目的(推測) の表にまとめる。
正式なLinks設定には現れないため、レビュー上重要な情報として明記する
```
### 作成時の注意
- 各サイトの `spec.md`7. 他サイト連携で洗い出した内容を集約すればよい。個別spec.mdより先に全サイト分の
spec.mdを作成しておくと効率的。
- サイト名はTitleを使い、SiteIdは補助情報として併記する概要書と違い、こちらは技術仕様書寄りの位置づけのため
SiteId自体は隠さなくてよい
## PDF化
概要書または他のMarkdownをPDF化する依頼があった場合は、プロジェクトルートの `md-to-pdf.js` を使う。
```
node md-to-pdf.js ./docs/site-{SiteId}_{サイト名}_spec.md ./docs/site-{SiteId}_{サイト名}_overview.md
```
- 追加npmパッケージ非依存本プロジェクトに`package.json`が無いことに合わせた設計。Markdown→HTML変換は
スクリプト内の簡易パーサ(見出し/表/太字/リンク/箇条書き/水平線/段落で行い、PDF化はOS標準のMicrosoft Edge
無ければGoogle Chromeのヘッドレス印刷機能を使う。
- 出力は入力と同じフォルダに`{同名}.pdf`中間HTMLは一時フォルダに作成し、変換後に自動削除
- 複数ファイルを一度に渡せる(`node md-to-pdf.js ./docs/*.md`)。
- **既知の落とし穴**: フォルダパスに`#`を含む場合(例: `#GitHub`)、素朴に`file://` + パス文字列を連結すると
`#`以降がURLフラグメントと誤認識され、無関係なページが開かれてPDFの中身が空別物になる。
`md-to-pdf.js`は`url.pathToFileURL()`で正しくエンコードしているため通常は問題にならないが、
同種のブラウザ自動化コードを新たに書く場合は同じ罠に注意する。
## 関連ツールScripts/Styles/ServerScripts/Processesの抽出・編集・反映
仕様書化とは別に、「スクリプトを個別ファイルに分けて」「プロセス設定を直して反映して」等の依頼では、
以下のツールチェーンを使う(詳細は `README.md` 参照)。
1. `node get-site-config.js``siteSettingJsons/site-{SiteId}_latest.json` を取得し、続けて自動的に抽出も行う
`extract-site-config.js` が内部から呼ばれ、`configs/site-{SiteId}_{サイト名}/` に以下を分割保存する。未取得なら先にこれを実行)
1. `node get-site-config.js``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` を取得し、
続けて自動的に抽出も行う(`extract-site-config.js` が内部から呼ばれ、`configs/site-{SiteId}_{サイト名}/` に以下を分割保存する。未取得なら先にこれを実行)
- `scripts/{Id}_{Title}.js`(クライアントスクリプト本体)
- `styles/{Id}_{Title}.css`(スタイル本体)
- `serverscripts/{Id}_{Title}.js`(サーバースクリプト本体)
- `html/{GuideName}.html`GridGuide等のガイドHTML。SiteSettingsではなくサイトデータ直下の項目
- `processes.json``SiteSettings.Processes` の定義ファイル、1ファイルにまとめる。分割しない
- `manifest.json`上記ファイルとId/Titleの対応表。再構成に必須
- `config.json``SiteId` はカンマ区切りで複数指定可能。また、取得したサイトの `InheritPermission`
自サイト以外を指している場合、権限の実体を持つそのサイトも自動的に追いかけて取得する(後述)。
- 抽出だけをやり直したい場合(`latest.json`は既に取得済み)は `node extract-site-config.js` を単独実行してもよい
- **注意**: `get-site-config.js` を再実行すると、その時点のサイトの生の状態で `scripts/` `styles/` 等が
上書きされる。あるサイトの内容を「別サイトへ適用するための下書き」として編集している最中に、
同じサイトへ `get-site-config.js` を再実行すると編集内容が失われるので、クロスサイト作業中は
むやみに元サイトを再取得しない(詳しくは後述の「クロスサイト構造変更」参照)。
- **下書きファイルの整理**: `scripts/` `styles/` `serverscripts/` 配下に `_修正` 等のサフィックス付きで
`manifest.json` 未登録の下書きファイルが残っていることがある(過去に本文を直接編集した際の作業ファイル)。
再取得後は、この下書きファイルと対応する正規ファイル(`manifest.json` の `File` に記載された方)を
diffで比較する。内容が完全一致していれば、下書きの編集内容は既にサイトへ反映済みということなので、
その下書きファイルは削除してよい。内容が異なる場合は未反映の編集が残っている可能性があるため、
削除せずユーザーに確認する。
2. ユーザーの指示に従い、`scripts/*.js` 等の中身を直接編集する(`manifest.json` のId/メタ情報は変更しない)。
新規追加の場合は `manifest.json` に対応エントリ(`Id`は既存と重複しない値、`File`名)を追加してからファイルを作成する。
3. `node build-desired-config.js site-{SiteId}` … 分割ファイルを再構成し `desired_config.json` を生成(送信なし)。
@ -252,15 +192,106 @@ node md-to-pdf.js ./docs/site-{SiteId}_{サイト名}_spec.md ./docs/site-{SiteI
4. `node apply-site-config.js` … ドライラン確認差分・送信Body・curlコマンド表示のみ、送信なし
5. 内容をユーザーに確認してもらい、問題なければ表示されたcurlコマンドをユーザー自身が手動実行する
### 厳守事項(安全設計)
## 関連ツールプロセス設定のMermaidフローチャート化
「フローチャートを作って」「フローチャートを更新」等の依頼では、`generate-process-flowchart.js`を使う。
```
node generate-process-flowchart.js site-335411
```
`processes.json``extract-site-config.js`で抽出済みであること)と、サイトの`Status`列`ChoicesText`
`siteSettingJsons/site-{SiteId}_latest.json`から、横向きMermaidフローチャートStatus遷移図
標準出力へ表示する。ファイル書き込み・API送信は行わない。出力された```mermaid```ブロックは、仕様書
「8. プロセス設定」節へ手動で組み込む。
- ノード = Status値例: `450`)。ラベルは`Status`列`ChoicesText`から解決(`{ラベル}<br/>{値}`形式)
- エッジ = プロセス名。`Depts`指定があれば`(Dept25)`を付記、`Notifications`配列が空でなければ先頭に📧を付与
- `CurrentStatus`/`ChangedStatus`が`-1`(任意状態からの実行・状態変化なしのアクション)の場合、
`View.ColumnFilterHash.Status`があればその対象Statusごとに矢印、なければ仮想ード`((任意))`を使う
- 対象は`processes.json`の構造化データのみ。Scripts/ServerScriptsによるボタン非表示化・独自ボタン経由
呼び出しの検出は含まない(サイトごとに実装が異なり自動化困難なため)。該当する非表示化・独自ボタンの
有無は、仕様書8節の表・文章側で別途補足すること
- **注意**: このスクリプトは`site-paths.js`を使わず、上記「Scripts/Styles/ServerScripts/Processesの
抽出・編集・反映」節の`get-site-config.js`/`extract-site-config.js`/`build-desired-config.js`と同じ
実装パターン(`configs/site-*/`と`siteSettingJsons/`を直接参照)に合わせている
## 関連ツール:ユーザー/組織/グループのマスターデータ取得
権限Permissionsやスクリプト内の担当者振り分けなどをID表記ではなく名前で扱いたい場合に使う。
1. `node get-master-data.js``configs/master/` へ以下を取得・保存する(ページング自動対応。
`/api/users/get` はレスポンスの `TotalCount` が1回あたりの件数を超えることがあるため、
トップレベルの `Offset` パラメータで全件を取得しきるまでループする)。
- `master_users.json``UserId` / `LoginId` / `Name` / `DeptId` 等)
- `master_depts.json``DeptId` / `DeptCode` / `DeptName`
- `master_groups.json``GroupId` / `GroupName` / `GroupMembers[]` / `GroupChildren[]`
- 参照した公式マニュアル: [ユーザ取得(全て)](https://pleasanter.org/ja/manual/api-user-get-all) /
[組織取得](https://pleasanter.org/ja/manual/api-dept-get) / [グループ取得](https://pleasanter.org/ja/manual/api-group-get)
2. `resolve-names.js` をスクリプトから `require` して使う:
- `loadMasterData(baseDir)` … 3つの `master_*.json` を読み込む
- `resolveName(master, { Type: "User"|"Dept"|"Group", Id })` … IDから名前を引く
- `resolveId(master, { Type, Name })` … 名前から一致するIDの配列を引く完全一致。0件/複数件はエラーとして扱う)
## 関連ツール:クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用)
「Aサイトを土台に項目を追加してBサイトへ適用して」「この項目の後ろに新しい項目を追加して、以降の連番を
ズラして」のような依頼で使うパターン。2026-07-16に SiteId 480111 → 496626 への適用(「解体紹介料申請」項目の
追加・連番リネーム・GridDesign一括改修・改善要望3件の反映で実際に使った構成を一般化したもの。
`restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` は、その際の実例
(そのまま次回動かせるテンプレートとして残っている)。**次回別の依頼が来たら、これらをコピーして
対象サイトIDやフェーズの中身を書き換えて使う**(汎用パラメータ化はしていない。要望ごとに列の増減や
チェックリストの中身が違うため、都度スクリプトとして書き下ろす方が確実)。
### 全体の流れ
1. **マスターデータ+サイト情報の取得**: `node get-master-data.js``node get-site-config.js`
`config.json` の `SiteId` を一時的に `"元サイトId,対象サイトId"` にして実行し、終わったら単一IDへ戻す
`InheritPermission` が自サイト以外を指していれば、その継承元サイトも自動的に追いかけて取得される。
2. **抽出済みScripts/Stylesの編集**: 元サイトの `configs/site-{元SiteId}_.../scripts/*.js` 等を直接編集する。
⚠️ ここは元サイト自身の抽出フォルダなので、元サイトを `get-site-config.js` で再取得すると
編集内容が消える。編集後は速やかに restructure スクリプト(次項)を走らせて `modify/` フォルダへ
本文を取り込んでおくこと。
3. **`restructure-site-{元SiteId}.js`(新規作成)**: 元サイトの `sitesettings/latest.json` を読み込み、
構造変更列の挿入・連番リネーム・GridDesign書き換え等を行い、`manifest.json` 経由で
編集済みのScripts/Styles本文を組み込んだ上で、対象サイトの `modify/{リクエストラベル}/` フォルダへ
`site-{元SiteId}_updated.json` として書き出す(`site-paths.js` の `findSiteDir` / `newModifyDir` を使う)。
同じフォルダへ対象サイトの変更前スナップショット(`before_site-{対象SiteId}_latest.json`)も
コピーしておく。書き込み前に、想定した件数増分・重複の有無などの整合性チェックを必ず入れる
今回の実装では「フェーズA:事前ガード」「フェーズH:整合性チェック」として実施)。
4. **`apply-to-site-{対象SiteId}.js`(新規作成)**: `modify/` フォルダの `site-{元SiteId}_updated.json`
`updatesite``Mode:"full"`)で対象サイトへ送る。`Title`/`ReferenceType`/`ParentId`/`InheritPermission`は
**対象サイト自身の現状値**から取得し、`SiteSettings`のみ元サイト由来の内容で置き換える。
`Permissions`は送信ボディに含めない(対象サイトが権限を継承している場合、直接の書き込み対象ではないため。
継承有無は `InheritPermission` で判定する)。デフォルトは差分プレビュー+アーティファクト保存のみ、
`--execute` 指定時のみ実際に送信する。差分プレビュー・送信結果は同じ `modify/` フォルダへ保存する。
5. **権限を名前指定で追加する場合**: `apply-permission-grant.js` のパターンで、対象サイトの
`InheritPermission` が指す継承元サイトに対して行う(対象サイト自身の `Permissions` は空配列=継承の
ままにしておき、直接は変更しない)。`Permissions` はサイトデータ直下の項目で `"Type,Id,PermissionType"`
形式の文字列配列(例: `"User,579,15"`)。`updatesitesettings`部分更新APIはPermissionsを更新対象に
含まないことを公式マニュアルで確認済みのため、`updatesite`(全体更新)を使い、継承元サイトの
`SiteSettings` 等は一切変更せずそのまま再送信しつつ `Permissions` のみ新規エントリを追加する。
⚠️ 公式マニュアルには `updatesite` のリクエストボディで `Permissions` を指定できるという明記が
無いため、実行前に必ずプレビュー出力をユーザーに確認してもらうこと。
6. **内容確認 → 実行**: 生成された `modify/` フォルダの中身(`before_*.json` / `site-*_updated.json` /
`*_diff_*.txt`)をユーザーに提示し、レビューを受けてから `--execute` を実行する。
### 厳守事項(安全設計、クロスサイト構造変更にも適用)
- **`apply-site-config.js` は常にドライラン。Claude自身がAPIへ実送信することはしない**
`ENABLE_ACTUAL_SEND` を有効化する改修や、curlコマンドの代理実行も行わない
反映の最終実行はユーザー自身に委ねる。
`ENABLE_ACTUAL_SEND` を有効化する改修や、curlコマンドの代理実行も行わない。これは変更しない。
- 一方で `apply-to-site-{対象SiteId}.js` / `apply-permission-grant.js` のような**専用の直接実行スクリプト**は、
ユーザーがその場で明示的に「今回は直接実行してよい」と許可した場合に限り作成・実行してよい
2026-07-16の実例。これは **その依頼一回限りの例外** であり、標準運用(ドライラン+手動反映)を
恒久的に変更するものではない。次回以降も、実送信を伴うスクリプトを作る/`--execute` を走らせる前には、
改めてユーザーに差分プレビューを提示し、明示的な許可を得ること。
- `build-desired-config.js` が作る `desired_config.json``Mode:"partial"``updatesitesettings`)専用。
Scripts/Styles/ServerScripts/Processes以外のサイト設定には触れない。
Scripts/Styles/ServerScripts/Processes以外のサイト設定には触れない。Columns/EditorColumnHash/Sections/
GridColumns/Permissions等の構造変更は、この節で説明した `restructure-site-*.js` パターンで行う。
- ガイドHTML`html/*.html`)は `updatesitesettings` では反映できない(サイト直下の項目のため)。
反映が必要なら `Mode:"full"``updatesite`用の別JSONが要ることをユーザーに伝える。自動生成はしない。
- 抽出したスクリプト・サーバースクリプトの中に **APIキーやWebhook URL等の秘密情報がハードコードされている場合がある**
(実例あり)。抽出・編集作業でこれらの値を見つけたら、チャット上にそのまま貼り付けず、ユーザーに直接ファイルを確認してもらう。
また `siteSettingJsons/` フォルダ・`configs/` フォルダ・`config.json` は `.gitignore` 済みだが、誤ってコミットしないよう注意喚起する。
また `configs/` フォルダと `config.json``.gitignore` 済みだが、誤ってコミットしないよう注意喚起する。
- 権限変更・別サイトへの丸ごと上書きはブラスト半径が大きい。実行前に必ず差分プレビューを提示し、
対象サイトIDや継承関係`InheritPermission`)の理解が誤っていないか、実行のたびに確認する。

View File

@ -0,0 +1,6 @@
<!-- mermaid-ai-skills:start -->
## Mermaid Diagrams
When the user asks to create, edit, or visualize a diagram, follow the
instructions in `.github/instructions/mermaid.instructions.md`.
<!-- mermaid-ai-skills:end -->

View File

@ -0,0 +1,85 @@
---
applyTo: "**"
---
# Mermaid AI Skills
When the user asks to create, edit, or visualize any diagram, use the Mermaid
VS Code extension tools and commands described below.
## Workflow
1. Determine the diagram type and generate Mermaid syntax.
2. Write the diagram to a `.mmd` file in the project.
3. Validate syntax: correct first-line keyword, arrow types, balanced brackets.
4. Preview via the Mermaid extension — open the `.mmd` file (auto-preview) or run
**MermaidChart: Preview Diagram** (`mermaidChart.preview`).
## LM Tools — call these for every diagram interaction
- `mermaid-diagram-validator` — validate Mermaid syntax before presenting any diagram
- `mermaid-diagram-preview` — render a live preview inside VS Code after generating
- `get-syntax-docs-mermaid` — fetch correct syntax docs for any diagram type
## VS Code Commands
Invoke via Command Palette or the VS Code command API (GitHub Copilot in VS Code only).
Do not invent command IDs. Prefer writing/editing `.mmd` files when a command is not needed.
### Diagram editing & preview
- **Preview** (`mermaidChart.preview`) — preview the active Mermaid editor (`.mmd` / `.mermaid` must be open).
- **Create Diagram** (`mermaidChart.createMermaidFile`) — creates a demo flowchart and opens preview side by side.
- **Repair Diagram** (`mermaidChart.repairDiagram`) — Mermaid AI repair for the active diagram; uses Mermaid AI credits — tell the user before running.
- **Improve Diagram** (`mermaidChart.improveDiagram`) — uses Copilot / LM API; suggests layout + styling variants for the active diagram.
### Generate diagrams (GitHub Copilot required)
- **Generate Diagram from Code** (`mermaidChart.generateDiagramFromCode`)
- **Generate Cloud Diagram** (`mermaidChart.generateCloudDiagram`)
- **Generate ER Diagram** (`mermaidChart.generateERDiagram`)
- **Generate Docker Diagram** (`mermaidChart.generateDockerDiagram`)
- **Open AI Chat** (`mermaidChart.openCopilotChat`)
### Mermaid Chart cloud
- **Login** (`mermaidChart.login`) / **Logout** (`mermaidChart.logout`)
- **Connect Diagram** (`mermaidChart.connectDiagramToMermaidChart`) — link a local diagram to Mermaid Chart.
- **Sync Diagram** (`mermaidChart.syncDiagramWithMermaid`) — only for diagrams already connected (frontmatter has `id:`). Example:
```yaml
---
id: cbd9e9ba-a2cb-47c5-a98e-8c28a753428d
---
```
### Review Mermaid Sync
For diagrams updated by the Mermaid Chart GitHub Sync app (or pre-commit regenerate):
- **Review Mermaid Sync** (`mermaidChart.reviewAppCommits`) — start / open the review flow.
- **Regenerate with Mermaid AI** (`mermaidChart.regenerateDiagramWithMermaidAI`) — regenerate from source references.
Do not manually rewrite diagrams managed by this workflow. Accept/reject/diff UI actions stay in the extension UI.
### Install / update this pack
- **MermaidChart: Install AI Skills…** (`mermaidChart.installAiSkills`)
## @mermaid-chart slash commands
| Command | Purpose |
|---|---|
| `/generate_diagram_from_code` | General diagram from any source file |
| `/generate_execution_sequence` | Sequence diagram from code flow |
| `/generate_er_diagram` | ER diagram from schema / models |
| `/generate_cloud_architecture_diagram` | Cloud / CI-CD architecture |
| `/generate_docker_diagram` | Architecture from Dockerfiles |
| `/generate_c4_topdown_architecture` | C4 top-down architecture |
| `/analyze_code_ownership` | Code ownership diagram |
| `/generate_dependency_diagram` | Dependency / security visualisation |
## Rules
1. Always call `mermaid-diagram-validator` before showing any diagram.
2. Always call `mermaid-diagram-preview` after generating a diagram.
3. Use `get-syntax-docs-mermaid` before generating an unfamiliar diagram type.
4. Prefer `@mermaid-chart` slash commands for complex generation.
5. Write diagrams to `.mmd` files; never return unvalidated Mermaid syntax.
6. Warn the user before Repair (Mermaid AI credits).
7. Cooperate with the Sync workflow — do not manually regenerate managed diagrams.
## Docs
More commands and features: https://marketplace.visualstudio.com/items?itemName=MermaidChart.vscode-mermaid-chart

View File

@ -4,16 +4,16 @@
```
① node get-site-config.js
→ siteSettingJsons/site-{SiteId}_latest.json に構成生JSONを保存
configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json に構成を保存
→ 続けて自動的に extract-site-config.js が実行され、
Scripts / Styles / ServerScripts / Processes / ガイドHTML が
configs/site-{SiteId}_{サイト名}/ 配下に個別ファイルとして分割保存される
→ config.json の SiteId をカンマ区切りにすると、複数サイトをまとめて①の処理でループ取得する
→ 取得したサイトの InheritPermission が自サイト以外を指していれば、権限の実体を持つ
そのサイトも自動的に追いかけて取得する
② Claudeに「仕様書を作って」と依頼
→ siteSettingJsons/site-{SiteId}_latest.json と configs/site-{SiteId}_{サイト名}/ を
Claudeが読み解き、docs/site-{SiteId}_{サイト名}_spec.md として仕様書化
機械的なテンプレート処理ではなく、Claudeが構成内容をレビューしながら執筆する
② その latest.json をClaudeのチャットにアップロード
→ Claudeが構成をレビュー・仕様書化configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md
③ configs/site-{SiteId}_{サイト名}/ 配下のファイルを直接編集、
またはClaudeに「このスクリプトを◯◯に直して」等と修正を指示
@ -25,28 +25,14 @@
→ 実際には送信せず、差分・送信予定Body・curlコマンドのみ表示常にドライラン
⑥ 内容を確認し、問題なければ表示されたcurlコマンドを手動実行
--- (任意:非エンジニア向けドキュメントが必要な場合) ---
⑦ Claudeに「概要書を作って」と依頼
→ 技術仕様書とは別に、ColumnName等の内部コードを含まない業務向け概要書を
docs/site-{SiteId}_{サイト名}_overview.md として生成siteSettingJsons/・configs/とは異なりGit管理対象
⑧ 複数サイトが連携する場合、Claudeに「全体の関連図を作って」と依頼
→ docs/site-relations_overview.md として、サイトツリー・明示的連携Links
暗黙的連携ServerScriptsのハードコード参照を1枚にまとめたドキュメントを生成
⑨ node md-to-pdf.js ./docs/*.md
→ docs/ 配下の全Markdownspec.md / overview.md / site-relations_overview.md
同名の .pdf に一括変換
```
`extract-site-config.js``node get-site-config.js` の中から自動実行されるため、通常は個別に実行する必要はありません
(取得済みの `latest.json` から抽出をやり直したい場合のみ、単独で `node extract-site-config.js` を実行してください)。
仕様書・概要書②⑦⑧はClaudeが対話的に読み解いて執筆するものです。過去に `generate-site-documentation.js` という
機械的なテンプレート処理での一括自動生成を試みましたが、サイト間の暗黙的な関連や業務的な類推までは反映できず
品質面の問題があったため、**現在のワークフローでは使用していません**(ファイル自体は後方互換のため削除せず残置)
上記はScripts/Styles/ServerScripts/Processesの反映フロー単一サイト内で完結です。項目の追加や連番リネーム、
別サイトへの丸ごと適用、権限の名前指定追加など、より大きな構造変更は
`.claude/skills/pleasanter-site-spec/SKILL.md` の「クロスサイト構造変更」の節を参照してください。
## 初回セットアップ
@ -64,24 +50,28 @@
|---|---|
| `config.json` | 接続情報BaseUrl/SiteId/ApiKey※Git管理・共有厳禁 |
| `config.example.json` | 上記のテンプレート |
| `get-site-config.js` | サイト構成取得(`getsite``siteSettingJsons/site-{SiteId}_latest.json` に保存し、続けて `extract-site-config.js` を自動実行。`SiteId` をカンマ区切りにすると全サイトを1回の実行で順次取得 |
| `site-paths.js` | `configs/` 配下のフォルダ構成(後述)を解決する共通ヘルパー。他のスクリプトから`require`して使う |
| `get-site-config.js` | サイト構成取得(`getsite``configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` に保存し、続けて `extract-site-config.js` を自動実行。`SiteId` をカンマ区切りにすると全サイトを1回の実行で順次取得。`InheritPermission` が自サイト以外を指していれば継承元サイトも自動追跡 |
| `extract-site-config.js` | `latest.json` から Scripts/Styles/ServerScripts/Processes/ガイドHTML を `configs/site-{SiteId}_{サイト名}/` に個別ファイル分割(`get-site-config.js` から自動呼び出し、または単独実行も可) |
| `build-desired-config.js` | 分割ファイルを再構成し `desired_config.json` を生成(**送信は行わない** |
| `apply-site-config.js` | 希望構成JSONとの差分表示・送信内容確認**送信は行わない** |
| `generate-site-documentation.js` | **未使用(レガシー)**。仕様書・概要書を機械的なテンプレート処理で一括自動生成する旧スクリプト。品質面の問題から現在のワークフローでは使用しない。後方互換のため削除はしていない |
| `md-to-pdf.js` | Markdownファイルを同名の`.pdf`に変換追加npmパッケージ非依存、OS標準のEdge/Chromeヘッドレス印刷を利用 |
| `apply-desired-config-full.js` | `desired_config.json`のScripts/Styles/ServerScripts/Processesを、現状サイト全体getsiteに上書きマージし`updatesite`(全体更新)で反映する専用実行スクリプト。`updatesitesettings`部分更新がこの環境で反映されない問題下記「注意事項」参照を踏まえた標準の反映手段。Columns/GridColumns/Permissions等は現状のまま変更しない。デフォルトは差分プレビューのみ、`--execute`指定時のみ実際に送信する |
| `get-master-data.js` | ユーザー/組織/グループのマスターデータを取得し `configs/master/` へ保存(ページング自動対応) |
| `resolve-names.js` | `configs/master/` のマスターデータを使い、ID⇔名前を相互変換するユーティリティ`require`して使う) |
| `restructure-site-480111.js` / `apply-to-site-496626.js` / `apply-permission-grant.js` | クロスサイト構造変更(項目追加・連番リネーム・別サイトへの丸ごと適用・権限の名前指定追加)の実例。次回同様の依頼が来たらコピーして書き換える。詳細は `.claude/skills/pleasanter-site-spec/SKILL.md` 参照 |
| `generate-process-flowchart.js` | `processes.json`とStatus列`ChoicesText`から、プロセス設定のStatus遷移を横向きMermaidフローチャートとして標準出力へ表示ファイル書き込み・API送信なし。`node generate-process-flowchart.js site-{SiteId}`。出力は仕様書「8. プロセス設定」節へ手動で組み込む |
| `desired_config.json` | Claude、または `build-desired-config.js` が生成する「希望構成」 |
| `siteSettingJsons/site-{SiteId}_latest.json` | 直近取得した現状構成の生JSONClaudeへのアップロード対象。`.gitignore`対象) |
| `configs/master/master_{users,depts,groups}.json` | ユーザー/組織/グループのマスターデータ |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_latest.json` | 直近取得した現状構成Claudeへのアップロード対象 |
| `configs/site-{SiteId}_{サイト名}/sitesettings/site-{SiteId}_{timestamp}.json` | 取得時点ごとの履歴(タイムスタンプ付き) |
| `configs/site-{SiteId}_{サイト名}/docs/site-{SiteId}_spec.md` | SiteSettingsを読み解いたMarkdown仕様書。改善要望プラン等その他ドキュメントもここに置く |
| `configs/site-{SiteId}_{サイト名}/manifest.json` | 分割ファイルの一覧Id/Title/ファイル名の対応表) |
| `configs/site-{SiteId}_{サイト名}/scripts/*.js` | クライアントスクリプト本体1ファイル=1スクリプト |
| `configs/site-{SiteId}_{サイト名}/styles/*.css` | スタイル本体 |
| `configs/site-{SiteId}_{サイト名}/serverscripts/*.js` | サーバースクリプト本体 |
| `configs/site-{SiteId}_{サイト名}/html/*.html` | ガイドHTMLGridGuide等。値が空でないもののみ |
| `configs/site-{SiteId}_{サイト名}/processes.json` | プロセス設定(`SiteSettings.Processes` の定義ファイル) |
| `docs/site-{SiteId}_{サイト名}_spec.md` | SiteSettingsを読み解いたMarkdown技術仕様書Claudeが執筆。Git管理対象 |
| `docs/site-{SiteId}_{サイト名}_overview.md` | 業務担当者向けの概要書ColumnName等の内部コードを含まない機能説明。Git管理対象 |
| `docs/site-relations_overview.md` | 複数サイトの全体関連図(サイトツリー・明示的/暗黙的連携をまとめたもの。Git管理対象 |
| `docs/*.pdf` | 上記各Markdownに対応するPDF版 |
| `configs/site-{SiteId}_{サイト名}/modify/{リクエストラベル}/` | 修正依頼ごとに新規作成。変更前後のconfig・差分プレビュー・送信結果をまとめて保管 |
## Scripts/Styles/ServerScripts/Processes の反映について
@ -101,15 +91,26 @@
https://pleasanter.org/manual/api-site-update
- サイト設定部分更新: `POST {BaseUrl}/api/items/{SiteId}/updatesitesettings`
https://pleasanter.org/ja/manual/api-update-sitesettings
- ユーザ取得(全て): `POST {BaseUrl}/api/users/get`
https://pleasanter.org/ja/manual/api-user-get-all
(※ ページングはトップレベルの `Offset` パラメータで行う。`View.Offset` ではない点に注意)
- 組織取得: `POST {BaseUrl}/api/depts/get`
https://pleasanter.org/ja/manual/api-dept-get
- グループ取得: `POST {BaseUrl}/api/groups/get`
https://pleasanter.org/ja/manual/api-group-get
## 安全設計について
- `apply-site-config.js`**常にドライラン** です。デフォルトでは実際のAPI呼び出しコード自体に到達しないようガード`ENABLE_ACTUAL_SEND = false` 固定)しています。
- 反映する際は、表示されたcurlコマンドをご自身で実行するか、内容を確認の上でスクリプトを改修してくださいClaudeが安全策なしに自動実行することはありません
- APIキーは `config.json` にのみ保持し、Claudeにアップロードする `latest.json` には含まれませんget時のレスポンスにAPIキーは含まれないため。念のためアップロード前に目視確認することをおすすめします。
- `siteSettingJsons/``configs/` はいずれもScripts/ServerScripts本文に秘密情報がハードコードされている場合があるため `.gitignore` 対象です。`docs/` 配下の仕様書・概要書・PDFのみがGit管理対象です。
- `apply-to-site-*.js` / `apply-permission-grant.js` のような、ユーザーがその場で明示的に許可した場合に限って
作成される「専用の直接実行スクリプト」は例外です(`--execute` 指定時のみ実送信)。これは依頼ごとの一回限りの
許可であり、標準運用(ドライラン+手動反映)を恒久的に変更するものではありません。
## 注意事項
- プリザンターのバージョンにより `get` レスポンスの構造(`Response.Site.SiteSettings` の位置など)が異なる場合があります。初回実行時にコンソールへ出力されるキー構造を確認してください。
- サイト設定の更新系APIは **テナント管理者権限のAPIキー** が必要です一般ユーザーのAPIキーでは403等になります
- ★重要: この環境では `updatesitesettings`(部分更新)が **HTTP 200・成功メッセージを返すにもかかわらず、実際にはScripts/Styles/ServerScriptsの内容がDBに反映されないことを確認済み**です2026-07-16、SiteId 496626で発生。直後に`getsite`し直しても内容・Ver・UpdatedTimeが変化しなかった
Scripts/Styles/ServerScriptsを実際に反映する専用スクリプトを書く場合は、`updatesitesettings`ではなく`updatesite`全体更新。Title/ReferenceType/ParentId/InheritPermission/Permissionsは現状のまま再送信し、SiteSettingsは現状を取得した上でScripts/Styles/ServerScriptsだけ差し替えるを使うこと。反映後は必ず`getsite`し直し、`UpdatedTime`が更新されているか・目的の文字列が含まれているかを確認する。

View File

@ -0,0 +1,89 @@
/**
* apply-335411-full.js
* ------------------------------------------------------------
* 一回限りの直接実行スクリプト2026-07-30SiteId 335411 見積回答ファイル転記一括DL追加
* ユーザーが反映させてくださいと明示許可したため作成
*
* updatesitesettings部分更新はこのPleasanterデプロイでは200を返しつつ
* 実際には永続化されないケースが確認されているためproject-pleasanter-updatesitesettings-noop
* updatesiteMode:"full"で送信する
*
* デフォルトはプレビューのみ送信なし--execute を付けたときだけ実送信する
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const baseDir = __dirname;
const config = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
const desired = JSON.parse(fs.readFileSync(path.join(baseDir, "desired_config.json"), "utf-8"));
const siteId = 335411;
const currentPath = path.join(baseDir, "siteSettingJsons", `site-${siteId}_latest.json`);
const current = JSON.parse(fs.readFileSync(currentPath, "utf-8")).Response.Data;
// SiteSettingsは現状を丸ごとベースにし、Scriptsのみdesired_config.json編集済みで置き換える
const siteSettings = JSON.parse(JSON.stringify(current.SiteSettings));
siteSettings.Scripts = desired.SiteSettings.Scripts;
const body = {
ApiVersion: config.ApiVersion || "1.1",
ApiKey: config.ApiKey,
SiteId: siteId,
Title: current.Title,
ReferenceType: current.ReferenceType,
ParentId: current.ParentId,
InheritPermission: current.InheritPermission,
Permissions: current.Permissions || [],
SiteSettings: siteSettings,
};
const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/updatesite`;
function maskApiKey(key) {
return key && key.length > 8 ? key.slice(0, 4) + "****" + key.slice(-4) : "****";
}
const execute = process.argv.includes("--execute");
console.log("========================================");
console.log(` Mode: full (updatesite) execute=${execute}`);
console.log("========================================");
console.log(`URL : ${url}`);
console.log(`SiteId: ${siteId}`);
console.log("Scripts Id1/Id3 本文差分ありtranscribeEstimateAttachments/estimateAttachmentsDLButton追加。Styles/ServerScripts/Processesは現状のまま。");
console.log("");
const maskedBody = { ...body, ApiKey: maskApiKey(body.ApiKey) };
const outDir = path.join(
baseDir,
"configs",
"site-335411_②問い合わせ内容管理 Ver0.3",
"modify",
"2026-07-30T1000_見積回答ファイル転記_一括DL追加"
);
fs.writeFileSync(path.join(outDir, "send_body_full_masked.json"), JSON.stringify(maskedBody, null, 2), "utf-8");
console.log(`送信予定Bodyマスク済みを保存しました: ${path.join(outDir, "send_body_full_masked.json")}`);
if (!execute) {
console.log("\n[プレビューのみ] 実際の送信は行っていません。--execute を付けて再実行すると送信します。");
process.exit(0);
}
(async () => {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await res.text();
console.log(`HTTP ${res.status}`);
console.log(text);
fs.writeFileSync(
path.join(outDir, `site-335411_full_update_result_${new Date().toISOString().replace(/[:.]/g, "-")}.json`),
JSON.stringify({ status: res.status, body: text }, null, 2),
"utf-8"
);
})();

View File

@ -0,0 +1,80 @@
// site-335411向け Attachments061 のCSS適用箇所修復送信はしない・プレビュー専用
// ユーザーが他8項目Date061/Class061/Class062/Date062/Class063/Date063/Class065/Description061
// 管理画面から FieldCss ではなく ExtendedFieldCss に "code-readonly2" を設定する形へ手動で直したが、
// Attachments061だけは管理画面のUIから直せないため、同じパターンをコマンドで適用する。
// - FieldCss: "code-readonly2" を削除(元は未設定だったため)
// - ExtendedFieldCss: "code-readonly2" を新規追加
// Scripts/Styles/ServerScripts/Processesは現状のconfigsフォルダの内容をそのまま使う変更なし
// Attachments061以外のColumnsは一切変更しない。
const fs = require("fs");
const path = require("path");
function main() {
const baseDir = __dirname;
const modifyRel = process.argv[2];
if (!modifyRel) {
console.error("[エラー] 出力先の modify フォルダを指定してください");
process.exit(1);
}
const siteId = 335411;
const siteDir = path.join(baseDir, "configs", "site-335411_②問い合わせ内容管理 Ver0.3");
const outDir = path.isAbsolute(modifyRel) ? modifyRel : path.join(siteDir, modifyRel);
const latest = JSON.parse(
fs.readFileSync(path.join(baseDir, "siteSettingJsons", `site-${siteId}_latest.json`), "utf8")
);
const d = latest.Response.Data;
const manifest = JSON.parse(fs.readFileSync(path.join(siteDir, "manifest.json"), "utf8"));
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, `before_site-${siteId}_latest.json`), JSON.stringify(latest, null, 2), "utf-8");
const siteSettings = JSON.parse(JSON.stringify(d.SiteSettings));
function rebuildList(manifestEntries, dir) {
return (manifestEntries || []).map((entry) => {
const { File, ...meta } = entry;
const body = fs.readFileSync(path.join(siteDir, dir, File), "utf-8");
return { ...meta, Body: body };
});
}
siteSettings.Scripts = rebuildList(manifest.Scripts, "scripts");
siteSettings.Styles = rebuildList(manifest.Styles, "styles");
siteSettings.ServerScripts = rebuildList(manifest.ServerScripts, "serverscripts");
const processesPath = path.join(siteDir, manifest.ProcessesFile || "processes.json");
siteSettings.Processes = fs.existsSync(processesPath) ? JSON.parse(fs.readFileSync(processesPath, "utf8")) : [];
let changeLog = null;
siteSettings.Columns = siteSettings.Columns.map((col) => {
if (col.ColumnName !== "Attachments061") return col;
const before = { FieldCss: col.FieldCss, ExtendedFieldCss: col.ExtendedFieldCss };
const { FieldCss, ...rest } = col;
const after = { ...rest, ExtendedFieldCss: "code-readonly2" };
changeLog = { before, after: { FieldCss: after.FieldCss, ExtendedFieldCss: after.ExtendedFieldCss } };
return after;
});
const body = {
ApiVersion: "1.1",
ApiKey: "***(送信時にconfig.jsonから読み込み。プレビューJSONには含めません)***",
TenantId: d.TenantId,
SiteId: d.SiteId,
Title: d.Title,
ReferenceType: d.ReferenceType,
ParentId: d.ParentId,
InheritPermission: d.InheritPermission,
SiteSettings: siteSettings,
};
fs.writeFileSync(path.join(outDir, "desired_body_full.json"), JSON.stringify(body, null, 2), "utf-8");
console.log("[OK] 生成しました:", path.join(outDir, "desired_body_full.json"));
console.log("\n=== Attachments061 変更内容 ===");
console.log("変更前:", JSON.stringify(changeLog.before));
console.log("変更後:", JSON.stringify(changeLog.after));
console.log("\n次のステップ: 内容確認後、send-335411-full.js相当のスクリプトで送信してください。");
}
main();

View File

@ -0,0 +1,91 @@
// site-335411向け全体反映(updatesite)ボディ生成スクリプト(送信はしない・プレビュー専用)。
// siteSettingJsons/site-335411_latest.json のSiteSettingsを土台に、
// - Scripts/Styles/ServerScripts/Processes は configs/site-335411_.../ 配下の現在のファイル一式で丸ごと置き換え
// - Columns は Section-6/Section-7配下の指定列のみ FieldCss に "code-readonly2" を追加(既存クラスは保持、重複追加はしない)
// を反映した送信予定ボディを modify/{ラベル}/desired_body_full.json として書き出す。
// 実行方法: node build-full-335411.js "modify/2026-07-20T2000_Status同期_readonly2追加"
const fs = require("fs");
const path = require("path");
const READONLY2_TARGET_COLUMNS = [
"Date061", "Class061", "Class062", "Date062", "Class063",
"Date063", "Class065", "Description061", "Attachments061",
];
function main() {
const baseDir = __dirname;
const modifyRel = process.argv[2];
if (!modifyRel) {
console.error("[エラー] 出力先の modify フォルダを指定してください(例: node build-full-335411.js \"modify/2026-07-20T2000_xxx\"");
process.exit(1);
}
const siteId = 335411;
const siteDir = path.join(baseDir, "configs", "site-335411_②問い合わせ内容管理 Ver0.3");
const outDir = path.isAbsolute(modifyRel) ? modifyRel : path.join(siteDir, modifyRel);
const latest = JSON.parse(
fs.readFileSync(path.join(baseDir, "siteSettingJsons", `site-${siteId}_latest.json`), "utf8")
);
const d = latest.Response.Data;
const manifest = JSON.parse(fs.readFileSync(path.join(siteDir, "manifest.json"), "utf8"));
// 変更前スナップショットを保存
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, `before_site-${siteId}_latest.json`), JSON.stringify(latest, null, 2), "utf-8");
const siteSettings = JSON.parse(JSON.stringify(d.SiteSettings));
function rebuildList(manifestEntries, dir) {
return (manifestEntries || []).map((entry) => {
const { File, ...meta } = entry;
const body = fs.readFileSync(path.join(siteDir, dir, File), "utf-8");
return { ...meta, Body: body };
});
}
siteSettings.Scripts = rebuildList(manifest.Scripts, "scripts");
siteSettings.Styles = rebuildList(manifest.Styles, "styles");
siteSettings.ServerScripts = rebuildList(manifest.ServerScripts, "serverscripts");
const processesPath = path.join(siteDir, manifest.ProcessesFile || "processes.json");
siteSettings.Processes = fs.existsSync(processesPath) ? JSON.parse(fs.readFileSync(processesPath, "utf8")) : [];
// Columns: 対象列のFieldCssへ code-readonly2 を追加(既存があれば変更なし)
const fieldCssChanges = [];
siteSettings.Columns = siteSettings.Columns.map((col) => {
if (!READONLY2_TARGET_COLUMNS.includes(col.ColumnName)) return col;
const before = col.FieldCss || "";
const classes = before.split(/\s+/).filter(Boolean);
if (classes.includes("code-readonly2")) {
fieldCssChanges.push({ ColumnName: col.ColumnName, before, after: before, changed: false });
return col;
}
classes.push("code-readonly2");
const after = classes.join(" ");
fieldCssChanges.push({ ColumnName: col.ColumnName, before, after, changed: true });
return { ...col, FieldCss: after };
});
const body = {
ApiVersion: "1.1",
ApiKey: "***(送信時にconfig.jsonから読み込み。プレビューJSONには含めません)***",
TenantId: d.TenantId,
SiteId: d.SiteId,
Title: d.Title,
ReferenceType: d.ReferenceType,
ParentId: d.ParentId,
InheritPermission: d.InheritPermission,
SiteSettings: siteSettings,
};
fs.writeFileSync(path.join(outDir, "desired_body_full.json"), JSON.stringify(body, null, 2), "utf-8");
console.log("[OK] 生成しました:", path.join(outDir, "desired_body_full.json"));
console.log("\n=== FieldCss変更内容Section-6/Section-7配下 ===");
for (const c of fieldCssChanges) {
console.log(` ${c.ColumnName}: "${c.before}" → "${c.after}" ${c.changed ? "(変更)" : "(既存のため変更なし)"}`);
}
console.log("\n次のステップ: 内容を確認の上、実行が許可されたら send-335411-full.js で送信してくださいconfig.jsonのApiKeyを使って別途送信します。");
}
main();

View File

@ -9,7 +9,7 @@
このサイトは「①物件基本情報 Ver0.3」を管理するための業務画面です。Resultsとして扱われ、登録・照会・更新の基本的な業務フローに沿って利用されます。
- 画面上の案内では、新規物件は右上「+新規作成」ボタンから登録してください。 見積依頼は「新規見積依頼」ボタンから依頼を進めてください。 ■LINEWORKS[営業積算相談窓口](https://line.worksmobile.com/message/bot/view?version=10&botNo=11910622) ※窓口の対応内容や対応時間の確認は[こちら](https://board.worksmobile.com/main/article/408000000078578285...
- 画面上の案内では、新規物件は、一覧画面の右上「+新規作成」ボタンから登録してください。 見積依頼は「新規見積依頼」ボタンから依頼を進めてください。 ■LINEWORKS[営業積算相談窓口](https://line.worksmobile.com/message/send?userNoList=800300000001353&version=25&type=counselor) ※窓口の対応内容や対応時間の確認は[こちら](https://board.worksmobile.com/mai...
## 2. 主に入力・参照する情報
@ -24,9 +24,8 @@
| ブランド記号 | |
| 承認図予定日 | |
| 着工予定日 | |
| 備考欄 | |
| 添付ファイル | |
| 変更履歴 | |
| 施主名 | |
## 3. 業務でよく行う操作

View File

@ -1,9 +1,9 @@
# サイト仕様書①物件基本情報 Ver0.3SiteId: 335410
- 取得元データ: [site-335410_latest.json](./site-335410_latest.json)
- 取得元データ: [site-335410_latest.json](../siteSettingJsons/site-335410_latest.json)
- 取得日時: 2026-07-05T17:28:21
- 取得日時: 2026-07-25T18:08:14
- 設定バージョン: 1.017
@ -20,9 +20,9 @@
| Publish | false |
| DisableCrossSearch | false |
| Creator | 737 |
| Updator | 460 |
| Updator | 737 |
| CreatedTime | 2026-03-10T13:41:04 |
| UpdatedTime | 2026-07-05T17:28:21 |
| UpdatedTime | 2026-07-25T18:08:14 |
| SiteSettings.Version | 1.017 |
@ -30,14 +30,14 @@
| 種別 | 対象 | 権限値 |
| --- | --- | --- |
| | | |
| | | |
| | | |
| | | |
| | | |
| Dept | 25 | 511 |
| Group | 27 | 3 |
| User | 460 | 511 |
| User | 678 | 511 |
| User | 995 | 511 |
※ 権限値は環境のロール定義に依存します。
※ 権限値はビットフラグの加算方式のため、正確な意味は環境のロール定義に依存します。
## 3. 画面構成
@ -45,23 +45,84 @@
| 順番 | 列名 | 表示ラベル |
| --- | --- | --- |
| 1 | | |
| 2 | | |
| 3 | | |
| 4 | | |
| 5 | | |
| 6 | | |
| 7 | | |
| 8 | | |
| 9 | | |
| 10 | | |
| 1 | ResultId | |
| 2 | ClassA | 管理番号 |
| 3 | ClassS | 案件状況 |
| 4 | ClassB | 登録者 |
| 5 | Class001 | 施主1・姓 |
| 6 | Class021 | 建上地 |
| 7 | Class022 | エリア |
| 8 | Class023 | 契約ブランド |
| 9 | Class040 | |
| 10 | UpdatedTime | |
### 3.2 編集画面レイアウトEditorColumnHash / Sections
| タブ/キー | 項目一覧 |
| --- | --- |
| General | ResultId, Ver, Title, Comments, ClassA, ClassB, Status, ClassF, _Section-1, ClassY, ClassZ, ClassS, _Section-2, Class001, Class002, Class003, Class004, _Section-7, Class005, Class006, Class007, Class008, _Section-8, Class009, Class010, Class011, Class012, ClassK, Class015, _Section-9, Class021, Class022, Class023, Class024, Class025, ClassM, ClassN, _Section-4, Date001, Date002, _Section-3, Class031, Class032, Class033, Class034, Class035, Class036, Class037, Class038, Class151, _Section-6, Class061, Class062, Class063, Class064, Description151, Class152, Class153, _Section-5, DescriptionA, AttachmentsA, Description150, Class200, _Links-335411 |
#### タブ: General
| 順番 | 列名(内部) | 表示ラベル |
| --- | --- | --- |
| 1 | ResultId | |
| 2 | Ver | |
| 3 | Title | |
| 4 | Comments | 変更履歴 |
| 5 | ClassA | 管理番号 |
| 6 | ClassB | 登録者 |
| 7 | Status | 登録状況 |
| 8 | ClassF | 年 |
| 9 | _Section-1 | ―(セクション「」開始) |
| 10 | ClassY | マスターシート |
| 11 | ClassZ | 契約コード |
| 12 | ClassS | 案件状況 |
| 13 | _Section-2 | ―(セクション「物件情報」開始) |
| 14 | Class001 | 施主1・姓 |
| 15 | Class002 | 施主1・名 |
| 16 | Class003 | 施主1・姓カナ |
| 17 | Class004 | 施主1・名カナ |
| 18 | _Section-7 | ―(セクション「」開始) |
| 19 | Class005 | 施主2・姓 |
| 20 | Class006 | 施主2・名 |
| 21 | Class007 | 施主2・姓カナ |
| 22 | Class008 | 施主2・名カナ |
| 23 | _Section-8 | ―(セクション「」開始) |
| 24 | Class009 | 施主3・姓 |
| 25 | Class010 | 施主3・名 |
| 26 | Class011 | 施主3・姓カナ |
| 27 | Class012 | 施主3・名カナ |
| 28 | ClassK | 施主名 |
| 29 | Class015 | 施主名マージ |
| 30 | _Section-9 | ―(セクション「」開始) |
| 31 | Class021 | 建上地 |
| 32 | Class022 | エリア |
| 33 | Class023 | 契約ブランド |
| 34 | Class024 | 防火指定 |
| 35 | Class025 | 建物種類 |
| 36 | ClassM | エリア記号 |
| 37 | ClassN | ブランド記号 |
| 38 | _Section-4 | ―(セクション「スケジュール ※商談中の場合は、仮予定日を選択してください(後から変更可)」開始) |
| 39 | Date001 | 承認図予定日 |
| 40 | Date002 | 着工予定日 |
| 41 | _Section-3 | ―(セクション「物件担当者」開始) |
| 42 | Class031 | 契約営業所 |
| 43 | Class032 | 主担当営業 |
| 44 | Class033 | 副担当営業 |
| 45 | Class034 | 営業設計 |
| 46 | Class035 | 管理設計 |
| 47 | Class036 | 担当IC |
| 48 | Class037 | 担当監督 |
| 49 | Class038 | 所属長 |
| 50 | Class151 | 担当者一覧 |
| 51 | _Section-6 | ―セクション「CCで送ってほしい社員・パートがいる場合は、名前を検索・選択してください」開始 |
| 52 | Class061 | CC① |
| 53 | Class062 | CC② |
| 54 | Class063 | CC③ |
| 55 | Class064 | CC④ |
| 56 | Class152 | CC一覧 |
| 57 | Class153 | 担当者・CC一覧 |
| 58 | Description150 | 変更履歴 |
| 59 | Class200 | 重複チェック |
| 60 | _Links-335411 | |
@ -77,7 +138,6 @@
| 4 | スケジュール ※商談中の場合は、仮予定日を選択してください(後から変更可) | false | true |
| 3 | 物件担当者 | false | true |
| 6 | CCで送ってほしい社員・パートがいる場合は、名前を検索・選択してください | false | true |
| 5 | その他 ※物件担当者間で共有したいことがありましたらこちらへ | false | true |
### 3.3 タイトル表示TitleColumns / TitleSeparator
@ -88,60 +148,58 @@
## 4. 項目定義Columns
| 列名(内部) | 種別 | 表示ラベル | 説明/入力ガイド | 備考 |
| --- | --- | --- | --- | --- |
| Comments | 不明 | 変更履歴 | | |
| Status | 状態 | 登録状況 | | |
| ClassA | 文字列 | 管理番号 | | NoWrap, ExtendedHtmlAfterControl |
| ClassB | 文字列 | 登録者 | | |
| ClassF | 文字列 | 年 | | |
| ClassZ | 文字列 | 契約コード | | |
| ClassS | 文字列 | 案件状況 | | NoWrap |
| Class001 | 文字列 | 施主1・姓 | | NoWrap |
| Class002 | 文字列 | 施主1・名 | | |
| Class003 | 文字列 | 施主1・姓カナ | | NoWrap |
| Class004 | 文字列 | 施主1・名カナ | | |
| Class005 | 文字列 | 施主2・姓 | | NoWrap |
| Class006 | 文字列 | 施主2・名 | | |
| Class010 | 文字列 | 施主3・名 | | |
| Class012 | 文字列 | 施主3・名カナ | | |
| ClassM | 文字列 | エリア記号 | | NoWrap |
| ClassN | 文字列 | ブランド記号 | | |
| Date001 | 日付 | 承認図予定日 | | |
| Date002 | 日付 | 着工予定日 | | |
| DescriptionA | 説明 | 備考欄 | ※こちらの内容は営業積算課へ通知されません | ExtendedHtmlAfterControl |
| AttachmentsA | 添付 | 添付ファイル | | |
| Description150 | 説明 | 変更履歴 | | |
| Class200 | 文字列 | 重複チェック | | |
| ClassK | 文字列 | 施主名 | 施主:姓名を結合 | NoWrap |
| UpdatedTime | 不明 | | | |
| Class061 | 文字列 | CC① | | |
| Class062 | 文字列 | CC② | | |
| Class063 | 文字列 | CC③ | | NoWrap |
| Class064 | 文字列 | CC④ | | |
| Class031 | 文字列 | 契約営業所 | | |
| Class032 | 文字列 | 主担当営業 | | NoWrap |
| Class033 | 文字列 | 副担当営業 | | |
| Class034 | 文字列 | 営業設計 | | NoWrap |
| Class035 | 文字列 | 管理設計 | | |
| Class036 | 文字列 | 担当IC | | NoWrap |
| Class037 | 文字列 | 担当監督 | | |
| Class038 | 文字列 | 所属長 | | NoWrap |
| Class007 | 文字列 | 施主2・姓カナ | | NoWrap |
| Class008 | 文字列 | 施主2・名カナ | | |
| Class009 | 文字列 | 施主3・姓 | | NoWrap |
| Class022 | 文字列 | エリア | | |
| Class011 | 文字列 | 施主3・姓カナ | | NoWrap |
| Class023 | 文字列 | 契約ブランド | | NoWrap |
| Class024 | 文字列 | 防火指定 | | NoWrap |
| Class025 | 文字列 | 建物種類 | | |
| Class021 | 文字列 | 建上地 | | FieldCss |
| Class040 | 文字列 | | | |
| ClassY | 文字列 | マスターシート | | NoWrap, FieldCss, ExtendedHtmlAfterControl |
| Class015 | 文字列 | 施主名マージ | | NoWrap, FieldCss |
| Class151 | 文字列 | 担当者一覧 | | FieldCss |
| Class152 | 文字列 | CC一覧 | | FieldCss |
| Class153 | 文字列 | 担当者・CC一覧 | | FieldCss |
| 列名(内部) | 種別 | 表示ラベル | 説明/入力ガイド | 備考 | 参照状況 |
| --- | --- | --- | --- | --- | --- |
| Comments | 不明 | 変更履歴 | | | |
| Status | 状態 | 登録状況 | | | |
| ClassA | 文字列 | 管理番号 | | NoWrap, ExtendedHtmlAfterControl | |
| ClassB | 文字列 | 登録者 | | | |
| ClassF | 文字列 | 年 | | | |
| ClassZ | 文字列 | 契約コード | | | |
| ClassS | 文字列 | 案件状況 | | NoWrap | |
| Class001 | 文字列 | 施主1・姓 | | NoWrap | |
| Class002 | 文字列 | 施主1・名 | | | |
| Class003 | 文字列 | 施主1・姓カナ | | NoWrap | |
| Class004 | 文字列 | 施主1・名カナ | | | |
| Class005 | 文字列 | 施主2・姓 | | NoWrap | |
| Class006 | 文字列 | 施主2・名 | | | |
| Class010 | 文字列 | 施主3・名 | | | |
| Class012 | 文字列 | 施主3・名カナ | | | |
| ClassM | 文字列 | エリア記号 | | NoWrap | |
| ClassN | 文字列 | ブランド記号 | | | |
| Date001 | 日付 | 承認図予定日 | | | |
| Date002 | 日付 | 着工予定日 | | | |
| Description150 | 説明 | 変更履歴 | | | 未使用(自動処理からの参照なし。入力欄として存在するのみ) |
| Class200 | 文字列 | 重複チェック | | | |
| ClassK | 文字列 | 施主名 | 施主:姓名を結合 | NoWrap | |
| UpdatedTime | 不明 | | | | |
| Class061 | 文字列 | CC① | | | |
| Class062 | 文字列 | CC② | | | |
| Class063 | 文字列 | CC③ | | NoWrap | |
| Class064 | 文字列 | CC④ | | | |
| Class031 | 文字列 | 契約営業所 | | | |
| Class032 | 文字列 | 主担当営業 | | NoWrap | |
| Class033 | 文字列 | 副担当営業 | | | |
| Class034 | 文字列 | 営業設計 | | NoWrap | |
| Class035 | 文字列 | 管理設計 | | | |
| Class036 | 文字列 | 担当IC | | NoWrap | |
| Class037 | 文字列 | 担当監督 | | | |
| Class038 | 文字列 | 所属長 | | NoWrap | |
| Class007 | 文字列 | 施主2・姓カナ | | NoWrap | |
| Class008 | 文字列 | 施主2・名カナ | | | |
| Class009 | 文字列 | 施主3・姓 | | NoWrap | |
| Class022 | 文字列 | エリア | | | |
| Class011 | 文字列 | 施主3・姓カナ | | NoWrap | |
| Class023 | 文字列 | 契約ブランド | | NoWrap | |
| Class024 | 文字列 | 防火指定 | | NoWrap | |
| Class025 | 文字列 | 建物種類 | | | |
| Class021 | 文字列 | 建上地 | | FieldCss | |
| Class040 | 文字列 | | | | |
| ClassY | 文字列 | マスターシート | | NoWrap, FieldCss, ExtendedHtmlAfterControl | |
| Class015 | 文字列 | 施主名マージ | | NoWrap, FieldCss | |
| Class151 | 文字列 | 担当者一覧 | | FieldCss | |
| Class152 | 文字列 | CC一覧 | | FieldCss | |
| Class153 | 文字列 | 担当者・CC一覧 | | FieldCss | |
## 4.1 項目ごとの補足HTMLExtendedHtmlAfterControl
@ -151,40 +209,128 @@
| ClassA | 管理番号 | <div class="commentAdd small blue">
※自動採番
</div> |
| DescriptionA | 備考欄 | <div class="commentAdd red">
※こちらの内容は営業積算課へは通知されません
</div> |
| ClassY | マスターシート | <div class="commentAdd red nowrap">
※マスターシート登録済み物件は、上記より選択し作成ボタンを押すと登録情報が自動反映されます
</div> |
## 5. 作成・更新権限(フィールド単位)
### PermissionForCreating
| 項目 | 権限設定 |
| --- | --- |
| Class061 | 5 |
| Class062 | 5 |
| Class063 | 5 |
| Class064 | 5 |
| Class032 | 5 |
| Class033 | 5 |
| Class034 | 5 |
| Class035 | 5 |
| Class036 | 5 |
| Class037 | 5 |
| ClassB | 5 |
### PermissionForUpdating
| 項目 | 権限設定 |
| --- | --- |
| Class061 | 4 |
| Class062 | 4 |
| Class063 | 4 |
| Class064 | 4 |
| Class032 | 4 |
| Class033 | 4 |
| Class034 | 4 |
| Class035 | 4 |
| Class036 | 4 |
| Class037 | 4 |
## 7. 他サイト連携Links / ルックアップ)
| 起点列 | 参照先SiteId | 参照先の値→コピー先 | JsonFormat |
| --- | --- | --- | --- |
| | 210699 | | true |
| | 109100 | | true |
| | 96243 | | true |
| | 189112 | | true |
### 参照列: Class022参照先SiteId: 210699
| 参照先の列From | 自サイトへのコピー先To | 上書き設定 |
| --- | --- | --- |
| ClassA | ClassM | true |
- JsonFormat: true
### 参照列: Class023参照先SiteId: 109100
| 参照先の列From | 自サイトへのコピー先To | 上書き設定 |
| --- | --- | --- |
| ClassA | ClassN | true |
- JsonFormat: true
### 参照列: Class031参照先SiteId: 96243
| 参照先の列From | 自サイトへのコピー先To | 上書き設定 |
| --- | --- | --- |
| ClassK | Class038 | true |
- JsonFormat: true
### 参照列: ClassY参照先SiteId: 189112
| 参照先の列From | 自サイトへのコピー先To | 上書き設定 |
| --- | --- | --- |
| ClassA | ClassZ | |
| Class001 | Class001 | false |
| Class002 | Class002 | false |
| Class003 | Class003 | false |
| Class004 | Class004 | false |
| Class009 | Class005 | false |
| Class010 | Class006 | false |
| Class011 | Class007 | false |
| Class012 | Class008 | false |
| Class017 | Class009 | false |
| Class018 | Class010 | false |
| Class019 | Class011 | false |
| Class020 | Class012 | false |
| Class029 | Class021 | |
| ClassW | Class022 | |
| Class033 | Class023 | |
| Class040 | Class024 | |
| Class043 | Class025 | |
| ClassF | Class031 | |
| ClassG | Class032 | false |
| ClassH | Class033 | false |
| ClassI | Class034 | false |
| ClassL | Class035 | false |
| ClassK | Class036 | false |
| ClassJ | Class037 | false |
| Date063 | Date001 | |
| DateH | Date002 | |
- 検索表示フォーマット: `[ClassA] - [ClassQ] 様 ([ClassF] [ClassG])`
- JsonFormat: true
## 8. プロセス設定Processes
- 定義ファイル: [configs/configs/site-335410_①物件基本情報 Ver0.3/processes.json]
- 定義ファイル: [../configs/site-335410_①物件基本情報 Ver0.3/processes.json](../configs/site-335410_%E2%91%A0%E7%89%A9%E4%BB%B6%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E3%80%80Ver0.3/processes.json)
| Id | Name | DisplayName | 実行条件(Current→Changed) | 動作 |
| --- | --- | --- | --- | --- |
| 3 | 重複チェック | 重複チェック | | |
| 4 | 入力必須 | 入力必須 | | |
| 3 | 重複チェック | 重複チェック | -1 → -1 | データ変更DataChanges |
| 4 | 入力必須 | 入力必須 | -1 → -1 | 入力必須チェックValidateInputs |
## 9. スタイルStyles
| Id | Title | ファイル | 備考 |
| --- | --- | --- | --- |
| 1 | 01.読み取り専用 | [1_01.読み取り専用.css](./site-335410_①物件基本情報 Ver0.3/styles/1_01.読み取り専用.css) | |
| 2 | 02.共通 | [2_02.共通.css](./site-335410_①物件基本情報 Ver0.3/styles/2_02.共通.css) | |
| 1 | 01.読み取り専用 | [1_01.読み取り専用.css](../configs/site-335410_%E2%91%A0%E7%89%A9%E4%BB%B6%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E3%80%80Ver0.3/styles/1_01.%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8A%E5%B0%82%E7%94%A8.css) | |
| 2 | 02.共通 | [2_02.共通.css](../configs/site-335410_%E2%91%A0%E7%89%A9%E4%BB%B6%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E3%80%80Ver0.3/styles/2_02.%E5%85%B1%E9%80%9A.css) | |
## 10. スクリプトScriptsサーバースクリプトServerScripts
@ -193,10 +339,10 @@
| Id | Title | ファイル | 要約 |
| --- | --- | --- | --- |
| 1 | testページ遷移 | [1_testページ遷移.js](./site-335410_①物件基本情報 Ver0.3/scripts/1_testページ遷移.js) | $p.events.after_send_Control_1 = function () { |
| 2 | サンプル④担当者マージNG6/21 | [2_サンプル④担当者マージNG6_21.js](./site-335410_①物件基本情報 Ver0.3/scripts/2_サンプル④担当者マージNG6_21.js) | //---------------------------------------------------------------------- |
| 3 | test①施主名マージOK6/21 | [3_test①施主名マージOK6_21.js](./site-335410_①物件基本情報 Ver0.3/scripts/3_test①施主名マージOK6_21.js) | //---------------------------------------------------------------------- |
| 4 | 01_基本スクリプト.js | [4_01_基本スクリプト.js.js](./site-335410_①物件基本情報 Ver0.3/scripts/4_01_基本スクリプト.js.js) | /* |
| 1 | testページ遷移 | [1_testページ遷移.js](../configs/site-335410_%E2%91%A0%E7%89%A9%E4%BB%B6%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E3%80%80Ver0.3/scripts/1_test%EF%BC%88%E3%83%9A%E3%83%BC%E3%82%B8%E9%81%B7%E7%A7%BB%EF%BC%89.js) | $p.events.after_send_Control_1 = function () { |
| 2 | サンプル④担当者マージNG6/21 | [2_サンプル④担当者マージNG6_21.js](../configs/site-335410_%E2%91%A0%E7%89%A9%E4%BB%B6%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E3%80%80Ver0.3/scripts/2_%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E2%91%A3%E6%8B%85%E5%BD%93%E8%80%85%E3%83%9E%E3%83%BC%E3%82%B8%EF%BC%9ANG6_21.js) | //---------------------------------------------------------------------- |
| 3 | test①施主名マージOK6/21 | [3_test①施主名マージOK6_21.js](../configs/site-335410_%E2%91%A0%E7%89%A9%E4%BB%B6%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E3%80%80Ver0.3/scripts/3_test%E2%91%A0%E6%96%BD%E4%B8%BB%E5%90%8D%E3%83%9E%E3%83%BC%E3%82%B8%EF%BC%9AOK6_21.js) | //---------------------------------------------------------------------- |
| 4 | 01_基本スクリプト.js | [4_01_基本スクリプト.js.js](../configs/site-335410_%E2%91%A0%E7%89%A9%E4%BB%B6%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E3%80%80Ver0.3/scripts/4_01_%E5%9F%BA%E6%9C%AC%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88.js.js) | /* |
## 11. その他設定
@ -216,7 +362,9 @@
| SectionLatestId | 9 |
| StatusControls | [object Object] |
| LinkColumns | ResultId, Title, ClassA, ClassB, ClassK, ClassS, ClassY, ClassZ, Class021, Class022, Class023, Class031, Class032 |
| StatusControls | {"Id":1,"Name":"登録完了後ロック","Description":"物件登録完了後、項目欄の入力ができないようにロックをかける","Status":-1,"View":{"Id":0,"ColumnFilterHash":{"Status":"[\"200\",\"900\",\"910\",\"920\"]"},"ApiColumnKeyDisplayType":0,"ApiColumnValueDisplayType":0,"CalendarSiteId":0,"ApiDataType":0}} |
| AutoVerUpType | 3 |

View File

@ -9,14 +9,14 @@
このサイトは「②問い合わせ内容管理 Ver0.3」を管理するための業務画面です。Resultsとして扱われ、登録・照会・更新の基本的な業務フローに沿って利用されます。
- 画面上の案内では、新規依頼は【①物件登録情報】の各施主ページ内から行ってください! ※②問い合わせ内容管理ページの「+新規作成」から依頼するとエラーが出ます ■LINEWORKS[営業積算相談窓口](https://line.worksmobile.com/message/bot/view?version=10&botNo=11910622) ※窓口の対応内容や対応時間の確認は[こちら](https://board.worksmobile.com/main/article/40800000...
- 画面上の案内では、新規依頼は【①物件登録情報】の各施主ページ内から行ってください! ※②問い合わせ内容管理ページの「+新規作成」から依頼するとエラーが出ます ■LINEWORKS[営業積算相談窓口](https://line.worksmobile.com/message/send?userNoList=800300000001353&version=25&type=counselor) ※窓口の対応内容や対応時間の確認は[こちら](https://board.worksmobile.co...
## 2. 主に入力・参照する情報
| 項目 | 用途 |
| --- | --- |
| 親番 | |
| 主担当営業担当 | |
| 主担当営業 | |
| アップ希望時間 | |
| 事前相談者 | |
| 現在の進捗 | |
@ -27,29 +27,19 @@
| 承認図予定日 | |
| 着工予定日 | |
| 受付日 | |
| 依頼者 | この案件の質疑応答で「回答する側」となる本人(既定では案件作成者) |
| 質疑内容 | 営業積算課が依頼者に確認したい内容を入力する欄(送信すると欄はクリアされ、下部の履歴に記録される) |
| 回答内容 | 依頼者が質疑への回答を入力する欄(送信すると欄はクリアされ、下部の履歴に記録される) |
| 質疑回答ステータス | 質疑なし質疑あり回答あり回答確認済み、の4段階で今の状態を表す |
| 質疑ファイル/回答ファイル | 質疑・回答それぞれに添付できるファイル欄 |
| 質疑送付先 | 質疑を送る相手(複数選択可)を指定する欄。履歴の表示に使われる |
| 質疑回答期日 | 質疑送信時に自動計算される回答期限土日祝を除く5営業日後 |
| 回答確認日 | 営業積算課が回答内容を確認した日付(自動記録) |
| 質疑回答送信者 | 直近に質疑または回答を送信した人の氏名(自動記録) |
## 3. 質疑応答の業務の流れ(ボタン操作)
## 3. 業務でよく行う操作
営業積算課と依頼者との間で、案件についての質疑・回答をやり取りするための機能です。やり取りは画面下部に時系列の一覧表として表示され、質疑は赤系、回答は黄色系の色で区別されます。
| 操作 | 誰が行うか | 状態の変化 | 説明 |
| --- | --- | --- | --- |
| 質疑送信 | 営業積算課 | 受付済~社内確認中 → 依頼者質疑中 | 質疑内容を入力して送信すると、依頼者へ確認を求める状態になります。回答期日送信日から土日祝を除く5営業日後が自動的に設定されます |
| 回答送信 | 依頼者本人のみ | (状態はそのまま) | 依頼者が回答内容を入力して送信します。ボタンは依頼者本人にしか表示・実行できません |
| 回答確認 | 営業積算課 | 依頼者質疑中 → 積算対応中 | 依頼者からの回答内容を確認すると、案件は通常の対応状態に戻ります。確認した日付が記録されます |
| 質疑回答削除 | 営業積算課 | (状態はそのまま。依頼者質疑中の間のみ操作可) | 誤って送信した質疑・回答を、履歴のチェックボックスで選んで削除(無効化)します |
※「質疑送信」ボタンは、案件のステータスが「受付済」〜「社内確認中」のいずれかの間だけ表示されます。
| 操作 | 状態の変化 | 説明 |
| --- | --- | --- |
| 質疑送信 | -1 → 450 | 業務状態を遷移させるための操作です。 |
| 回答送信 | 450 → 450 | 業務状態を遷移させるための操作です。 |
| 営業積算確認 | 450 → 500 | 業務状態を遷移させるための操作です。 |
| 質疑回答削除 | 450 → 450 | 業務状態を遷移させるための操作です。 |
| ①物件基本情報へ戻る | -1 → -1 | 業務状態を遷移させるための操作です。 |
| 遅延連絡 | -1 → -1 | 業務状態を遷移させるための操作です。 |
| 見積回答 | -1 → -1 | 業務状態を遷移させるための操作です。 |
## 4. 他の管理表とのつながり
@ -58,9 +48,17 @@
- 109100 の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
- 335450 の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
- 335410 の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
- 335450 の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
- 関連サイト の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
- 関連サイト の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
- 関連サイト の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
- 関連サイト の情報を参照・反映する連携です。関連するマスタや別サイトの情報を参照する連携です。
## 5. 利用時のポイント

View File

@ -1,9 +1,9 @@
# サイト仕様書②問い合わせ内容管理 Ver0.3SiteId: 335411
- 取得元データ: [site-335411_latest.json](./site-335411_latest.json)
- 取得元データ: [site-335411_latest.json](../siteSettingJsons/site-335411_latest.json)
- 取得日時: 2026-07-05T18:20:51
- 取得日時: 2026-07-31T18:07:15
- 設定バージョン: 1.017
@ -22,40 +22,185 @@
| Creator | 737 |
| Updator | 460 |
| CreatedTime | 2026-03-10T13:41:38 |
| UpdatedTime | 2026-07-05T18:20:51 |
| UpdatedTime | 2026-07-31T18:07:15 |
| SiteSettings.Version | 1.017 |
## 2. アクセス権限Permissions
このサイト自体には権限設定がなく、SiteId 74663 の権限を継承しています。継承元サイトの権限設定を別途確認してください。
## 3. 画面構成
### 3.1 一覧画面GridColumns
| 順番 | 列名 | 表示ラベル |
| --- | --- | --- |
| 1 | | |
| 2 | | |
| 3 | | |
| 4 | | |
| 5 | | |
| 6 | | |
| 7 | | |
| 8 | | |
| 9 | | |
| 10 | | |
| 11 | | |
| 12 | | |
| 13 | | |
| 14 | | |
| 1 | ClassA | 親番 |
| 2 | ClassB | 子番 |
| 3 | Status | |
| 4 | ClassO | |
| 5 | CheckA | 事前相談済み |
| 6 | ClassE | アップ希望時間 |
| 7 | ClassC | 依頼者 |
| 8 | DescriptionC | 依頼内容 |
| 9 | Attachments031 | 依頼ファイル |
| 10 | Check071 | 質疑あり |
| 11 | Check072 | 回答あり |
| 12 | DescriptionF | |
| 13 | Attachments061 | |
| 14 | Comments | |
| 15 | UpdatedTime | |
### 3.2 編集画面レイアウトEditorColumnHash / Sections
| タブ/キー | 項目一覧 |
| --- | --- |
| General | ResultId, Ver, Title, ClassZ, Comments, ClassA, ClassB, ClassX, ClassC, ClassD, _Section-1, DateA, DateJ, ClassE, CheckA, ClassF, ClassG, ClassH, ClassI, DescriptionA, DateB, DateC, _Section-2, Class001, Class002, Class003, Class004, Class005, Class006, _Section-3, Class031, Class032, Class033, Class034, Description031, Class045, Class050, _Section-17, Class038, Class039, Class040, DescriptionC, _Section-18, Attachments031, Class052, _Section-16, Class055, Class056, Attachments055, _Section-6, Date061, Class061, Class062, Date062, Class063, _Section-7, Date063, Class065, Description061, Description062 |
| _Tab-2 | _Section-11, Date081, Class081, Date082, Class082, Class083, Class084, Status, Class080, _Section-12, Class085, Class086, Date084, Class087, Date085, Description081, Description083, _Section-13, Date090, Class091, Description091, Description092, Attachments101, Num101, Date101, Class101, Date102, Class102, Date103, Class103, Class095, Description095, Description101 |
| _Tab-1 | Class071, Description071, Class075, Date071, _Section-8, _Section-9, Description075, Attachments071, Date073, Class077, Num071, _Section-10, Description076, Attachments072, Num072, Date074 |
| _Tab-3 | _Section-14, Description111, Attachments111, Attachments112, Class111, Attachments113, _Section-15, Date111 |
#### タブ: General
| 順番 | 列名(内部) | 表示ラベル |
| --- | --- | --- |
| 1 | ResultId | |
| 2 | Ver | |
| 3 | Title | レコード名 |
| 4 | ClassZ | テスト・リンク用 |
| 5 | Comments | |
| 6 | ClassA | 親番 |
| 7 | ClassB | 子番 |
| 8 | ClassX | 連番処理 |
| 9 | ClassC | 依頼者 |
| 10 | ClassD | 主担当営業 |
| 11 | _Section-1 | ―(セクション「案件情報」開始) |
| 12 | DateA | アップ希望日 |
| 13 | DateJ | 選択可最短日 |
| 14 | ClassE | アップ希望時間 |
| 15 | CheckA | 事前相談済み |
| 16 | ClassF | 事前相談者 |
| 17 | ClassG | 現在の進捗 |
| 18 | ClassH | 現場の進捗 |
| 19 | ClassI | 必要な見積金額 |
| 20 | DescriptionA | 問い合わせ経緯 |
| 21 | DateB | 承認図予定日 |
| 22 | DateC | 着工予定日 |
| 23 | _Section-2 | ―(セクション「物件情報」開始) |
| 24 | Class001 | 施主名 |
| 25 | Class002 | 建上地 |
| 26 | Class003 | エリア |
| 27 | Class004 | ブランド |
| 28 | Class005 | 建物種類 |
| 29 | Class006 | 防火指定 |
| 30 | _Section-3 | ―(セクション「問い合わせ内容 ※入力不備があった場合は差し戻します※」開始) |
| 31 | Class031 | メーカー |
| 32 | Class032 | 商品名 |
| 33 | Class033 | 品番 |
| 34 | Class034 | 施工範囲・箇所 |
| 35 | Description031 | 依頼内容補足 |
| 36 | Class045 | 標準仕様 |
| 37 | Class050 | 前回の管理番号 |
| 38 | _Section-17 | ―(セクション「」開始) |
| 39 | Class038 | 積算Ver. |
| 40 | Class039 | マトリックスサイズ |
| 41 | Class040 | nexthouseシリーズ |
| 42 | DescriptionC | 依頼内容 |
| 43 | _Section-18 | ―(セクション「」開始) |
| 44 | Attachments031 | 依頼ファイル |
| 45 | Class052 | 回答希望送付先 |
| 46 | _Section-16 | ―(セクション「不備・追加ある場合はこちら ※必ず営業積算窓口へ変更がある旨をご連絡ください」開始) |
| 47 | Class055 | 不備・追加 |
| 48 | Class056 | 不備・追加理由 |
| 49 | Description055 | 理由補足 |
| 50 | Attachments055 | 不備・追加ファイル |
| 51 | _Section-6 | ―(セクション「営業積算 ※※読み取り専用※※」開始) |
| 52 | Date061 | 受付日 |
| 53 | Class061 | 受付者 |
| 54 | Class062 | 状況 |
| 55 | Date062 | 見積依頼日 |
| 56 | Class063 | 見積依頼先 |
| 57 | _Section-7 | ―(セクション「見積回答結果」開始) |
| 58 | Date063 | 見積受信日 |
| 59 | Class065 | 見積種別 |
| 60 | Description061 | 見積結果 |
| 61 | Description062 | 見積回答ファイル |
| 62 | _Links-335410 | |
| 63 | Class153 | 担当者・CC一覧 |
#### タブ: _Tab-2
| 順番 | 列名(内部) | 表示ラベル |
| --- | --- | --- |
| 1 | _Section-11 | ―(セクション「受付処理」開始) |
| 2 | Date081 | 受付日 |
| 3 | Class081 | 受付者 |
| 4 | Date082 | 見積依頼日 |
| 5 | Class082 | 見積依頼者 |
| 6 | Class083 | 見積依頼先 |
| 7 | Class084 | 見積備考 |
| 8 | Status | |
| 9 | Class080 | 問い合わせジャンル |
| 10 | _Section-12 | ―(セクション「遅延連絡」開始) |
| 11 | Class085 | 遅延送付先 |
| 12 | Class086 | 遅延理由 |
| 13 | Date084 | 遅延連絡日 |
| 14 | Class087 | 遅延連絡者 |
| 15 | Date085 | 返信予定日 |
| 16 | Description081 | 遅延連絡内容 |
| 17 | Description082 | 遅延連絡内容(控え) |
| 18 | Description083 | 遅延連絡ログ |
| 19 | _Section-13 | ―(セクション「算出回答結果」開始) |
| 20 | Date090 | 見積受信日 |
| 21 | Class091 | 見積種別 |
| 22 | Description091 | 見積回答 |
| 23 | Description092 | 別途費用・伝達事項 |
| 24 | Description093 | |
| 25 | Attachments101 | 見積回答ファイル |
| 26 | Num101 | 算出回答回数 |
| 27 | Date101 | 算出日 |
| 28 | Class101 | 算出者 |
| 29 | Date102 | チェック日 |
| 30 | Class102 | チェック者 |
| 31 | Date103 | 返信日 |
| 32 | Class103 | 返信者 |
| 33 | Class095 | 回答送付先 |
| 34 | Description095 | 回答送付先一覧 |
| 35 | Description101 | 見積回答ログ |
#### タブ: _Tab-1
| 順番 | 列名(内部) | 表示ラベル |
| --- | --- | --- |
| 1 | Class071 | 質疑回答ステータス |
| 2 | Description071 | 質疑回答データ保存用 |
| 3 | Class075 | 質疑回答送信者 |
| 4 | Date071 | 質疑回答送信日時 |
| 5 | _Section-8 | ―(セクション「質疑回答ログ」開始) |
| 6 | _Section-9 | ―(セクション「営業積算課からの質疑」開始) |
| 7 | Description075 | 質疑内容 |
| 8 | Attachments071 | 質疑ファイル |
| 9 | Date073 | 質疑回答期日 |
| 10 | Class077 | 質疑送付先 |
| 11 | Check071 | 質疑あり |
| 12 | Num071 | 質疑回数 |
| 13 | _Section-10 | ―(セクション「依頼者からの回答」開始) |
| 14 | Description076 | 回答内容 |
| 15 | Attachments072 | 回答ファイル |
| 16 | Check072 | 回答あり |
| 17 | Num072 | 回答回数 |
| 18 | Date074 | 回答確認日 |
#### タブ: _Tab-3
| 順番 | 列名(内部) | 表示ラベル |
| --- | --- | --- |
| 1 | _Section-14 | ―(セクション「営業積算課からの引継ぎ」開始) |
| 2 | Description111 | 引継ぎ備考 |
| 3 | Attachments111 | NET見積原本 |
| 4 | Attachments112 | 算出根拠資料 |
| 5 | Class111 | 採用書類 |
| 6 | Attachments113 | その他 |
| 7 | _Section-15 | ―(セクション「積算発注課管理」開始) |
| 8 | Date111 | データ出力日 |
@ -68,13 +213,13 @@
| 3 | 問い合わせ内容 ※入力不備があった場合は差し戻します※ | false | true |
| 17 | | false | true |
| 18 | | false | true |
| 16 | 不備・追加ある場合はこちら ※必ずコメント欄へ追加や変更がある旨記載してください | false | false |
| 6 | 営業積算 ※※ここから下は営業積算タブから参照し入力されるように設定予定のため、基本は読み取り専用にします※※ | false | true |
| 16 | 不備・追加ある場合はこちら ※必ず営業積算窓口へ変更がある旨をご連絡ください | false | false |
| 6 | 営業積算 ※※読み取り専用※※ | false | true |
| 7 | 見積回答結果 | false | true |
| 11 | 受付処理 | false | true |
| 12 | 遅延連絡 | false | true |
| 13 | 算出回答結果 | false | true |
| 8 | 質疑回答ログ ※※詳細未設定のため、ログは表示されません※※ | false | true |
| 8 | 質疑回答ログ | true | true |
| 9 | 営業積算課からの質疑 | false | true |
| 10 | 依頼者からの回答 | false | true |
| 14 | 営業積算課からの引継ぎ | false | true |
@ -93,7 +238,7 @@
| --- | --- | --- | --- | --- |
| ClassA | 文字列 | 親番 | | NoWrap |
| Owner | ユーザー | | | NoWrap |
| ClassD | 文字列 | 主担当営業担当 | | |
| ClassD | 文字列 | 主担当営業 | | |
| ClassE | 文字列 | アップ希望時間 | | NoWrap |
| ClassF | 文字列 | 事前相談者 | 事前相談した担当を選択してください | |
| ClassG | 文字列 | 現在の進捗 | | NoWrap |
@ -126,7 +271,7 @@
| Class064 | 文字列 | 採用状況 | | NoWrap |
| Class065 | 文字列 | 見積種別 | | NoWrap |
| Description061 | 説明 | 見積結果 | | |
| Description062 | 説明 | 回答ファイル | | |
| Description062 | 説明 | 見積回答ファイル | | |
| Date071 | 日付 | 質疑回答送信日時 | | |
| Date073 | 日付 | 質疑回答期日 | | |
| Date074 | 日付 | 回答確認日 | | NoWrap |
@ -158,7 +303,7 @@
| Class101 | 文字列 | 算出者 | | |
| Class102 | 文字列 | チェック者 | | |
| Class103 | 文字列 | 返信者 | | |
| Description101 | 説明 | 見積回答データ保存用 | | |
| Description101 | 説明 | 見積回答ログ | | |
| Attachments101 | 添付 | 見積回答ファイル | | |
| Attachments111 | 添付 | NET見積原本 | | |
| Attachments112 | 添付 | 算出根拠資料 | | |
@ -182,7 +327,7 @@
| Class077 | 文字列 | 質疑送付先 | | FieldCss |
| UpdatedTime | 不明 | | | |
| ClassO | 文字列 | | | |
| DescriptionC | 説明 | | 依頼内容マージ | |
| DescriptionC | 説明 | 依頼内容 | 依頼内容マージ | |
| DescriptionF | 説明 | | | |
| Class032 | 文字列 | 商品名 | 正しい商品名を入力してください(例)イエリア 格子間仕切 | FieldCss |
| Class033 | 文字列 | 品番 | 正しい品番を入力してくださいQW404-■■ | FieldCss |
@ -193,9 +338,14 @@
| ClassZ | 文字列 | テスト・リンク用 | | FieldCss |
| Class034 | 文字列 | 施工範囲・箇所 | 施工場所を入力してください ※複数パターンある場合は『A●●、B■■』のようにわかるように記載してください | FieldCss |
| Date084 | 日付 | 遅延連絡日 | | NoWrap |
| DateJ | 日付 | アップ希望期限 | | |
| DateJ | 日付 | 選択可最短日 | | |
| ClassB | 文字列 | 子番 | | |
| ClassX | 文字列 | 連番処理 | | NoWrap |
| Description082 | 説明 | 遅延連絡内容(控え) | | |
| Check071 | チェック | 質疑あり | | NoWrap |
| Check072 | チェック | 回答あり | | NoWrap |
| Class153 | 文字列 | 担当者・CC一覧 | | FieldCss |
| Description055 | 説明 | 理由補足 | 不備・追加理由の詳細をご入力ください | FieldCss |
## 4.1 項目ごとの補足HTMLExtendedHtmlAfterControl
@ -222,43 +372,130 @@
</div> |
## 5. 作成・更新権限(フィールド単位)
設定なし。
## 7. 他サイト連携Links / ルックアップ)
| 起点列 | 参照先SiteId | 参照先の値→コピー先 | JsonFormat |
| --- | --- | --- | --- |
| | 210699 | | |
| | 109100 | | |
| | 335410 | | true |
| | 335450 | | true |
### 参照列: Class003参照先SiteId: 210699
- JsonFormat:
### 参照列: Class004参照先SiteId: 109100
- JsonFormat:
### 参照列: Class086参照先SiteId: 335450
| 参照先の列From | 自サイトへのコピー先To | 上書き設定 |
| --- | --- | --- |
| DescriptionA | Description081 | |
- JsonFormat: true
### 参照列: ClassZ参照先SiteId: 335410
| 参照先の列From | 自サイトへのコピー先To | 上書き設定 |
| --- | --- | --- |
| ClassA | ClassA | |
| Class032 | ClassD | |
| Date001 | DateB | |
| Date002 | DateC | |
| Class015 | Class001 | |
| Class021 | Class002 | |
| Class022 | Class003 | |
| Class023 | Class004 | |
| Class025 | Class005 | |
| Class024 | Class006 | |
| Class153 | Class153 | |
| Class153 | Class052 | |
| Class153 | Class077 | |
| Class153 | Class085 | |
| Class153 | Class095 | |
- JsonFormat: true
### 参照列: Class052マスタテーブル: Users
- JsonFormat: true
### 参照列: Class077マスタテーブル: Users
- JsonFormat: true
### 参照列: Class085マスタテーブル: Users
- JsonFormat: true
### 参照列: Class095マスタテーブル: Users
- JsonFormat: true
## 8. プロセス設定Processes
質疑応答機能下記10.のScripts「03_質疑応答スクリプト」と対として、2026-07-11時点で4件のプロセスが設定されている。
定義ファイル: [processes.json](./site-335411_②問い合わせ内容管理 Ver0.3/processes.json)(ドラフト版: [processes_質疑応答ドラフト.json](./site-335411_②問い合わせ内容管理 Ver0.3/processes_質疑応答ドラフト.json)
- 定義ファイル: [../configs/site-335411_②問い合わせ内容管理 Ver0.3/processes.json](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/processes.json)
Statusメインのステータスのラベル対応: 100新規200登録済300積算依頼400受付済450依頼者質疑中500積算対応中510外部見積待ち520社内確認中800積算回答900完了901依頼不要910保留920取消
| Id | Name | DisplayName | 実行条件(Current→Changed) | 動作 |
| --- | --- | --- | --- | --- |
| 1 | 質疑送信 | 質疑送信 | -1 → 450 | $p.ex.processScript($(this)); |
| 2 | 回答送信 | 回答送信 | 450 → 450 | $p.ex.processScript($(this)); |
| 3 | 営業積算確認 | 営業積算確認 | 450 → 500 | $p.ex.processScript($(this)); |
| 4 | 質疑回答削除 | 質疑回答削除 | 450 → 450 | $p.ex.processScript($(this)); |
| 5 | ①物件基本情報へ戻る | ①物件基本情報へ戻る | -1 → -1 | $p.ex.processScript($(this)); |
| 6 | 遅延連絡 | 遅延連絡 | -1 → -1 | $p.ex.processScript($(this)); |
| 7 | 見積回答 | 見積回答 | -1 → -1 | $p.ex.processScript($(this)); |
| Id | Name | 実行条件(CurrentStatus→ChangedStatus) | 追加条件(View) | 権限 | OnClick |
| --- | --- | --- | --- | --- | --- |
| 1 | 質疑送信 | 510(外部見積待ち)→450(依頼者質疑中) | Statusが400/450/500/510/520のいずれかの間のみ表示 | Depts:[25](営業積算課), Groups:[19] | `$p.ex.processScript($(this));` |
| 2 | 回答送信 | 450(依頼者質疑中)→450(変化なし) | ClassC(依頼者)が自分自身の場合のみ表示(`"Own"` | 制限なしView条件で依頼者本人に限定 | `$p.ex.processScript($(this));` |
| 3 | 回答確認 | 450(依頼者質疑中)→500(積算対応中) | なし | Depts:[25](営業積算課) | `$p.ex.processScript($(this));` |
| 4 | 質疑回答削除 | 450(依頼者質疑中)→450(変化なし) | なし | Depts:[25](営業積算課) | `$p.ex.processScript($(this));` |
### 8.1 Status遷移フローチャート
補足:
- OnClickはいずれもクリックされたボタン要素自体`$(this)`)を渡す形式。呼び出し先の`$p.ex.processScript`03_質疑応答スクリプトに実装は、procNum数値ではなくボタンの表示ラベル文字列で処理を振り分けている。これはProcessesのId構成や件数が変わっても影響を受けないようにするための実装判断。
- Id1「質疑送信」はCurrentStatus単体では510のみだが、View.ColumnFilterHashでStatus 400/450/500/510/520の間は表示されるようになっており、実質的な運用可能範囲はView条件側が決めている。
- Id2「回答送信」はDeptsによる部署制限ではなく、View.ColumnFilterHashの`"ClassC": ["Own"]`条件でサーバー側に「依頼者本人のみ実行可」を強制しているPleasanter標準機能
ノード=`Status`値、エッジ=プロセス名(`(Dept番号)`は実行可能部署制限、📧は通知あり)。`CurrentStatus`/`ChangedStatus`が`-1`でView条件による絞り込みも無いプロセス①物件基本情報へ戻る遅延連絡は仮想ード`((任意))`で表現。
```mermaid
flowchart LR
450((依頼者質疑中<br/>450))
100((新規<br/>100))
400((受付済<br/>400))
500((積算対応中<br/>500))
510((外部見積待ち<br/>510))
520((社内確認中<br/>520))
800((一部完了<br/>800))
480((依頼者回答有<br/>480))
900((完了<br/>900))
901((依頼不要<br/>901))
any((任意))
100 -->|"📧質疑送信"| 450
400 -->|"📧質疑送信"| 450
450 -->|"📧質疑送信"| 450
500 -->|"📧質疑送信"| 450
510 -->|"📧質疑送信"| 450
520 -->|"📧質疑送信"| 450
800 -->|"📧質疑送信"| 450
450 -->|"📧回答送信"| 480
450 -->|"営業積算確認 (Dept25)"| 500
450 -->|"質疑回答削除 (Dept25)"| 450
any -->|"①物件基本情報へ戻る"| any
any -->|"📧遅延連絡 (Dept25)"| any
800 -->|"📧見積回答 (Dept25)"| 800
900 -->|"📧見積回答 (Dept25)"| 900
901 -->|"📧見積回答 (Dept25)"| 901
```
このフローチャートは`processes.json`の構造化データCurrentStatus/ChangedStatus/Depts/View条件/Notifications有無のみを機械的に図化したもので、ボタン非表示化・独自ボタン経由呼び出しの検出は含まないScripts/ServerScripts解析はサイトごとに実装が異なり自動化困難なため
site-335411では実際、`Processes`全7件が`serverscripts/1_祝日リスト取得スクリプト.js.js``elements.DisplayType('Process_N', 3)`で標準ボタン表示を非表示化されており、Id5「①物件基本情報へ戻る」を除く6件は`scripts/3_03_質疑応答スクリプト.js.js`の`clickProcessByName(name)`経由で独自配置ボタンから呼び出される構成になっている(画面上に標準のプロセスボタンは出ない)。
> 生成コマンド: `node generate-process-flowchart.js site-335411`
## 9. スタイルStyles
| Id | Title | ファイル | 備考 |
| --- | --- | --- | --- |
| 1 | 00_読み取り専用スタイル.css | [1_00_読み取り専用スタイル.css.css](./site-335411_②問い合わせ内容管理 Ver0.3/styles/1_00_読み取り専用スタイル.css.css) | |
| 2 | 01_共通スタイル.css | [2_01_共通スタイル.css.css](./site-335411_②問い合わせ内容管理 Ver0.3/styles/2_01_共通スタイル.css.css) | |
| 3 | 04_質疑回答テーブル.css | [3_04_質疑回答テーブル.css.css](./site-335411_②問い合わせ内容管理 Ver0.3/styles/3_04_質疑回答テーブル.css.css) | 質疑応答履歴テーブルの配色・罫線 |
| 1 | 00_読み取り専用スタイル.css | [1_00_読み取り専用スタイル.css.css](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/styles/1_00_%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8A%E5%B0%82%E7%94%A8%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB.css.css) | |
| 2 | 01_共通スタイル.css | [2_01_共通スタイル.css.css](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/styles/2_01_%E5%85%B1%E9%80%9A%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB.css.css) | |
| 3 | 3_04_質疑回答テーブル.css | [3_3_04_質疑回答テーブル.css.css](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/styles/3_3_04_%E8%B3%AA%E7%96%91%E5%9B%9E%E7%AD%94%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB.css.css) | |
## 10. スクリプトScriptsサーバースクリプトServerScripts
@ -267,15 +504,17 @@ Statusメインのステータスのラベル対応: 100新規200登録
| Id | Title | ファイル | 要約 |
| --- | --- | --- | --- |
| 1 | 01_基本スクリプト.js | [1_01_基本スクリプト.js.js](./site-335411_②問い合わせ内容管理 Ver0.3/scripts/1_01_基本スクリプト.js.js) | 画面初期化、アップ希望日と期限日のチェック、依頼内容マージ、遅延連絡・算出対応ログの保存など。新規作成時にServerScript「祝日リスト取得スクリプト」の結果(HolidaysList)を使って期限日を算出する |
| 2 | 02_添付ファイルDL.js | [2_02_添付ファイルDL.js.js](./site-335411_②問い合わせ内容管理 Ver0.3/scripts/2_02_添付ファイルDL.js.js) | 添付ファイル欄のダウンロードチェックボックス生成 |
| 3 | 03_質疑応答スクリプト.js | [3_03_質疑応答スクリプト_修正版.js.js](./site-335411_②問い合わせ内容管理 Ver0.3/scripts/3_03_質疑応答スクリプト_修正版.js.js) | 質疑送信・回答送信・回答確認・削除の各プロセス8.参照)から呼ばれ、質疑回答データ(Description071)への保存、履歴テーブル表示、タブの色分け、質疑回答期日の自動計算を行う |
| 1 | 01_基本スクリプト.js | [1_01_基本スクリプト.js.js](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/scripts/1_01_%E5%9F%BA%E6%9C%AC%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88.js.js) | /* |
| 2 | 02_添付ファイルDL.js | [2_02_添付ファイルDL.js.js](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/scripts/2_02_%E6%B7%BB%E4%BB%98%E3%83%95%E3%82%A1%E3%82%A4%E3%83%ABDL.js.js) | /* |
| 3 | 03_質疑応答スクリプト.js | [3_03_質疑応答スクリプト.js.js](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/scripts/3_03_%E8%B3%AA%E7%96%91%E5%BF%9C%E7%AD%94%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88.js.js) | /* |
### ServerScripts
| Id | Title | ファイル | 実行タイミング | 要約 |
| --- | --- | --- | --- | --- |
| 1 | 祝日リスト取得スクリプト.js | [1_祝日リスト取得スクリプト.js](./site-335411_②問い合わせ内容管理 Ver0.3/serverscripts/1_祝日リスト取得スクリプト.js) | BeforeOpeningPage画面表示前 | 祝日テーブル(SiteId 192267)から本日30日〜+60日分の祝日を取得し、Hidden項目`HolidaysList`に格納する |
| Id | Title | ファイル | 要約 |
| --- | --- | --- | --- |
| 1 | 祝日リスト取得スクリプト.js | [1_祝日リスト取得スクリプト.js.js](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/serverscripts/1_%E7%A5%9D%E6%97%A5%E3%83%AA%E3%82%B9%E3%83%88%E5%8F%96%E5%BE%97%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88.js.js) | /* |
| 0 | 子番自動保存スクリプト.js | [0_子番自動保存スクリプト.js.js](../configs/site-335411_%E2%91%A1%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E3%80%80Ver0.3/serverscripts/0_%E5%AD%90%E7%95%AA%E8%87%AA%E5%8B%95%E4%BF%9D%E5%AD%98%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88.js.js) | /* |
## 11. その他設定
@ -293,16 +532,20 @@ Statusメインのステータスのラベル対応: 100新規200登録
| TabLatestId | 4 |
| Tabs | [object Object], [object Object], [object Object], [object Object] |
| Tabs | {"LabelText":"質疑回答","Id":1}, {"LabelText":"営業積算","Id":2}, {"LabelText":"積算発注","Id":3} |
| SectionLatestId | 18 |
| LinkColumns | ClassA, ClassB, ClassC, Class062, DateA, Class091, Description061 |
| Htmls | [object Object] |
| Htmls | {"Title":"外部ライブラリ","PositionType":9000,"New":true,"Edit":true,"Body":"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/axios/1.18.1/axios.min.js\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js\"></script>\n","Id":1} |
| AutoVerUpType | 2 |
| HideLink | true |
| NoDisplayIfReadOnly | false |
| NotInheritPermissionsWhenCreatingSite | false |
| CreateColumnAccessControls | {"ColumnName":"Class062","Depts":[25]} |

View File

@ -0,0 +1,17 @@
Subject: 【営業積算】[ClassA]-[ClassB] [Class001]様 : 新規依頼
To: e-sekisan@next-hd.co.jp
CC: [ClassC]
依頼者より新規依頼が届きました。
内容を確認し、受付処理をお願いします。
{Url}
----------
親番:[ClassA]
子番:[ClassB]
施主名:[Class001]
----------
■■■

View File

@ -0,0 +1,25 @@
Subject: 【営業積算】[ClassA]-[ClassB] [Class001]様 : 質疑送信
To: [ClassC]
CC: [Class077]
営業積算より
依頼内容についての質疑が届きました。
内容を確認し、該当の質疑回答欄へ回答を入力してください。
{Url}
----------
親番:[ClassA]
子番:[ClassB]
施主名:[Class001]
----------
質疑回答期日:[Date073]
営業積算からの質疑:
[Description075]
----------
■■■

Some files were not shown because too many files have changed in this diff Show More