Skip to content

Commit 2403990

Browse files
committed
訪問日をカレンダーで期間指定できるようにし、リスト追加まわりの操作を補う
3つの変更をまとめている(いずれも地図の訪問日・訪問予定リスト周り)。 1. 訪問日をカレンダー(VisitDateCalendar)で選ぶようにし、単日に加えて期間も 指定できるようにした。訪問記録のある日には緑の点を打つ —— どの日に記録が あるか分からないまま総当たりで選ぶことになるのを避けるため。カレンダーは 絞り込みモーダルに直に置かず別モーダルで開く(他の条件を見るのに毎回 その分スクロールすることになるため)。終了日は visitedDateTo に別途持つので、 単日はこれまでどおり visitedDate だけで表せ、保存済みの条件もそのまま読める。 2. 作成モードの「リストに追加しますか?」に、スポットの説明とWikipediaの概要への 入口を出した。名前だけでは入れるか決められないため。表示には api.spots.get で 取り直す —— 地図の公開スポットはIndexedDBキャッシュ由来で description も spot_type_id も持たず、空の spot_type_id では種別が引けずWikipediaの可否が 設定ではなく既定値(true)で決まってしまうため。 3. 訪問予定リストの基本情報モーダルに「保存」を足し、地図へ行かずタイトルや 期間だけ直せるようにした。編集時は既存の spot_ids を送り直す(PATCHは経由 スポットを丸ごと置き換えるため、送らないと全部消える)。キャンセルは 見出し行の右端へ移し、下の行は前へ進む2つの操作に使う。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2d43391 commit 2403990

8 files changed

Lines changed: 568 additions & 85 deletions

File tree

CLAUDE.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,11 @@ PATCH `/api/spot-types/[id]`は`series_styles`・`categories`と同じく、保
209209

210210
`SpotFilters``visitedDate`(`YYYY-MM-DD`のローカル日付)は**絞り込みではなく、地図で「訪問順の経路」を描く対象日**。選んだ日に訪問したスポットを訪問時刻の昇順に緑の矢印で結ぶための日で、`null`=経路を表示しない、既定はその日(今日)。**地図専用**で、スポット一覧(`SpotsView`)は経路を持たないため訪問日のUI自体を出さない(`passesFilters``visitedDate`を見ない。旧・第5引数の`visitedDates`は廃止)。
211211

212-
絞り込みモーダルの「訪問日」セレクト(`MapView`。重ね表示セクションと同じ`border-t`区切り線を上に置く)の選択肢は、先頭が常に「今日」、次が「表示しない」(=`null`)、続けて自分の訪問記録がある他の日(新しい順。`visitDates`/`visitDateOptions`。**他のスポット種別への訪問しかない日も含める** — 経路が種別をまたぐようになったので、その日を選べないと辿れないため。選択中の日が一覧に無くても選択を保てるよう残す)。既定値は今日。「表示しない」(`null`)は`saveFilters`が文字列`"none"`で保存し、`loadSavedFilters`は`"none"`のときだけ`null`=表示しないにする。**「今日」(値が`todayKey()`と一致)は具体的な日付ではなく文字列`"today"`で保存し、読み込み時にその日の今日へ解決する**(日付のまま保存すると翌日に開いたとき前日が選ばれた状態で復元されてしまうため)。日付はその日、それ以外(旧仕様の絞り込みだった頃の`null`・キー欠落・不正値)は今日に倒すため、**既存ユーザーも初回から今日の経路が出る**(`todayKey`/`defaultMapFilters`)。セレクトで日を選んだとき(`handleSelectVisitDate`)は、対象日をセットしたうえで**その日の訪問順の経路全体が画面に収まるよう`fitBounds`する**(1地点だけの日はmaxZoomまで寄る。経路が0件・「表示しない」のときは地図を動かさない)。ユーザーが明示的に選んだときだけ移動し、マウント時の既定(今日)の復元では移動しない。日付キーへの変換は`toVisitDateKey`(`visits.visited_on`はtimestamptzでUTC文字列のため、**必ずローカル時刻で日を切る** — UTCのまま切ると日本時間の朝9時前の訪問が前日になる)。
212+
**対象は単日でも期間でも選べる**(`visitedDate`=開始日、`visitedDateTo`=終了日。`null`=単日)。期間を選ぶと日をまたいだ訪問が**1本の経路**になる(旅行の何日ぶんかをそのまま辿れるように)。**列を分けてあるので単日は従来どおり`visitedDate`だけで表せ**、この項目より前に保存された条件もそのまま単日として読める。判定は`isInVisitedRange`(日付キーが`YYYY-MM-DD`なので文字列比較で足りる)。
213+
214+
選び方は**カレンダー**(`components/VisitDateCalendar.tsx`)。絞り込みモーダルの訪問日セクションには**選択中の日(期間)を出すボタン**だけを置き、押すと**カレンダーを別モーダル(z-[60]。絞り込みモーダルのz-50の上)で開く** —— 絞り込みモーダルにカレンダーを直に置くと、他の条件を見るのに毎回その分スクロールすることになるため。**カレンダーは日を選んでも閉じない**(期間は2回のタップで決まるので、1回目で閉じると期間を選べない)。選択はその場で反映されるので、閉じる操作は「閉じる」だけでよい。
215+
216+
**訪問記録のある日には日付の下に緑の点**を打つ(`visitDateSet`。どの日に記録があるか分からないまま総当たりで選ぶことになるのを避けるため。**他のスポット種別への訪問も数える** — 経路が種別をまたぐようになったので、その日を落とすと辿れない)。1回目のタップで開始日、2回目で終了日。既に期間が決まっている状態でのタップは新しい開始日として選び直し(範囲を狭めるのにリセットを挟まずに済む)、開始日より前をタップしたときはその日を開始日にして元の開始日を終了日にする(前方向にも伸ばせる)。同じ日を2回タップしても単日のまま。「今日」「表示しない」はよく使うので、カレンダーの中と絞り込みモーダルの両方に置く。既定値は今日。かつてはセレクト(今日 / 表示しない / 訪問のある日の一覧)だったが、期間指定とデータのある日の可視化のためカレンダーに変えた。「表示しない」(`null`)は`saveFilters`が文字列`"none"`で保存し、`loadSavedFilters`は`"none"`のときだけ`null`=表示しないにする。**「今日」(値が`todayKey()`と一致)は具体的な日付ではなく文字列`"today"`で保存し、読み込み時にその日の今日へ解決する**(日付のまま保存すると翌日に開いたとき前日が選ばれた状態で復元されてしまうため)。日付はその日、それ以外(旧仕様の絞り込みだった頃の`null`・キー欠落・不正値)は今日に倒すため、**既存ユーザーも初回から今日の経路が出る**(`todayKey`/`defaultMapFilters`)。終了日(`visitedDateTo`)は**「今日」のような相対表現を持たず具体的な日付でだけ保存する**(終了日だけ動くと期間の長さが日をまたぐたびに変わってしまうため)。カレンダーで日を選んだとき(`handleSelectVisitDate`)は、対象日(期間)をセットしたうえで**その経路全体が画面に収まるよう`fitBounds`する**(1地点だけならmaxZoomまで寄る。経路が0件・「表示しない」のときは地図を動かさない)。開始日を`null`にするときは終了日も一緒に落とす(残っていると次に日を選んだとき意図しない期間になる)。ユーザーが明示的に選んだときだけ移動し、マウント時の既定(今日)の復元では移動しない。日付キーへの変換は`toVisitDateKey`(`visits.visited_on`はtimestamptzでUTC文字列のため、**必ずローカル時刻で日を切る** — UTCのまま切ると日本時間の朝9時前の訪問が前日になる)。
213217

214218
訪問日が選ばれているとき、`MapView`は**その日の訪問記録を訪問時刻の昇順に矢印で結んだ「訪問順の経路」**を描く(`buildVisitPath`)。ルートCSVのルートと同じ`spot-routes`ソース・同じ線/矢印レイヤーに載せるだけなので描画コードは共用で、色だけ`VISIT_PATH_COLOR`(緑`#16a34a`=訪問済みピンの塗りと同じ)にしてルートと区別する。同じスポットへの再訪はそのまま複数回経由地として現れる(行って戻る線になる)が、連続する同じスポットへの訪問は長さ0の線分になり矢印の向きが定まらないためまとめる。日時不明の訪問は除外する。**別のスポット種別のスポットも経路に含める**(訪問予定リストと同じ扱い) — 本体種別で解決できないスポットは`api.spots.get`で座標だけ補完する(`pathExtraSpots`/`pathSpotById`。訪問予定リストと共用の仕組み)。かつては表示中の種別の訪問だけを繋いでいたが、同じ日に別の種別のスポットも回っていると経路がそこで途切れていた。**この経路上のスポット(`pathIds`)は、シリーズ・カテゴリ・訪問状況の絞り込みで外れていても必ずピンを表示する**(絞り込みではなくその日の訪問を辿るための表示のため全条件を免除。ルートCSVの経由地はシリーズ・カテゴリのみ免除で訪問状況は適用する点と対照的)。**この免除は重ね表示側にも同じく効く**(別種別のスポットが経路に入るようになったため。詳しくは「別スポット種別の重ね表示」)。免除の判定には経路(並び順つき)ではなく訪問記録から作ったIDの集合(`visitedSpotIdsOn`)を使う —— 座標の補完を待たずに判定でき、ピンを出すかどうかに並び順は要らないため。
215219

@@ -313,10 +317,12 @@ CSVインポートは差分更新で、`AdminView`側が事前読み込み済み
313317

314318
複数スポットを順序付きでまとめる「訪問予定リスト」(旅程)。1スポットごとの`visit_plans`(行きたい場所のブックマーク)とは**独立**で、`/[type]/spots`の訪問予定欄に個別の予定スポットと**混じって**並ぶ(見出しは0件でも常に表示する)。スキーマは`visit_plan_lists`(種別ごと=`spot_type_id``title``description``start_date``end_date`(単日は開始=終了)・`user_id`)+`visit_plan_list_items`(`list_id``spot_id``seq``(list_id, spot_id)`一意)の2テーブル(`db/init/01_schema.sql`、移行は`migrations/006`)。**種別ごと**(地図の作成が`/[type]/map`上で行われるため。CLAUDE作成時の判断で種別横断は不可)。
315319

316-
作成フローは、訪問予定欄の「+ 訪問予定リストを追加」→ 基本情報モーダル(`VisitPlanListFormModal`。タイトル・説明・期間)→ **下書きをlocalStorageへ保存**(`lib/planListDraft.ts`。「入力完了」までDBに保存しないため、SpotsView→MapViewのページ遷移をまたいで保持する必要がある)→ `/[type]/map?buildList=1`へ遷移して**地図の作成モード**に入る。作成モードでは`MapView`が右側に`PlanBuildPanel`(リスト名・選択済みスポットの並び替え/削除・「入力完了」)を出し、**ピンのタップを詳細表示ではなく追加確認ダイアログに回す**(`buildModeRef`で`ensureClusterLayers`が一度だけ束縛するクリックハンドラ`handleSpotSelect`の分岐を切り替える)。並び替えはタッチでも動くようポインタイベントの自前実装(ライブラリ非依存。3本線ハンドル)。「入力完了」で`POST /api/visit-plan-lists`(`{ type, title, description, start_date, end_date, spot_ids }`。spot_idsはseq順)して下書きを消し`/[type]/spots`へ戻る。
320+
作成フローは、訪問予定欄の「+ 訪問予定リストを追加」→ 基本情報モーダル(`VisitPlanListFormModal`。タイトル・説明・期間)→ **下書きをlocalStorageへ保存**(`lib/planListDraft.ts`。「入力完了」までDBに保存しないため、SpotsView→MapViewのページ遷移をまたいで保持する必要がある)→ `/[type]/map?buildList=1`へ遷移して**地図の作成モード**に入る。作成モードでは`MapView`が右側に`PlanBuildPanel`(リスト名・選択済みスポットの並び替え/削除・「入力完了」)を出し、**ピンのタップを詳細表示ではなく追加確認ダイアログに回す**(`buildModeRef`で`ensureClusterLayers`が一度だけ束縛するクリックハンドラ`handleSpotSelect`の分岐を切り替える)。この確認ダイアログには名前だけでなく**スポットの説明とWikipediaの概要への入口**(スポット詳細と同じ`SpotInfoModal`)を出す —— 名前だけでは入れるかどうか決められないため。**表示にはスポットを`api.spots.get`で取り直す**(`addCandidateDetail`)—— 地図の公開スポットはIndexedDBキャッシュ由来で、容量のため`description`も`spot_type_id`も保存されておらず`expandSpot`がnull・空文字を返すため(`lib/spotCacheDb.ts`)。**空の`spot_type_id`のまま種別を引くと必ず見つからず、Wikipediaの可否が種別の設定ではなく既定値(true)で決まってしまう**ので、取り直しが済むまでボタン自体を出さない(ちらつき防止も兼ねる)。Wikipediaの言語・検索の起点は**スポット自身の種別**の設定で引く(重ね表示のピンから開いたときは今の地図の種別と食い違う)。並び替えはタッチでも動くようポインタイベントの自前実装(ライブラリ非依存。3本線ハンドル)。「入力完了」で`POST /api/visit-plan-lists`(`{ type, title, description, start_date, end_date, spot_ids }`。spot_idsはseq順)して下書きを消し`/[type]/spots`へ戻る。
317321

318322
一覧APIは`GET /api/visit-plan-lists?type=<キー>`で、各リストの経由スポットを**seq順の`spot_ids`(UUID配列)**として返す(スポット詳細は呼び出し側が保持済みの一覧から解決するため軽い)。リストのタップで`VisitPlanListDetailModal`(タイトル・説明・期間・経由スポット一覧・編集・削除)。`GET/PATCH/DELETE /api/visit-plan-lists/[id]`は作成者本人のみ。
319323

324+
**基本情報モーダルには出口が2つある**。「スポットを選ぶ/編集 →」が上記の地図へ進む経路で、もう一方の**「保存」は経由スポットに触らずその場で保存する**(新規はPOST、編集はPATCH。`onSaved`で呼び出し元が一覧を読み直す)。タイトルや期間だけ直したいときに地図まで行かなくて済むようにしたもので、**編集で「保存」したときは既存の`spot_ids`をそのまま送り直す** —— PATCHは経由スポットを丸ごと置き換える仕様なので、送らないと全部消える。ボタンは下の行に「保存」「スポットを選ぶ →」の2つを並べ、**キャンセルは見出し行の右端の`×`**へ移した(下の行を前へ進む操作だけにするため)。
325+
320326
**編集は作成フローを再利用する**。詳細の「編集」→ `VisitPlanListFormModal`(`edit`propに既存リストを渡して基本情報を初期表示)→ 下書きに`editingId`と既存の`spotIds`を入れて地図の作成モードへ → スポットを足す/外す/並び替えて「更新」で`PATCH /api/visit-plan-lists/[id]`。PATCHは基本情報を更新し、経由スポットは受け取った`spot_ids`**丸ごと置き換える**(items全削除→seq付きで入れ直し)。作成モードは下書きの`editingId`の有無で「入力完了(新規=POST)」と「更新(編集=PATCH)」を出し分ける(`completeBuild`)。
321327

322328
**作成モード中は、下書きの選択済みスポットを選んだ順に紫の矢印で結んだ経路を地図に描く**(`MapView`の`buildDraftPath`。訪問予定リストの経路表示と同じ`spot-routes`ソース/色で、追加・削除・並び替えに即追従する。保存済みリストの経路表示と同じく、現在地(青丸)の表示中は現在地から下書き先頭のスポットまでも青(`CURRENT_LOCATION_PATH_COLOR`)の線・矢印で結ぶ。線のタップで詳細は開かない — 作成中のタップはピンの追加操作を優先するため`pathKind`を付けない)。編集対象のリスト自身を絞り込みの「訪問予定リスト」経路表示(`filters.planListId`)にしていた場合は、更新前の経路が古い形のまま二重に残らないよう保存済み側は描かない。下書きの経由スポットのピンはシリーズ・カテゴリ等の絞り込みから免除し(`planPathIds`)、本体種別で解決できないスポット(別種別を重ねて追加したもの)は経路表示中のリストと同じ補完(`planListExtraSpots`)で座標を解決する。編集など**スポットが既にある下書きで作成モードに入ったときは、経路全体が見えるよう一度だけ`fitBounds`する**(`buildFitPendingRef`。新規作成で最初のスポットを足したときには動かさない)。

components/AddToPlanListModal.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ export default function AddToPlanListModal({
6666
typeKey={typeKey}
6767
initialSpotIds={[spotId]}
6868
onClose={() => setShowNewForm(false)}
69+
// 地図へ行かず「保存」で作った場合も、このスポットは種として入っている。
70+
// 追加の目的は果たせているので、呼び出し元に知らせて全体を閉じる
71+
onSaved={() => {
72+
onAdded?.();
73+
onClose();
74+
}}
6975
/>
7076
);
7177
}

components/FilterBar.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ export interface SpotFilters {
2929
* 訪問順の経路を描く対象日(`YYYY-MM-DD`のローカル日付)。絞り込みではなく、
3030
* 地図でその日に訪問したスポットを訪問順に矢印で結ぶための対象日。
3131
* null = 経路を表示しない。既定はその日(今日)。地図専用(一覧では使わない)。
32+
* **期間で指定するときは開始日**(終了日は`visitedDateTo`)。
3233
*/
3334
visitedDate: string | null;
35+
/**
36+
* 訪問順の経路の対象期間の終了日(`YYYY-MM-DD`)。null = 単日(`visitedDate`のみ)。
37+
* **開始日と別の列にしてあるので、単日はこれまでどおり`visitedDate`だけで表せる**
38+
* —— 保存済みの条件(この項目が無い)もそのまま単日として読める。
39+
* 旅行のように複数日にまたがる訪問を1本の経路として辿るための指定。
40+
*/
41+
visitedDateTo: string | null;
3442
/**
3543
* 訪問予定リスト(旅程)の経路を描く対象リストのID。絞り込みではなく、
3644
* 訪問日と同様に、そのリストのスポットをリスト順に矢印で結ぶための対象。
@@ -58,6 +66,7 @@ export const DEFAULT_FILTERS: SpotFilters = {
5866
categories: [],
5967
visited: ["unvisited"],
6068
visitedDate: null,
69+
visitedDateTo: null,
6170
planListId: null,
6271
showRoutes: true,
6372
isolate: null,

0 commit comments

Comments
 (0)