運営者向けの運用ガイドです。初期設定から日次運用まで、必要な手順を順番にまとめています。
〜.pages.dev)をお使いの方へ:ログインして使うだけです。2章の「GAS接続」と設定画面の「GAS URL」「APIキー」は不要・操作禁止です。詳しくは 9. Cloudflare版の運用 を読んでください。abc-5f3a…)Code.gs に下記コードを貼り付け、先頭の VALID_KEY を決めたキーに書き換えるGAS URL に、キーを APIキー 欄に入れて保存このHTMLに固定 を押す接続テスト 実行後、右上 ↻ で同期確認// =====================================================
// 予実.app GAS バックエンド
// =====================================================
// 【重要】VALID_KEY は会社ごとに別の値にする。他社と共用しない。
// 予実.app 設定タブ > APIキー欄 にこのキーを入力すること。
const VALID_KEY = 'ここに決めたAPIキーを入れる';
const DEALS_SHEET_NAME = 'Deals';
const CONFIG_SHEET_NAME = 'Config';
function validateKey(e) {
return ((e && e.parameter && e.parameter.key) || '') === VALID_KEY;
}
// 案件シートに必要な見出し。足りないものは initSheets が右端に足す
const DEAL_HEADERS = ['id', 'year', 'month', 'cat', 'st', 'tanto', 'name', 'uri', 'gen', 'note', 'genYear', 'genMonth', 'startYear', 'startMonth', 'kind'];
function initSheets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let dealSheet = ss.getSheetByName(DEALS_SHEET_NAME);
if (!dealSheet) {
dealSheet = ss.insertSheet(DEALS_SHEET_NAME);
dealSheet.appendRow(DEAL_HEADERS);
} else {
// 10列しかない古いシートでも、原価計上月・開始月・区分が保存されるよう見出しを揃える
const lastCol = Math.max(dealSheet.getLastColumn(), 1);
const headers = dealSheet.getRange(1, 1, 1, lastCol).getValues()[0].map(String);
const missing = DEAL_HEADERS.filter(h => headers.indexOf(h) === -1);
if (missing.length) {
dealSheet.getRange(1, lastCol + 1, 1, missing.length).setValues([missing]);
}
}
if (!ss.getSheetByName(CONFIG_SHEET_NAME)) {
const s = ss.insertSheet(CONFIG_SHEET_NAME);
s.appendRow(['key', 'value']);
}
}
function doGet(e) {
if (!validateKey(e)) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: 'Unauthorized' }))
.setMimeType(ContentService.MimeType.JSON);
}
try {
initSheets();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const dealSheet = ss.getSheetByName(DEALS_SHEET_NAME);
const dealsData = dealSheet.getDataRange().getValues();
const headers = dealsData[0];
const deals = [];
for (let i = 1; i < dealsData.length; i++) {
if (!dealsData[i][0]) continue;
let obj = {};
for (let j = 0; j < headers.length; j++) {
obj[headers[j]] = dealsData[i][j];
}
deals.push(obj);
}
const configSheet = ss.getSheetByName(CONFIG_SHEET_NAME);
const configData = configSheet.getDataRange().getValues();
let config = {};
for (let i = 1; i < configData.length; i++) {
const key = configData[i][0];
const val = configData[i][1];
if (!key) continue;
try {
config[key] = JSON.parse(val);
} catch(err) {
config[key] = val;
}
}
return ContentService
.createTextOutput(JSON.stringify({ ok: true, deals: deals, config: config }))
.setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: err.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function doPost(e) {
if (!validateKey(e)) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: 'Unauthorized' }))
.setMimeType(ContentService.MimeType.JSON);
}
try {
initSheets();
const ss = SpreadsheetApp.getActiveSpreadsheet();
let data;
try {
data = JSON.parse(e.postData.contents);
} catch(err) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: 'Invalid JSON' }))
.setMimeType(ContentService.MimeType.JSON);
}
if (data.action === 'update_config') {
const sheet = ss.getSheetByName(CONFIG_SHEET_NAME);
const rows = sheet.getDataRange().getValues();
let found = false;
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] === data.key) {
sheet.getRange(i + 1, 2).setValue(JSON.stringify(data.value));
found = true;
break;
}
}
if (!found) sheet.appendRow([data.key, JSON.stringify(data.value)]);
SpreadsheetApp.flush();
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
}
// 同時に届いた保存が行を取り合わないよう、案件の書き込みは1件ずつ行う
const lock = LockService.getScriptLock();
lock.waitLock(10000);
try {
const dealSheet = ss.getSheetByName(DEALS_SHEET_NAME);
// 見出し(1行目)に合わせて書き込む。見出しの無い項目は書かない
const dealHeaders = dealSheet.getRange(1, 1, 1, dealSheet.getLastColumn()).getValues()[0];
const rowFromDeal = (d) => dealHeaders.map(h => d[h] !== undefined && d[h] !== null ? d[h] : '');
const findRow = (id) => {
const rows = dealSheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (String(rows[i][0]) === String(id)) return i + 1;
}
return 0;
};
if (data.action === 'create' || data.action === 'update') {
const d = data.deal;
const row = findRow(d.id);
if (row) {
// 再送で同じ案件の create が届いた場合も、行を増やさず上書きする
dealSheet.getRange(row, 1, 1, dealHeaders.length).setValues([rowFromDeal(d)]);
} else if (data.action === 'create') {
dealSheet.appendRow(rowFromDeal(d));
}
SpreadsheetApp.flush();
}
else if (data.action === 'delete') {
const rows = dealSheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (String(rows[i][0]) === String(data.id)) {
dealSheet.deleteRow(i + 1);
break;
}
}
SpreadsheetApp.flush();
}
else {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: 'Unknown action: ' + data.action }))
.setMimeType(ContentService.MimeType.JSON);
}
} finally {
lock.releaseLock();
}
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: err.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
+ 案件追加 をクリック🔗 でリンク付与| 症状 | 原因 | 対応 |
|---|---|---|
| お試しモードのまま | GAS URL未設定 / 権限不足 | URLとデプロイ権限を確認 |
| 同期エラー | GAS停止 / CORS / タイムアウト | GAS再デプロイ・URL再確認 |
| Access denied | 許可条件(メール/APIキー)不一致 | Code.gsとHTML設定を合わせる |
| 数字不一致 | 計上月/ステータス混在 | フィルタ条件を確認 |
https://〜.pages.dev/)で使う形です。画面やデータの持ち方は通常版と同じで、違うのは「入口」と「接続のしくみ」だけです。VALID_KEY を新しくして既存デプロイを「新バージョン」に更新 → Cloudflareの秘密変数 API_KEY を同じ値に更新して再デプロイ。この順で行うと止まる時間は1〜2分ですDeals タブを右クリック →「コピーを作成」→ 名前を Deals_FY10 のように変えるConfig タブも同じくコピーして Config_FY10 にする(目標・販管費・KPIボード・期初期末はここに入っています)Deals / Config に戻したり中身を編集したりしない(アプリはこの2つの名前のタブしか読みません)2027-07 〜 2028-06。必ず12か月にする)Deals に残したままで構いません。期初・期末を変えれば損益には出なくなります↻ で再同期。直らなければ再読み込み