Skip to content

Commit 6224631

Browse files
rtcode337claude
andauthored
訪問予定リストの作成・編集中に下書きの経路を地図に描く (#52)
作成モード中、パネルの選択済みスポットを選んだ順に紫の矢印で結んだ経路 (buildDraftPath)を地図に描き、追加・削除・並び替えに即追従させる。 編集対象のリスト自身を経路表示していた場合は、更新前の経路が古い形の まま二重に残らないよう保存済み側は描かない。 - 下書きの経由スポットのピンはシリーズ・カテゴリ絞り込みから免除 - 別種別スポットの座標補完(planListExtraSpots)を下書きにも適用 - スポットが既にある下書き(既存リストの編集など)で作成モードに 入ったときは、経路全体が見えるよう一度だけfitBoundsする Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 00510df commit 6224631

2 files changed

Lines changed: 87 additions & 38 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ CSVインポートは差分更新で、`AdminView`側が事前読み込み済み
224224

225225
**編集は作成フローを再利用する**。詳細の「編集」→ `VisitPlanListFormModal`(`edit`propに既存リストを渡して基本情報を初期表示)→ 下書きに`editingId`と既存の`spotIds`を入れて地図の作成モードへ → スポットを足す/外す/並び替えて「更新」で`PATCH /api/visit-plan-lists/[id]`。PATCHは基本情報を更新し、経由スポットは受け取った`spot_ids`**丸ごと置き換える**(items全削除→seq付きで入れ直し)。作成モードは下書きの`editingId`の有無で「入力完了(新規=POST)」と「更新(編集=PATCH)」を出し分ける(`completeBuild`)。
226226

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

229231
tourist spotsの`series`(A〜E)はこのリポジトリの外で一度だけ計算されたパイプラインの成果物であり、アプリ側が動的に計算するものではない。Wikipedia(ja)月次ページビュー数に基づく相対順位(パーセンタイル)の機械分類(詳細はtravel-log-data/README.md「各データの出典」参照。シリーズの決め方自体はデータの成り立ちの話のためtravel-log本体のREADMEには置いていない)。手動でスポットを追加する場合も、この基準に沿ったシリーズを付けること。

components/MapView.tsx

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,39 +1132,6 @@ export default function MapView({
11321132
[filters, setFilters, planLists, planPathSpotById, fitMapToSpots]
11331133
);
11341134

1135-
// 経路表示中のリストに本体種別で解決できないスポット(別種別を重ねて追加したもの)が
1136-
// あれば、api.spots.get で座標を補完する(経路線から抜けないように)
1137-
useEffect(() => {
1138-
const listId = filters.planListId;
1139-
if (!listId) return;
1140-
const list = planLists.find((l) => l.id === listId);
1141-
if (!list) return;
1142-
const missing = list.spot_ids.filter(
1143-
(id) => !spotById.has(id) && !planListResolvedRef.current.has(id)
1144-
);
1145-
if (missing.length === 0) return;
1146-
// 二重取得を防ぐため先に予約する。取得結果は id をキーにした追記のみの解決
1147-
// キャッシュに足すだけなので、この effect が(リスト変更などで)途中で作り直されても
1148-
// 破棄しない。破棄すると予約だけ残って経路からスポットが抜けたままになる
1149-
missing.forEach((id) => planListResolvedRef.current.add(id));
1150-
Promise.all(missing.map((id) => api.spots.get(id))).then((results) => {
1151-
const fetched = results
1152-
.map((r) => r.data)
1153-
.filter((s): s is Spot => s != null);
1154-
// 取得できなかった id は予約を外し、次に条件が変わったとき再取得できるようにする
1155-
const fetchedIds = new Set(fetched.map((s) => s.id));
1156-
for (const id of missing) {
1157-
if (!fetchedIds.has(id)) planListResolvedRef.current.delete(id);
1158-
}
1159-
if (fetched.length === 0) return;
1160-
setPlanListExtraSpots((prev) => {
1161-
const next = new Map(prev);
1162-
for (const s of fetched) next.set(s.id, s);
1163-
return next;
1164-
});
1165-
});
1166-
}, [filters.planListId, planLists, spotById]);
1167-
11681135
// 地図で訪問予定リストを経路表示中に、そのリスト内のスポットへ新しく訪問記録したら、
11691136
// 自動でそのスポットをリストから外す(訪問済みが経路に残り続けないように)。
11701137
// 表示中のリスト(filters.planListId)にそのスポットが含まれるときだけ動く
@@ -1231,6 +1198,9 @@ export default function MapView({
12311198
// ピンのクリックハンドラ(レイヤー作成時に一度だけ束縛される)から現在の作成モードを
12321199
// 参照するためのref。作成モード中はピンタップを詳細表示でなくリスト追加に回す
12331200
const buildModeRef = useRef(false);
1201+
// 作成モードに入った時点でスポットのある下書き(既存リストの編集など)は、
1202+
// その経路全体が見えるようスポット読み込み後に一度だけfitBoundsする
1203+
const buildFitPendingRef = useRef(false);
12341204
// 作成中は下タブ(NavBar)を隠して、別タブへ移動して入力中の内容を失うのを防ぐ
12351205
const { setHideNav } = useNavVisibility();
12361206
useEffect(() => {
@@ -1241,13 +1211,52 @@ export default function MapView({
12411211
// マウント時/種別切替時に ?buildList=1 なら下書きを読み込んで作成モードに入る
12421212
useEffect(() => {
12431213
if (buildListParam === "1") {
1244-
setBuildDraft(loadPlanListDraft(spotTypeKey));
1214+
const draft = loadPlanListDraft(spotTypeKey);
1215+
setBuildDraft(draft);
1216+
buildFitPendingRef.current = (draft?.spotIds.length ?? 0) > 0;
12451217
// 経路詳細の「編集」から来た場合、基本情報モーダルは閉じて地図の作成モードに移る
12461218
// (同一ページ遷移のため自動では閉じない。SpotsView からの遷移では unmount で消える)
12471219
setEditingPlanList(null);
12481220
}
12491221
}, [buildListParam, spotTypeKey]);
12501222

1223+
// 経路表示中のリスト・作成モード中の下書きに、本体種別で解決できないスポット
1224+
// (別種別を重ねて追加したもの)があれば、api.spots.get で座標を補完する
1225+
// (経路線から抜けないように)
1226+
useEffect(() => {
1227+
const list = filters.planListId
1228+
? planLists.find((l) => l.id === filters.planListId)
1229+
: undefined;
1230+
const targetIds = new Set([
1231+
...(list?.spot_ids ?? []),
1232+
...(buildDraft?.spotIds ?? []),
1233+
]);
1234+
const missing = [...targetIds].filter(
1235+
(id) => !spotById.has(id) && !planListResolvedRef.current.has(id)
1236+
);
1237+
if (missing.length === 0) return;
1238+
// 二重取得を防ぐため先に予約する。取得結果は id をキーにした追記のみの解決
1239+
// キャッシュに足すだけなので、この effect が(リスト変更などで)途中で作り直されても
1240+
// 破棄しない。破棄すると予約だけ残って経路からスポットが抜けたままになる
1241+
missing.forEach((id) => planListResolvedRef.current.add(id));
1242+
Promise.all(missing.map((id) => api.spots.get(id))).then((results) => {
1243+
const fetched = results
1244+
.map((r) => r.data)
1245+
.filter((s): s is Spot => s != null);
1246+
// 取得できなかった id は予約を外し、次に条件が変わったとき再取得できるようにする
1247+
const fetchedIds = new Set(fetched.map((s) => s.id));
1248+
for (const id of missing) {
1249+
if (!fetchedIds.has(id)) planListResolvedRef.current.delete(id);
1250+
}
1251+
if (fetched.length === 0) return;
1252+
setPlanListExtraSpots((prev) => {
1253+
const next = new Map(prev);
1254+
for (const s of fetched) next.set(s.id, s);
1255+
return next;
1256+
});
1257+
});
1258+
}, [filters.planListId, planLists, spotById, buildDraft]);
1259+
12511260
// ピンのタップ: 作成モード中は追加確認へ、それ以外は従来どおり詳細表示へ
12521261
const handleSpotSelect = useCallback((id: string) => {
12531262
if (buildModeRef.current) setAddCandidate(id);
@@ -1369,6 +1378,33 @@ export default function MapView({
13691378
for (const [id, s] of spotById) m.set(id, s);
13701379
return m;
13711380
}, [overlaySpotById, spotById]);
1381+
1382+
// 作成モード中の下書きの経路(選択済みスポットを選んだ順に繋いだもの)。地図に
1383+
// 訪問予定リストと同じ紫の矢印で描き、追加・削除・並び替えに即追従する。
1384+
// スポットは本体+重ね表示+別種別の補完(planListExtraSpots)で解決する
1385+
const buildDraftPath = useMemo(() => {
1386+
if (!buildDraft) return [];
1387+
return buildDraft.spotIds
1388+
.map(
1389+
(id) =>
1390+
spotById.get(id) ?? overlaySpotById.get(id) ?? planListExtraSpots.get(id)
1391+
)
1392+
.filter((s): s is Spot => s !== undefined);
1393+
}, [buildDraft, spotById, overlaySpotById, planListExtraSpots]);
1394+
1395+
// 作成モードに入った時点でスポットのある下書き(既存リストの編集など)は、
1396+
// 経路が解決でき次第、全体が見えるよう一度だけ地図を移動する
1397+
// (新規作成で最初のスポットを足したときに地図が飛ばないよう、入場時のみ)
1398+
useEffect(() => {
1399+
if (!buildFitPendingRef.current) return;
1400+
if (!buildDraft) {
1401+
buildFitPendingRef.current = false;
1402+
return;
1403+
}
1404+
if (buildDraftPath.length === 0) return;
1405+
buildFitPendingRef.current = false;
1406+
fitMapToSpots(buildDraftPath);
1407+
}, [buildDraft, buildDraftPath, fitMapToSpots]);
13721408
// 重ね表示の絞り込み変更をその種別のlocalStorageへ保存しつつstateへ反映する
13731409
// (overlayFiltersが変わると重ね表示の描画effectが再実行され、地図に即反映される)
13741410
const setOverlayFiltersAndSave = useCallback(
@@ -2024,9 +2060,11 @@ export default function MapView({
20242060
const visitPathIds = new Set(
20252061
buildVisitPath(visits, filters, spotById).map((s) => s.id)
20262062
);
2027-
const planPathIds = new Set(
2028-
buildPlanListPath(planLists, filters, planPathSpotById).map((s) => s.id)
2029-
);
2063+
// 作成モード中は、下書きの経由スポットも経路と同様に絞り込みから免除する
2064+
const planPathIds = new Set([
2065+
...buildPlanListPath(planLists, filters, planPathSpotById).map((s) => s.id),
2066+
...(buildDraft?.spotIds ?? []),
2067+
]);
20302068
// 「これだけを表示」中は、その経路のスポットだけに絞る(他のスポット・ルート・
20312069
// もう一方の経路は隠す)。それ以外は従来どおり絞り込み+経路+ルート経由地で出す
20322070
const isolate = effectiveIsolate(filters);
@@ -2089,6 +2127,7 @@ export default function MapView({
20892127
planPathSpotById,
20902128
visits,
20912129
planLists,
2130+
buildDraft,
20922131
visitedIds,
20932132
filters,
20942133
runWhenMapReady,
@@ -2111,8 +2150,11 @@ export default function MapView({
21112150
: [];
21122151
const visitPath =
21132152
isolate === "plan" ? [] : buildVisitPath(visits, filters, spotById);
2153+
// 作成モード中に編集対象のリスト自身を経路表示していた場合は、更新前の経路が
2154+
// 下書きの経路と古い形のまま二重に残らないよう、保存済み側は描かない
21142155
const planListPath =
2115-
isolate === "visit"
2156+
isolate === "visit" ||
2157+
(buildDraft !== null && filters.planListId === buildDraft.editingId)
21162158
? []
21172159
: buildPlanListPath(planLists, filters, planPathSpotById);
21182160

@@ -2131,6 +2173,9 @@ export default function MapView({
21312173
// 現在地(青丸)を表示中は、現在地からリスト先頭のスポットまでも結ぶ
21322174
start: currentLocation,
21332175
},
2176+
// 作成モード中の下書きの経路。kind無し=線のタップで詳細は開かない
2177+
// (作成中のタップはピンの追加操作を優先するため)
2178+
{ path: buildDraftPath, color: PLAN_LIST_PATH_COLOR },
21342179
])
21352180
);
21362181
});
@@ -2144,6 +2189,8 @@ export default function MapView({
21442189
spotById,
21452190
planPathSpotById,
21462191
currentLocation,
2192+
buildDraft,
2193+
buildDraftPath,
21472194
openRouteDetail,
21482195
openPathDetail,
21492196
]);

0 commit comments

Comments
 (0)