デュエマ買うリスト 再現仕様書 〜 Muse スペース(web_fullstack)を同等に再ビルドするための完全仕様 〜 ================================================================ ■ このファイルの使い方(最初に読んでください) ================================================================ ・このファイルは、Muse のスペース「デュエマ買うリスト」と 全く同じものを、別のアカウント/別の Muse でもビルドできる ようにするための再現仕様書です。 ・使い方:このファイルの内容をそのまま、ビルドを担当する Muse に 貼る/渡してください。それだけで同等のスペースを再現ビルド できる粒度で書いてあります。 ・このファイルには、元の利用者が実際に登録したカード名・価格・ メモ等の個人データは一切含まれていません。 出てくるカード型番や価格の例は、すべて「形式サンプル」です。 ・ビルド担当 Muse へのお願い: - 下記の仕様を省略せず実装すること。特に「型番の正規化」、 「在庫判定」、「店舗リストの並び替え」の3点は挙動が 仕様そのものなので、勝手に簡略化しないこと。 - 取得できない店舗・商品については推測で埋めず、正直に 「取扱なし/取得不可/エラー」と表示すること。 - 店舗サイトへの取得は、負荷配慮の章(■9)を必ず守ること。 ================================================================ ■ 0. 対象と技術前提 ================================================================ 対象:Muse のスペース(web_fullstack / TypeScript スペース) 名称:「デュエマ買うリスト」 想定構成(同等であれば代替可): - クライアント:React 19 + TypeScript、TanStack Query、 Tailwind CSS 4 系 - サーバー:スペースの server actions(TypeScript) - DB:スペース付属の SQLite + Drizzle ORM 相当 - スキーマ定義ファイル、マイグレーション、クライアント画面、 サーバーアクションを分離する 参考パッケージ構成(元実装): react / react-dom 19.x、@tanstack/react-query 5.x、 drizzle-orm、zod、tailwindcss 4.x ※ バージョンはビルド時点のスペース標準に合わせてよい。 重要なのは下記の「挙動」であって、版数そのものではない。 設定: - スペースを作成し、public_web_read を有効化する (店舗ページの HTTP 取得に必要)。 - refresh trigger / cron を設定する(詳細は ■10)。 - カード画像は使わない。テキスト中心のメモ帳デザイン。 ウォッチ中のカードは商品ページへのリンクで代替する。 ================================================================ ■ 1. コンセプト ================================================================ デュエルマスターズの「買うカード」リストを、メモ帳感覚で 管理するスペース。 ・カード名は正式名称との一致を求めない自由入力。 略称・うろ覚え・自分だけの呼び方でもそのまま書ける。 ・追加・編集・削除が気軽にできることが最優先。 堅いマスタ登録やカード DB 照合はしない。 ・リストの中の一部のカードだけ、「そのうち買う」として 型番を指定して価格ウォッチできる。 ・価格ウォッチは型番(=特定の版・収録の商品)単位。 同じ名前でも版で値段が違うため、名前マッチでは追わない。 ================================================================ ■ 2. 画面構成(上から順に) ================================================================ モバイル前提。全体的に上下の余白・行間を詰めたコンパクトな 表示にし、1画面の情報量を多くする(■12 の UI 方針参照)。 1) 一番上:フリーメモ欄(■3) 2) 値下がり通知エリア(未確認のアラートがあるときだけ表示) 3) リスト追加フォーム(■4) 4) ウォッチ状況行(ウォッチ中が1件以上あるときだけ表示) 「ウォッチ中 N件(3日ごとに自動チェック)」+ 「今すぐ価格チェック」ボタン+チェック結果の要約文 5) 未購入リスト(ユーザーが登録した順=挿入順を維持) 6) 「買ったもの」見出し+買ったものリスト(あるときだけ) 空状態: - アイテムが0件:「まだ何も書かれていません。買いたい カードを、思いついたまま書き足していけます。」 - 未購入が0件で買ったものがある:「買いたいものは全部買えました」 - 読み込み中/読み込み失敗の文言も用意する。 ================================================================ ■ 3. フリーメモ欄(一番上) ================================================================ リストには登録されない、フリーのメモ欄を一番上に置く。 ・内容は1件の自由テキストのみ。専用テーブル free_notes に保存。 ・カードとしては登録されない。買うリスト・ウォッチ・通知の いずれにも混ざらない。 ・入力すると自動保存する: - 入力のデバウンス(目安 900ms)で保存 - 欄からフォーカスが外れたとき(blur)にも保存 ・保存状態を表示する:「保存中…」/「保存済み M/D H:MM」 ・高さは固定(目安:textarea rows=5、max-height 約 40=10rem、 縮小可能・リサイズ可)。 中身が長くなったらメモ欄の中だけが独立してスクロールする (overflow-y-auto)。下のリストを押し下げない。 ・プレースホルダー例: 「リストに入らないメモを自由に…(自動で保存されます)」 ・用途イメージ:予算の目安、買いに行く店の話など、 「リストじゃないけど書いておきたいこと」置き場。 ================================================================ ■ 4. 買うリスト本体 ================================================================ ・名前入力欄のプレースホルダーは短く: 「買いたいカードを書く…」 ・その下に補足文を置く。1行に詰めて見切れないように しないこと。折り返し表示で見切れない形(実質2行配置)にする: 「正式名称じゃなくてOKです。思いついたまま書けます。」 ・メモ欄(任意):プレースホルダー「メモ(任意)」 ・必要枚数(■8):数値入力、既定 4、範囲 1〜99 ・「追加」ボタン。名前が空のときは押せない。 ・追加後は入力欄を空に戻し、必要枚数は 4 に戻す。 ・買ったチェック用のチェックボックス (aria-label 例:「(名前) を買ったことにする」) ・カード名(自由入力のまま表示)。買ったものは打ち消し線+ 薄い文字色。 ・メモがあれば名前の下に小さく表示。 ・ウォッチ中なら名前の横に「ウォッチ中」バッジ。 ・操作ボタン(小さいテキストボタンでよい): 編集/そのうち買う(価格ウォッチ)または 型番を変更・ ウォッチ解除/削除 ・削除は即時削除せず「本当に削除」→「やめる」の 確認ステップを挟む。 ・名前・メモ・必要枚数を編集でき、「保存/キャンセル」。 ・名前が空なら保存できない。 ・チェックすると「買ったもの」セクションへ移動する。 ・買ったことにした時点で、そのカードの未確認の値下がり 通知(price_alerts)は消す(dismissed にする)。 ・チェックを外せば未購入に戻る。 ・カード自体の並びはユーザーの登録順のまま。 価格や在庫でカード自体を並び替えたり、下に寄せたり、 グレーアウトしたりしない。 ※ 並び替えとグレーアウトは「カードの中の店舗リスト」 の中だけの話(■8)。ここを取り違えないこと。 ================================================================ ■ 5. 価格ウォッチ:開始と基準価格 ================================================================ ・ウォッチはカードごとに個別に開始する。 「そのうち買う(価格ウォッチ)」から型番入力フォームを開く。 ・型番はカードごとに個別入力する方式。名前からの自動推定や 一括登録はしない。 ・価格の基準店は「遊々亭」。開始時点の遊々亭価格を 基準価格(baseline_price)として記録し、現在価格にも同値を入れる。 ・開始と同時に他店舗(■7)も取得し、多店舗スナップショットと 価格履歴の初回分を保存する。 ・基準が新しくなったら、古い値下がり通知は無効化する。 ・ウォッチ中は型番を変更して再チェックできる (新しい型番で基準を取り直す)。 ・ウォッチ解除もできる。解除時は未確認アラートを消す。 価格履歴は残してよい。 ・取得や解決に失敗したら、ウォッチを開始せず、理由を フォーム内に赤字等で正直に表示する。 ・見出し:「型番」 ・プレースホルダー例: 「例:DM26EX3 DCR10/DCR15 / dmrp20/10036 / 公式URL」 ・補足文例: 「公式サイトの型番表記や公式URLでも大丈夫です (ゼロ埋め・大文字小文字・ハイフンの有無は吸収します)。 同じ名前でも版で値段が違うので、この型番の商品だけを 追います。価格は今の時点を基準にして、3日ごとに チェックします。」 ・ボタン:「ウォッチ開始」/確認中は「価格を確認中…」/「閉じる」 ================================================================ ■ 6. 型番の受理形式と正規化(重要) ================================================================ 以下の入力をすべて受理し、同じ商品に解決できること。 (1) 遊々亭の商品コード 例:dmrp20/10036 (dmrp20-10036 のような区切り違いも可) (2) 収録番号形式 例:DMRP-20 20/95 (3) 公式(タカラトミー)の印刷表記 例:DM26EX3 DCR10/DCR15 (4) 公式サイトのカード詳細 URL 例:https://dm.takaratomy.co.jp/card/detail/?id=dm26ex3-DCR010 → URL の id= パラメータを取り出して (5) と同じに処理する (5) 公式 ID そのまま 例:dm26ex3-DCR010 (6) 前面だけの部分入力(両面カード等の片面だけ) 例:DM26EX3 DCR10 例:収録番号の前面の数字だけ (7) 遊々亭の商品 URL 例:https://yuyu-tei.jp/sell/dm/card/dmrp20/10036 次をすべて同一視する: ・大文字/小文字の違い ・ハイフン(-)・アンダースコア(_)・スラッシュ位置等の 区切りの有無と違い(形式ごとに解釈する) ・空白の有無(半角/全角スペース) ・ゼロ埋め:DCR010 ⇔ DCR10、010 ⇔ 10 → 数値部分は一度数値として解釈し、先行ゼロを落とす ・全角英数字 → 半角に変換してから処理する (日本語 IME で入力されることが多いため必須) ・セットコードは小文字化し、区切り記号を除去して比較する 例:DMRP-20 → dmrp20、DM26EX3 → dm26ex3 ・収録番号は大文字化し、空白を除去し、数値部のゼロ埋めを 落として比較する。文字プレフィックス(DCR、SSP、S 等)や 「/」で区切られた複数面の表記に対応する。 ・商品 URL/商品コード形式(数値のみの商品番号)は、 遊々亭の商品 URL https://yuyu-tei.jp/sell/dm/card/<セットslug>/<商品番号> に直接対応付ける。 ・それ以外(収録番号・公式表記・公式 ID・部分入力)は、 遊々亭のセット一覧ページ https://yuyu-tei.jp/sell/dm/s/<セットslug> を取得し、一覧内の各商品について 「セット+収録番号」を照合して商品を特定する。 照合の目安:商品リンク先 URL と、商品画像の alt 先頭トークン (収録番号が先頭に入っている)を使う。 ・完全一致(収録番号の全表記が一致)を優先する。 完全一致が無ければ、「/」で区切った面のうち1つが入力と 一致する商品を候補として採用する(部分入力 (6) のため)。 ・解決できない場合は正直にエラーを返す: - 型番が空:「型番を入力してください」 - 形式が読めない:入力形式の例を示してエラーを返す - セットが見つからない:その旨を返す - セット内に該当する収録番号が無い:その旨を返す ・絶対に、推測で別のカードに紐付けない。 ================================================================ ■ 7. 価格取得:店舗・代表価格・在庫 ================================================================ 1. 遊々亭(yuyu-tei.jp) … 基準店。キー:yuyutei 2. フルアヘッド(https://fullahead-dm.com) … キー:fullahead 3. カードラッシュ(https://www.cardrush-dm.jp/phone/) … 表示名は「カードラッシュ」。キー:cardrush 4. kolo屋(https://www.koloya-online.com) … 表示名は必ず「kolo屋」(「コロ屋」と表示しない)。 キー:koloya 5. マスターズスクウェア(https://www.tcgacademy.com/phone/product-list/1418) … ドメインは tcgacademy.com だが、表示名は必ず 「マスターズスクウェア」。 「TCGアカデミー」と表示しない。キー:tcgacademy ・最安価格の店名表示など、店名が出る全箇所で上記の表示名を使う。 ・カーナベル(ka-nabell.com)は含めない。 理由:Cloudflare の bot 対策("Just a moment…" チャレンジ)で サーバー側の通常取得では 403 になり価格が取れないため。 無理に突破を試みないこと。取得先リスト・平均・最安の 計算のいずれにも入れない。過去データに残骸行があっても 表示・計算から除外する。 ・名前だけで照合しない。必ず「セット+収録番号(正規化済み)」の 両方が一致した商品だけを、そのカードの価格として採用する。 ・検索キーワードとして使うカード名は、遊々亭の商品名から レアリティ接頭辞と末尾の括弧書き仕様名を取り除いた クリーンな名前を使う。 ・店舗のタイトル内の型番表記は店ごとに違うので、店ごとに 表記ゆれを吸収する: - カードラッシュ:タイトル中の {26EX3DCR10/DCR15} のような 波括弧コード(セット slug から "dm" を除いた形が 収録番号に直結している) - マスターズスクウェア:[DM_26EX3_DCR10/DCR15_SR] のような 角括弧コード(末尾のレアリティトークンは落とす、 ゼロ埋め+レアリティ文字が付く場合がある) - kolo屋:(DM26-EX5 G1/G20 SR)のような括弧コード - フルアヘッド:DM26-EX3/DCR10/SR/カード名のような スラッシュ区切りタイトル(前面のみ表記) 収録番号の比較は面ごとに行い、数値部のゼロ埋めと、 プレフィックスの無い面に付いたレアリティ文字 (例:020R → 20)を吸収する。前組みのみ表記の店では 前面一致でも可とする。 ・店舗ごとに代表価格は1件だけ使う。 ・在庫のある通常状態の商品を優先する。 ・傷あり品・状態グレード違い(「状態A-」等)・キズ表記のある 商品は代表にしない(候補から後回し/除外する)。 それしか無い場合のみ候補に残してよいが、通常状態と 混ぜて平均しない。 ・取扱なし(not_found)/取得不可(error / blocked)の店は、 平均価格・最安価格の計算に入れない。その代わり店舗リストに その旨を表示する(■8)。 ・商品ページを取得し、JSON-LD(application/ld+json)の Product ブロックから offers.price(JPY 整数)、 offers.availability、name、description を読む。 ※ description には収録番号(例:DCR10/DCR15)が入っている。 ・非 200 や Product ブロック欠落は「価格 0」にはせず、 取得エラーとしてアイテムに記録し UI に正直に表示する。 ・在庫数は買い物かご欄の「在庫: N 点」表記から読む (PC/モバイル両レイアウトにあり、最初に現れたものを採用)。 ・カードラッシュ/kolo屋/マスターズスクウェアは 同系のショッププラットフォーム(OCNK 系): キーワード検索 <ベース>/product-list?keyword=<カード名> (カードラッシュとマスターズスクウェアは /phone/ 配下)で 結果一覧(list_item_cell ブロック)を取得し、 タイトル・販売価格(selling_price)・在庫表記・商品 URL を読む。 ・フルアヘッドは MakeShop 系でページが EUC-JP: セット別ブランドページ /shopbrand/<セット由来 slug> を辿る (名前検索は EUC-JP エンコードが必要で実装が重いため ブランドページ方式を採る)。ページは最大6ページ程度まで、 TextDecoder("euc-jp") でデコードする。 タイトル・価格(itemPrice)・在庫文言・商品リンクを読む。 ブランド slug はセット slug から候補を複数生成して試す (例:dm26ex3 → dm26-ex03 等)。 ★取りこぼし注意 ・在庫数が取れる店は、価格と一緒に在庫数も取得する。 表記ゆれに対応すること: - 遊々亭:「在庫: N 点」 - OCNK 系各店(カードラッシュ/kolo屋/マスターズスクウェア): 「在庫数 N点」 - フルアヘッド:「残りあとN個」 ・売り切れ判定は「在庫数 0」だけに頼らないこと。 次の明示シグナルもすべて売り切れとして拾う: - 本文の明示文「在庫なし」「売り切れ」「SOLD OUT」 - soldout クラス(例:class="stock soldout" の在庫要素) - カードラッシュの行自体に付く list_item_soldout クラス (売り切れ行はカートフォームが無くなることもある) ★注意書き:これらを取りこぼすと、実際は在庫なしの商品が 「在庫不明」扱いになり、並び替えで在庫潤沢側に誤表示される バグになる(実際に起きた不具合)。在庫数のある商品が 正常でも、文言タイプの売り切れは別途拾うこと。 ・その他の判定: - 遊々亭:JSON-LD の availability(InStock かどうか) - フルアヘッド:在庫ラベルの文言 (「あとN個」「在庫あり」→あり/「切れ」「売り切れ」 「なし」→なし/カート追加要素があればあり) - OCNK 系:在庫数が1以上、またはカートフォームがあればあり ・在庫数が読み取れない店は stock_count = null(不明)とする。 例:遊々亭で数ではなく「◯」のような記号表記しか出ない場合。 不明な店は「在庫あり(潤沢側)」として扱い、在庫不足とは 断定しない。不足判定は「数が判明していて足りない」場合だけ。 ================================================================ ■ 8. 価格表示・在庫ラベル・店舗リストの並び替え ================================================================ ・平均価格(価格が取れた店のみの平均、四捨五入した円整数) ・最安価格+最安の店名(表示名で) 例:「最安 3,780円(カードラッシュ)」 ※ 価格が取れた店が1店だけの場合は、平均/最安ではなく その価格(遊々亭の現在価格)を大きく表示する従来形式でよい。 ・前回比の推移チップ(■価格履歴を参照) ・小さな折れ線グラフ(スパークライン) ・サブ情報行(小さめ文字): 必要 N 枚/遊々亭 現在価格/基準価格/基準との差 (値下がりは強調色で「N円 値下がり」、値上がりは「+N円」、 同額は「±0」)/在庫ラベル/最終チェック日時 ・店舗別リスト(下記ソート済み):店名・価格・在庫ラベル・ 商品ページへのリンク ・商品名と、ユーザーが入力した型番(そのまま表示) ・遊々亭の取得エラーがあれば内容を表示する ・在庫数 1〜3:「在庫僅少(残りN枚)」 ・在庫数 4以上、または数不明で在庫あり:「在庫あり」 ・在庫数 0/売り切れ:「在庫なし」 ・取扱なし:「取扱なし」 ・取得不可(アクセス制限):「取得不可(アクセス制限)」 ・その他の取得不可:「取得不可」 ★「在庫僅少」は目立つ色・太字にしない。通常の文字色にする。 理由:デュエマでは同名カードはデッキに最大4枚までで、 4枚が一度に揃わない在庫は「お得」ではなくマイナス要素 だから。注意喚起色で目立たせてはいけない。 (例外として従来からある「在庫なし」の強調表示は維持してよい) ・カードごとに必要枚数を持つ。既定は 4 (デュエマの同名デッキ上限)。追加・編集で 1〜99 に変更可。 ・既存カードを移行する場合は一律 4 を初期値とする。 ・パネルに「必要 N 枚」と表示する。 ★最重要 各カードの中の店舗リストだけを、次の「段(tier)」順に ソートする。各段の中では価格の安い順(価格なしは段の最後)。 第1段:在庫潤沢 在庫ありで、在庫数が 4以上、または在庫数不明。 かつ必要枚数を満たしている。 第2段:在庫僅少だが必要枚数は満たす 在庫数 1〜3 で、かつ在庫数が必要枚数以上。 (必要枚数が3以下のときだけ到達する。グレーアウトしない) 第3段:在庫はあるが必要枚数に足りない 在庫数が判明していて、必要枚数未満。 ラベルは「在庫僅少(残りN枚)」。 第4段:在庫なし 第5段:取扱なし・取得不可 ・グレーアウトは第3段から(文字色を薄くし、opacity を少し下げる)。 ・つまり下段グループの中では 「在庫あるけど足りない僅少」→「在庫なし」→「取扱なし」 の順になる。 ・在庫数不明の店は絶対に下段へ落とさない (不足は数が判明した場合だけ断定する)。 ・既定の必要枚数 4 の場合、第2段は空になり、並びは 潤沢(安い順)→ 足りない僅少(安い順・グレー)→ 在庫なし(グレー)→ 取扱なし(グレー)に帰着する。 ・価格履歴は店舗別系列+平均系列(store_key = 各店舗キー および "avg")で保持する。チェックのたびに、価格が取れた 店舗ごとに1行+平均1行を追加する。 ・表示系列:平均系列の点が2つ以上あれば平均系列を、 無ければ遊々亭系列を使う(多店舗化前の履歴互換のため)。 ・前回比チップ:表示系列の最後の2点を比べて 「▲値上がり/▼値下がり/→維持」を差額付きで表示する (例:「▼ 値下がり 前回−200円」。差額0のときは差額を出さない)。 値下がりのときだけ強調色にする。 ・スパークライン:表示系列の直近最大12点を、幅72×高さ20程度の 小さな SVG 折れ線で描く。場所を取らないこと。 最後の点にドットを付ける。 ・履歴のテキスト一覧は標準では畳んでおく/置かない。 スペースを増やさないシンプルデザインにする。 ================================================================ ■ 9. 負荷配慮クロール(店舗サイトへの配慮) ================================================================ ・価格取得は AI の作業ではなく、プログラムによる通常の HTTP 取得で行う。LLM トークンは消費しない。 (店舗を増やしてもトークン消費は増えない、と利用者に 説明できる設計にすること) ・取得は直列:店舗ごと・カードごとに順番に取得し、 並列バーストをしない。 ・実リクエストの間隔は約0.6秒以上空ける(pace 処理)。 ・共有できるページは1回のチェック内で1回だけ取得する: 取得成功したページテキストを短時間(目安10分)キャッシュし、 同じセットの一覧ページや同じ名前検索結果を複数カードで 使い回す(例:フルアヘッドのセットページは、そのセットの 全カード分をまかなえる)。 ・取得タイムアウトは1リクエスト 15〜20秒程度。 User-Agent は一般的なブラウザのものを付ける。 ・自動チェックの頻度は3日おき(■10)。この頻度と上記の 配慮があれば、店舗側の通常アクセスに対して十分に 小さい負荷である、という前提で運用する。 ・「今すぐ価格チェック」の連打だけは利用者に避けてもらう 想定でよい(特別なロックは不要)。 ================================================================ ■ 10. 定期チェックと値下がり通知 ================================================================ ・cron / refresh trigger:72時間間隔(3日おき)に checkPrices アクションを実行する。 ・手動チェック:画面の「今すぐ価格チェック」ボタンからも 同じアクションを実行できる。実行後は 「N件チェックしました」「N件が基準価格より値下がり しています」「N件は取得できませんでした」のような 要約を表示する。ウォッチ中が0件ならその旨を表示する。 ウォッチ中の全カードについて順番に: 1. 遊々亭の商品ページを再取得し、現在価格・在庫・在庫数を更新、 最終チェック日時を更新する(失敗時は last_error に記録)。 2. 他店舗も取得し、store_prices を最新スナップショットに 差し替える(1カード×1店舗=1行)。 3. price_history に店舗別系列と平均系列の点を追加する。 4. 遊々亭価格が基準価格を下回ったら値下がりとして記録する。 ・判定基準はあくまで遊々亭価格 vs 基準価格。 他店舗の平均・最安は表示用で、通知判定には使わない。 ・値下がりしたら: - スペース上部に通知カードを表示する (カード名/基準価格 → 現在価格/差額/検出日時/ 遊々亭の商品リンク/閉じるボタン)。 既に未確認の通知があるカードは、通知の現在価格を 最新の値下がり価格に更新する(重複を作らない)。 - チャットでも知らせる:カードごとに 「カード名/基準価格 → 現在価格/いくら安くなったか/ 遊々亭の商品URL」をまとめて1通だけ送る。 ・下がっていないときは何もしない。通知もチャット送信もしない (静かに回す)。取得エラーだけのときもチャット通知はしない (エラーはスペース内の各カードに表示される)。 ================================================================ ■ 11. データモデルとアクション ================================================================ 買うリスト本体 id 整数 主キー 自動採番 name テキスト 必須(自由入力のカード名) memo テキスト 既定 '' needed_count 整数 既定 4(必要枚数) bought 真偽 既定 false sort_order 整数 既定 0 watching 真偽 既定 false card_code テキスト NULL可(ユーザーが入力した型番・原文のまま) product_url テキスト NULL可(解決済みの遊々亭商品 URL) collector テキスト NULL可(収録番号 例:DCR10/DCR15、20/95) product_name テキスト NULL可(遊々亭の商品名・レアリティ接頭辞付き) baseline_price 整数 NULL可(ウォッチ開始時点の遊々亭価格) current_price 整数 NULL可(最新の遊々亭価格) in_stock 真偽 NULL可(遊々亭の在庫有無、不明は NULL) stock_count 整数 NULL可(遊々亭の在庫数、不明は NULL) last_checked_at 日時 NULL可 last_error テキスト NULL可 created_at / updated_at 日時 店舗別スナップショット(最新のみ、1カード×1店舗=1行) id、item_id、 store_key テキスト(yuyutei / fullahead / cardrush / koloya / tcgacademy) price 整数 NULL可 in_stock 真偽 NULL可 stock_count 整数 NULL可 product_url テキスト NULL可 product_name テキスト NULL可 status テキスト 既定 'ok'(ok / not_found / error / blocked) error テキスト NULL可 checked_at 日時 価格履歴 id、item_id、price(整数)、 store_key テキスト 既定 'yuyutei' (各店舗キー、または平均系列の 'avg') checked_at 日時 ※ listItems では系列ごとに直近12点までを返す程度に抑える。 値下がり通知 id、item_id、baseline_price、dropped_price、 created_at、dismissed(真偽 既定 false) フリーメモ(1行だけ使う) id(=1 固定)、text(既定 '')、updated_at getFreeNote 引数なし → { text} saveFreeNote { text(最大 20,000 文字)} → { ok} id=1 へ upsert する listItems 引数なし → { items, alerts} items には店舗配列(store_key, store_name, price, in_stock, stock_count, product_url, status, error, checked_at)、履歴、平均価格 avg_price、最安価格 min_price、最安店名 min_store_name を含める。 店舗定義に無い store_key の残骸行は除外する。 alerts は未確認(dismissed=false)のもの。 addItem { name(1〜200文字), memo(最大500文字), neededCount(1〜99、既定4)} → { id} updateItem { id, name, memo, neededCount(任意)} → { ok} deleteItem { id} → { ok} 本体と一緒に price_history / price_alerts / store_prices も消す setBought { id, bought} → { ok} bought=true のとき未確認アラートを消す startWatch { id, cardCode(1〜300文字)} → { ok, error, price, product_name, product_url} 型番解決→遊々亭取得→基準記録→他店舗取得まで行う。 失敗時は ok:false と理由を返し、状態を変えない。 stopWatch { id} → { ok}(未確認アラートも消す) checkPrices 引数なし → { checked, dropped, errors} dismissAlert { id} → { ok} ・価格はすべて JPY の整数。価格 0 や NaN を実価格として保存しない。 ・日時はミリ秒タイムスタンプで保持し、表示は閲覧側の タイムゾーン(Asia/Tokyo 想定)で「M/D H:MM」形式にする。 ================================================================ ■ 12. UI 方針・デザイン ================================================================ ・全体的に上下を詰めたコンパクトな表示。行の上下 padding は 小さめ(目安 py-2)、セクション間も詰める。 1画面の情報量を増やすことを優先する。 ・モバイル前提の1カラム。コンテンツ幅は最大で 2xl 程度、 左右 padding あり。 ・メモ帳のようなテキスト中心の素朴さ。装飾は最小限。 カード画像は使わない。 ・色の目安(CSS 変数でライト/ダーク両対応): --bg #fdfcf8 / ダーク #171410 --surface #fffefb / ダーク #201c16 --surface-alt #f5f1e6 / ダーク #29241c --text #23201b / ダーク #f2ede1 --dim #8a8378 / ダーク #a29885(補助文字) --border #e5ddcb / ダーク #3a342a --accent #c22730 / ダーク #e5534b (値下がり・在庫なし・削除確認などの強調) --accent-soft #fbeeee / ダーク #3a1f1d(通知カード地) --watch #b07f10 / ダーク #eebd4d(ウォッチ系の色) --watch-soft #fdf3d7 / ダーク #38300f(ウォッチバッジ地) --radius 10px ※ 「在庫僅少」にこれらの強調色を使ってはいけない(■8)。 ・見出しフォントはゴシック系ディスプレイフォント (例:Zen Kaku Gothic New)、本文は system-ui 系。 ・タップ対象は最低 32px 程度の高さを確保する。 ・文字サイズの目安:本文 16px、補助 14px/12px、 パネルの平均価格は 20px の太字表示。 ================================================================ ■ 13. ビルド手順の目安 ================================================================ 1. Muse で web_fullstack スペース「デュエマ買うリスト」を作成。 2. public_web_read を有効化する。 3. ■11 のデータモデルでスキーマとマイグレーションを作る (初期状態は空。サンプルや他人のデータを入れない)。 4. ■11 のアクションを実装する。 特に startWatch の型番解決(■6)、各店舗の取得と 在庫判定(■7)、checkPrices の直列+間隔+キャッシュ(■9)。 5. クライアント画面を ■2〜■5・■8・■12 通りに実装する。 特に店舗リストの段ソート(■8)とフリーメモ(■3)。 6. refresh trigger / cron を設定する: 72時間間隔で checkPrices を実行し、結果の dropped が 1以上のときだけ、値下がりしたカードの 「カード名/基準価格 → 現在価格/差額/遊々亭商品 URL」を まとめてチャットに1通知らせる。dropped が 0 のときは 一切メッセージを送らない。 7. ■14 の検証観点をすべて確認してから公開する。 ================================================================ ■ 14. 検証観点(ビルド後に必ず確認) ================================================================ □ 公式の印刷表記(例:DM26EX3 DCR10/DCR15)と公式サイト URL (?…id=dm26ex3-DCR010)が、同じ遊々亭商品に解決すること。 □ ゼロ埋め・ハイフン・空白・大文字小文字・全角入力の バリエーションでも同じ商品に解決すること。 □ 前面だけの部分入力(例:DM26EX3 DCR10)でも解決すること。 □ 存在しない型番・存在しないセットは、推測で別カードに せず、正直にエラーになること。 □ 「在庫なし」と明記された商品(在庫数表記が無いタイプ)が、 在庫なしとして店舗リストの下段にグレー表示されること。 □ 在庫数のある商品(例:在庫数 14点)が売り切れに 誤判定されないこと。 □ 在庫数 1〜3 の店が「在庫僅少(残りN枚)」と通常文字色で 表示され、必要枚数 4 のカードではその店が下段グループに グレーで入り、在庫なしよりは上に並ぶこと。 □ 必要枚数を 2 に変えると、在庫 2〜3 の店がグレーにならず 上の段に残ること。 □ 店名が「kolo屋」「マスターズスクウェア」と表示され、 「コロ屋」「TCGアカデミー」と表示されないこと。 カーナベルが店舗リストにも平均・最安にも出ないこと。 □ 平均価格・最安価格(店名付き)が、価格の取れた店だけで 計算されていること。 □ 履歴が2回分たまると、前回比チップとスパークラインが 出ること。 □ フリーメモがリストに混ざらず、自動保存され、長文でも メモ欄内だけがスクロールして下のリストを押し下げないこと。 □ リスト追加フォームの補足文が見切れずに読めること。 □ 3日おきのチェックで、値下がりが無いときは通知が 来ないこと。値下がりしたときだけ通知が来ること。 ================================================================ ■ 15. やらないこと(スコープ外・禁止事項) ================================================================ ・カード名の正式名称マスタ照合や、名前からの価格推定。 ・カーナベルの bot 対策の突破。 ・取得できなかった価格・在庫を推測で埋めること。 ・店舗サイトへの高頻度・並列バースト取得。 ・カード自体を価格や在庫で並び替えること (並び替えはカード内の店舗リストだけ)。 ・購入処理やカート連携(あくまで買うリストと価格ウォッチ)。 ―― 以上が再現仕様のすべてです。このファイルだけで、 元の「デュエマ買うリスト」と同等のスペースを 再ビルドできます。