{"openapi":"3.1.0","info":{"title":"Parky API (Mobile App)","version":"0.1.0","description":"## 日本語\nParky BFF — Cloudflare Workers 上で動作する `/v1/*` API。モバイル・管理ポータル・公開 Web のすべてのクライアントがこの API のみを叩く。\n\n各 operation には想定呼出し元（portal-admin / portal-owner / app-web / app-mobile）を `x-channels` に保持し、Redoc ではバッジとしてタイトル横に表示される。ドキュメント用メタデータであり、サーバー側の振る舞いは呼出し元に依存しない。\n\n## English\nParky BFF — the `/v1/*` API running on Cloudflare Workers. Every client (mobile, admin portal, public web) hits only this API.\n\nEach operation declares its intended callers (portal-admin / portal-owner / app-web / app-mobile) via `x-channels`, rendered by Redoc as colored badges next to the operation title. This is documentation-only metadata; server behavior does not branch on the caller.\n\n> Route summaries / descriptions are primarily in Japanese. An English overlay is applied when the docs page language is switched to EN — see `docs/i18n/openapi-en-overlay.json` for current coverage."},"servers":[{"url":"https://dev-api.parky.co.jp","description":"Dev"},{"url":"https://api.parky.co.jp","description":"Production"},{"url":"http://localhost:8787","description":"Local"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Supabase Auth が発行した JWT（HS256 / SUPABASE_JWT_SECRET 署名）"}},"schemas":{"AppleAppSiteAssociation":{"type":"object","properties":{"applinks":{"type":"object","properties":{"details":{"type":"array","items":{"type":"object","properties":{"appIDs":{"type":"array","items":{"type":"string"}},"components":{"type":"array","items":{"type":"object","properties":{"/":{"type":"string"},"comment":{"type":"string"}},"required":["/"]}}},"required":["appIDs","components"]}}},"required":["details"]},"webcredentials":{"type":"object","properties":{"apps":{"type":"array","items":{"type":"string"}}},"required":["apps"]}},"required":["applinks","webcredentials"]},"AssetLinksEntry":{"type":"object","properties":{"relation":{"type":"array","items":{"type":"string"}},"target":{"type":"object","properties":{"namespace":{"type":"string"},"package_name":{"type":"string"},"sha256_cert_fingerprints":{"type":"array","items":{"type":"string"}}},"required":["namespace","package_name","sha256_cert_fingerprints"]}},"required":["relation","target"]},"AssetLinksResponse":{"type":"array","items":{"$ref":"#/components/schemas/AssetLinksEntry"}},"CodeRow":{"type":"object","properties":{"category_id":{"type":"string"},"code":{"type":"string"},"display_label":{"type":"string"},"lang":{"type":"string"},"sort_order":{"type":"integer"},"metadata":{"type":["object","null"],"additionalProperties":{}}},"required":["category_id","code","display_label","lang","sort_order","metadata"]},"CodesResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CodeRow"}},"lang":{"type":"string"}},"required":["items","lang"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","examples":["not_found"]},"message":{"type":"string","examples":["Not Found"]},"request_id":{"type":"string","examples":["7d4e5…-…"]}},"required":["code","message","request_id"]}},"required":["error"]},"Me":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid"},"email":{"type":["string","null"]},"app_user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","display_name","status","created_at"]},"admin":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"status":{"type":"string"}},"required":["id","name","status"]}},"required":["user_id","email","app_user","admin"]},"MeUpdate":{"type":"object","properties":{"display_name":{"type":"string","minLength":1,"maxLength":100}}},"SavedParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","parking_lot_id","created_at"]},"ParkingLotRating":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"value":{"type":"string","enum":["good","bad"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","parking_lot_id","value","created_at","updated_at"]},"UserVehicle":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"vehicle_type":{"type":"string"},"nickname":{"type":["string","null"]},"plate_number":{"type":["string","null"]},"color":{"type":["string","null"]},"is_default":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","user_id","vehicle_type","nickname","plate_number","color","is_default","created_at"]},"SearchQueryV1":{"type":"object","properties":{"v":{"type":"number","enum":[1],"description":"スキーマバージョン","examples":[1]},"center":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"placeName":{"type":"string","maxLength":200}},"required":["lat","lng"]},"radius_m":{"type":"integer","exclusiveMinimum":0,"maximum":50000},"price_min":{"type":["integer","null"],"minimum":0,"maximum":100000},"price_max":{"type":["integer","null"],"minimum":0,"maximum":100000},"attributes":{"type":"array","items":{"type":"string","enum":["covered","open_24h","entry_24h","ev_charging","oversized_ok","motorcycle_ok","wheelchair_accessible","barrier_free","reservable","security_camera","has_max_fee","monthly_available","coin_500_or_less","near_station","low_price","partner_facility","24h","max_fee"]},"maxItems":20},"difficulty":{"type":"array","items":{"type":"string","enum":["easy","normal","hard"]},"maxItems":3},"operator_codes":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":50},"vehicle":{"type":"object","properties":{"height_m":{"type":["number","null"],"exclusiveMinimum":0,"maximum":10},"width_m":{"type":["number","null"],"exclusiveMinimum":0,"maximum":5},"length_m":{"type":["number","null"],"exclusiveMinimum":0,"maximum":20},"weight_t":{"type":["number","null"],"exclusiveMinimum":0,"maximum":50},"clearance_cm":{"type":["integer","null"],"minimum":0,"maximum":100},"tire_width_mm":{"type":["integer","null"],"exclusiveMinimum":0,"maximum":500}}},"keywords":{"type":"array","items":{"type":"string","minLength":1,"maxLength":50},"maxItems":10},"meter_ticket_included":{"type":"boolean"},"sort":{"type":"string","enum":["distance","price","recommended"]}},"additionalProperties":false},"SearchPreset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"query_json":{"$ref":"#/components/schemas/SearchQueryV1"},"is_default":{"type":"boolean"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","name","query_json","is_default","sort_order","created_at","updated_at"]},"ParkingSession":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"vehicle_type":{"type":["string","null"]},"status":{"type":"string"},"start_at":{"type":["string","null"]},"end_at":{"type":["string","null"]},"fee_amount":{"type":["integer","null"]},"memo":{"type":["string","null"]},"personal_rating":{"type":["string","null"]},"start_lat":{"type":["number","null"]},"start_lng":{"type":["number","null"]}},"required":["id","user_id","parking_lot_id","vehicle_type","status","start_at","end_at","fee_amount","memo","personal_rating","start_lat","start_lng"]},"ParkingSessionDetail":{"allOf":[{"$ref":"#/components/schemas/ParkingSession"},{"type":"object","properties":{"photos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["during","after"]},"slot":{"type":"integer"},"r2_key":{"type":"string"},"content_type":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"created_at":{"type":"string"}},"required":["id","phase","slot","r2_key","content_type","size_bytes","created_at"]}}},"required":["photos"]}]},"SessionPhoto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["during","after"]},"slot":{"type":"integer","minimum":1,"maximum":4},"r2_key":{"type":"string"},"content_type":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"created_at":{"type":"string"}},"required":["id","phase","slot","r2_key","content_type","size_bytes","created_at"]},"MyReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","parking_lot_id","user_id","rating","comment","status","created_at"]},"PublicReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_name":{"type":"string"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","parking_lot_id","user_id","user_name","rating","comment","status","created_at"]},"UserNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"type":{"type":"string"},"target":{"type":"string"},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"sent_at":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","title","body","type","target","status","scheduled_at","sent_at","created_at"]},"NotifType":{"type":"object","properties":{"code":{"type":"string"},"display_label":{"type":"string"}},"required":["code","display_label"]},"NotifPrefItem":{"type":"object","properties":{"notif_type":{"type":"string"},"push_enabled":{"type":"boolean"},"in_app_enabled":{"type":"boolean"},"email_enabled":{"type":"boolean"}},"required":["notif_type","push_enabled","in_app_enabled","email_enabled"]},"UserPushToken":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"fcm_token":{"type":"string"},"device_type":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","fcm_token","device_type","created_at","updated_at"]},"MyExp":{"type":"object","properties":{"total_exp":{"type":"integer"},"level":{"type":"integer"},"next_level_exp":{"type":["integer","null"]},"exp_to_next_level":{"type":["integer","null"]}},"required":["total_exp","level","next_level_exp","exp_to_next_level"]},"MyBadge":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"badge_id":{"type":"string","format":"uuid"},"earned_at":{"type":"string"},"badge":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"category":{"type":"string"}},"required":["id","name","description","icon","category"]}},"required":["id","badge_id","earned_at","badge"]},"MyBadgeProgress":{"type":"object","properties":{"badge_id":{"type":"string","format":"uuid"},"count":{"type":"integer"},"threshold":{"type":"integer"},"percent":{"type":"number"},"badge":{"type":["object","null"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"category":{"type":"string"}},"required":["name","description","icon","category"]}},"required":["badge_id","count","threshold","percent","badge"]},"ThemeListItem":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"is_free":{"type":"boolean"},"price_yen_minor":{"type":["integer","null"]},"preview_asset_id":{"type":["string","null"],"format":"uuid"},"sort_order":{"type":"integer"}},"required":["id","name","description","is_free","price_yen_minor","preview_asset_id","sort_order"]},"MyTheme":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"theme_id":{"type":"string","format":"uuid"},"acquired_at":{"type":"string"},"acquisition_type":{"type":"string"},"theme":{"$ref":"#/components/schemas/ThemeListItem"}},"required":["id","theme_id","acquired_at","acquisition_type","theme"]},"SubscriptionPlan":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"price_minor":{"type":"integer"},"currency":{"type":"string"},"billing_period":{"type":"string"},"accent_color":{"type":["string","null"]},"features":{},"sort_order":{"type":"integer"}},"required":["id","code","name","description","price_minor","currency","billing_period","accent_color","sort_order"]},"MySubscription":{"type":"object","properties":{"id":{"type":["string","null"],"format":"uuid"},"plan_id":{"type":["string","null"],"format":"uuid"},"plan":{"$ref":"#/components/schemas/SubscriptionPlan"},"status":{"type":["string","null"]},"started_at":{"type":["string","null"]},"ended_at":{"type":["string","null"]}},"required":["id","plan_id","plan","status","started_at","ended_at"]},"VerifyIapResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"plan_id":{"type":"string","format":"uuid"},"status":{"type":"string","description":"active / expired / revoked / grace / on_hold / paused / pending / unknown"},"started_at":{"type":"string"},"ended_at":{"type":["string","null"]},"auto_renew_status":{"type":["boolean","null"],"description":"ios: autoRenewStatus (1=true)。android: SUBSCRIPTION_STATE_ACTIVE / CANCELED で導出。不明または取得不可の場合は null。"},"expires_at":{"type":["string","null"],"description":"サブスク期限の ISO 8601 文字列。ended_at と同値。"},"environment":{"type":["string","null"],"enum":["Sandbox","Production"],"description":"ios: Apple のレスポンスから取得。android: 常に Production（Play は環境を分離しない）。"},"platform":{"type":"string","enum":["ios","android"]},"external_subscription_id":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","plan_id","status","started_at","ended_at","auto_renew_status","expires_at","environment","platform","external_subscription_id","created_at","updated_at"]},"VerifyIapRequest":{"type":"object","properties":{"platform":{"type":"string","enum":["ios","android"],"description":"購入プラットフォーム。ios = App Store Server API (ES256 JWT → /inApps/v1/transactions/{id})、android = Google Play Developer API (subscriptionsV2 + OAuth2 SA JWT)"},"receipt":{"type":"string","minLength":1,"description":"ios: StoreKit2 が返す JWS (signedTransactionInfo)、またはレガシー base64 レシート。android: BillingClient が返す purchaseToken。"},"product_id":{"type":"string","minLength":1,"description":"subscription_plans.code に対応する productId / subscriptionId。"},"transaction_id":{"type":"string","description":"ios のみ。StoreKit2 がデコード済みの transactionId を渡してくれる場合に指定。未指定時は receipt の JWS ペイロードから transactionId クレームを抽出。"}},"required":["platform","receipt","product_id"]},"RefreshIapRequest":{"type":"object","properties":{"platform":{"type":"string","enum":["ios","android"],"description":"verify-iap に渡したものと同じプラットフォーム。"},"receipt":{"type":"string","minLength":1,"description":"ios: 最新の signedTransactionInfo JWS（StoreKit2 が自動更新後に返す）。android: 変わらず purchaseToken（同一トークンで最新状態を取得できる）。"},"product_id":{"type":"string","minLength":1,"description":"subscription_plans.code に対応する productId / subscriptionId。"},"transaction_id":{"type":"string","description":"ios のみ。最新の transactionId があれば指定（省略時は receipt から抽出）。"}},"required":["platform","receipt","product_id"]},"MeErrorReport":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"parking_lot_name":{"type":"string"},"report_type":{"type":"string"},"severity":{"type":["string","null"]},"status":{"type":"string"},"description":{"type":"string"},"evidence_urls":{"type":["array","null"],"items":{"type":"string"}},"created_at":{"type":"string"}},"required":["id","parking_lot_id","parking_lot_name","report_type","severity","status","description","evidence_urls","created_at"]},"ParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"total_spaces":{"type":["integer","null"]},"status":{"type":["string","null"]},"structure":{"type":["string","null"]},"operating_hours":{"type":["string","null"]},"created_at":{"type":"string"},"max_height_m":{"type":["number","null"]},"max_width_m":{"type":["number","null"]},"max_length_m":{"type":["number","null"]},"max_weight_t":{"type":["number","null"]},"min_clearance_cm":{"type":["integer","null"]},"max_tire_width_mm":{"type":["integer","null"]},"max_parking_duration_min":{"type":["integer","null"]},"entry_method":{"type":["string","null"]},"source":{"type":["string","null"]},"shape_type":{"type":["string","null"]},"area":{},"operator_code":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]}},"required":["id","name","address","lat","lng","total_spaces","status","structure","operating_hours","created_at"]},"NearbyParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"distance_m":{"type":"number"}},"required":["id","name","address","lat","lng","distance_m"]},"NearbyParkingLotWithRanking":{"allOf":[{"$ref":"#/components/schemas/NearbyParkingLot"},{"type":"object","properties":{"ranking_score":{"type":"number","description":"ランキングスコア 0-100（料金40%+距離40%+星評価20%）"},"ranking_rank":{"type":"integer","description":"score 降順の順位（1-based）"},"is_top3":{"type":"boolean","description":"上位3位バッジ用フラグ"}}}]},"ParkingLotWithDetail":{"allOf":[{"$ref":"#/components/schemas/ParkingLot"},{"type":"object","properties":{"pricing_rules":{"type":"array","items":{}},"images":{"type":"array","items":{}},"tags":{"type":"array","items":{}},"operator":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"color":{"type":"string"}},"required":["id","name","slug","color"]},"is_open_now":{"type":["boolean","null"],"description":"現在営業中かどうか（null = 営業時間未設定）"}}}]},"PricingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"category":{"type":"string"},"rule_order":{"type":"integer"},"time_start":{"type":["string","null"]},"time_end":{"type":["string","null"]},"day_type":{"type":["string","null"]},"per_minutes":{"type":["integer","null"]},"price_minor":{"type":["integer","null"]},"cap_type":{"type":["string","null"]},"cap_duration_hours":{"type":["number","null"]},"cap_price_minor":{"type":["integer","null"]},"cap_repeat":{"type":["boolean","null"]}},"required":["id","parking_lot_id","category","rule_order","time_start","time_end","day_type","per_minutes","price_minor","cap_type","cap_duration_hours","cap_price_minor","cap_repeat"]},"NearbyStationItem":{"type":"object","properties":{"distance_m":{"type":["number","null"]},"walk_min":{"type":["number","null"]},"station":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"code":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]}},"required":["id","name","code","lat","lng"]}},"required":["distance_m","station"]},"ParkingLotImage":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid"},"is_main":{"type":"boolean"},"sort_order":{"type":["integer","null"]}},"required":["id","parking_lot_id","asset_id","is_main","sort_order"]},"FeeCalcResponse":{"type":"object","properties":{"total_amount_minor":{"type":"integer"},"breakdown":{"type":"array","items":{}},"note":{"type":"string"}},"required":["total_amount_minor","breakdown"]},"FeeCalcRequest":{"type":"object","properties":{"entry_at":{"type":"string","format":"date-time"},"exit_at":{"type":"string","format":"date-time"},"vehicle_type":{"type":"string","default":"sedan"}},"required":["entry_at","exit_at"]},"Review":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"},"owner_reply":{"type":["string","null"],"description":"オーナーからの返信テキスト（未返信時は null）"},"owner_replied_at":{"type":["string","null"],"description":"オーナー返信日時（ISO8601）"}},"required":["id","parking_lot_id","user_id","rating","comment","status","created_at","owner_reply","owner_replied_at"]},"ParkingDetailBff":{"type":"object","properties":{"lot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"status":{"type":["string","null"]},"total_spaces":{"type":["integer","null"]},"operating_hours":{"type":["string","null"]},"updated_at":{"type":"string"}},"required":["id","name","address","lat","lng","status","total_spaces","operating_hours","updated_at"]},"reviews":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"user_name":{"type":["string","null"]},"rating":{"type":"integer"},"comment":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","user_name","rating","comment","created_at"]}},"pricing_rules":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"category":{"type":"string"},"price_minor":{"type":"integer"},"rule_order":{"type":"integer"}},"required":["id","category","price_minor","rule_order"]}},"nearby":{"type":"object","properties":{"lots":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"distance_m":{"type":"number"}},"required":["id","name","distance_m"]}},"sponsors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"distance_m":{"type":"number"}},"required":["id","name","distance_m"]}}},"required":["lots","sponsors"]},"is_open_now":{"type":["boolean","null"]}},"required":["lot","reviews","pricing_rules","nearby","is_open_now"]},"Tag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":["string","null"]},"sort_order":{"type":["integer","null"]},"slug":{"type":["string","null"]},"category":{"type":["string","null"]},"icon_name":{"type":["string","null"]}},"required":["id","name","color","sort_order"]},"ArticleListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":["string","null"]},"title":{"type":"string"},"excerpt":{"type":["string","null"]},"category":{"type":"string"},"content_format":{"type":["string","null"]},"author_name":{"type":["string","null"]},"author_slug":{"type":["string","null"]},"published_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"hero_image_url":{"type":["string","null"]},"hero_image_alt_text":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"related_hubs":{"type":"array","items":{}},"story_number":{"type":["integer","null"]},"view_count":{"type":["integer","null"]}},"required":["id","slug","title","excerpt","category","content_format","author_name","author_slug","published_at","updated_at","thumbnail_url","hero_image_url","tags","related_hubs","story_number","view_count"]},"Article":{"allOf":[{"$ref":"#/components/schemas/ArticleListItem"},{"type":"object","properties":{"updated_at":{"type":"string"},"view_count":{"type":"integer"},"body":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["body","created_at"]}]},"Ad":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"ad_type":{"type":"string"},"placement":{"type":"string"},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"alt_text":{"type":["string","null"]},"start_date":{"type":["string","null"]},"end_date":{"type":["string","null"]}},"required":["id","name","ad_type","placement","banner_url","link_url","alt_text","start_date","end_date"]},"SupportTicket":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"category":{"type":"string"},"priority":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","user_id","user_email","user_name","subject","body","category","priority","status","created_at"]},"ErrorReport":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"parking_lot_name":{"type":"string"},"report_type":{"type":"string"},"description":{"type":"string"},"severity":{"type":["string","null"]},"evidence_urls":{"type":["array","null"],"items":{"type":"string"}},"status":{"type":"string"},"photo_asset_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","user_email","user_name","parking_lot_id","parking_lot_name","report_type","description","severity","evidence_urls","status","photo_asset_id","created_at"]},"ReviewFlagResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"review_id":{"type":"string","format":"uuid"},"reason":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","review_id","reason","status","created_at"]},"AreaSponsor":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category":{"type":"string"},"description":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"radius_m":{"type":"integer"}},"required":["id","name","category","description","logo_url","banner_url","link_url","phone","address","lat","lng","radius_m"]},"UploadUrlResponse":{"type":"object","properties":{"asset_id":{"type":"string","format":"uuid"},"upload_url":{"type":"string","format":"uri"},"s3_key":{"type":"string"},"public_url":{"type":"string","format":"uri"},"expires_in":{"type":"integer"}},"required":["asset_id","upload_url","s3_key","public_url","expires_in"]},"Asset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"file_name":{"type":"string"},"file_size":{"type":"integer"},"mime_type":{"type":"string"},"s3_key":{"type":"string"},"category":{"type":"string"},"entity_type":{"type":["string","null"]},"entity_id":{"type":["string","null"],"format":"uuid"},"is_public":{"type":"boolean"},"uploaded_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","file_name","file_size","mime_type","s3_key","category","entity_type","entity_id","is_public","uploaded_by","created_at"]},"SearchLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"total_spaces":{"type":["integer","null"]},"status":{"type":["string","null"]},"structure":{"type":["string","null"]},"operating_hours":{"type":["string","null"]},"max_height_m":{"type":["number","null"]},"max_width_m":{"type":["number","null"]},"max_length_m":{"type":["number","null"]},"max_weight_t":{"type":["number","null"]},"min_clearance_cm":{"type":["integer","null"]},"max_tire_width_mm":{"type":["integer","null"]},"max_parking_duration_min":{"type":["integer","null"]},"entry_method":{"type":["string","null"]},"shape_type":{"type":["string","null"]},"area":{},"operator_code":{"type":["string","null"]},"operator_name":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]},"pricing_rules":{"type":"array","items":{}},"tags":{"type":"array","items":{}}},"required":["id","name","address","lat","lng","total_spaces","status","structure","operating_hours","max_height_m","max_width_m","max_length_m","max_weight_t","min_clearance_cm","max_tire_width_mm","max_parking_duration_min","entry_method","shape_type","operator_code","operator_name","entry_difficulty","pricing_rules","tags"]},"AiSearchQuery":{"type":"object","properties":{"keywords":{"type":"array","items":{"type":"string"}},"max_price_per_hour":{"type":"integer"},"roof":{"type":"boolean"},"open_24h":{"type":"boolean"},"vehicle_type":{"type":"string","enum":["sedan","kei","minivan","suv","truck"]}}},"AiSearchResponse":{"type":"object","properties":{"status":{"type":"string","enum":["parsed","need_info","error"]},"query":{"$ref":"#/components/schemas/AiSearchQuery"},"reply":{"type":"string"}},"required":["status","reply"]},"AiSearchRequest":{"type":"object","properties":{"message":{"type":"string","minLength":1,"maxLength":500},"session_id":{"type":"string"}},"required":["message"]},"HubStation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"name_en":{"type":["string","null"]},"lines":{"type":"array","items":{"type":"string"}},"code":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"city":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"name_en":{"type":["string","null"]},"slug":{"type":["string","null"]}},"required":["id","name","slug"]},"prefecture":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"name_en":{"type":["string","null"]},"slug":{"type":["string","null"]}},"required":["id","name","slug"]}},"required":["id","name","code","lat","lng","city","prefecture"]},"HubPublishableItem":{"type":"object","properties":{"stats":{"type":"object","properties":{"station_id":{"type":"string","format":"uuid"},"total_count":{"type":"integer"},"in_stock_count":{"type":"integer"},"out_of_stock_count":{"type":"integer"}},"required":["station_id","total_count","in_stock_count","out_of_stock_count"]},"station":{"$ref":"#/components/schemas/HubStation"}},"required":["stats","station"]},"HubParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"total_spaces":{"type":["integer","null"]},"status":{"type":["string","null"]},"structure":{"type":["string","null"]},"operating_hours":{"type":["string","null"]},"max_height_m":{"type":["number","null"]},"max_width_m":{"type":["number","null"]},"max_length_m":{"type":["number","null"]},"max_weight_t":{"type":["number","null"]},"min_clearance_cm":{"type":["integer","null"]},"operator_code":{"type":["string","null"]},"operator_name":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]},"pricing_rules":{"type":"array","items":{}},"tags":{"type":"array","items":{}}},"required":["id","name","address","lat","lng","total_spaces","status","structure","operating_hours","max_height_m","max_width_m","max_length_m","max_weight_t","min_clearance_cm","operator_code","operator_name","entry_difficulty","pricing_rules","tags"]},"HubParkingLotItem":{"type":"object","properties":{"distance_m":{"type":["number","null"]},"walk_min":{"type":["number","null"]},"parking_lot":{"$ref":"#/components/schemas/HubParkingLot"}},"required":["distance_m","parking_lot"]},"ActivityTypeMeta":{"type":"object","properties":{"type":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","enum":["session","engagement","asset","discovery","account"]},"emitted_by":{"type":"string"},"emitted":{"type":"boolean"},"metadata_schema":{}},"required":["type","description","category","emitted_by","emitted"]},"ActivityTypesResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ActivityTypeMeta"}}},"required":["items"]},"DeepLinkItem":{"type":"object","properties":{"pattern":{"type":"string","examples":["/parking-lots/:id"]},"screen":{"type":"string","examples":["ParkingLotDetail"]},"params":{"type":"array","items":{"type":"string"},"examples":[["id"]]},"requires_auth":{"type":"boolean","description":"未設定時は true 扱い（認証必須）。false のものは未ログインでも遷移できる。"}},"required":["pattern","screen","params"]},"DeepLinksResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/DeepLinkItem"}}},"required":["items"]},"AppConfig":{"type":"object","properties":{"min_app_version_ios":{"type":"string","examples":["1.0.0"]},"min_app_version_android":{"type":"string","examples":["1.0.0"]},"is_maintenance":{"type":"boolean","examples":[false]},"maintenance_message":{"type":["string","null"]},"store_url_ios":{"type":["string","null"]},"store_url_android":{"type":["string","null"]},"updated_at":{"type":"string"}},"required":["min_app_version_ios","min_app_version_android","is_maintenance","maintenance_message","store_url_ios","store_url_android","updated_at"]},"DataExport":{"type":"object","properties":{"export_date":{"type":"string"},"user_id":{"type":"string","format":"uuid"},"profile":{},"parking_sessions":{"type":"array","items":{}},"reviews":{"type":"array","items":{}},"ratings":{"type":"array","items":{}},"saved_parking_lots":{"type":"array","items":{}},"vehicles":{"type":"array","items":{}},"search_presets":{"type":"array","items":{}},"registered_device_count":{"type":"number"}},"required":["export_date","user_id","parking_sessions","reviews","ratings","saved_parking_lots","vehicles","search_presets","registered_device_count"]},"MyReferralCode":{"type":"object","properties":{"code":{"type":"string"},"usage_count":{"type":"integer"},"max_usages":{"type":["integer","null"]}},"required":["code","usage_count","max_usages"]},"ApplyReferralResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"referrer_user_id":{"type":"string","format":"uuid"},"error":{"type":"string"}},"required":["ok"]},"ReferralHistoryItem":{"type":"object","properties":{"applied_at":{"type":"string"},"code":{"type":"string"},"referee_label":{"type":"string"}},"required":["applied_at","code","referee_label"]},"ReferralHistory":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ReferralHistoryItem"}}},"required":["items"]},"ConsentType":{"type":"object","properties":{"code":{"type":"string","examples":["terms_of_service"]},"current_version":{"type":"string","examples":["2026-04-01"]},"required":{"type":"boolean","examples":[true]},"display_label":{"type":"string","examples":["利用規約"]},"document_url":{"type":["string","null"],"examples":["https://parky.co.jp/terms"]},"updated_at":{"type":"string","examples":["2026-04-01T00:00:00Z"]}},"required":["code","current_version","required","display_label","document_url","updated_at"]},"ConsentItem":{"type":"object","properties":{"consent_type":{"type":"string","examples":["terms_of_service"]},"version":{"type":"string","examples":["2026-04-01"]},"granted":{"type":"boolean","examples":[true]},"granted_at":{"type":"string","examples":["2026-04-21T03:00:00Z"]},"source":{"type":["string","null"],"examples":["mobile_ios"]}},"required":["consent_type","version","granted","granted_at","source"]},"ConsentPostBody":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"consent_type":{"type":"string","minLength":1,"examples":["terms_of_service"]},"version":{"type":"string","minLength":1,"examples":["2026-04-01"]},"granted":{"type":"boolean"},"source":{"type":"string","examples":["mobile_ios"]}},"required":["consent_type","version","granted"]},"minItems":1,"maxItems":20}},"required":["items"]},"ClientEventCreated":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"created_at":{"type":"string"}},"required":["id","created_at"]},"SharePublic":{"type":"object","properties":{"parking_session_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"parking_lot_name":{"type":["string","null"]},"start_lat":{"type":["number","null"]},"start_lng":{"type":["number","null"]},"start_at":{"type":["string","null"]},"status":{"type":"string"},"access_count":{"type":"integer"},"expires_at":{"type":"string"}},"required":["parking_session_id","parking_lot_id","parking_lot_name","start_lat","start_lng","start_at","status","access_count","expires_at"]},"Admin":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"role_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"last_login_at":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","name","email","role_id","status","last_login_at","created_at"]},"AdminNotificationPrefs":{"type":"object","properties":{"new_owner":{"type":"boolean","default":true},"new_parking":{"type":"boolean","default":true},"sales_daily":{"type":"boolean","default":true},"sales_monthly":{"type":"boolean","default":true},"system_alert":{"type":"boolean","default":true}}},"Role":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"color":{"type":"string"},"is_system":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","name","description","color","is_system","created_at"]},"AdminParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"status":{"type":["string","null"]},"total_spaces":{"type":["integer","null"]},"operating_hours":{"type":["string","null"]},"operator_code":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]}},"required":["id","name","address","lat","lng","status","total_spaces","operating_hours"]},"AdminAppUser":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"auth_user_id":{"type":["string","null"],"format":"uuid"},"display_name":{"type":"string"},"email":{"type":"string"},"vehicle_type":{"type":["string","null"]},"premium":{"type":"boolean"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","auth_user_id","display_name","email","vehicle_type","premium","status","created_at"]},"AdminTag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":"string"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"slug":{"type":["string","null"]},"category":{"type":["string","null"]},"icon_name":{"type":["string","null"]},"usage_count":{"type":"integer"}},"required":["id","name","color","sort_order","created_at","slug","category","icon_name"]},"AdminOperator":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"color":{"type":"string"},"sort_order":{"type":"integer"},"created_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]},"deleted_at":{"type":["string","null"]},"usage_count":{"type":"integer"}},"required":["id","name","slug","color","sort_order","created_at","updated_at","deleted_at"]},"AdminNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"message":{"type":"string"},"category":{"type":"string"},"avatar":{"type":"string"},"read":{"type":"boolean"},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","title","message","category","avatar","read","created_at","deleted_at"]},"AdminUserNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"type":{"type":"string"},"target":{"type":"string"},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"sent_at":{"type":["string","null"]},"total_recipients":{"type":["integer","null"]},"success_count":{"type":["integer","null"]},"failure_count":{"type":["integer","null"]},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","title","body","type","target","status","scheduled_at","sent_at","total_recipients","success_count","failure_count","created_at","deleted_at"]},"DispatchEnqueued":{"type":"object","properties":{"notification_id":{"type":"string","format":"uuid"},"enqueued":{"type":"integer"},"batches":{"type":"integer"}},"required":["notification_id","enqueued","batches"]},"AdminReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"rating":{"type":"integer"},"comment":{"type":["string","null"]},"status":{"type":"string"},"admin_note":{"type":["string","null"]},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","parking_lot_id","user_id","rating","comment","status","admin_note","created_at","deleted_at"]},"AdminSupportTicket":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"category":{"type":"string"},"priority":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","user_id","user_email","user_name","subject","body","category","priority","status","created_at"]},"AdminErrorReport":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"parking_lot_name":{"type":"string"},"report_type":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"severity":{"type":["string","null"]},"evidence_urls":{"type":["array","null"],"items":{"type":"string"}},"resolved_by":{"type":["string","null"],"format":"uuid"},"admin_note":{"type":["string","null"]},"created_at":{"type":"string"},"resolved_at":{"type":["string","null"]},"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]}},"required":["id","display_name"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]}},"required":["id","user_id","user_email","user_name","parking_lot_id","parking_lot_name","report_type","description","status","severity","evidence_urls","resolved_by","admin_note","created_at","resolved_at"]},"AdminTask":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"task_kind":{"type":"string"},"ref_id":{"type":"string","format":"uuid"},"assignee_id":{"type":["string","null"],"format":"uuid"},"urgency":{"type":"string"},"due_at":{"type":["string","null"]},"memo":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","task_kind","ref_id","assignee_id","urgency","due_at","memo","created_at","updated_at"]},"AdminArticle":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":["string","null"]},"title":{"type":"string"},"body":{"type":["string","null"]},"excerpt":{"type":["string","null"]},"category":{"type":"string"},"author_name":{"type":["string","null"]},"status":{"type":"string"},"published_at":{"type":["string","null"]},"view_count":{"type":"integer"},"tags":{"type":"array","items":{"type":"string"}},"thumbnail_url":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"publish_to_web":{"type":"boolean"},"publish_to_app":{"type":"boolean"},"hero_image_url":{"type":["string","null"]},"hero_image_alt_text":{"type":["string","null"]},"author_slug":{"type":["string","null"]},"content_format":{"type":"string"},"related_hubs":{}},"required":["id","slug","title","body","excerpt","category","author_name","status","published_at","view_count","tags","thumbnail_url","created_at","updated_at"]},"AdminArticleTag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"sort_order":{"type":"integer"},"created_at":{"type":"string"}},"required":["id","name","sort_order","created_at"]},"AdminAd":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"ad_type":{"type":"string"},"placement":{"type":"string"},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"alt_text":{"type":["string","null"]},"status":{"type":"string"},"start_date":{"type":["string","null"]},"end_date":{"type":["string","null"]},"impressions":{"type":"integer"},"clicks":{"type":"integer"},"memo":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","ad_type","placement","banner_url","link_url","alt_text","status","start_date","end_date","impressions","clicks","memo","created_at","updated_at"]},"AdminBoost":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"credit_per_impression":{"type":"integer"},"daily_budget":{"type":["integer","null"]},"total_budget":{"type":["integer","null"]},"status":{"type":"string"},"started_at":{"type":"string"},"ended_at":{"type":["string","null"]},"owner":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":["string","null"]}},"required":["id","name","email"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]}},"required":["id","name","address"]}},"required":["id","owner_id","parking_lot_id","name","credit_per_impression","daily_budget","total_budget","status","started_at","ended_at"]},"AdminSponsor":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category":{"type":"string"},"description":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]}},"required":["id","name","category","description","logo_url","banner_url","link_url"]},"AdminOwner":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_type":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":["string","null"]},"status":{"type":"string"},"company_name":{"type":["string","null"]},"representative_name":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"parking_count":{"type":"integer"}},"required":["id","owner_type","name","email","phone","status","company_name","representative_name","created_at","updated_at"]},"AdminOwnerCredit":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"balance":{"type":"integer"},"total_purchased":{"type":"integer"},"total_consumed":{"type":"integer"},"updated_at":{"type":["string","null"]},"owner":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"owner_type":{"type":["string","null"]}},"required":["id","name","email","owner_type"]}},"required":["id","owner_id","balance","total_purchased","total_consumed","owner"]},"AdminOwnerApplication":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":["string","null"],"format":"uuid"},"owner_name":{"type":"string"},"owner_email":{"type":"string"},"company_name":{"type":["string","null"]},"parking_lot_id":{"type":"string","format":"uuid"},"proof_asset_id":{"type":["string","null"],"format":"uuid"},"additional_notes":{"type":["string","null"]},"status":{"type":"string"},"reviewed_by":{"type":["string","null"],"format":"uuid"},"reviewed_at":{"type":["string","null"]},"reject_reason":{"type":["string","null"]},"created_at":{"type":"string"},"owner":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"owner_type":{"type":["string","null"]}},"required":["id","name","email","owner_type"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]}},"required":["id","name","address"]},"proof_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"reviewer":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]}},"required":["id","name"]}},"required":["id","owner_id","owner_name","owner_email","company_name","parking_lot_id","status","reviewed_by","reviewed_at","reject_reason","created_at"]},"AdminActivityLog":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"admin_id":{"type":["string","null"],"format":"uuid"},"admin_email_snapshot":{"type":"string"},"action":{"type":"string"},"target_type":{"type":["string","null"]},"target_id":{"type":["string","null"]},"target_label":{"type":["string","null"]},"metadata":{},"ip_address":{"type":["string","null"]},"user_agent":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","admin_id","admin_email_snapshot","action","target_type","target_id","target_label","ip_address","user_agent","created_at"]},"RevenueMonthlyRow":{"type":"object","properties":{"month":{"type":"string"},"channel":{"type":"string"},"amount_minor":{"type":"integer"},"count":{"type":"integer"}},"required":["month","channel","amount_minor","count"]},"RevenueTransaction":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"occurred_at":{"type":"string"},"channel":{"type":"string"},"transaction_type":{"type":"string"},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"external_id":{"type":["string","null"]},"description":{"type":["string","null"]},"user_id":{"type":["string","null"],"format":"uuid"},"plan_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","occurred_at","channel","transaction_type","amount_minor","currency","external_id","description","user_id","plan_id","created_at"]},"RevenueTransactionWithRelations":{"allOf":[{"$ref":"#/components/schemas/RevenueTransaction"},{"type":"object","properties":{"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]}},"required":["id","display_name","email"]},"plan":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":["string","null"]},"name":{"type":["string","null"]}},"required":["id","code","name"]}},"required":["user","plan"]}]},"AdminUserSubscription":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"plan_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"started_at":{"type":"string"},"ended_at":{"type":["string","null"]},"external_subscription_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","plan_id","status","started_at","ended_at","external_subscription_id","created_at"]},"AdminSubscriptionPlan":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"price_minor":{"type":"integer"},"currency":{"type":"string"},"billing_period":{"type":"string"},"accent_color":{"type":["string","null"]},"features":{},"sort_order":{"type":"integer"},"is_active":{"type":"boolean"}},"required":["id","code","name","description","price_minor","currency","billing_period","accent_color","sort_order","is_active"]},"StoreIntegration":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"display_name":{"type":"string"},"package_or_app_id":{"type":["string","null"]},"vendor_number":{"type":["string","null"]},"connection_status":{"type":"string"},"last_status_message":{"type":["string","null"]},"last_synced_at":{"type":["string","null"]},"last_sync_error":{"type":["string","null"]}},"required":["id","store","display_name","package_or_app_id","vendor_number","connection_status","last_status_message","last_synced_at","last_sync_error"]},"StoreSalesRow":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"report_date":{"type":"string"},"product_id":{"type":"string"},"country_code":{"type":"string"},"units":{"type":"integer"},"proceeds_minor":{"type":"integer"},"gross_minor":{"type":"integer"},"currency":{"type":"string"},"fetched_at":{"type":"string"}},"required":["id","store","report_date","product_id","country_code","units","proceeds_minor","gross_minor","currency","fetched_at"]},"StoreMetricsRow":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"metric_date":{"type":"string"},"installs":{"type":["integer","null"]},"uninstalls":{"type":["integer","null"]},"active_devices":{"type":["integer","null"]},"impressions":{"type":["integer","null"]},"product_page_views":{"type":["integer","null"]},"crash_rate":{"type":["number","null"]},"anr_rate":{"type":["number","null"]},"rating_average":{"type":["number","null"]},"rating_count":{"type":["integer","null"]}},"required":["id","store","metric_date","installs","uninstalls","active_devices","impressions","product_page_views","crash_rate","anr_rate","rating_average","rating_count"]},"StoreReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"external_review_id":{"type":"string"},"author_name":{"type":["string","null"]},"rating":{"type":["integer","null"]},"title":{"type":["string","null"]},"body":{"type":["string","null"]},"reply_body":{"type":["string","null"]},"reply_at":{"type":["string","null"]},"posted_at":{"type":"string"}},"required":["id","store","external_review_id","author_name","rating","title","body","reply_body","reply_at","posted_at"]},"StoreSyncRun":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"started_at":{"type":"string"},"finished_at":{"type":["string","null"]},"rows_upserted":{"type":"integer"},"message":{"type":["string","null"]},"triggered_by":{"type":"string"}},"required":["id","store","task","status","started_at","finished_at","rows_upserted","message","triggered_by"]},"BadgeDefinition":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"activity_type":{"type":"string"},"threshold":{"type":"integer"},"sort_order":{"type":"integer"},"is_active":{"type":"boolean"},"asset_id":{"type":["string","null"],"format":"uuid"},"conditions":{},"category":{"type":"string"},"created_at":{"type":"string"},"holders_count":{"type":"integer"},"asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"tags":{"type":"array","items":{"type":"string"}}},"required":["id","name","description","icon","activity_type","threshold","sort_order","is_active","asset_id","category","created_at"]},"ActivityExpRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"activity_type":{"type":"string"},"exp_amount":{"type":"integer"},"description":{"type":"string"},"is_active":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","activity_type","exp_amount","description","is_active","created_at","updated_at"]},"LevelDefinition":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"level":{"type":"integer"},"required_exp":{"type":"integer"},"created_at":{"type":"string"}},"required":["id","level","required_exp","created_at"]},"AdminTheme":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"is_free":{"type":"boolean"},"price_yen_minor":{"type":["integer","null"]},"preview_asset_id":{"type":["string","null"],"format":"uuid"},"is_active":{"type":"boolean"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"preview_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"owners_count":{"type":"integer"},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"theme_id":{"type":"string","format":"uuid"},"category":{"type":"string"},"part_id":{"type":"string","format":"uuid"}},"required":["id","theme_id","category","part_id"]}}},"required":["id","name","description","is_free","price_yen_minor","preview_asset_id","is_active","sort_order","created_at","updated_at"]},"AdminThemePart":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":"string"},"primary_color":{"type":["string","null"]},"accent_color":{"type":["string","null"]},"pin_asset_id":{"type":["string","null"],"format":"uuid"},"icon_asset_id":{"type":["string","null"],"format":"uuid"},"loading_asset_id":{"type":["string","null"],"format":"uuid"},"loading_type":{"type":["string","null"]},"is_active":{"type":"boolean"},"sort_order":{"type":"integer"},"used_in_themes_count":{"type":"integer"},"tags":{"type":"array","items":{"type":"string"}},"pin_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"icon_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"loading_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]}},"required":["id","name","description","category","primary_color","accent_color","pin_asset_id","icon_asset_id","loading_asset_id","loading_type","is_active","sort_order"]},"AiProvider":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider_key":{"type":"string"},"display_name":{"type":"string"},"model_name":{"type":"string"},"vault_secret_id":{"type":["string","null"],"format":"uuid"},"is_enabled":{"type":"boolean"},"priority":{"type":"integer"},"config":{},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","provider_key","display_name","model_name","vault_secret_id","is_enabled","priority","created_at","updated_at"]},"AiUsageLog":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider_key":{"type":"string"},"user_input":{"type":"string"},"parsed_query":{},"response_status":{"type":"string"},"attempt_number":{"type":"integer"},"fallback_reason":{"type":["string","null"]},"error_code":{"type":["string","null"]},"error_message":{"type":["string","null"]},"session_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","provider_key","user_input","response_status","attempt_number","fallback_reason","error_code","error_message","session_id","created_at"]},"AdminCode":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"category_id":{"type":"string"},"code":{"type":"string"},"display_label":{"type":"string"},"lang":{"type":"string"},"sort_order":{"type":["integer","null"]},"is_deleted":{"type":"boolean"}},"required":["id","category_id","code","display_label","lang","sort_order","is_deleted"]},"AdminParkingSession":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"vehicle_id":{"type":["string","null"],"format":"uuid"},"started_at":{"type":["string","null"]},"ended_at":{"type":["string","null"]},"status":{"type":"string"},"total_amount_minor":{"type":["number","null"]},"memo":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","parking_lot_id","vehicle_id","started_at","ended_at","status","total_amount_minor","created_at"]},"AdminParkingSessionWithRelations":{"allOf":[{"$ref":"#/components/schemas/AdminParkingSession"},{"type":"object","properties":{"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]}},"required":["id","display_name","email"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]}},"required":["id","name","address"]}},"required":["user","parking_lot"]}]},"IgSlideCategory":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"prefix":{"type":["string","null"]},"sort_order":{"type":"integer"},"is_deleted":{"type":"integer"}},"required":["code","label","prefix","sort_order","is_deleted"]},"IgPostCategory":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"color":{"type":["string","null"]},"sort_order":{"type":"integer"},"is_deleted":{"type":"integer"}},"required":["code","label","color","sort_order","is_deleted"]},"IgTag":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":["string","null"]},"usage_count":{"type":"integer"}},"required":["id","name","color","usage_count"]},"IgTemplate":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"slide_type":{"type":"string"},"html_body":{"type":"string"},"slot_schema":{"type":"string"},"sample_content":{"type":"string","default":"{}"},"sample_html":{"type":"string","default":""},"uses_parking_lot":{"type":"integer","default":0},"sort_order":{"type":"integer"},"is_active":{"type":"integer"}},"required":["id","code","name","slide_type","html_body","slot_schema","sort_order","is_active"]},"IgPostTemplate":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"slide_refs":{"type":"string"}},"required":["id","code","name","description","slide_refs"]},"IgSlide":{"type":"object","properties":{"id":{"type":"string"},"campaign_id":{"type":"string"},"template_id":{"type":"string"},"slide_index":{"type":"integer"},"content":{"type":"string"},"html_override":{"type":["string","null"]},"png_r2_key":{"type":["string","null"]},"png_url":{"type":["string","null"]},"revision_notes":{"type":["string","null"]},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","campaign_id","template_id","slide_index","content","html_override","png_r2_key","png_url","revision_notes","created_at","updated_at"]},"IgCampaign":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"title":{"type":"string"},"theme":{"type":["string","null"]},"area":{"type":["string","null"]},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"ig_media_id":{"type":["string","null"]},"notes":{"type":["string","null"]},"source_material":{"type":["string","null"]},"post_category_code":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","code","title","theme","area","status","scheduled_at","ig_media_id","notes","source_material","post_category_code","created_by","created_at","updated_at"]},"IgCaption":{"type":["object","null"],"properties":{"id":{"type":"string"},"campaign_id":{"type":"string"},"body":{"type":["string","null"]},"hashtags":{"type":["string","null"]},"draft_body":{"type":["string","null"]},"generated_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","campaign_id","body","hashtags","draft_body","generated_at","created_at","updated_at"]},"IgCompetitorSnapshot":{"type":"object","properties":{"id":{"type":"string"},"campaign_id":{"type":["string","null"]},"source_url":{"type":["string","null"]},"account_handle":{"type":["string","null"]},"raw_notes":{"type":["string","null"]},"ai_ideas":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","campaign_id","source_url","account_handle","raw_notes","ai_ideas","created_by","created_at"]},"AdminAsset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"category":{"type":"string"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"file_size":{"type":"integer"},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"created_at":{"type":"string"},"updated_at":{"type":["string","null"]}},"required":["id","category","s3_key","file_name","mime_type","file_size","width","height","created_at"]},"AdminUserVehicle":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"nickname":{"type":["string","null"]},"vehicle_type_code":{"type":["string","null"]},"maker_code":{"type":["string","null"]},"size_code":{"type":["string","null"]},"color_code":{"type":["string","null"]},"model_name":{"type":["string","null"]},"year":{"type":["integer","null"]},"asset_id":{"type":["string","null"],"format":"uuid"},"is_primary":{"type":"boolean"},"notes":{"type":["string","null"]},"deleted_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]}},"required":["id","display_name"]},"asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]}},"required":["id","user_id","nickname","vehicle_type_code","maker_code","size_code","color_code","model_name","year","asset_id","is_primary","notes","deleted_at","created_at","updated_at"]},"AdminUserSearchPreset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"query_json":{"$ref":"#/components/schemas/SearchQueryV1"},"is_default":{"type":"boolean"},"sort_order":{"type":"integer"},"deleted_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]}},"required":["id","display_name"]}},"required":["id","user_id","name","query_json","is_default","sort_order","deleted_at","created_at","updated_at"]},"PlacesImportLotResult":{"type":"object","properties":{"lot_id":{"type":"string","format":"uuid"},"fetched":{"type":"integer"},"upserted":{"type":"integer"},"skipped":{"type":"integer"},"auto_on_map":{"type":"integer"},"auto_on_facility":{"type":"integer"}},"required":["lot_id","fetched","upserted","skipped","auto_on_map","auto_on_facility"]},"PlacesImportAllResult":{"type":"object","properties":{"total_lots":{"type":"integer"},"processed":{"type":"integer"},"total_upserted":{"type":"integer"},"errors":{"type":"array","items":{"type":"object","properties":{"lot_id":{"type":"string","format":"uuid"},"message":{"type":"string"}},"required":["lot_id","message"]}},"dry_run":{"type":"boolean"}},"required":["total_lots","processed","total_upserted","errors","dry_run"]},"AdminAreaPlace":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category":{"type":"string"},"description":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"banner_url":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"radius_m":{"type":"integer"},"status":{"type":"string"},"is_sponsored":{"type":"boolean"},"source":{"type":"string"},"source_place_id":{"type":["string","null"]},"rating":{"type":["number","null"]},"user_ratings_total":{"type":["integer","null"]},"place_types":{"type":["array","null"],"items":{"type":"string"}},"show_on_map":{"type":"boolean"},"show_as_facility":{"type":"boolean"},"last_refreshed_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","name","category","lat","lng","radius_m","status","is_sponsored","source","show_on_map","show_as_facility","created_at","updated_at"]},"AdminPlaceDiscount":{"type":"object","properties":{"place_id":{"type":"string","format":"uuid"},"lot_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":["string","null"]},"terms":{"type":["string","null"]},"valid_from":{"type":["string","null"]},"valid_until":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]},"place_name":{"type":"string"},"lot_name":{"type":"string"}},"required":["place_id","lot_id","title","created_at","updated_at"]},"AdminAppConfig":{"type":"object","properties":{"min_app_version_ios":{"type":"string","examples":["1.0.0"]},"min_app_version_android":{"type":"string","examples":["1.0.0"]},"is_maintenance":{"type":"boolean","examples":[false]},"maintenance_message":{"type":["string","null"]},"store_url_ios":{"type":["string","null"]},"store_url_android":{"type":["string","null"]},"updated_at":{"type":"string"},"updated_by_admin_id":{"type":["string","null"],"format":"uuid"}},"required":["min_app_version_ios","min_app_version_android","is_maintenance","maintenance_message","store_url_ios","store_url_android","updated_at","updated_by_admin_id"]},"UpdateAppConfig":{"type":"object","properties":{"min_app_version_ios":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$","examples":["1.2.0"]},"min_app_version_android":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$","examples":["1.2.0"]},"is_maintenance":{"type":"boolean"},"maintenance_message":{"type":["string","null"],"maxLength":500},"store_url_ios":{"type":["string","null"],"format":"uri"},"store_url_android":{"type":["string","null"],"format":"uri"}}},"AdminClientEventListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"event_type":{"type":"string"},"severity":{"type":"string"},"message":{"type":["string","null"]},"app_version":{"type":["string","null"]},"platform":{"type":["string","null"]},"os_version":{"type":["string","null"]},"device_model":{"type":["string","null"]},"parking_session_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","event_type","severity","message","app_version","platform","os_version","device_model","parking_session_id","created_at"]},"AdminClientEventDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"event_type":{"type":"string"},"severity":{"type":"string"},"message":{"type":["string","null"]},"stack_trace":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":{}},"app_version":{"type":["string","null"]},"platform":{"type":["string","null"]},"os_version":{"type":["string","null"]},"device_model":{"type":["string","null"]},"parking_session_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","event_type","severity","message","stack_trace","metadata","app_version","platform","os_version","device_model","parking_session_id","created_at"]},"SnsMetricsSeriesItem":{"type":"object","properties":{"week":{"type":"string","examples":["2026-W15"]},"followers":{"type":"integer"},"engagementRate":{"type":"number"}},"required":["week","followers","engagementRate"]},"SnsMetricsPlatform":{"type":"object","properties":{"platform":{"type":"string","enum":["instagram","x"]},"followers":{"type":"integer"},"engagementRate":{"type":"number"},"weeklyDiffFollowers":{"type":"integer"},"weeklyDiffEngagementPt":{"type":"number"},"monthlyDiffFollowers":{"type":"integer"},"series":{"type":"array","items":{"$ref":"#/components/schemas/SnsMetricsSeriesItem"}}},"required":["platform","followers","engagementRate","weeklyDiffFollowers","weeklyDiffEngagementPt","monthlyDiffFollowers","series"]},"SnsMetricsResponse":{"type":"object","properties":{"platforms":{"type":"array","items":{"$ref":"#/components/schemas/SnsMetricsPlatform"}}},"required":["platforms"]},"PostCalendarDay":{"type":"object","properties":{"date":{"type":"string","examples":["2026-04-01"]},"instagram":{"type":"object","properties":{"draft":{"type":"integer"},"scheduled":{"type":"integer"},"published":{"type":"integer"}},"required":["draft","scheduled","published"]},"x":{"type":"object","properties":{"draft":{"type":"integer"},"scheduled":{"type":"integer"},"published":{"type":"integer"}},"required":["draft","scheduled","published"]}},"required":["date","instagram","x"]},"PostCalendarResponse":{"type":"object","properties":{"days":{"type":"array","items":{"$ref":"#/components/schemas/PostCalendarDay"}}},"required":["days"]},"ArticlesPvItem":{"type":"object","properties":{"id":{"anyOf":[{"type":"integer"},{"type":"string"}]},"slug":{"type":["string","null"]},"title":{"type":"string"},"pv":{"type":"integer"},"pvDiffPct":{"type":"number"}},"required":["id","slug","title","pv","pvDiffPct"]},"ArticlesPvResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ArticlesPvItem"}},"sources":{"type":"object","properties":{"direct":{"type":"integer"},"google":{"type":"integer"},"twitter":{"type":"integer"},"facebook":{"type":"integer"},"other":{"type":"integer"}},"required":["direct","google","twitter","facebook","other"]}},"required":["items","sources"]},"AdsCtrDaily":{"type":"object","properties":{"date":{"type":"string"},"clicks":{"type":"integer"}},"required":["date","clicks"]},"AdsCtrItem":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"impressions":{"type":"integer"},"clicks":{"type":"integer"},"ctr":{"type":"number"},"daysRemaining":{"type":"integer"},"daily":{"type":"array","items":{"$ref":"#/components/schemas/AdsCtrDaily"}}},"required":["id","name","startDate","endDate","impressions","clicks","ctr","daysRemaining","daily"]},"AdsCtrResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdsCtrItem"}}},"required":["items"]},"MarketingSubscriber":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string"},"source":{"type":["string","null"]},"lang":{"type":["string","null"]},"subscribed_at":{"type":"string"},"unsubscribed_at":{"type":["string","null"]},"meta":{},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","email","source","lang","subscribed_at","unsubscribed_at","created_at","updated_at"]},"MarketingBroadcast":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"subject":{"type":["string","null"]},"body_markdown":{"type":["string","null"]},"segment":{"type":["string","null"]},"status":{"type":"string"},"engine":{"type":["string","null"]},"scheduled_at":{"type":["string","null"]},"sent_at":{"type":["string","null"]},"delivered":{"type":"integer"},"opened":{"type":"integer"},"clicked":{"type":"integer"},"open_rate":{"type":"number"},"click_rate":{"type":"number"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","title","subject","body_markdown","segment","status","engine","scheduled_at","sent_at","delivered","opened","clicked","open_rate","click_rate","created_at","updated_at"]},"MarketingXAccount":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"handle":{"type":"string"},"display_name":{"type":["string","null"]},"tier":{"type":["string","null"]},"is_primary":{"type":"boolean"},"followers_count":{"type":"integer"},"has_token":{"type":"boolean"},"updated_at":{"type":["string","null"]},"following":{"type":"integer"},"postsTotal":{"type":"integer"},"verified":{"type":"boolean"},"bio":{"type":["string","null"]},"displayName":{"type":["string","null"]},"followers":{"type":"integer"},"updatedAt":{"type":["string","null"]}},"required":["id","handle","display_name","tier","is_primary","followers_count","has_token","updated_at","following","postsTotal","verified","bio","displayName","followers","updatedAt"]},"MarketingXPost":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":["string","null"],"format":"uuid"},"body":{"type":"string"},"thread_of":{"type":["string","null"],"format":"uuid"},"threadCount":{"type":"integer"},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"published_at":{"type":["string","null"]},"x_tweet_id":{"type":["string","null"]},"impressions":{"type":"integer"},"likes":{"type":"integer"},"reposts":{"type":"integer"},"replies":{"type":"integer"},"error_message":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"accountId":{"type":["string","null"],"format":"uuid"},"threadOf":{"type":["string","null"],"format":"uuid"},"scheduledAt":{"type":["string","null"]},"publishedAt":{"type":["string","null"]},"xTweetId":{"type":["string","null"]},"errorMessage":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","account_id","body","thread_of","threadCount","status","scheduled_at","published_at","x_tweet_id","impressions","likes","reposts","replies","error_message","created_at","updated_at","accountId","threadOf","scheduledAt","publishedAt","xTweetId","errorMessage","createdAt","updatedAt"]},"MarketingXScheduleRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"dow":{"type":"array","items":{"type":"integer"}},"hour":{"type":"integer"},"minute":{"type":"integer"},"source_kind":{"type":"string"},"source_ref":{"type":["string","null"]},"category":{"type":["string","null"]},"enabled":{"type":"boolean"},"last_fired_at":{"type":["string","null"]},"created_at":{"type":"string"},"weekdays":{"type":"array","items":{"type":"integer"}},"sourceType":{"type":"string"},"sourceLabel":{"type":["string","null"]},"categoryFilter":{"type":"string"},"lastFiredAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","name","dow","hour","minute","source_kind","source_ref","category","enabled","last_fired_at","created_at","weekdays","sourceType","sourceLabel","categoryFilter","lastFiredAt","createdAt"]},"MarketingXListenRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"listen_kind":{"type":"string"},"target":{"type":"string"},"min_likes_threshold":{"type":["integer","null"]},"must_contain":{"type":["string","null"]},"must_not_contain":{"type":["string","null"]},"action_kinds":{"type":"array","items":{"type":"string"}},"reply_template":{"type":["string","null"]},"cooldown_minutes":{"type":"integer"},"enabled":{"type":"boolean"},"last_fired_at":{"type":["string","null"]},"created_at":{"type":"string"},"type":{"type":"string"},"minLikes":{"type":"integer"},"keywordContains":{"type":"string"},"actions":{"type":"array","items":{"type":"string"}},"replyTemplate":{"type":["string","null"]},"cooldownMinutes":{"type":"integer"},"lastFiredAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","name","listen_kind","target","min_likes_threshold","must_contain","must_not_contain","action_kinds","reply_template","cooldown_minutes","enabled","last_fired_at","created_at","type","minLikes","keywordContains","actions","replyTemplate","cooldownMinutes","lastFiredAt","createdAt"]},"MarketingXAutomationLog":{"type":"object","properties":{"id":{"type":"string"},"firedAt":{"type":"string"},"ruleName":{"type":"string"},"sourcePost":{"type":"string"},"author":{"type":"string"},"actionTaken":{"type":"string"},"rule_id":{"type":["string","null"],"format":"uuid"},"rule_kind":{"type":["string","null"]},"fired_at":{"type":"string"},"target_tweet_id":{"type":["string","null"]},"action":{"type":["string","null"]},"status":{"type":["string","null"]},"error":{"type":["string","null"]}},"required":["id","firedAt","ruleName","sourcePost","author","actionTaken","rule_id","rule_kind","fired_at","target_tweet_id","action","status","error"]},"MarketingXCompetitor":{"type":"object","properties":{"id":{"type":"string"},"handle":{"type":"string"},"display_name":{"type":["string","null"]},"memo":{"type":["string","null"]},"followers_count":{"type":"integer"},"posts_per_day":{"type":["number","null"]},"added_at":{"type":"string"},"displayName":{"type":"string"},"followers":{"type":"integer"},"postsPerWeek":{"type":"number"},"engagementRate":{"type":"number"},"topPost":{"type":"object","properties":{"body":{"type":"string"},"likes":{"type":"integer"},"postedAt":{"type":"string"}},"required":["body","likes","postedAt"]},"addedAt":{"type":"string"}},"required":["id","handle","display_name","memo","followers_count","posts_per_day","added_at","displayName","followers","postsPerWeek","engagementRate","topPost","addedAt"]},"MarketingIntegration":{"type":"object","properties":{"provider":{"type":"string","enum":["meta","x","ga4","search_console","resend","sendgrid","supabase_email"]},"status":{"type":"string"},"config":{},"updated_at":{"type":["string","null"]}},"required":["provider","status","updated_at"]},"MarketingBrand":{"type":"object","properties":{"logoUrl":{"type":["string","null"]},"primaryColor":{"type":["string","null"]},"secondaryColor":{"type":["string","null"]},"ngWords":{"type":"array","items":{"type":"string"}},"defaultHashtags":{"type":"array","items":{"type":"string"}},"defaultSignature":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["logoUrl","primaryColor","secondaryColor","ngWords","defaultHashtags","defaultSignature","updatedAt"]},"MarketingAsset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"kind":{"type":"string","enum":["image","video","gif","logo","icon","banner"]},"mime_type":{"type":["string","null"]},"r2_key":{"type":"string"},"public_url":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"byte_size":{"type":["integer","null"]},"duration_seconds":{"type":["integer","null"]},"tags":{"type":"array","items":{"type":"string"}},"alt_text":{"type":["string","null"]},"usage_count":{"type":"integer"},"uploaded_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","name","kind","mime_type","r2_key","public_url","width","height","byte_size","duration_seconds","tags","alt_text","usage_count","uploaded_by","created_at"]},"MarketingNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"kind":{"type":"string","enum":["x_post_failed","newsletter_failed","campaign_start","campaign_end","automation_fired","integration_error"]},"title":{"type":"string"},"body":{"type":["string","null"]},"severity":{"type":"string","enum":["info","warning","error","success"]},"read_at":{"type":["string","null"]},"link":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","kind","title","body","severity","read_at","link","created_at"]},"MarketingActivityLog":{"type":"object","properties":{"id":{"anyOf":[{"type":"integer"},{"type":"string"}]},"user_id":{"type":["string","null"],"format":"uuid"},"action":{"type":"string"},"target_kind":{"type":["string","null"]},"target_id":{"type":["string","null"]},"detail":{},"created_at":{"type":"string"}},"required":["id","user_id","action","target_kind","target_id","created_at"]},"MarketingCampaign":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"objective":{"type":["string","null"]},"status":{"type":"string"},"start_date":{"type":"string"},"end_date":{"type":["string","null"]},"budget_minor":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"channels":{"type":"array","items":{"type":"string"}},"target_kpis":{},"tags":{"type":"array","items":{"type":"string"}},"created_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","description","objective","status","start_date","end_date","budget_minor","channels","tags","created_by","created_at","updated_at"]},"MarketingCampaignItem":{"type":"object","properties":{"campaign_id":{"type":"string","format":"uuid"},"item_kind":{"type":"string","enum":["instagram_post","x_post","article","ad","newsletter"]},"item_id":{"type":"string"},"linked_at":{"type":"string"}},"required":["campaign_id","item_kind","item_id","linked_at"]},"MarketingCampaignDetail":{"allOf":[{"$ref":"#/components/schemas/MarketingCampaign"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingCampaignItem"}}},"required":["items"]}]},"MarketingCampaignMetrics":{"type":"object","properties":{"campaign_id":{"type":"string","format":"uuid"},"totals":{"type":"object","properties":{"articles":{"type":"integer"},"ads":{"type":"integer"},"x_posts":{"type":"integer"},"instagram_posts":{"type":"integer"},"newsletters":{"type":"integer"}},"required":["articles","ads","x_posts","instagram_posts","newsletters"]},"pv":{"type":"integer"},"ad_impressions":{"type":"integer"},"ad_clicks":{"type":"integer"},"ctr":{"type":"number"},"x_impressions":{"type":"integer"},"x_likes":{"type":"integer"},"newsletters_sent":{"type":"integer"}},"required":["campaign_id","totals","pv","ad_impressions","ad_clicks","ctr","x_impressions","x_likes","newsletters_sent"]},"MarketingAnalyticsSummary":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"totalPv":{"type":"integer"},"totalImpressions":{"type":"integer"},"totalClicks":{"type":"integer"},"ctr":{"type":"number"},"newsletters_sent":{"type":"integer"},"posts_published":{"type":"integer"},"campaigns_active":{"type":"integer"}},"required":["from","to","totalPv","totalImpressions","totalClicks","ctr","newsletters_sent","posts_published","campaigns_active"]},"MarketingAnalyticsArticleRow":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":["string","null"]},"title":{"type":"string"},"category":{"type":["string","null"]},"author_name":{"type":["string","null"]},"published_at":{"type":["string","null"]},"pv":{"type":"integer"},"pv_diff_pct":{"type":"number"}},"required":["id","slug","title","category","author_name","published_at","pv","pv_diff_pct"]},"MarketingAnalyticsAdRow":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"start_date":{"type":["string","null"]},"end_date":{"type":["string","null"]},"impressions":{"type":"integer"},"clicks":{"type":"integer"},"ctr":{"type":"number"},"days_total":{"type":"integer"},"days_elapsed":{"type":"integer"},"progress":{"type":"number"},"daily":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"clicks":{"type":"integer"}},"required":["date","clicks"]}}},"required":["id","name","status","start_date","end_date","impressions","clicks","ctr","days_total","days_elapsed","progress","daily"]},"MarketingAnalyticsCrossChannelRow":{"type":"object","properties":{"channel":{"type":"string"},"posts":{"type":"integer"},"impressions":{"type":"integer"},"likes":{"type":"integer"},"clicks":{"type":"integer"},"pv":{"type":"integer"}},"required":["channel","posts","impressions","likes","clicks","pv"]},"MarketingContentPoolItem":{"type":"object","properties":{"kind":{"type":"string","enum":["article","instagram_post","x_post","ad","newsletter","asset"]},"id":{"type":"string"},"title":{"type":"string"},"status":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"updated_at":{"type":["string","null"]},"meta":{"type":"object","additionalProperties":{}}},"required":["kind","id","title","status","thumbnail_url","updated_at"]},"MarketingCalendarEvent":{"type":"object","properties":{"kind":{"type":"string","enum":["x_post","article","ad","newsletter","instagram_post"]},"id":{"type":"string"},"title":{"type":"string"},"status":{"type":["string","null"]},"channel":{"type":"string","enum":["x","article","ad","newsletter","instagram"]},"at":{"type":"string"}},"required":["kind","id","title","status","channel","at"]},"MarketingCalendarDay":{"type":"object","properties":{"date":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingCalendarEvent"}}},"required":["date","items"]},"MarketingCalendarResponse":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"days":{"type":"array","items":{"$ref":"#/components/schemas/MarketingCalendarDay"}}},"required":["from","to","days"]},"MarketingArticleCategory":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"description":{"type":["string","null"]},"sort_order":{"type":"integer"},"is_deleted":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["code","label","description","sort_order","is_deleted","created_at","updated_at"]},"OwnerParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"status":{"type":["string","null"]},"total_spaces":{"type":["integer","null"]},"structure":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","name","address","status","total_spaces","structure","created_at"]},"OwnerSearchParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"has_owner":{"type":"boolean"}},"required":["id","name","address","lat","lng","has_owner"]},"OwnerApplication":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":["string","null"],"format":"uuid"},"owner_name":{"type":"string"},"owner_email":{"type":"string"},"company_name":{"type":["string","null"]},"parking_lot_id":{"type":"string","format":"uuid"},"proof_asset_id":{"type":["string","null"],"format":"uuid"},"additional_notes":{"type":["string","null"]},"status":{"type":"string"},"reviewed_at":{"type":["string","null"]},"reject_reason":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","owner_id","owner_name","owner_email","company_name","parking_lot_id","proof_asset_id","additional_notes","status","reviewed_at","reject_reason","created_at"]},"OwnerReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_name":{"type":"string"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"owner_reply":{"type":["string","null"]},"owner_replied_at":{"type":["string","null"]},"owner_replied_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]}},"required":["id","parking_lot_id","user_name","rating","comment","status","owner_reply","owner_replied_at","owner_replied_by","created_at","updated_at","parking_lot"]},"OwnerBoost":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"credit_per_impression":{"type":["integer","null"]},"daily_budget":{"type":["integer","null"]},"total_budget":{"type":["integer","null"]},"started_at":{"type":["string","null"]},"ended_at":{"type":["string","null"]},"impressions":{"type":["integer","null"]},"clicks":{"type":["integer","null"]},"sessions":{"type":["integer","null"]},"status":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","owner_id","parking_lot_id","name","credit_per_impression","daily_budget","total_budget","started_at","ended_at","impressions","clicks","sessions","status","created_at"]},"OwnerCreditTxn":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"type":{"type":"string"},"amount":{"type":"integer"},"balance_after":{"type":"integer"},"description":{"type":["string","null"]},"boost_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","owner_id","type","amount","balance_after","description","boost_id","created_at"]}},"parameters":{}},"paths":{"/.well-known/apple-app-site-association":{"get":{"tags":["well-known"],"summary":"iOS Universal Links のアソシエーション JSON","description":"Apple が Universal Links 検証時に取得する JSON。\n\n- 認証不要・公開エンドポイント。\n- Content-Type は application/json 固定。\n- Cache-Control: public, max-age=86400（24 時間）。\n- `appIDs` は環境変数 `IOS_APP_ID`（形式: TEAMID.co.jp.parky.app）から取得。未設定時はダミー値を使用。\n- 対応パス: /parking-lots/*, /parking-sessions/*, /share/parking/*, /articles/*, /auth/*","responses":{"200":{"description":"Apple App Site Association","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppleAppSiteAssociation"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/.well-known/assetlinks.json":{"get":{"tags":["well-known"],"summary":"Android App Links の Digital Asset Links JSON","description":"Android が App Links 検証時に取得する JSON。\n\n- 認証不要・公開エンドポイント。\n- Content-Type は application/json 固定。\n- Cache-Control: public, max-age=86400（24 時間）。\n- `package_name` は環境変数 `ANDROID_PACKAGE_NAME` から取得。未設定時は co.jp.parky.app を使用。\n- `sha256_cert_fingerprints` は本番 keystore の署名指紋（要投入）。","responses":{"200":{"description":"Digital Asset Links","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetLinksResponse"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/codes":{"get":{"tags":["codes"],"summary":"コードマスターをバルク取得（カテゴリ横断）","description":"### 用途\n全 `codes`（ステータス・カテゴリ等の列挙値ラベル）を 1 リクエストでバルク取得する。\nクライアントは起動時に一度だけ取得し、`category_id` + `code` → `display_label` 変換テーブルとしてキャッシュ利用する。\n\n### モバイルアプリでの使用タイミング\n- アプリ起動直後のブートストラップ（ログイン前でも OK）\n- 言語切替後の再取得（`lang` クエリを付け替え）\n- キャッシュ有効期限切れ時のバックグラウンド更新\n\n### 認証\n不要（パブリックエンドポイント）。認証ミドルウェアは掛かっていない。\n\n### 挙動・制約\n- `lang` クエリで言語フィルタ（default `ja`）\n- `is_deleted=false` のみ返す\n- `category_id, sort_order` 昇順\n- エッジキャッシュ有効: `Cache-Control: public, max-age=60, s-maxage=3600`（ブラウザ 60s / CDN 1h）\n- `metadata` フィールドにはコード値固有の拡張情報（color / logoUrl 等）が JSON で入る\n\n### 関連\n- プロジェクトルール: 列挙値は DB にコード値保存、UI ラベルはこのマスターで変換","parameters":[{"schema":{"type":"string","examples":["ja"]},"required":false,"name":"lang","in":"query"}],"responses":{"200":{"description":"コード一覧","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodesResponse"}}}},"500":{"description":"サーバーエラー","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me":{"get":{"tags":["me"],"summary":"自分のプロフィールと権限スコープを取得","description":"### 用途\nログイン中ユーザーの基本情報（`app_users` 行）と管理者権限（`admins` 行）を 1 リクエストで返す。\nクライアント起動直後に呼び、ユーザー/管理者判定と表示名取得に使う。\n\n### モバイルアプリでの使用タイミング\n- アプリ起動直後のブートストラップ（セッション復元直後）\n- プロフィール画面を開いたとき\n- ログイン / サインアップ成功直後の初期化\n\n### 認証\n要 Bearer JWT。`userId` は JWT の `sub`（auth.uid）。\n\n### 挙動・制約\n- `app_user` と `admin` は並列に問い合わせ、該当なしはそれぞれ null で返す\n- `email` は現状 null 固定（Supabase Auth からの取得は未実装）\n- ここで 404 は返さない。新規ユーザーで `app_users` が未作成でも 200 + `app_user: null`\n\n### 関連\n- `PATCH /v1/me` — プロフィール更新\n- `POST /v1/me/withdraw` — 退会","responses":{"200":{"description":"自分の情報","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"patch":{"tags":["me"],"summary":"自分のプロフィールを更新","description":"### 用途\nログイン中ユーザーの `app_users` 行を部分更新する。現状は `display_name` のみ。\n\n### モバイルアプリでの使用タイミング\n- プロフィール編集画面の「保存」タップ\n- オンボーディングでニックネームを入力した直後\n\n### 認証\n要 Bearer JWT。自分の `app_users` 行（`id = auth.uid`）のみが対象。\n\n### 挙動・制約\n- `display_name` は 1〜100 文字\n- ボディが空のときは現在値を返す（no-op）\n- 対象行が存在しなければ 404 `not_found`\n- 戻り値は `GET /v1/me` と同じ shape\n\n### 関連\n- `GET /v1/me` — 現在値の取得","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeUpdate"}}}},"responses":{"200":{"description":"更新後のプロフィール","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"app_users に該当なし","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/withdraw":{"post":{"tags":["me"],"summary":"退会（匿名化 + ステータス変更）","description":"### 用途\nユーザーの退会処理を実行する。DB 側 RPC `withdraw_account(p_user_id)` を呼び、\n`app_users` のステータス変更と個人情報（表示名・email 等）の匿名化を一括で行う。\n\n### モバイルアプリでの使用タイミング\n- 設定 → アカウント → 退会フローの最終確認ダイアログ「退会する」タップ\n- 退会前に最終的な確認画面を出すこと（操作取り消し不可）\n\n### 認証\n要 Bearer JWT。`p_user_id` は JWT 由来の `userId` をサーバー側で渡し、他人の退会は不可。\n\n### 挙動・制約\n- 副作用は DB 側の RPC に集約（匿名化、`deleted_at` セット、セッション終了など）\n- BFF は RPC を呼ぶのみで冪等性は DB 側に委譲\n- 呼び出し成功後、クライアントはローカルトークンを破棄してサインイン画面に戻ること\n\n### 関連\n- `GET /v1/me` — 現在の status 確認（`withdrawn` 等）","responses":{"200":{"description":"退会完了","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/saved-parking-lots":{"get":{"tags":["me"],"summary":"保存した駐車場の一覧","description":"### 用途\nログイン中ユーザーがお気に入り登録（保存）している駐車場の一覧を返す。\n登録済みの駐車場 ID と登録時刻のみを含む軽量レスポンスで、詳細は別途駐車場 API で引く想定。\n\n### モバイルアプリでの使用タイミング\n- プロフィール → 「お気に入り」タブを開いたとき\n- 検索画面の「お気に入りから選ぶ」ショートカット\n- 駐車場詳細画面でハートボタンの初期状態を解決する前段\n\n### 認証\n要 Bearer JWT。`userId` 一致のレコードのみ返す。\n\n### 挙動・制約\n- `created_at` 降順（直近保存したものから並ぶ）\n- ページングなし（1 ユーザーあたり件数は多くない想定）\n- 駐車場そのものが削除されても `user_saved_parkings` の行は残り得る点に注意\n\n### 関連\n- `POST /v1/me/saved-parking-lots` — 保存を追加\n- `DELETE /v1/me/saved-parking-lots/{lotId}` — 保存を解除","responses":{"200":{"description":"保存一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SavedParkingLot"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"post":{"tags":["me"],"summary":"保存を追加","description":"### 用途\n指定駐車場をお気に入り（保存）リストに追加する。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細のハート（🤍 → ♥️）タップ\n- 検索結果カードの長押し → 「保存する」\n- 駐車完了画面の「よく使うのでお気に入り登録」導線\n\n### 認証\n要 Bearer JWT。保存は `userId` 紐付けで作成される。\n\n### 挙動・制約\n- `user_saved_parkings` に INSERT。(user_id, parking_lot_id) の UNIQUE 制約で二重保存は 409 `conflict`\n- 成功時に `parking_saved` アクティビティをベストエフォートで emit → EXP / バッジ判定に使われる\n- 非存在の `parking_lot_id` は FK 違反になり DB エラー経由で 400 系として返る\n\n### 関連\n- `GET /v1/me/saved-parking-lots` — 保存一覧\n- `DELETE /v1/me/saved-parking-lots/{lotId}` — 保存解除","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"parking_lot_id":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]}},"required":["parking_lot_id"]}}}},"responses":{"200":{"description":"追加した行","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedParkingLot"}}}},"409":{"description":"既に保存済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/saved-parking-lots/{lotId}":{"delete":{"tags":["me"],"summary":"保存を解除","description":"### 用途\n指定駐車場をお気に入り（保存）リストから外す。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細のハート（♥️ → 🤍）再タップ\n- お気に入り一覧でスワイプ削除\n\n### 認証\n要 Bearer JWT。`userId` 一致行のみが DELETE 対象。\n\n### 挙動・制約\n- 物理 DELETE（ソフト削除しない。再保存すれば新しい `created_at` で行が作り直される）\n- 対象行が無くても 204 を返す（冪等）\n- EXP / アクティビティの取り消しは行わない（過去の `parking_saved` は履歴として残る）\n\n### 関連\n- `GET /v1/me/saved-parking-lots` — 保存一覧\n- `POST /v1/me/saved-parking-lots` — 保存追加","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"lotId","in":"path"}],"responses":{"204":{"description":"削除成功"}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/parking-lots/{id}/rating":{"get":{"tags":["me","ratings"],"summary":"自分の指定駐車場への評価を取得","description":"### 用途\nログイン中ユーザーが指定駐車場に既に Good/Bad 評価を付けているかを取得する。\nUI の★ボタン（または Good/Bad トグル）の初期表示を解決するために使う軽量 API。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面を開いた初回レンダリング時（既に評価済みかを確認）\n- 評価 UI を楽観的更新する前の現在状態取得\n\n### 認証\n要 Bearer JWT。JWT の `auth.uid` → `app_users.id` に解決し、自分の行のみを返す。\n\n### 挙動・制約\n- 未評価なら `null` を 200 で返す（404 にはしない）。UI は `null` を「未評価」と解釈する\n- 1 ユーザー × 1 駐車場 = 最大 1 行（UNIQUE 制約）\n- `app_users` が見つからない場合のみ 404（アカウント破損状態）\n\n### 関連\n- `POST /v1/me/parking-lots/{id}/rating` — 評価を付ける・上書きする\n- `DELETE /v1/me/parking-lots/{id}/rating` — 評価を撤回","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"既存の評価 or null","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkingLotRating"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"post":{"tags":["me","ratings"],"summary":"駐車場に Good/Bad 評価を付ける（1 ユーザー 1 票、再送で上書き）","description":"ログインユーザーが指定駐車場に Good/Bad の賛否評価を付ける。\n\n- **UPSERT**: (user, parking_lot) UNIQUE に対して ON CONFLICT UPDATE。2 回目以降は value を上書き。\n- **ゲーミフィケーション**: 初回 INSERT（新規付与）のみ `rating` アクティビティを emit（EXP 3）。上書きは EXP 非付与。\n- **参考**: 同じ駐車場に文章レビューを書きたい場合は POST /v1/me/reviews を使う。こちらは軽量な指差しアクション。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"string","enum":["good","bad"]}},"required":["value"]}}}},"responses":{"200":{"description":"作成 or 上書きされた行","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ParkingLotRating"},{"type":"object"}]}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"駐車場が存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"delete":{"tags":["me","ratings"],"summary":"自分の評価を撤回（無評価に戻す）","description":"### 用途\n自分が付けた Good/Bad 評価を撤回し、当該駐車場に対して未評価の状態に戻す。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面で自分の ⭐/👍/👎 を再タップしてトグル解除するとき\n- 「評価を取り消す」メニューから明示的に削除するとき\n\n### 認証\n要 Bearer JWT。他人の評価に触れる経路は存在しない（`user_id` 一致条件で DELETE）。\n\n### 挙動・制約\n- 物理 DELETE。ソフト削除ではない\n- 対象行が無くても 204 を返す（冪等）\n- EXP の取り消しは行わない（過去の `rating` アクティビティは残る）\n\n### 関連\n- `GET /v1/me/parking-lots/{id}/rating` — 現在の評価取得\n- `POST /v1/me/parking-lots/{id}/rating` — 評価を付ける・上書きする","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功（評価が無くても 204 を返す）"}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/vehicles":{"get":{"tags":["me"],"summary":"自分の車両一覧","description":"### 用途\nログイン中ユーザーが登録している車両一覧を返す。デフォルト車両（`is_default=true`）が先頭。\n\n### モバイルアプリでの使用タイミング\n- 車両選択シート・切替 UI を開いたとき\n- プロフィール → 登録車両画面の表示\n- 駐車セッション開始前の車両確認（デフォルト車両を既定選択）\n\n### 認証\n要 Bearer JWT。`user_vehicles` を `user_id = auth.uid` で検索。\n\n### 挙動・制約\n- ソフト削除済み（`deleted_at IS NOT NULL`）は含まれない\n- `is_default DESC, created_at DESC` 順\n- `is_default=true` は通常 1 件のみ\n\n### 関連\n- `POST /v1/me/vehicles` — 新規登録\n- `PATCH /v1/me/vehicles/{id}` — 更新（主登録切替含む）\n- `DELETE /v1/me/vehicles/{id}` — ソフト削除","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserVehicle"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"post":{"tags":["me"],"summary":"車両を登録","description":"### 用途\n新しい車両（`vehicle_type` + ニックネーム・ナンバー・色など）を登録する。\n`is_default=true` で登録すると、以降の駐車検索・セッション開始の既定車両となる。\n\n### モバイルアプリでの使用タイミング\n- 車両登録画面の「保存」タップ\n- オンボーディングの車両登録ステップ\n- 「+ 車両を追加」シートから新規追加\n\n### 認証\n要 Bearer JWT。`user_id` は JWT 由来で上書きされるため、クライアントからは送らない。\n\n### 挙動・制約\n- `vehicle_type` はコードマスター `vehicle_type` の code 値（`kei`, `compact` 等）\n- `nickname` 最大 50 / `plate_number` 最大 20 / `color` 最大 30 文字\n- 登録成功時に `vehicle_added` アクティビティがベストエフォートで記録される（EXP 加算対象）\n- 既存の主登録を解除するロジックは現状このエンドポイントでは行わない（DB 側トリガーに委譲）\n\n### 関連\n- `GET /v1/me/vehicles` — 一覧\n- `PATCH /v1/me/vehicles/{id}` — 部分更新","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"vehicle_type":{"type":"string"},"nickname":{"type":["string","null"],"maxLength":50},"plate_number":{"type":["string","null"],"maxLength":20},"color":{"type":["string","null"],"maxLength":30},"is_default":{"type":"boolean"}},"required":["vehicle_type"]}}}},"responses":{"200":{"description":"登録済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserVehicle"}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/vehicles/{id}":{"patch":{"tags":["me"],"summary":"車両を更新","description":"### 用途\n既存の車両情報を部分更新する。ニックネーム変更、ナンバー修正、主登録切替などに使う。\n\n### モバイルアプリでの使用タイミング\n- 車両編集画面の「保存」タップ\n- 車両一覧の「⭐ 主登録にする」アクション\n\n### 認証\n要 Bearer JWT。他ユーザーの車両 ID を指定しても `not_found` (404) を返す\n（`WHERE user_id = ${userId}` で絞り込み）。\n\n### 挙動・制約\n- ボディ空の場合は現在値を返す（no-op）\n- 指定フィールドのみ `SET` で書き換え、未指定は維持\n- 存在しない / 自分のものでない場合は 404 `not_found`\n- `is_default=true` で更新すると、他の主登録解除は DB トリガー側で処理\n\n### 関連\n- `GET /v1/me/vehicles` — 一覧\n- `DELETE /v1/me/vehicles/{id}` — ソフト削除","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"vehicle_type":{"type":"string"},"nickname":{"type":["string","null"],"maxLength":50},"plate_number":{"type":["string","null"],"maxLength":20},"color":{"type":["string","null"],"maxLength":30},"is_default":{"type":"boolean"}}}}}},"responses":{"200":{"description":"更新済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserVehicle"}}}},"404":{"description":"存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"delete":{"tags":["me"],"summary":"車両をソフト削除","description":"### 用途\n車両を論理削除（`deleted_at = NOW()`）する。物理削除はしない。\n\n### モバイルアプリでの使用タイミング\n- 車両一覧のスワイプ削除 / ゴミ箱タップ\n- 車両編集画面の「削除」アクション\n\n### 認証\n要 Bearer JWT。他ユーザーの ID を指定しても 204（冪等）。\n\n### 挙動・制約\n- 以降、一覧 API からは返らなくなる\n- 過去の駐車セッションから参照されている車両でも削除可能（外部キーは残る）\n- 削除対象が主登録だった場合でも、他車両を自動で主登録に昇格させる処理はクライアント側で再指定\n\n### 関連\n- `GET /v1/me/vehicles` — 削除後の一覧確認","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"削除成功"}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/search-presets":{"get":{"tags":["me","search-presets"],"summary":"自分の検索プリセット一覧（sort_order 昇順）","description":"### 用途\nログイン中ユーザーが保存している **検索条件プリセット** の一覧を返す。\nプリセットは「駅から徒歩5分以内・屋根付き」「自宅周辺・月極」のように、\nよく使う検索条件を名前付きで保存した `SearchQueryV1` の JSON。\n\n### モバイルアプリでの使用タイミング\n- 検索画面で「保存した条件から選ぶ」シートを開いたとき\n- プロフィール → 検索プリセット管理画面を開いたとき\n- アプリ起動時のデフォルト条件を解決するとき（`is_default=true` を採用）\n\n### 認証\n要 Bearer JWT。BFF 内部で `auth.uid → app_users.id` に解決し、自分のレコードのみ返す。\n\n### 並び順・件数\n`sort_order` 昇順 → `created_at` 昇順。1 ユーザーあたり最大 20 件。\nソフト削除済み（`deleted_at != NULL`）は含まれない。`is_default=true` は通常 1 件のみ。\n\n### 関連\n- `POST /v1/me/search-presets` — 新規作成\n- `PATCH /v1/me/search-presets/{id}` — 名前・条件・並び順を更新\n- `POST /v1/me/search-presets/{id}/set-default` — デフォルト切替","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchPreset"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"post":{"tags":["me","search-presets"],"summary":"プリセットを新規作成（最大 20 件まで）","description":"### 用途\n検索画面で組み立てた条件に名前を付けて保存する。\n\n### モバイルアプリでの使用タイミング\n- 検索画面の「条件を保存」ボタンをタップした直後\n- オンボーディング時に「よく停める場所」を登録する導線\n\n### 認証\n要 Bearer JWT。\n\n### バリデーション\n- `name` は 1〜50 文字の必須項目\n- `query_json` は `SearchQueryV1` スキーマ準拠\n- `is_default: true` で作成すると、既存のデフォルトは自動で解除される（トランザクション）\n\n### 件数上限\n1 ユーザー 20 件。超過した場合は `plan_limit_exceeded`（400）を返す。\nアプリ側は UI で事前に件数を表示しておくのが望ましい。\n\n### 副作用\n作成成功時に `save_search_condition` アクティビティがベストエフォートで記録され、\nゲーミフィケーションの EXP に反映される。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50},"query_json":{"$ref":"#/components/schemas/SearchQueryV1"},"is_default":{"type":"boolean"},"sort_order":{"type":"integer"}},"required":["name","query_json"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchPreset"}}}},"400":{"description":"入力不正 or 件数上限","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/search-presets/{id}":{"get":{"tags":["me","search-presets"],"summary":"プリセット 1 件取得","description":"### 用途\n特定のプリセットを詳細取得する。編集画面で現在値を表示するために使う。\n\n### モバイルアプリでの使用タイミング\n- プリセット編集画面に遷移したとき（リストの古い値を信用せず最新を取り直す）\n- ディープリンク（通知 → プリセット詳細）で直接開かれたとき\n\n### 認証と認可\n要 Bearer JWT。他ユーザーのプリセット ID を指定しても `not_found` (404) を返す\n（存在有無で他者データの漏洩を起こさないため 403 ではなく 404）。\n\n### エラー\n- 404 `not_found` — 存在しない・自分のものでない・ソフト削除済み","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"1 件","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchPreset"}}}},"404":{"description":"存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"patch":{"tags":["me","search-presets"],"summary":"プリセットを部分更新（名前・条件・並び順）","description":"### 用途\nプリセットの名前・検索条件・表示順を部分更新する。指定されたフィールドだけを上書きし、\n未指定のカラムは現行値を維持する（サーバー側で COALESCE）。\n\n### モバイルアプリでの使用タイミング\n- プリセット編集画面の「保存」タップ\n- リスト並び替え（ドラッグ&ドロップ）のコミット時\n\n### 更新可能フィールド\n- `name` — 1〜50 文字\n- `query_json` — `SearchQueryV1`\n- `sort_order` — 整数\n\n### デフォルト切替について\n`is_default` はこのエンドポイントでは変更しない。専用の\n`POST /v1/me/search-presets/{id}/set-default` を使うこと（他プリセットのデフォルト解除を\nアトミックに行うため）。\n\n### 認可\n他ユーザーのプリセット ID を指定しても `not_found` (404)。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50},"query_json":{"$ref":"#/components/schemas/SearchQueryV1"},"sort_order":{"type":"integer"}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchPreset"}}}},"404":{"description":"存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"delete":{"tags":["me","search-presets"],"summary":"プリセットをソフト削除","description":"### 用途\nプリセットを論理削除（`deleted_at = NOW()`）する。物理削除はしない。\n\n### モバイルアプリでの使用タイミング\n- プリセット一覧でスワイプ削除 / ゴミ箱タップ\n\n### 挙動\n- 削除対象が `is_default=true` だった場合、`is_default=false` に同時に落とす\n- 以降、一覧 API からは返らなくなる\n- 存在しない ID / 他ユーザーの ID の場合でも 204 を返す（冪等）\n\n### 関連\nデフォルト扱いだった場合、ユーザー側でデフォルト再指定が必要。アプリは削除後の一覧再取得で\nデフォルトが 0 件になっていたら UI で案内する。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"削除成功"}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/search-presets/{id}/set-default":{"post":{"tags":["me","search-presets"],"summary":"指定プリセットをデフォルトに設定（他を解除）","description":"### 用途\n指定プリセットを唯一のデフォルト（`is_default=true`）にする。同ユーザーの他プリセットは\nすべて `is_default=false` にアトミックに切り替わる。\n\n### モバイルアプリでの使用タイミング\n- プリセット一覧で ⭐ ボタンをタップ\n- 新規登録ウィザードの最後で「これを普段使いにする」選択時\n\n### なぜ専用エンドポイントか\n複数プリセットの `is_default` を 1 度のリクエストで整合性を保って切り替える必要があるため、\nPATCH では提供せず、このアトミック API に集約している。\n\n### 冪等性\n既にデフォルトのプリセットに対して呼んでも 200（現在値）を返す。\n\n### 認可\n自分のプリセットでなければ 404 `not_found`。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchPreset"}}}},"404":{"description":"存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/parking-sessions":{"get":{"tags":["me"],"summary":"自分の駐車セッション一覧","description":"### 用途\nログイン中ユーザー自身の駐車セッション履歴をページングして返す。\n進行中（`active`）・確定済み（`completed`）・キャンセル（`cancelled`）など\n`status` で絞り込める。`start_at` 降順で、直近のセッションから並ぶ。\n\n### モバイルアプリでの使用タイミング\n- プロフィール → 駐車履歴タブを開いたとき\n- ホーム画面復帰時に進行中セッション（`status=active`）を検出してタイマー UI を復帰するとき\n- レビュー投稿導線で「どの駐車場に停めたか」を選ばせるリストを出すとき\n\n### 認証\n要 Bearer JWT。JWT の `userId` に一致する `parking_sessions.user_id` のみを返す。\n\n### 挙動・制約\n- `status` クエリ未指定なら全ステータスを返す\n- `page` / `limit` は `PageQuerySchema` に従い、`items` + `total` を返す標準形\n- `start_at` NULL のレコードは末尾に並ぶ（`NULLS LAST`）\n\n### 関連\n- `GET /v1/me/parking-sessions/{id}` — 1 件の詳細\n- `POST /v1/parking-sessions` — セッション開始\n- `POST /v1/parking-sessions/{id}/finalize` — 終了・料金確定","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"履歴","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ParkingSession"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/parking-sessions/active":{"get":{"tags":["me"],"summary":"進行中の駐車セッションを取得（バブル表示用）","description":"モバイルのホーム画面バブル・ウィジェット表示用。進行中（status=active）のセッションを1件返す。\nアクティブなセッションがない場合は session: null を返す（404ではない）。\nリアルタイム性が重要なため Cache-Control: no-store を設定。","responses":{"200":{"description":"進行中セッションまたは null","content":{"application/json":{"schema":{"type":"object","properties":{"session":{"allOf":[{"$ref":"#/components/schemas/ParkingSession"},{"type":["object","null"]}]}},"required":["session"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/parking-sessions/{id}":{"get":{"tags":["me"],"summary":"自分の駐車セッション詳細","description":"### 用途\n指定セッションの詳細 1 件を返す。一覧に含まれる情報を再取得する正本として使う。\n編集画面や精算画面で、最新のステータス・料金・メモ等を表示するために呼ぶ。\n\n### モバイルアプリでの使用タイミング\n- 履歴一覧から 1 件タップして詳細画面を開いたとき\n- 精算画面で最新の `fee_amount` / `end_at` を再取得したいとき\n- プッシュ通知から直接詳細にディープリンクで飛んできたとき\n\n### 認証\n要 Bearer JWT。他人のセッション ID を指定した場合も `not_found` (404) を返す\n（存在有無を他人に漏らさないため 403 ではなく 404）。\n\n### 挙動・制約\n- `id` と `user_id` の両方で WHERE するため、他ユーザーのセッションは触れない\n- ソフト削除の仕組みは無く、`cancelled` も結果として返る\n\n### 関連\n- `GET /v1/me/parking-sessions` — 自分の履歴一覧\n- `PATCH /v1/me/parking-sessions/{id}` — メモ・個人評価の更新","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"詳細（photos 配列含む）","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkingSessionDetail"}}}},"404":{"description":"存在しないか他人の","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"patch":{"tags":["me"],"summary":"メモ・個人評価を更新","description":"### 用途\n自分の駐車セッションに個人メモ・個人評価（非公開ラベル）を追記／更新する。\nここで書くのは自分だけが見るメモで、駐車場の公開レビュー（`parking_reviews`）とは別系統。\n\n### モバイルアプリでの使用タイミング\n- 駐車終了後の「メモを残す」導線（例: 「屋根あり・入口狭い」など次回のための私的メモ）\n- 履歴一覧で星ラベル付けをしたとき（personal_rating）\n- 精算画面から詳細を編集するシーン\n\n### 認証\n要 Bearer JWT。自分のセッション（`user_id` 一致）でなければ `not_found` (404)。\n\n### 挙動・制約\n- `memo` / `personal_rating` のみ更新可能。ステータス・料金等は別 API（finalize 等）で変更する\n- body が空の場合は更新をスキップして現在値をそのまま返す（冪等）\n- `null` を渡すと該当フィールドをクリアできる\n\n### 関連\n- `POST /v1/me/reviews` — 公開レビュー（★＋コメント）の投稿\n- `GET /v1/me/parking-sessions/{id}` — 更新前の現在値を取得","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"memo":{"type":["string","null"]},"personal_rating":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkingSession"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/parking-sessions/{id}/photos":{"get":{"tags":["me"],"summary":"駐車セッション写真一覧","description":"指定セッションに紐づく写真の一覧を返す（slot 昇順）。\n`phase` クエリを省略した場合は during / after の全件を返す。\n他人のセッション ID を指定した場合は 404 を返す（存在有無を漏らさないため）。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["during","after"]},"required":false,"name":"phase","in":"query"}],"responses":{"200":{"description":"写真一覧（slot 昇順）","content":{"application/json":{"schema":{"type":"object","properties":{"photos":{"type":"array","items":{"$ref":"#/components/schemas/SessionPhoto"}}},"required":["photos"]}}}},"404":{"description":"セッションが存在しないか他人の","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/parking-sessions/{id}/photos/{phase}/{slot}":{"put":{"tags":["me"],"summary":"駐車セッション写真を登録・更新（upsert）","description":"R2 へのアップロード完了後にこのエンドポイントを呼び出し、r2_key をセッションに紐付ける。\n同一 (session_id, phase, slot) が既に存在する場合は上書き更新（ON CONFLICT DO UPDATE）。\nR2 への実際のアップロードは `POST /v1/storage/upload-url` で presigned URL を取得してから行う。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["during","after"]},"required":true,"name":"phase","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":4},"required":true,"name":"slot","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"r2_key":{"type":"string","minLength":1},"content_type":{"type":"string"},"size_bytes":{"type":"integer","exclusiveMinimum":0}},"required":["r2_key"]}}}},"responses":{"200":{"description":"登録・更新後の写真","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionPhoto"}}}},"404":{"description":"セッションが存在しないか他人の","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"delete":{"tags":["me"],"summary":"駐車セッション写真を削除","description":"指定スロットのレコードを DB から削除する。\n**R2 オブジェクト本体はこのエンドポイントでは削除しない**。\nR2 の実体は別途クリーンアップ Cron（未登録の r2_key を週次で掃除する想定）で削除する。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["during","after"]},"required":true,"name":"phase","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":4},"required":true,"name":"slot","in":"path"}],"responses":{"200":{"description":"削除結果","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"レコードが存在しないか他人の","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/reviews":{"post":{"tags":["me","reviews"],"summary":"レビューを投稿（status=pending で作成、admin が承認）","description":"ログインユーザーが駐車場にレビュー（★1-5 + コメント）を投稿する。\n\n- **モデレーション**: `status='pending'` で作成され、管理ポータルで approved/rejected に遷移する。\n- **重複投稿**: 同一 (user, parking_lot) は parking_reviews 側の一意制約で弾かれ 409 conflict になる。編集は PATCH /v1/me/reviews/{id}。\n- **ゲーミフィケーション**: 成功時に `review_post` アクティビティを emit し、EXP 15 を付与する（activity_exp_rules）。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"parking_lot_id":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"],"maxLength":2000}},"required":["parking_lot_id","rating"]}}}},"responses":{"200":{"description":"作成済みレビュー（status=pending）","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyReview"}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"既に同じ駐車場へレビュー済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/reviews/{id}":{"patch":{"tags":["me"],"summary":"自分のレビューを編集（再審査扱いで status=pending に戻る運用を想定）","description":"### 用途\n自分が投稿した駐車場レビューの ★ 評価 / コメントを編集する。\n運用としては、編集は再審査扱いとして `status=pending` に戻ることを想定\n（ただし本エンドポイントは受け取った項目をそのまま UPDATE する軽量 PATCH）。\n\n### モバイルアプリでの使用タイミング\n- プロフィール → 自分のレビュー一覧 → 編集ボタンを押したとき\n- 投稿直後の「しまった書き間違えた」リカバリ導線\n\n### 認証\n要 Bearer JWT。`id` と `user_id` の両方で絞るため、他人のレビューは編集不可（404 を返す）。\n\n### 挙動・制約\n- `rating`（1〜5）/ `comment`（最大 2000 文字・null 可）を部分更新\n- body が空なら現在値を返す（冪等）\n- 審査中に再編集された場合のポリシー（再 pending 化など）は admin 側で運用\n- ゲーミフィケーションの EXP は編集では付与しない（初回投稿時のみ付与済）\n\n### 関連\n- `POST /v1/me/reviews` — レビュー新規投稿\n- `GET /v1/parking-lots/{id}/reviews` — 駐車場ごとの公開レビュー一覧","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"],"maxLength":2000}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyReview"}}}},"404":{"description":"存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/reviews":{"get":{"tags":["reviews"],"summary":"公開レビューの横断一覧（status=approved、SSG ビルド向け）","description":"### 用途\n全駐車場を横断した `status=approved` の承認済みレビューをページングで返す。\nSSG ビルドで駐車場ごとの集計（平均★・件数）を 1 回のクエリで構築したり、\n「新着レビュー」フィードを作る用途に使える公開 API。\n\n### モバイルアプリでの使用タイミング\n- ホーム画面「新着レビュー」カルーセル\n- 駐車場詳細画面初期表示と合わせて直近レビューを一気に取得\n- 静的サイト（Astro SSG）側でビルド時にレビュー全件を取る\n\n### 認証\n不要。`optionalUser` ミドルウェア経由なので未ログインでも叩ける。\nログインしていても特別扱いはしない。\n\n### 挙動・制約\n- `status` を省略すると `approved` 固定。`pending` / `rejected` を指定すれば該当のみ返す（通常用途では approved のみ）\n- `created_at` 降順\n- レスポンスに `Cache-Control: public, max-age=30, s-maxage=300` を付与してエッジキャッシュ有効\n- `user_id` は `nullable`（匿名化投稿・退会ユーザー対応）\n\n### 関連\n- `POST /v1/me/reviews` — レビュー投稿\n- `GET /v1/parking-lots/{id}/reviews` — 1 駐車場に絞った公開レビュー","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","examples":["approved"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"レビュー一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PublicReview"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/notifications":{"get":{"tags":["me"],"summary":"自分宛の通知一覧","description":"ログインユーザー自身の `user_notifications` を新しい順で返す。\n\n- **認証**: 必須。`user_id` は JWT から取得し、他人の通知は絶対に返さない（コード層 + RLS 両方で防御）。\n- **ページング**: `page` / `limit`（最大 100）。PullToRefresh で先頭のみ、スクロール末尾で次ページ。\n- **Realtime 併用**: 同条件で supabase-flutter の Realtime を購読すれば INSERT を即時受信可能（データプレーン例外）。","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"通知一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/UserNotification"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/notifications/mark-read":{"post":{"tags":["me"],"summary":"通知を既読にする（IDs か before 指定、両方 null なら全件）","description":"通知を既読化する。既読は `read_at` に現在時刻を書き込むことで表現される。\n\n- **モード**: `ids` → 特定通知のみ / `before` → その時刻以前をまとめて / 両方 null → 自分の未読全件。\n- **冪等**: 既読済み行への呼び出しは no-op（`read_at` を上書きしない）。\n- **レスポンス**: `updated` に更新行数を返す。バッジ表示の差分更新に利用できる。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"}},"before":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"既読にした件数","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}},"required":["updated"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/notifications/types":{"get":{"tags":["me"],"summary":"通知カテゴリ一覧","description":"設定可能な通知カテゴリ（notif_type）の一覧を返す。`codes` マスタの `category_id='notif_type'` 行から取得。","responses":{"200":{"description":"通知カテゴリ一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NotifType"}}},"required":["items"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/notifications/preferences":{"get":{"tags":["me"],"summary":"通知カテゴリ別設定を取得","description":"ログインユーザーの通知オプトアウト設定を返す。\n\n- `items`: 設定済みカテゴリのみ返す（未設定カテゴリは `default_fallback` が適用される運用）。\n- `default_fallback`: 未設定カテゴリに適用されるデフォルト値（push/in_app=ON、email=OFF）。","responses":{"200":{"description":"通知設定一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NotifPrefItem"}},"default_fallback":{"type":"object","properties":{"push_enabled":{"type":"boolean"},"in_app_enabled":{"type":"boolean"},"email_enabled":{"type":"boolean"}},"required":["push_enabled","in_app_enabled","email_enabled"]}},"required":["items","default_fallback"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"put":{"tags":["me"],"summary":"通知カテゴリ別設定を更新","description":"リクエストに含まれるカテゴリのみ upsert する（差分更新）。\n\n- `notif_type` は `GET /types` で返すコード値のみ有効。不明コードは 400。\n- 既存行を消さずに値のみ上書き（`ON CONFLICT DO UPDATE`）。\n- リクエストに無いカテゴリの既存行は一切変更しない。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"notif_type":{"type":"string","minLength":1},"push_enabled":{"type":"boolean"},"in_app_enabled":{"type":"boolean"},"email_enabled":{"type":"boolean"}},"required":["notif_type"]},"minItems":1}},"required":["items"]}}}},"responses":{"200":{"description":"更新したカテゴリ数","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}},"required":["updated"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/push-tokens":{"put":{"tags":["me"],"summary":"FCM デバイストークンを upsert","description":"端末の FCM 登録トークンをサーバに登録する。Push 通知配信の前提となる。\n\n- **呼び出しタイミング**: アプリ起動直後・`onTokenRefresh` イベント時に叩く。FCM トークンは変わり得る。\n- **upsert**: `(user_id, device_type)` で一意。再登録は値上書き。\n- **サインアウト時**: 端末側で DELETE を投げる（誤配信防止）。\n- **現状**: サーバ側配信（`parky-fcm-dispatch` キュー + consumer）は完成済。Flutter 側 `firebase_messaging` 組み込みが残タスク。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"fcm_token":{"type":"string","minLength":1},"device_type":{"type":"string","enum":["android","ios","web"],"default":"android"}},"required":["fcm_token"]}}}},"responses":{"200":{"description":"登録済みトークン","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPushToken"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/exp":{"get":{"tags":["me"],"summary":"自分の EXP・レベル（次レベルまでの残 EXP を含む）","description":"### 用途\nゲーミフィケーション用に、自分の累計 EXP、現在レベル、次レベル到達に必要な EXP を返す。\nプロフィール画面の EXP バー・レベル表示に使う。\n\n### モバイルアプリでの使用タイミング\n- プロフィール画面表示時の EXP バー描画\n- アクティビティ完了後のレベルアップ判定（駐車完了・レビュー投稿など直後）\n- ホーム画面のユーザーステータスサマリー\n\n### 認証\n要 Bearer JWT。`user_exp` テーブルを `user_id = auth.uid` で検索。\n\n### 挙動・制約\n- 初回ユーザー（`user_exp` 行なし）は `total_exp=0, level=1` を返す\n- `next_level_exp` は `level_definitions` から `level + 1` の `required_exp` を引く\n- 最大レベル到達時は `next_level_exp` / `exp_to_next_level` が null\n- `exp_to_next_level` は負にならないよう `Math.max(0, ...)` でクランプ\n\n### 関連\n- `GET /v1/me/badges` — 獲得バッジ一覧\n- `GET /v1/me/badge-progress` — 未獲得バッジの進捗","responses":{"200":{"description":"EXP 情報","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyExp"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/badges":{"get":{"tags":["me"],"summary":"獲得済みバッジ一覧","description":"### 用途\nログイン中ユーザーが獲得済みのバッジを、定義情報（名前・アイコン・説明・カテゴリ）込みで返す。\n最新獲得順に並ぶ。\n\n### モバイルアプリでの使用タイミング\n- プロフィールのバッジ一覧タブ表示時\n- バッジ獲得直後のお祝いモーダル（直前の `earned_at` と突合）\n- ホーム画面の最新バッジハイライト\n\n### 認証\n要 Bearer JWT。`user_badges` を `user_id = auth.uid` で検索。\n\n### 挙動・制約\n- `earned_at` DESC で最新順\n- `badge_definitions` と LEFT JOIN し、定義が削除済みの場合 `badge: null`\n- ペイロードが大きくなりすぎないよう、クライアントは必要に応じて最新 N 件だけ表示する\n\n### 関連\n- `GET /v1/me/badge-progress` — 未獲得バッジの進捗率\n- `GET /v1/me/exp` — 現在レベル","responses":{"200":{"description":"バッジ一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MyBadge"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/badge-progress":{"get":{"tags":["me"],"summary":"進捗中のバッジ一覧（未獲得のみ）","description":"### 用途\nまだ獲得していないバッジについて、現在カウント / 閾値 / 達成率(%) を返す。\n「あと何回で次のバッジ」の可視化に使う。\n\n### モバイルアプリでの使用タイミング\n- プロフィールのバッジタブで未獲得セクションを描画するとき\n- バッジ詳細ダイアログで進捗バーを表示するとき\n\n### 認証\n要 Bearer JWT。`user_badge_progress` を `user_id = auth.uid` で検索。\n\n### 挙動・制約\n- `badge_definitions.is_active = false` のバッジは除外\n- `percent` は `min(100, round(count / threshold * 100))` で整数化\n- `threshold` が未定義（null）の場合は 1 として扱い、0 除算を避ける\n- 定義が未紐付けの場合は `badge: null`\n\n### 関連\n- `GET /v1/me/badges` — 獲得済みバッジ\n- `GET /v1/me/exp` — EXP・レベル","responses":{"200":{"description":"進捗一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MyBadgeProgress"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/themes":{"get":{"tags":["me"],"summary":"所有テーマ一覧","description":"### 用途\nログインユーザーが所有しているテーマ（購入 / 付与済み）を一覧で返す。\n各行にはテーマ本体情報（`theme` フィールド）を LEFT JOIN でネストし、削除済みテーマでも行は返す。\n\n### モバイルアプリでの使用タイミング\n- プロフィール → カスタマイズ画面の「所有テーマ」タブ\n- ショップ画面で「所有済み」バッジを付けるための突合\n- テーマ適用画面（自分が切替可能なスキン一覧）\n\n### 認証\n要 Bearer JWT。`user_id` は JWT から取得し、他人の所有情報は絶対に返さない。\n\n### 挙動・制約\n- 並び順: `acquired_at DESC`（最近手に入れたものが上）\n- カタログから削除された過去のテーマは `theme = null` で返る\n- ページングなし（1 ユーザーの所有数は限定的な前提）\n\n### 関連\n- `GET /v1/themes` — 公開中テーマカタログ\n- `POST /v1/me/themes/{id}/apply` — 所有テーマを適用","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MyTheme"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/themes/{id}/apply":{"post":{"tags":["me"],"summary":"所有テーマを適用（app_users.active_theme_id などに反映される想定）","description":"### 用途\n所有しているテーマをアクティブなスキンとして切り替える。`app_users.active_theme_id` に\n選択テーマ ID を書き込み、以降のクライアント描画に反映される。\n\n### モバイルアプリでの使用タイミング\n- カスタマイズ画面でテーマカードをタップ → 適用ボタンを押したとき\n- 購入直後に「すぐ使う」導線からそのまま適用させるとき\n\n### 認証\n要 Bearer JWT。自分の `user_id` に対してのみ書き込む。\n\n### 挙動・制約\n- 所有していないテーマ ID を指定した場合も 200（`{ok:true, theme_id}`）を返す（UI をクラッシュさせない）\n- `user_themes` に行がある場合のみ `app_users.active_theme_id` を更新\n- 冪等: 既に同じテーマを適用中でも 200 を返す\n\n### 関連\n- `GET /v1/me/themes` — 所有テーマ一覧\n- `GET /v1/themes` — 公開中テーマカタログ","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"適用結果","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"theme_id":{"type":"string","format":"uuid"}},"required":["ok","theme_id"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/subscription":{"get":{"tags":["me"],"summary":"自分の現行サブスク（アクティブが無ければ null フィールドで返す）","description":"### 用途\nログイン中ユーザーの現在アクティブな購読契約と、紐付く `subscription_plans` 情報を 1 件返す。\n契約がなければ全フィールド `null` の 1 オブジェクトを返すので、呼び出し側は常に同じ形で扱える。\n\n### モバイルアプリでの使用タイミング\n- 設定 → プラン管理画面を開いたとき（現行プランの強調表示）\n- 有料機能のゲート判定（`plan.code` / `status` を参照）\n- ホーム画面表示時の機能フラグ解決\n\n### 認証\n要 Bearer JWT。`user_subscriptions.user_id = userId` かつ `status = 'active'` を 1 件だけ返す。\n\n### 挙動・制約\n- 複数の active 行がある場合は `started_at` 降順の最新 1 件を採用\n- 未契約ユーザーには `{ id:null, plan_id:null, plan:null, status:null, started_at:null, ended_at:null }` を返す（空配列ではない）\n- `plan` はサーバー側で `jsonb_build_object` により結合済み。別途プラン API を叩く必要はない\n\n### 関連\n- `GET /v1/subscription-plans` — 選択可能なプラン一覧\n- `POST /v1/me/subscription/verify-iap` — 購入検証で契約を更新","responses":{"200":{"description":"契約状態","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MySubscription"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/subscription/verify-iap":{"post":{"tags":["me"],"summary":"IAP 購入レシートを検証して user_subscriptions を更新","description":"### 用途\nApple App Store / Google Play の In-App Purchase レシートをサーバーで検証し、\n正当性が確認できたら `user_subscriptions` を最新状態にアップサートして返す。\nクライアントで購入完了 → このエンドポイントに投げて → サーバーの契約状態を真とする流れ。\n\n### プラットフォーム別フロー\n**iOS:** `receipt` = StoreKit2 の signedTransactionInfo (JWS)。\nES256 JWT で署名した Bearer を付けて App Store Server API (`/inApps/v1/transactions/{id}`) を叩き、\nsignedTransactionInfo ペイロードから productId / originalTransactionId / expiresDate 等を取得。\n**Android:** `receipt` = BillingClient が返す purchaseToken。\nサービスアカウント RS256 JWT → OAuth2 access_token を取得し、\nPlay Developer API (`subscriptionsv2/tokens/{purchaseToken}`) を叩いて lineItems から productId / expiryTime を取得。\n\n### モバイルアプリでの使用タイミング\n- プラン購入フロー（StoreKit / BillingClient）で購入成功コールバックを受けた直後\n- アプリ再起動時に未送信のレシートが残っていた場合のリトライ\n- 復元購入（Restore Purchases）ボタン押下時\n\n### 認証\n要 Bearer JWT。検証結果は JWT の `userId` に紐付けて記録される（クライアント入力の user は信用しない）。\n\n### 挙動・制約\n- `platform` ごとに App Store / Play Developer API でレシートを検証（実装は `lib/iap`）\n- `product_id` から `subscription_plans` を解決。未登録の product_id は 422 `unknown_product`\n- 該当プラットフォームのキー類が `c.env` に入っていない場合は 503（設定不足）\n- 同一 `transaction_id` での再送は冪等。`user_subscriptions` の 1 行に収束する\n- `IapError` / `ApiError` / `HTTPException` はグローバル error-handler が `{ error: { code, message, request_id } }` 形で整形\n\n### 関連\n- `GET /v1/subscription-plans` — product_id と紐付くプラン一覧\n- `GET /v1/me/subscription` — 検証後の最新契約状態\n- `POST /v1/me/subscription/refresh-iap` — 既存 receipt を再検証して expires_at を最新化","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyIapRequest"}}}},"responses":{"200":{"description":"検証成功。user_subscriptions の最新行を返す","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyIapResponse"}}}},"422":{"description":"productId に対応する subscription_plans が無い","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"該当プラットフォームの IAP 設定（キー類）が未投入","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/subscription/refresh-iap":{"post":{"tags":["me"],"summary":"既存 IAP レシートを再検証して expires_at / status を最新化","description":"### 用途\n既に `verify-iap` で登録済みのサブスクリプションを、最新のレシートで再評価する。\n自動更新後の期限延長や、一時的に `grace` / `on_hold` → `active` に復帰したケースを\nクライアントが能動的に同期するために使う。\n\n### 内部動作\n1. `verify-iap` と同じ検証フローを通す（App Store / Play Developer API を叩く）\n2. `user_subscriptions` を `external_subscription_id` で特定して `status` / `ended_at` / `updated_at` を上書き\n3. 更新後の行を VerifyIapResponse 形式で返す\n\n### 認証\n要 Bearer JWT。自分のサブスクのみ更新可。\n\n### 挙動・制約\n- 既存行が無い場合は新規 INSERT（verify-iap と同じアップサート挙動）\n- レシートが失効していても更新する（`status: expired` に上書き）\n- 503 / 422 は verify-iap と同じ条件\n\n### 関連\n- `POST /v1/me/subscription/verify-iap` — 初回購入直後の登録はこちら\n- `GET /v1/me/subscription` — 更新後の最新契約状態","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshIapRequest"}}}},"responses":{"200":{"description":"再検証成功。user_subscriptions の最新行を返す","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyIapResponse"}}}},"422":{"description":"productId に対応する subscription_plans が無い","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"該当プラットフォームの IAP 設定（キー類）が未投入","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/error-reports":{"get":{"tags":["me","error-reports"],"summary":"自分の通報履歴","description":"### 用途\nログイン中ユーザーが過去に投稿した誤情報通報の一覧を返す。\nアプリ内の「マイ通報」画面や通報後の追跡確認に使用する。\n\n### 認証\n要 Bearer JWT。`user_id` は JWT 由来で絞り込む。\n\n### 挙動・制約\n`created_at DESC` でページング。`status` で絞り込み可能。\n\n### 関連\n- `POST /v1/error-reports` — 通報を投稿\n- `GET /v1/error-reports/types` — 通報種別一覧","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"自分の通報一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MeErrorReport"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots":{"get":{"tags":["parking-lots"],"summary":"駐車場を検索（フィルタ + ソート + ページング）","description":"公開されている駐車場の一覧をフィルタ・ソートして返す。\n\n- **認証**: 不要（optionalUser）。JWT があれば文脈に載るが、結果は全ユーザーで同じ。\n- **ページング**: `page` / `limit`（デフォルト 1 ページ 20 件）。レスポンスは `{ items, page, limit, total }`。\n- **テキストフィルタ**: `q` は `name` / `address` の ILIKE 部分一致、`status` はコード値完全一致。\n- **数値・bool フィルタ**: `max_price_per_hour`（代表時間単価の上限）, `roof`（屋根あり）, `open_24h`（24 時間営業）, `tag_slugs`（タグ slug カンマ区切り）, `vehicle_type`（車格適合チェック）。\n- **ソート**: `sort=name_asc`（デフォルト） | `distance_asc`（lat/lng 必須、PostGIS） | `cheap_asc`（代表時間単価）。\n- **キャッシュ**: Cache-Control で Cloudflare エッジに 5 分（`s-maxage=300`）、ブラウザ 30 秒。","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"名称・住所の部分一致"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"コード値（active 等）"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","enum":["name_asc","distance_asc","cheap_asc"],"description":"ソート順。distance_asc は lat/lng 必須","examples":["name_asc"]},"required":false,"name":"sort","in":"query"},{"schema":{"type":"string","description":"distance_asc 時の基準緯度","examples":["35.6762"]},"required":false,"name":"lat","in":"query"},{"schema":{"type":"string","description":"distance_asc 時の基準経度","examples":["139.6503"]},"required":false,"name":"lng","in":"query"},{"schema":{"type":"string","description":"代表時間単価の上限（円）","examples":["500"]},"required":false,"name":"max_price_per_hour","in":"query"},{"schema":{"type":"string","description":"屋根あり限定（true/false）","examples":["true"]},"required":false,"name":"roof","in":"query"},{"schema":{"type":"string","description":"24時間営業限定（true/false）","examples":["true"]},"required":false,"name":"open_24h","in":"query"},{"schema":{"type":"string","description":"タグ slug カンマ区切り（AND 条件）","examples":["ev-charger,covered"]},"required":false,"name":"tag_slugs","in":"query"},{"schema":{"type":"string","enum":["sedan","kei","minivan","suv","truck"],"description":"指定車格が収まる駐車場のみ返す","examples":["sedan"]},"required":false,"name":"vehicle_type","in":"query"},{"schema":{"type":"string","enum":["filter","flag"],"default":"filter","description":"vehicle_type 指定時の挙動。filter=除外（デフォルト）, flag=全件＋fits フラグ付与","examples":["flag"]},"required":false,"name":"vehicle_fit_mode","in":"query"}],"responses":{"200":{"description":"検索結果（vehicle_fit_mode=flag 時は fits フィールドを付与、未指定時は null）","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/ParkingLot"},{"type":"object","properties":{"fits":{"type":["boolean","null"]}}}]}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}},"400":{"description":"パラメータ不正","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots/nearby":{"get":{"tags":["parking-lots"],"summary":"GPS 周辺の駐車場（PostGIS RPC）","description":"指定座標（`lat` / `lng`）を中心とした半径内の駐車場を距離昇順で返す。\n\n- **計算**: PostgreSQL の `nearby_parking_lots` RPC（PostGIS `ST_DWithin` + `ST_Distance`）。\n- **半径**: `radius_m` 省略時は 1000m。正の数値必須で、不正な値は 400。\n- **レスポンス**: `distance_m` 付きの配列。クライアントは距離表示やソートに利用できる。\n- **キャッシュ**: Edge 60 秒 / ブラウザ 30 秒。GPS は高頻度更新になるので TTL は短め。\n- **モバイル主用途**: マップ初期ロード、'近くの駐車場' ボタン、駐車開始画面の候補リスト。\n- **ランキング**: `with_ranking=true` で `ranking_score`（0-100）/ `ranking_rank`（1-based） / `is_top3` を付与。スコア = 料金の安さ 40% + 距離の近さ 40% + 星評価 20%。\n- **車格フラグ**: `vehicle_type` + `fit_mode=show_all` で全件に `fits` フラグを付与（検索 API と同仕様）。","parameters":[{"schema":{"type":"string","examples":["35.6762"]},"required":true,"name":"lat","in":"query"},{"schema":{"type":"string","examples":["139.6503"]},"required":true,"name":"lng","in":"query"},{"schema":{"type":"string","examples":["1000"]},"required":false,"name":"radius_m","in":"query"},{"schema":{"type":"string","description":"true を渡すと ranking_score / ranking_rank / is_top3 を付与","examples":["true"]},"required":false,"name":"with_ranking","in":"query"},{"schema":{"type":"string","enum":["sedan","kei","minivan","suv","truck"],"description":"車格フィルタ or fits フラグ付与","examples":["sedan"]},"required":false,"name":"vehicle_type","in":"query"},{"schema":{"type":"string","enum":["filter","flag"],"default":"filter","description":"vehicle_type 指定時の挙動。filter=除外（デフォルト）, flag=全件＋fits フラグ付与","examples":["flag"]},"required":false,"name":"vehicle_fit_mode","in":"query"}],"responses":{"200":{"description":"周辺一覧（距離昇順 / ランキング付き）","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/NearbyParkingLotWithRanking"},{"type":"object","properties":{"fits":{"type":["boolean","null"]}}}]}}}}},"400":{"description":"lat/lng/radius_m が不正","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots/{id}":{"get":{"tags":["parking-lots"],"summary":"駐車場詳細（画像・タグ・属性を含む）","description":"指定 ID の駐車場の詳細情報を返す。モバイルの詳細画面 / Web SSG の個別ページで利用する。\n\n- **include**: `images` / `tags` / `pricing_rules` / `operator` のうち必要なものをカンマ区切りで指定。不要なら省略して軽量取得。\n- **寸法・制約**: `max_height_m` / `max_width_m` / `max_length_m` / `max_weight_t` / `min_clearance_cm` / `max_parking_duration_min` を含むので、車両適合チェックはクライアント側で実施可能。\n- **operator**: Include 時は `{ id, name, slug, color }` の運営会社サマリを返す。\n- **キャッシュ**: Edge 5 分。存在しない ID は 404。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"カンマ区切りで追加取得 (images / tags / pricing_rules / operator)","examples":["images,tags,pricing_rules,operator"]},"required":false,"name":"include","in":"query"}],"responses":{"200":{"description":"駐車場詳細","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkingLotWithDetail"}}}},"404":{"description":"存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots/{id}/pricing-rules":{"get":{"tags":["parking-lots"],"summary":"駐車場の料金ルール一覧","description":"### 用途\n指定駐車場の料金ルール（時間帯 / 曜日種別 / 単価 / 上限 cap）を配列で返す。\n詳細画面の料金テーブル表示や、クライアント側で独自のシミュレーションを行うときに使う。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面の「料金」タブを開いたとき（詳細 API で include=pricing_rules を使わない軽量経路）\n- 料金シミュレータ画面で時間帯別内訳を描画するとき\n- 検索結果カードで「平日昼料金」だけを抜粋表示するとき\n\n### 認証\n不要（optionalUser）。公開情報。\n\n### 挙動・制約\n- 並び順: `category` 昇順 → `rule_order` 昇順\n- キャッシュ: ブラウザ 30 秒 / エッジ 5 分（`s-maxage=300`）\n- 存在しない駐車場 ID を指定しても空配列が返る（404 は返さない）\n- 料金試算そのものは `POST /v1/parking-lots/{id}/calc-fee` に任せる\n\n### 関連\n- `GET /v1/parking-lots/{id}` — `include=pricing_rules` で詳細と同時取得\n- `POST /v1/parking-lots/{id}/calc-fee` — 入出庫時刻を指定して合計金額を計算","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"料金ルール","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PricingRule"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots/{id}/nearby-stations":{"get":{"tags":["parking-lots"],"summary":"駐車場の近傍駅 TOP 5（距離昇順）","description":"### 用途\n指定駐車場から近い鉄道駅を距離昇順で最大 5 件返す。`parking_lot_nearby_spots`（spot_type='station'）の逆引き。\n「この駐車場は○○駅から徒歩△分」といった詳細ページの導線表示や SEO 用の構造化データ生成に利用する。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面の「最寄り駅」セクション\n- 目的地選びで「どの駅に近いか」を提示するとき\n- 共有カード / 通知のプレビュー文に最寄り駅を埋め込むとき\n\n### 認証\n不要（optionalUser）。公開情報。\n\n### 挙動・制約\n- 並び順: `distance_m` 昇順、上限 5 件\n- `walk_min` は `parking_lot_nearby_spots` に列が存在すれば返る（スキーマ移行中のため null の可能性あり）\n- キャッシュ: ブラウザ 5 分 / エッジ 10 分\n- 該当データが無い駐車場では空配列を返す（駐車場自体が存在しなくても 404 ではなく []）\n\n### 関連\n- `GET /v1/parking-lots/{id}` — 駐車場詳細（近傍駅は含まれない）\n- `GET /v1/hubs/{stationId}/parking-lots` — 駅ハブから逆に駐車場を引く","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"近傍駅一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NearbyStationItem"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots/{id}/images":{"get":{"tags":["parking-lots"],"summary":"駐車場の画像一覧","description":"### 用途\n指定駐車場に紐付く画像（メタデータのみ）を返す。実体 URL は `asset_id` を\nStorage / 画像 CDN 側で解決する前提のポインタ配列。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面の画像カルーセル\n- 検索結果カードのサムネイル（is_main=true の 1 枚だけ抜粋）\n- 画像プレビューモーダルでの全件一覧表示\n\n### 認証\n不要（optionalUser）。公開情報。\n\n### 挙動・制約\n- 並び順: `is_main` 降順 → `sort_order` 昇順（NULL は最後）\n- 返すのは `{ id, parking_lot_id, asset_id, is_main, sort_order }` のメタデータのみ。署名付き URL や変換後 URL はクライアントが別途解決\n- キャッシュ: ブラウザ 30 秒 / エッジ 5 分\n- 画像が 1 枚も無い駐車場では空配列\n\n### 関連\n- `GET /v1/parking-lots/{id}` — `include=images` で詳細と同時取得\n- `POST /v1/storage/upload-url` — 新規画像アップロード用の署名 URL 発行","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"画像一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ParkingLotImage"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots/{id}/calc-fee":{"post":{"tags":["parking-lots"],"summary":"入庫〜出庫の料金を試算（RPC calc_parking_fee）","description":"指定した駐車場で `entry_at` 〜 `exit_at` に駐車したと仮定したときの料金を試算する。\n\n- **計算ロジック**: PostgreSQL の `calc_parking_fee` RPC（時間帯ルール + 上限 cap + 車種別割増をまとめて評価）。\n- **レスポンス**: `total_amount_minor`（JPY 整数）と `breakdown`（時間帯別の内訳配列）。モバイルの料金シミュレータ画面で利用。\n- **冪等**: 副作用なし。何度叩いても同じ入力なら同じ結果。\n- **認証**: 不要。ゲストでも試算できる（駐車開始は別 API で要認証）。\n- **vehicle_type**: モバイルは車両プリセット（ユーザー設定の保有車両情報）から vehicle_type を解決して渡すこと。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeeCalcRequest"}}}},"responses":{"200":{"description":"試算結果","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeeCalcResponse"}}}},"400":{"description":"時刻指定が不正","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-lots/{id}/reviews":{"get":{"tags":["reviews"],"summary":"駐車場の承認済みレビュー一覧","description":"### 用途\n指定駐車場に対してモデレーションを通過した（`status='approved'`）レビューを\nページング形式で返す。モバイル詳細画面の口コミタブや Web の SEO ページで使う。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面の「レビュー」タブ初期表示・無限スクロール\n- 検索結果カードで星評価サマリを表示するための 1 ページ目プリフェッチ\n- レビュー投稿後に一覧を再読込して自分の投稿状態を確認するとき\n\n### 認証\n不要（optionalUser）。未ログインでも閲覧可能。\n\n### 挙動・制約\n- 並び順: `sort=rating` で星評価降順、それ以外は `created_at` 降順（デフォルト newest）\n- ページング: `page` / `limit`（共通 `PageQuerySchema`）、レスポンスは `{ items, page, limit, total }`\n- `pending` / `rejected` のレビューは返さない（公開面では見えない）\n- キャッシュ: ブラウザ 30 秒 / エッジ 60 秒（投稿直後の反映速度を優先して短め）\n\n### 関連\n- `POST /v1/parking-lots/{id}/reviews` — 同駐車場へのレビュー投稿（要認証）\n- `GET /v1/me/reviews` — 自分の投稿一覧（pending 含む）","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","enum":["newest","rating"]},"required":false,"name":"sort","in":"query"}],"responses":{"200":{"description":"レビュー一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Review"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"post":{"tags":["reviews"],"summary":"レビューを投稿（status=pending で作成）","description":"ログインユーザーとして指定駐車場に対するレビュー（星評価 + 任意コメント）を投稿する。\n\n- **認証**: 必須（Supabase JWT）。`requireUser` ミドルウェアを通過する必要がある。\n- **初期ステータス**: `pending`。管理者モデレーションで `approved` / `rejected` に遷移し、承認済みのみ公開一覧に出る。\n- **画像**: このエンドポイントでは画像は受け取らない。レビュー写真は別途 `/v1/storage/upload-url` で asset を作ってから紐付ける運用。\n- **重複投稿**: 同一ユーザーが同一駐車場にすでにレビューを持つ場合は編集にフォールバックさせる（`/v1/me/reviews/{id}` PATCH）。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"],"maxLength":2000}},"required":["rating"]}}}},"responses":{"200":{"description":"作成済みレビュー","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"レート制限","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking/{id}/detail":{"get":{"tags":["parking","mobile-bff"],"summary":"駐車場詳細（lot + reviews + nearby + pricing を 1 往復で取得）","description":"### 用途\nモバイルアプリの駐車場詳細画面が従来 4-5 本の API を個別に呼んでいたのを 1 本に集約する。\n\n### 挙動\n- lot の head（id / updated_at）を先に解決 → 座標が取れたら nearby 系を並列実行\n- 取得内容: 基本情報 / 承認済みレビュー最新 10 件 / 料金ルール / 近隣駐車場・スポンサー（500m 20 件）\n- `is_parking_lot_open_now(id)` もまとめて返す\n\n### キャッシュ\n- KV 300 秒 TTL（parking_lot_detail リソース）\n- Weak ETag（lot.updated_at ベース）で If-None-Match→304 対応\n- Cache-Control: `parking_lot_detail` プリセット（s-maxage=300, swr=600）\n- 駐車場情報・料金・レビューの write ハンドラは `parking_lot_detail` を invalidate する","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"radius_m","in":"query"}],"responses":{"200":{"description":"駐車場詳細 aggregate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkingDetailBff"}}}},"304":{"description":"Not Modified（If-None-Match 一致）"},"404":{"description":"parking_lot not found"}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-sessions":{"post":{"tags":["parking-sessions"],"summary":"駐車開始（RPC create_parking_session）","description":"ログインユーザーが指定駐車場に駐車を開始したことを記録する。\n\n- **RPC**: `create_parking_session` が `parking_sessions` に 1 行 INSERT し、`session_id` を返す。\n- **冪等**: `client_request_id`（任意 uuid）を必須で受け取り、同一キーで再送しても 1 セッションしか作らない。ネットワーク再送・二重タップ対策。\n- **GPS**: `start_lat` / `start_lng` 省略可。地図上でのピン表示用なので精度は粗くて良い。\n- **認証**: 必須。`user_id` は JWT から取得し、クライアント入力を信用しない。\n- **次の動線**: 終了時は `POST /v1/parking-sessions/{id}/finalize`、5 分以内のキャンセルは `/cancel`。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"parking_lot_id":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"vehicle_type":{"type":"string","default":"sedan"},"start_lat":{"type":"number"},"start_lng":{"type":"number"},"client_request_id":{"type":"string","description":"冪等キー（再送時に重複作成を防ぐ）"}},"required":["parking_lot_id","client_request_id"]}}}},"responses":{"200":{"description":"作成結果（セッション詳細）","content":{"application/json":{"schema":{}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-sessions/{id}/finalize":{"post":{"tags":["parking-sessions"],"summary":"駐車終了 + 料金確定（RPC finalize_parking_session）","description":"駐車セッションを終了し、料金を確定する。\n\n- **RPC**: `finalize_parking_session` が `end_at` を確定、`fee_amount` を `calc_parking_fee` と同じロジックで計算して保存する。\n- **冪等**: `client_request_id` で重複終了を防止。すでに終了済みなら現在の料金をそのまま返す。\n- **レスポンス**: `{ total_amount_minor, breakdown }`（calc-fee と同じ形）。モバイルはこれをレシート画面に表示する。\n- **キャンセルとの違い**: finalize は料金発生を確定させる。5 分以内の誤操作取り消しは `/cancel` を使う。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"exit_at":{"type":"string","format":"date-time"},"client_request_id":{"type":"string"}},"required":["client_request_id"]}}}},"responses":{"200":{"description":"確定結果","content":{"application/json":{"schema":{}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/parking-sessions/{id}/cancel":{"post":{"tags":["parking-sessions"],"summary":"駐車キャンセル（RPC cancel_parking_session）","description":"駐車開始直後の誤操作を取り消す（5 分以内）。料金は発生しない。\n\n- **RPC**: `cancel_parking_session` がセッションを `cancelled` に遷移し、`end_at` を `start_at` 近傍に寄せる。\n- **制限**: 開始から 5 分を超えた場合は finalize しか受け付けない（RPC 側で弾く）。\n- **冪等**: すでに cancelled なら no-op で 200 を返す。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"キャンセル結果","content":{"application/json":{"schema":{}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/tags":{"get":{"tags":["tags"],"summary":"タグ一覧（マスター）","description":"### 用途\n駐車場に付与されるタグ（屋根付き / EV 対応 / 24 時間 など）のマスターを一覧で返す。\nクライアントは検索画面のフィルタチップや詳細画面のバッジ描画、\n未登録タグを「不明」として扱うための全量リファレンスに使う。\n\n### モバイルアプリでの使用タイミング\n- アプリ起動直後 / 画面復帰時にマスターをプリロードしてキャッシュ\n- 検索画面のタグフィルタシートを開いたとき\n- 駐車場詳細で「タグ無しで不明扱いのもの」を全マスターとの差分で補完するとき\n- オフラインキャッシュ更新（アイコン・色・slug の再同期）\n\n### 認証\n不要。公開マスターデータ。\n\n### 挙動・制約\n- 並び順: `sort_order` 昇順（NULL は末尾）→ `name` 昇順\n- 返却フィールド: `{ id, name, color, sort_order, slug, category, icon_name }`\n  - `slug` / `category` は migration 028 で追加、`icon_name` は 041 で追加（Lucide の kebab-case 名）\n- 論理削除カラムは現状なく、全件を返す\n- キャッシュ: ブラウザ 60 秒 / エッジ 10 分（マスター更新頻度が低いので長め）\n\n### 関連\n- `GET /v1/parking-lots/{id}` — `include=tags` で駐車場ごとの付与状態を取得\n- `GET /v1/search/lots` — 全駐車場 + タグ付与状態のダンプ（SSG 用）","responses":{"200":{"description":"タグ一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Tag"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/articles":{"get":{"tags":["articles"],"summary":"公開記事の一覧（published のみ）","description":"### 用途\nTOKYO CAR LIFE / TOKYO CAR STORY などの公開記事一覧を新しい順で返す。\n`status='published'` + `publish_to_web=true` + `published_at IS NOT NULL` のみ対象。\n\n### モバイルアプリでの使用タイミング\n- ホーム画面の「おすすめ記事」カルーセル表示\n- 記事一覧タブ（カテゴリ / タグ絞り込み含む）のページング取得\n- 「もっと見る」スクロールでの追加読込\n\n### 認証\n不要。匿名でも取得できる公開エンドポイント。\n\n### 挙動・制約\n- 並び順: `published_at DESC`\n- ページング: `PageQuerySchema`（`page` / `limit`）\n- 絞り込み: `category`（完全一致）/ `tag`（配列 contains）\n- キャッシュ: `Cache-Control: public, max-age=60, s-maxage=300`\n\n### 関連\n- `GET /v1/articles/{slug}` — 記事本体取得\n- `GET /v1/articles/by-author/{slug}` — 著者別の一覧","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"category","in":"query"},{"schema":{"type":"string"},"required":false,"name":"tag","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ArticleListItem"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/articles/by-author/{slug}":{"get":{"tags":["articles"],"summary":"author_slug 指定の公開記事一覧","description":"### 用途\n特定の執筆者（`author_slug`）が書いた公開記事の一覧を返す。著者ページの記事リスト構築用。\n\n### モバイルアプリでの使用タイミング\n- 記事詳細の「この著者の他の記事」セクション\n- 著者プロフィール画面の記事一覧\n\n### 認証\n不要。匿名可の公開エンドポイント。\n\n### 挙動・制約\n- 並び順: `published_at DESC`（ページングなし、全件返却）\n- 対象: `status='published'` + `publish_to_web=true` の記事のみ\n- キャッシュ: `Cache-Control: public, max-age=60, s-maxage=300`\n- ルート登録順: `/{slug}` の前に定義（catch-all に呑まれないように）\n\n### 関連\n- `GET /v1/articles` — 全公開記事一覧\n- `GET /v1/articles/{slug}` — 記事本体取得","parameters":[{"schema":{"type":"string"},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"記事一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleListItem"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/articles/{slug}":{"get":{"tags":["articles"],"summary":"記事本体を slug から取得","description":"### 用途\nslug をキーに公開記事 1 件を本文付きで返す。`body` を含むため、記事詳細描画の本命 API。\n\n### モバイルアプリでの使用タイミング\n- 記事一覧から詳細画面への遷移時\n- プッシュ通知 / ディープリンクから直接記事を開いたとき\n- Web 側（Astro）の SSG / ISR による事前フェッチ\n\n### 認証\n不要。匿名可の公開エンドポイント。\n\n### 挙動・制約\n- 対象: `status='published'` + `publish_to_web=true` + `published_at IS NOT NULL`\n- 条件を満たさない slug は 404 `not_found`\n- キャッシュ: `Cache-Control: public, max-age=60, s-maxage=300`\n\n### 関連\n- `GET /v1/articles` — 記事一覧\n- `GET /v1/articles/by-author/{slug}` — 著者別一覧","parameters":[{"schema":{"type":"string"},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"記事","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Article"}}}},"404":{"description":"存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/ads":{"get":{"tags":["ads"],"summary":"配信対象の広告一覧（期間・ステータスで絞り込み済み）","description":"### 用途\n今日の日付で配信中（`status='active'` かつ期間内）の広告クリエイティブを返す。\nバナー画像・リンク先・alt テキストを含むのでアプリ側はそのまま表示できる。\n\n### モバイルアプリでの使用タイミング\n- ホーム画面のバナー枠（placement 指定なしで全枠取得）\n- 記事詳細の本文中 PR 枠（`placement=article_body` で絞る）\n- 駐車場詳細画面の下部広告枠\n\n### 認証\n不要。匿名でも取得できる公開エンドポイント。\n\n### 挙動・制約\n- 期間判定: `start_date IS NULL OR start_date <= today` かつ `end_date IS NULL OR end_date >= today`\n- `placement` クエリで特定枠のみ取得可能（省略時は全枠）\n- キャッシュ: `Cache-Control: public, max-age=30, s-maxage=60`（広告更新が反映されやすいよう短め）\n\n### 関連\n- `/v1/sponsors` — エリアスポンサー（広告枠ではなく実店舗）","parameters":[{"schema":{"type":"string","description":"article_body など特定枠を絞る"},"required":false,"name":"placement","in":"query"}],"responses":{"200":{"description":"広告一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Ad"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/newsletter-track/open/{broadcast_id}/{hash}":{"get":{"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"broadcast_id","in":"path"},{"schema":{"type":"string","minLength":8,"maxLength":64},"required":true,"name":"hash","in":"path"}],"responses":{"200":{"description":"1x1 GIF"}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/newsletter-track/unsubscribe":{"get":{"parameters":[{"schema":{"type":"string","minLength":10},"required":true,"name":"token","in":"query"},{"schema":{"type":"string","format":"email"},"required":true,"name":"email","in":"query"}],"responses":{"200":{"description":"HTML confirmation page"}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/support/tickets":{"post":{"tags":["support"],"summary":"サポートチケットを作成","description":"### 用途\nユーザーからの問い合わせ（バグ報告・機能要望・決済トラブル等）を受け付け、\n`support_tickets` テーブルに `status='new'` で登録する。運営は管理ポータルから処理する。\n\n### モバイルアプリでの使用タイミング\n- 設定 → お問い合わせフォームの「送信」タップ\n- エラーダイアログからの「サポートに報告する」導線\n\n### 認証\n要 Bearer JWT。`user_id` は JWT 由来で自動セット。\n`user_email` / `user_name` はフォーム入力値を保持（連絡先として別途使うため）。\n\n### 挙動・制約\n- `subject` 1〜200 / `body` 1〜10000 文字\n- `priority` は `low|medium|high|urgent`（default `medium`）\n- `category` はコードマスター準拠、未指定時 `other`\n- 作成時 `status` は常に `new`（管理者が後続で更新）\n\n### 関連\n- `GET /v1/support/tickets` — 自分のチケット一覧","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string","minLength":1,"maxLength":10000},"category":{"type":"string","default":"other"},"priority":{"type":"string","enum":["low","medium","high","urgent"],"default":"medium"},"user_email":{"type":"string","format":"email"},"user_name":{"type":"string","minLength":1,"maxLength":100}},"required":["subject","body","user_email","user_name"]}}}},"responses":{"200":{"description":"作成済みチケット","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportTicket"}}}},"429":{"description":"レート制限","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"get":{"tags":["support"],"summary":"自分のサポートチケット一覧","description":"### 用途\nログイン中ユーザーが過去に送信したサポートチケット一覧を、作成日時降順 + ページングで返す。\n回答ステータスの確認に使う。\n\n### モバイルアプリでの使用タイミング\n- 設定 → お問い合わせ履歴画面\n- チケット詳細から一覧への戻り\n\n### 認証\n要 Bearer JWT。`WHERE user_id = ${userId}` で自分のチケットのみ返す。\n\n### 挙動・制約\n- `status` クエリで絞り込み可能（`new` / `in_progress` / `resolved` 等、コードマスター準拠）\n- ページングは `PageQuerySchema`（`page` / `limit`）。`total` は COUNT を返す\n- `created_at DESC` の最新順\n\n### 関連\n- `POST /v1/support/tickets` — 新規チケット作成","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"チケット一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SupportTicket"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/error-reports/types":{"get":{"tags":["error-reports"],"summary":"通報種別一覧（公開）","description":"### 用途\nモバイルアプリが通報フォームを開く前にコードマスタから通報種別の選択肢を取得する。\n認証不要・公開エンドポイント。\n\n### 挙動・制約\n`codes(category_id='report_type', is_deleted=false)` を `sort_order ASC` で返す。\n追加種別は管理者がコードマスタへ挿入することで自動的に反映される。\n\n### 関連\n- `POST /v1/error-reports` — 通報を投稿","responses":{"200":{"description":"通報種別一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"sort_order":{"type":"integer"}},"required":["code","label","sort_order"]}}},"required":["items"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/error-reports":{"post":{"tags":["error-reports"],"summary":"駐車場情報の誤り報告を投稿","description":"### 用途\n駐車場情報の誤り（料金・営業時間・場所・閉鎖・設備等）をユーザーが報告する。\n管理者が内容を検証し、マスターデータ修正に反映する。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面の「情報が違う」ボタンから報告フォームを開いたとき\n- 駐車完了画面の「現地情報を報告」導線\n\n### 認証\n要 Bearer JWT。`user_id` は JWT 由来で保存。未ログインからの送信は受け付けない。\n\n### 挙動・制約\n- `report_type` は `GET /v1/error-reports/types` で取得したコード値のみ許可\n- `severity`: `low` / `medium` / `high`（未指定時 `medium`）\n- `evidence_urls`: 証拠画像の R2 キー配列（`POST /v1/storage/upload-url` で発行）\n- `description` 1〜4000 文字、`parking_lot_name` 1〜200 文字\n- `parking_lot_id` は null 許容（未登録駐車場の報告対応）\n- `photo_asset_id` で添付画像（後方互換のため維持）\n- 作成時 `status='new'`、成功時 `error_reported` アクティビティを記録（EXP 対象）\n\n### 関連\n- `GET /v1/error-reports/types` — 通報種別一覧\n- `GET /v1/me/error-reports` — 自分の通報履歴\n- `POST /v1/storage/upload-url` — 添付写真のアップロード URL 発行","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"parking_lot_id":{"type":["string","null"],"format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"parking_lot_name":{"type":"string","minLength":1,"maxLength":200},"report_type":{"type":"string","minLength":1},"description":{"type":"string","minLength":1,"maxLength":4000},"user_email":{"type":"string","format":"email"},"user_name":{"type":"string","minLength":1,"maxLength":100},"severity":{"type":"string","enum":["low","medium","high"],"default":"medium"},"evidence_urls":{"type":"array","items":{"type":"string"},"maxItems":10},"photo_asset_id":{"type":["string","null"],"format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]}},"required":["parking_lot_name","report_type","description","user_email","user_name"]}}}},"responses":{"200":{"description":"作成済み報告","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorReport"}}}},"400":{"description":"無効な report_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"レート制限","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/review-reports/reasons":{"get":{"tags":["review-reports"],"summary":"レビュー通報理由一覧（公開）","description":"### 用途\nモバイルアプリが「このレビューを通報」フォームを表示する際に、\n通報理由のドロップダウン項目をコードマスタから取得する。\n認証不要・公開エンドポイント。\n\n### 挙動・制約\n`codes(category_id='review_report_reason', is_deleted=false)` を `sort_order ASC` で返す。\nカテゴリの追加・削除は管理者が `codes` を編集することで自動反映される。\n\n### 関連\n- `POST /v1/reviews/{reviewId}/flag` — 実際に通報を投稿","responses":{"200":{"description":"通報理由一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"sort_order":{"type":"integer"}},"required":["code","label","sort_order"]}}},"required":["items"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/reviews/{reviewId}/flag":{"post":{"tags":["reviews","review-reports"],"summary":"レビューを通報（不適切報告）","description":"### 用途\nユーザーが他人のレビューを不適切（スパム・誹謗中傷・トピック違い 等）として運営に通報する。\n通報は `public.review_reports` に 1 行記録され、同時に `admin_notifications` に\n運営向け通知が 1 件 INSERT される（Realtime 購読で管理ポータルに即時表示）。\n\n### モバイルアプリでの使用タイミング\n- 駐車場詳細画面のレビュー項目にある「…」メニューから「通報」選択時\n- レビュー一覧のロングプレスコンテキストメニュー\n\n### 認証\n要 Bearer JWT。JWT の sub から app_users 行を解決し `reporter_user_id` として保存する。\n\n### 挙動・制約\n- `reason` は `GET /v1/review-reports/reasons` のコード値のみ許可\n  (`spam` / `inappropriate` / `harassment` / `off_topic` / `other`)\n- `description` 任意、最大 2000 文字\n- 同じユーザーが同じレビューを 2 回通報すると 409 `already_reported`\n- 通報対象レビューが存在しない場合は 404 `review_not_found`\n- 作成時 `status='pending'` 固定。管理者がポータルで状態遷移させる\n\n### 関連\n- `GET /v1/review-reports/reasons` — 通報理由一覧\n- `POST /v1/me/reviews` — レビューの新規投稿","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"reviewId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","enum":["spam","inappropriate","harassment","off_topic","other"]},"description":{"type":"string","maxLength":2000}},"required":["reason"]}}}},"responses":{"201":{"description":"通報を受け付けました（status=pending）","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewFlagResponse"}}}},"404":{"description":"対象レビューが存在しない","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"既に同じレビューを通報済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"reason が無効","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"レート制限","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/sponsors":{"get":{"tags":["sponsors"],"summary":"公開中スポンサー一覧（地図ピン・Web ハブ向け）","description":"### 用途\nソフト削除されていないエリアスポンサー（提携店舗）の全一覧を返す。\n名前・カテゴリ・ロゴ・座標・半径などスポンサーピン描画に必要な情報を含む。\n\n### モバイルアプリでの使用タイミング\n- 地図画面を開いた直後にスポンサーピンを一括描画するとき\n- Web ハブ（spnsors 紹介ページ）の静的ビルドで全スポンサーをリスト化するとき\n- 近傍判定を端末側でやる場合のベースデータ取得\n\n### 認証\n任意（`optionalUser`）。未ログインでも叩ける公開エンドポイント。\n\n### 挙動・制約\n- 並び順: `name ASC`\n- `deleted_at IS NOT NULL` は除外\n- ページングなし（件数が少ない前提）\n- キャッシュ: `Cache-Control: public, max-age=60, s-maxage=300`\n\n### 関連\n- `GET /v1/sponsors/nearby` — 現在地近傍のスポンサーだけ距離昇順で取得\n- `POST /v1/sponsors/{id}/checkin` — 来店チェックイン（EXP/バッジ）","responses":{"200":{"description":"スポンサー一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AreaSponsor"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/sponsors/nearby":{"get":{"tags":["sponsors"],"summary":"指定座標の半径内にあるスポンサー","description":"指定座標の半径内で公開中のスポンサー施設を距離昇順で返す。駐車中のユーザーに「近くでお得な店」を提案するのに使う。\n\n- **RPC**: `nearby_sponsors(lng, lat, radius_m)`（PostGIS）を Hyperdrive 越しに呼ぶ。\n- **半径**: 省略時 1500m。`radius_m` 正値必須。\n- **耐障害性**: RPC 未登録などで失敗しても 200 + 空配列で返す（SSG ビルドや地図 UI を壊さない方針）。\n- **Cron**: 同じ RPC を 10 分毎の `handleSponsorProximity` でも利用。駐車中ユーザーに近接通知を送る。","parameters":[{"schema":{"type":"string","examples":["139.6503"]},"required":true,"name":"lng","in":"query"},{"schema":{"type":"string","examples":["35.6762"]},"required":true,"name":"lat","in":"query"},{"schema":{"type":"string","examples":["1500"]},"required":false,"name":"radius_m","in":"query"}],"responses":{"200":{"description":"近傍スポンサー一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AreaSponsor"}}}}},"400":{"description":"lng/lat 不正","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/sponsors/{id}/notification":{"post":{"tags":["sponsors"],"summary":"近接通知の発火をサーバーに記録 (Phase 4 ローカル geofence 用)","description":"モバイル側で OS geofence から ローカル通知が発火したときに呼び出す計測エンドポイント。\n\n- **認証**: 必須 (requireUser)\n- **副作用**: `sponsor_notification_logs` に行を追加 + `area_places.total_notifications_sent` を +1\n- **冪等性なし**: 呼出毎にログ行が増える。モバイル側で重複送信しない責任\n- **耐障害性**: スポンサーが削除済みでも 204 で受理 (クライアントを壊さない)","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"記録済み"},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/sponsors/{id}/checkin":{"post":{"tags":["sponsors"],"summary":"スポンサー施設にチェックイン（EXP 付与は DB トリガー経由を想定）","description":"ログインユーザーがスポンサー施設に来店したことを記録する。ゲーミフィケーション（EXP / バッジ）のトリガー。\n\n- **認証**: 必須。`sponsor_checkins` に行を追加し、DB トリガーが EXP / バッジ進捗を自動更新する。\n- **重複防止**: 同一ユーザー × 同一スポンサーのチェックインは日単位で制限（DB 制約で重複弾く）。\n- **GPS 検証**: body に現在位置が含まれる場合、サーバ側でスポンサー座標との距離を検証し、遠距離からの不正チェックインを防ぐ。","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"memo":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"チェックイン結果","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"checked_in_at":{"type":"string"}},"required":["id","checked_in_at"]}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/themes":{"get":{"tags":["themes"],"summary":"公開中テーマ一覧","description":"### 用途\nアプリ UI をカスタマイズするテーマ（スキン）のカタログを返す。\n無料 / 有料、価格、プレビュー画像 ID を含み、ショップ画面で並べるのに必要な情報を一覧で渡す。\n\n### モバイルアプリでの使用タイミング\n- ショップ画面の「テーマ一覧」タブ\n- プロフィール → カスタマイズ → 「テーマを変える」から購入導線を開いたとき\n- オンボーディング時の「好きなスキンを選ぼう」導線（無料テーマ限定で提示）\n\n### 認証\n不要。匿名でも取得できる公開エンドポイント。\n\n### 挙動・制約\n- `is_active = true` のみ対象（非公開テーマは返さない）\n- 並び順: `sort_order ASC`\n- キャッシュ: `Cache-Control: public, max-age=60, s-maxage=600`（カタログはあまり変わらないため長め）\n\n### 関連\n- `GET /v1/me/themes` — 自分が所有しているテーマ\n- `POST /v1/me/themes/{id}/apply` — 所有テーマを適用","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/ThemeListItem"},{"type":"object"}]}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/subscription-plans":{"get":{"tags":["subscriptions"],"summary":"購読プラン一覧（公開）","description":"### 用途\nアクティブな購読プラン（`is_active = true`）を `sort_order` 昇順で返す。\n料金・通貨・特徴 JSON・アクセントカラー等、プラン比較画面を描画するのに必要な\n静的情報がすべて入っている公開 API。\n\n### モバイルアプリでの使用タイミング\n- 設定 → プラン管理画面で「プラン一覧」を表示するとき\n- オンボーディングの「無料 / プレミアム比較」スライド\n- 課金訴求モーダルを開くとき\n\n### 認証\n不要。公開エンドポイント（未ログインでも叩ける）。\n\n### 挙動・制約\n- `is_active = false`（販売停止）のプランは返さない\n- `sort_order` 昇順で並ぶ（UI の左→右、無料→有料の順を DB で管理）\n- `Cache-Control: public, max-age=120, s-maxage=600` を付与しエッジでキャッシュされる\n\n### 関連\n- `GET /v1/me/subscription` — 自分の現行契約\n- `POST /v1/me/subscription/verify-iap` — IAP レシート検証","responses":{"200":{"description":"プラン一覧","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/SubscriptionPlan"},{"type":"object"}]}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/storage/upload-url":{"post":{"tags":["storage"],"summary":"Cloudflare R2 の presigned PUT URL を発行","description":"画像・PDF 等のバイナリアップロード用に、Cloudflare R2 に PUT できる presigned URL を返す。\n\n- **2 ステップ**: (1) このエンドポイントで URL + asset_id を取得 → (2) クライアントが presigned URL に直接 `PUT` する。バイト列は Workers を経由しない（エグレス削減）。\n- **DB 副作用**: Workers は `assets` テーブルに行を先に INSERT する（`uploaded_by` / `s3_key` / `mime_type` / `file_name` 等）。PUT 失敗時は孤立する可能性があるので、クライアント側は PUT 後に親エンティティに `asset_id` を紐付ける。\n- **s3_key 生成規則**: `{category}/{yyyymm}/{uuid}_{sanitized-name}`。衝突しないので再試行も安全。\n- **公開 URL**: `R2_PUBLIC_BASE=https://cdn.parky.co.jp` が設定されていれば匿名 GET 可能な URL が `public_url` で返る。\n- **認証**: 必須（`requireUser`）。`uploaded_by` に JWT 由来の `user_id` を入れ、後から監査可能にする。\n- **Content-Type 注意**: presign で `content_type` を指定した場合、クライアントの PUT ヘッダも完全一致させないと 403 になる。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"file_name":{"type":"string","minLength":1,"maxLength":300},"file_size":{"type":"integer","exclusiveMinimum":0},"mime_type":{"type":"string","minLength":1},"category":{"type":"string","default":"other"},"entity_type":{"type":["string","null"]},"entity_id":{"type":["string","null"],"format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"is_public":{"type":"boolean","default":true}},"required":["file_name","file_size","mime_type"]}}}},"responses":{"200":{"description":"presigned URL + メタ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlResponse"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/storage/assets/{id}/finalize":{"post":{"tags":["storage"],"summary":"アップロード完了をコミット（メタ調整が必要な場合に）","description":"### 用途\nR2 への PUT 完了後、`assets` 行に紐づく `entity_type` / `entity_id` / `is_public` を更新する。\nupload-url 発行時に親エンティティ ID が未確定だった場合（例: 下書きレビュー）や、\n公開/非公開の切替が必要になった場合のみ呼ぶ。\n\n### モバイルアプリでの使用タイミング\n- レビュー投稿フォームで写真アップロード → レビュー確定時に `entity_id` を付与\n- プロフィール画像差し替え完了時の `is_public` 制御\n- upload-url 時点でエンティティ未確定な添付ファイル全般\n\n### 認証\n要 Bearer JWT。`WHERE uploaded_by = ${userId}` で自分がアップした asset のみ更新可。\n\n### 挙動・制約\n- `undefined` のフィールドは触らない。送ったフィールドのみ SET（`null` 明示は反映される）\n- すべてのフィールドが空ボディの場合は現在の `assets` 行を返す（no-op）\n- 対象 asset が存在しない / 他人のもの → 404 `not_found`\n- R2 上のオブジェクト本体はこのエンドポイントでは触らない（メタのみ）\n\n### 関連\n- `POST /v1/storage/upload-url` — presigned PUT URL の発行","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"entity_type":{"type":["string","null"]},"entity_id":{"type":["string","null"],"format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"is_public":{"type":"boolean"}}}}}},"responses":{"200":{"description":"確定後の asset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Asset"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/search/lots":{"get":{"tags":["search-lots"],"summary":"active 駐車場を料金/属性/タグ付きで一括取得（SSG ビルド向け・差分同期対応）","description":"### 用途\n`status='active'` な全駐車場を 1 リクエストで丸ごと返す、公開 SEO / SSG ビルド用のダンプ API。\n各駐車場に `pricing_rules` と `tags`（3 値 state 付き）、運営会社名、寸法・入庫制約までフラットに詰めて\nクライアント側だけで検索・フィルタ UI を成立させることを目的とする。\n\n### オフライン差分同期（`since` パラメータ）\n`since` を指定すると `parking_lots.updated_at >= since` の行だけ返す（差分同期モード）。\n初回は `since` 省略で全量取得し、返却された `cursor`（= `server_time`）を次回の `since` に使う。\n関連テーブル（`pricing_rules` / `tags`）も `since` 以降に更新された駐車場を含む行を返す。\n（注: pricing_rules / tags 単体の updated_at は現時点では参照せず、親 parking_lot の updated_at のみでフィルタ）\n\n### モバイルアプリでの使用タイミング\n- 原則モバイルからは叩かない（ペイロードが重いので検索は `/v1/parking-lots` 系を使う）\n- Astro SSG（`web/home`）のビルド時 1 回 + 管理画面の全量エクスポートがメインユーザー\n- バックグラウンド差分同期（`since` 付き）でオフライン用キャッシュを更新するユースケース\n\n### 認証\n不要（optionalUser）。公開データのみ返す。\n\n### 挙動・制約\n- 並び順: `name` 昇順\n- `limit` は 1〜30000、省略時 5000。超過値はバリデーションで 400\n- `since` 指定時はキャッシュを付与しない（差分なので毎回フレッシュに取得する必要がある）\n- `operator_name` は `codes(category_id='operator', lang='ja')` の表示名を JOIN\n- `tags` は 3 値（state=true / false / 行なし=不明、20260421 で boolean 化）で返り、`icon_name` 等を含む\n- `area` は line / area 形状のみ GeoJSON を詰め、point 形状は null\n- キャッシュ（`since` 未指定時）: ブラウザ 60 秒 / エッジ 5 分（SSG ビルドの短期連打を吸収）\n\n### 関連\n- `GET /v1/parking-lots` — 普段の検索・ページング用\n- `GET /v1/parking-lots/nearby` — GPS 周辺検索\n- `GET /v1/hubs/{stationId}/parking-lots` — 駅ハブ近傍ダンプ","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"最大件数（デフォルト 5000）","examples":["5000"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","format":"date-time","description":"差分同期用タイムスタンプ（ISO 8601 UTC）。指定時は updated_at >= since の行のみ返す。前回レスポンスの cursor 値をそのまま使う。","examples":["2026-04-21T00:00:00.000Z"]},"required":false,"name":"since","in":"query"}],"responses":{"200":{"description":"駐車場ダンプ（server_time / cursor 付き）","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SearchLot"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0},"server_time":{"type":"string","description":"サーバー側のクエリ実行時刻（ISO 8601 UTC）。次回 since に使う。","examples":["2026-04-21T06:00:00.000Z"]},"cursor":{"type":["string","null"],"description":"次回リクエストで since に渡す値。現状は server_time と同値。全件取得後は null（ページング非対応のため常に全件返す）。","examples":["2026-04-21T06:00:00.000Z"]}},"required":["items","page","limit","total","server_time","cursor"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/search/ai":{"post":{"tags":["search"],"summary":"自然言語クエリを駐車場検索条件にパース","description":"ユーザーの自然言語入力（例: '渋谷駅から徒歩 5 分以内で 1 時間 500 円以下の屋根付き駐車場'）を構造化検索フィルタに変換する。\n\n- **プロバイダ**: Anthropic Claude (primary) → Google Gemini → OpenAI GPT のフォールバック順。全て Cloudflare AI Gateway (`parky-ai-gateway`) 経由でキャッシュ・ログ・リトライを得る。\n- **契約**: 各プロバイダに同一 JSON Schema を与え、tool_use / function_calling で構造化レスポンスを強制する。\n- **API キー**: Supabase Vault に暗号化保存。Workers が `vault_read_secret` RPC で復号して利用（レスポンスには漏れない）。\n- **レート制限**: `RATE_LIMIT_USER` binding で user_id 単位 10 req / 60 秒。超過時 429。\n- **usage ログ**: Analytics Engine (`parky_ai_usage`) + PG `ai_usage_logs` に dual write。\n- **status**: `parsed`（query に構造化結果）/ `need_info`（聞き返し文 reply）/ `error`（プロバイダ全滅）をクライアントが分岐で処理する。","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiSearchRequest"}}}},"responses":{"200":{"description":"パース結果 or 聞き返し or エラー（クライアントは status で判別）","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiSearchResponse"}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"レート制限","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"利用可能な AI プロバイダーなし","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/hubs/publishable":{"get":{"tags":["hubs"],"summary":"公開可能なハブ（station, 在庫 >= min）一覧","description":"### 用途\n駅ハブ（spot_type='station'）のうち、近傍駐車場が `min` 件以上あるものを返す。\nAstro SSG のビルド時に「どの駅ページを静的生成するか」の判定・一覧にそのまま使える。\n\n### モバイルアプリでの使用タイミング\n- 駅ハブブラウズ画面（「エリア・駅から探す」導線）の一覧表示\n- 検索候補の駅オートコンプリート用のプレロード\n- ホーム画面「人気の駅から探す」カルーセル\n\n### 認証\n不要（optionalUser）。公開データのみ。\n\n### 挙動・制約\n- 並び順: `total_count` 降順（在庫が多い = 需要がある駅が上位）\n- `min` 省略時は 5、1〜100 の範囲。dev 環境のシード薄さを補うため 1 まで許可\n- `in_stock_count` / `out_of_stock_count` は現行スキーマに存在しないため 0 固定\n- `city` / `prefecture` は `cities` / `prefectures` 結合で `{ id, name, slug }` を詰める\n- キャッシュ: ブラウザ 5 分 / エッジ 10 分\n\n### 関連\n- `GET /v1/hubs/by-slug/{prefSlug}/{citySlug}/{spotSlug}` — 個別駅ハブをピンポイント取得\n- `GET /v1/hubs/{stationId}/parking-lots` — 駅ハブ近傍の駐車場一覧","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"最低駐車場件数 (default 5)","examples":["1"]},"required":false,"name":"min","in":"query"}],"responses":{"200":{"description":"ハブ一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/HubPublishableItem"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/hubs/by-slug/{prefSlug}/{citySlug}/{spotSlug}":{"get":{"tags":["hubs"],"summary":"slug 指定で単一駅ハブを取得","description":"### 用途\nURL slug 3 階層（都道府県 / 市区町村 / 駅）で 1 件の駅ハブを特定する。\n`/publishable` 一覧を取って配列から find する SSR アンチパターンを避けるため、\nDB 1 クエリで解決できるピンポイント API として切り出している。\n\n### モバイルアプリでの使用タイミング\n- ディープリンク（例: `parky://hub/tokyo/shibuya-ku/shibuya`）から直接開いたとき\n- シェア URL を踏んで駅ハブ画面を復元するとき\n- Web からの共有導線（Universal Link）でアプリに遷移するとき\n\n### 認証\n不要（optionalUser）。公開データのみ。\n\n### 挙動・制約\n- slug は `prefectures.slug` / `cities.slug` / `stations.slug` の完全一致（大文字小文字区別）\n- 見つからない場合は `404 not_found`\n- `in_stock_count` / `out_of_stock_count` は 0 固定（スキーマ未整備）\n- キャッシュ: ブラウザ 60 秒 / エッジ 5 分 / stale-while-revalidate 10 分\n\n### 関連\n- `GET /v1/hubs/publishable` — 公開可能な駅ハブの一覧\n- `GET /v1/hubs/{stationId}/parking-lots` — 取得した `station.id` で近傍駐車場を取る次の一手","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"prefSlug","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"citySlug","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"spotSlug","in":"path"}],"responses":{"200":{"description":"ハブ 1 件","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HubPublishableItem"}}}},"404":{"description":"該当なし","content":{"application/json":{"schema":{"type":"object","properties":{"error":{}}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/hubs/{stationId}/parking-lots":{"get":{"tags":["hubs"],"summary":"駅ハブ近傍の駐車場（距離昇順）","description":"### 用途\n指定駅（`station_id`）の近傍に登録されている駐車場を、歩行距離で近い順にまとめて返す。\n各駐車場に `pricing_rules` と `tags`、寸法制約、運営会社名までフラットに同梱するので、\n駅ハブページ側はこれ 1 本でカード列 + フィルタまで描画できる。\n\n### モバイルアプリでの使用タイミング\n- 駅ハブ詳細画面の駐車場リスト（初期表示・ピッカー）\n- 地図画面で駅ピンをタップしたときの周辺駐車場フェッチ\n- 「この駅の駐車場」カード化した検索結果プレビュー\n\n### 認証\n不要（optionalUser）。公開データのみ。\n\n### 挙動・制約\n- 並び順: `parking_lot_nearby_spots.distance_m` 昇順\n- `walk_min` は `walking_minutes` 列から取得（null の可能性あり）\n- `operator_name` は `codes(category_id='operator', lang='ja')` の display_label を JOIN\n- `tags` は `parking_lot_tags` 経由でネストオブジェクトで返す（attributes は 028 migration で統合済み）\n- 該当なしの駅 ID でも空配列を返す（404 は返さない）\n- キャッシュ: ブラウザ 5 分 / エッジ 10 分\n\n### 関連\n- `GET /v1/hubs/publishable` — 公開可能な駅ハブ一覧\n- `GET /v1/hubs/by-slug/{prefSlug}/{citySlug}/{spotSlug}` — slug 経由で `station.id` を解決\n- `GET /v1/parking-lots/{id}/nearby-stations` — 逆方向（駐車場から最寄り駅）","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"stationId","in":"path"}],"responses":{"200":{"description":"駐車場一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/HubParkingLotItem"}}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/meta/activity-types":{"get":{"tags":["meta"],"summary":"全 activity_type の定義と metadata JSON Schema を返す","description":"BFF が `award_user_activity` に emit する全 activity_type のカタログ。\n\n- 各エントリに JSON Schema 7 形式の `metadata_schema` を含む。\n- `emitted=false` は seed には定義されているが BFF 側 emit は未配線の予約済み種別。\n- SSoT: `api/src/lib/activity-types.ts`。新しい種別を足したら当エンドポイントの出力にも自動反映される。","responses":{"200":{"description":"カタログ全件","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityTypesResponse"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/meta/deep-links":{"get":{"tags":["meta"],"summary":"ディープリンク URL → 画面ルーティングテーブルを返す","description":"モバイルアプリが起動時に取得する URL → 画面マッピング一覧。\nクライアントはこれをローカルにキャッシュし、Universal Links / App Links で\n受け取った URL を `pattern` とマッチさせて対応 `screen` に遷移する。\n\n- `params` はパターン中の `:param` 部分をリスト化したもの（パース結果の受け取りキー）。\n- `requires_auth` が `false` のルートは未ログイン状態でも開ける（省略時は true 扱い）。\n- 認証不要・公開エンドポイント（起動直後に叩くため）。\n- Cache-Control: public, max-age=300, s-maxage=3600（頻繁に変わらないため長めにキャッシュ）。","responses":{"200":{"description":"ディープリンク一覧","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeepLinksResponse"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/meta/app":{"get":{"tags":["meta"],"summary":"アプリ設定情報を取得（強制アップデート・メンテナンスモード）","description":"モバイルアプリ起動時に必ず叩くエンドポイント。\n- `is_maintenance: true` のとき、アプリはメンテナンス画面を表示する\n- `min_app_version_*` より現在のバージョンが古ければ強制アップデート画面を表示する\n- 設定が未登録の場合はデフォルト値を返す（エラーにしない）","responses":{"200":{"description":"アプリ設定","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppConfig"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/data-export":{"get":{"tags":["me"],"summary":"自分の全個人データを JSON でエクスポート（GDPR 対応）","description":"ユーザー自身の全個人データを JSON 形式でエクスポートする。\nGDPR 第20条（データポータビリティ権）への対応として実装。\n- レート制限: 10リクエスト/60秒（重いクエリのため）\n- ファイル名: Content-Disposition で parky-data-<id>-<date>.json として提供\n- プッシュ通知トークンは機密情報のため含めない（登録デバイス数のみ）\n\n### エクスポート対象\n- プロフィール（app_users 最新1件）\n- 駐車セッション（parking_sessions 最新500件）\n- レビュー（parking_reviews 全件）\n- 個人評価（parking_lot_ratings 全件。テーブル未定義時は空配列）\n- お気に入り駐車場（user_saved_parking_lots 全件）\n- 登録車両（user_vehicles 全件）\n- 検索プリセット（user_search_presets 全件）\n- 登録デバイス数のみ（user_notification_tokens の件数）","responses":{"200":{"description":"個人データエクスポート JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataExport"}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"レート制限","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/referrals/my-code":{"get":{"tags":["me","referrals"],"summary":"自分の紹介コードを取得（初回は自動生成）","description":"### 用途\n自分の招待コード（8 桁英数大文字）を返す。未生成であれば DB 上で生成してから返す（冪等）。\n\n### コード生成仕様\n- 読み間違いが起きやすい `0/O/1/I` を除いた 32 文字セットから 8 桁\n- 衝突した場合は最大 10 回リトライ（`generate_user_referral_code` RPC に委譲）\n\n### 認証\n要 Bearer JWT。","responses":{"200":{"description":"紹介コード情報","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyReferralCode"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/referrals/apply":{"post":{"tags":["me","referrals"],"summary":"招待コードを適用する（自分が招待を受けたとして登録）","description":"### 用途\n他ユーザーの紹介コードを入力し、紹介関係を登録する。\n1 ユーザーは 1 回のみ適用可能（`referee_user_id` に UNIQUE 制約）。\n\n### エラーコード（ok=false 時）\n| error | 意味 |\n|---|---|\n| `already_referred` | 既に紹介を受けている |\n| `code_not_found` | コードが存在しない or 無効化済み |\n| `self_referral` | 自分のコードを使用しようとした |\n| `code_exhausted` | コードの利用上限に達した |\n\n### 認証\n要 Bearer JWT。`apply_user_referral` RPC に委譲。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":16}},"required":["code"]}}}},"responses":{"200":{"description":"適用成功","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyReferralResponse"}}}},"400":{"description":"コードが無効 / 既に適用済み / 自己紹介 / 上限到達","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyReferralResponse"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/referrals/history":{"get":{"tags":["me","referrals"],"summary":"自分が紹介した人の履歴一覧（匿名化）","description":"### 用途\n自分の紹介コードを使って登録したユーザーの一覧を返す。\nプライバシー保護のため `referee_user_id` は返さず、`referee_label`（\"ユーザー 1\" 形式）で匿名化する。\n\n### 認証\n要 Bearer JWT。","responses":{"200":{"description":"紹介履歴","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferralHistory"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/consents/types":{"get":{"tags":["me-consents"],"summary":"同意タイプ一覧を取得（認証不要）","description":"### 用途\n`consent_types` テーブルの全レコードを返す。\nクライアントは起動時に 1 回叩き、`required: true` のタイプで未同意があれば同意モーダルを表示する。\n\n### 認証\n不要（匿名でも取得可能）。","responses":{"200":{"description":"同意タイプ一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ConsentType"}}},"required":["items"]}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/consents":{"get":{"tags":["me-consents"],"summary":"自分の同意状況を取得","description":"### 用途\nログイン中ユーザーの同意履歴を返す。\nタイプごとに **最新バージョン分のみ** を返すため、\nクライアントは現在の同意状態確認に使える。\n\n### 認証\n要 Bearer JWT。\n\n### 関連\n- `GET /v1/me/consents/status` — 必須同意の未承諾リストのみ確認したい場合","responses":{"200":{"description":"同意一覧（タイプごと最新バージョン）","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ConsentItem"}}},"required":["items"]}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]},"post":{"tags":["me-consents"],"summary":"同意を一括登録・更新","description":"### 用途\nクライアントが表示した同意画面の結果を一括で記録する。\n同一ユーザー × 同一タイプ × 同一バージョンの行は UPDATE で上書き（UPSERT）。\n\n### `version` の扱い\nクライアントが同意取得時に表示したバージョンを必ず送る。\nサーバー側の `current_version` ではなくクライアント提示値を証跡として保持するため。\n\n### `source`\n`mobile_ios` / `mobile_android` / `web` 等を任意で付与。\n\n### 認証\n要 Bearer JWT。他ユーザーの同意は登録不可。","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentPostBody"}}}},"responses":{"200":{"description":"登録成功","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"count":{"type":"number"}},"required":["ok","count"]}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"バリデーションエラー","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/me/consents/status":{"get":{"tags":["me-consents"],"summary":"必須同意の未承諾タイプ一覧を取得","description":"### 用途\n`required: true` のタイプのうち、最新バージョンを **承諾していない**（未記録または拒否）ものを返す。\nクライアントはアプリ起動時にこれを叩き、`pending_required` が空になるまで同意モーダルを表示する。\n\n### `pending_required` の条件\n- `consent_types.required = true` かつ\n- `user_consents` に `granted = true` かつ `version = current_version` の行が存在しない\n\n### 認証\n要 Bearer JWT。","responses":{"200":{"description":"未承諾の必須同意タイプ一覧","content":{"application/json":{"schema":{"type":"object","properties":{"pending_required":{"type":"array","items":{"type":"string"},"description":"未承諾の必須同意タイプ一覧","examples":[["terms_of_service"]]}},"required":["pending_required"]}}}},"401":{"description":"未認証","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/client-events":{"post":{"tags":["client-events"],"summary":"クライアント側イベント（クラッシュ・エラー・UX 計測）を送信","description":"### 用途\nモバイルアプリ (iOS / Android) と Web クライアントが検知したクライアント側の\n- クラッシュ (`event_type=crash`, `severity=fatal`)\n- ハンドルされた例外 (`event_type=error`)\n- パフォーマンス計測 (`event_type=performance`)\n- UX イベント / ライフサイクル (`event_type=ux|lifecycle`)\nを BFF 経由で `client_events` テーブルに蓄積する。管理者ポータルの監視ダッシュボードや\nQA 時の再現調査に利用する。\n\n### 認証\nBearer JWT は任意（`optionalUser`）。未ログインでもクラッシュを送信できる\n（起動直後に落ちた場合などを取りこぼさないため）。JWT があれば `user_id` を紐付ける。\n\n### レート制限\n`RATE_LIMIT_USER` binding があり、かつログイン済みの場合はユーザー単位でソフトリミット。\n超過時は 429 を返さず 204 No Content でサイレントに drop する\n（クラッシュ報告経路をブロックしないことを優先）。\n\n### 挙動・制約\n- `event_type` と `severity` は CHECK 制約で厳格化\n- `metadata` は任意の JSON（オプション）\n- 成功時は 201 で id / created_at を返す","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"event_type":{"type":"string","enum":["crash","error","performance","ux","lifecycle"]},"severity":{"type":"string","enum":["fatal","error","warning","info"],"default":"info"},"message":{"type":"string","maxLength":2000},"stack_trace":{"type":"string","maxLength":10000},"metadata":{"type":"object","additionalProperties":{}},"app_version":{"type":"string","maxLength":20},"platform":{"type":"string","enum":["ios","android","web"]},"os_version":{"type":"string","maxLength":50},"device_model":{"type":"string","maxLength":100},"parking_session_id":{"type":"string","format":"uuid"}},"required":["event_type"]}}}},"responses":{"201":{"description":"受信して格納済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientEventCreated"}}}},"204":{"description":"レート超過によりサイレント drop"},"400":{"description":"バリデーションエラー","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}},"/v1/shares/{token}":{"get":{"tags":["shares"],"summary":"共有トークンで駐車位置情報を取得（公開）","description":"### 用途\n`POST /v1/me/parking-sessions/{id}/shares` で発行した共有 URL のビューア向けエンドポイント。\n認証不要。有効期限内・未 revoke のトークンに対して駐車セッションの概要情報を返す。\n\n### 挙動\n- 有効期限切れ（`expires_at < NOW()`）または `revoked_at IS NOT NULL` なら 404\n- 閲覧ごとに `access_count` を +1 インクリメント\n- 返却フィールド: 駐車場名・lat/lng・開始時刻・ステータス\n- 個人情報（user_id 等）は含まない","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"token","in":"path"}],"responses":{"200":{"description":"駐車位置情報","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharePublic"}}}},"404":{"description":"トークンが存在しない・期限切れ・revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-channels":["app-web","app-mobile"],"x-badges":[{"name":"app-web","color":"#3b82f6"},{"name":"app-mobile","color":"#f97316"}]}}},"webhooks":{}}