個人開発 / Web アプリ

オフライン待ち合わせアプリ

電波が届かない場所でも、「ここに集合」が伝わる

オフライン待ち合わせアプリ

Overview

プロジェクト情報

開発期間

2026.03

クライアント

個人開発

役割

設計・開発(フルスタック)

チーム構成

1名

Outline

概要

「迷ったらここに集合しよう」を地図上のピン一つで伝えるための待ち合わせ Web アプリ。待ち合わせ場所は電波が不安定なことも多く、既存の地図アプリは一番必要なタイミングで開けなくなる。本アプリはオンラインのうちに地図タイルと目的地を端末へ保存し、通信できない状況でも全員が同じ地図とピンを確認できる状態を担保する。企画から設計・実装・デプロイまでを一人で担当した。

Objectives

目的・背景

開発背景

待ち合わせは、電話や文字で場所を説明するより地図上のピン一つで伝えるほうが確実だと感じていた。一方で、現地が圏外や電波の弱い環境だと地図そのものが開けず、結局は口頭案内に逆戻りしてしまう。「現地では通信できない」を例外ではなく前提条件として設計に組み込み、事前ダウンロード方式のシンプルな待ち合わせアプリとして開発した。

届けたいユーザー

スマホ操作に慣れていない友人・家族も含む幅広いユーザー。オフライン対応は事前準備を伴うため、その準備自体でつまずくと機能そのものが届かない。難しい操作なしに地図と目的地へ到達できることを最優先に設計した。

Key Features

主な特長

1

地図・目的地のオフライン対応

Service Worker で fetch をインターセプトし、オンライン時に地図タイルと目的地情報を IndexedDB へ保存。電波のない環境でもダウンロード済みの地図とピンをそのまま閲覧できる。オンライン/オフラインで描画経路を分岐させず、通信状態に関わらず一貫した表示になるよう設計した。

地図・目的地のオフライン対応
2

ワンタップで地図をダウンロード

用意された地図をタップするだけで端末に保存できる。範囲選択やズーム調整といった判断をユーザーに求めず、操作に不慣れな人でも準備を最後まで完了できる導線にした。

3

目的地のマーキング・URL 共有

事前に目的地をピンで登録し、URL で共有できる。受け取った側はリンクをタップするだけで同じ地図・目的地を確認・保存でき、アプリのインストールや会員登録を挟まない。カテゴリはアイコンで表現し、文字を読まずに識別できるようにした。

Tech Stack

技術スタック

ReactTypeScriptMapLibre GL JSService WorkerIndexedDBSupabaseFirebase HostingGitHub Actions

アーキテクチャ図

architecture

フォルダ構成

ディレクトリ構成を見る
.
├── assets
│   └── icon
│       ├── attraction.svg
│       ├── cafe.svg
│       ├── fastfood.svg
│       ├── index.ts
│       ├── restaurant.svg
│       ├── shop.svg
│       └── toilet.svg
├── components
│   └── ui
│       ├── Header.tsx
│       └── Layout.tsx
├── config
│   └── mapStyle
│       ├── constants.ts
│       ├── disneylandMapStyle.ts
│       ├── index.ts
│       ├── mabashiMapStyle.ts
│       └── types.ts
├── features
│   ├── home
│   │   ├── components
│   │   │   ├── __tests__
│   │   │   │   └── Home.spec.tsx
│   │   │   ├── CacheClearSection.tsx
│   │   │   ├── Card.tsx
│   │   │   ├── Cards.tsx
│   │   │   ├── Home.tsx
│   │   │   ├── HomeHeader.tsx
│   │   │   ├── HomeLoading.tsx
│   │   │   └── NoData.tsx
│   │   └── hooks
│   │       ├── __tests__
│   │       │   └── useHome.test.ts
│   │       └── useHome.ts
│   └── map
│       ├── application
│       │   ├── DestinationMarkerService.ts
│       │   ├── IDestinationMarker.ts
│       │   ├── IDestinationMarkerFactory.ts
│       │   ├── IMap.ts
│       │   └── MarkerStoreActions.ts
│       ├── components
│       │   ├── Map.tsx
│       │   ├── MapLoading.tsx
│       │   └── MapNotFound.tsx
│       ├── domains
│       │   ├── entities
│       │   │   ├── __tests__
│       │   │   │   ├── Destination.test.ts
│       │   │   │   └── Map.test.ts
│       │   │   ├── Destination.ts
│       │   │   ├── DestinationMarker.ts
│       │   │   └── Map.ts
│       │   ├── repositories
│       │   │   ├── DestinationRepository.ts
│       │   │   └── MapRepository.ts
│       │   └── valueObjects
│       │       ├── __tests__
│       │       │   └── LngLat.test.ts
│       │       └── LngLat.ts
│       ├── hooks
│       │   ├── __tests__
│       │   │   └── useMapEvent.test.tsx
│       │   └── useMapEvent.ts
│       ├── infrastructure
│       │   ├── __tests__
│       │   │   └── SupabaseMapRepository.test.ts
│       │   ├── maplibre
│       │   │   ├── DestinationMarkerFactory.ts
│       │   │   ├── MapFactory.ts
│       │   │   ├── MaplibreDestinationMarker.ts
│       │   │   ├── MaplibreMap.ts
│       │   │   └── marker.module.css
│       │   └── supabase
│       │       ├── destinationMapper.ts
│       │       ├── mapMapper.ts
│       │       ├── SupabaseDestinationRepository.ts
│       │       └── SupabaseMapRepository.ts
│       ├── loader
│       │   ├── __tests__
│       │   │   └── mapLoader.spec.tsx
│       │   └── mapLoader.ts
│       ├── utils
│       │   ├── marker.ts
│       │   └── userMarker.ts
│       └── constants.ts
├── lib
│   ├── indexedDB
│   │   ├── constants.ts
│   │   ├── database.ts
│   │   └── types.ts
│   └── supabase
│       ├── schema.ts
│       └── supabaseClient.ts
├── router
│   ├── __tests__
│   │   └── NotFound.spec.tsx
│   ├── AppRouteProvider.tsx
│   └── NotFound.tsx
├── store
│   └── useMapStore.ts
├── styles
│   └── index.css
├── main.tsx
└── serviceWorker.ts

工夫したこと

Service Worker によるオフラインキャッシュ設計

「現地では通信できない」を回避すべき例外ではなく要件として先に固定し、通信できるうちに必要なデータを端末へ移す方式を選んだ。fetch インターセプトで地図タイルと目的地データを IndexedDB へ保存し、通信断をエラーではなく想定内の状態として扱えるようにしている。

データ取得元をリポジトリ層に隔離

地図・目的地・マーカーをドメインモデルとして定義し、Supabase と IndexedDB という取得元の差異をリポジトリ層に閉じ込めた。上位のコンポーネントはオンライン/オフラインを意識せず、同じインターフェースで描画できる。

最も操作に不慣れなユーザーを基準にした導線設計

オフライン機能は事前準備をしてもらえて初めて価値が出るため、機能実装と同等の比重で準備導線の単純化に工数を割いた。設定項目や判断を伴う操作を削り、「リンクを開く → タップする」だけで準備が完了する形に絞り込んでいる。

TDD とクリーンアーキテクチャを意識した設計

ドメインロジックを TDD で実装し、テストコードが設計ドキュメントとして機能するよう意識した。レイヤードアーキテクチャとフィーチャーアーキテクチャを組み合わせ、責務の分離を保ちながらファイル構成の見通しを高めている。期間を空けて再着手しても設計意図を辿れる状態を維持することが狙い。

Next Step

今後の展望

  • 地図スタイル・UI の改善による視認性と操作性の向上
  • 対応地図エリアの拡充
  • 認証認可の導入によるユーザーごとの地図・目的地管理

Link

外部リンク