0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サークルの作品や活動記録をGASで管理できるようにした

Posted at

私はサークルのホームページを作成していて、メンバーの作品や活動記録をどんどん追加していく中で、「もっと効率的に管理できないだろうか?」と感じるようになりました。

特に、メンバー数や活動内容が増えてくると、更新作業に時間がかかったり、次の代への引き継ぎが難しくなったりします。

そこで、Google Apps Script(GAS)を使って、作品や活動記録をスプレッドシートで管理し、ホームページと連携できる仕組みを実装してみることにしました。

今回は、GASを使ってgoogleスプレットシートをデータベース代わりにし、動的にページを生成する方法を書いていこうと思います。

目次

  1. プロジェクト概要(作品管理・活動記録管理について)

  2. スプレッドシートの構成とデータ構造

  3. GASのWebアプリ設定と表示

  4. まとめ

1.プロジェクト概要

今回書く内容として、技術系サークル等では作品を作るのでそれが見れるように作品一覧と活動記録が見れるようにしたプロダクトになります。

作品投稿の内容として

タイトル、詳細、作者、カテゴリー、作成年度、使用技術、作品URL、作品写真URL

活動記録の内容として

タイトル、詳細、カテゴリー、実施日、参加人数、詳細URL、写真URL

といった内容で作成しました。

2.スプレッドシートの構成とデータ構造

まず最初にスプレットシートを作成します。名前は自分でわかりやすいもので
作品用と活動記録用の二つのシートを作成します。

次にスプレットシートの準備を行います

作品
スクリーンショット 2025-10-18 12.09.42.png

活動記録
スクリーンショット 2025-10-18 12.09.54.png

idはなくても大丈夫です。
内容は自分の使用したい項目、順番で大丈夫です。

3.GASのWebアプリ設定と表示

次にGASのコードを書いていきます。
スクリーンショット 2025-10-18 12.00.32.png

Apps Scriptを押します。

スクリーンショット 2025-10-18 12.25.20.png

こちらのコードに書いていきます。

書く内容として

.gs
// 作品用スプレッドシートのID
const WORKS_SPREADSHEET_ID = '';

// 活動記録用スプレッドシートのID
const ACTIVITIES_SPREADSHEET_ID = '';

//=====================================================
// メイン関数: HTTPリクエストを処理
// =====================================================

/**
 * GETリクエストを処理してJSONを返す
 */
function doGet(e) {
  try {
    const type = e.parameter.type; // 'works' or 'activities'
    
    if (!type) {
      return createErrorResponse('type parameter is required. Use ?type=works or ?type=activities');
    }
    
    let data;
    if (type === 'works') {
      data = getWorks();
    } else if (type === 'activities') {
      data = getActivities();
    } else {
      return createErrorResponse('Invalid type parameter. Use "works" or "activities"');
    }
    
    return createJsonResponse(data);
    
  } catch (error) {
    return createErrorResponse(error.toString());
  }
}
function convertDriveLinkToDirect(url) {
  if (!url) return '';
  const match = url.match(/\/d\/(.*)\//);
  if (match && match[1]) {
    return `https://drive.google.com/uc?export=view&id=${match[1]}`;
  }
  return url;
}

// =====================================================
// データ取得関数
// =====================================================

/**
 * 作品データを取得
 */
function getWorks() {
  const ss = SpreadsheetApp.openById(WORKS_SPREADSHEET_ID);
  const sheet = ss.getSheets()[0];
  const data = sheet.getDataRange().getValues();
  if (data.length <= 1) return [];

  const works = [];
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (!row[0] && !row[1]) continue;

    const work = {
      id: row[0] || i,
      title: row[1] || '',
      artist: row[2] || '',
      category: row[3] || '',
      year: row[4] || '',
      description: row[5] || '',
      technology: row[6] || '',
      works: row[7] || '',
      image: convertDriveLinkToDirect(row[8] || ''),
    };
    works.push(work);
  }

  // ✅ 年で降順ソート(2025 → 2024 → ...)
  works.sort((a, b) => Number(b.year) - Number(a.year));

  return works;
}


/**
 * 活動記録データを取得
 */
function getActivities() {
  const ss = SpreadsheetApp.openById(ACTIVITIES_SPREADSHEET_ID);
  const sheet = ss.getSheets()[0];
  const data = sheet.getDataRange().getValues();
  if (data.length <= 1) return [];

  const activities = [];
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (!row[0] && !row[1]) continue;

    const activity = {
      id: row[0] || i,
      year: row[1] || '', // "2024/12/12" のような形式
      title: row[2] || '',
      category: row[3] || '',
      description: row[4] || '',
      participants: row[5] || 0,
      active: row[6] || '',
      image: convertDriveLinkToDirect(row[7] || ''),
    };
    activities.push(activity);
  }

  // ✅ 日付で降順ソート(新しい日付が先)
  activities.sort((a, b) => {
    const dateA = new Date(a.year);
    const dateB = new Date(b.year);
    return dateB - dateA;
  });

  return activities;
}



// =====================================================
// レスポンス作成関数
// =====================================================

/**
 * JSON形式のレスポンスを作成
 */
function createJsonResponse(data) {
  const output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({
    success: true,
    data: data,
    count: data.length
  }));
  return output;
}

/**
 * エラーレスポンスを作成
 */
function createErrorResponse(message) {
  const output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({
    success: false,
    error: message
  }));
  return output;
}

まず

.gs
// 作品用スプレッドシートのID
const WORKS_SPREADSHEET_ID = '';

// 活動記録用スプレッドシートのID
const ACTIVITIES_SPREADSHEET_ID = '';

このスプレットシートのIDは 

スクリーンショット 2025-10-18 12.32.45.png

https://docs.google.com/spreadsheets/d/ここがID/edit
この部分を入れ込みます

メイン関数

.gs
function doGet(e) {
  try {
    const type = e.parameter.type; // 'works' か 'activities'

    if (!type) {
      return createErrorResponse('type parameter is required. Use ?type=works or ?type=activities');
    }

    let data;
    if (type === 'works') {
      data = getWorks();
    } else if (type === 'activities') {
      data = getActivities();
    } else {
      return createErrorResponse('Invalid type parameter. Use "works" or "activities"');
    }

    return createJsonResponse(data);

  } catch (error) {
    return createErrorResponse(error.toString());
  }
}

  • リクエストを受け取りデータを変える役割
  • e.parameter.type でリクエストURLのクエリパラメータを取得
    https://script.google.com/.../exec?type=works
  • type が works なら作品データ、activities なら活動記録データを取得
  • エラーが発生した場合、エラーメッセージを返す

Googleドライブのリンクを直接表示用に変換

.gs
function convertDriveLinkToDirect(url) {
  if (!url) return '';
  const match = url.match(/\/d\/(.*)\//);
  if (match && match[1]) {
    return `https://drive.google.com/uc?export=view&id=${match[1]}`;
  }
  return url;
}

  • スプレッドシートに入っているGoogleドライブの共有リンクを、Web上で直接表示できる形式に変換

データ取得関数(works)

.gs
function getWorks() {
  const ss = SpreadsheetApp.openById(WORKS_SPREADSHEET_ID);
  const sheet = ss.getSheets()[0];
  const data = sheet.getDataRange().getValues();
  if (data.length <= 1) return [];

  const works = [];
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (!row[0] && !row[1]) continue;

    const work = {
      id: row[0] || i,
      title: row[1] || '',
      artist: row[2] || '',
      category: row[3] || '',
      year: row[4] || '',
      description: row[5] || '',
      technology: row[6] || '',
      works: row[7] || '',
      image: convertDriveLinkToDirect(row[8] || ''),
    };
    works.push(work);
  }

  // 年で降順ソート(2025 → 2024 )
  works.sort((a, b) => Number(b.year) - Number(a.year));

  return works;
}
  • スプレッドシートの1行目をヘッダーとみなし、2行目以降をデータとして処理
  • row[0] は ID、row[1] はタイトル、row[8] は画像URL
  • year で降順ソート → 最新の作品が先頭に

データ取得関数(activities)

.gs
function getActivities() {
  const ss = SpreadsheetApp.openById(ACTIVITIES_SPREADSHEET_ID);
  const sheet = ss.getSheets()[0];
  const data = sheet.getDataRange().getValues();
  if (data.length <= 1) return [];

  const activities = [];
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (!row[0] && !row[1]) continue;

    const activity = {
      id: row[0] || i,
      year: row[1] || '', // "2024/12/12" のような形式
      title: row[2] || '',
      category: row[3] || '',
      description: row[4] || '',
      participants: row[5] || 0,
      active: row[6] || '',
      image: convertDriveLinkToDirect(row[7] || ''),
    };
    activities.push(activity);
  }

  // 日付で降順ソート(新しい日付が先)
  activities.sort((a, b) => {
    const dateA = new Date(a.year);
    const dateB = new Date(b.year);
    return dateB - dateA;
  });

  return activities;
}

作品と同じ感じ

JSONレスポンス作成

.gs
function createJsonResponse(data) {
  const output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({
    success: true,
    data: data,
    count: data.length
  }));
  return output;
}

  • データをJSONに変換して返す

次にスプレットシートの共有

スプレットシートの共有を押して
スクリーンショット 2025-10-19 15.51.10.png
リンクを知っている全員にして 
閲覧者にしたら大丈夫です。

Webページに表示

Typescriptで書いています。
まずgasのデータを取得するコードを書いていきます。api.tsとします

// Google DriveのリンクをWebビューアブルな直接リンクに変換
export function convertDriveLinkToDirect(url: string): string {
  if (!url) return '';

  // 既に直接リンクの形式なら、そのまま返す
  if (url.includes('drive.google.com/uc?')) {
    return url;
  }

  // 共有リンクからファイルIDを抽出
  // 形式: https://drive.google.com/file/d/FILE_ID/view?usp=sharing
  const match = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);

  if (match && match[1]) {
    const fileId = match[1];
    return `https://drive.google.com/uc?export=view&id=${fileId}`;
  }

  // マッチしない場合は元のURLを返す
  return url;
}

// 作品カテゴリーの定義
export type WorkCategory = 'モバイルアプリ' | 'Webアプリ' | 'ゲーム' | 'イラスト' | '';

// 活動カテゴリーの定義
export type ActivityCategory = 'モクモク会' | 'ハッカソン' | '展示会' | '懇親会' | 'イベント';

export interface Work {
  id: number;
  title: string;
  artist: string;
  category: WorkCategory | string; // string も許可(既存データとの互換性のため)
  year: string;
  description: string;
  technology: string;
  works: string;
  image: string;
}

export interface Activity {
  id: number;
  year: string;
  title: string;
  category: ActivityCategory | string; // string も許可(既存データとの互換性のため)
  description: string;
  participants: number;
  active: string;
  image: string;
}

const GAS_API_URL = process.env.NEXT_PUBLIC_GAS_API_URL; //apiキーを設定

// フォールバックデータ
const fallbackWorks: Work[] = [
  {
    id: 1,
    title: "タスク管理アプリ",
    artist: "田中 太郎",
    category: "モバイルアプリ",
    year: "2024",
    description: "タスク管理アプリを作成しました",
    technology: "React Native, TypeScript",
    works: "https://example.com/works/1",
    image: ""
  },
 
];

const fallbackActivities: Activity[] = [
  {
    id: 1,
    year: "2024",
    title: "作品展示会",
    category: "展示会",
    description: "展示会を開きました",
    participants: 35,
    active: "https://example.com/activities/1",
    image: ""
  },
  
];

export async function getWorksFromGAS(): Promise<Work[]> {
  // 環境変数が設定されていない場合はフォールバックデータを返す
  if (!GAS_API_URL) {
    console.warn('GAS_API_URLがありません');
    return fallbackWorks;
  }

  try {
    const response = await fetch(`${GAS_API_URL}?type=works`, {
      next: { revalidate: 3600 } // 1時間キャッシュ
    });

    if (!response.ok) {
      throw new Error(`: ${response.status} ${response.statusText}`);
    }

    const result = await response.json();
    console.log('Works:', result);

    // GASのレスポンス形式: {success: true, data: [...], count: N}
    if (result.success && result.data && result.data.length > 0) {
      return result.data;
    } else {
      console.warn('フォールバックを返す');
      return fallbackWorks;
    }
  } catch (error) {
    console.error('Error :', error);
    return fallbackWorks;
  }
}

export async function getActivitiesFromGAS(): Promise<Activity[]> {
  // 環境変数が設定されていない場合はフォールバックデータを返す
  if (!GAS_API_URL) {
    console.warn('フォールバックを返す');
    return fallbackActivities;
  }

  try {
    const response = await fetch(`${GAS_API_URL}?type=activities`, {
      next: { revalidate: 3600 } // 1時間キャッシュ
    });

    if (!response.ok) {
      throw new Error(`activities: ${response.status} ${response.statusText}`);
    }

    const result = await response.json();
    console.log('Activities:', result);

    // GASのレスポンス形式: {success: true, data: [...], count: N}
    if (result.success && result.data && result.data.length > 0) {
      return result.data;
    } else {
      console.warn('フォールバックを返す');
      return fallbackActivities;
    }
  } catch (error) {
    console.error('Error:', error);
    return fallbackActivities;
  }
}

環境変数はlocalで見るならenvでいいですが、デプロイしているならホスティングしているところで環境変数を入れておきましょう。
あとは表示するだけです。(コードは省略します)

3 まとめ

今回はGASを使うことにより簡単にデータを管理、表示ができる仕組みを書いていきました。

この方法のいいところは

  • サーバーがいらずにできること
  • めんどくさい引き継ぎが簡単

今後はもっとパフォーマンスを上げるための取り組みを行なっていこうと思います。

GASは簡単にデータを管理できるのでみなさん使ってみてください。


💡おまけ
もし記事の内容に間違いがあったり、「こうした方がいい!」という意見があれば、
ぜひコメントで教えていただけると嬉しいです!
より良い仕組みにアップデートしていけたらと思います 🙌

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?