書棚ページの見通しが悪くなっていた。Kindleからインポートした蔵書がどれか画面から判別できず、漫画のシリーズものが1巻ずつカードを占有して★4.5以上の棚を埋めていた。1日かけて4つの改善を順に積んだ。
Kindle蔵書識別バッジを足す
kindle_library テーブルに521件入っているうち、amazon_metadata 側ともASINで突き合うのは467件あった。APIをASIN JOINに直して is_kindle と is_kindle_unlimited の2フラグを返すようにし、shelf.vue のカード左下に黄色い「K」バッジを描画させた。
最初はWHERE句のカラム名を am. プレフィックス付きにregexで一括書換えしようとして、置換が脆かったのでやめた。SQL本体を手で書き直したほうが速い、と気づくのに数分かかった。
ブラウザで開くと、Kindle蔵書249件に黄色の「K」が乗っていて、フィルタで絞り込みも効いた。
シリーズもの集約でカードを1つにまとめる
カイジ、インベスターZ、BLOODY MONDAYのように1巻〜N巻が独立カードで並ぶと、★4.5以上の棚が同じシリーズで埋まる。タイトルから巻数を除去して正規化キーを作り、N巻あるシリーズは「📚 N巻」の紫バッジ付きカード1枚に集約。クリックでモーダルを開いて全巻を順序通り並べる方針にした。
最初の実装ではカードを物理的にスタック表示する案を出してきたが、デザインがうるさくなるので止めて、バッジ+モーダルの形に落ち着いた。
開いたページではカイジ13巻、インベスターZ 20巻が正しくまとまっている。「これでよさそう」と思った直後、ユーザーから「BLOODY MONDAYとかもそうじゃないですか。なんでカイジとインベスターZだけなんですか」と差し戻された。
BLOODY MONDAYのregexバグ
原因を追わせると、巻数除去regexの先頭が \s+ になっていた。
// 修正前: スペース必須でマッチしない
const stripVolume = (t: string) => t.replace(/\s+\d+巻$/, '')
// 修正後: スペース無しでもマッチ
const stripVolume = (t: string) => t.replace(/\s*\d+巻$/, '')
【極!単行本シリーズ】 を 【】 ごと先に削った後、タイトルが「BLOODY MONDAY9巻」とスペース無しで巻数が直結する。\s+ だと0文字スペースに一致しないので、巻数が残ったまま別カードとして数えられていた。\s* に1文字直したらBLOODY MONDAYが「📚 11巻」として集約され、★4.5以上の件数も 63 → 55 に減った。
regex は「最低1文字必要」と「0文字でもいい」で挙動が真逆になる典型例だった。前段で文字を削る処理を入れたら、後段のregexがその削り方に依存する。
漫画フィルタを「初期非表示」で入れる
ユーザーから「漫画をタグで分けられるようにしてほしい」と言われ、kindle_library.tags の comic を真実として is_comic をAPIで返し、フロントで初期非表示にした。除外94件、表示500冊 → 406冊。★5.0が8→3件、★4.5以上が165→117件に絞られて、棚が一気に実用書中心になった。
紙本のチ。が漫画扱いされない
絞り込んだ画面を見ると、チ。が★4.5以上の棚に残っていた。Pythonの kindle_library_tagger.py でtagsを付けているので判定漏れかと思ったが、調べたら理由は単純で、チ。は紙本でKindle Unlimited対象外、つまり kindle_library に1行も入っていなかった。タグを付ける入口にすら立っていない。
API側の判定を二段構えに変えた:
kindle_library.tagsにcomicがあれば漫画扱い(既存)amazon_metadata.titleのパターンマッチでも漫画扱い(追加)
「チ。」「進撃の巨人」など紙本だけで持っている漫画のtitle正規表現を足したら、除外件数が 94 → 102件に増えて、★4.5以上の棚から漫画が完全に消えた。残ったのは『コンピュータシステムの理論と実装』『デザインの小骨話』など実用書だけ。
イラスト技法書も漫画枠で除外
「スカートの描き方、髪の描き方、ちょっぴりHな〜、漫画の基礎デッサンも漫画枠で除外してほしい」とユーザー追加要望。漫画ではないが棚に出すと並びが乱れる類の本だ。title正規表現に描き方系・基礎デッサン系のパターンを足して除外 102 → 105件。★4.5以上から技法書が消えて、棚の意味がさらに鮮明になった。
漫画フィルタは「漫画かどうか」ではなく「初期表示から外したい本かどうか」のフラグに育っている。名前は is_comic のまま残しているが、実質は「棚を読書ログとして見たいときに邪魔になる本」のフラグ、と捉え直した。
/books 一覧をKindle/その他で分割
最後にユーザーから「/books の一覧でKindle取り込みとそれ以外を上下に分けたい。これからKindle蔵書を積み増していくので、直近のKindleインポートが上に並んでいる形にしたい」と依頼。
ページを2セクションに割った:
- 📱 Kindle 蔵書から取り込み: 黄色ヘッダー、3冊、
createdAt降順 - 📕 その他(PDF / 紙書籍 OCR / Notion インポート): グレーヘッダー、265冊
Kindle側に直近インポートの『明治維新とは何だったのか』『戦略質問』『31歳でFIRE』が並んでいて、これからKindleインポートを進めるたびに上のセクションが伸びる形になった。
学び
- regex を二段で動かすときは、前段の削除が後段のマッチ条件にどう影響するか想像する。
\s+と\s*の1文字差で BLOODY MONDAY が落ちた - 「漫画」というラベルは、API的には「初期表示から外したい本」のフラグに自然に拡張される。名前と中身が乖離してきたら一度立ち止まる
- データの真実が2つ(Kindle蔵書テーブルと Amazon メタデータテーブル)あるとき、フラグはどちらに置いても漏れる。両方からORで拾うのが安全
- 「Kindleからインポートしたぞ」という事実を画面のどこかに残す。これからKindleを軸にしていく予定が決まっているなら、識別子は早めに足したほうが後の整理が効く