紡Book の勉強ノートに戻る
開発記録2026.06.22

管は通ったのに、web はモックのデータを見たままだった — 表示側のデータ源を差し替える

設計アーキテクチャ紡Book

起きたこと

スマホで撮った写真に言葉を綴って同期した。アプリは「アップロード成功」と返している。それを web の /lab/shelf(振り返りの本棚)で開いた。テキストは出るのに、写真が割れアイコンで出る。

最初は「画像配信のバグだ」と思った。前日まで「画像経路 完全開通」と進捗メモに書いたばかりだったので、なおさら回帰を疑った。

1層ずつ剥がす

ここから AI に手を貸してもらいながら、症状から原因の層を1つずつ剥がしていった。

(以下、docker ps=サーバのプロセスが動いているか確認する、curl=ブラウザを介さずサーバに直接問い合わせる、といった道具を使う。)

① backend は生きているか

docker ps でコンテナは Up。/api/sync/memories/ を curl で叩くと HTTP 401(認証要求)が返る。コンテナ内のサーバプロセスは生きている=層①は正常。

② DB に届いているか

Django shell から DB をダンプ。

=== PHOTOS 25 ===
{'id': 34, 'image': 'photos/c7844987-....jpg', 'sync_state': 'synced', ...}
{'id': 35, 'image': 'photos/6f1d3357-....jpg', 'sync_state': 'synced', ...}
...
=== MEANINGS 27 ===
{'id': 25, 'photo_id': 34, 'text': 'テスト\n(本文は省略)', ...}
...

今日アップした9枚、全部画像つきで届いている。sync_state='synced'。さらに media/photos/<uuid>.jpg を直接 HTTP で取ったら 410KB の image/jpeg が 200 で返る。

→ 「届いてない」ではない。同期も画像アップロードも成功している。

③ じゃあ web は何を見ているのか

/api/favorites/ を叩く。返ってきたのは 3件だけ。前日までに☆を付けた古いデータで、今日の9枚は1件も入っていない。

favorites という名前の通り、これは「お気に入りに登録した写真」だけを返すAPI。web の create プールも本棚も、このAPIだけを見ていた。

④ じゃあ「☆お気に入りに登録する」のはどこでやる

backend の同期処理を読んだ。

python
is_favorite = item.get("is_favorite", False)
if is_favorite:
    FavoriteMemory.objects.get_or_create(...)

backend には「同期時に is_favorite: true を受けたら☆登録する」橋がある。ある。じゃあ mobile はどう送っているか。

dart
// `is_favorite` は常に false(モバイルにお気に入り概念なし)
'is_favorite': false,

ここで全部つながった。

正体は、モックからの移し替えが途中だったこと

web の /lab は、もともとクリッカブルモックとして作ったものだ。全画面の導線を先に組んで、中身は sampleData という固定のサンプルで動かしていた。同期の管がまだ通っていなかったので、それでよかった。

そのあと管が通り、web を実データに繋ぎ直した。ただしその移し替えが途中で止まっていた。

今日の9枚が居る場所と、web が見ている場所は別である。サーバに届いている(web は見ていない)Photo 表サーバ DB25件・synced今日の9枚はここMeaning 表サーバ DB27件の言葉今日の綴りもここmedia/photos画像ファイルHTTP 200 で取れるDB の表ではないweb 側が掴んでいるFavoriteMemory 表サーバ DBGET /api/favorites/昨日までの☆ 3件。今日の9枚は無いsampleData固定サンプル本棚の並び軸に残留DB ではない同期の本文はメモリにしか無く、保存しない。is_favorite は常に false。true のときだけ下段の表に行が増える。web に☆追加 UI も無い。

☆が付く経路はどこにも無い。だから同期した9枚は、web のどの画面にも出てこない。バグを踏んだのではなく、モックから実データへ移すときに、繋ぎ先が「全部」ではなく「選ばれたものだけ」を返すAPIのままだった、という話だった。

進捗メモには前から「器の粒度ズレ(web curated ↔ mobile raw)の橋渡し=未対応」と書いてあった。文字としては知っていたが、それがどの画面のどのAPIに穴として出るかまでは繋がっていなかった。

☆をやめた

繋ぎ直すには2つ道があります。☆を付ける経路を作るか、☆そのものをやめるか。やめる方を選んだ。

理由は、☆を残すと選ぶ場所が2つになるからだ。当初は web を「作る/選ぶ/見せる」の3つで考えていて、「選ぶ」が本棚、「作る」が CreateWizard だった。でも実際に作品を作ろうとすると作るときにも結局選ぶ。それなら本棚側の☆は要らない。

書き直したモデルはこう。

同期済みはサーバ DB に居る。選ぶのは画面のメモリである。Photo 表 / Meaning 表サーバ DB・同期済み。プールの母集団はここ。閉じても残る。読む読むプール全同期写真の入口☆という別動作は廃止本棚振り返り専用選ぶ/☆動作を持たない作る瞬間の選択画面のメモリ☆表には書かないFavoriteMemory 表は使わない。サーバ DB に表は残る。

backend は「もう書いてあった」

設計を spec に落として実装プランを書き始めた時、もう1つ発見があった。

「全同期写真を返す API」を新規に作るつもりで urls.py を確認したら、既に GET /api/memories/ が登録されている。エンドポイントとは「URLを叩くと決まった形のデータが返ってくる窓口」のこと。MeaningViewSet が Meaning.objects.filter(owner=...).select_related("photo").order_by("-created_at") を返し、serializer は { id, photo, photo_image(null許容・絶対URL), text, mood, photo_taken_at, created_at } を出します。新しく要るものが全部既にあった。

$ curl -H "Authorization: Bearer ..." http://localhost:8000/api/memories/
count= 27
33 photo= 42 img= OK mood= 'happy' text= 'テスト'
32 photo= 41 img= OK mood= 'like'  text= 'テスト'
...

ライブで叩いて27件返ることまで確認した。新規エンドポイントを作らず、契約をテストでピン留めするだけで済みます。実装プランから backend 1タスクが丸ごと「テスト追加だけ」に縮んだ。

実装の進め方

ここからは AI(Sonnet サブエージェント)に実装してもらい、Opus にレビューしてもらう運用で進めた(Sonnet は速度重視で実装タスクをこなす実行役、Opus は判断の重い局面を担うレビュー役)。8タスクに分けて、1タスクずつ(useRemoteMemories・shelfAxes.ts 等は綴Photo 内部の画面・部品名。1つずつ追わなくてよい付録として置く):

  1. backend /api/memories/ の契約テストをピン留め
  2. frontend useRemoteMemories 新設(画面に渡す構造は変えず、読みに行く元だけ差し替え=画面側は無改修)
  3. CreateWizard のプール源を memories へ+画像 null プレースホルダ
  4. shelfAxes.ts を全同期メモリの月/気分グルーピングへ書き換え(残っていた sampleData をここで撤去)
  5. 本棚を振り返り専用化(☆系コピー撤去・Axis 縮小)
  6. /lab/favorites ページと FavoritesDock を撤去(970行削除)
  7. CreateWizard に作業トレイ SelectionTray を新設(撤去したドック概念の転用)
  8. 全体ゲート+進捗メモ更新

タスクごとに「失敗するテスト → 実装 → 緑 → コミット → レビュー → 次へ」を回した。frontend 247テスト・backend 60テスト・tsc 0・eslint 0 で全緑。

判断はいくつか AI と対話しながら自分で切った: ☆面を今いっしょに整理するか後回しか/本棚を畳むか作り直すか(→作り直す)/ドックを捨てるか(→「作業トレイ」概念に転用する)/FavoriteMemory テーブルを drop するか(→休眠で残す)。

撤去したドックを「概念だけ残して別の仕事に転用する」のは、当初思いつかなかった。「ドックが見づらかった」と話しているうちに、ドックという形は『今選んでいるものを見せる』に向いていると気づいた。☆という意味から外して、選択中の作業トレイにする。同じ "下部常駐ストリップ" でも、目的を1つにすれば意味が立ちます。

学んだこと

  • 「アップロード成功」と「web に出る」は別の層。症状が出た層から1つずつ剥がしていくと、原因の階層が見えます。今回は backend→DB→media→favorites契約→mobile送信→web UI と、6回くらい剥がした。
  • モックを実データに繋ぎ直すときは、繋ぎ先が本当に全件を返すAPIかを見ます。 今回は「選ばれたものだけ」を返すAPIに繋いでいて、しかも選ぶ経路が存在しなかった。モック側の残骸(sampleData)も同時に消しきる必要があります。
  • 設計を spec に書き出す前に、実装の現状を1回見ます。新規に作ろうとしているものが、もう存在することがあります。
  • 「捨てる」と「概念だけ転用する」は別の選択肢。撤去しながら、その形が次の仕事に効く面を残せることがあります。

この記事は2026年6月時点の自分の理解で書いています。仕様や認識が変わっている可能性があります。