is_pinned=falseだけでは特別回を除外できず、日程追加の雛形に選ばれた
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
「ピン留めされていない直近1件」という条件は、特別回がピン留めされていなければ素通りする。特別さと表示上の固定(ピン留め)は別の軸で、片方で後者を代用すると本来除外したいものを取り込む。
結論
「ピン留めされていない直近1件」を自動コピーする作りは、特別回がピン留めされていない限り、その特別回を直近1件として拾ってしまう。 特別さと表示上の固定(ピン留め)は別の軸であり、片方の有無で他方を判定することはできない。直し方は条件を増やすことではなく、自動選択そのものをやめてコピー元を選択式にすることだった。
症状
ワークショップの管理画面には、あるカテゴリに新しい日程を追加するとき、そのカテゴリの既存イベントの内容(本文・価格・定員・場所・画像など)を自動でコピーしてくる機能がある。コピー元は次のクエリで選ばれていた。
const { data: latestWs } = await supabase
.from('workshops')
.select('*')
.eq('category_id', fromCategory)
.eq('is_private', false)
.eq('is_pinned', false) // ピン留めの特別回(例: 夏休み親子特別回)は雛形にしない
.eq('is_service', false) // サービスも雛形にしない
.order('event_date', { ascending: false })
.limit(1)
.maybeSingle()
コメントの意図どおり「ピン留めされた特別回は雛形にしない」つもりだったが、実際にはピン留めされていない特別回(夏休み親子特別回のような一回限りの回)が存在し、それが直近のイベント日を持っていたため、通常回の日程を追加したときにこの特別回の内容がそのままコピーされてしまった。
原因
除外条件が is_pinned=false の1本だけだったことが原因になる。この条件が実際に除外できるのは「ピン留めされている行」だけで、「特別回である」こととは独立している。特別回を作る側は、画面上の表示を固定したい時だけピン留めを使う運用だったため、特別回がピン留めされていないケースが普通にあり得た。
// 8/3 に追加された特別回。is_pinned の指定は無い=既定で false
// chore: 夏休み親子特別回 8/11(火)14:00 の日程追加を本番反映(再ビルド)
その状態で「event_date の降順で先頭1件」を取ると、直近に追加されたこの特別回が条件を満たす最新の1件になり、次に別カテゴリの通常回を追加した管理者の意図とは無関係にコピー元として選ばれた。コードは「特別回を除外する」という意図でコメントまで書かれていたが、実装はその意図を満たす条件を1つしか持っておらず、特別さそのものを直接表す値が無かった。
直す
除外条件を増やす(たとえば「特別回フラグ」を新設してそれも eq(false) に加える)方向ではなく、自動選択自体をやめてコピー元を選択式に変えた。
<select
value={sourceId}
onChange={(e) => applySource(e.target.value)}
>
<option value="">コピーしない(空から作成)</option>
{categories.map((cat) => {
const items = sourceWorkshops.filter((ws) => ws.category_id === cat.id)
return (
<optgroup key={cat.id} label={cat.name}>
{items.map((ws) => (
<option key={ws.id} value={ws.id}>{formatSourceLabel(ws)}</option>
))}
</optgroup>
)
})}
</select>
コピー元の一覧はカテゴリ別にグルーピングし、イベント日時とタイトルを添えて降順に並べる。is_service=false でサービスだけは一覧から外すが、「特別回かどうか」を推測する条件はもう無い。管理者がどのイベントがコピー元として候補に挙がっているかを実際に見て選ぶため、意図しない回が黙って選ばれることがなくなる。カテゴリに応じた既定値(従来の自動選択に近い挙動)は残しつつ、applySource で選び直せるようにしたことで、既定が外れていた場合の訂正コストも「選び直すだけ」に下がっている。
再発しない形にする
「直近の1件」や「最新の有効な行」を自動でコピー元・既定値・テンプレートに使う作りは、一覧の中に一時的な特別枠や例外的な行が1つ混ざった時点で、意図しないものが選ばれる余地を持つ。この種の事故を防ぐ条件は、気づいた例外の種類ごとに eq() を増やしていく対症療法になりやすく、次の種類の例外が来るたびに同じ事故が再発する。
今回取った手は条件を増やすことではなく、「何が選ばれそうか」を一覧として人の目に見せ、選び直せるようにすることだった。自動選択の便利さを完全に捨てたわけではなく、カテゴリに応じた既定値は残している。違いは、既定が外れていた場合に黙って進むのではなく、選び直す経路が常に見えている点にある。同種の「直近1件を自動で使う」設計を見直すときは、除外フラグを足す前に、そもそも選択を人に見せられないかを先に検討するとよい。
よくある質問
Q1もともとの「直近1件をコピー」はなぜ特別回を除外できなかったのですか?
除外条件が is_pinned=false(ピン留めされていない)だけだったためです。特別回自体はピン留めされていなかったので、この条件では素通りしてしまい、イベント日の降順で単純に直近1件を取ると特別回がその1件に当たりました。「特別かどうか」と「ピン留めされているかどうか」は別の軸で、片方の有無で他方を判定できる保証はありません。
Q2直し方はコピー元の除外条件を増やすことですか?
いいえ。is_service=falseでサービスは除外したままですが、「特別回かどうか」を自動で見分ける条件は増やしていません。代わりに自動選択そのものをやめ、カテゴリ別にグルーピングした全イベントの一覧から管理者が選ぶ方式に変えました。条件を増やすたびに次の例外が出る構造だったため、推測をやめて選択式にしています。
Q3この話はこのワークショップ管理画面以外にも当てはまりますか?
「直近1件」や「最新の有効な行」を自動でコピー元・既定値・テンプレートに使う作りは、一覧のどこかに一時的な特別枠や例外行が混ざった時点で同じ事故が起きます。除外フラグを都度増やすのではなく、何が選ばれているかを人の目に見せて選び直せるようにするのも有効な手です。
確認した環境
- Next.js App Router(管理画面)/ Supabase
- 2026-08-15 に修正
この記事の根拠
- TypeScriptファイル 57〜69行目コミット e7c0894
- TypeScriptファイル 66〜135行目コミット d7c7cae
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。