📊 仕組み
運営が1つだけスプレッドシートを用意すれば、全チームの記入内容がチーム番号つきで自動的に集まります。生徒側の設定は不要です。種別ごとに見やすいシート(ビジネスモデル/ヒアリング準備/ヒアリング記録)へ自動で振り分けられます。
✅ チーム番号ごとに同じ行を上書きするので、何回保存しても行は増えません。
✅ 別の端末で同じチーム番号を入れれば、続きから編集できます(スプレッドシートから自動で読み戻し)。
※ヒアリング記録は「1人=1行」。同じ人を編集し直しても増えず、別の人を追加すると新しい行になります。
- sheets.new で新しいスプレッドシートを作成
- 「拡張機能」→「Apps Script」を開く
- 下のコードをすべてコピーして貼り付け→保存(Ctrl+S)
- 「デプロイ」→「新しいデプロイ」→種類「ウェブアプリ」
アクセスできるユーザー: 全員 に設定して「デプロイ」
- 表示された ウェブアプリURL をコピー
- このHTMLファイルを編集し、コード内の
const GAS_URL = '' の ''の中 にURLを貼り付けて保存
→ 全チームに同じHTMLを配れば、全員のデータが集まります
// ===== 設定:シートの列 =====
var SHEETS = {
bizModel: { name:'ビジネスモデル',
headers:['更新日時','チーム','お客さん','困りごと','提供するもの',
'収益モデル','収益モデル(その他)','価格','価格の根拠','競合','強み','_data'] },
hearingPrep: { name:'ヒアリング準備',
headers:['更新日時','チーム','対象','目的','仮説','質問リスト','_data'] },
hearing: { name:'ヒアリング記録',
headers:['更新日時','チーム','ヒアリングID','実施日','相手','属性','仮説の結果',
'質問と回答','印象的な言葉','気づき','モデル変更','変更内容','_data'] }
};
// ===== 保存(チーム番号ごとに同じ行を上書き。行は増えない)=====
function doPost(e) {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var body = JSON.parse(e.postData.contents);
var team = String(body.teamNo || '');
var d = body.data || {};
var now = new Date().toLocaleString('ja-JP');
var type = body.type;
if (type === 'bizModel') {
upsert_(ss, SHEETS.bizModel,
[now, team, d.target, d.problem, d.product, d.revenue,
d.revenueOther, d.price, d.priceReason, d.competitor, d.strength, JSON.stringify(d)],
function(r){ return String(r[1]) === team; });
} else if (type === 'hearingPrep') {
var qs = (d.questions || []).map(function(q,i){return (i+1)+'. '+q.text;}).join('\n');
upsert_(ss, SHEETS.hearingPrep,
[now, team, d.target, d.purpose, d.hypothesis, qs, JSON.stringify(d)],
function(r){ return String(r[1]) === team; });
} else if (type === 'hearing') {
var hid = String(d.id || '');
var ans = (d.answers || []).map(function(a,i){
return 'Q'+(i+1)+': '+a.qText+'\n→ '+a.text;}).join('\n\n');
upsert_(ss, SHEETS.hearing,
[now, team, hid, d.date, d.name, d.attr, d.hypResult, ans,
d.quote, d.insight, d.modelChange, d.modelChangeDetail, JSON.stringify(d)],
function(r){ return String(r[1])===team && String(r[2])===hid; });
} else if (type === 'hearingDelete') {
deleteRows_(ss, SHEETS.hearing,
function(r){ return String(r[1])===team && String(r[2])===String(d.id||''); });
}
return out_({status:'ok'}, null);
} catch(err) {
return out_({status:'error', msg:err.toString()}, null);
}
}
// ===== 読み戻し(チーム番号でデータを返す。別端末で続きから)=====
function doGet(e) {
var res = { bizModel:null, hearingPrep:null, hearings:[] };
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var team = String(e.parameter.team || '');
res.bizModel = readOne_(ss, SHEETS.bizModel, team);
res.hearingPrep = readOne_(ss, SHEETS.hearingPrep, team);
res.hearings = readMany_(ss, SHEETS.hearing, team);
} catch(err) { res.error = err.toString(); }
return out_(res, e.parameter.callback);
}
// ===== 共通処理 =====
function upsert_(ss, cfg, row, match) {
var sheet = ensure_(ss, cfg);
var data = sheet.getDataRange().getValues();
for (var i=1; i=1; i--) if (match(data[i])) sheet.deleteRow(i+1);
}
function readOne_(ss, cfg, team) {
var sheet = ss.getSheetByName(cfg.name); if (!sheet) return null;
var data = sheet.getDataRange().getValues(); var jc = cfg.headers.length-1;
for (var i=1;i