Skip to content

Commit d5441dc

Browse files
rtcode337claude
andauthored
依存のメジャーアップデート(Next.js 16・MapLibre 6・Node 24) (#66)
Dependabotのバージョン更新PR(#59, #62, #63, #64, #65)への対応。 - Next.js 15.5 → 16.2: middleware.tsをproxy.tsへ改名、dev/buildは--webpack明示 (TurbopackにwatchOptions.ignored相当が無いため)、廃止されたnext lintを撤去 - maplibre-gl 5 → 6: 名前空間importへ変更、geolocateイベントの型修正 - Dockerfileのnodeを24-alpine(現行Active LTS)へ、@types/nodeも24へ - TypeScript 7(Next.js未対応)とNode 26(LTS入り前)は見送り、 理由をdependabot.ymlのignoreコメントに記録 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PG1jLCQ15b7jyhWS2GS4gH
1 parent bd33f12 commit d5441dc

17 files changed

Lines changed: 196 additions & 148 deletions

.github/dependabot.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ updates:
1313
groups:
1414
minor-and-patch:
1515
update-types: ["minor", "patch"]
16+
ignore:
17+
# TypeScript 7(Goネイティブ版)はNext.jsが未対応(JS実装のCompiler APIが同梱されなく
18+
# なり、next buildがTypeScriptを検出できず失敗する)。Next側の対応後に手動で上げる
19+
- dependency-name: "typescript"
20+
update-types: ["version-update:semver-major"]
21+
# Node本体のメジャーはLTS入りしてから上げる方針(Dockerfileのnodeイメージと同時に、
22+
# ランタイムと同じメジャーへ手動で上げる)。26は2026年10月にLTS入り予定
23+
- dependency-name: "@types/node"
24+
update-types: ["version-update:semver-major"]
1625

1726
# Dockerfileのベースイメージ(アプリ本体=node、db-init=postgres)
1827
- package-ecosystem: "docker"
@@ -28,6 +37,10 @@ updates:
2837
# dump/restore)が必要なため、自動PRにはしない(意図的に手動で上げる)
2938
- dependency-name: "postgres"
3039
update-types: ["version-update:semver-major"]
40+
# NodeのメジャーはLTS入りしたものだけを使う(Currentは追わない)。26は2026年10月に
41+
# LTS入り予定で、それまでは現行Active LTSの24を使う。上げるときは@types/nodeも同時に
42+
- dependency-name: "node"
43+
update-types: ["version-update:semver-major"]
3144

3245
# docker-compose直書きのイメージ(dbサービスのpostgres:16-alpine)
3346
- package-ecosystem: "docker-compose"

CLAUDE.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1212
docker compose -f docker-compose.dev.yml up --build # 開発用: アプリ(localhost:3000, next dev+ホットリロード)+Postgres。スキーマ作成・未適用マイグレーションはdb-migrateサービスが自動で行う
1313
docker compose pull && docker compose up -d # 本番用: GHCRのビルド済みイメージ(mainへのpushでGitHub Actionsが自動ビルド)で起動。未適用のマイグレーションはdb-migrateサービスが自動で当てる。SESSION_SECRET環境変数が必須(.env可)
1414
npm run dev # Next.js開発サーバー(ローカルPostgresを直接使う場合のみ)
15-
npm run build # 本番ビルド
16-
npm run lint # next lint
15+
npm run build # 本番ビルド(型チェック込み)
1716
```
1817

18+
`npm run dev`/`npm run build`はどちらも`--webpack`を明示している(Next.js 16の既定バンドラーのTurbopackには、bindマウントされた`db/data``photos`をファイル監視から外す`watchOptions.ignored`相当の設定が無いため。`next.config.ts`のコメント参照)。
19+
1920
`docker-compose.yml`(本番用)と`docker-compose.dev.yml`(開発用)はプロジェクト名を分けてある(`travel-log-prod`/`travel-log-dev`)ため、同一ホスト上で両方動かしてもコンテナ・イメージ・ボリュームは衝突しない。
2021

21-
このプロジェクトにテストスイート/テストコマンドは存在しない。
22+
このプロジェクトにテストスイート/テストコマンドは存在しない。リンターも未導入(Next.js 16で`next lint`が廃止された際、代替のESLint導入は見送った — eslint-config-nextの依存チェーンに未修正のbrace-expansion脆弱性(GHSA-mh99-v99m-4gvg)が含まれ、導入するとDependabotの高深刻度アラートが解消不能な形で付くため。エコシステム側の修正後に導入を検討する)。型チェックは`next build`が行う。
2223

2324
### スキーマ変更のルール
2425

@@ -79,11 +80,11 @@ Next.jsのRoute Handlersのみで、別立てのAPIサーバーは存在しな
7980

8081
### 認証
8182

82-
NextAuthではなく自前実装。`lib/auth/session.ts`がHMAC-SHA256で署名したCookie(Web Crypto APIのみ使用、外部依存なし)を発行し、Edge実行の`middleware.ts`とNode実行のRoute Handlersの両方で同じロジックにより検証できるようにしている。Cookieには`{ sub: userId, exp }`のみを持たせ、roleは意図的にCookieに含めていない — `lib/auth/current-user.ts`経由で毎リクエストDBから引き直すことで、管理者によるロール変更やDB作り直しが古いCookieのまま反映されない事態を防いでいる。`middleware.ts``/login``/api/**`、および`/manifest.webmanifest`(ブラウザのmanifest取得は既定でCookieを送らないため、ガードするとPWAとしてインストールできなくなる)以外の全ルートをガードする。
83+
NextAuthではなく自前実装。`lib/auth/session.ts`がHMAC-SHA256で署名したCookie(Web Crypto APIのみ使用、外部依存なし)を発行し、`proxy.ts`(Next.js 16で`middleware.ts`から改名されたもの。Node実行)とRoute Handlersの両方で同じロジックにより検証できるようにしている。Cookieには`{ sub: userId, exp }`のみを持たせ、roleは意図的にCookieに含めていない — `lib/auth/current-user.ts`経由で毎リクエストDBから引き直すことで、管理者によるロール変更やDB作り直しが古いCookieのまま反映されない事態を防いでいる。`proxy.ts``/login``/api/**`、および`/manifest.webmanifest`(ブラウザのmanifest取得は既定でCookieを送らないため、ガードするとPWAとしてインストールできなくなる)以外の全ルートをガードする。
8384

8485
### PWA(インストール可能化)
8586

86-
manifest(`app/manifest.ts`、Next.jsのMetadata Files規約で`/manifest.webmanifest`として配信)+アイコン(`public/icons/`のmanifest用3枚と、`app/icon.png`・`app/apple-icon.png`のファビコン/apple-touch-icon)による最小構成のPWA対応で、Service Worker・オフライン対応は意図的に持たない(「デプロイしたのに古い画面が出る」系の問題を避けるため、必要になるまで導入しない方針)。インストール後も中身は同じWebアプリで、認証Cookieもそのまま使われる。iOSはmanifestの`display`/`icons`を見ないため、`app/layout.tsx`の`metadata.appleWebApp`と`app/apple-icon.png`で別途同等の設定をしている。ページ自体のズームは`app/layout.tsx`の`viewport`(`maximumScale: 1`+`userScalable: false`)で無効化してある — 検索窓等への入力フォーカス時の自動ズームで下のタブバーが隠れ、地図表示中はピンチが地図操作に取られてページのズームを戻せなくなるため(地図の拡大縮小はMapLibreのジェスチャなので影響しない)。`/manifest.webmanifest`は`middleware.ts`のガード対象から除外が必要(上記「認証」参照)。アイコンPNGは`scripts/generate-icons.mjs`(sharp使用、依存には含めない)で生成したものをコミットしてあり、デザイン変更時のみ再生成する。iOSのスタンドアロン起動では`target="_blank"`の外部リンクがアプリ内ブラウザ(オーバーレイ)で開かれてしまうため、外部サイトへのリンク(`SpotInfoModal`の「Wikipediaで続きを読む」)はiOS+スタンドアロンのときだけ`x-safari-https://`スキーム(未文書化だがiOSが解釈する)で本物のSafariに切り替えている(`isIosStandalone`)。
87+
manifest(`app/manifest.ts`、Next.jsのMetadata Files規約で`/manifest.webmanifest`として配信)+アイコン(`public/icons/`のmanifest用3枚と、`app/icon.png`・`app/apple-icon.png`のファビコン/apple-touch-icon)による最小構成のPWA対応で、Service Worker・オフライン対応は意図的に持たない(「デプロイしたのに古い画面が出る」系の問題を避けるため、必要になるまで導入しない方針)。インストール後も中身は同じWebアプリで、認証Cookieもそのまま使われる。iOSはmanifestの`display`/`icons`を見ないため、`app/layout.tsx`の`metadata.appleWebApp`と`app/apple-icon.png`で別途同等の設定をしている。ページ自体のズームは`app/layout.tsx`の`viewport`(`maximumScale: 1`+`userScalable: false`)で無効化してある — 検索窓等への入力フォーカス時の自動ズームで下のタブバーが隠れ、地図表示中はピンチが地図操作に取られてページのズームを戻せなくなるため(地図の拡大縮小はMapLibreのジェスチャなので影響しない)。`/manifest.webmanifest`は`proxy.ts`のガード対象から除外が必要(上記「認証」参照)。アイコンPNGは`scripts/generate-icons.mjs`(sharp使用、依存には含めない)で生成したものをコミットしてあり、デザイン変更時のみ再生成する。iOSのスタンドアロン起動では`target="_blank"`の外部リンクがアプリ内ブラウザ(オーバーレイ)で開かれてしまうため、外部サイトへのリンク(`SpotInfoModal`の「Wikipediaで続きを読む」)はiOS+スタンドアロンのときだけ`x-safari-https://`スキーム(未文書化だがiOSが解釈する)で本物のSafariに切り替えている(`isIosStandalone`)。
8788

8889
### ビルド番号
8990

@@ -93,7 +94,7 @@ GitHub Actions(`.github/workflows/docker-publish.yml`)がビルド時に`<JST日
9394

9495
単一の`spots`テーブルを`spot_types`により複数の「種別」で使い回す設計。`tourist`(観光地)がアプリ初期化時(`db/init/01_schema.sql`)に必ず作成される唯一の既定種別で、それ以外の種別は管理者が`/[type]/admin`から追加する(空の種別ではデータが入らないだけで、削除しない限り存在し続ける)。既定種別といっても自動で作られるのは`spot_types`の行だけで、スポットデータ自体は他の種別と同様シードデータを`db/init/`に直接コミットせず外部リポジトリ[travel-log-data](../travel-log-data)にCSVとして置き、`/[type]/admin`のCSVインポートから取り込む(下記「外部データソース」の段落参照)。観光地データの`description`はWikipedia記事冒頭文の引用でCC BY-SA 4.0、`lat`/`lng`はWikipedia記事座標(CC BY-SA 4.0)またはWikidata `P625`(CC0)由来のため、travel-logリポジトリ本体には同梱せずtravel-log-data側でのみ管理・出典表示する。
9596

96-
画面は`/[type]/map`のように`spot_types.key`をURLの動的セグメントとして持ち、種別ごとに独立してアクセスする(ルート`/`アクセス時のリダイレクト先は、最後に開いていた種別のCookie `last_spot_type` — `lib/last-spot-type.ts`。`middleware.ts`が`/[type]/(map|spots|account|admin)`アクセス時に書き込み、`app/page.tsx`が読み取り時に`canViewSpotType`で検証する — を最優先し、無い・開けない場合に`app_settings.active_spot_type_id`の既定へフォールバックする。どちらも他の種別を隠すものではない)。種別の切り替えは`/[type]/map`の**左下の種別チップ(`MapView`。現在の種別名を表示し、タップすると他の種別の一覧メニューが上向きに開き、選ぶと`/[別種別]/map`へ遷移する)**から行う(かつてはアカウントタブ`AccountView`の「別のスポットを見る」一覧だったが、地図から直接切り替えられるよう移した。アカウントタブは現在のモード表示のみ残す)。他の種別が無いときはチップはタップしても何も起きない(現在名の表示だけ)。`public_visible=false`の種別はAPI(`api.spotTypes.list`)がadmin/spot_admin以外には返さないため、一般ユーザーのメニューには公開種別のみ並ぶ。種別ごとの公開範囲は`spot_type_settings`の`public_visible`設定(既定false=admin/spot_admin限定)で制御し、`lib/spot-type-access.ts`の`canViewSpotType`で判定する(`/[type]/admin`だけは`public_visible`に関わらず常にアクセス可)。かつてあった`spot_types.visibility`列(`public`/`admin_only`/`disabled`の3値)は廃止し、`disabled`(誰にも見せない)相当は種別自体の削除で代替するようにした。
97+
画面は`/[type]/map`のように`spot_types.key`をURLの動的セグメントとして持ち、種別ごとに独立してアクセスする(ルート`/`アクセス時のリダイレクト先は、最後に開いていた種別のCookie `last_spot_type` — `lib/last-spot-type.ts`。`proxy.ts`が`/[type]/(map|spots|account|admin)`アクセス時に書き込み、`app/page.tsx`が読み取り時に`canViewSpotType`で検証する — を最優先し、無い・開けない場合に`app_settings.active_spot_type_id`の既定へフォールバックする。どちらも他の種別を隠すものではない)。種別の切り替えは`/[type]/map`の**左下の種別チップ(`MapView`。現在の種別名を表示し、タップすると他の種別の一覧メニューが上向きに開き、選ぶと`/[別種別]/map`へ遷移する)**から行う(かつてはアカウントタブ`AccountView`の「別のスポットを見る」一覧だったが、地図から直接切り替えられるよう移した。アカウントタブは現在のモード表示のみ残す)。他の種別が無いときはチップはタップしても何も起きない(現在名の表示だけ)。`public_visible=false`の種別はAPI(`api.spotTypes.list`)がadmin/spot_admin以外には返さないため、一般ユーザーのメニューには公開種別のみ並ぶ。種別ごとの公開範囲は`spot_type_settings`の`public_visible`設定(既定false=admin/spot_admin限定)で制御し、`lib/spot-type-access.ts`の`canViewSpotType`で判定する(`/[type]/admin`だけは`public_visible`に関わらず常にアクセス可)。かつてあった`spot_types.visibility`列(`public`/`admin_only`/`disabled`の3値)は廃止し、`disabled`(誰にも見せない)相当は種別自体の削除で代替するようにした。
9798

9899
新しい種別は`/[type]/admin`のキー+表示名の手入力フォームのほか、`{ key, label, settings?, series?, categories? }`形式のJSONファイルアップロードでも作成できる(`lib/types.ts`の`parseSpotTypeDefinition`でバリデーション、`AdminView`側で`spotTypes.create`→(settings/series/categoriesがあれば)`spotTypes.applySettings`の2段APIコールに分解する。バックエンドに専用エンドポイントは増やしていない)。travel-log-dataリポジトリの`<スポットキー>/settings.json`がこの形式の実例。同じ形式のJSONは、既存の種別に対して「スポット種別の設定」セクション(admin専用)の「JSONファイルから設定を反映」からも読み込める(`AdminView`の`handleApplyTypeFromJson`)。こちらは既存の`spotTypes.applySettings`(PATCH `/api/spot-types/[id]`)をそのまま使ってlabel/settings/series/categoriesを上書きする(PATCHの`label`は元々`settings`専用だったこのエンドポイントに追加した省略可能フィールドで、指定時のみ`spot_types.label`列をUPDATEする)。keyの変更だけは影響が大きい(URLの`/[type]/`セグメント・`app_settings.active_spot_type_id`・地図の表示位置記憶等、あらゆる箇所がkeyで紐づいているため)ため意図的にサポートせず、JSONのkeyが現在開いている種別のkeyと一致しない場合は何も反映せずエラーにする。
99100

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:22-alpine AS deps
1+
FROM node:24-alpine AS deps
22
WORKDIR /app
33
COPY package.json package-lock.json ./
44
RUN npm ci
@@ -16,7 +16,7 @@ RUN npm run build
1616

1717
# 本番実行用: next buildの成果物のみを含む最小イメージ。mainへのpushでGitHub Actionsが
1818
# このステージをビルドしてGHCRへ公開し、docker-compose.ymlのappサービスがそれを参照する
19-
FROM node:22-alpine AS prod
19+
FROM node:24-alpine AS prod
2020
WORKDIR /app
2121
ENV NODE_ENV=production
2222
# GitHub Actionsが --build-arg BUILD_NUMBER=<JST日時>-<短縮コミットハッシュ> で渡すビルド番号。

app/manifest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { MetadataRoute } from "next";
22

33
// /manifest.webmanifest として配信される(Next.jsのMetadata Files規約)。
4-
// このパスはmiddleware.tsの認証ガードから除外している — ブラウザのmanifest取得は
4+
// このパスはproxy.tsの認証ガードから除外している — ブラウザのmanifest取得は
55
// 既定でCookieを送らないため、ガード対象のままだと/loginへのリダイレクトになり
66
// インストール不能になる。
77
export default function manifest(): MetadataRoute.Manifest {

app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { LAST_SPOT_TYPE_COOKIE } from "@/lib/last-spot-type";
66

77
/**
88
* ログイン後にどのスポット種別の地図を開くかは、最後に開いていた種別のCookie
9-
* (middleware.tsが書き込む。lib/last-spot-type.ts参照)を最優先し、無い・
9+
* (proxy.tsが書き込む。lib/last-spot-type.ts参照)を最優先し、無い・
1010
* 開けない(種別の削除や非公開化・ロール変更後など)場合は管理画面で設定した
1111
* app_settings.active_spot_type_id(既定はtourist。アプリ初期化時に必ず存在する)に
1212
* フォールバックする。切替がすぐ反映されるよう、ビルド時に静的化せず常に

components/MapView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
savePlanListDraft,
1414
type PlanListDraft,
1515
} from "@/lib/planListDraft";
16-
import maplibregl from "maplibre-gl";
16+
import * as maplibregl from "maplibre-gl";
1717
import "maplibre-gl/dist/maplibre-gl.css";
1818
import { api } from "@/lib/api-client";
1919
import {
@@ -1718,7 +1718,7 @@ export default function MapView({
17181718
};
17191719

17201720
// 青丸は初回測位時に生成されるため、geolocate イベントで初めて子要素として差し込む
1721-
const handleGeolocate = (e: GeolocationPosition) => {
1721+
const handleGeolocate = (e: maplibregl.GeolocatePositionEvent) => {
17221722
// 訪問予定リストの経路の始点(現在地→先頭スポットの線)に使う現在地を覚える。
17231723
// 測位のたびの微小な揺れで再レンダーしないよう、約1m未満の変化は無視する
17241724
const { longitude, latitude } = e.coords;

components/MiniMap.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useEffect, useRef } from "react";
4-
import maplibregl from "maplibre-gl";
4+
import * as maplibregl from "maplibre-gl";
55
import "maplibre-gl/dist/maplibre-gl.css";
66
import { osmStyle } from "@/lib/mapStyle";
77
import type { Series } from "@/lib/types";

components/NavBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function NavBar() {
2626
if (!data) {
2727
// Cookieの署名は有効でも、DBを作り直す等でユーザー自体が
2828
// 既に存在しない場合はここに来る。Cookieを消してログイン画面に戻す
29-
// (消さないとmiddlewareが「署名は正しい」と判断し/loginへ戻れなくなる)
29+
// (消さないとproxyが「署名は正しい」と判断し/loginへ戻れなくなる)
3030
await api.auth.logout();
3131
router.replace("/login");
3232
return;

components/SpotRepositionModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useEffect, useRef, useState } from "react";
4-
import maplibregl from "maplibre-gl";
4+
import * as maplibregl from "maplibre-gl";
55
import "maplibre-gl/dist/maplibre-gl.css";
66
import { osmStyle } from "@/lib/mapStyle";
77
import { api } from "@/lib/api-client";

lib/auth/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* HMAC署名付きの自前セッションCookie。
3-
* DBアクセスを必要としないため Edge Middleware でも Node のRoute Handlerでも
4-
* 同じロジックで検証できる(Web Crypto API のみ使用)。
3+
* DBアクセスを必要とせずWeb Crypto APIのみ使用のため、proxy.tsでも
4+
* Route Handlerでも同じロジックで検証できる(実行ランタイムを選ばない)。
55
*/
66

77
export const SESSION_COOKIE = "session";

0 commit comments

Comments
 (0)