commit
a52904b458
|
|
@ -0,0 +1,171 @@
|
||||||
|
/**
|
||||||
|
* Posimai Events - /api/events
|
||||||
|
*
|
||||||
|
* GET /api/events -> イベント一覧を返す(モックデータ or KVストア)
|
||||||
|
* POST /api/events -> n8n から構造化データを受け取り保存するエンドポイント
|
||||||
|
*
|
||||||
|
* 現在はモックデータを返す。
|
||||||
|
* n8n 連携時は EVENTS_STORE_TOKEN を環境変数に設定し、
|
||||||
|
* POST で受け取ったデータを Vercel KV または外部DBへ書き込む実装に差し替える。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MOCK_EVENTS = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: '春の産直マルシェ',
|
||||||
|
startDate: '2026-03-03', startTime: '09:00',
|
||||||
|
endDate: '2026-03-05', endTime: '16:00',
|
||||||
|
location: '市民広場 イベントスペース',
|
||||||
|
address: '○○市中央1-1',
|
||||||
|
description: '地元農家が丹精込めて育てた旬の野菜・果物が並ぶ春の産直市。パン工房や手作りスイーツのブースも出店。家族でゆっくり楽しめます。',
|
||||||
|
category: 'マルシェ',
|
||||||
|
url: 'https://example.com/marche',
|
||||||
|
source: '市役所公式サイト'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: '朝の太極拳教室(無料体験)',
|
||||||
|
startDate: '2026-03-03', startTime: '07:00',
|
||||||
|
endDate: '2026-03-03', endTime: '08:30',
|
||||||
|
location: '中央公園 芝生広場',
|
||||||
|
address: '○○市中央公園',
|
||||||
|
description: '毎週火・木・土曜開催の太極拳サークルが無料体験会を開催。初心者・シニアの方歓迎。動きやすい服装でお越しください。',
|
||||||
|
category: '体験・スポーツ',
|
||||||
|
url: 'https://example.com/taichi',
|
||||||
|
source: '地域掲示板'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: '防災訓練・地域説明会',
|
||||||
|
startDate: '2026-03-05', startTime: '10:00',
|
||||||
|
endDate: '2026-03-05', endTime: '12:00',
|
||||||
|
location: '○○公民館 大ホール',
|
||||||
|
address: '○○市西町2-5',
|
||||||
|
description: '年1回の地区防災訓練と、今年度の避難計画変更に関する説明会を同日開催します。参加無料。',
|
||||||
|
category: '地域・行政',
|
||||||
|
url: 'https://example.com/bousai',
|
||||||
|
source: '町内会回覧板'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: '伝統工芸・陶芸ワークショップ',
|
||||||
|
startDate: '2026-03-06', startTime: '13:00',
|
||||||
|
endDate: '2026-03-06', endTime: '17:00',
|
||||||
|
location: '○○文化センター 工芸室',
|
||||||
|
address: '○○市文化通り3-8',
|
||||||
|
description: '地元陶芸家による手びねり体験。土から形を作り、釉薬を選んで焼き上げ(後日お渡し)。定員12名・要事前申込。参加費 2,500円。',
|
||||||
|
category: 'ワークショップ',
|
||||||
|
url: 'https://example.com/ceramics',
|
||||||
|
source: '文化センターHP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
title: '春のクラフトフェア 2026',
|
||||||
|
startDate: '2026-03-07', startTime: '10:00',
|
||||||
|
endDate: '2026-03-08', endTime: '17:00',
|
||||||
|
location: '○○公園 野外広場',
|
||||||
|
address: '○○市北公園',
|
||||||
|
description: '全国から集まるクラフト作家80組が出展。アクセサリー、革工芸、テキスタイル、木工など多彩なジャンル。フードトラックも10台出店。入場無料。',
|
||||||
|
category: 'マーケット',
|
||||||
|
url: 'https://example.com/craft',
|
||||||
|
source: '実行委員会HP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
title: '地域清掃ボランティア',
|
||||||
|
startDate: '2026-03-08', startTime: '09:00',
|
||||||
|
endDate: '2026-03-08', endTime: '11:00',
|
||||||
|
location: '○○川 河川敷',
|
||||||
|
address: '○○市河川敷公園',
|
||||||
|
description: '春の清掃活動。参加自由・事前申込不要。軍手・ゴミ袋は主催者が用意。終了後、軽食の提供あり。',
|
||||||
|
category: '地域・ボランティア',
|
||||||
|
url: 'https://example.com/cleanup',
|
||||||
|
source: '市環境課HP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
title: '春のクラシックコンサート',
|
||||||
|
startDate: '2026-03-14', startTime: '15:00',
|
||||||
|
endDate: '2026-03-14', endTime: '17:30',
|
||||||
|
location: '○○市民ホール 小ホール',
|
||||||
|
address: '○○市文化町1-1',
|
||||||
|
description: '地元弦楽四重奏団による春のコンサート。ハイドン・シューベルトを中心に演奏。全席自由・入場料 1,000円(高校生以下無料)。',
|
||||||
|
category: '音楽・アート',
|
||||||
|
url: 'https://example.com/concert',
|
||||||
|
source: '市民ホールHP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
title: 'まちなかマルシェ(4月)',
|
||||||
|
startDate: '2026-04-04', startTime: '10:00',
|
||||||
|
endDate: '2026-04-05', endTime: '16:00',
|
||||||
|
location: '商店街アーケード',
|
||||||
|
address: '○○市本町通り',
|
||||||
|
description: '毎月第1土日開催の定期マルシェ。4月は春のテーマで出店者募集中(出店費無料)。',
|
||||||
|
category: 'マルシェ',
|
||||||
|
url: 'https://example.com/monthly',
|
||||||
|
source: '商店街組合HP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9',
|
||||||
|
title: '子ども映画祭(無料上映)',
|
||||||
|
startDate: '2026-02-28', startTime: '10:00',
|
||||||
|
endDate: '2026-03-02', endTime: '17:00',
|
||||||
|
location: '中央図書館 多目的ホール',
|
||||||
|
address: '○○市図書館通り',
|
||||||
|
description: '国内外の子ども向けアニメ・映画の無料上映会。3日間で10作品上映。入場無料・予約不要。',
|
||||||
|
category: '映画・文化',
|
||||||
|
url: 'https://example.com/film',
|
||||||
|
source: '図書館HP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10',
|
||||||
|
title: '早春の野鳥観察会',
|
||||||
|
startDate: '2026-03-01', startTime: '07:00',
|
||||||
|
endDate: '2026-03-01', endTime: '09:30',
|
||||||
|
location: '○○自然公園 入口',
|
||||||
|
address: '○○市郊外 自然公園',
|
||||||
|
description: '自然観察指導員が案内するバードウォッチング入門ツアー。双眼鏡の貸し出しあり。申込不要。',
|
||||||
|
category: '自然・体験',
|
||||||
|
url: 'https://example.com/birds',
|
||||||
|
source: '環境教育センターHP'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function handler(req, res) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
return res.status(200).json({
|
||||||
|
events: MOCK_EVENTS,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
source: 'mock'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: n8n からのデータ受信エンドポイント(プロトタイプでは受け入れのみ)
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const token = process.env.EVENTS_STORE_TOKEN;
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
|
||||||
|
if (token && authHeader !== `Bearer ${token}`) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = req.body;
|
||||||
|
// TODO: Vercel KV または外部DBへの書き込みをここに実装する
|
||||||
|
// const { kv } = require('@vercel/kv');
|
||||||
|
// await kv.set(`event:${body.id}`, JSON.stringify(body));
|
||||||
|
|
||||||
|
console.log('Received event from n8n:', JSON.stringify(body));
|
||||||
|
return res.status(200).json({ ok: true, received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "Posimai Events",
|
||||||
|
"short_name": "Events",
|
||||||
|
"description": "AIがノイズを削ぎ落とした地域イベント情報",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0a0a0a",
|
||||||
|
"theme_color": "#6EE7B7",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"lang": "ja",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/logo.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/logo.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["lifestyle", "local", "news"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "posimai-events",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Posimai Events - AIがキュレーションする地域イベント情報 PWA",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
const CACHE_NAME = 'posimai-events-v1';
|
||||||
|
const STATIC_ASSETS = ['/', '/index.html', '/manifest.json', '/logo.png'];
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(keys =>
|
||||||
|
Promise.all(
|
||||||
|
keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// API: network first, fallback to cache
|
||||||
|
if (url.pathname.startsWith('/api/')) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then(response => {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match(event.request))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static: cache first, fallback to network
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then(cached => {
|
||||||
|
if (cached) return cached;
|
||||||
|
return fetch(event.request).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"rewrites": [
|
||||||
|
{ "source": "/api/(.*)", "destination": "/api/$1" }
|
||||||
|
],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"source": "/(.*)",
|
||||||
|
"headers": [
|
||||||
|
{ "key": "X-Content-Type-Options", "value": "nosniff" },
|
||||||
|
{ "key": "X-Frame-Options", "value": "DENY" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue