注目機能の深堀り Highlighted features — deep dives

他のカテゴリで触れた機能のうち、設計・実装が独特でドキュメント化の価値が高いものをピックアップしました。

A selection of features whose design or implementation is interesting enough to warrant its own deep dive.

1. 料金ルール評価エンジンFee calculation engine

駐車場ごとに複雑な料金ルール (時間帯・曜日・キャップ・繰り返し) を設定でき、 入庫/出庫時刻からクライアント側で料金を計算します。管理者は詳細画面の計算機で即座に検証可能です。

Each lot can define complex pricing (time windows, day-of-week, caps, repeating caps). The portal evaluates it on the client from entry / exit timestamps, so admins can sanity-check via the live calculator.

ルール構造Rule shape

interface ParkingRule {
  rule_id: number;
  category: 'unit' | 'cap';
  when: {
    days: string[];               // ['mon','tue',..,'all','holiday']
    time: [string, string] | null // ['09:00','18:00'] or null for 24h
  };
  charge: {
    tiers?: { per_minutes: number; price: number }[]; // unit pricing
    cap_type?: 'duration';
    duration_hours?: number;
    price?: number;
    repeat?: boolean;
    scope?: string | null;
  };
}

計算の流れEvaluation flow

flowchart TD
  A["Entry / Exit time"] --> B["Iterate minute by minute"]
  B --> C{"Match any unit rule
for day and time?"} C -- yes --> D["Add tier price"] C -- no --> E["No charge this minute"] D --> F{"Cap rule triggered?
duration hours exceeded"} E --> F F -- yes --> G["Replace with cap price
restart window if repeat"] F -- no --> H["Continue"] G --> H H --> I{"More minutes?"} I -- yes --> B I -- no --> J["Return total"]
Why it matters: ルールを JSON のままテーブルに保存することで、DB スキーマを変えずに新しい料金ポリシーを追加できます。モバイルアプリ側も同じ JSON を解釈すれば表示金額が一致します。 Storing rules as JSON means new pricing policies don't require schema changes, and any client (mobile, portal, API) that parses the same JSON will render identical prices.

2. バッジ条件エンジンBadge condition engine

バッジの獲得条件は、アクティビティログ (user_activity_logs.metadata) の JSONB に対する 条件式配列として定義されます。条件は PostgreSQL 関数 evaluate_badge_conditions() で AND 結合で評価され、一致したイベント数がバッジの進捗となります。

Each badge holds an array of conditions that are evaluated against the JSONB metadata of user_activity_logs by the evaluate_badge_conditions() Postgres function. All conditions are AND-combined; each matching event increments progress.

[
  { "field": "metadata.parking_lot_id", "operator": "eq",  "value": "uuid" },
  { "field": "metadata.count",          "operator": "gte", "value": "10" }
]

サポート演算子Supported operators

eq · neq · contains · gt · gte · lt · lte · in

バックフィルBackfill

条件を変更した場合、既存ユーザーの進捗を再計算する必要があります。単体バッジ (backfill_badge_progress) と全バッジ (backfill_all_badge_progress) の 2 種類の RPC が用意されています。

When conditions change, existing progress must be recomputed. Two RPCs cover this: backfill_badge_progress (single) and backfill_all_badge_progress (all).

DELETE FROM user_badge_progress WHERE badge_id = ?;
INSERT INTO user_badge_progress (user_id, badge_id, count)
SELECT user_id, ?, COUNT(*)
FROM user_activity_logs
WHERE activity_type = ?
  AND evaluate_badge_conditions(metadata, conditions)
GROUP BY user_id;

3. カスタマイズ (着せ替え)Customization / theming

flowchart LR
  Theme[customization_themes] --> Item[customization_theme_items]
  Item --> Part[customization_theme_parts]
  Part --> PinA[(pin_asset)]
  Part --> IconA[(icon_asset)]
  Part --> LoadA[(loading_asset)]
  Part --> Color((color))
  Gift[theme_gifts] --> UT[user_themes]
  Theme --> Gift

テーマは複数のパーツを束ねる入れ物。管理者は gift_theme_to_user() RPC でユーザーにテーマを配布でき、 ユーザー側の所有履歴は user_themes に記録されます。

A theme is a container that pulls several parts together. Admins gift a theme via gift_theme_to_user(), with ownership recorded in user_themes.

4. 統合タスク (Admin Tasks)Unified admin task inbox

異質なタスク源 (サポートチケット / 誤情報報告 / オーナー申請 / 駐車場新規登録) を、 同一の admin_tasks テーブルで管理することで、1 画面の統合インボックスを実現しています。

Heterogeneous task sources (support / error reports / owner apps / new lot registrations) are all recorded in a single admin_tasks table so the portal can surface a unified inbox in one screen.

admin_tasks (
  id, task_kind, ref_id, assignee_id,
  urgency, due_at, memo, ...
)

task_kind = 'support'                 → support_tickets.id
task_kind = 'misinformation_report'   → error_reports.id
task_kind = 'owner_application'       → owner_applications.id
task_kind = 'parking_new_registration'→ parking_lots.id

5. Realtime 購読Realtime subscriptions

Supabase Realtime を使い、2 つのチャネルで UI をライブ更新しています。

Two Realtime channels drive live UI updates.

チャネルChannel テーブル / イベントTable / Event 用途Use
live-user-activity user_activity_logs / INSERT アクティビティフィードに即時反映Streams into the activity feed instantly
admin-notifications-sidebar admin_notifications / * サイドバーの通知バッジを自動更新Auto-refreshes the sidebar notification badge

6. 大規模リストの最適化Large-list performance

7. マスターコードシステムMaster code system

codes テーブルには user_status, vehicle_type 等のカテゴリ別ラベル定義が入っており、 CodeProvider が起動時に一括ロードしてアプリ全体で利用します。UI のドロップダウン・ラベルは DB 駆動です。

The codes table holds labels by category (user_status, vehicle_type, etc.). The CodeProvider loads them once at boot, so every dropdown and label in the UI is DB-driven.

code.label('user_status', 'active')       // → '利用中'
code.options('vehicle_type')              // → [{code, label}, ...]

8. 外部 API 統合External API integrations

API用途Use
Mapbox Geocoding住所 ⇄ 座標変換、候補サジェストAddress ⇄ coordinate, suggestions
Mapbox GL JS駐車場一覧のマップビジュアライザー (lazy ロード)Lazy-loaded map visualizer for the lot list
Cloudflare R2画像・アセット保存。Cloudflare Workers が署名 URL を発行し、クライアントが PUT。Asset storage via Edge-Function-issued signed URLs and client-side PUT.