私は、友達の紹介でさどんでこプロジェクトのIT担当として様々システム構築などを行っています。今回、スタンプラリー作成でかなり大変だったということから、開発秘話をブログにまとめてみました。興味ある方ぜひ御覧ください。

第1,2話は以下です、合わせてお読みください。


今回のテーマ
- スタンプラリーの“スポット”情報を管理するためのDjango管理画面
- Google Mapsと連携して地図上で位置を登録・編集する仕組み
- スタンプの近接や重複をチェックするUIの工夫
スタンプの緯度・経度を簡単に設定できるUIを目指して
LINEでスタンプを押してもらうには、「この場所に来たらスタンプを獲得できる」=位置情報の判定が必要です。
つまり、「スタンプごとに緯度・経度を持たせる必要がある」ということ。
ですが、管理者(多くは非エンジニア)がそれを扱うには、Djangoの管理画面で数値を直接入力させるのではなく、地図上で直感的にポチッと登録できるUIが求められました。
Google Maps × Django 管理画面をカスタム実装
そこで導入したのが、Google Maps JavaScript API を使った Django 管理画面のカスタマイズ。
実装ポイント
- Djangoモデルに緯度(latitude)・経度(longitude)・有効範囲(radius_m)フィールドを追加
- 管理画面テンプレートでGoogle Mapsを読み込み、地図を表示
- マーカーをクリックで移動 → 緯度経度を即座にフィールドへ反映
- 保存時にモデルに反映
class StampSpot(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
latitude = models.FloatField()
longitude = models.FloatField()
radius_m = models.FloatField(default=100)
image = models.ImageField(upload_to="stamps/", blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
① input要素の取得と初期値の設定
const latInput = document.getElementById("id_latitude");
const lngInput = document.getElementById("id_longitude");
const radiusInput = document.getElementById("id_radius_m");
これはDjangoモデルのフィールドに対応するinput要素(<input id=”id_latitude”>など)を取得しています。
const initialLat = parseFloat(latInput.value) || 38.2;
ユーザーが未入力の場合でも地図が出るようにデフォルト値(佐渡の中心付近)を設定。
② Google Map & マーカー & 円を描画
const map = new google.maps.Map(...);
const marker = new google.maps.Marker(...);
const circle = new google.maps.Circle(...);
- markerはドラッグ可能
- circleはスタンプの有効範囲を視覚化
- circle.centerとmarker.positionは連動させている
③ 地図クリック → マーカー&円を移動
map.addListener("click", (e) => {
marker.setPosition(e.latLng);
circle.setCenter(e.latLng);
updateInputs(e.latLng);
});
- 管理者が地図をクリックすればその位置が「スポットの中心」になる
- クリックするだけでinput欄の緯度・経度もリアルタイムで更新
④ マーカーをドラッグ → 円の位置を追従
marker.addListener("dragend", (e) => {
circle.setCenter(e.latLng);
updateInputs(e.latLng);
});
- マーカーを直接動かしても、その動きをcircleとinput欄に反映
- ユーザーがどちらの操作でも違和感なく使える
⑤ 半径の変更 → 円のサイズ更新
radiusInput.addEventListener("input", () => {
const newRadius = parseFloat(radiusInput.value);
if (!isNaN(newRadius)) {
circle.setRadius(newRadius);
}
});
- 半径(radius_m)の数値を入力すれば、円のサイズも即時更新
- フォーム側と地図が双方向に連動している
実際の完成した管理画面
- 地図を見ながら「このへん」と直感的にクリック
- 有効半径も表示されるので調整しやすい
- 座標入力欄に数値がリアルタイムで反映

重複チェックも大事な機能だった
鬼太鼓スタンプラリーには複数のコースが存在し、同じスタンプが別コースにも登場する構造があります。
そのため、同じスポットが重複登録されないよう、スタンプごとの近接チェックも実装しました。
チェック機能の仕組み
- スタンプ登録時、一定距離以内に同じIDのスポットがないか自動チェック
- 色付きマーカーで近接スポットを可視化
- 複数スポットが重なると警告を表示

スタンプスポットを一覧で表示する画面を追加し、それぞれの範囲を表示、範囲に重複が無いか確認が簡単になった
画像合成の土台としての座標管理
次にスタンプカードの上にスタンプを合成するに当たって、スタンプカード上のスタンプの位置を記録する必要があります。それを管理画面でマウスで操作することで簡単に設定できるようにしました。
直感的なスタンプ配置ができる管理画面
管理画面の中には、以下のような機能を詰め込みました:
- 拡大率(倍率)をコースごとに設定・反映
- スタンプカード画像をベースに表示
- プルダウンから追加したいスタンプ(スポット)を選択
- 画像上をクリックしてスタンプを追加
- スタンプをドラッグで移動、右クリックで削除
<img id="map-image" src="{{ course.image.url }}" />
<div class="marker" data-x="120" data-y="340" data-spot-id="7"></div>
JavaScriptで画像の縮尺に応じてマーカー位置をリアルタイムで反映。レスポンシブにも対応し、スマホでも操作可能な設計です。
管理画面で設定した内容:

現在のスタンプの位置を確認できる

マウスでスタンプは移動できる、位置は即時保存される
技術的ポイント(JS + Django連携)
成功・失敗をトースト通知(Bootstrap風)
画像の 実サイズと表示サイズの比率(displayScale) を取得し、ピクセル位置を変換
新規スタンプはクリック座標をPOST
移動時はPATCH、削除時はDELETEでAPI呼び出し
const displayScale = image.clientWidth / image.naturalWidth;
const x = (e.clientX - rect.left) / displayScale;
const y = (e.clientY - rect.top) / displayScale;
スタンプが表示されるサイズも 40 * 拡大率 * displayScale のように動的に設定され、すべてのスタンプが一貫した見た目になるよう調整しています。
管理画面の完成!
これで、スタンプの登録、スタンプの位置情報の登録、スタンプカードの登録、スタンプカード上のスタンプの位置の登録ができる管理画面が完成しました!

管理画面トップ

スタンプスポット一覧

スタンプコース一覧
次回予告:位置情報とLINEの連携!LIFFでスタンプを獲得する仕組み
いよいよ、LIFF(LINE Front-end Framework) の登場です。
- ユーザーが現在地をLINE内で送信できる仕組み
- 指定エリアに入ったときにだけスタンプを獲得できるようにする処理
- 合言葉の発行とLINE Botとのやり取り
“LINEでスタンプを獲得できる仕組み”の全貌を解説します!


コメント
コメント一覧 (1件)
[…] LINE API × 鬼太鼓スタンプラリー誕生秘話③ 座標管理とDjango管理画面 – … […]