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

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

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

0. 2026-04-24 更新ハイライト — 時間窓 / 祝日 / 日付オーバーライド2026-04-24 update — window types, holidays, date overrides

駐車場の営業/入庫/出庫を別管理できるよう、parking_lot_hourswindow_type (business / entry / exit / after_hours_exit) を追加。 祝日マスター jp_holidays (2026・2027 年 34 件 seed) と、特定日だけルールを上書きできる parking_lot_date_overrides (花火大会 / 元旦特別営業など) を新設しました。

Added window_type to parking_lot_hours (business / entry / exit / after_hours_exit), introduced the jp_holidays master (34 rows for 2026 / 2027), and added parking_lot_date_overrides for per-day exceptions (fireworks nights, New-Year specials, …).

判定 RPC の優先順Resolution order in the RPCs

is_parking_lot_window_active(lot_id, at, window_type) が以下の優先度で解決します(is_parking_lot_open_nowwindow_type='business' ラッパに差し替え済):

is_parking_lot_window_active(lot_id, at, window_type) resolves in this order (is_parking_lot_open_now is now a thin business wrapper):

  1. 日付オーバーライド (parking_lot_date_overrides.hours->window_type)
  2. Date override (parking_lot_date_overrides.hours->window_type)
  3. 祝日行 (is_jp_holiday(date)=true かつ day_type='holiday')
  4. Holiday row (is_jp_holiday(date)=true and day_type='holiday')
  5. 曜日行 (day_of_week 一致)
  6. Day-of-week row (day_of_week match)
  7. day_type (weekday / saturday / sunday / holiday_eve)
  8. day_type (weekday / saturday / sunday / holiday_eve)
  9. all フォールバック
  10. all fallback

追加 RPC: can_enter_parking_lot_now / can_exit_parking_lot_now / is_jp_holiday(date) / get_parking_lot_date_override(lot_id, date)。 UI 影響: 駐車場編集画面で「営業 / 入庫 / 出庫 / 時間外出庫」タブ分離、日付オーバーライドタブ新設(別対応)。 詳細: 2026-04-24 ADR

Why it matters: 入庫と出庫を独立して管理できるため、「夜間は入庫できないが出庫は翌朝まで可」といった実運用のパターンを DB で表現できる。祝日判定も DB 内に閉じているので、クライアント側で祝日 SDK を抱え込む必要がなくなる。 Entry and exit windows are now decoupled, so real-world patterns like "no overnight entry but exit allowed until morning" fit the schema cleanly. Holiday detection lives entirely in the DB, eliminating the need for client-side holiday SDKs.

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.admin_tasks (
  id, task_kind, ref_id, assignee_id,
  urgency, due_at, memo, ...,
  UNIQUE (task_kind, ref_id),
  CHECK urgency IN ('urgent','high','medium','low')
)

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
task_kind = 'parking_fee_mismatch'     → parking_sessions.id  (fee_mismatch_flag=true)

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 BFF (POST /v1/storage/upload-url) が aws4fetch で SigV4 presign URL を発行し、クライアントが presigned URL に直接 PUT。配信は cdn.parky.co.jpAsset storage. The Cloudflare Workers BFF (POST /v1/storage/upload-url) mints a SigV4 presigned URL via aws4fetch; the client PUTs directly to it. Public delivery via cdn.parky.co.jp.
FCM v1プッシュ通知。Workers Queue (parky-fcm-dispatch) consumer が KV キャッシュした OAuth access_token で並列送信。Push notifications. The Workers Queue consumer (parky-fcm-dispatch) calls FCM in parallel using a KV-cached OAuth access_token.
PostHog管理者操作のプロダクト分析(identifyPostHog/resetPostHog)。Product analytics for admin actions (identifyPostHog/resetPostHog).
Sentry@sentry/react による未処理例外・ErrorBoundary 捕捉。BFF request_id を tag に伝播。@sentry/react captures unhandled exceptions and ErrorBoundary; BFF request_id is propagated as a tag.