集酒(shushu)— テスト環境を用意し、図鑑の「収集済み / 未収集」の不具合を直した

作成: 2026-09-13 JST。対象: business/shushu/shushu-app(図鑑の取得)と business/shushu/shushu-supabase(テストデータ投入・マイグレーション 1 本)。作業はすべて ローカルの Supabase(http://127.0.0.1:54321)に対して行い、クラウド(ref kngwgetrmddpyxrfiami)には一切触れていない。UI の配色を白基調に変えた作業は同日の別作業。

1. 結論(先に)

2. テスト環境の使い方

2.1 起こしかた

cd business/shushu/shushu-supabase
npx supabase start                     # 落ちているとき
python3 scripts/seed-test-data.py      # テストデータを作り直す

cd ../shushu-app
npx expo start                         # Metro。シミュレータのアプリは入っている

2.2 アカウントと、それぞれで何が見えるか

メールアドレス表示名投稿フォローこのアカウントで確かめられること
tester@shushu.testテスター太郎8(+下書き 1)フォロー 3・フォロワー 3主役。図鑑 6 / 500 銘柄、同じ銘柄を 2 回飲んだ記録(×2 バッジ)、非公開投稿、親しい友達限定の投稿、図鑑未登録の自由記述、マイランキング Top3、そらへのフォロー申請(承認待ち)
hanako@shushu.test花子4相互フォロー親しい友達限定の投稿を書く側。マイランキングあり
kenji@shushu.test健二3相互フォロー相互フォローの相手をもう 1 人
yuki@shushu.testゆき2テスターからの片方向フォローされていない側の見え方
sora@shushu.testそら1申請 3 件が承認待ち非公開アカウント。フォロー一覧の「リクエスト」タブ・承認・拒否
tsumugi@shushu.testつむぎ2テスターへの片方向フォロー返ししていないフォロワーの扱い(フォロワー削除・ブロック)
newbie@shushu.testはじめ0なし登録した直後の状態。図鑑 0 / 500、フォロー中タイムラインが空、マイページが空のときの見え方

投稿写真はスクリプトが生成している(徳利とお猪口の図を PNG で書き出す)。実写ではないので、レイアウトの確認には使えるが「写真の写り」の判断材料にはならない。

2.3 ひと通り触るときの順路

テスター太郎で

そらで(非公開アカウント)

はじめで(何も無いアカウント)

3. 図鑑の「収集済み / 未収集」が壊れていた理由

3.1 症状と原因

図鑑で「収集済み」または「未収集」を押すと、一覧が更新されず「図鑑を読み込めませんでした」の状態になる(直前に表示していた「すべて」の中身が残るので、押しても何も変わらないように見える)。

原因: src/services/collection.ts の getCollectionItems が、収集状態で絞り込むときに次の順で動いていた。

  1. ページングをやめて全銘柄(500 件)を取る
  2. 取れた 500 件の ID を全部 URL に並べて、自分の投稿を引く(.in('sake_product_id', productIds))
  3. 手元で収集済み / 未収集に分けてから、必要な 30 件を切り出す

2 で作られるリクエスト URL が 約 18KB。Supabase の前段(Kong / nginx)はリクエスト行の上限が 8KB なので、必ず HTTP 414 URI too long で弾かれる。

$ curl -G .../rest/v1/sake_products --data-urlencode "id=in.(<500件のUUID>)"
ids length: 18499
URL length: 18562
HTTP 414
URI too long

2026-09-13 にローカルで実測。UUID 1 個で 37 文字なので、銘柄が約 215 件を超えると必ず起きる。「すべて」タブはページング(30 件)が効いているので ID が 30 個で済み、壊れていなかった。ファーストリリースの 151 銘柄なら偶然セーフだが、少し増やした瞬間に壊れる作りだった。

3.2 直しかた

銘柄の件数に比例して伸びるものを URL に載せないのが要点。収集状態の絞り込みを DB 側に移し、自分の投稿は銘柄に埋め込んで一緒に取ってくる(1 往復で済むようになった)。

タブ直す前直した後
すべて銘柄 30 件 + 別クエリで自分の投稿(ID 30 個を URL に並べる)posts(...) を左結合で埋め込み、1 回で取る
収集済み全 500 件を取る → 414posts!inner(...) で「自分の投稿がある銘柄」だけを DB が返す
未収集全 500 件を取る → 414posts=is.null で「自分の投稿が無い銘柄」だけを DB が返す

埋め込む投稿は posts.user_id = 自分 かつ is_draft = false に絞り、新しい順に並べる。先頭がサムネイルと最新評価、配列の長さが「×N」になる。これでどのタブでもページングと総件数がサーバー側で正しく決まる(以前は収集済み / 未収集だけ手元で切り出していた)。

3.3 直った確認

アプリと同じ @supabase/supabase-js で、同じ組み立てを叩いた結果(2026-09-13 実測)。

アカウントタブ / 絞り込み返った件数総件数URL 長
テスター太郎
(6 銘柄を記録済み)
すべて30500267
収集済み66275
未収集30494281
未収集・2 ページ目30494282
収集済み + 秋田県22323
未収集 + 秋田県1111329
すべて + 純米大吟醸3046327
はじめ
(1 件も記録していない=報告のあった状態)
すべて30500267
収集済み0(エラーではなく空)0275
未収集30500281

6 + 494 = 500、秋田県は 2 + 11 = 13(マスタの秋田県の総数と一致)。URL は 18,562 文字から 267〜329 文字になった。

あわせて、0 件のときの文言を状況で変えた。以前はどの理由でも「該当する銘柄が見つかりません」だったので、「まだ 1 本も記録していない」のか「絞り込みの結果 0 件」なのかが区別できなかった。

状況出す文言
収集済み・絞り込みなし・0 件🍶 まだ記録した銘柄がありません/右下の+から一杯を記録すると、ここに集まります。
収集済み・絞り込みあり・0 件🍶 この絞り込みでは、まだ記録がありません/産地や種類の絞り込みを外すと、記録した銘柄が出ます。
未収集・絞り込みなし・0 件🎉 全部そろいました/図鑑の銘柄はすべて記録済みです。
それ以外🔍 該当する銘柄が見つかりません

4. ついでに見つかった 2 件(直した)

テストデータを作る途中で、テストアカウントを消そうとして失敗したのが入口。原因はどちらも「トリガー関数が、呼んだ人の権限と探索経路のまま走っていた」ことで、supabase/migrations/20260913000001_fix_trigger_functions.sql にまとめた。

4.1 乾杯の件数が、他人の投稿では絶対に増えない

kanpais に行が入ると update_kanpai_count() が UPDATE posts SET kanpai_count = kanpai_count + 1 を走らせる。この UPDATE は乾杯した本人の権限で実行されるが、posts の RLS は posts_update USING (auth.uid() = user_id)。つまり他人の投稿は 1 行も更新されず、件数は 0 のまま黙って据え置かれる。自分の投稿に乾杯したときだけ増える、という一貫性のない状態だった。

【直す前】他人の公開投稿に authenticated で乾杯
  乾杯する前の kanpai_count: 0
  挿入 HTTP 201
  乾杯した後の kanpai_count: 0      ← 増えない
  実際の乾杯行数: 1

【直した後】
  乾杯する前: 0 → 挿入 HTTP 201 → 乾杯した後: 1
  取り消し HTTP 204 → 取り消した後: 0

いま表には出ていない(アプリに乾杯ボタンがまだ無く、数字を表示しているだけ)。ボタンを付けた瞬間に「全員 0 のまま」になるので、先に塞いだ。

4.2 管理 API / ダッシュボードからユーザーを削除できない

乾杯を 1 件でも持つユーザーを消すと 500: Database error deleting user。DB 側の原因は ERROR: relation "posts" does not exist。

auth.users の削除は supabase_auth_admin ロールで走り、このロールの探索経路に public が入っていない。関数側に search_path が設定されていないと実行時のセッションのものが使われるため、CASCADE で kanpais が消えるときに発火する update_kanpai_count() の中の posts が解決できずに落ちていた。

運営がサポート対応や退会代行でユーザーを消せないという形で効く。なおアプリ内の退会(delete_my_account)は影響を受けない — こちらは SECURITY DEFINER 側で search_path = public, auth を設定しているため。乾杯を持つ捨てアカウントで実測して確認した(HTTP 204・ユーザーは消える)。Google Play のアカウント削除要件は満たせている。

4.3 直した内容

normalize_sake_name() にも search_path が無いが、これは生成列(posts.sake_name_normalized など)で使われている。SET を足すと生成列の作り直しが要るうえ、中身は組み込み関数だけなので実害が無い。触っていない。

5. 直していないが、決めてほしいこと

非公開アカウントの公開投稿が、フォローしていない人の「新着」に出る。

いまの is_private はフォローに承認が要るかどうかだけを決めていて、投稿の見え方は変えていない。そら(非公開)の visibility = public の投稿は、フォローしていない「はじめ」の新着にも出る(実測)。

Instagram 型の非公開アカウントは「フォロワー以外には投稿を一切見せない」なので、ユーザーの期待とずれる。直すなら posts_select の RLS に「投稿者が非公開なら、フォロワーか本人だけ」を足すことになる(クライアント側では直せない)。

ただし仕様の決めの問題で、「非公開=勝手にフォローされないだけ。投稿の公開範囲は投稿ごとに選ぶ」という設計も成り立つ。その場合はラベルを変えるのが筋(「非公開アカウント」→「フォローを承認制にする」)。どちらにするか決めてください。

6. 確認したこと / していないこと

やった結果
tsc --noEmit / eslintどちらも終了コード 0
マイグレーションを 0 から適用(supabase db reset)16 本すべて適用・seed 投入まで成功
図鑑の 3 タブ × 絞り込み × 2 ページ目を、アプリと同じ @supabase/supabase-js で実行件数・総数・URL 長すべて期待どおり(3.3)
ホーム 2 種・マイページ・フォロー・申請・マイランキング・図鑑統計・ユーザー検索を 3 アカウントで実行すべて期待どおり
乾杯の増減・管理 API のユーザー削除・アプリ内の退会実測で確認(4 章)
投稿写真が実際に取れるか(シミュレータは NSAllowsLocalNetworking で HTTP 可)HTTP 200 / 約 8KB
アプリの起動とログイン画面白基調の新配色で表示
やっていない理由
画面をタップしての通しテストシミュレータを操作する権限(アクセシビリティ)が無く、ログイン以降の画面に入れない。2.3 の順路は shika さんの手で
Android での確認この Mac に Android SDK が無い(前回のレビューと同じ)
クラウド(ref kngwgetrmddpyxrfiami)への反映今回の変更はローカルのみ。マイグレーション 1 本が未反映なので、次にクラウドを触るときは supabase db push が要る
iPhone 17(iOS 26.5)のシミュレータの作り直しJS は最新だがネイティブは旧ビルド。使うなら APP_VARIANT=development npx expo run:ios

7. 触ったファイル

ファイル変更
shushu-app/src/services/collection.tsgetCollectionItems を作り直し(埋め込み + DB 側の絞り込み + サーバー側ページング)
shushu-app/src/screens/collection/CollectionScreen.tsx0 件のときの文言を状況で出し分け
shushu-app/src/components/collection/CollectionGrid.tsx0 件のときの絵柄・見出し・説明を受け取れるように
shushu-supabase/supabase/migrations/20260913000001_fix_trigger_functions.sql新規。トリガー関数 3 本の権限と探索経路 + 乾杯件数の数え直し
shushu-supabase/scripts/seed-test-data.py新規。テストデータ投入(写真の生成も含む)
shushu-supabase/README.mdテストデータの節を追加