| 項目 | 内容 | ||
|---|---|---|---|
| 実行環境 | ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照) |
||
| 実行状態 | ★★完了(2026-09-11。D-120)★★ — ★詳細は文末の「実行状態」表★ | ||
| 実行順 | ★★指示書Y → 指示書W → 指示書Z の後に実行すること★★(詳細は下の「実行順」節)。★★特に指示書Z とは同時に走らせないこと★★(components/features/detail-log/ が真正面から衝突します) |
||
| 対象 | ★★一覧6画面が per_page=200(PAGINATION.maxPerPage)で全件を要求して全部並べているだけで、★ページ送りUIが1つも無い★。★★201件目以降は画面から一生見えません★★★。★本番想定は約360行/日(docs/spec.md:45)なので、★当日1日を「全件」で見ただけで切れます★★ |
||
| ★着手前確認★ | ★★なし★★ — ★「ページ送りを作る」「2ページ目以降ができたらその数だけ表示選択できるようにする」「3ページ目を押したら有りません、みたいなのはだめです」はユーザー回答(2026-09-11)で確定済み★。★★段階0 から通しで実行してよい★★ | ||
| 取得する決定番号 | ★★**着手直前に `grep -nE "^### D-(0\ | 1)[0-9][0-9]" docs/decisions.md \ | tail -3で最終番号を実測すること**★★。★★^### D-0だけでは3桁台(D-100 以降)を拾えない★★。★2026-09-07 にD-068` の二重採番が実際に起きています★。★★見込み番号はここに書きません — 指示書W と指示書Z がそれぞれ番号を取る見込みなので、必ず自分で実測してから採番すること★★ |
| 前提 | D-050(段階ごとに検証してからコミット)/ D-053(グループ・会社の絞り込みは未実装)/ D-054(summary は対応状況フィルタの前に計算する。★本書の禁止事項8 の根拠★)/ D-092 項3-3(db.cleanLogs() の全件取得がボトルネックになりうる。★本書の「性能について」節★)/ CLAUDE.md §5(モックが画面の正)§6-2(期待値表→テスト→実装)§7(閾値のハードコード禁止 / git add -A 禁止)§9.1〜§9.6 |
||
| 段階数 | ★8段階★(段階0〜段階7)。★コミットは段階1 / 2 / 3 / 4 / 5 / 7 の6つ★ | ||
| DB接続 | ★★しません★★。★本書は fixtures(USE_FIXTURES=true)だけで完結します★ |
||
| サーバ変更 | ★★ありません★★。★API 6本は page / per_page を受け取り meta: { total, page, per_page } を返す実装が既に入っています★ |
| 画面 | ファイル | 現在の件数表示 | レスポンス型 |
|---|---|---|---|
| 清掃詳細ログ F-02 | components/features/detail-log/DetailLogScreen.tsx |
★有り★ {total}件(表示 N件) |
ListResponse<CleanLogItem> |
| アラート(事前チェック)F-03 | components/features/alerts/AlertsScreen.tsx |
★無し★ | SummaryListResponse<PrecheckItem, PrecheckSummary> |
| D-Secログ F-06 | components/features/ds-logs/DsLogScreen.tsx |
★無し★ | SummaryListResponse<DsDevice, DsDeviceSummary> |
| D-Secアラート F-07 | components/features/ds-alerts/DsAlertsScreen.tsx |
★無し★ | SummaryListResponse<DsAlert, DsAlertSummary> |
| 施設ロボットマスタ F-08 | components/features/facilities/FacilitiesScreen.tsx |
★有り★ 全{n}施設 |
ListResponse<Facility> |
| スケジュール管理 F-09 | components/features/schedules/SchedulesScreen.tsx |
★無し★ | ListResponse<Schedule> |
★★6画面とも params.set("per_page", String(PAGINATION.maxPerPage)); で 常に200件を要求し、返ってきた分を全部並べているだけ★★です。
★★着手時に自分で実測すること★★(★行番号は変わる前提。grep で取ること★):
grep -rn 'params.set("per_page"' components/features/
grep -rn 'body.meta' components/features/
per_page の上限は 200(PAGINATION.maxPerPage)★★。★★201件目以降は画面に出す手段が存在しない★★docs/spec.md:45「データ量: 約360行/日、年間約13万行、保持2年」)★ → ★★清掃詳細ログで当日1日を「全件」にしただけで切れます★★MAX_DATE_RANGE_DAYS)★ → ★★指定できる範囲(31日 ≒ 11,000行)のほうが、表示できる範囲(200行)より広い★★。 ★ユーザーは「31日ぶん見られる」と思って操作しますが、実際には先頭200件しか見ていません★{total}件(表示 N件) で気づけますが、 残り5画面は総件数すら出していないので★★黙って消えます★★d-watch-mock.jsx にページ送りのUIは1つもありません★lib/i18n.ts に対訳あり。★あれは limit を増やす方式で、ページ送りではありません★)CLAUDE.md §5 は「モック(d-watch-mock.jsx)が画面レイアウト・文言・遷移の正」と定めています★★ → ★★本書はその例外です★★。★★ユーザーの明示の承認(2026-09-11「ページ送りは作成してください」)に基づき、 モックに無いUIを新規に作ります★★。★★この経緯を docs/decisions.md に必ず残すこと(段階7)★★★ページ送りは作成してください★ ★2ページ目以降ができたらその数だけ表示選択できるようにしてください★ ★3ページ目を押したら有りません、みたいなのはだめです★
★★= 実際に存在するページだけを選択肢に出す。存在しないページ番号を1つも描画しない。★★ ★★これが本書の合否基準です★★(段階1 の不変条件テストで機械的に固定します)。
| # | 決めたこと | 理由 |
|---|---|---|
| ★1★ | ★★ページ番号方式(1 2 3 … の番号ボタン)★★ | ★ユーザー要件「2ページ目以降ができたらその数だけ表示選択できるように」=何ページあるか見えて、直接飛べること★。★「もっと見る」方式ではページ数が見えないので要件を満たさない★ |
| ★2★ | ★★共通部品を1つ作り、6画面すべてに入れる★★ | ★★API側が6本とも page / per_page / meta に対応済み★★。★2画面だけに入れると作りが不揃いになり、次に触る人がどちらを真似ればいいか分からなくなる★ |
| ★3★ | ★★1ページあたりの件数は PAGINATION.defaultPerPage(現在 50)から読む★★ |
CLAUDE.md §7(★閾値のハードコード禁止★)。★★50 を画面に直書きしないこと★★ |
| ★4★ | ★★総ページ数 = Math.ceil(meta.total / per_page)★★。★総ページ数が1以下ならボタンを出さない★ |
★1ページしかないのにボタンを出すと、押せないUIが並ぶだけ★ |
| ★5★ | ★★ページ数が多いときは先頭・末尾・現在の前後を出し、間を「…」で省略する★★ | ★223ページ分のボタンは並べられない(年間13万行 ÷ 50 = 2,600ページ)★ |
| ★6★ | ★★「…」は button にしない★★。★★描画する数字は必ず実在するページ★★ |
★★ユーザー要件「3ページ目を押したら有りません、みたいなのはだめです」★★ |
| ★7★ | ★★件数表示を6画面すべてに出す。形は「全{total}件中 {from}〜{to}件」に統一★★ | ★現状バラバラ(清掃詳細ログ {total}件(表示 N件) / 施設 全{n}施設 / 残り4画面は無し)★。★モックの「{n}件 / 全{n}件」とも食い違っている★ → ★★1つに揃える★★ |
| ★8★ | ★★meta.total を使う。summary.total を絶対に使わない★★ |
★アラート / D-Secログ / D-Secアラートの3本は summary をフィルタ前に計算している(D-054)★。★★app/api/prechecks/route.ts のコメントが「meta.total と summary.total は意味が違う」と明記しています★★ |
| ★9★ | ★★CSV は全件のまま。画面に注記を出す★★ | ★app/api/clean-logs/export/route.ts の冒頭コメント「page / per_page は無視する」が契約★。★★変えないこと★★ |
| ★10★ | ★★ページ番号を URL に持たせない(React の state だけで持つ)★★ | ★既存6画面はフィルタも検索もURLに持っていない(全部 useState)★。★URL同期を入れると useSearchParams の Suspense 境界が要り、6画面の構造を変えることになる★ → ★★本書の範囲を超える。申し送り①に回す★★ |
| 案 | 却下理由 |
|---|---|
★PAGINATION.maxPerPage を 200 から 10000 に上げるだけ★ |
★★問題を先送りするだけ★★。★年間13万行では結局切れるし、1万行を1画面に描くとブラウザが固まる★ |
| ★無限スクロール★ | ★★ユーザー要件「その数だけ表示選択できるように」を満たさない★★(何ページあるか見えない・特定のページに戻れない)。★翌朝確認型の運用では「昨日見た位置に戻る」が要る★ |
| ★「もっと見る」で追記していく方式(レポート画面と同じ)★ | ★同上。★★総ページ数が見えない★★★。★レポート画面のあれは「上位10件+全部」の2状態であって、ページ送りではない★ |
| ★サーバ側にカーソルページングを足す★ | ★★d-watch-api.yaml の変更が要る★★。★★page / per_page / meta で既に足りている★★ので、契約を変える理由が無い |
| ★ページ送りと同時に SQL 絞り込みも入れる★ | ★★下の「性能について」節を読むこと。同時にやると壊れます★★ |
★★ページ送りを付けても、DB からは毎リクエスト全件読みます★★。
DataSource は引数を取らない契約になっており、prismaSource.cleanLogs() は全期間・全件の findMany()★★コード自身が警告を書いています★(★着手時に実測すること★):
grep -rn "13万" lib/db/ # → lib/db/index.ts / lib/db/clean-log-mapping.ts の2箇所に # 「年間13万行(docs/spec.md:45)の実データではここがボトルネックになりうる」
★絞り込み・並べ替え・ページ切り出しはすべてAPI ルートの中で JavaScript の配列操作として走っています★ (filterCleanLogs() → sorted.slice(start, start + perPage))
★★つまり本書で解決するのは「見えない行が見えるようになる」ことだけで、★性能は1ミリも改善しません★★★。 ★★むしろページを繰るたびに全件取得が走るので、体感は悪化する方向です★★。
★★これは別課題(SQL に絞り込みを降ろす改修)として切り離します★★。
★★ページ送りと性能改善を同時にやらないこと★★ — ★連続日数の計算(
consecutive_problem_days)が全期間を走査して数えているため、 「期間で絞って取る」改修を入れると★★連続日数が同時に壊れます★★★。 ★2つが同じコミットに入ると、どちらが壊したのか切り分けられなくなります★。 ★本書はlib/db/に1行も触りません★(禁止事項5)。
// lib/config/index.ts(★着手時に grep -n "PAGINATION" lib/config/index.ts で位置を取ること★)
export const PAGINATION = {
defaultPerPage: 50,
maxPerPage: 200,
} as const;
★★本書で maxVisiblePages を1つ足します(段階1)★★。★それ以外は変えません★。
| API | ページ切り出しの場所 | meta |
|---|---|---|
/api/clean-logs |
filtered.slice(start, start + perPage) |
有り |
/api/prechecks |
sorted.slice(...) |
有り(★summary と併記。コメントで違いを明記済み★) |
/api/ds-devices |
sorted.slice(...) |
有り(同上) |
/api/ds-alerts |
sorted.slice(...) |
有り(同上) |
/api/facilities |
paginateFacilities()(lib/facilities/query.ts) |
有り |
/api/schedules |
sorted.slice(...) |
有り |
★★6本とも parse*Query() が★page = Math.max(1, toInt(searchParams.get("page"), 1))★ ★perPage = Math.min(PAGINATION.maxPerPage, Math.max(1, toInt(..., PAGINATION.defaultPerPage)))★ という同一の形で実装済みです★★。★★サーバは1行も変えません★★。
★確認コマンド★:
grep -rn 'searchParams.get("page")' lib/
grep -rn "meta: { total, page, per_page: perPage }" app/api/
meta.total と summary.total は意味が違う(★踏むと総ページ数がずれます★)★★app/api/prechecks/route.ts の既存コメント(★原文。着手時に実測すること★):
★meta.total = 一覧(対応状況フィルタ後)の総件数。ページング計算用★ ★summary.total = 日付範囲で絞った当日の全件(対応状況フィルタの影響を受けない)★ ★status=all のときだけ一致する。status=open 等で絞ると meta.total < summary.total になりうる★
★★/api/ds-devices / /api/ds-alerts にも同趣旨のコメントがあります★★(D-054)。
★★アラート / D-Secログ / D-Secアラートの3画面は、現在 body.summary しか読んでおらず body.meta を捨てています★★ → ★★段階4 で meta.total を持つ state を新設すること★★。★summary.total を流用しないこと★。
lib/i18n.ts に既存の解説コメントがあります(★原文。着手時に実測すること★):
★方式A(プレースホルダ + replace)★推奨★★ ★
t("他{n}件を表示 →").replace("{n}", String(n))★ ★語順が言語ごとに違っても対訳側で吸収できるため、こちらを推す★
★★本書も方式A を使います★★。★★方式B(前後を分割して連結)は使わないこと★★。
★既存の前例★: "他{n}件を表示 →" / "全{n}施設を表示 →" / "全{n}施設" / "{n}分前" / "{n}日以上連続" / "第{n}回チェック"
★★注意: 本書のキーはプレースホルダが3つ({total} / {from} / {to})で、既存の {n} 1つより多い★★。 ★.replace() を3回つなげること★。★★replaceAll は不要(各プレースホルダは1回しか出ない)★★。
// vitest.config.ts
test: {
environment: "node",
include: ["lib/**/*.test.ts", "app/**/*.test.ts"],
}
★★components/** は include に入っておらず、environment は node(jsdom ではない)で、 React Testing Library も入っていません★★ →
lib/pagination/ の純粋関数に置くこと★★。 ★そこならテストが走ります★package.json に足さないこと★★(禁止事項10)。 ★依存追加は本書の範囲外です★| 画面 | 空表示 |
|---|---|
| 清掃詳細ログ | CleanLogTable の中に「該当するログはありません」 |
| アラート | 「該当するアラートはありません」 |
| D-Secログ | 「該当するデバイスはありません」 |
| D-Secアラート | 「該当するアラートはありません」 |
| 施設 | 「該当する施設はありません」 |
| スケジュール | 「該当するスケジュールはありません」 |
★★だから total === 0 のときは部品ごと出さない(件数表示もページ送りも出さない)★★。 ★「全0件中 0〜0件」と空表示が二重に出るのを避けます★。
components/features/reports/ReportsScreen.tsx は★期間変更・検索実行のたびに setPerfShowAll(false) を呼んでいます★ (★3箇所。着手時に grep -n "setPerfShowAll(false)" components/features/reports/ReportsScreen.tsx で実測すること★)。
★★同じ発想です★★ — ★★フィルタを変えたのにページ3に留まっていると、件数が減ったときに 存在しないページに居座って空表示になります★★。★これはユーザーが名指しで禁じた状態です★。
★★清掃詳細ログ / D-Secログ / 施設 / スケジュールの検索欄は debounce(300ms)があります★★ → ★★debounce の反映時にもページを戻すこと★★(段階3 の 3-4 に具体的な書き方を示します)。
| # | 落とし穴 | ||
|---|---|---|---|
| ★1★ | ★★Math.ceil(0 / 50) は 0、Math.ceil(total / 0) は Infinity★★。★★perPage が0以下のときは総ページ数を0にすること★★(防御。純粋関数のテストに入れる) |
||
| ★2★ | ★★total === 0 のとき from を 1 にしないこと★★。★(1-1)*50+1 = 1 になるので、素直に書くと「全0件中 1〜0件」が出ます★ |
||
| ★3★ | ★★to は Math.min(page * perPage, total)★★。★items.length を使わないこと★(★ローディング中や失敗時に items が空になり、件数だけ0に見える★) |
||
| ★4★ | ★★「…」が1ページだけを隠す形にしないこと★★(1 … 3 4 5 の「…」が 2 だけを隠している状態)。★数字1つぶんの幅に「…」を出すのは、押せる番号が減るだけで損★。★段階1 の不変条件テストで機械的に禁じます★ |
||
| ★5★ | ★★フィルタ変更時に setPage(1) を useEffect で後追いすると、古いページのままフェッチが1回余計に飛びます★★。★★state を変えるその場で一緒に戻すこと★★(3-4 参照) |
||
| ★6★ | ★★page を queryString の useMemo の依存に入れ忘れると、ボタンを押しても取り直しません★★(★useCallback の依存は queryString(★DetailLogScreen.tsx だけは [queryString, rangeDays] の2本。rangeDays は日付範囲の検証に要るので消さないこと★)なので、queryString が変わらないと fetch* も変わらない★) |
||
| ★7★ | ★★清掃詳細ログの CSV リンクは queryString をそのまま href に入れています★★。★page が混ざりますが、export は page / per_page を無視する契約なので動作は変わりません★。★★リンクを分岐させないこと(余計な差分になります)★★ |
||
| ★8★ | ★★d-watch-api.yaml を見て「page が無い」と思わないこと★★ — ★着手時に `grep -n "per_page\ |
page" d-watch-api.yaml \ | head -30` で実測すること★。★★もし本当に無ければ、それは別の不整合なので、本書を止めて確認を求めること(推測で YAML を書き換えないこと)★★ |
| ★9★ | ★★components/ui/ の既存部品(Pill / FilterButton / GradeBadge / StatCard / ProgressBar)はどれも useTheme() を使っていません★★。★色は CSS 変数(var(--ac) / var(--bd) / var(--tx-m))か props の color★。★★本書の部品は文言のために useTheme() を使います(理由は 2-1 に明記)★★ |
| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| ★1★ | ★★git add -A を使うこと★★ |
CLAUDE.md §7。★過去に別作業の途中ファイルを巻き込む事故が起きている★。★★必ずファイルパスを明示すること★★ |
| ★2★ | ★★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / 他の指示書 / `.claude/worktrees/` に触ること**★★ |
★★すべて別作業のもの★★。★★読むのは自由。編集・削除・コミットへの同梱は禁止★★ |
| ★3★ | ★★件数・ページ数を直書きすること(50 / 200 / 7 / Math.ceil(total / 50) 等)★★ |
CLAUDE.md §7。★★lib/config/index.ts の PAGINATION から読むこと★★ |
| ★4★ | ★★summary.total を総ページ数の計算に使うこと★★ |
D-054。★summary はフィルタ前の集計で、一覧の件数とは別物★。★★総ページ数がずれます★★ |
| ★5★ | ★★存在しないページ番号を描画すること★★ | ★★ユーザーが名指しで禁じた状態★★。★段階1 の不変条件テストで固定します★ |
| ★6★ | ★★lib/fixtures/ を1行でも変更すること★★ |
CLAUDE.md §9.5。★★「2ページ目が出ないから件数を増やす」は禁止★★(検証方法は段階6 に書いてあります) |
| ★7★ | ★★d-watch-api.yaml / `app/api//lib//query.ts/lib/db/*/lib/types/index.ts` を変更すること★★ |
★★変える必要がありません★★ — ★API は6本とも対応済み★。★★変えたくなったら、それは本書の設計が違うサインなので、止めて確認を求めること★★ |
| ★8★ | ★★レポート画面(`components/features/reports/)とダッシュボード(components/features/dashboard/`)に手を入れること★★ |
★レポートは limit 方式で別の作り(申し送り②)★。★ダッシュボードは per_page=5 の意図的な上位5件★ |
| ★9★ | ★★点検記録(F-05)を対象に含めること★★ | ★画面もAPIも未実装。★★後回しと決定済み★★★ |
| ★10★ | ★★package.json に依存を足すこと(jsdom / @testing-library / ページャライブラリ等)★★ |
★★純粋関数+CSS Module で足ります★★。★依存追加は本書の範囲外★ |
| ★11★ | ★★docs/decisions.md / docs/handoff.md を段階7 より前に編集すること★★ |
★★別担当(指示書W / 指示書Z)が番号を取る可能性があります★★。★段階7 でも「自分の追記だけを足す」こと★ |
| ★12★ | ★★PAGINATION.defaultPerPage を 50 以外にしたままコミットすること★★ |
★段階6 の検証で一時的に小さくすることを許可しますが、★★戻し忘れると本番の1ページ件数が変わります★★★ |
★段階0(前提の確認と実測。★読むだけ・コミットなし★) → 段階1(★lib/pagination/ の純粋関数。★★期待値表 → テスト → 実装★★) → 段階2(★components/ui/Pagination.tsx と i18n。★画面にはまだ入れない★) → 段階3(★清掃詳細ログ F-02 に入れて作法を確定させる★) → 段階4(★summary 系3画面 F-03 / F-06 / F-07★) → 段階5(★ListResponse 系2画面 F-08 / F-09★) → 段階6(★6画面の実測検証。★コミットなし★) → 段階7(記録と push)★
★★段階1・2・3・4・5・7 でそれぞれ1コミット(計6コミット)★★。 ★★段階ごとに npx tsc --noEmit / npm run lint / npm test を実行し、通ってからコミットすること★★(D-050)。
★★期待値表 → テスト → 実装 の順序を崩さないこと★★(
CLAUDE.md§6-2 / §9.4)。 ★★段階1 は「期待値表を書く → テストを書く → 落ちることを確認する → 実装する」★★。★★なぜこの順番なのか★★
- ★段階1 を独立させるのは、★★ページ番号の並びは純粋関数でテストが完結する★★から。 ★ここだけで止めても何も壊れない★
- ★段階2 を独立させるのは、★★部品を作った時点では画面の見た目が1ピクセルも変わらない★★から。 ★レビューで「部品の設計」と「画面への適用」を分けて見られる★
- ★段階3 を1画面だけにするのは、★★最初の1画面で作法(state の持ち方・ページを1に戻す場所・置き場所)を確定させる★★ため。 ★★6画面を一度に書くと、間違った作法が6箇所に増えます★★
- ★段階4 と段階5 を分けるのは、★★summary 系3画面は
metaを捨てているので新しい state が要る★★から (★ListResponse 系2画面とは作業が違う★)★★段階の並びは実装者が判断して入れ替えてよい★★(★上は推奨順です★)。 ★★ただし動かせない制約が2つあります★★ — ★★① 段階1(純粋関数とテスト)を段階2 より先にやること★★ ★★② 段階3(1画面目)を段階4・5(横展開)より先にやること★★
| # | 指示書 | 順序 | ★衝突するファイル★ |
|---|---|---|---|
| ★1★ | docs/handoff/指示書Y_DATETIMEを日本時間として読む.md |
★本書より先★ | lib/db/ / lib/format/ / scripts/ — ★★本書とは重ならない★★ |
| ★2★ | docs/handoff/指示書W_連続問題施設を実DBから作る.md |
★本書より先★ | ★★lib/i18n.ts★★ / d-watch-api.yaml / lib/types/index.ts / components/features/reports/ReportsScreen.tsx — ★★lib/i18n.ts が真正面から衝突します★★ |
| ★3★ | docs/handoff/指示書Z_エリア別完了率を実装する.md |
★本書より先★ | ★★components/features/detail-log/★★ — ★★本書の段階3 と同じディレクトリ★★ |
| ★4★ | ★★本書(指示書AA)★★ | ★★最後★★ | — |
★★着手時にやること★★:
# 3本の実行状態を実測する(本書作成時点の記録: Y=完了 / W=未着手 / Z=起草中) grep -n "実行状態" -A 3 docs/handoff/指示書Y_*.md | head -20 grep -n "実行状態" -A 3 docs/handoff/指示書W_*.md | head -20 ls docs/handoff/ | grep "指示書Z" git status --short # ★他の担当の未コミット差分が無いことを確認する★
★★3本のいずれかが未完了なら、本書に着手しないこと★★。 ★★特に指示書Z と同時に走らせると components/features/detail-log/ で確実に衝突します★★。
★★指示書Y / W / Z は ★読むだけ。編集しないこと★★(禁止事項2)。
| # | 対象 | 何を見るか |
|---|---|---|
| 1 | CLAUDE.md §5 / §6 / §7 / §9.1〜§9.6 |
★モックが正であること(★本書はその例外★)/期待値表→テスト→実装/ハードコード禁止/ディレクトリ構成★ |
| 2 | lib/config/index.ts の PAGINATION |
★defaultPerPage / maxPerPage の現在値を実測すること★ |
| 3 | vitest.config.ts |
★★include が lib/** と app/** だけであることを自分の目で確認すること★★ |
| 4 | lib/i18n.ts の「数値が文中に入るラベル」節 |
★方式A の作法★ |
| 5 | components/ui/ の5部品と .module.css |
★★命名・props の形・CSS 変数の使い方を真似ること★★ |
| 6 | app/api/prechecks/route.ts の meta / summary コメント |
★禁止事項4 の根拠★ |
| 7 | app/api/clean-logs/export/route.ts の冒頭コメント |
★「page / per_page は無視する」★ |
| 8 | 6画面のファイル | ★★state の持ち方・debounce・フィルタのUIを1つずつ数え上げること(0-2)★★ |
★★着手時に自分で数えること★★(★下は監督の実測。★★変わっている前提で確認すること★★★):
| 画面 | ページを1に戻すべき操作 |
|---|---|
| 清掃詳細ログ F-02 | dateFrom / dateTo の変更 / 判定フィルタ3ボタン(problem / all / critical)/ ★★facilityName・model・serial のdebounce 反映時★★ |
| アラート F-03 | dateFrom / dateTo / 対応状況3ボタン(open / all / resolved) |
| D-Secログ F-06 | ★★q のdebounce 反映時★★ / ステータスフィルタ |
| D-Secアラート F-07 | 種別フィルタ / 対応状況フィルタ |
| 施設 F-08 | ★★q のdebounce 反映時★★ |
| スケジュール F-09 | ★★q のdebounce 反映時★★ |
★★ソート操作のUIは現状どの画面にもありません★★(★並べ替えはサーバ固定★)。 ★★将来ソートUIが付いたら、そこでも setPage(1) すること★★ — ★段階7 の申し送りに書くこと★。
# ★fixtures の件数を実測する(★数字を推測しないこと★)★
npx tsx -e "import('./lib/fixtures/index.ts').then(m=>console.log(Object.entries(m).map(([k,v])=>[k,Array.isArray(v)?v.length:typeof v])))"
★上が動かない場合は、各画面を開いて総件数を目で控えること★。 ★★控えるもの★★: 6画面それぞれの「フィルタ既定状態での総件数」。 ★★段階6 で「全{total}件中 …」の total がこの数と一致すること★★を確認します。
git status --short がクリーン★vitest.config.ts の include が lib/** と app/** だけであることを確認した★★| ファイル | 内容 |
|---|---|
lib/pagination/page-items.ts |
★★純粋関数3本と型★★ |
lib/pagination/__tests__/page-items.test.ts |
★★期待値表そのままのテスト★★ |
lib/config/index.ts |
★PAGINATION に maxVisiblePages を1つ足すだけ★ |
★★lib/types/index.ts に型を置かないこと★★ — ★あそこは d-watch-api.yaml のスキーマ名に揃える場所(CLAUDE.md §9.2)で、 PageItem は API 契約の型ではありません★。
★★import は @/lib/pagination/page-items と直接書くこと★★(★lib/clean-logs/query.ts 等と同じ作法。barrel は作らない★)。
// lib/config/index.ts の PAGINATION に足す
export const PAGINATION = {
defaultPerPage: 50,
maxPerPage: 200,
/**
* ★ページ番号ボタンを省略せずに全部並べる上限★
*
* これを超えたら先頭・末尾・現在の前後だけを出し、間を「…」で省略する。
* ★★奇数かつ 7 以上であること★★(`buildPageItems()` が左右対称に切り出すため。
* テストで固定している)。
*
* ★なぜ設定値にするのか★: `CLAUDE.md` §7(閾値のハードコード禁止)。
* 画面幅の都合で並べる数を変えたくなったとき、直す場所をここ1箇所にする。
*
* ★消費者★: `lib/pagination/page-items.ts` の `buildPageItems()` のみ。
*/
maxVisiblePages: 7,
} as const;
totalPageCount(total: number, perPage: number): number| # | total | perPage | ★期待★ | 意図 |
|---|---|---|---|---|
| A-1 | 0 | 50 | ★0★ | ★★0件は0ページ(1ページではない)★★ |
| A-2 | 1 | 50 | 1 | 最小 |
| A-3 | 50 | 50 | ★1★ | ★★ちょうど per_page の倍数★★ |
| A-4 | 51 | 50 | ★2★ | ★★端数あり(+1件で2ページ)★★ |
| A-5 | 100 | 50 | ★2★ | ★★ちょうど倍数★★ |
| A-6 | 101 | 50 | ★3★ | ★端数あり★ |
| A-7 | 33 | 50 | 1 | ★fixtures 相当(ローカルでは1ページ)★ |
| A-8 | 360 | 50 | ★8★ | ★★本番の1日ぶん(docs/spec.md:45)★★ |
| A-9 | 11150 | 50 | ★223★ | ★大量ページ★ |
| A-10 | 10 | 5 | 2 | perPage が変わっても効く |
| A-11 | 10 | 0 | ★0★ | ★★防御。Infinity を返さないこと(落とし穴1)★★ |
| A-12 | 10 | -1 | ★0★ | ★防御★ |
| A-13 | -1 | 50 | ★0★ | ★防御★ |
pageRange(total, page, perPage): { from: number; to: number }| # | total | page | perPage | ★期待★ | 意図 |
|---|---|---|---|---|---|
| B-1 | 0 | 1 | 50 | ★{from:0, to:0}★ | ★★0件で from:1 にしないこと(落とし穴2)★★ |
| B-2 | 1 | 1 | 50 | {1, 1} | 最小 |
| B-3 | 50 | 1 | 50 | {1, 50} | ちょうど1ページ |
| B-4 | 51 | 1 | 50 | {1, 50} | 1ページ目 |
| B-5 | 51 | 2 | 50 | ★{51, 51}★ | ★★端数の最終ページ★★ |
| B-6 | 100 | 2 | 50 | {51, 100} | ちょうど倍数の最終ページ |
| B-7 | 360 | 8 | 50 | ★{351, 360}★ | 本番相当 |
| B-8 | 51 | ★3★ | 50 | ★{51, 51}★ | ★★存在しないページを渡されたら最終ページに丸める★★ |
| B-9 | 51 | ★0★ | 50 | ★{1, 50}★ | ★0以下は1ページ目に丸める★ |
buildPageItems(page: number, totalPages: number): PageItem[]★★表記★★: 数字は { kind: "page", page: n }、… は { kind: "ellipsis" }。 ★★現行 maxVisiblePages = 7 での期待値です★★。
| # | page | totalPages | ★期待★ | 意図 |
|---|---|---|---|---|
| C-1 | 1 | ★0★ | ★[]★ |
★★0件★★ |
| C-2 | 1 | 1 | [1] |
★1ページ(★部品側では出さない。1-5 参照★)★ |
| C-3 | 1 | 2 | [1, 2] |
★★2ページ(ユーザー要件の最小ケース)★★ |
| C-4 | 2 | 2 | [1, 2] |
現在ページが末尾 |
| C-5 | 1 | 7 | [1,2,3,4,5,6,7] |
★★省略しない上限ちょうど★★ |
| C-6 | 4 | 7 | [1,2,3,4,5,6,7] |
現在ページが中央 |
| C-7 | 1 | 8 | ★[1,2,3,4,5, …, 8]★ |
★★省略が始まる最初のケース★★ |
| C-8 | 4 | 8 | [1,2,3,4,5, …, 8] |
先頭寄りの境界 |
| C-9 | ★5★ | 8 | ★[1, …, 4,5,6,7,8]★ |
★★末尾寄りに切り替わる境界★★ |
| C-10 | 8 | 8 | [1, …, 4,5,6,7,8] |
現在ページが末尾 |
| C-11 | 1 | 9 | [1,2,3,4,5, …, 9] |
— |
| C-12 | ★5★ | 9 | ★[1, …, 4,5,6, …, 9]★ |
★★中央(「…」が2つ出る最小ケース)★★ |
| C-13 | 9 | 9 | [1, …, 5,6,7,8,9] |
— |
| C-14 | 1 | ★223★ | ★[1,2,3,4,5, …, 223]★ |
★★大量ページ・先頭★★ |
| C-15 | 4 | 223 | [1,2,3,4,5, …, 223] |
先頭寄りの境界 |
| C-16 | 5 | 223 | ★[1, …, 4,5,6, …, 223]★ |
中央に切り替わる境界 |
| C-17 | ★112★ | 223 | ★[1, …, 111,112,113, …, 223]★ |
★★大量ページ・中央★★ |
| C-18 | 219 | 223 | [1, …, 218,219,220, …, 223] |
末尾寄りに切り替わる直前 |
| C-19 | ★220★ | 223 | ★[1, …, 219,220,221,222,223]★ |
★末尾寄りの境界★ |
| C-20 | ★223★ | 223 | ★[1, …, 219,220,221,222,223]★ |
★★大量ページ・末尾★★ |
| C-21 | ★0★ | 5 | [1,2,3,4,5](page=1 扱い) |
★範囲外を丸める★ |
| C-22 | ★99★ | 5 | [1,2,3,4,5](page=5 扱い) |
★範囲外を丸める★ |
★★for (totalPages = 1; totalPages <= 300; totalPages++) × for (page = 1; page <= totalPages; page++) を 総当たりして、以下をすべて満たすこと★★:
| # | 不変条件 |
|---|---|
| ★D-1★ | ★★kind === "page" の値がすべて 1 <= n <= totalPages に入る★★ ← ★★「3ページ目を押したら有りません」の禁止★★ |
| D-2 | ★必ず 1 を含む★ |
| D-3 | ★必ず totalPages を含む★ |
| D-4 | ★必ず page(現在ページ)自身を含む★ |
| D-5 | ★数字は狭義単調増加(重複なし・逆転なし)★ |
| ★D-6★ | ★★「…」が飛ばすページ数は必ず2以上★★(★1ページだけを「…」で隠さない。落とし穴4★) |
| D-7 | ★「…」が連続しない★ |
| D-8 | ★先頭と末尾は必ず数字(「…」で始まらない・終わらない)★ |
| D-9 | ★要素数は totalPages <= maxVisiblePages なら totalPages、それ以外は常に maxVisiblePages★(★幅がガタつかない★) |
| D-10 | ★PAGINATION.maxVisiblePages が奇数かつ7以上★(★設定値そのものへのアサーション★) |
W = PAGINATION.maxVisiblePages // 7
half = (W + 1) / 2 // 4
buildPageItems(page, totalPages):
if totalPages <= 0: return []
cur = clamp(page, 1, totalPages)
if totalPages <= W:
return [1 .. totalPages]
if cur <= half:
// 先頭寄り: 1..(W-2), …, totalPages
return [1 .. W-2] + [ELLIPSIS] + [totalPages]
if cur >= totalPages - (half - 1):
// 末尾寄り: 1, …, (totalPages-(W-3))..totalPages
return [1] + [ELLIPSIS] + [totalPages-(W-3) .. totalPages]
// 中央: 1, …, cur-1, cur, cur+1, …, totalPages
side = (W - 5) / 2 // 1
return [1] + [ELLIPSIS] + [cur-side .. cur+side] + [ELLIPSIS] + [totalPages]
★★7 / 4 / 5 / 2 を関数の中に直書きしないこと★★ — ★★すべて W から導くこと★★(禁止事項3)。
型:
export type PageItem =
| { kind: "page"; page: number }
| { kind: "ellipsis"; key: string }; // key は React の key 用("gap-left" / "gap-right" 等)
★★kind: "ellipsis" に page を持たせないこと★★ — ★持たせると「…を押したら何ページに飛ぶ?」という 存在しない問いが生まれます★。★★型で押せなくしておくのが一番確実です★★。
npm test が全パス★lib/pagination/page-items.ts に 7 / 50 / 200 の直書きが無い★npx tsc --noEmit = 0 / npm run lint = 0★`git add lib/config/index.ts lib/pagination/page-items.ts lib/pagination/__tests__/page-items.test.ts`★
Pagination と i18n★(1コミット)★★この段階では画面の見た目は1ピクセルも変わりません★★(★部品を作るだけ★)。
| ファイル | 内容 |
|---|---|
components/ui/Pagination.tsx |
部品本体 |
components/ui/Pagination.module.css |
★CSS Module(★既存5部品と同じ作法★)★ |
"use client";
interface PaginationProps {
/** ★★`meta.total` を渡すこと。`summary.total` を渡さないこと★★(D-054 / 禁止事項4) */
total: number;
/** 現在ページ(1始まり) */
page: number;
/** ★1ページあたりの件数。`PAGINATION.defaultPerPage` を渡す。直書き禁止★ */
perPage: number;
onPageChange: (page: number) => void;
}
export function Pagination({ total, page, perPage, onPageChange }: PaginationProps) { … }
★★描画の規則★★:
| 条件 | 描画 |
|---|---|
total === 0 |
★★null(何も出さない)★★ — ★各画面の空表示に任せる(調査6)★ |
| ★総ページ数が1★ | ★★件数表示だけ出す。ページ送りのボタンは出さない★★ |
| ★総ページ数が2以上★ | ★件数表示 + 「前へ」+ 番号 + 「…」+「次へ」★ |
★★細部★★:
display:flex; justify-content:space-between; align-items:center;)<span aria-hidden="true"> で出す。<button> にしないこと★★(禁止事項5)disabled かつ aria-current="page"★(★押しても何も起きない・スクリーンリーダーに現在地が伝わる★)disabled、「次へ」は最終ページで disabled★onPageChange に渡す値は必ず 1 <= n <= totalPages★★(★部品の中で clamp すること★)var(--ac) / var(--bd) / var(--tx-m) / var(--tx-d))★。 ★★THEME_TOKENS を import しないこと★★(★components/ui/ の既存部品はどれも使っていない。落とし穴9★)useTheme() は t の取得のためだけに使う★★。 ★★理由: 件数表示の文言を6画面に散らすと .replace() の連鎖が6箇所に増える★★。 ★★components/ui/ で useTheme() を使う最初の部品になるので、その旨を docstring に1行書くこと★★"use client" を付けること★(★useTheme() を使うため★)// lib/i18n.ts の EN に足す
// ---- 一覧のページ送り(指示書AA)----
// ★プレースホルダが3つある(既存の {n} 1つより多い)。replace を3回つなげる★
// t("全{total}件中 {from}〜{to}件")
// .replace("{total}", String(total))
// .replace("{from}", String(from))
// .replace("{to}", String(to))
"全{total}件中 {from}〜{to}件": "{from}–{to} of {total}",
前へ: "Prev",
次へ: "Next",
// ★CSVは表示中のページではなく全件(app/api/clean-logs/export/route.ts が page/per_page を無視する契約)★
"CSVは絞り込み条件に一致する全件を出力します(表示中のページだけではありません)":
"CSV exports all rows matching the filters, not just the current page",
★★前へ / 次へ が既存キーと衝突しないことを grep -n "前へ\|次へ" lib/i18n.ts で確認すること★★ (★監督の実測では0件★)。
page / perPage を部品の内部 state にすること★★(★★親が持つ。部品は表示と通知だけ★★)fetch すること★★(★★純粋な表示部品にすること★★)components/ui/Pagination.tsx / .module.css を作った★total === 0 で null を返す★<button> になっていない★lib/i18n.ts に4キーを足した(★既存キーを1つも消していない★)★50 / 200 / 7 の直書きが無い★npx tsc --noEmit = 0 / npm run lint = 0 / npm test 全パス★git add components/ui/Pagination.tsx components/ui/Pagination.module.css lib/i18n.ts★★★対象: components/features/detail-log/DetailLogScreen.tsx★★ (★★指示書Z が同じディレクトリを触ります。指示書Z の完了を確認してから着手すること★★)
per_page と page を送る- params.set("per_page", String(PAGINATION.maxPerPage));
+ params.set("per_page", String(PAGINATION.defaultPerPage));
+ params.set("page", String(page));
★★page を useMemo の依存配列に入れること★★(★★落とし穴6。入れ忘れるとボタンを押しても取り直しません★★)。
const [page, setPage] = useState(1);
★★total は既に useState(0) で持っています★★(setTotal(body.meta.total))。★そのまま使うこと★。
fetchLogs() の成功時に:
setItems(body.items); setTotal(body.meta.total); // ★★フィルタ外の要因(データ更新など)で総ページ数が減り、存在しないページに // 取り残された場合の保険。最終ページに寄せて取り直す★★ const pages = totalPageCount(body.meta.total, body.meta.per_page); if (pages > 0 && body.meta.page > pages) setPage(pages);
★★pages > 0 の判定を落とさないこと★★(★0件のときに setPage(0) して無限ループになります★)。
★★useEffect で後追いしないこと★★。★★state を変えるその場で一緒に戻すこと★★。
★debounce のあるもの(施設名・機種・シリアル)★ — ★★タイムアウトのコールバックの中で一緒に戻す★★:
useEffect(() => {
const id = window.setTimeout(() => {
setDebouncedFacility(facilityName);
setPage(1); // ★★debounce の反映と同時に戻す(React が1回の再描画にまとめる)★★
}, 300);
return () => window.clearTimeout(id);
}, [facilityName]);
★debounce の無いもの(日付・判定フィルタ)★ — ★ハンドラの中で一緒に戻す★:
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
onClick={() => { setGradeFilter("problem"); setPage(1); }}
★★0-2 で数え上げた操作を1つ残らず潰すこと★★。
- <div className={styles.footer}>
- {total}件
- {items.length < total ? `(表示 ${items.length}件)` : ""}
- </div>
+ <Pagination
+ total={total}
+ page={page}
+ perPage={PAGINATION.defaultPerPage}
+ onPageChange={setPage}
+ />
★★items.length を件数表示に使わないこと★★(落とし穴3)。
★CSVボタンの近く(toolbarRight の下、または CSV ボタンの直後)に小さく出すこと★:
<span className={styles.csvNote}>
{t("CSVは絞り込み条件に一致する全件を出力します(表示中のページだけではありません)")}
</span>
★★CSVリンクの href は queryString のまま変えないこと★★(落とし穴7)。
PAGINATION.maxPerPage の参照がこのファイルから消えている★npx tsc --noEmit = 0 / npm run lint = 0 / npm test 全パス★components/features/detail-log/ 配下で自分が触ったファイルだけを明示★)★★★対象★★: components/features/alerts/AlertsScreen.tsx / components/features/ds-logs/DsLogScreen.tsx / components/features/ds-alerts/DsAlertsScreen.tsx
★★この3画面は現在 body.summary しか読まず、body.meta を捨てています★★ → ★★total の state を新設すること★★。
const [total, setTotal] = useState(0); … setItems(body.items); setSummary(body.summary); setTotal(body.meta.total); // ★★summary.total ではない★★(禁止事項4)
★★setTotal(body.summary.total) と書いたら間違いです★★ — ★★summary は対応状況フィルタの前に 計算されているので(D-054)、status=open で絞ると summary.total > meta.total になり、 存在しないページのボタンが出ます★★(★★ユーザーが名指しで禁じた状態★★)。
★あとは段階3 と同じ★:
per_page を defaultPerPage に、page を送る(★useMemo の依存に page を入れる★)setPage(1)<Pagination …/> を置く★★D-Secログ(F-06)だけの注意★★: ★備考のインライン編集があり、commitNote() が setItems() でその行だけ差し替えています★ (★全件再取得を避けるため。既存コメント参照★)。 ★★備考の保存で page を触らないこと★★(★保存のたびに1ページ目に飛ばされたら使い物になりません★)。
★★また、DsLogScreen.tsx の docstring に 「一覧は最大 PAGINATION.maxPerPage 件並ぶため」という古くなる記述があります★★ → ★★PAGINATION.defaultPerPage 件に直すこと★★(★★grep で実測して直すこと。行番号は変わります★★)。
body.meta.total を読んでいる★grep -rn "summary.total" components/features/ にページ送り由来の参照が1件も無い★★
(★`StatCard` の表示に使っているものは残ってよい★)
npx tsc --noEmit = 0 / npm run lint = 0 / npm test 全パス★★★対象★★: components/features/facilities/FacilitiesScreen.tsx / components/features/schedules/SchedulesScreen.tsx
★total の state は既にあります★(setTotal(body.meta.total))。
- <div className={styles.footer}>
- {t("全{n}施設").replace("{n}", String(total))}
- </div>
+ <Pagination total={total} page={page} perPage={PAGINATION.defaultPerPage} onPageChange={setPage} />
★★全{n}施設 が使われなくなります★★ →
grep -rn '全{n}施設"' --include=*.tsx --include=*.ts . | grep -v node_modules | grep -v "\.claude/"
★★上が lib/i18n.ts の定義行だけになったら、キー "全{n}施設": "{n} facilities total" と その直上の解説コメント(t("全{n}施設").replace(...) の例が書かれた3行)を削除すること★★。 ★★"全{n}施設を表示 →" は別のキー(レポート画面が使用中)なので残すこと★★。
★★total の state がありません★★ → ★新設して body.meta.total を入れること★。 ★既存コメント「fixtures は13件だが、ページャの無い画面なので上限で一度に取り切る」を★★書き換えること★★ (★★もうページャがあります★★)。
全{n}施設 を「全{total}件中 …」に置き換え、未使用になったキーを削除した★"全{n}施設を表示 →" を消していない★★grep -rn "PAGINATION.maxPerPage" components/features/ が
`components/features/reports/ReportsScreen.tsx` の ★★2件(JSDoc の `:38` とコード `:42` の `SHOW_ALL_LIMIT`)だけ★★ になっている (★1件ではない。実測済み。★★コメント行を消しにいかないこと★★ — reports に差分を出すと禁止事項8 と完了条件16 に違反する)
npx tsc --noEmit = 0 / npm run lint = 0 / npm test 全パス★lib/i18n.ts を明示)★★★lib/fixtures/ の件数は数十件(監督の実測では清掃詳細ログで33件)しかなく、 defaultPerPage = 50 ではどの画面も1ページで収まります★★。
★★対処(★この順で試すこと★)★★:
| # | 方法 | 注意 |
|---|---|---|
| ★1★ | ★★lib/config/index.ts の defaultPerPage を一時的に 5 にして6画面を確認する★★ |
★★確認後に必ず 50 に戻し、git diff lib/config/index.ts が★maxVisiblePages の追加ぶんだけ★になることを確認すること★★(禁止事項12) |
| 2 | ★API を直接叩いて meta を確認する★ |
★curl "http://localhost:3000/api/clean-logs?date_from=…&date_to=…&result=all&per_page=5&page=2"★。★認証・IP制限の回避方法は docs/handoff.md / D-101 項2(ALLOW_DIRECT_ACCESS)を参照★ |
| 3 | ★大量ページの見た目は段階1 のテストで固定済み★ | ★★223ページの並びを画面で作る必要はありません★★ |
★★禁止★★: ★★lib/fixtures/ に行を足して件数を増やすこと★★(禁止事項6)。
★各画面について、以下を順に確認して結果を控えること★:
| # | 確認 |
|---|---|
| ★1★ | ★画面を開くと件数表示が出る(「全{total}件中 1〜N件」)★ |
| ★2★ | ★★total が 0-3 で控えた件数と一致する★★ |
| ★3★ | ★defaultPerPage を小さくするとページ送りのボタンが出る★ |
| ★4★ | ★★描画されているページ番号がすべて実在する(最大の番号を押しても空にならない)★★ |
| ★5★ | ★2ページ目を押すと内容が変わり、件数表示が「51〜100件」のように追随する★ |
| ★6★ | ★最終ページの to が total と一致する★ |
| ★7★ | ★★フィルタ/検索を変えると1ページ目に戻る★★(★2ページ目に居る状態で試すこと★) |
| ★8★ | ★★debounce のある検索欄でも戻る★★(★文字を打って300ms待つ★) |
| ★9★ | ★0件になる条件で絞ると、★★ページ送りも件数表示も消え、空表示だけが出る★★★ |
| ★10★ | ★1ページに収まるとき、★★件数表示は出るがボタンは出ない★★★ |
| ★11★ | ★ダーク/ライト両方で崩れない★ |
| ★12★ | ★EN に切り替えて「{from}–{to} of {total}」「Prev」「Next」が出る★ |
★★画面ごとの追加確認★★:
| 画面 | 追加で見ること |
|---|---|
| 清掃詳細ログ F-02 | ★CSVの注記が出ている★ / ★★CSVを実際に落として、表示中のページではなく全件入っていること★★ / ★行展開(アドバイス)がページ移動後も動く★ |
| アラート F-03 | ★★StatCard の3枚(未解消/解消済み/合計)がページ移動で変わらない★★(★summary はページに依存しない★) |
| D-Secログ F-06 | ★★備考を編集して保存してもページが飛ばない★★ / ★StatCard がページ移動で変わらない★ |
| D-Secアラート F-07 | ★StatCard がページ移動で変わらない★ |
| 施設 F-08 | ★★全{n}施設 が消え、新しい形になっている★★ |
| スケジュール F-09 | ★件数表示が新規に出ている★ |
| # | 確認 |
|---|---|
| 1 | ★★レポート画面(F-04)に差分が無い★★(git diff --stat components/features/reports/) |
| 2 | ★★ダッシュボード(F-01)に差分が無い★★ |
| 3 | ★git diff --stat に lib/fixtures/ / app/api/ / lib/db/ / d-watch-api.yaml / prisma/schema.prisma / lib/types/index.ts が出てこない★ |
| 4 | ★npm test のテスト数が着手前より増えている★ |
defaultPerPage が 50 に戻っている★★docs/decisions.md に追記(★★着手直前に実測した空き番号を使うこと★★)★★書くこと★★:
| # | 内容 |
|---|---|
| ★1★ | ★★モックに無いUIを新規に作った経緯と、それが CLAUDE.md §5 の例外であること★★。★★ユーザーの明示の承認(2026-09-11)を原文で引用すること★★ |
| ★2★ | ★「実在するページだけを描画する」を不変条件テストで固定したこと(段階1 の D-1〜D-10)★ |
| 3 | ★省略の規則(maxVisiblePages = 7、先頭寄り/中央/末尾寄りの3分岐)と、なぜ幅を固定したか★ |
| 4 | ★件数表示を「全{total}件中 {from}〜{to}件」に統一し、{total}件(表示 N件) と 全{n}施設 を廃止したこと★ |
| ★5★ | ★★meta.total を使い summary.total を使わない理由(D-054 の帰結)★★ |
| ★6★ | ★★ページ送りを付けても DB からは全件読む。性能は改善しないこと。別課題として切り離したこと★★ |
| ★7★ | ★却下した4案(maxPerPage を上げる / 無限スクロール / もっと見る / カーソルページング)と理由★ |
| 8 | ★対象外にしたもの(レポート / ダッシュボード / 点検記録)と理由★ |
docs/handoff.md に追記(★番号は実測すること★)★★grep -n "^## 20" docs/handoff.md | tail -3 で最終の追記番号を実測してから足すこと★★。
★冒頭表の「実行状態」と文末の「実行状態」表を★★完了★★に更新し、 ★実行日 / 決定番号 / コミットハッシュ / 実測値(tsc / lint / テスト数)★を書くこと。
docs/decisions.md に実測した番号で追記した(★7-1 の8点すべて★)★docs/handoff.md に追記した★git add docs/decisions.md docs/handoff.md docs/handoff/指示書AA_一覧にページ送りを付ける.md★★★以下は本指示書では直しません。★★ ★決定事項に列挙して残すこと★。
| # | 論点 | 状態 |
|---|---|---|
| ★★①★★ | ★★DB から全件読む構造は変わらない★★ — ★DataSource が引数を取らない契約で、prismaSource.cleanLogs() は全期間・全件 findMany()★。★コード自身が「年間13万行でボトルネックになりうる」と警告している(lib/db/index.ts / lib/db/clean-log-mapping.ts)★。★★ページを繰るたびに全件取得が走るので、体感はむしろ悪化する方向★★ |
★★別課題(SQL に絞り込みを降ろす改修)として切り出すこと★★。★★ページ送りと同時にやらないこと★★ — ★★連続日数の計算が全期間を走査しているため、同時に壊れます★★。★★合わせて「ページ番号を URL に持たせるか(useSearchParams + Suspense 境界)」も未決★★ |
| ★★②★★ | ★★レポート画面の「全{n}施設を表示 →」が200件で切れる潜在バグ★★ — ★components/features/reports/ReportsScreen.tsx の SHOW_ALL_LIMIT = PAGINATION.maxPerPage(=200)★。★★施設が350ある顧客では「全350施設を表示 →」を押しても200件しか出ません★★。★件数表示は totalCount(=350)を出すので、★★数字と中身が食い違います★★★ |
★★本書では直さない★★(禁止事項8)。★★別指示書で「レポート画面もページ送りに寄せる」か「SHOW_ALL_LIMIT を撤廃してサーバ側で全件返す」かを決めること★★。★★P1のドン・キホーテ1社で施設が何件になるかを先に確認すること★★(★200を超えないなら急がない★) |
| ★★③★★ | ★★CSV と画面の件数が食い違う★★ — ★CSV は絞り込み条件に一致する全件、画面は1ページぶん★。★本書では画面に注記を出すだけ(段階3-6)★ | ★★export に件数上限が無いことも未確認★★ — ★★年間13万行を1本のCSVに吐く挙動を、実データが入る前に一度確かめること★★ |
| ★★④★★ | ★★fixtures は数十件しかなく、ローカルでは2ページ目が出ない★★(★清掃詳細ログで33件★) | ★★検証は defaultPerPage を一時的に 5 にして行い、必ず 50 に戻すこと★★(段階6-1)。★★lib/fixtures/ に行を足さないこと★★(禁止事項6)。★★大量ページの見た目は段階1 の期待値表(223ページ)で固定済み★★ |
| ⑤ | ★1ページあたりの件数を切り替えるUI(20/50/100)は作っていない★ | ★要望が出たら perPage を state 化して Pagination に選択UIを足す★。★★そのとき PAGINATION.maxPerPage(200)を超えないこと(サーバがクランプする)★★ |
| ⑥ | ★ソートUIが付いたときの setPage(1)★ — ★現状どの画面にもソートUIが無い(並べ替えはサーバ固定)★ |
★★将来ソートUIを足す人は、そこでも setPage(1) すること★★ |
| ⑦ | ★点検記録(F-05)は画面もAPIも未実装★ | ★実装するときは最初から Pagination を入れること★ |
| # | 条件 |
|---|---|
| ★1★ | ★lib/pagination/page-items.ts に totalPageCount() / pageRange() / buildPageItems() があり、PageItem の ellipsis が page を持っていない★ |
| ★2★ | ★★段階1 の期待値表(A 13 + B 9 + C 22 + D 10)がすべてテストになっている★★ |
| ★3★ | ★★不変条件テストが totalPages = 1..300 × 全ページを総当たりし、★存在しないページ番号が1つも描画されない★ことを固定している★★ |
| ★4★ | ★★「…」が飛ばすページ数が必ず2以上であることがテストで固定されている★★ |
| ★5★ | ★PAGINATION.maxVisiblePages が追加され、奇数かつ7以上であることがテストで固定されている★ |
| ★6★ | ★components/ui/Pagination.tsx があり、total === 0 で null、総ページ数1でボタンを出さない★ |
| ★7★ | ★★「…」が <button> になっていない★★ |
| ★8★ | ★★6画面すべてに件数表示(「全{total}件中 {from}〜{to}件」)とページ送りが入っている★★ |
| ★9★ | ★★grep -rn "PAGINATION.maxPerPage" components/features/ が reports/ReportsScreen.tsx の★2件(JSDoc の :38 とコード :42)だけ★(★1件ではない。実測済み。コメント行を消しにいかないこと★)★★ |
| ★10★ | ★★6画面すべてが body.meta.total を使っており、summary.total を総ページ数に使っていない★★ |
| ★11★ | ★★フィルタ・検索(debounce 経由を含む)を変えると6画面すべてが1ページ目に戻る★★ |
| ★12★ | ★清掃詳細ログにCSVの注記が出ており、CSVの中身は全件のまま★ |
| ★13★ | ★lib/i18n.ts に4キーを足し、未使用になった "全{n}施設" を削除した(★"全{n}施設を表示 →" は残っている★)★ |
| ★14★ | ★★50 / 200 / 7 がコンポーネントにも純粋関数にも直書きされていない★★ |
| ★15★ | ★★lib/fixtures/ / app/api/** / lib/db/** / lib/*/query.ts / d-watch-api.yaml / lib/types/index.ts / prisma/schema.prisma に差分が無い★★ |
| ★16★ | ★★components/features/reports/** / components/features/dashboard/** に差分が無い★★ |
| ★17★ | ★★package.json / package-lock.json に差分が無い★★ |
| ★18★ | ★★lib/config/index.ts の差分が maxVisiblePages の追加だけ(defaultPerPage が 50 に戻っている)★★ |
| ★19★ | ★npx tsc --noEmit = 0 / npm run lint = 0 / npm test 全パス(★テスト数は着手前より増えている★)★ |
| ★20★ | ★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / 他の指示書 / .claude/worktrees/** に触っていない★ |
| ★21★ | ★★docs/decisions.md(★実測した空き番号★)と docs/handoff.md に記録した。★モック例外の承認経緯と、性能が改善しないことを明記した★★★ |
| ★22★ | ★6コミット(段階1 / 2 / 3 / 4 / 5 / 7)で push した★ |
| 項目 | 内容 |
|---|---|
| 状態 | ★★完了★★(★段階0〜7 をすべて実行。★段階0〜3 と段階4〜7 は別のエージェントが分担★★) |
| 実行日 | 2026-09-11 |
| 決定番号 | ★★D-120★★(docs/decisions.md:7762)/ docs/handoff.md ★追記40★(:3363) |
| コミット | 段階1 fd4ca5b / 段階2 fcdcb07 / 段階3 9e9704c / ★段階4 78fbfac★ / ★段階5 77693c4★ / 段階7(記録)。★★push は未実施(監督確認待ち)★★ |
| 実測値 | ★テスト 48ファイル / 1137件 全パス★(★段階1 で +48件。段階4・5 は画面コードのみでテスト追加なし★)/ npx tsc --noEmit = ★0★ / npx eslint . = ★0★ / grep -rn "PAGINATION.maxPerPage" components/features/ = ★reports/ReportsScreen.tsx の2件(:38 JSDoc / :42 コード)だけ★ |
| ★段階6 について★ | ★★ブラウザでの目視は行っていない★★(★dev server がアクセストークンのログインを要求し、認証情報の入力は行わない方針のため★)。★代わりに APIルートの GET を直接呼び、6画面ぶんの meta と「描画される最大のページ番号を押しても空にならないこと」を実測した★。★★目視が要る項目は docs/handoff.md 追記40 項6 に列挙してある★★ |
★defaultPerPage について★ |
★★lib/config/index.ts は本書の対象外ファイルなので、段階6-1 の一時変更は行っていない★★(defaultPerPage は 50 のまま)。★代わりにクエリの per_page=5 で同等の確認を行った★ |