LOVE_推し診断 技術解説

111 Views

July 21, 26

スライド概要

profile-image

Railsメインでやってるプログラミング歴14年のフリーランスSEです🍾 Progakuというエンジニアコミュニティを運営してます😎 https://note.com/tokuyuuuuuu/n/nd5d777751b60

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

TECHNICAL BRIEF / READ-ALONG ⚠ 非公式・ファンメイド / Unofficial fan project — 公式とは一切関係ありません =LOVE 推し診断 30問に答えると相性の良い =LOVE メンバーを提示するWebアプリの 仕様書 / アーキテクチャドキュメント TL;DR 静的TSデータ + クライアント計算のみで完結する Next.js 16 (App Router) 製のミニアプリ。 サーバーDBなし、回答は完走時のみ localStorage 保 存、診断は純関数で表現。一致率は 70〜99% にクランプ。 FOR READING TIME STACK SCOPE フロントエンド / Webアプリ開発者 約 10〜15 分 Next.js 16 / React 19 TypeScript / Tailwind 4 仕様 / データ設計 / 診断ロジック / 永続化 / 運用 / 設計意図 (13ページ / 図解込み) 前提 =LOVE (イコラブ) は10人組アイドルグループ。本資料内の members / questions はプロダクト内の静的TSデータを指し、データ内容の解説ではなく実装仕様の解説を目的とする。 DOC · =love-shindan · v1 01 / 13

2.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 02 / HOW TO READ & TABLE OF CONTENTS この資料の読み方 と 目次 READING GUIDE TABLE OF CONTENTS 読み方 目次 🎯 目的 03 プロダクト概要 09 状態管理と永続化 04 提供価値とスコープ 10 アーキテクチャ全体図 05 画面構成とフロー 11 技術スタックと運用 📖 表記ルール 06 画面ごとの責務 12 設計意図とトレードオフ OFF = トレードオフの明示。 07 ドメインモデルとデータ 13 FAQ と 参考情報 個々の質問文・メンバー名などのデータ中身は非掲載。あくまで実装の仕様・構 08 診断アルゴリズム ★ プロダクトの仕様と技術構成を、後から特定箇所を引ける形で残す。 説明の網羅 性より、1ページ1トピックの参照性を優先。 👤 読み手 Next.js / React / TypeScript の基礎を前提とする。 Tailwind、shadcn、Vercel、 localStorage API の存在は既知として扱う。 monospace = ソース中の識別子・キー名 / bold = 定義される用語 / TRADE- 🚫 扱わない範囲 造を対象とする。 =LOVE 推し診断 / 02 How to read What / For whom / How long In-scope / Out-of-scope / → /diagnosis → /result TOP / 診断 / 結果 4 entities + dataset stats useState + localStorage Data flow (client only) Stack / Deploy / OGP / ENV なぜDBなし / なぜクランプ / max-w-md よくある疑問 / 拡張余地 集計 → ランキング → 一致率 ★ = コア。時間が無ければ 08 を最優先で読む。 02 / 13

3.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 03 / PRODUCT OVERVIEW プロダクト概要 DEFINITION =LOVE 推し診断 とは、ユーザーが 30問4択のアンケート に答えると、事前に定義されたスコアリング関数を通じて 「相性の良いメンバ ー」を 10人の中から1人 選出し、一致率 (70〜99%) と 上位3人のランキング を提示する クライアント計算完結型のWebアプリを指す (以降 本資料では単に「本アプリ」と表記)。 WHAT · 何を提供するか FOR · 想定利用者と文脈 ・30問4択のアンケート形式の診断UI ・一次想定: =LOVE のファン (グループ既知) ・1位メンバーの詳細 (プロフィール・SNS・センター曲・ソロ曲) ・流入経路: SNS (X) からのモバイル直リンク中心 ・上位3人までの ranking と相対比 (ratio) ・利用状況: 電車内など片手・数分の隙間時間 ・一致率帯 (3段階) に応じた診断コメント ・再訪動機: 診断のやり直し、結果のスクショ共有 TIME · 想定所要時間 ・診断本体: 約 1 分 (30問 × 数秒/問) ・初回ロード: 静的アセット + フォント読込 (Zen Maru Gothic 等) ・離脱リスクを下げるため、質問文と選択肢は短文で統一 NATURE · 位置づけ ・個人開発のファンメイド作品であり、公式運営・所属事務所とは 一切関係を持たない ・公式サイト等へのリンクは掲載するが、公式コンテンツの二次配信は行わない ・広告・課金・アカウント機能は無い =LOVE 推し診断 / 03 Product Overview 03 / 13

4.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 04 / VALUE & SCOPE 提供価値 と やらないこと In-scope / Out-of-scope を明示することで、実装の判断基準を残す。 ✓ In-scope · 提供する機能 × 診断フローの進行 サーバーサイドのDB / API 30問4択を最後まで進める。2問目以降は前の質問へ戻れる。 静的TSデータ + クライアント計算のみで完結。REST/GraphQLエンドポイントなし。 1位選出と上位3人のランキング 回答 / 結果のサーバー保存 10人からスコア降順で並べ、上位3人を ratio と共に表示。 送信もしない。ユーザー端末の localStorage 内で完結する。 一致率と診断コメント ユーザー登録・ログイン・履歴 matchRate (70–99%) を算出し、帯 (3段階) に応じたコメントを付与。 認証機構を持たない。過去の診断履歴の閲覧・共有機能もなし。 メンバー情報の提示 公式コンテンツの二次配信 プロフィール・SNS・センター曲・ソロ曲を結果画面から辿れる。 楽曲・映像・画像等の配信は行わない。SNS等への外部リンクのみ。 再診断 運営・ファンクラブ機能の代替 結果画面から answers をクリアし、/diagnosis へ戻す。 =LOVE Out-of-scope · 意図的に提供しない 推し診断 / 04 Scope 公式との差別化・置換ではなく、あくまでファンによる遊びとして位置づける。 04 / 13

5.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 05 / SCREEN FLOW 画面構成 と ユーザーフロー ルートは 3 つ。永続化タイミングと再診断時の挙動を明示する。 / /diagnosis TOP CTA click /result 診断 save & push 結果 ・サービス説明 ・30問を1問ずつ表示 / 進捗バー ・localStorage から answers を読込 ・診断スタート CTA ・4択選択で自動遷移 (2問目以降は戻る可) ・一致率・コメント・ランキング・メンバー情報 ・公式チャンネルリンク ・完走時に answers を localStorage 保存 ・「もう一度診断」で再開 (公式サイト / YouTube / X / Instagram) 再診断 = answers をクリアして /diagnosis へ BEHAVIOR SPEC · 挙動仕様 診断中のリロード /result 直リンク 再診断ボタン 別デバイス / 別ブラウザ =LOVE 推し診断 / 05 Screen Flow useState 上の answers は消え、/diagnosis は最初 (Q1) から始まる。 意図: 中断状態を残さない localStorage に answers があれば結果を復元表示。無ければエラー/リダイレクト扱い。 answers をクリア (removeItem) してから router.push('/diagnosis')。前の結果は都度消える。 localStorage はオリジン + デバイス単位。結果は他デバイスに引き継がれない。 05 / 13

6.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 06 / SCREEN SPEC 画面ごとの責務 各画面が保持する状態、外部との依存、遷移先を明示する。 TOP / 推奨) /diagnosis 結果 /result Server Component ( Client Component ("use client") Client Component ("use client") RESPONSIBILITY RESPONSIBILITY RESPONSIBILITY サービス説明とスタートCTAを提示。外部リンク集を表示。 30問4択の対話フローを進行させ、完走時に answers を保存す answers を読み、calculateDiagnosisResult を呼んで結果を提 UI ELEMENTS る。 示する。 ・ヒーロー (サービス名・キャッチ) UI ELEMENTS UI ELEMENTS ・診断スタートボタン (→ /diagnosis) ・進捗バー (step / 30) ・1位メンバーカード + 一致率 ・現在の質問文 + 4つの選択肢ボタン ・診断コメント (matchRate 帯で3分岐) ・戻るボタン (Q2 以降で活性) ・上位3人のランキング (member × ratio) ・完了時のトランジション → /result ・プロフィール / SNS / センター曲 / ソロ曲 ・公式チャンネル 4リンク ・非公式である旨のディスクレーマ STATE なし (純粋な静的ページ) DEPENDENCIES 外部URL文字列のみ (メンバーデータ非依存) =LOVE 診断 推し診断 / 06 Screen Spec STATE answers: Answer[] と currentIndex: number を useState で保 ・「もう一度診断」ボタン STATE 持 マウント時に localStorage.getItem → useState に格納 DEPENDENCIES DEPENDENCIES questions (静的) / localStorage (書込) members / questions / 純関数 calculateDiagnosisResult 06 / 13

7.
[beta]
=LOVE 推し診断 / Technical Brief

非公式・ファンメイド

07 / DOMAIN MODEL & DATA

ドメインモデル と データ
4つの型定義でアプリ全体の情報を表現する。左3つは静的入力、右1つは派生結果。

STATIC ·

入力

STATIC ·

Question

テーマ色

30問。intent は編集時のガイド用メタ。

has 4

入力

topMember: Member
matchRate: number // 70-99
comment: string
ranking: {
member: Member,
score: number,
ratio: number // 0-1
populates }[]
totalScore: number

し、外部APIを呼ばずに詳細を出せる設計。

MEMBERS

10

人

10人組アイドルグループ全員

QUESTIONS

30

問

× 4 choices

10人分。結果画面で必要となる表示要素をすべて保持

Choice

answers から都度計算する派生値。永続化しないため、

= 120 選択肢

ロジック改善が即座に反映される。

text: string
scores: Record<
memberId, number>
typeHint: string // UI

非表示

scores はメンバーID → 加点。
通常は 3 / 2 / 1 のいずれか、他は 0。

推し診断 / 07 Domain

派生 (非保存)

DiagnosisResult

id: string
name: string
description: string
color: string //
songs: string[]
profile: { ... }
sns: { ... }
officialUrl: string

非表示

=LOVE

COMPUTED ·

Member

id: string
question: string
intent: string // UI
choices: Choice[4]

STATIC ·

入力

references
by id

SCORE WEIGHT

3/2/1

primary / secondary /
tertiary (
0)

他は

07 / 13

8.
[beta]
=LOVE 推し診断 / Technical Brief

08 / CORE

非公式・ファンメイド

★ DIAGNOSIS ALGORITHM

診断アルゴリズム

本アプリのコア。純関数 calculateDiagnosisResult(answers) 内で 3 ステップで処理する。

1

集計 · Aggregate

全回答について、選ばれた Choice.scores をメンバーごとに合
算する。他メンバーは 0 なので実質的に「選択肢が持つ 3/2/1 の
加点を1〜3人に配る」構造。

メンバー → 合計スコア

//
ID
const total = {} as Record<string, number>;
for (const a of answers) {
const choice = questions
.find(q => q.id === a.questionId)!
.choices[a.choiceIndex];
for (const [id, s] of
Object.entries(choice.scores)) {
total[id] = (total[id] ?? 0) + s;
}
}
weight: primary=3 / secondary=2 / tertiary=1 / other=0

⚠ 用語の区別
=LOVE

ランキング · Sort

2

スコア降順で並べ、比率は1位スコアを基準とした相対比 (ratio

∈ [0, 1]) を各エントリに持たせる。上位3人を ranking に格

3

一致率クランプ · Clamp

理論最大点 answeredCount × 3 に対する 1位の到達度を求め、線
形写像で 70〜99% の帯に変換する。

納。

const entries = Object.entries(total)
.map(([id, score]) => ({
member: byId[id], score,
}));
entries.sort((a, b) => b.score - a.score);
const top = entries[0].score;
const ranking = entries.map(e => ({
...e,
ratio: e.score / top, // 0.0-1.0
})).slice(0, 3);

注: ratio は「1位比」であって「一致率」ではない (次のステップと別物)

const raw = topScore /
(answeredCount * 3); // 0.0-1.0
const matchRate = clamp(
70 + Math.round(raw * 29),
70, 99
); // always 70..99

数値例

//
// raw=0.30
// raw=0.60
comment

→ 70 + round(8.7) = 79%
→ 70 + round(17.4) = 87%

は 3 段階の閾値で切替 (例: high / mid / low)

matchRate (一致率, 70–99%) = 理論最大点への到達度を写像した表示値 / ratio (ランキング比率, 0–1) = 1位を1.0とした相対比。別の量である。

推し診断 / 08 Algorithm (Core)

08 / 13

9.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 09 / STATE & PERSISTENCE 状態管理 と 永続化 「診断中はメモリ」「完了時のみ localStorage」「結果画面で再計算」の 3 原則で構成する。 SEQUENCE · 診断フローと永続化タイミング RULES · 3 原則 User × DiagnosisFlow × localStorage User DiagnosisFlow (useState) localStorage ① Choice を選択 (× N 回) 状態の置き場所 💭 診断中 · メモリのみ 回答は useState でメモリ保持。 リロード / ブラウザバック / タブ閉じで消える。 ② setAnswers([...prev, a]) // React state のみ更新 ③ answers.length === 30 localStorage.setItem(KEY, JSON) ④ router.push('/result') ⑤ getItem(KEY) → answers を復元 ⑥ calculateDiagnosisResult() // 純関数 / 結果は保存しない → 中途半端な進捗を残さない 💾 完了時のみ保存 30問全部揃った瞬間だけ setItem。 キー: equal-love-diagnosis-answers 値: Answer[] の JSON.stringify 🔄 結果画面で再計算 DiagnosisResult は永続化しない。 calculateDiagnosisResult をマウント時に呼ぶ。 → ロジック更新が即座に反映される サーバー送信 · DB 保存 · Cookie 使用はすべて無し =LOVE 推し診断 / 09 State & Persistence 09 / 13

10.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 10 / ARCHITECTURE アーキテクチャ全体図 すべてブラウザ内で完結。ネットワークが関わるのは初回の静的アセット取得のみ。 — BROWSER (Client) — サーバー送信 · D B 書込みなし STATIC COMPONENT · CSR BROWSER STORAGE COMPONENT · CSR PURE FUNCTION members.ts questions.ts DiagnosisFlow localStorage ResultView /diagnosis で 30問進行。 useState で answers 保持。 完走時のみ localStorage 書込。 key: equal-lovediagnosis-answers value: Answer[] JSON /result で表示。 DiagnosisResult は保存しない。 再診断で answers を removeItem。 calculateDiagnosis Result TS でコード同梱。 内容変更はコード修正+デプロイ 経由。 型は import 時に静的解決。 import save on complete members / questions read on mount を参照 EXTERNAL SERVICES · Vercel Analytics / Speed Insights ( DATA FLOW · call 集計 → ランキング → 一致率クランプ。 副作用なし / テスト容易。 計測のみ、診断ロジックは通らない ) 一行サマリ members / questions (静的TS) → DiagnosisFlow (state) → localStorage → ResultView → calculateDiagnosisResult() 特徴: (1) すべてクライアント内で完結 / (2) 静的データと純関数の間に副作用がない / (3) localStorage は「答えの受け渡しバッファ」であり結果自体は毎回計算 =LOVE 推し診断 / 10 Architecture 10 / 13

11.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 11 / STACK & OPS 技術スタック と 運用 採用ライブラリ / デプロイ経路 / 環境変数 / メタデータの一覧。特殊な運用要件は無い。 STACK · 採用技術 DEPLOY & OPS · 技術スタック 運用 デプロイ / 運用 Framework Next.js 16 (App Router) UI Runtime React 19 / TypeScript Styling Tailwind CSS 4 / shadcn/ui / lucide-react Font Zen Maru Gothic (本文) + Mochiy Pop One (見出し) Layout モバイルファースト / max-w-md 中央寄せ Pkg Manager pnpm (workspaces は使用しない) Data Layer 静的 TS モジュール members.ts / questions.ts Rendering TOP は Server Component / 診断・結果は Client Component Observability Vercel Analytics / Speed Insights Not used DB / REST API / GraphQL / 認証基盤 / 状態管理ライブラリ RUNTIME NOTE 診断ロジックは Server Action / Route Handler / API Route を経由せず、クライアント側で完結する。 HOSTING Vercel Gitリポジトリと連携したプッシュベース自動デプロイ。プレビューデプロイでレビュー可能。データ 更新もコード修正→デプロイのフロー。 METADATA / OGP OGP画像: /og-top.png (静的画像) Twitter card: summary_large_image タイトル / 説明は Next.js の metadata API で定義 ENVIRONMENT VARIABLE NEXT_PUBLIC_SITE_URL Next.js の metadataBase に使用。OGP等の絶対URL生成用。 未設定時は http://localhost をフォールバックとし、本番環境では必ず設定する。 計測範囲 MONITORING · Vercel Analytics: アクセス数 / ページ別PV Vercel Speed Insights: LCP / CLS / INP 等の Web Vitals ※ 個人特定情報 (回答内容・診断結果) は計測対象外 SSR/SSG は主に SEO・OGP 対応の側面で活用する。 =LOVE 推し診断 / 11 Stack & Ops 11 / 13

12.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 12 / DESIGN RATIONALE 設計上の意図 と トレードオフ 各判断について「判断」「理由」「捨てたもの」を対にして残す。将来の拡張時に判断の再検討がしやすくなる。 DECISION 1 DECISION 2 DECISION 3 DBを持たない 一致率を 70–99% にクランプ モバイル前提 · max-w-md WHY WHY (1) 非公式サービスとして個人情報を扱う責任を負わない (2) 診断が純関数で表現でき、サーバー計算の必然性がない (3) Vercel の静的配信だけで運用が回り、コスト・障害面が減る (1) raw値は10人に加点が分散するため 0〜33%程度 に収まる (2) そのまま出すと「相性 12%」など体験として苦しい数字が出る (3) 上限 99% にすることで「絶対100%」を避け、余白を演出 WHY TRADE-OFF · 捨てたもの 診断履歴の集計、シェア画像のサーバー生成、A/Bの中央測定はできな TRADE-OFF · 捨てたもの 数値の厳密さ。「相性」表現とセットで運用し、誤読を減らす。 い。 DECISION 4 静的TSデータをコード同梱 TRADE-OFF · 捨てたもの PCでの余白活用。大画面では「そっけない」印象を許容する。 拡張しやすいポイント 現行構成の伸ばしどころ members.ts / questions.ts をソースコードに同梱。 これにより 型安全・PRレビュー可能・Git履歴が ◆ 別グループ版: members / questions を差し替えるだけで転用可能。UIロジックは不変。 残る を同時に得る。 CMSやJSONファイル分離を敢えて選ばない構成。 ◆ A/B: calculateDiagnosisResult が純関数なので、別実装を並走させて比較しやすい。 TRADE-OFF · =LOVE FUTURE · (1) 想定ユーザーがほぼモバイル (SNS流入) (2) 4択タップ・戻る・進捗バーは片手UIと相性が良い (3) SNSシェア時のスクショが縦長で映える 捨てたもの ◆ シェア画像: Next.js の ImageResponse による OG 動的生成なら、現在のクライアント計算境界を壊さず追 質問1問追加でも「コード修正 → ビルド → デプロイ」が必要。 加できる。 非エンジニアが単独で更新することはできない (現状想定内)。 ◆ i18n: 質問文・コメントに翻訳リソースを噛ませる余地はあるが、現状スコープ外。 推し診断 / 12 Design Rationale 12 / 13

13.

=LOVE 推し診断 / Technical Brief 非公式・ファンメイド 13 / FAQ & REFERENCES よくある疑問 と 参考情報 本資料を読み終えた後、実装者が疑問に思いやすい点をあらかじめ整理する。 FAQ · 実装上ありがちな疑問 Q&A Q1. 一致率が絶対に 100% にならないのは仕様ですか? はい、意図的です。clamp(70 + round(raw × 29), 70, 99) により表示値は必ず 70〜99 に収まります。「絶対100%」を避け、余白を残しつつ体験の質を保つ狙い。 Q2. 診断中にリロードしたら進捗は残りますか? 残りません。回答は useState 上のメモリに保持され、完走時のみ localStorage に書き込まれます。中途離脱・タブ閉じ・リロードでは進捗はゼロに戻ります (仕様)。 GLOSSARY · 用語 用語対応表 answers ユーザーの回答配列 (Answer[])。localStorage の唯一の保存対象。 matchRate 一致率 (70〜99)。raw を線形写像した表示値。 ratio Q3. 別のデバイス / 別のブラウザで結果を共有できますか? できません。localStorage はブラウザローカルで、サーバー同期は行いません。共有したい場合はスクリーンショットを利用してください。 Q4. 質問や選択肢を追加・修正するにはどうすればよいですか? questions.ts を編集して PR → Vercel にデプロイ、で反映されます。CMS は使っておらず、コード修正+デプロイが唯一の更新経路です。 ランキング内相対比 (0〜1)。1位のスコアを1.0とする。 intent / typeHint データ編集時のガイド用メタ。UI には表示しない。 REFERENCES · Q5. 回答データが外部に送信されている心配はありますか? 回答内容の送信は一切ありません。Vercel Analytics / Speed Insights はページ単位のアクセスと Web Vitals のみを計測しており、回答内容・診断結果は計測対象外です。 Q6. このプロジェクトは公式が関与していますか? していません。 個人開発のファンメイド作品で、公式運営・所属事務所とは一切関係ありません。公式サイト等へのリンクは掲載しますが、公式コンテンツの二次配信は 行いません。 参考 ・Next.js App Router: nextjs.org/docs/app ・Web Storage API (localStorage): developer.mozilla.org ・Vercel Analytics / Speed Insights: vercel.com/docs ・shadcn/ui: ui.shadcn.com DOCUMENT INFO version: v1 pages: 13 target audience: FE / Web =LOVE 推し診断 / 13 FAQ & References — END アプリ開発者 13 / 13