47
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

9割のエンジニア未経験者がつまずく『最初の壁』 。それでもアプリを作りたい #2

47
Last updated at Posted at 2026-06-08

はじめまして。株式会社PRUMでエンジニアをしている人見です。

日々、プログラミング学習や実務の中で、つまずきやすいポイントや、
仕事で起きやすい“ズレ”について整理して発信しています。
誰かの助けになれば幸いです。

9割のエンジニア未経験者がつまずく『最初の壁』 それでもアプリを作りたい -30分でアプリを作ってみよう -

image.png

はじめに

前回は、「そもそもアプリって何だろう?」
という話をしました。今回は実際にアプリを作ってみます。
とはいっても、

  • Javaのインストール
  • Pythonのインストール
  • Docker
  • AWS

みたいな難しい準備はありません。
Googleアカウントさえあれば大丈夫です。
まずは、「自分でもアプリを作れた!」
という体験をしてみましょう。

今回作るもの

今回作るのはこちらです。

  • 参加する
  • 不参加にする

を登録できる簡単なアプリです。
登録された内容はスプレッドシートに保存されます。

完成イメージ
【完成画面キャプチャ】
image.png

STEP1 スプレッドシートを作成する

まずはGoogleを開きます。

【画像① Googleホーム画面】右上のアプリ一覧を選択します。
image.png

【画像② アプリ一覧】 スプレッドシートを選択します。
image.png

【画像③ スプレッドシートホーム】「空白」を選択します。
image.png

【画像④ 空白シート】
image.png

STEP2 スプレッドシートに名前を付ける

【画像⑤ タイトル変更】 参加管理アプリ としてみます。
image.png

STEP3 Apps Scriptを開く

上部メニューから

拡張機能 → Apps Script

を選択します。

【画像⑥】
image.png

すると以下のような画面が表示されます。

【画像⑦】
image.png

STEP4 プログラムを書く

【画像⑧】最初から入っているコードを削除して、以下のコードを貼り付けます。
image.png

Code.gs ← 貼り付けるコードはコチラ

ここをクリックすると展開されます
const SHEET_NAME_RESPONSES = '回答一覧';
const SHEET_NAME_MEMBERS = 'メンバー一覧';
const EVENT_NAME = '本日の朝会';

function doGet() {
  setupSheets_();

  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('参加・不参加確認アプリ');
}

function submitAnswer(data) {
  setupSheets_();

  const name = String(data.name || '').trim();
  const status = String(data.status || '').trim();
  const memo = String(data.memo || '').trim();

  if (!name) {
    throw new Error('名前を入力してください。');
  }

  if (status !== '参加' && status !== '不参加') {
    throw new Error('参加または不参加を選択してください。');
  }

  registerMemberIfNeeded_(name);
  upsertResponse_(name, status, memo);

  return getSummary();
}

function getSummary() {
  setupSheets_();

  const members = getMembers_();
  const responses = getLatestResponses_();

  const joined = [];
  const absent = [];
  const unanswered = [];

  members.forEach(name => {
    const response = responses[name];

    if (!response) {
      unanswered.push(name);
      return;
    }

    if (response.status === '参加') {
      joined.push({
        name: name,
        memo: response.memo,
        updatedAt: response.updatedAt
      });
      return;
    }

    if (response.status === '不参加') {
      absent.push({
        name: name,
        memo: response.memo,
        updatedAt: response.updatedAt
      });
      return;
    }
  });

  return {
    eventName: EVENT_NAME,
    total: members.length,
    answered: joined.length + absent.length,
    joinedCount: joined.length,
    absentCount: absent.length,
    unansweredCount: unanswered.length,
    joined: joined,
    absent: absent,
    unanswered: unanswered,
    generatedAt: Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd HH:mm')
  };
}

function sendSlackSummary() {
  const webhookUrl = PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK_URL');

  if (!webhookUrl) {
    throw new Error('スクリプト プロパティに SLACK_WEBHOOK_URL が設定されていません。');
  }

  const summary = getSummary();
  const message = buildSlackMessage_(summary);

  const payload = {
    text: message
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(webhookUrl, options);
  const statusCode = response.getResponseCode();

  if (statusCode < 200 || statusCode >= 300) {
    throw new Error('Slack通知に失敗しました。status=' + statusCode + ' body=' + response.getContentText());
  }
}

function createDailySlackTrigger() {
  deleteDailySlackTriggers_();

  ScriptApp.newTrigger('sendSlackSummary')
    .timeBased()
    .everyDays(1)
    .atHour(8)
    .create();
}

function deleteDailySlackTriggers_() {
  const triggers = ScriptApp.getProjectTriggers();

  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'sendSlackSummary') {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

function buildSlackMessage_(summary) {
  const joinedNames = summary.joined.length > 0
    ? summary.joined.map(item => '' + item.name).join('\n')
    : 'なし';

  const absentNames = summary.absent.length > 0
    ? summary.absent.map(item => '' + item.name).join('\n')
    : 'なし';

  const unansweredNames = summary.unanswered.length > 0
    ? summary.unanswered.map(name => '' + name).join('\n')
    : 'なし';

  return [
    '【参加確認】' + summary.eventName,
    '',
    '現在 ' + summary.total + '人中 ' + summary.answered + '人が回答済みです。',
    '',
    '参加:' + summary.joinedCount + '',
    '不参加:' + summary.absentCount + '',
    '未回答:' + summary.unansweredCount + '',
    '',
    '■ 参加',
    joinedNames,
    '',
    '■ 不参加',
    absentNames,
    '',
    '■ 未回答',
    unansweredNames,
    '',
    '未回答の方は、参加確認をお願いします。'
  ].join('\n');
}

function setupSheets_() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

  let responsesSheet = spreadsheet.getSheetByName(SHEET_NAME_RESPONSES);
  if (!responsesSheet) {
    responsesSheet = spreadsheet.insertSheet(SHEET_NAME_RESPONSES);
    responsesSheet.appendRow(['更新日時', '名前', '回答', 'メモ']);
  }

  let membersSheet = spreadsheet.getSheetByName(SHEET_NAME_MEMBERS);
  if (!membersSheet) {
    membersSheet = spreadsheet.insertSheet(SHEET_NAME_MEMBERS);
    membersSheet.appendRow(['名前']);
    membersSheet.appendRow(['佐藤']);
    membersSheet.appendRow(['鈴木']);
    membersSheet.appendRow(['田中']);
    membersSheet.appendRow(['山田']);
    membersSheet.appendRow(['高橋']);
  }
}

function getMembers_() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_MEMBERS);
  const values = sheet.getDataRange().getValues();

  return values
    .slice(1)
    .map(row => String(row[0] || '').trim())
    .filter(name => name);
}

function getLatestResponses_() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_RESPONSES);
  const values = sheet.getDataRange().getValues();
  const responses = {};

  values.slice(1).forEach(row => {
    const updatedAt = row[0];
    const name = String(row[1] || '').trim();
    const status = String(row[2] || '').trim();
    const memo = String(row[3] || '').trim();

    if (!name) {
      return;
    }

    responses[name] = {
      status: status,
      memo: memo,
      updatedAt: Utilities.formatDate(new Date(updatedAt), 'Asia/Tokyo', 'yyyy/MM/dd HH:mm')
    };
  });

  return responses;
}

function registerMemberIfNeeded_(name) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_MEMBERS);
  const members = getMembers_();

  if (!members.includes(name)) {
    sheet.appendRow([name]);
  }
}

function upsertResponse_(name, status, memo) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_RESPONSES);
  const values = sheet.getDataRange().getValues();

  for (let i = 1; i < values.length; i++) {
    const rowName = String(values[i][1] || '').trim();

    if (rowName === name) {
      const rowNumber = i + 1;
      sheet.getRange(rowNumber, 1, 1, 4).setValues([[
        new Date(),
        name,
        status,
        memo
      ]]);
      return;
    }
  }

  sheet.appendRow([
    new Date(),
    name,
    status,
    memo
  ]);
}

STEP5 HTMLファイルを作る

【画像9】 左上の「+」ボタンから、「HTML」を選択し、HTMLファイルを作成します。
image.png

【画像⑩】 ファイル名は index にします。
image.png

【画像⑪】最初から入っているコードを削除して、
以下のコードを貼り付けます。
image.png

index.html ← 貼り付けるコードはコチラ

ここをクリックすると展開されます ```html

参加・不参加確認アプリ

ボタンを押すだけで参加確認ができるGASデモアプリです。
回答はスプレッドシートに保存され、集計結果をSlackに自動通知できます。

<section class="grid">
  <div class="card">
    <h2>回答する</h2>

    <label for="name">名前</label>
    <input id="name" type="text" placeholder="例:佐藤">

    <label for="memo" style="margin-top: 16px;">メモ</label>
    <textarea id="memo" placeholder="任意:少し遅れます、午前だけ参加など"></textarea>

    <div class="button-row">
      <button id="joinButton" class="join" onclick="submitAnswer('参加')">参加</button>
      <button id="absentButton" class="absent" onclick="submitAnswer('不参加')">不参加</button>
    </div>

    <div id="status" class="status"></div>
  </div>

  <div class="card">
    <h2>現在の集計</h2>
    <div id="summaryArea">
      読み込み中です...
    </div>
    <button class="reload" onclick="loadSummary()">最新に更新</button>
  </div>

  <div class="card full">
    <h2>回答一覧</h2>
    <div id="listArea">
      読み込み中です...
    </div>
  </div>
</section>
```
---

STEP6 Webアプリとして公開する

【画像⑫】 右上の「デプロイ」 → 「新しいデプロイ」 を選択します。
image.png

【画像⑬】 種類は 「ウェブアプリ」 を選択します。
image.png

【画像⑭】 以下の設定にし、「デプロイ」ボタンをクリック
image.png

googleが許可を求める 設定があるので、各自調べて実行してください。
image.png

【画像⑮】 ウェブアプリのURLが表示されたら完成
image.png

STEP8 URLを開く

【画像⑯】発行されたURLを開きます。
image.png

【完成!】
これでアプリが完成しました。
今作ったものも立派なWebアプリです。

なぜアプリになったの?

image.png

不思議ですよね。私たちは

  • サーバーを契約していない
  • Javaもインストールしていない
  • AWSも使っていない

それなのに、インターネットからアクセスできるアプリができました。
実はGoogleが

  • サーバー
  • 実行環境
  • 公開機能

を用意してくれているからです。

次回予告

今回は 「画面を表示する」ところまでできました。
やっぱり実際に動かした瞬間が一番楽しいですよね!!

次回は、これのどこがアプリなのかについて触れるとともに、
アプリ作りの楽しい部分に触れていきましょう。


PRUMのエンジニアの95%以上は未経験からの採用です。
もし弊社にご興味あれば覗いてみてくださいね。

コーポレートサイト

47
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?