プロジェクトへ戻る

Google Play公開 · 次の仮説を検討中

Umaso

日本で韓国料理を作る人向けに、まずチーム管理のレシピを届けるv1を公開しました。

当初は日本で韓国料理を作る際の材料・代替品までつなぐ構想でしたが、機能が先に増え、誰のどの問題を解くかが曖昧になりました。配布を止め続ける代わりに、チーム管理のレシピを中心としたv1へ縮小してGoogle Playへ公開しました。現在はこの版を運用しながら、次に検証する利用者と価値をチームで再定義しています。

期間
2026.02 - 現在
担当範囲
Flutter機能 · Go API · 管理画面 · 運用
チーム
デザイナー2・Flutter 4・サーバー4で開始
リポジトリ
非公開リポジトリ

レシピ探索から材料購入までをつなぐサービス構想

日本に住むチームメンバーの経験から、レシピを見つけても現地の材料や代替品をもう一度探すという仮説で始まりました。しかし開発中は機能が先に増え、対象利用者と最初に変える行動が曖昧になりました。

そこで新規機能を止め、チーム管理のレシピコンテンツを中心としたv1へ縮小してGoogle Playへ公開しました。現在はこの版を運用しながら、当初の材料・代替品構想を含め、次に検証する価値を再定義しています。

利用から運用まで

レシピを見る人と、内容を管理するチームをつなぐ流れ

  1. 01

    レシピアプリ

    Flutterで閲覧・検索

  2. 02

    アプリの裏側

    Go APIで認証とルールを管理

  3. 03

    レシピデータ

    PostgreSQLに翻訳・材料を保存

  4. 04

    管理画面

    Next.js管理画面+R2画像

機能拡張を止め、コンテンツ中心のv1を出すまで

材料・代替品、買い物、コミュニティ、推薦の案が並列に増え、誰のどの行動を最初に変えるプロダクトかを一文で説明できなくなりました。

サイドプロジェクトでは担当者の時間が変動し、完全な再設計を待てば配布とフィードバックがさらに遅れます。一方、利用データがない状態で当初の仮説を正解として押し切る根拠もありませんでした。

新規機能を止め、チームが管理できるレシピコンテンツを中心としたv1へ縮小して先にGoogle Playへ公開しました。当初の材料・代替品構想は成果として包装せず、次に検証する仮説へ戻しました。

  • Flutter・Go API・管理画面でコンテンツ登録から表示までを閉じました。
  • アプリ/サーバーをモノレポへ移し、機能単位の変更を追いやすくしました。
  • 次フェーズでは機能責任者が一つの行動を最後まで担当する案を整理しました。

コンテンツ中心v1をGoogle Playへ公開し、チームがレシピ・材料・翻訳を継続運用できる管理フローを残しました。利用・継続指標はまだありません。

配布は仮説検証の終点ではなく、何を測るかを決められる最初のチェックポイントだと学びました。

翻訳を閲覧経路から運用経路へ移した

日本語のレシピを必要とする利用者がいる一方で、閲覧のたびに翻訳を生成すると、待ち時間・費用・品質のばらつきを利用者ごとに抱えることになります。

第1段階のコンテンツはチームが管理画面から登録・確認できましたが、ユーザー生成レシピやコメントを自動翻訳する費用と確認ルールはまだ決まっていませんでした。

利用時ではなく、管理画面で韓国語の原文を登録するときに翻訳案を作り、確認後に保存する方式を選びました。保存された翻訳がない場合だけ、別言語の内容へフォールバックします。

  • 管理画面で原文入力と翻訳案の確認を同じコンテンツ運用フローへ統合しました。
  • is_translatedで翻訳状態を示し、運用担当者が未翻訳のコンテンツを確認できるようにしました。
  • アプリAPIでは保存済みの翻訳を返し、翻訳がない場合だけ別言語へフォールバックしました。

管理画面でレシピ・材料・翻訳を一緒に運用でき、アプリは毎回翻訳を生成せず保存済みの内容を読み取れます。

翻訳は表示機能ではなく、誰がいつ確認し、どの状態を公開するかを含むコンテンツのライフサイクルとして設計する必要があると学びました。

公開版で料理候補を選ぶルーレット

材料一つとソースの組み合わせは、同じ代替品ではない

材料Aを材料Bに置き換える場合と、ソースを複数の材料の組み合わせで置き換える場合では、利用者が必要とする情報が異なります。

単純な1対1の関係だけで作ると、複数の材料や別のソースを含む代替案を表せません。一方で柔軟な多形性関係にすると、DBの外部キーだけではすべての参照整合性を保証できません。

材料の代替は1対1の関係、ソースの代替は複数の構成要素と分量を持つ構造に分けました。component_typeとcomponent_idを使う代わりに、参照の正しさはクエリとアプリケーション側で確認する方針にしました。

  • 材料の代替とソースの代替を別々の取得構造へ分離しました。
  • component_typeに応じてingredient/sourceを条件付きJOINし、COALESCEで実際の名称を取得しました。
  • ソース作成・構成要素登録・翻訳・監査ログ記録を一つのトランザクションで処理しました。

一つの材料を別の材料へ置き換えるケースと、複数の構成要素からなるソースをAPI・管理画面で扱えるようにしました。

柔軟なデータモデルは表現力を得る代わりに、DB制約で守れない整合性をアプリケーションの検証責任として引き受ける設計だと理解しました。

食材から作れる料理を探す公開版の画面

複雑なレシピ取得をSQLに見える形で残す

レシピ詳細は、材料・翻訳・代替材料・ソースなど複数の関係を一つの画面へ組み立てる必要がありました。

ロケールごとの翻訳がなければ別言語へフォールバックし、ユースケースごとに必要な列とJOINが変わるため、テーブル中心の抽象化だけでは実際の取得意図をレビューしにくい状態でした。

ORMではなくpgxとsqlcを使い、ユースケースごとのSQLを明示しながら、生成型でアプリケーションコードの型安全性を保ちました。ORMが常に劣るという意味ではなく、このサービスでは取得形状をSQLで直接確認できる点を重視しました。

  • レシピ詳細・翻訳・代替材料の取得をユースケース別SQLに分離しました。
  • sqlcの生成コードでクエリ結果の型を固定しました。
  • 言語フォールバックと条件付きの関係取得をSQLに明示しました。

Go APIでレシピ、材料、翻訳、代替材料の取得を実装し、必要なデータの関係をクエリ単位で確認できる状態にしました。ORMとの速度比較やN+1解消の数値は測定していません。

技術選択はライブラリ名の列挙ではなく、必要なデータの形をどこでレビューし、どの不変条件を守るかまで含めて説明するものだと学びました。

保有食材を管理する公開版の冷蔵庫

繰り返すログアウトは認証関数の問題か

Flutterアプリで利用者のログインが繰り返し解除され、認証が安定しているかを確認する必要がありました。

ユーザー用と管理者用で秘密情報と有効期限が異なる一方、依存性注入の組み立てでは似た設定が同じ境界に並んでいました。関数を単体で読むだけでは原因を見つけられませんでした。

利用者用ログイン関数で止まらず、DIコンテナからどのユーザー用リフレッシュ用の秘密情報と有効期限が注入されるかを追跡し、ユーザーと管理者の認証経路をそれぞれの設定へ戻しました。

  • ユーザーリフレッシュトークン用ミドルウェアへ注入される秘密情報と有効期限を追跡しました。
  • ユーザー経路に入っていた管理者JWT設定をユーザー用リフレッシュ設定へ修正しました。
  • 認証ロジック・設定・期限・失敗時の画面を一つの流れで確認しました。

ユーザー用のリフレッシュ設定と管理者用の設定を分離し、繰り返しログアウトする原因となったDIの接続を修正しました。公開できる定量的な再発率やセッション維持率はありません。

認証では関数の正しさだけでなく、秘密情報・有効期限・ミドルウェアへ設定が届く組み立て境界までテスト対象にする必要があると学びました。

コンテンツ中心のv1をGoogle Playへ公開しました。私はFlutter機能に加え、レシピ・材料・翻訳を管理するGo APIと、チームが使う管理画面・内部運用を担当しました。

この経験以降、実装前に『誰が何に困っているか』『どのデータを誰が管理するか』『どの画面とAPIまで完成すればリリースできるか』を確認しています。例えばレシピ項目を追加するときは、Flutterの表示だけでなく、Go API、DBマイグレーション、管理画面の入力まで一緒に確認します。

公開できる利用・継続指標はまだありません。Google Play公開は配布能力の証拠であり、当初の材料・代替品仮説が検証されたことを意味しません。

  • 公開中v1の閲覧・再訪・レシピ到達を測定する
  • 次の対象利用者と一つの中心行動を決める
  • 代替材料モデルを次フェーズに残すか、実測から判断する