目的: 「どのフォルダに何を書くか」を具体例つきで一望できるようにする。
- 方針の正(ルール):
docs/TRD.md§1「フロントエンド(Expo)— クリーンアーキテクチャ」 - 本書: 読み手向けのガイド・例。実装追加時は TRD と矛盾しないように更新する。
採用方針(確定): リポジトリ全体のディレクトリ構成・components / hooks の役割・**UI と処理の分け方(コンポーネント + useXxx)**は、本書に従う。クリーンの層(domain 〜 infrastructure)の定義は docs/TRD.md §1 と一致させる。
SubTrack/
├── app/ # expo-router:ルート定義。画面は薄く(レイアウト・ナビ・イベント委譲)
│ _layout.tsx
│ modal.tsx
│ (auth)/ # 未認証フロー(必要になったら追加)
│ (tabs)/
│ _layout.tsx
│ index.tsx
│ subscriptions/ # 例:一覧・詳細(画面が増えたら)
│ index.tsx
│ [id].tsx
│ settings/
│ index.tsx
├── src/ # クリーン寄り:ドメイン〜インフラ
│ domain/
│ application/
│ ports/
│ infrastructure/
│ supabase/
├── components/ # 汎用・横断 UI(原則ダム寄り)
│ ui/ # 小さな部品
│ # 機能に寄った塊は components/<feature>/(例: subscriptions/)
├── hooks/ # アプリ全体の presentation 用 hook(テーマ等。テンプレ由来)
├── constants/
├── docs/
├── supabase/
│ migrations/
├── scripts/
└── …
app/の(auth)/subscriptions/等は、まだ無くてよい。機能追加に合わせて増やす。src/presentation/は、共通 hook や画面専用部品がcomponents/に溢れたら後から切り出す選択でよい(初期は必須としない)。
| 場所 | 役割 |
|---|---|
components/ |
見た目の部品。汎用はダム(props のみ)。振る舞いが載る場合は 同隣の useXxx.ts(コンポーネント + フック)に分ける。DB 呼び出しは書かず、src/application を呼ぶ配線に留める。 |
hooks/ |
全画面共通の軽い hook(カラースキーム・テーマなど)。機能専用は components/<機能名>/useYyy.ts に寄せやすい。 |
| コンテナ/プレゼン分割 | 1 画面が太いときだけ XxxView + XxxContainer のように明示分割してよい。日々は コンポーネント + useXxx で足りることが多い。 |
| 層 | ここに書く | ここに書かない |
|---|---|---|
| domain | 金額換算、paused を合計から除外、日付・入力の検証、ドメイン型 |
useState、createClient、fetch |
| application | 「1 ボタン 1 操作」の流れ(取得 →domain で加工 → 保存) | Supabase の .from() 直書き |
| ports | interface SubscriptionRepository { ... } など抽象だけ |
実装 |
| infrastructure | @supabase/supabase-js の呼び出し、行データ ↔ domain の変換 |
画面用の色・ナビ |
| app/ | ルート、UI、ユーザ操作をユースケースに渡す | 長い if のビジネス分岐(domain / use case へ) |
import の例(tsconfig の @/*):
import type { Subscription } from '@/src/domain/subscription';
import { recordUsageToday } from '@/src/application/recordUsageToday';(実際のファイル名は機能に合わせて増やす。)
- ファイル数・レビューで「どこに書くか」を揃えるコストはあるが、致命的な理由で本構成をやめる必要はないという認識で採用する。段階的導入(薄い画面は簡略、
srcは太い機能から)で負荷を抑える。
流れ: 画面タップ → ユースケース → リポジトリが usage_logs に insert。
| 置き場所 | ファイル例(命名は一例) | 中身のイメージ |
|---|---|---|
| ports | src/ports/usageLogRepository.ts |
saveForToday(userId, subscriptionId, localDate): Promise<void> |
| infrastructure | src/infrastructure/supabase/usageLogRepositorySupabase.ts |
insert / ユニーク制約エラーをアプリ用エラーに変換 |
| domain | src/domain/usageDate.ts |
「今日」の日付を端末タイムゾーンで決める純粋関数(または引数で受け取る) |
| application | src/application/recordUsageToday.ts |
日付取得 → ports の save を呼ぶだけ |
| app | app/(tabs)/index.tsx や専用コンポーネント |
ボタン onPress で recordUsageToday(...) を await、トーストはここ |
書かない例: app/**/*.tsx の中で supabase.from('usage_logs').insert(...) をベタ書き(原則 infra に閉じる)。
流れ: 一覧取得 → domain で「active のみ」「年額 ÷12」等を反映した合計 → 画面に渡す。
| 置き場所 | ファイル例 | 中身のイメージ |
|---|---|---|
| domain | src/domain/subscriptionTotals.ts |
computeMonthlyTotal(rows): number(paused 除外、周期ごとの換算) |
| ports | src/ports/subscriptionRepository.ts |
ダッシュボード用の一覧取得メソッド |
| infrastructure | src/infrastructure/supabase/subscriptionRepositorySupabase.ts |
join 込みの select、マッパーで domain 型へ |
| application | src/application/getMonthlyDashboard.ts |
取得 → computeMonthlyTotal → 結果 DTO |
| app | app/(tabs)/index.tsx |
フック useMonthlyDashboard がユースケースを呼び、表示だけ |
別案: 集計を Postgres のビュー / SQL 関数に寄せる場合、リポジトリは「ビューを読む」だけになり、金額ルールの説明は DB 側にもコメント or 設計書に残す。
| 置き場所 | 役割 |
|---|---|
supabase/functions/fx_rates/ |
外部 API を叩き、キーを隠す(仕様は docs/API_DESIGN.md) |
| ports | FxRatePort(getRates(baseCurrency) など) |
| infrastructure | fxRateClientEdge.ts が fetch(functions/v1/fx_rates) |
ドメインは 「換算後の金額を計算する」純粋関数だけ持ち、HTTP は infra だけ。
| 書く場所 | 内容 |
|---|---|
docs/DATABASE_DESIGN.md |
テーブル・列の意味 |
supabase/migrations/*.sql |
実行可能 DDL / RLS / policy |
docs/MIGRATIONS.md |
追加・検証・PR の手順 |
アプリの infrastructure は マイグレーション済みスキーマを前提に書く。
- 設定の 1 トグル即保存など極薄い処理は、一時的に
app/近くに書いてもよい(docs/TRD.md§1 に従う)。 - 分岐・金額・状態が増えたら、該当ロジックを
domain/applicationに切り出す。
| 日付 | 内容 |
|---|---|
| 2026-05-15 | リポジトリ全体の推奨ツリー、components / hooks、トレードオフ、**採用方針(確定)**を追記 |
| 2026-05-15 | 初版 |