/** * プラモデル在庫管理 - Google Apps Script * * 使い方: * 1. スプレッドシートを開き「拡張機能」→「Apps Script」を開く * 2. このコードを貼り付けて保存 * 3. 「デプロイ」→「新しいデプロイ」→ 種類「ウェブアプリ」 * 実行するユーザー: 自分 * アクセスできるユーザー: 全員 * 4. 表示されたURL(/exec で終わるもの)をHTMLの CONFIG.gasUrl に貼る * * 注意: コードを直したら「デプロイを管理」→ 鉛筆 → 新バージョン → デプロイ * (保存だけでは /exec のURLに反映されません) */ const SHEET_NAME = '在庫'; const HEADERS = ['ID', 'グレード', 'キット名', '購入先', '状態', '登録日時', '更新日時']; const GRADES = ['HG', 'RG', 'MG', 'PG', 'SD', 'EG', 'その他']; const STATUSES = ['積みプラ', '組立中', '素組', '完成品', '出品・販売済み']; const COLOR_SHEET_NAME = 'カラー'; const COLOR_HEADERS = ['ID', 'メーカー', '色名', '型式番号', '所有数', '状態', '登録日時', '更新日時']; const COLOR_STATUSES = ['在庫あり', '残りわずか', '切れている', '欲しい']; /* ---------- 入口 ---------- */ function doGet(e) { return handle_(e.parameter || {}); } function doPost(e) { let params = {}; try { params = JSON.parse((e.postData && e.postData.contents) || '{}'); } catch (err) { return json_({ ok: false, error: 'リクエストの形式が正しくありません' }); } return handle_(params); } function handle_(p) { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); } catch (err) { return json_({ ok: false, error: '混み合っています。少し待ってからもう一度お試しください' }); } try { switch (p.action) { case 'list': return json_({ ok: true, items: list_(getSheet_()) }); case 'add': return json_(add_(getSheet_(), p)); case 'update': return json_(update_(getSheet_(), p)); case 'delete': return json_(delete_(getSheet_(), p)); case 'colorList': return json_({ ok: true, items: colorList_(getColorSheet_()) }); case 'colorAdd': return json_(colorAdd_(getColorSheet_(), p)); case 'colorUpdate': return json_(colorUpdate_(getColorSheet_(), p)); case 'colorDelete': return json_(colorDelete_(getColorSheet_(), p)); default: return json_({ ok: false, error: '不明な操作です' }); } } catch (err) { return json_({ ok: false, error: 'エラーが発生しました: ' + err.message }); } finally { lock.releaseLock(); } } /* ---------- シート(キット) ---------- */ function getSheet_() { const ss = SpreadsheetApp.getActiveSpreadsheet(); let sh = ss.getSheetByName(SHEET_NAME); if (!sh) { sh = ss.insertSheet(SHEET_NAME); } if (sh.getLastRow() === 0) { sh.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]); sh.setFrozenRows(1); // IDの先頭ゼロや英字混じりが勝手に変換されないよう、文字列形式にする sh.getRange('A:A').setNumberFormat('@'); } return sh; } /* ---------- シート(カラー) ---------- */ function getColorSheet_() { const ss = SpreadsheetApp.getActiveSpreadsheet(); let sh = ss.getSheetByName(COLOR_SHEET_NAME); if (!sh) { sh = ss.insertSheet(COLOR_SHEET_NAME); } if (sh.getLastRow() === 0) { sh.getRange(1, 1, 1, COLOR_HEADERS.length).setValues([COLOR_HEADERS]); sh.setFrozenRows(1); sh.getRange('A:A').setNumberFormat('@'); } return sh; } /* ---------- 一覧(キット) ---------- */ function list_(sh) { const last = sh.getLastRow(); if (last < 2) return []; const rows = sh.getRange(2, 1, last - 1, HEADERS.length).getValues(); return rows .filter(function (r) { return r[0] !== ''; }) .map(function (r) { return { id: String(r[0]), grade: r[1], name: r[2], shop: r[3], status: r[4], createdAt: fmt_(r[5]), updatedAt: fmt_(r[6]) }; }); } /* ---------- 追加(キット) ---------- */ function add_(sh, p) { const v = validate_(p); if (v) return { ok: false, error: v }; const id = nextId_(sh); const now = new Date(); sh.appendRow([id, p.grade, p.name.trim(), (p.shop || '').trim(), p.status, now, now]); return { ok: true, id: id }; } /* ---------- 更新(キット) ---------- */ function update_(sh, p) { const v = validate_(p); if (v) return { ok: false, error: v }; const row = findRow_(sh, p.id); if (!row) return { ok: false, error: '対象のキットが見つかりません' }; sh.getRange(row, 2, 1, 4).setValues([[p.grade, p.name.trim(), (p.shop || '').trim(), p.status]]); sh.getRange(row, 7).setValue(new Date()); return { ok: true }; } /* ---------- 削除(キット) ---------- */ function delete_(sh, p) { const row = findRow_(sh, p.id); if (!row) return { ok: false, error: '対象のキットが見つかりません' }; sh.deleteRow(row); return { ok: true }; } /* ---------- 一覧(カラー) ---------- */ function colorList_(sh) { const last = sh.getLastRow(); if (last < 2) return []; const rows = sh.getRange(2, 1, last - 1, COLOR_HEADERS.length).getValues(); return rows .filter(function (r) { return r[0] !== ''; }) .map(function (r) { return { id: String(r[0]), maker: r[1], name: r[2], code: r[3], qty: r[4], status: r[5], createdAt: fmt_(r[6]), updatedAt: fmt_(r[7]) }; }); } /* ---------- 追加(カラー) ---------- */ function colorAdd_(sh, p) { const v = validateColor_(p); if (v) return { ok: false, error: v }; const id = nextColorId_(sh); const now = new Date(); sh.appendRow([id, p.maker.trim(), p.name.trim(), (p.code || '').trim(), qtyOf_(p.qty), p.status, now, now]); return { ok: true, id: id }; } /* ---------- 更新(カラー) ---------- */ function colorUpdate_(sh, p) { const v = validateColor_(p); if (v) return { ok: false, error: v }; const row = findRow_(sh, p.id); if (!row) return { ok: false, error: '対象の色が見つかりません' }; sh.getRange(row, 2, 1, 5).setValues([[p.maker.trim(), p.name.trim(), (p.code || '').trim(), qtyOf_(p.qty), p.status]]); sh.getRange(row, 8).setValue(new Date()); return { ok: true }; } /* ---------- 削除(カラー) ---------- */ function colorDelete_(sh, p) { const row = findRow_(sh, p.id); if (!row) return { ok: false, error: '対象の色が見つかりません' }; sh.deleteRow(row); return { ok: true }; } /* ---------- 補助 ---------- */ function validate_(p) { if (!p.name || !String(p.name).trim()) return 'キット名を入力してください'; if (GRADES.indexOf(p.grade) === -1) return 'グレードが正しくありません'; if (STATUSES.indexOf(p.status) === -1) return '状態が正しくありません'; if (String(p.name).length > 200) return 'キット名は200文字以内にしてください'; return null; } function validateColor_(p) { if (!p.maker || !String(p.maker).trim()) return 'メーカーを入力してください'; if (!p.name || !String(p.name).trim()) return '色名を入力してください'; if (COLOR_STATUSES.indexOf(p.status) === -1) return '状態が正しくありません'; if (String(p.maker).length > 100) return 'メーカーは100文字以内にしてください'; if (String(p.name).length > 100) return '色名は100文字以内にしてください'; if (p.code && String(p.code).length > 50) return '型式番号は50文字以内にしてください'; return null; } function qtyOf_(v) { const n = parseInt(v, 10); return isNaN(n) || n < 0 ? 0 : n; } // ID は K0001 のように英字を頭に付ける(先頭ゼロ消え対策) function nextId_(sh) { const last = sh.getLastRow(); let max = 0; if (last >= 2) { sh.getRange(2, 1, last - 1, 1).getValues().forEach(function (r) { const m = /^K(\d+)$/.exec(String(r[0])); if (m) max = Math.max(max, parseInt(m[1], 10)); }); } return 'K' + ('0000' + (max + 1)).slice(-4); } // ID は C0001 のように英字を頭に付ける(キットの K と区別) function nextColorId_(sh) { const last = sh.getLastRow(); let max = 0; if (last >= 2) { sh.getRange(2, 1, last - 1, 1).getValues().forEach(function (r) { const m = /^C(\d+)$/.exec(String(r[0])); if (m) max = Math.max(max, parseInt(m[1], 10)); }); } return 'C' + ('0000' + (max + 1)).slice(-4); } function findRow_(sh, id) { const last = sh.getLastRow(); if (last < 2 || !id) return 0; const ids = sh.getRange(2, 1, last - 1, 1).getValues(); for (let i = 0; i < ids.length; i++) { if (String(ids[i][0]) === String(id)) return i + 2; } return 0; } function fmt_(d) { if (!(d instanceof Date)) return ''; return Utilities.formatDate(d, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm'); } function json_(obj) { return ContentService.createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); }