【後編2】React+Leaflet連携 & Twitter連携で「地図×SNS×ナラティブ」アプリを完成させよう
お待たせしました!前回の「後編1」では、Express+PostgreSQLでモックアップAPIを作り、データベースに投稿を保存する一連の流れを確認しました。今回は後編2として、実際にReact(フロントエンド)とLeaflet(地図表示)を組み合わせ、さらにTwitter APIを活用してSNS上のデータを取得し、地図に可視化するイメージを実装していきます。
全体構成のおさらい
my-project/
┣ frontend/ ← React + Leaflet
┣ server/ ← Node.js (Express) + PostgreSQL接続
┗ database ← (PostgreSQL/PostGIS)
- フロントエンド: React + Leaflet.js
- バックエンド: Express(Node.js)でAPI化
- DB: PostgreSQLで投稿データやTwitterデータを管理
- Twitter API: Bearer Tokenなどを使い位置情報付きツイートを取得
前編1では /api/posts
による投稿の読み書きを実装しました。
今回のステップは以下のとおりです。
- Leafletを使った地図表示&ユーザーが地図クリック→投稿できるフォーム
- PostGISを使った位置情報カラムの追加と保存
- Twitter APIを呼び出して位置情報付きツイートを取得し、DBに取り込む(簡易版)
- 画面に投稿 & ツイートを両方マッピングして可視化
1. フロントエンド:React + Leafletで地図表示
1.1 フロントエンド初期設定
既にfrontend
フォルダを作成している前提です。無い場合は以下のように:
cd my-project
npx create-react-app frontend
cd frontend
npm install leaflet react-leaflet axios
-
leaflet
,react-leaflet
: 地図表示ライブラリ -
axios
: HTTPクライアント
package.json 一例
{
"name": "frontend",
"dependencies": {
"react": "^18.x",
"react-dom": "^18.x",
"react-leaflet": "^4.x",
"leaflet": "^1.x",
"axios": "^0.27.x"
},
"scripts": {
"start": "react-scripts start",
// ...
}
}
1.2 地図コンポーネント作成
src/components/MapView.js
のように分けると管理しやすいです。
// src/components/MapView.js
import React, { useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
function MapView({ markers }) {
const [clickPosition, setClickPosition] = useState(null);
// 地図クリックイベントを捕捉
useMapEvents({
click(e) {
setClickPosition(e.latlng);
}
});
return (
<MapContainer center={[35.0, 135.0]} zoom={5} style={{ height: '80vh' }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="© OpenStreetMap contributors"
/>
{markers.map((m) => (
<Marker key={m.id} position={[m.lat, m.lng]}>
<Popup>
<h3>{m.title}</h3>
<p>{m.content}</p>
</Popup>
</Marker>
))}
{clickPosition && (
<Marker position={[clickPosition.lat, clickPosition.lng]}>
<Popup>ここに投稿しますか?</Popup>
</Marker>
)}
</MapContainer>
);
}
export default MapView;
-
markers
はサーバーから取得した投稿データを配列として受け取り、Markerとして表示する想定。 -
clickPosition
にはユーザーがクリックした座標が入り、そこからフォーム送信などの操作を行う(後述)。
1.3 ルートコンポーネントに組み込む
src/App.js
など:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import MapView from './components/MapView';
function App() {
const [markers, setMarkers] = useState([]);
// 起動時にAPIから既存投稿を取得
useEffect(() => {
axios.get('http://localhost:4000/api/posts')
.then(res => setMarkers(res.data))
.catch(err => console.error(err));
}, []);
return (
<div>
<h1>地図×ナラティブ × SNS</h1>
<MapView markers={markers} />
</div>
);
}
export default App;
動作確認
cd frontend
npm start
ブラウザが http://localhost:3000/
で起動し、地図が表示されるはずです。
まだ投稿を地図に反映する機能を追加していないので、markers
が空ならMarkerも無しとなります。
2. PostGISで位置情報を保存
2.1 DBテーブルの拡張
「後編1」ではposts
テーブルに title, content, created_at
しかありませんでした。ここに「geom」列を追加して地理情報を扱えるようにします。
ALTER TABLE posts
ADD COLUMN geom geometry(Point, 4326);
-- Default値は無し。必要なら:
-- UPDATE posts SET geom = ST_SetSRID(ST_Point(lon, lat), 4326) ...
これにより、
geom
に世界測地系(SRID=4326)のポイント情報を保存できます。
2.2 Express側:INSERT時にgeomをセット
server/index.js
のPOST処理を以下のように改修:
app.post('/api/posts', async (req, res) => {
try {
const { title, content, lat, lng } = req.body;
const queryText = `
INSERT INTO posts (title, content, geom)
VALUES ($1, $2, ST_SetSRID(ST_Point($3, $4), 4326))
RETURNING *
`;
const values = [title, content, lng, lat];
const result = await pool.query(queryText, values);
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'DB insert error' });
}
});
ここでは
(lng, lat)
の順序でST_Point
を作成している点に注意。実際には「経度が先、緯度が後」が標準です。
2.3 GETのレスポンスに lat/lng を含める
同じくGET /api/posts
時にDBのgeom
を緯度経度に変換して返却:
app.get('/api/posts', async (req, res) => {
try {
const sql = `
SELECT id, title, content,
ST_X(geom) as lng,
ST_Y(geom) as lat,
created_at
FROM posts
ORDER BY id DESC
`;
const result = await pool.query(sql);
res.json(result.rows); // each row now has lat/lng
} catch (err) {
res.status(500).json({ error: 'DB query error' });
}
});
こうすればフロントエンドで { id, title, content, lat, lng }
をMarkerの座標に使えます。
3. 地図上で投稿フォームを実装
3.1 フォームの追加
先ほどの MapView.js
内 clickPosition
を利用し、ポップアップや別コンポーネントで入力を行う仕組みにします。以下は簡易例:
// MapView.js
function MapView({ markers }) {
const [clickPosition, setClickPosition] = useState(null);
const [formVisible, setFormVisible] = useState(false);
useMapEvents({
click(e) {
setClickPosition(e.latlng);
setFormVisible(true);
}
});
return (
<div style={{ position: 'relative' }}>
<MapContainer ...>
<TileLayer ... />
{markers.map(...)}
{clickPosition && (
<Marker position={[clickPosition.lat, clickPosition.lng]}>
<Popup>投稿フォームを表示しますか?</Popup>
</Marker>
)}
</MapContainer>
{formVisible && (
<PostForm
latlng={clickPosition}
onClose={() => setFormVisible(false)}
/>
)}
</div>
);
}
3.2 PostForm コンポーネント
// PostForm.js (簡易例)
import axios from 'axios';
import React, { useState } from 'react';
function PostForm({ latlng, onClose }) {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const handleSubmit = async () => {
try {
const payload = {
title,
content,
lat: latlng.lat,
lng: latlng.lng
};
await axios.post('http://localhost:4000/api/posts', payload);
alert('投稿しました');
onClose();
} catch (err) {
console.error(err);
alert('エラーが発生しました');
}
};
return (
<div style={{
position: 'absolute', top: 50, right: 50, backgroundColor: '#fff', padding: '1rem'
}}>
<h3>新規投稿</h3>
<p>緯度: {latlng.lat}, 経度: {latlng.lng}</p>
<div>
<label>タイトル:</label>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
<div>
<label>本文:</label>
<textarea value={content} onChange={(e) => setContent(e.target.value)} />
</div>
<button onClick={handleSubmit}>送信</button>
<button onClick={onClose}>キャンセル</button>
</div>
);
}
export default PostForm;
フロントエンド上で「マップクリック → PostForm表示 → 送信」でDBに保存 → 位置情報付き投稿が完成です。
送信後、Markerリストを再取得するように工夫するとリアルタイムにマーカーが追加されます。
4. Twitter API連携(位置情報付きツイート取得)
4.1 Expressルート: /api/twitter
などを用意
server/index.js
(例):
const { TwitterApi } = require('twitter-api-v2');
const TWITTER_BEARER = process.env.TWITTER_BEARER_TOKEN || '...';
app.get('/api/twitter/search', async (req, res) => {
try {
const client = new TwitterApi(TWITTER_BEARER);
const keyword = req.query.q || '公園';
// API v2 search
const results = await client.v2.search(keyword, {
expansions: ['geo.place_id'],
'tweet.fields': ['created_at', 'geo', 'text']
// place_id情報など地理データを使うには特別な承認が必要なケースも
});
res.json(results);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Twitter API error' });
}
});
注意: Twitterの位置情報付きツイートはAPIポリシーが変わりやすく、最新の規約をチェックしてください。
4.2 取得したツイートをDBに格納
やり方は「POST /api/twitter/save
」などを作り、取得したツイートをPostGISへ挿入するフローを実装します。
いったん簡易化のため省略例:
app.post('/api/twitter/save', async (req, res) => {
// req.body.tweets に { text, lat, lng, created_at } といった形式で渡す
// DBにINSERT (INSERT INTO posts(title, content, geom) ... )
});
4.3 フロントエンドで呼び出し & 地図に表示
// 例: Twitter検索ボタンを設置
function TwitterSearch() {
const [keyword, setKeyword] = useState('');
const handleSearch = async () => {
const res = await axios.get(`http://localhost:4000/api/twitter/search?q=${keyword}`);
console.log(res.data); // ここでTwitterデータを確認
// 必要に応じてDBに保存 or 画面にマップする
};
return (
<div>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<button onClick={handleSearch}>Twitter検索</button>
</div>
);
}
フロントUIは用途に合わせて設計します。位置情報のあるツイート(geo
フィールドやplace
情報)を抽出できた場合、lat, lng
をMarker化すればOK。
5. デプロイと運用のヒント
-
フロント&バックの同時起動:
-
npm run start
(frontend) とnpm run start
(server) を並行して行う。 - CORS設定を正しくする (
app.use(cors())
)。
-
-
Docker Composeで一括起動:
-
docker-compose.yml
を用意し、Node.js/Express
+PostgreSQL/PostGIS
+React
などをまとめて管理する。
-
-
Twitterの有料プラン注意:
- 位置情報検索や大規模データ取得には制限や有料枠があるかもしれない。常に最新情報を確認。
-
地図UI:
- Leaflet以外にもMapbox GLやDeck.glを組み合わせる選択肢あり。
- 大量のマーカーを描画する際はパフォーマンスに注意。
6. まとめ
- Leaflet + PostGISを組み合わせることで、「位置情報付きのナラティブ投稿」を直感的に扱えるようになりました。
- Twitter APIを絡めれば外部から取得したデータも地図に統合可能です。
- 今回は最小限の例ですが、さらに写真アップロードや検索フィルタなどを追加すると実用度が高まります。
これで「後編2」完了です!
ぜひ、このサンプルをベースにしたアプリを拡張し、**「地図上で多様なナラティブを収集&可視化できる仕組み」**を作り上げてみてください。お疲れさまでした。
参考リンク
- Leaflet: https://leafletjs.com/
- React Leaflet: https://react-leaflet.js.org/
- PostGIS: https://postgis.net/
- Twitter API: https://developer.twitter.com/
以上で後編2の解説でした。投稿フォームやTwitter連携など、ぜひ実際に動かしてみてさらなる機能追加に挑戦してみてください!